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

/.well-known/mcp.json and llms.txt offer the harness plugins only when their source repository is readable without a token #8455

Merged opened this

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

Merged

mason/forge:mason/8431-well-known-mcp-json-and-llms-txt-offer-t → main 1 commit fork

  • Merged. mason/8431-well-known-mcp-json-and-llms-txt-offer-t was merged into main as de250a8.

Review

Sign in to review this change.
5 files changed +63 −20
src/core/mcp_card.ts +11 −3
@@ -6,6 +6,8 @@
66 * The MCP server-card format is still a draft, so the card may not match a
77 * later standard's field names; when one settles, the card follows it.
88 */
9 import type { Database } from "bun:sqlite";
10 import { getRepo } from "./repos.ts";
911 import { running, type Running } from "../kernel/version.ts";
1012
1113 /** The endpoint and the protected-resource metadata that names how to sign in. */
@@ -34,9 +36,15 @@ export interface HarnessPlugin {
3436 readonly setup: string;
3537 }
3638
37 /** The repository that holds the plugins; it must be public on the forge for the lines to work. */
39 /** The repository that holds the plugins; the lines are offered only while it is public on this forge. */
3840 const PLUGIN_REPO = "trevato/forge";
3941
42 /** Whether the plugin source can be read without a token here, so every printed install line works. */
43 export function pluginsOffered(db: Database): boolean {
44 const [owner = "", name = ""] = PLUGIN_REPO.split("/");
45 return getRepo(db, owner, name)?.visibility === "public";
46 }
47
4048 /** The clone URL of that repository on this forge's origin. */
4149 export function pluginSource(origin: string): string {
4250 return `${origin}/${PLUGIN_REPO}.git`;
@@ -67,7 +75,7 @@ export const HARNESS_PLUGINS: readonly HarnessPlugin[] = [
6775 * true is named: who answers, where, over what transport, how to sign in,
6876 * and the one tool that needs no token.
6977 */
70 export function mcpCard(origin: string, now: Running = running()) {
78 export function mcpCard(origin: string, offered: boolean, now: Running = running()) {
7179 const { url, oauth } = mcpPointers(origin);
7280 return {
7381 name: "forge",
@@ -79,7 +87,7 @@ export function mcpCard(origin: string, now: Running = running()) {
7987 documentation: `${origin}/llms.txt`,
8088 openapi: `${origin}/contract/openapi.json`,
8189 tokenFreeTools: [...TOKEN_FREE_TOOLS],
82 plugins: HARNESS_PLUGINS.map((p) => ({
90 plugins: (offered ? HARNESS_PLUGINS : []).map((p) => ({
8391 harness: p.harness,
8492 path: p.path,
8593 source: pluginSource(origin),
src/web/standards.ts +14 −12
@@ -39,7 +39,7 @@ import type { Ctx } from "../http/context.ts";
3939 import { notFound } from "../http/respond.ts";
4040 import { SITE_LINE } from "./site_line.ts";
4141 import { mcpToolsSection } from "./llms_tools.ts";
42 import { HARNESS_PLUGINS, mcpCard, mcpPointers, pluginSource } from "../core/mcp_card.ts";
42 import { HARNESS_PLUGINS, mcpCard, mcpPointers, pluginSource, pluginsOffered } from "../core/mcp_card.ts";
4343 import { running } from "../kernel/version.ts";
4444 import { timePhase, timePhaseAsync } from "../core/route_timing.ts";
4545 import { shippedReleases, type Shipped } from "../release/shipped.ts";
@@ -106,7 +106,7 @@ export function securityTxt(ctx: Ctx): Response {
106106
107107 /** GET /.well-known/mcp.json — the MCP server card (a draft format; see docs/MCP.md). */
108108 export function mcpJson(ctx: Ctx): Response {
109 const res = text(JSON.stringify(mcpCard(ctx.url.origin), null, 2), "application/json");
109 const res = text(JSON.stringify(mcpCard(ctx.url.origin, pluginsOffered(ctx.app.db)), null, 2), "application/json");
110110 res.headers.set("access-control-allow-origin", "*");
111111 return res;
112112 }
@@ -343,8 +343,8 @@ function runningSentence(): string {
343343 : `Running ${r.version} at ${r.commit.slice(0, 7)}`;
344344 }
345345
346 /** llms.txt and its ETag per origin, valid while the listing read, the build and the tell command are the same. */
347 const llmsBodies = new Map<string, { signedIn: boolean; repos: LlmsListing; running: string; tell: string | undefined; body: string; etag: string }>();
346 /** llms.txt and its ETag per origin, valid while the listing read, the build, the tell command and the plugin source being public are the same. */
347 const llmsBodies = new Map<string, { signedIn: boolean; repos: LlmsListing; running: string; tell: string | undefined; plugins: boolean; body: string; etag: string }>();
348348
349349 /**
350350 * GET /llms.txt — the forge as an agent should meet it (llmstxt.org): what
@@ -356,15 +356,16 @@ export function llmsTxt(ctx: Ctx): Response {
356356 const o = ctx.url.origin;
357357 const tell = sshTellCommand(ctx.app);
358358 const running = runningSentence();
359 const plugins = pluginsOffered(ctx.app.db);
359360 const repos = timePhase(ctx.request, "repos", () => llmsRepos(ctx.app.db));
360361 // The tools section depends on who asks, so the signed-in and the anonymous body are kept apart.
361362 const signedIn = ctx.auth.user !== undefined;
362363 const key = `${o} ${signedIn}`;
363364 let kept = llmsBodies.get(key);
364 if (kept === undefined || kept.repos !== repos || kept.running !== running || kept.tell !== tell) {
365 const body = timePhase(ctx.request, "render", () => renderLlmsTxt(ctx, o, tell, running, repos, signedIn));
365 if (kept === undefined || kept.repos !== repos || kept.running !== running || kept.tell !== tell || kept.plugins !== plugins) {
366 const body = timePhase(ctx.request, "render", () => renderLlmsTxt(ctx, o, tell, running, repos, signedIn, plugins));
366367 const etag = timePhase(ctx.request, "etag", () => `"${new Bun.CryptoHasher("sha256").update(body).digest("hex").slice(0, 32)}"`);
367 kept = { signedIn, repos, running, tell, body, etag };
368 kept = { signedIn, repos, running, tell, plugins, body, etag };
368369 // Origins come from the Host header: keep a handful, not one per guess.
369370 if (llmsBodies.size >= 16) llmsBodies.clear();
370371 llmsBodies.set(key, kept);
@@ -380,7 +381,7 @@ export function llmsTxt(ctx: Ctx): Response {
380381 }
381382
382383 /** The llms.txt document for one origin. */
383 function renderLlmsTxt(ctx: Ctx, o: string, tell: string | undefined, running: string, repos: LlmsListing, signedIn: boolean): string {
384 function renderLlmsTxt(ctx: Ctx, o: string, tell: string | undefined, running: string, repos: LlmsListing, signedIn: boolean, plugins: boolean): string {
384385 const mcp = mcpPointers(o);
385386 return (
386387 [
@@ -404,10 +405,11 @@ function renderLlmsTxt(ctx: Ctx, o: string, tell: string | undefined, running: s
404405 "## Use it from your harness",
405406 "",
406407 `- MCP endpoint: ${mcp.url} (streamable HTTP; OAuth metadata at ${mcp.oauth}). Or send a token as Authorization: Bearer.`,
407 ...HARNESS_PLUGINS.map(
408 (p) =>
409 `- ${p.label} plugin (source ${pluginSource(o)}, path ${p.path}; ${p.setup}; needs that repository to be public): \`${p.install(o)}\``,
410 ),
408 ...(plugins
409 ? HARNESS_PLUGINS.map(
410 (p) => `- ${p.label} plugin (source ${pluginSource(o)}, path ${p.path}; ${p.setup}): \`${p.install(o)}\``,
411 )
412 : ["- Harness plugins: not offered on this forge (their source is private)"]),
411413 "",
412414 "## What you can do on any page",
413415 "",
test/mcp-card-plugins.test.ts +33 −1
@@ -5,23 +5,47 @@ import { mkdtemp, rm } from "node:fs/promises";
55 import { tmpdir } from "node:os";
66 import { join } from "node:path";
77 import { createApp, type App } from "../src/app.ts";
8 import { createRepo, updateRepoSettings } from "../src/actions/repos.ts";
9 import { getRepo } from "../src/core/repos.ts";
10 import { createUser, type User } from "../src/core/users.ts";
811 import { serve, type Servers } from "../src/http/server.ts";
912
1013 let dataDir: string;
1114 let app: App;
1215 let servers: Servers;
16 let owner: User;
1317
1418 beforeAll(async () => {
1519 dataDir = await mkdtemp(join(tmpdir(), "forge-card-plugins-"));
1620 app = createApp({ dataDir, port: 0 });
1721 servers = await serve(app);
18 });
22 const made0 = await createUser(app.db, { origin: "seed", trusted: true, username: "trevato", email: "[email protected]", password: "a-good-passphrase", adminIfFirst: true });
23 if (!made0.ok) throw new Error(made0.error);
24 owner = made0.value;
25 const made = await createRepo(app, owner, { name: "forge", visibility: "private" });
26 if (!made.ok) throw new Error(String(made.error));
27 }, 60_000);
1928 afterAll(async () => {
2029 await servers.stop();
2130 await rm(dataDir, { recursive: true, force: true });
2231 });
2332
33 const setVisibility = async (visibility: "public" | "private") => {
34 const r = await updateRepoSettings(app, owner, getRepo(app.db, "trevato", "forge")!, { visibility });
35 if (!r.ok) throw new Error(String(r.error));
36 };
37
38 test("a private plugin source offers no plugins in the card or llms.txt", async () => {
39 const card = (await fetch(`${servers.url}/.well-known/mcp.json`).then((r) => r.json())) as any;
40 const llms = await fetch(`${servers.url}/llms.txt`).then((r) => r.text());
41 expect(card.plugins).toEqual([]);
42 expect(llms).toContain("Harness plugins: not offered on this forge (their source is private)");
43 expect(llms).not.toContain("Claude Code plugin");
44 expect(llms).not.toContain("needs that repository to be public");
45 });
46
2447 test("card and llms.txt name both plugins, their install lines, and paths that exist", async () => {
48 await setVisibility("public");
2549 const card = (await fetch(`${servers.url}/.well-known/mcp.json`).then((r) => r.json())) as any;
2650 const llms = await fetch(`${servers.url}/llms.txt`).then((r) => r.text());
2751 expect(card.plugins.map((p: any) => p.harness).sort()).toEqual(["claude-code", "opencode"]);
@@ -43,3 +67,11 @@ test("card and llms.txt name both plugins, their install lines, and paths that e
4367 for (const p of card.plugins) expect(existsSync(join(import.meta.dir, "..", p.path))).toBe(true);
4468 expect(card.name).toBe("forge");
4569 });
70
71 test("llms.txt follows the source going private again, not its cached body", async () => {
72 await setVisibility("public");
73 expect(await fetch(`${servers.url}/llms.txt`).then((r) => r.text())).toContain("OpenCode plugin");
74 await setVisibility("private");
75 expect(await fetch(`${servers.url}/llms.txt`).then((r) => r.text())).not.toContain("OpenCode plugin");
76 expect(((await fetch(`${servers.url}/.well-known/mcp.json`).then((r) => r.json())) as any).plugins).toEqual([]);
77 });
test/mcp-card.test.ts +2 −2
@@ -68,8 +68,8 @@ describe("the MCP server card", () => {
6868
6969 test("carries the commit it runs, or null when nothing says", () => {
7070 const sha = "a1b2c3d4e5f60718293a4b5c6d7e8f9012345678";
71 expect(mcpCard("https://x", running({ FORGE_APP_COMMIT: sha })).commit).toBe(sha);
72 expect(mcpCard("https://x", running({})).commit).toBeNull();
71 expect(mcpCard("https://x", true, running({ FORGE_APP_COMMIT: sha })).commit).toBe(sha);
72 expect(mcpCard("https://x", true, running({})).commit).toBeNull();
7373 });
7474
7575 test("the live card's commit is what running() says", async () => {
test/standards.test.ts +3 −2
@@ -230,8 +230,9 @@ describe("what the open web expects", () => {
230230 expect(section).toContain(`${base}/mcp`);
231231 expect(section).toContain(`${base}/.well-known/oauth-protected-resource`);
232232 expect(section).toContain("streamable HTTP");
233 expect(section).toContain("Claude Code plugin");
234 expect(section).toContain("OpenCode plugin");
233 // trevato/forge is not here, so no install line is printed that could not work.
234 expect(section).toContain("Harness plugins: not offered on this forge (their source is private)");
235 expect(section).not.toContain("needs that repository to be public");
235236 });
236237
237238 test("sitemap and llms.txt are each built once for 50 requests, and a repository made private drops out at once", async () => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs