Public: anyone can read this; the crew can change it.

MCP claim_link answers the one line of what the recipient will see first, beside the link #11748

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/11494-mcp-claim-link-answers-the-one-line-of-w → main 2 commits fork

  • Merged. scout/11494-mcp-claim-link-answers-the-one-line-of-w was merged into main as bcc1369.

Review

Sign in to review this change.
2 files changed +83 −1
test/mcp-claim-first-screen.test.ts added +78
@@ -0,0 +1,78 @@
1 /** `claim_link` says, beside the link, what the recipient sees first: the one line the clients page reads. */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createRepo } from "../src/actions/repos.ts";
7 import { createApp, type App } from "../src/app.ts";
8 import { firstScreenLine } from "../src/core/next-steps.ts";
9 import { createToken } from "../src/core/tokens.ts";
10 import { createUser } from "../src/core/users.ts";
11 import { commitFiles } from "../src/git/write.ts";
12 import { repoDir } from "../src/git/repos.ts";
13 import { serve, type Servers } from "../src/http/server.ts";
14 import { quietLogs } from "./helpers/globals.ts";
15
16 quietLogs();
17
18 let dir = "";
19 let app: App;
20 let servers: Servers;
21 let token = "";
22 let id = 1;
23
24 const post = (path: string, body: unknown) =>
25 fetch(`${servers.url}${path}`, {
26 method: "POST",
27 headers: { authorization: `Bearer ${token}`, "content-type": "application/json", accept: "application/json, text/event-stream" },
28 body: JSON.stringify(body),
29 });
30 const claimLink = async (name: string) => {
31 const res = await post("/mcp", {
32 jsonrpc: "2.0", id: id++, method: "tools/call",
33 params: { name: "claim_link", arguments: { name, reveal: true } },
34 });
35 const body = (await res.json()) as any;
36 return JSON.parse(body.result.content[0].text) as { firstScreen?: string; claimUrl?: string };
37 };
38
39 beforeAll(async () => {
40 dir = await mkdtemp(join(tmpdir(), "forge-claim-first-"));
41 app = createApp({ dataDir: dir, url: "https://forge.test", port: 0 });
42 const alice = await createUser(app.db, { origin: "seed", trusted: true, username: "alice", email: "[email protected]", password: "a-long-claim-passphrase", isAdmin: true });
43 if (!alice.ok) throw new Error("user");
44 token = createToken(app.db, alice.value.id, "rw", "rw").token;
45 for (const n of ["bakery", "cafe"]) {
46 const repo = await createRepo(app, alice.value, { name: n });
47 if (!repo.ok) throw new Error("repo");
48 const c = await commitFiles(repoDir(app.reposRoot, "alice", n), {
49 branch: "main", message: n, files: [{ path: "README.md", content: `# ${n}\n` }],
50 author: { name: "a", email: "[email protected]" },
51 });
52 if (!c.ok) throw new Error("commit");
53 }
54 servers = await serve(app);
55 for (const [name, apps] of [["duo", ["alice/bakery", "alice/cafe"]], ["bare", []]] as const) {
56 const made = await post("/api/v1/forges", { name, apps, dryRun: false });
57 if (made.status !== 201) throw new Error(`spawn ${name} ${made.status} ${await made.text()}`);
58 }
59 }, 60_000);
60
61 afterAll(async () => {
62 servers.stop();
63 app.close();
64 await rm(dir, { recursive: true, force: true });
65 });
66
67 test("two apps: the line names both, and is the clients page's own sentence", async () => {
68 const got = await claimLink("duo");
69 expect(got.claimUrl).toContain("#");
70 expect(got.firstScreen).toBe(firstScreenLine(["alice/bakery", "alice/cafe"]));
71 expect(got.firstScreen).toContain("alice/bakery, alice/cafe");
72 });
73
74 test("no apps: the line says the apps they pick", async () => {
75 const got = await claimLink("bare");
76 expect(got.firstScreen).toBe(firstScreenLine([]));
77 expect(got.firstScreen).toContain("the apps you pick");
78 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs