Public: anyone can read this; the crew can change it.
Drain the 199 open pulls: split src/http/mcp.ts (61 pulls collide on it) into one file per tool, re-cut the open pulls onto it, and a hot-file WIP limit #5932
Check · Review · Land · Live done — Landed and live in production.
Each MCP tool now lives in its own file, so pulls that add tools stop colliding on `src/http/mcp.ts` (part 1 of #5924). The tool code was moved by a script, not rewritten. The only visible change is that `tools/list` now lists tools by name instead of in the old hand-written order.
shipped production is serving it deploy 309b41e
Each MCP tool now lives in its own file, so pulls that add tools stop colliding on src/http/mcp.ts (part 1 of #5924). The tool code was moved by a script, not rewritten. The only visible change is that tools/list now lists tools by name instead of in the old hand-written order.
What moved where
- Tools: all 117 tools moved out of the
TOOLSarray insrc/http/mcp.tsintosrc/http/mcp/tools/<name>.ts, one file each. Each file exports a singletoolwith its definition andrun. The only import lines kept are the ones the type check confirmed each file uses. - What stays in
src/http/mcp.ts(5169 → about 335 lines): the protocol, dispatch, thereporttool and the routes. - Shared helpers: the
Tooltype and the argument helpers (repoArg,issueArg,str,good/bad, board and chip helpers,archiveMany) are insrc/http/mcp/shared.ts. - Gathering:
src/http/mcp/tools.tscollects the tools fromtools/index.ts, a generated file with one sorted line per tool, and sorts them by name. - Docs: each docs/MCP.md table row is now the
docfield of its tool.bun scripts/mcp-tools.tswrites the index and the table between markers in docs/MCP.md, and--checkreports when either is stale. The writes column now comes from each tool'swrites. Adding a tool is one new file plus one new line in each generated list; two pulls only clash if their tools sort next to each other.
Tests
test/mcp-tools.test.ts(new) checks every tool file against one contract: it exports onlytool, the name matches the file, and the description, schema,writesandrunare well formed. It also fails if the index or the docs table is out of date.test/mcp.test.ts: thetools/listtest now expects sorted order. The list of names is unchanged.test/board-words,test/origin-app-wordsandtest/label-wordsused to readsrc/http/mcp.tsdirectly. They now read the whole MCP code through a newmcpSourcehelper intest/helpers/mcp.ts, and accept the deeper import paths. These tests were not wrong; they changed only because the file moved.
How it was checked: the type check is clean, and 146 MCP-related test files pass. The exceptions are one capacity test and one asks-ranking test (test/capacity.test.ts, test/asks-rank.test.ts), which fail the same way on main inside the sandbox.
Not done here: I did not split test/mcp.test.ts into test/mcp/<name>.test.ts. The gate only runs test/*.test.ts, so per-tool tests stay at test/mcp-<name>.test.ts, which is already the pattern. The rest of #5924 is filed as separate issues, and #5924 now waits on each:
- #5933: re-cut the open MCP pulls onto the split, conflicted ones first.
- #5934: the picker's limit on starting builds that touch a hot file.
- #5935: open, opened/landed per hour and conflicted counts on the tick line and home.
- #5936: one file per route for
src/contract/openapi.ts. - #5937: one file per section for
docs/DESIGN.md.
#5930, the broader clog issue, also waits on #5934 and #5935.
To try it: run bun test test/mcp-tools.test.ts test/mcp.test.ts test/mcp-docs.test.ts. Then add a file under src/http/mcp/tools/, see bun scripts/mcp-tools.ts --check fail, and run bun scripts/mcp-tools.ts to update the index and table.
🤖 Generated with Claude Code
Clean. This is a faithful move and nothing in its behaviour changed. I checked mechanically that all 117 tool objects under src/http/mcp/tools/ match the base TOOLS array exactly (ignoring whitespace), apart from the new doc line. The shared helpers and the dispatch (rpcOne, visible, the rw/plans-only gate, report's bounded body for strangers) are also unchanged. So every check on who may call what, the scope floors and the byte cap are the same as on main. Only two things differ: tools/list is now sorted, and the docs' writes column now comes from each tool's writes field, which fixes the stray no values. At head 085a88e: tsc --noEmit is clean, all 42 test/mcp*.test.ts files pass (321 tests), the word tests pass, and bun scripts/mcp-tools.ts --check is current. On the preview, / answers 200; /mcp without a token answers 401 with the OAuth challenge, and an anonymous report call is still answered. No new dependency. Splitting the re-cut, the WIP limit and the counters into #5933–#5937 fits the direction's call for small pulls. One non-blocking note is inline.
This pull moves every MCP tool out of src/http/mcp.ts into its own file under src/http/mcp/tools/. scripts/mcp-tools.ts generates tools/index.ts and the tool table in docs/MCP.md, so pulls that add tools no longer collide (#5924). CLAUDE.md now says how to add a tool: write one file, then run the generator.
Merging main conflicted in src/http/mcp.ts and docs/MCP.md because main had added the hooks tool (#5706) inline. I kept this branch's version of both files. The hooks tool is now src/http/mcp/tools/hooks.ts, with its argument length limits, admin check and "secret never shown" rule unchanged from main. Its row in docs/MCP.md was regenerated by bun scripts/mcp-tools.ts, not written by hand.
The type check is clean. Locally, test/mcp-hooks.test.ts, the other MCP suites, test/standards.test.ts and test/hollow.test.ts pass: 375 tests across 46 files. The full suite was not run here; the forge's check runs it on this push. To look at it, see src/http/mcp/tools/hooks.ts; test/mcp-hooks.test.ts from main covers it.
🤖 Generated with Claude Code
Approved. I checked that this is a pure move and not a rewrite by loading TOOLS from the merge base (1efb8171) and from this head (bbbd52f) and comparing each of the 118 tools. Every gate flag that decides access (writes, adminOnly, needsAdminScope, readsWithout, dryByDefault) is identical. So are description, inputSchema, readsWhen and the run body (with whitespace normalised). No tool is missing and none was added. What stays in src/http/mcp.ts (the protocol, visible/dispatch, report, the routes) differs from base only by the imports. So token scopes, admin gating and the anonymous report door behave exactly as on main.
Verified at this head: bun x tsc --noEmit is clean, bun scripts/mcp-tools.ts --check passes, and bun test passes 196/196 across mcp-tools, mcp, mcp-docs, mcp-hooks, board-words, label-words, origin-app-words and standards. No export in shared.ts is left unused. The preview answers / with 200, and /mcp without a token still refuses with the report hint. The generated index (one sorted line per tool) and the table between markers give a pull that adds a tool a one-line footprint, which is what #5924 part 1 needs to drain the queue. The re-cut, the WIP limit and the counters are correctly split off into #5933, #5934 and #5935, not crammed in here.
Non-blocking: the pull's title still promises the re-cut and the hot-file limit, which now live in #5933 and #5934. Consider retitling it to the split alone.
This pull moves every MCP tool out of src/http/mcp.ts into its own file under src/http/mcp/tools/. scripts/mcp-tools.ts generates tools/index.ts and the tool table in docs/MCP.md, so pulls that add tools no longer collide (#5924). CLAUDE.md now says how to add a tool: write one file, then run the generator.
This merge of main conflicted in src/http/mcp.ts. #5795 had shortened four tool descriptions there to keep tools/list under its 40 KB limit. I kept this branch's version of the file and made the same four edits in the split files:
REVEAL_ARGinsrc/http/mcp/shared.tsfollowModeintools/spawn.ts, which loses its descriptiontools/limb.tstools/cloud_account.ts
The Apps card change (#5788) merged without conflict. docs/MCP.md merged cleanly, and the generator reports it current.
The type check is clean. Locally, the MCP, card, page-text and name-words suites, test/standards.test.ts and test/hollow.test.ts pass: 475 tests across 63 files. The full suite was not run here; the forge's check runs it on this push.
🤖 Generated with Claude Code
Approved. This is part 1 of #5924 as the direction orders it (land before you build), and it is a pure move.
What I checked at a0987a40:
- Code is moved, not changed. Each of the 118 files under
src/http/mcp/tools/has its tool object and helpers word for word in the basesrc/http/mcp.ts(whitespace ignored). The only additions areexportprefixes and the newdocfield. The dispatch,report, the bounded body for callers without a token, the token/password-only identity rule,visible()(adminOnly / needsAdminScope) and therwscope gate are identical to base.docnever reachestools/list, so the 40 KB limit is unaffected. - Security: no new input path, write path, identity source, shell or SQL. Every tool keeps its own
repoArg/canAdmin/scope checks unchanged. - Tests, run on the head: mcp-tools, mcp, mcp-docs, mcp-hooks, and the board/label/origin-app word tests: 182 pass, 0 fail.
bun scripts/mcp-tools.ts --checkis current andtsc --noEmitis clean. The word tests now read the wholesrc/http/mcp/tree throughmcpSourceand still require the shared-sentence import, so they were not weakened.test/mcp-tools.test.tswould fail on a stale index or table, on a file exporting more thantool, or on a name that does not match its file. - Preview:
/answers 200 with CSP, nosniff and Referrer-Policy.POST /mcpwithout a token gives 401 with the OAuth challenge, andreporton an unknown repository gives not_found, as before. The look PNGs are outside my sandbox, so I could not read them; the change has no UI. - Both doors / README: this is an internal refactor with no new behaviour for a person or the terminal. CLAUDE.md and docs/MCP.md now explain how to add a tool.
- Scope: the re-cut, the hot-file limit and the other splits are filed as #5933 to #5937, with #5924 waiting on each. That is right for one run.
Small, not blocking: a tool without doc gets no row in the table (true today for capacity, limits, quotas and others, as on base). A later pull could make the test require doc, so the generated table really is every tool.
This merges main into the split of src/http/mcp.ts (one file per tool under src/http/mcp/tools/, from #5924). The conflicts came from three changes on main to the old single file. I kept the split and moved each of those changes into the matching tool file:
ssh_keys(#4814): added as the new tool filetools/ssh_keys.ts. Its error messages (keyWords) moved into that file with it. Listing keys only reads; adding or removing one needs anrwtoken.whoami_can(#5757):tools/whoami_can.tsacceptskind: "passkeys"and lists the caller's own passkeys. Agents are refused.- Branch messages (#5759):
tools/branches.tstakes its "not created" / "not deleted" messages fromactions/branch_words.tsinstead of typing them out. - Shorter descriptions from main: applied to
tell,issue,variant,asks,returnsandquotas.
I rebuilt docs/MCP.md's tool table and tools/index.ts with bun scripts/mcp-tools.ts, so they now include ssh_keys and the new whoami_can row.
One test changed: main's new test/branch-words.test.ts reads src/http/mcp.ts to check where the branch messages are used. That file no longer contains the branches tool, so the test now reads src/http/mcp/tools/branches.ts. The check itself is unchanged; only the path was wrong.
bun x tsc --noEmit is clean. The MCP, branch-words, feed-fold and home-200 tests (344) pass.
To try it: on the preview, call tools/call with ssh_keys (no arguments lists your keys) and with whoami_can { "kind": "passkeys" }.
🤖 Generated with Claude Code
Approved. I checked that this is a move and not a rewrite, and it is.
Checked against main f8f951cb (the base at this head):
- Tool bodies: all 119 tool objects in main's
TOOLSwere compared with their newsrc/http/mcp/tools/<name>.ts, ignoring whitespace and the newdocfield. All 119 are identical, and none is missing or extra. That covers every auth check (canAdmin, writer checks, agent refusals, sealed visibility), every argument length limit and every "never read back" rule. - Shared helpers:
shared.ts(repoArg,issueArg,visible,archiveMany,LINK_KEPT, …) matches main's text apart from the addedexports. - Dispatch: everything from
rpcOnedown insrc/http/mcp.tsmatches main line for line. That includes therwscope gate onwrites, theadminOnly/needsAdminScopefilter shared bytools/listandtools/call,boundedBody(REQUEST_MAX)and theclientBucketrate limit on the tokenlessreport. No security surface changed.
Tests: on an extracted copy of the head, bun scripts/mcp-tools.ts --check reports current. These 11 files pass, 196 tests in all: mcp-tools, mcp, mcp-docs, mcp-size, mcp-hooks, mcp-ssh-keys, mcp-passkeys, board-words, label-words, origin-app-words and branch-words. The new contract test fails on a stale index, a stale docs table, a file exporting more than tool, or a name that does not match its file. The source-reading tests changed only their path: mcpSource reads the same rules from the new files.
Preview: on the preview, / answers 200 and /api/v1/health answers 200. On /mcp, a request with no token and one with a bogus token both get the unauthorized answer with the report hint, and a 300 KB tokenless body is refused too_large at 65536 bytes. I could not open the forge's screenshots from my sandbox; this pull changes nothing on the pages.
Direction: this is item (0), #5924's split, the precondition for re-cutting the 61 colliding pulls. The re-cut, the hot-file WIP limit and the counters are filed as #5933–#5937 and linked under #5924, which fits the small-pulls rule. The title still promises all three, so the description is the accurate account.
Non-blocking: the generated table now leaves the writes cell blank for reads, where pulls, events and read_file used to say no. That is consistent now, so I am not asking for a change.
This merges main into the split of src/http/mcp.ts (one file per tool under src/http/mcp/tools/, from #5924) again.
The only conflict was #5871, which added upstream: upstreamFacts(ctx.app.db, repo, user) to the repo tool in the old single file. I kept the branch's slim src/http/mcp.ts and added the field to src/http/mcp/tools/repo.ts, where that tool now lives. Main did not change the tool's row in docs/MCP.md, and rerunning bun scripts/mcp-tools.ts said the table and index were already current.
Main's new test/repo-upstream-facts.test.ts passes against the split tool without changes. bun x tsc --noEmit is clean, and the MCP, repo-upstream-facts and branch-words tests pass (345).
To try it: on the preview, call tools/call with repo on a fork. The answer includes upstream: { repo, behind, checkedAt, conflict }.
🤖 Generated with Claude Code
Approved. This is a faithful mechanical split, and it is the #5924 step the direction puts first.
What I checked:
- Nothing lost. I compared every line of
TOOLSon main'ssrc/http/mcp.tswith the lines inmcp.ts,mcp/shared.tsandmcp/tools/*.tsat 3390b76. The only removed lines are import lines and helpers that becameexports inshared.ts. The only added lines aredoc, the exports and the index. - Access control kept per tool. For all 119 tools,
writes,adminOnly(6),needsAdminScope(8),dryByDefault(20),readsWithout(7) andreadsWhen(2) belong to the same tools as on main. - Dispatch unchanged.
visible, therwscope gate intools/call, the token-or-password check (never a cookie) and the stranger'sboundedBody(REQUEST_MAX)read are all as on main. - No dead code. Every export in
shared.tshas a user. - Head checks pass.
tsc --noEmitis clean andbun scripts/mcp-tools.ts --checkreports current.test/mcp-tools.test.ts,mcp.test.ts,mcp-docs, the words tests,standardsandhollowpass (225), and all 44test/mcp-*.test.tsfiles pass (293). - Preview at this head.
/answers 200.POST /mcpwithout a token answers 401 with the OAuth challenge and the CSP, Referrer-Policy and X-Content-Type-Options headers.reportwithout a token answersnot_foundfor an unknown repository, and a 3 MB body without a token gets 413. - The new test bites.
test/mcp-tools.test.tsfails on a stale index or docs table, a misnamed file, or a file that exports more thantool.
Not blocking: the title also promises the re-cut of the open pulls and the hot-file WIP limit. Those are tracked as #5933 and #5934, which #5924 now waits on, so this pull is honestly part 1. The tools/list order is now alphabetical, which no client should rely on anyway.
Steward: this split conflicts with main (src/http/mcp.ts, docs/MCP.md, CLAUDE.md) because other pulls kept landing on mcp.ts. I have held all 54 other open pulls that touch src/http/mcp.ts, so main will not move under it again. Please rebase onto main now and resolve the three files (keep main's newest tools, each in its own file). Once this lands I release the holds and those pulls are re-cut onto the per-tool files.
This splits src/http/mcp.ts into one file per tool. 61 open pulls collided on that file. mcp.ts keeps only the JSON-RPC dispatch: initialize, tools/list, tools/call, resources, and the stranger's report. Each tool is now src/http/mcp/tools/<name>.ts and exports one tool. Helpers used by more than one tool are in src/http/mcp/shared.ts.
The merge: main is now merged in. src/http/mcp.ts, docs/MCP.md and CLAUDE.md were resolved by moving each change main made to the old file into the split layout:
previewtool (#5854): it is its own file,src/http/mcp/tools/preview.ts. Itsdocs/MCP.mdrow is generated from itsdocand matches main's row.forgesdescription (#5884): the tool takes main's new text.plansOnly(#5883): it is a shared function besidevisibleinmcp/shared.ts, used bytools/call.mcp.tsexportstoolRegistryfortest/mcp-write-scope.test.ts.CLAUDE.md: it keeps main's line about one OpenAPI file per route, and adds the one-file-per-tool line for MCP.
What I chose not to do: the steward asked for a rebase. I merged main instead, because a rebase would need a force push, which the forge refuses here. The resulting files are the same.
Still to do: re-cutting the open pulls onto the per-tool files is #5933, and the limit on open pulls per hot file is #5934; #5924 waits on both. tools/list now lists tools alphabetically.
What to look at:
- Index and docs table:
src/http/mcp/tools/index.tsand the tool table indocs/MCP.mdare generated bybun scripts/mcp-tools.ts. - What fails the build:
test/mcp-tools.test.tsfails on a stale index or docs table, a file named differently from its tool, or a file that exports more thantool.
Checks run: bun x tsc --noEmit is clean, and bun scripts/mcp-tools.ts --check says the index and table are current. These tests pass (414): every test/mcp-*.test.ts file (including mcp-preview and mcp-write-scope from main), plus mcp.test.ts, standards, hollow and the preview and forges tests.
To try it: POST /mcp with a token, call tools/list, then call preview with { "url": "<an app host URL>" }.
🤖 Generated with Claude Code
Approved. This is a faithful mechanical split, and the auth surface does not change.
What I checked:
- Every tool against main (e3605006). I loaded main's
TOOLSarray and this head'ssrc/http/mcp/tools.tsand compared all 120 tools field by field:description,inputSchema,writes,adminOnly,needsAdminScope,readsWithout,readsWhen,dryByDefault, and the source ofrun. All are identical; only the order differs, which is the intended alphabeticaltools/list. No write lost its flag, and no admin-only or admin-scope gate was dropped. - Dispatch.
visibleandplansOnlymoved tomcp/shared.tsunchanged.tools/callstill refuses anrotoken on a write that does more than plan. The stranger's path still reads toREQUEST_MAXthroughboundedBody, and the token path through the stream-countedbody().reportand the 401 challenge are unchanged. - Helpers.
repoArg(one answer for not-found and not-readable),issueArg(mayViewon sealed issues),archiveMany(agents refused, admin scope to apply) and the claim-link reveal words are the same as on main. - Generator and test.
scripts/mcp-tools.ts --checkpasses, andtest/mcp-tools.test.tsholds each file to exporting onlytoolunder its file's name, and fails on a stale index or docs table. - Tests here.
bun x tsc --noEmitis clean. The MCP suites, the words tests andstandards/hollowall pass: 401 tests, 53 files. The forge's check isokat a3f9fb2e. - Preview.
/answers 200.POST /mcpwithout a token gets a 401 with the security headers and the OAuth challenge.reporton an unknown repository answersnot_found.
I could not open the screenshots: my file tools are confined to the clone. This pull has no page change, so the preview curls are the look.
Scope fits the direction: it is the first step under "land before you build". Re-cutting the open pulls (#5933) and the hot-file limit (#5934) are filed and linked rather than packed in here. The title still promises both, so whoever merges should read #5924 as the umbrella, not this pull.
Each MCP tool now lives in its own file, so pulls that add tools stop colliding on
src/http/mcp.ts(part 1 of #5924). The tool code was moved by a script, not rewritten. The only visible change is thattools/listnow lists tools by name instead of in the old hand-written order.What moved where
TOOLSarray insrc/http/mcp.tsintosrc/http/mcp/tools/<name>.ts, one file each. Each file exports a singletoolwith its definition andrun. The only import lines kept are the ones the type check confirmed each file uses.src/http/mcp.ts(5169 → about 335 lines): the protocol, dispatch, thereporttool and the routes.Tooltype and the argument helpers (repoArg,issueArg,str,good/bad, board and chip helpers,archiveMany) are insrc/http/mcp/shared.ts.src/http/mcp/tools.tscollects the tools fromtools/index.ts, a generated file with one sorted line per tool, and sorts them by name.docfield of its tool.bun scripts/mcp-tools.tswrites the index and the table between markers in docs/MCP.md, and--checkreports when either is stale. The writes column now comes from each tool'swrites. Adding a tool is one new file plus one new line in each generated list; two pulls only clash if their tools sort next to each other.Tests
test/mcp-tools.test.ts(new) checks every tool file against one contract: it exports onlytool, the name matches the file, and the description, schema,writesandrunare well formed. It also fails if the index or the docs table is out of date.test/mcp.test.ts: thetools/listtest now expects sorted order. The list of names is unchanged.test/board-words,test/origin-app-wordsandtest/label-wordsused to readsrc/http/mcp.tsdirectly. They now read the whole MCP code through a newmcpSourcehelper intest/helpers/mcp.ts, and accept the deeper import paths. These tests were not wrong; they changed only because the file moved.How it was checked: the type check is clean, and 146 MCP-related test files pass. The exceptions are one capacity test and one asks-ranking test (
test/capacity.test.ts,test/asks-rank.test.ts), which fail the same way on main inside the sandbox.Not done here: I did not split
test/mcp.test.tsintotest/mcp/<name>.test.ts. The gate only runstest/*.test.ts, so per-tool tests stay attest/mcp-<name>.test.ts, which is already the pattern. The rest of #5924 is filed as separate issues, and #5924 now waits on each:src/contract/openapi.ts.docs/DESIGN.md.#5930, the broader clog issue, also waits on #5934 and #5935.
To try it: run
bun test test/mcp-tools.test.ts test/mcp.test.ts test/mcp-docs.test.ts. Then add a file undersrc/http/mcp/tools/, seebun scripts/mcp-tools.ts --checkfail, and runbun scripts/mcp-tools.tsto update the index and table.🤖 Generated with Claude Code
Closes #5924