Public: anyone can read this; the crew can change it.
/.well-known/mcp.json names the commit it runs, so an agent on a preview knows which build it is talking to #7051
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6070-well-known-mcp-json-names-the-commit-it → main 1 commit fork
3 files
changed +25
−3
docs/MCP.md +9
| @@ -243,6 +243,15 @@ claude mcp add --transport http forge https://<your-forge>/mcp \ | ||
| 243 | 243 | |
| 244 | 244 | Any MCP client that can send a header works the same way. |
| 245 | 245 | |
| 246 | ## The server card | |
| 247 | ||
| 248 | `/.well-known/mcp.json` names the server (`name`, `version`, `endpoint`, | |
| 249 | `transport`, `protectedResourceMetadata`, `documentation`, `tokenFreeTools`) | |
| 250 | and the build that answers: `commit`, the full commit the forge runs | |
| 251 | (`FORGE_COMMIT`, or `FORGE_APP_COMMIT` on a preview), or `null` when nothing | |
| 252 | says. `version` is `dev` on a preview, so `commit` is how an agent tells two | |
| 253 | previews apart. | |
| 254 | ||
| 246 | 255 | ## Attaching with OAuth |
| 247 | 256 | |
| 248 | 257 | Clients that cannot paste a header — the Claude apps, Grok's connectors — |
src/core/mcp_card.ts +4 −3
| @@ -6,7 +6,7 @@ | ||
| 6 | 6 | * The MCP server-card format is still a draft, so the card may not match a |
| 7 | 7 | * later standard's field names; when one settles, the card follows it. |
| 8 | 8 | */ |
| 9 | import { running } from "../kernel/version.ts"; | |
| 9 | import { running, type Running } from "../kernel/version.ts"; | |
| 10 | 10 | |
| 11 | 11 | /** The endpoint and the protected-resource metadata that names how to sign in. */ |
| 12 | 12 | export function mcpPointers(origin: string): { url: string; oauth: string } { |
| @@ -21,11 +21,12 @@ export function mcpPointers(origin: string): { url: string; oauth: string } { | ||
| 21 | 21 | * true is named: who answers, where, over what transport, how to sign in, |
| 22 | 22 | * and the one tool that needs no token. |
| 23 | 23 | */ |
| 24 | export function mcpCard(origin: string) { | |
| 24 | export function mcpCard(origin: string, now: Running = running()) { | |
| 25 | 25 | const { url, oauth } = mcpPointers(origin); |
| 26 | 26 | return { |
| 27 | 27 | name: "forge", |
| 28 | version: running().version, | |
| 28 | version: now.version, | |
| 29 | commit: now.commit, | |
| 29 | 30 | endpoint: url, |
| 30 | 31 | transport: "streamable-http", |
| 31 | 32 | protectedResourceMetadata: oauth, |
test/mcp-card.test.ts +12
| @@ -5,6 +5,7 @@ import { tmpdir } from "node:os"; | ||
| 5 | 5 | import { join } from "node:path"; |
| 6 | 6 | import { createApp, type App } from "../src/app.ts"; |
| 7 | 7 | import { serve, type Servers } from "../src/http/server.ts"; |
| 8 | import { mcpCard } from "../src/core/mcp_card.ts"; | |
| 8 | 9 | import { running } from "../src/kernel/version.ts"; |
| 9 | 10 | |
| 10 | 11 | describe("the MCP server card", () => { |
| @@ -59,6 +60,17 @@ describe("the MCP server card", () => { | ||
| 59 | 60 | expect((await fetch(card.documentation)).status).toBe(200); |
| 60 | 61 | }); |
| 61 | 62 | |
| 63 | test("carries the commit it runs, or null when nothing says", () => { | |
| 64 | const sha = "a1b2c3d4e5f60718293a4b5c6d7e8f9012345678"; | |
| 65 | expect(mcpCard("https://x", running({ FORGE_APP_COMMIT: sha })).commit).toBe(sha); | |
| 66 | expect(mcpCard("https://x", running({})).commit).toBeNull(); | |
| 67 | }); | |
| 68 | ||
| 69 | test("the live card's commit is what running() says", async () => { | |
| 70 | const card = (await (await fetch(`${base}/.well-known/mcp.json`)).json()) as any; | |
| 71 | expect(card.commit).toBe(running().commit); | |
| 72 | }); | |
| 73 | ||
| 62 | 74 | test("the `mcp` block of /api/v1/here gives the same url and oauth", async () => { |
| 63 | 75 | const card = (await (await fetch(`${base}/.well-known/mcp.json`)).json()) as any; |
| 64 | 76 | const here = (await (await fetch(`${base}/api/v1/here?path=/`)).json()) as any; |
new above
offline