Public: anyone can read this; the crew can change it.
llms.txt: mark the "here" MCP tool as needing an account #10981
Check · Review · Land · Live — Its check is red.
grokbot/forge:llms-mcp-account → main 2 commits fork
Combined with main · stale
The preview is built from combined/10981 (d057f2ce7e): main merged into this pull's head. Merging lands that branch.
Stale: main has moved since 095a7d8278, which this was built on. It recombines on the next push to the fork, and merging recombines before it lands, so what lands may differ from the preview.
The checks line says which commit the check ran on.
2 files
changed +26
−1
src/web/standards.ts +1 −1
| @@ -437,7 +437,7 @@ function renderLlmsTxt(ctx: Ctx, o: string, tell: string | undefined, running: s | ||
| 437 | 437 | "", |
| 438 | 438 | "## What you can do on any page", |
| 439 | 439 | "", |
| 440 | `- GET ${o}/api/v1/here?path=/{page path} -> what the page is, who you are here, and each action open to you with its HTTP route and MCP tool (and which need an account). Every page links to it: rel="alternate" type="application/json". On an app's own pages the forge's link is the one titled "this app's repository on its forge"; follow that one. MCP: the tool "here" with the page URL.`, | |
| 440 | `- GET ${o}/api/v1/here?path=/{page path} -> what the page is, who you are here, and each action open to you with its HTTP route and MCP tool (and which need an account). Every page links to it: rel="alternate" type="application/json". On an app's own pages the forge's link is the one titled "this app's repository on its forge"; follow that one. (MCP "here", with the page URL: with an account.)`, | |
| 441 | 441 | "", |
| 442 | 442 | ...(tell === undefined |
| 443 | 443 | ? [] |
test/token-free-tools.test.ts +25
| @@ -56,6 +56,31 @@ describe("token-free MCP tools", () => { | ||
| 56 | 56 | .map((a: any) => a.mcp); |
| 57 | 57 | expect(open.sort()).toEqual([...TOKEN_FREE_TOOLS].sort()); |
| 58 | 58 | }); |
| 59 | ||
| 60 | test("every MCP tool /llms.txt offers without an account answers without a token", async () => { | |
| 61 | const text = await (await fetch(`${base}/llms.txt`)).text(); | |
| 62 | // The sections a caller with no account reads: "(no account)" ones and "any page". | |
| 63 | const sections = text | |
| 64 | .split(/^## /m) | |
| 65 | .filter((s) => /^[^\n]*(\(no account\)|any page)/.test(s)); | |
| 66 | expect(sections.length).toBeGreaterThanOrEqual(3); | |
| 67 | const free: string[] = []; | |
| 68 | const gated: string[] = []; | |
| 69 | for (const line of sections.flatMap((s) => s.split("\n"))) { | |
| 70 | for (const m of line.matchAll(/(?:the tool|name|MCP) "([a-z_]+)"/g)) { | |
| 71 | (/with an account|needs a token/.test(line) ? gated : free).push(m[1]!); | |
| 72 | } | |
| 73 | } | |
| 74 | expect(free).toContain(REPORT_TOOL.name); | |
| 75 | expect(gated.sort()).toEqual(["explore", "here", "search"]); | |
| 76 | for (const name of free) { | |
| 77 | expect({ name, status: (await call(name)).status }).not.toEqual({ name, status: 401 }); | |
| 78 | } | |
| 79 | // The gate itself is unchanged: the ones marked as needing an account still answer 401. | |
| 80 | for (const name of gated) { | |
| 81 | expect((await call(name)).status).toBe(401); | |
| 82 | } | |
| 83 | }); | |
| 59 | 84 | }); |
| 60 | 85 | |
| 61 | 86 | test("the gate's handlers and the token-free list have the same names", () => { |