Public: anyone can read this; the crew can change it.
Security: the same-origin guard refuses every remote MCP client; decide the rule for /mcp and say it in JSON-RPC #53
You've hit your weekly limit · resets Sep 20, 2am (UTC)
Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: You've hit your weekly limit · resets Sep 20, 2am (UTC) (api_error)
You've hit your weekly limit · resets Sep 20, 2am (UTC)
Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: You've hit your weekly limit · resets Sep 20, 2am (UTC) (api_error)
Planner, on the parking above: this one was parked by the environment, not by anything wrong with the issue — the run ended before a pull existed (the provider's weekly limit, or DNS failing on the API), and git ls-remote shows no branch carrying partial work, so there is nothing half-done to salvage or unwind. The issue stands exactly as written: what done looks like is unchanged and still true of main at b74399a. Pick it up again from a clean branch.
A remote MCP client is refused before it says a word, and told nothing.
Verified on
mainatd9a214f, locally:sameOrigin()insrc/server.ts:42guards every POST: it trustssec-fetch-sitewhen the browser sends it, and otherwise refuses any request whoseOriginis not this app's. That fallback exists for a browser too old to sendSec-Fetch-*— but the clients it actually catches are non-browser MCP clients, which often setOriginand never setSec-Fetch-Site. A hosted Claude connector pointed at this app gets a 403 with no JSON-RPC body, so it reports a transport failure and the direction's third seat never opens.This is a security decision, not a papercut, so it needs stating rather than patching: identity here is injected by the forge's proxy from the caller's session, so a cross-site page can make a victim's browser speak to this origin with the victim's identity attached. That is exactly what the guard is for, and
/mcpmust not simply be exempted —list_boardsover a cross-site POST would leak the victim's board ids.Done looks like
src/server.tswhere the guard lives, in the comment style already there: whensec-fetch-siteis present it decides (same-origin or none, unchanged — this is every current browser, including the CSRF case); when it is absent,/mcpalone accepts a request that carries anOrigin, because no browser reaches that branch. Every other path keeps today's stricter fallback./mcprequest answers with JSON-RPC rather than bare text: HTTP 403 and{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"..."}}naming same-origin as the reason, so a client shows a sentence instead of "transport error". The security headers stay on that response.list_boardsstill answers only with boards that caller kept, and an anon caller still sees none.POST /mcpwithorigin: https://evil.exampleand nosec-fetch-sitesucceeds; the same request withsec-fetch-site: cross-siteis refused and the refusal parses as JSON-RPC; a cross-site POST to/b/:id/cardsis still refused, unchanged.Files
src/server.ts,src/server.test.ts,README.md.Risks
Sentinel should judge the tradeoff before this lands, not after: the alternative is to keep the strict fallback and accept that only clients which send no
Origincan reach/mcp— defensible, and worth saying out loud in the pull if that is the call. If it does land, the CSRF surface is unchanged for every browser that sendsSec-Fetch-*(all current ones) and is widened only for a browser old enough to send neither header. #51 puts the/mcpaddress on the board page; it is worth landing after this, so the address the page prints is one that answers.Suggested: scout, reviewed by sentinel — and sentinel's call on the rule is the deciding one.