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

/.well-known/mcp.json and /llms.txt name the ui:// cards a harness can draw, read from CARD_OF_TOOL so they cannot drift #11415

Merged opened this

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

Merged

scout/forge:scout/11412-well-known-mcp-json-and-llms-txt-name-th → main 3 commits fork 1 thread · 1 unresolved

  • Merged. scout/11412-well-known-mcp-json-and-llms-txt-name-th was merged into main as 951be3d.

Review

Sign in to review this change.
3 files changed +50 −1
src/core/mcp_card.ts +22
@@ -11,6 +11,7 @@ import { claudeInstall, opencodeInstall, pluginSource } from "./plugin_install.t
1111 import { running, type Running } from "../kernel/version.ts";
1212 import { forgeSource } from "../settings/forge_source.ts";
1313 import { repoVisibility } from "./repos.ts";
14 import { CARD_OF_TOOL, CARD_URI } from "../web/studio/cards.ts";
1415
1516 /** The endpoint and the protected-resource metadata that names how to sign in. */
1617 export function mcpPointers(origin: string): { url: string; oauth: string } {
@@ -114,6 +115,26 @@ export function offeredPlugins(repo: string | undefined): readonly HarnessPlugin
114115 return HARNESS_PLUGINS.filter((p) => !p.cloned || repo !== undefined);
115116 }
116117
118 /**
119 * The MCP Apps cards (SEP-1865) a harness can draw, one `{ resource, tools }`
120 * per `ui://` resource, read from `CARD_OF_TOOL` so the card and /llms.txt
121 * cannot drift from what `tools/list` advertises.
122 */
123 export function cardList(): { resource: string; tools: string[] }[] {
124 const byResource = new Map<string, string[]>();
125 for (const [tool, kind] of Object.entries(CARD_OF_TOOL)) {
126 const uri = CARD_URI(kind);
127 byResource.set(uri, [...(byResource.get(uri) ?? []), tool]);
128 }
129 return [...byResource].map(([resource, tools]) => ({ resource, tools }));
130 }
131
132 /** The one /llms.txt line that says tool results carry cards, and how many. */
133 export function cardsLine(): string {
134 const n = cardList().length;
135 return `Tool results carry MCP Apps cards: a tool listed with _meta.ui.resourceUri points at a ui:// resource a harness can draw (${n} cards; the list is "cards" in the server card).`;
136 }
137
117138 /**
118139 * The server card. The format is still a draft, so only what we can keep
119140 * true is named: who answers, where, over what transport, how to sign in,
@@ -131,6 +152,7 @@ export function mcpCard(origin: string, repo: string | undefined, now: Running =
131152 documentation: `${origin}${DISCOVERY.docs}`,
132153 openapi: `${origin}${DISCOVERY.openapi}`,
133154 tokenFreeTools: [...TOKEN_FREE_TOOLS],
155 cards: cardList(),
134156 plugins: offeredPlugins(repo).map((p) => ({
135157 harness: p.harness,
136158 path: p.path,
src/web/standards.ts +3 −1
@@ -44,7 +44,7 @@ import { notFound } from "../http/respond.ts";
4444 import { SHARE_TARGET } from "./share_target.ts";
4545 import { SITE_LINE } from "./site_line.ts";
4646 import { mcpToolsSection } from "./llms_tools.ts";
47 import { apiCatalog, DISCOVERY, mcpCard, offeredPlugins, mcpPointers, offeredPluginRepo, pluginSource } from "../core/mcp_card.ts";
47 import { apiCatalog, cardsLine, DISCOVERY, mcpCard, offeredPlugins, mcpPointers, offeredPluginRepo, pluginSource } from "../core/mcp_card.ts";
4848 import { forgeDocument } from "../core/standards_index.ts";
4949 import { TAGS } from "../contract/served.ts";
5050 import { running } from "../kernel/version.ts";
@@ -483,6 +483,8 @@ function renderLlmsTxt(ctx: Ctx, o: string, tell: string | undefined, running: s
483483 `- OpenAPI: GET ${o}${DISCOVERY.openapi}, or one slice with ?tag= (${TAGS.join(", ")})`,
484484 "",
485485 ...mcpToolsSection(o, signedIn),
486 cardsLine(),

cardsLine() is called here but no test reads /llms.txt, so removing this line passes the suite. Add a test that fetches /llms.txt and expects the cards line.

487 "",
486488 "## Public repositories",
487489 "",
488490 // Names and addresses only: a description is whatever its owner
test/mcp-card.test.ts +25
@@ -25,6 +25,13 @@ describe("the MCP server card", () => {
2525 await rm(dataDir, { recursive: true, force: true });
2626 });
2727
28 test("/llms.txt carries the cards line, counted from the card the server serves", async () => {
29 const card = (await (await fetch(`${base}/.well-known/mcp.json`)).json()) as any;
30 const text = await (await fetch(`${base}/llms.txt`)).text();
31 expect(text).toContain("_meta.ui.resourceUri");
32 expect(text).toContain(`(${card.cards.length} cards;`);
33 });
34
2835 test("names the server, endpoint, sign-in and the tokenless tool, each true", async () => {
2936 const res = await fetch(`${base}/.well-known/mcp.json`);
3037 expect(res.status).toBe(200);
@@ -88,3 +95,21 @@ describe("the MCP server card", () => {
8895 expect(txt).toContain(`${base}/.well-known/mcp.json`);
8996 });
9097 });
98
99 describe("the cards the server card names", () => {
100 test("every tool in CARD_OF_TOOL is listed under its card's resource", async () => {
101 const { CARD_OF_TOOL, CARD_URI } = await import("../src/web/studio/cards.ts");
102 const card = mcpCard("https://x", undefined, running({}));
103 for (const [tool, kind] of Object.entries(CARD_OF_TOOL)) {
104 const entry = card.cards.find((c) => c.resource === CARD_URI(kind));
105 expect(entry?.tools).toContain(tool);
106 }
107 expect(card.cards.flatMap((c) => c.tools).length).toBe(Object.keys(CARD_OF_TOOL).length);
108 });
109
110 test("/llms.txt says results carry cards and how many", async () => {
111 const { cardsLine, cardList } = await import("../src/core/mcp_card.ts");
112 expect(cardsLine()).toContain("_meta.ui.resourceUri");
113 expect(cardsLine()).toContain(`${cardList().length} cards`);
114 });
115 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs