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

An agent works the board too: an MCP server at POST /mcp #13

Closed opened this
opened this

Serve MCP over JSON-RPC 2.0 at POST /mcp with list_boards, list_cards, add_card and move_card, so a Claude session moves cards under the same forge identity and everyone watching the board sees it happen.

Done looks like

  • [ ] POST /mcp speaks JSON-RPC 2.0: initialize (protocol version, serverInfo, capabilities.tools), tools/list, tools/call. The id is echoed; a notification (no id) gets 202 and no body; unknown method -32601, unparsable JSON -32700, bad params -32602. Errors are JSON-RPC errors, not HTML, and never leak a stack.
  • [ ] Tools, each with a JSON Schema in tools/list and the MCP result shape { content: [{ type: "text", text }], isError? }: list_boards (the caller's boards), list_cards (board id → columns with their cards in order), add_card (board, column, title), move_card (board, card, column, index).
  • [ ] Arguments are validated at the boundary with exactly the rules src/routes.ts uses — id shapes, trimmed titles, length caps, a non-negative integer index — and the database is reached only through src/db.ts.
  • [ ] Identity is who(request): list_boards returns only boards the caller owns, and a call acts under that identity. A caller who names a board id may work it, as the link does on the web; the README says so plainly.
  • [ ] Every mutating call publishes on the bus, so a person with the board open sees the agent's card move within a second — that is the demo: two browsers and one agent on one board.
  • [ ] /mcp is safe without the HTML same-origin rule: it accepts only content-type: application/json and never form encoding, so a cross-site form cannot reach it; a body over a sane cap (say 64 KiB) is refused. The reasoning sits in a comment beside the same-origin check in src/server.ts.
  • [ ] README: the endpoint's URL, the four tools, what identity a call acts under, and how to point a Claude session at it.
  • [ ] Tests: initialize, tools/list and tools/call over fetch; add_card then list_cards shows the card; move_card reorders it; an unknown tool, a bad argument, a form-encoded post and an oversized body each fail without changing the board; a mutating call emits a bus event.
  • [ ] bun test passes.

Files

src/mcp.ts (new), src/mcp.test.ts (new), src/routes.ts, src/server.ts, README.md.

Risks

An /mcp that accepted form encoding would be a CSRF hole straight past the same-origin check; a list_boards that ignores identity leaks every board; unbounded bodies and unbounded list_cards responses are a cheap denial of service; tool errors that echo internals are a disclosure.

Suggested brief: torque (tools with exact, validated inputs and results that match the tests); sentinel to review the pull.

commented

Correction to this issue's last line: the briefs I named (ledger, torque) are not on this crew — I invented them in the run that opened this. The crew is atlas (planning), scout (builds), mason (maintains and refactors), sentinel (security review) and juniper (experience review). Read the suggestion here as scout to build, with sentinel reviewing the pull where the issue asks for a security review and juniper where the change is visible on the page. A person still assigns.

commented

Closing this as already built, verified on main at e888c44 rather than read: src/mcp.ts (136 lines) serves initialize, ping, tools/list and tools/call at POST /mcp, with list_boards, list_cards, add_card and move_card; GET /mcp answers 405 with a sentence; a notification (no id) gets 202 and no body; list_boards returns [] for an anon caller so the endpoint cannot be used to enumerate boards. It shipped with the first build (#7), which is why it was never picked up — the map had it waiting behind #12 for a bus it turned out not to need.

The half this issue existed for — an agent's move appearing for the people watching — holds too. Booted locally, created a board, opened GET /b/<id>/events, then called tools/call add_card over HTTP: the stream produced id: 1 / data: {"by":null} within the second, because mcp.ts calls touch(board.id, null) itself after createCard and moveCard (src/mcp.ts:70, :97). Two people and one agent on one board is met for the web pages; what is left of the direction's "each seeing the others' moves" is the terminal, and that is #31.

Two gaps this closes over, both already filed, neither worth holding the endpoint open for: an MCP write is not rate-limited (#40, which asks for one bucket in front of every write, MCP included) and an MCP add_card title reaches the terminal unescaped (#34).

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs