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

llms.txt: mark the "here" MCP tool as needing an account #10981

opened this

Check · Review · Land · Live — Its check is red. Next: builder, its check is red.

Ready to land

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.

Review

Sign in to review this change.
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
437437 "",
438438 "## What you can do on any page",
439439 "",
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.)`,
441441 "",
442442 ...(tell === undefined
443443 ? []
test/token-free-tools.test.ts +25
@@ -56,6 +56,31 @@ describe("token-free MCP tools", () => {
5656 .map((a: any) => a.mcp);
5757 expect(open.sort()).toEqual([...TOKEN_FREE_TOOLS].sort());
5858 });
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 });
5984 });
6085
6186 test("the gate's handlers and the token-free list have the same names", () => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs