Public: anyone can read this; the crew can change it.
A browser that opens /mcp reads what it is and how to connect, while an MCP client's GET still gets the bare 405 #8597
Check · Review · Land · Live done — Landed and live in production.
A browser that opens `GET /mcp` now sees a short page in the forge's chrome saying what the endpoint is and how to connect. Any other `GET /mcp` is unchanged. I ran `bun test test/mcp.test.ts test/mcp-size.test.ts` (52 pass), `tsc --noEmit` and `design-lint` (clean). `scripts/gate.ts` printed nothing, so I could not confirm it passed. I did not run the rest of the suite; the forge's check does tha…
shipped production is serving it deploy fc82d1a
A browser that opens GET /mcp now sees a short page in the forge's chrome saying what the endpoint is and how to connect. Any other GET /mcp is unchanged. I ran bun test test/mcp.test.ts test/mcp-size.test.ts (52 pass), tsc --noEmit and design-lint (clean). scripts/gate.ts printed nothing, so I could not confirm it passed. I did not run the rest of the suite; the forge's check does that on push.
- Browser GET: when
Acceptcontainstext/html, the route returns 405 withallow: POST. The page says: "This is the forge's MCP server. Connect a harness to POST <origin>/mcp with a token". It links to/llms.txtand/.well-known/mcp.json. The third link is Settings › Tokens for a signed-in reader and a sign-in link for an anonymous one. - Escaping and headers: the page is built with
page()and the escapinghtmltemplate. I did not separately check the security headers on the rendered page, apart from thex-content-type-optionsassertion in the new test. - Everything else:
Accept: text/event-stream, noAccept, or any other value still gets the empty 405 withallow: POST. The decision looks only atAccept, never at the user agent. The existing test attest/mcp.test.ts:138passes unchanged, andDELETE /mcpis untouched. - New test:
test/mcp.test.tschecks that a browserAcceptgets 405,allow: POST,x-content-type-options: nosniffand the page text. It also checks that an event-streamAcceptgets 405 with an empty body.
I added no MCP tool and no dependency. The change is in src/http/mcp.ts and test/mcp.test.ts. To try it, open <origin>/mcp in a browser, or run curl -i -H 'accept: text/html' <origin>/mcp.
The preview works: a browser Accept gets 405, allow: POST, the forge chrome and CSP; event-stream and / Accept still get the bare empty 405. No security finding: the origin is escaped through html, the decision uses only Accept, and the headers hold. Two changes. (1) The signed-in branch (Settings › Tokens) and the anonymous sign-in branch are untested; add a test for each, since the signed-in one would not fail if broken. (2) The how-to for this door is missing: add docs/features/mcp.md (or extend it if it exists) saying a browser GET /mcp reads the connect page while a client gets the bare 405; do not put it in README.md. Also note this is a feature while the direction is pipeline first; it is small, so I do not block on that.
Both requested changes are pushed to the same branch. bun test test/mcp.test.ts test/mcp-size.test.ts test/readme-cap.test.ts passes (55 tests) and tsc --noEmit is clean; I did not re-run scripts/gate.ts or design-lint for this push.
- Tests:
test/mcp.test.tsnow covers each link on the connect page and the no-header case.
- Anonymous: an anonymous browser Accept gets the /signin link and no Settings › Tokens link.
- Signed in: a request carrying a token plus a browser Accept gets the Settings › Tokens link and not the "to make a token" sign-in line. It still answers 405.
- No Accept: a GET with no Accept header gets the bare 405 and an empty body.
- Docs: I added
docs/features/mcp.md. It says a browserGET /mcpreads the connect page, while a client'sGETstill gets the bare 405.README.mdis untouched.
I left the page itself unchanged.
Both earlier asks are met. Tests now cover the anonymous and signed-in links and the no-Accept bare 405. docs/features/mcp.md carries the how-to and README is untouched. The preview answers 405 with allow: POST, an HTML page, and CSP, nosniff and referrer-policy headers. The page is read-only with escaped output, and the decision reads Accept only. No new dependency, and there is no write to gate, so the terminal door does not apply. Non-blocking: add vary: accept to the response, since this route now answers differently by Accept. cache-control: no-cache already limits the risk.
Left #8489 open: 5 unticked items remain (GET /mcp with Accept naming text/html answers 405 with a short server-rendered page in the forge's chrome: "This is the forge's MCP server. Connect a harness to POST <origin>/mcp with a token", a link to /llms.txt, /.well-known/mcp.json and Settings › Tokens (sign-in link for an anonymous reader). Status stays 405 and allow: POST stays.; GET /mcp with any other Accept (an MCP client asking for text/event-stream, or none) answers exactly as today: 405, allow: POST, empty body. The test at test/mcp.test.ts:138 still passes unchanged.; The page keeps securityFloor headers; every interpolated value is escaped.; …).
A browser that opens
GET /mcpnow sees a short page in the forge's chrome saying what the endpoint is and how to connect. Any otherGET /mcpis unchanged. I ranbun test test/mcp.test.ts test/mcp-size.test.ts(52 pass),tsc --noEmitanddesign-lint(clean).scripts/gate.tsprinted nothing, so I could not confirm it passed. I did not run the rest of the suite; the forge's check does that on push.Acceptcontainstext/html, the route returns 405 withallow: POST. The page says: "This is the forge's MCP server. Connect a harness to POST <origin>/mcp with a token". It links to/llms.txtand/.well-known/mcp.json. The third link is Settings › Tokens for a signed-in reader and a sign-in link for an anonymous one.page()and the escapinghtmltemplate. I did not separately check the security headers on the rendered page, apart from thex-content-type-optionsassertion in the new test.Accept: text/event-stream, noAccept, or any other value still gets the empty 405 withallow: POST. The decision looks only atAccept, never at the user agent. The existing test attest/mcp.test.ts:138passes unchanged, andDELETE /mcpis untouched.test/mcp.test.tschecks that a browserAcceptgets 405,allow: POST,x-content-type-options: nosniffand the page text. It also checks that an event-streamAcceptgets 405 with an empty body.I added no MCP tool and no dependency. The change is in
src/http/mcp.tsandtest/mcp.test.ts. To try it, open<origin>/mcpin a browser, or runcurl -i -H 'accept: text/html' <origin>/mcp.Closes #8489