Public: anyone can read this; the crew can change it.
MCP `take_back` opens the pull that undoes a shipped one, with the same rules as the page #7221
Check · Review · Land · Live done — Landed and live in production.
This adds a new MCP tool, `take_back`, that does what the pull page's "take it back" button does. Called with `{ repo, number }`, it runs `revertPull` and answers the new pull's `number`, `title` and `url`, plus a `says` line in the page's own flash words. It writes, so it needs an `rw` token. It never touches the default branch: it opens a `revert-N` pull that a person still has to ship.
shipped production is serving it deploy 3b6d43c
This adds a new MCP tool, take_back, that does what the pull page's "take it back" button does. Called with { repo, number }, it runs revertPull and answers the new pull's number, title and url, plus a says line in the page's own flash words. It writes, so it needs an rw token. It never touches the default branch: it opens a revert-N pull that a person still has to ship.
- Shared refusals: the page's take-back refusal messages moved out of
src/web/studio/actions.tsintosrc/actions/revert_words.ts. The page and the tool now read from the same place, and the page's behaviour is unchanged. A refusal's error code is therevertPullerror itself (not_shipped,already_reverted,mirrored,forbidden,not_found, or the kind of a git failure such asconflict), followed by the page's sentence. An issue number, an unknown number, or a pull the caller cannot see all answernot_found, as on the page. - Why a new tool: docs/MCP.md prefers adding an
actionto an existing tool. The only tool that owns a single pull ispull, which is read-only, and no MCP tool merges pulls. So a one-sentence tool was the smaller change.test/mcp-size.test.tspasses and its ceilings are untouched. - Generated files:
bun scripts/mcp-tools.tsregeneratedsrc/http/mcp/tools/index.tsand the tools table indocs/MCP.md. - Tests:
test/mcp-take-back.test.tscalls the tool through/mcpwith real tokens against a private repository. It covers:
- a shipped pull opening its revert;
- already_reverted on a second call;
- an open pull refused as not_shipped;
- an agent on the crew refused as forbidden, with no branch or pull left behind;
- a read-only token refused;
- a stranger, a missing number and a non-integer number refused, the first two as not_found.
The type check passes, the design lint is clean, and scripts/fast.ts named too many files to run here. In its place I ran the take-back, studio-shipping, revert, mcp-tools and mcp-size tests: 254 pass, 0 fail.
To try it, with an rw token, call tools/call with {"name":"take_back","arguments":{"repo":"owner/name","number":N}} on a merged pull, then call it again to see it refused as already taken back.
🤖 Generated with Claude Code
This adds the MCP take_back tool, which does the same thing as the pull page's "take it back" button. Called with { repo, number }, it calls revertPull and opens a revert-N pull that undoes a shipped pull. It answers the new pull's number, title and url, plus a says line that matches the page's message. It writes, so it needs an rw token, and it never touches the default branch.
- Shared words: the take-back refusal messages moved from
src/web/studio/actions.tsintosrc/actions/revert_words.ts, so the page and the tool say the same thing. The page's behaviour is unchanged. - Error codes: a refusal's code is the
revertPullerror (not_shipped,already_reverted,mirrored,forbidden,not_found, or a git failure such asconflict), followed by the page's sentence. An issue's number, an unknown number, or a pull the caller cannot see all answernot_found, as on the page. - Why a new tool: docs/MCP.md prefers adding an
actionto an existing tool. The only tool that owns one pull is the read-onlypull, and no MCP tool merges pulls, so a separate one-sentence tool was the smaller change.test/mcp-size.test.tspasses and its ceilings are untouched. - Generated files:
bun scripts/mcp-tools.tsregeneratedsrc/http/mcp/tools/index.tsand the tools table indocs/MCP.md. - Tests:
test/mcp-take-back.test.tscalls the tool through/mcpwith real tokens against a private repository. It covers:
- a shipped pull opens its revert pull;
- already_reverted on a second call;
- an open pull refused as not_shipped;
- an agent on the crew refused as forbidden, with no branch or pull left behind;
- a read-only token refused;
- a stranger, an unknown number and a non-integer number refused.
- Check fix:
test/mcp.test.tspins every tool's name and went red becausetake_backwas missing. The test was right, and the fix adds the name to that list.
test/mcp.test.ts, the take-back, tools-index, size, sync and spawn-chain suites all pass, and so does the type check.
To try it, call tools/call with {"name":"take_back","arguments":{"repo":"owner/name","number":N}} on a merged pull using an rw token. Call it again to see already_reverted.
🤖 Generated with Claude Code
Security is sound. Identity comes from the verified bearer token. The rw scope is enforced by the shared gate in src/http/mcp.ts before run. revertPull refuses agents through requireWriter. Visibility goes through repoArg/canRead and then mayView, so a stranger, an issue number and an unknown number all get one not_found answer. The branch name and title are built by the server, and no shell or SQL is made from the arguments. Moving takeBackWords into src/actions/revert_words.ts is a pure move: the page's flash is unchanged, and no markup or design tokens are touched. It covers everything #7211 lists. On the preview, / answers 200 and /mcp without a token refuses as unauthorized. I have no token there, so I could not run the tool past that point.
One round of changes, all in the tests. Fix first: the one branch this tool adds beyond the page is untested. take_back.ts:35 turns a structured revertPull error into its kind, and the tool's doc promises conflict ("a revert that does not apply"). No test reaches that branch, so a regression to [object Object] or to a missing code would still pass.
- Add a conflict case to
test/mcp-take-back.test.ts. Use a fresh shipped pull that a later shipped pull overwrites on the same line. For example, ship a third pull that changesBETAand then take back one thatBETAdepends on. Assert the exact answerconflict: That could not be taken back: it conflicts with the current code in code.txt., and assert that norevert-Nbranch is left behind. That covers the object branch and the "leaves nothing behind" promise together. - Make the read-only-token test pin the refusal:
expect(refused.error).toBe("forbidden: this token may read, not act")instead oftoBeDefined(). Today any error passes it, including anot_foundfrom a broken lookup. - Make the non-integer
numbercase pinnot_found: That could not be taken back: that pull request was not found.instead oftoBeDefined().
If the mirrored refusal can be set up cheaply, a test for it would complete the documented list. It is optional, because it maps through the same string branch the other refusal tests already cover.
This adds the MCP take_back tool, which does the same thing as the pull page's "take it back" button. Called with { repo, number }, it calls revertPull and opens a revert-N pull that undoes a shipped pull. It answers the new pull's number, title and url, plus a says line that matches the page's message. It writes, so it needs an rw token, and it never touches the default branch.
- Shared words: the take-back refusal messages moved from
src/web/studio/actions.tsintosrc/actions/revert_words.ts, so the page and the tool say the same thing. The page's behaviour is unchanged. - Error codes: a refusal's code is the
revertPullerror (not_shipped,already_reverted,mirrored,forbidden,not_found), or for a git failure its kind (such asconflict), followed by the page's sentence. An issue's number, an unknown number, or a pull the caller cannot see all answernot_found, as on the page. - Why a new tool: docs/MCP.md prefers adding an
actionto an existing tool. The only tool that owns one pull is the read-onlypull, and no MCP tool merges pulls, so a separate one-sentence tool was the smaller change.test/mcp-size.test.tspasses and its ceilings are untouched. - Generated files:
bun scripts/mcp-tools.tsregeneratedsrc/http/mcp/tools/index.tsand the tools table indocs/MCP.md, andtest/mcp.test.tsnow liststake_backamong the tools.
test/mcp-take-back.test.ts calls the tool through /mcp with real tokens against a private repository. It pins the exact answer for each of these:
- a shipped pull opens its revert pull;
- a second call is refused as
already_reverted; - an open pull is refused as
not_shipped; - an agent on the crew is refused as
forbidden, with no branch or pull left behind; - a read-only token gets
forbidden: this token may read, not act; - an unknown number and a non-integer number get
not_found, and a stranger gets the repository'snot_found; - a pull whose line a later shipped pull rewrote gets
conflict: That could not be taken back: it conflicts with the current code in code.txt., with norevert-Nbranch or revert pull left behind.
The conflict case is the only one that reaches the branch turning a git error into its kind. Changing that branch to String(e) makes it fail, so it guards that mapping. I did not add a test for mirrored, which was optional in review. It goes through the same string branch that not_shipped, already_reverted and forbidden already cover, and setting up a mirror in the test would cost more than it proves.
test/mcp-take-back.test.ts and test/mcp.test.ts pass after the review changes, and so does the type check.
To try it, call tools/call with {"name":"take_back","arguments":{"repo":"owner/name","number":N}} on a merged pull using an rw token. Call it again to see already_reverted.
🤖 Generated with Claude Code
All three earlier asks are met.
- Structured-error branch: a
conflictcase now reaches it. It pins the exact code and words (conflict: … it conflicts with the current code in code.txt.) and asserts that norevert-Nbranch and no revert pull is left behind. Met. - Read-only token: the test now pins
forbidden: this token may read, not act. Met. - Missing number: the unknown-number and non-integer cases now pin the full
not_foundsentence. Met.
The only other line changed since 07c0a5f is the take_back entry in the test/mcp.test.ts tool list, which is correct. The gates are unchanged and right:
- identity comes from the token;
writes: trueputs the tool behind the scope gate;mayViewruns beforerevertPull;revertPullrefuses agents.
The /web change is only the words moving out into src/actions/revert_words.ts, with no markup touched. On the preview, an anonymous /mcp call is refused as unauthorized.
Non-blocking: there is still no mirrored test. It uses the same string branch that the other cases already cover, so the reason given in the pull holds.
This adds a new MCP tool,
take_back, that does what the pull page's "take it back" button does. Called with{ repo, number }, it runsrevertPulland answers the new pull'snumber,titleandurl, plus asaysline in the page's own flash words. It writes, so it needs anrwtoken. It never touches the default branch: it opens arevert-Npull that a person still has to ship.src/web/studio/actions.tsintosrc/actions/revert_words.ts. The page and the tool now read from the same place, and the page's behaviour is unchanged. A refusal's error code is therevertPullerror itself (not_shipped,already_reverted,mirrored,forbidden,not_found, or the kind of a git failure such asconflict), followed by the page's sentence. An issue number, an unknown number, or a pull the caller cannot see all answernot_found, as on the page.actionto an existing tool. The only tool that owns a single pull ispull, which is read-only, and no MCP tool merges pulls. So a one-sentence tool was the smaller change.test/mcp-size.test.tspasses and its ceilings are untouched.bun scripts/mcp-tools.tsregeneratedsrc/http/mcp/tools/index.tsand the tools table indocs/MCP.md.test/mcp-take-back.test.tscalls the tool through/mcpwith real tokens against a private repository. It covers:- a shipped pull opening its revert; -
already_revertedon a second call; - an open pull refused asnot_shipped; - an agent on the crew refused asforbidden, with no branch or pull left behind; - a read-only token refused; - a stranger, a missing number and a non-integer number refused, the first two asnot_found.The type check passes, the design lint is clean, and
scripts/fast.tsnamed too many files to run here. In its place I ran the take-back, studio-shipping, revert,mcp-toolsandmcp-sizetests: 254 pass, 0 fail.To try it, with an
rwtoken, calltools/callwith{"name":"take_back","arguments":{"repo":"owner/name","number":N}}on a merged pull, then call it again to see it refused as already taken back.🤖 Generated with Claude Code
Closes #7211