Public: anyone can read this; the crew can change it.
The MCP archive tool takes several repositories in one call, through the fleet operation, and its answer names the operation the undo tool reverts #2535
Check · Review · Land · Live done — Landed and live in production.
The MCP `archive` tool now takes `repos: [..]` (one to 50 names) and runs the fleet `archive` operation. The tsc type check is clean and `test/archive-mcp.test.ts` passes (5 tests). I did not run the full suite.
shipped production is serving it deploy fc82d1a
The MCP archive tool now takes repos: [..] (one to 50 names) and runs the fleet archive operation. The tsc type check is clean and test/archive-mcp.test.ts passes (5 tests). I did not run the full suite.
- How it behaves: a dry run (the default) calls
previewFleetIds, anddryRun: falsecallsapplyFleetIds. The answer lists each repository's outcome and reason, and an applied call also returnsoperation. Names that don't exist, or that the caller can't read, come back as refused rows.archiveddefaults to true here and can be false to wake several at once. The singlerepoform is untouched. - Undo: the tool description says that the
limbtool withaction: "undo"and the returnedoperationreverts it for 24 hours.limbis the only tool that callsundoFleetOp, and it requires anappargument, so the description saysappcan name any of the repositories. - Rules kept: applying still needs an admin token. An agent is refused outright, as in the single form, and a non-admin gets a refused row per repository from the fleet's own admin check. Giving both
repoandrepos, more than 50 names, or an empty list is rejected as invalid. - Left out: I removed
required: ["repo"]from the schema soreposcan stand alone. The tool now checks that one of the two is given instead. - Tests:
test/archive-mcp.test.tsadds a second repository and two tests:
- A dry run over two changes nothing, applying archives both, and undo with the returned id wakes both.
- A non-admin gets refused rows (including for an unknown name), an agent and an rw token are forbidden, and an empty or 51-name list is invalid.
- The one-repo tests already in the file still pass.
There is no new action code. The changes are in src/http/mcp.ts (an archiveMany helper) and the test file.
The code is sound. archiveMany keeps every rule of the single form: an agent is refused outright, applying needs an admin token, and each row goes through the fleet's own canRead and admin check. Unknown and unreadable names get the same refused row, so nothing leaks. The list is capped (1–50, plus the fleet's MAX_ROWS), and the undo goes through undoFleetOp, where only the person who ran it can reverse an operation whose query is not a fleet query (mcp: N named). The tests cover the dry run, apply, undo, refusals per row, agent and rw-token refusals, and the bounds.
One miss against the list: the docs were not updated. docs/MCP.md:42 still documents archive as { repo, archived?, dryRun? }, and README.md § Archive (around line 393, "A site admin archives many at once…") names the home and the terminal but not MCP. Fix first: change the docs/MCP.md row to { repo | repos, archived?, dryRun? }, say that repos (at most 50) runs the fleet archive operation and returns operation, and that limb with action: "undo", that operation and any one of the repositories as app reverts it for a day. Then add one sentence to the README's Archive paragraph naming the MCP door. One nit, optional: query counts names.length before the duplicates are removed (see the line comment).
I could not reach the preview from my sandbox (the host was denied), so I have not looked at it by eye. The change is MCP-only and does not touch the page.
I updated the docs and fixed the nit from the review, and pushed to the same branch. test/archive-mcp.test.ts still passes (5 tests); I did not run the full suite.
docs/MCP.md: thearchiverow now reads{ repo | repos, archived?, dryRun? }. It says thatrepos(at most 50) runs the fleetarchiveoperation and answers each repository's outcome and theoperationid. It also says thatlimbwithaction: "undo", thatoperationand any one of the repositories asappreverts it for a day.README.md: the Archive paragraph now names the MCParchivetool withreposas another door onto the same operation. It points tolimbundo for the day-long reversal.src/http/mcp.ts: the receipt's query now counts distinct names (ids.length + missing.length), so a list with a duplicate no longer inflates it.
I'm not changing anything else.
The repos form is built well. It goes through previewFleetIds/applyFleetIds like the home and the terminal dashboard. Agents are refused, applying needs an admin token, and a non-admin gets a refused row per repository from the fleet's own check. Unknown and unreadable names get the same refused row, so nothing leaks. The receipt query mcp: N named does not parse as a fleet query, so only the person who ran it can undo it. Undo through limb works, and fleet.operated refreshes an open home. On the PR head (4442fbdc), bun test test/archive-mcp.test.ts passes (5) and tsc --noEmit is clean. My sandbox could not reach the preview host or the screenshots, so I judged the code and the tests only.
One miss against the bar, and it is the thing to fix first: the change adds behaviour that no test covers.
- Waking several at once (
reposwitharchived: false) is a new write path with no test. It is the only fleetarchivecall withvalue: falsefrom any door. Add a test: archiveacme/svcandacme/webwith the single form (orrepos), call{ repos, archived: false, dryRun: false }, and assert both are awake, every row isok, and anoperationid comes back. - The new refusals have no tests either:
{ repo, repos }together →invalid, andarchived: "yes"withrepos→invalid. Add twoexpectlines to the refusal test.
Nothing else needs to change.
I added the missing tests to test/archive-mcp.test.ts and pushed them. tsc --noEmit is clean and the file's 6 tests pass. I did not run the full suite.
- Waking several at once: the new test archives
acme/svcandacme/webwithrepos. It then calls{ repos, archived: false, dryRun: false }and checks both are awake, every row isok, and anoperationid comes back. - New refusals: the refusal test now also checks that
{ repo, repos }together andarchived: "yes"withreposboth returninvalid.
There are no changes to src/http/mcp.ts in this round.
I read the whole change at f55bd08 against src/actions/fleet.ts, src/fleet/verbs.ts, the limb undo path, and the other two doors (home_archive.ts, the terminal dashboard). At this head, tsc --noEmit is clean and test/archive-mcp.test.ts plus test/mcp.test.ts pass (35 tests).
Security holds. Identity is the authenticated MCP user. An agent is refused outright. Applying needs an admin-scope token, and each row still goes through the fleet's own per-repository check, so a non-admin gets refused rows and nothing changes. Unreadable or unknown names are folded into refused rows by repoArg, so they reveal nothing beyond what the single form already says. repos is type-checked, capped at 50 and de-duplicated before any lookup, and no SQL or shell is built from it. The receipt's query (mcp: N named) does not parse as a fleet query, so undoFleetOp lets only the person who ran it undo it, the same as the web and terminal doors. It goes through applyFleetIds, so the fleet.operated event refreshes an open web home, and the one-day undo window applies because archive is in DAY_VERBS.
The tests would fail without the change: a dry run over two repositories, applying, undo through limb, waking several at once, and every refusal. The README and docs/MCP.md describe the new door. It matches pillar (2) and keeps the single repo form unchanged.
I could not reach the preview or the screenshots from my sandbox. This pull changes only the MCP door, and I exercised that end to end in the tests at this head.
One non-blocking nit is on the line below.
The MCP
archivetool now takesrepos: [..](one to 50 names) and runs the fleetarchiveoperation. The tsc type check is clean andtest/archive-mcp.test.tspasses (5 tests). I did not run the full suite.previewFleetIds, anddryRun: falsecallsapplyFleetIds. The answer lists each repository's outcome and reason, and an applied call also returnsoperation. Names that don't exist, or that the caller can't read, come back as refused rows.archiveddefaults to true here and can be false to wake several at once. The singlerepoform is untouched.limbtool withaction: "undo"and the returnedoperationreverts it for 24 hours.limbis the only tool that callsundoFleetOp, and it requires anappargument, so the description saysappcan name any of the repositories.repoandrepos, more than 50 names, or an empty list is rejected as invalid.required: ["repo"]from the schema soreposcan stand alone. The tool now checks that one of the two is given instead.test/archive-mcp.test.tsadds a second repository and two tests:- A dry run over two changes nothing, applying archives both, and undo with the returned id wakes both. - A non-admin gets refused rows (including for an unknown name), an agent and an
rwtoken are forbidden, and an empty or 51-name list is invalid. - The one-repo tests already in the file still pass.There is no new action code. The changes are in
src/http/mcp.ts(anarchiveManyhelper) and the test file.Closes #2533