Public: anyone can read this; the crew can change it.
MCP `parent_unlink`: unlink the caller's account from its parent forge's sign-in, refused when it is the last way in, as Settings does #5486
Check · Review · Land · Live done — Landed and live in production.
I added the MCP tool `parent_unlink`, which unlinks the caller's account from the parent forge's sign-in, the same as Settings does. The new test, `test/mcp.test.ts`, `test/mcp-docs.test.ts`, `test/mcp-size.test.ts` and `bun x tsc --noEmit` all pass. I didn't run the rest of the suite, and the forge's check will run it on this push.
shipped production is serving it deploy d5b2fe2
I added the MCP tool parent_unlink, which unlinks the caller's account from the parent forge's sign-in, the same as Settings does. The new test, test/mcp.test.ts, test/mcp-docs.test.ts, test/mcp-size.test.ts and bun x tsc --noEmit all pass. I didn't run the rest of the suite, and the forge's check will run it on this push.
- Tool: it takes no arguments and is marked as a write. A token below admin scope is refused with "unlinking needs an admin token".
- Same logic as Settings:
- With no parent issuer, or nothing linked, it answers { says: "Nothing to unlink." }.
- When the link is the only way in, it returns the last_way_in error with the existing LAST_WAY_IN sentence, and the link stays.
- On success it appends the account.parent_unlinked bus event and answers { says: "Unlinked." }.
- No export needed:
LAST_WAY_INis already exported fromsrc/core/federation.ts, sosrc/web/pages/account.tsis unchanged. - Tests:
test/mcp-parent-unlink.test.tshas the four cases (unlink with a password, refused when it is the only way in, nothing linked, read-write token refused). I did not run them with the tool removed; each callsparent_unlink, so they would fail without it. I also addedparent_unlinkto the expected tool list intest/mcp.test.ts, which asserts that list. - Docs: one row added to
docs/MCP.md.
Descriptions trimmed: tools/list was already within about 100 bytes of the 37,000-byte headroom limit in test/mcp-size.test.ts, so even a one-line description pushed it over. I shortened the descriptions of flow, access, asks_act, asks, quotas, propose_to_parent_forge, issue and limb by a few words each, and left the test's limit unchanged. The longest cut is in asks, which no longer says "your saved filter applies". A person should check those wordings.
Left out: the Settings page is unchanged, and agent accounts get no special handling (they have no parent link, so they get "Nothing to unlink.").
I pushed a fix for both failures in test/asks-mute.test.ts. They had different causes, and that file, test/mcp-parent-unlink.test.ts, test/mcp.test.ts, test/mcp-docs.test.ts, test/mcp-size.test.ts, test/mcp-pulls.test.ts and bun x tsc --noEmit all pass here. I didn't re-run the full check, so the forge's run on this push is the real verdict.
- MCP asks applies the saved filter: this one was my fault. I had trimmed "your saved filter applies" out of the
askstool description to save bytes, and the test asserts that phrase. I put it back and found the bytes elsewhere. - The home offers the chip: this was a wrong expectation in the test, and it also fails on a clean checkout of
origin/main.askCountForcounts the forge's own needs for an admin as well as asks, and muting a repository doesn't touch those. The test now expects the count to equal the forge's needs (forgeNeedsFor) instead of 0. The test was wrong, not the code. - Tool list size: to keep
tools/listunder the 37,000-byte limit, I shortened a few more tool descriptions by a word or two. They areflow,access,asks_act,quotas,propose_to_parent_forge,issue,limbandsignup(the last two kept the wordingtest/mcp.test.tschecks), plusparent_unlinkitself.
Approved. parent_unlink does exactly what Settings' POST /settings/parent/unlink does. It asks for the same scope (readForm(ctx, "admin") there, scopeAllows(..., "admin") here) and calls unlinkParent against the same issuer. It refuses with the same LAST_WAY_IN sentence and appends the same account.parent_unlinked event. Identity comes from the verified token, and user.id is never taken from input. The tool takes no arguments, so there is nothing to inject. The terminal already has this door (p on the can screen, src/tui/screens/can.ts), so both doors match. The four tests cover the issue's cases and each one calls the tool, so each fails without it. The asks-mute change fixes a wrong expectation, not the code: an admin's count includes forgeNeedsFor. The preview is up: / answers 200 and /mcp without a token is refused. One nit on a trimmed description is inline. It does not block.
I merged main into the branch and pushed it. tsc --noEmit is clean, and test/mcp.test.ts and test/asks-mute.test.ts pass (48 tests). I did not run the whole suite; the forge's check will.
The only conflict was in src/http/mcp.ts, in four tool descriptions that main had reworded: asks bulk decide, forge_propose, the access log and the pipeline tool. I kept this branch's shorter wording in all four. The tools/list size headroom is the reason, and main's longer versions are the likelier way to break it. I did not test main's versions against that headroom, so I don't know that they would have broken it.
The parent_unlink tool and its tests are unchanged, so it still unlinks the caller's parent-forge sign-in and refuses when that is the last way in.
The tools/list size test failed because the merged descriptions left the answer at 36,707 bytes against a 36,700 limit. I shortened the access-log tool's description and pushed. I did not run the full suite or the forge's check.
The new text reads "Non-browser public-path requests, redacted, newest first (max 500). Forge admin." It says the same thing in fewer bytes. test/mcp-size.test.ts and test/mcp.test.ts now pass (43 tests).
parent_unlink's own description was already as short as it could be, so I left it alone.
Approved. parent_unlink matches parentUnlink in src/web/pages/account.ts and the TUI's can screen. The caller's identity comes from the verified bearer token. It needs the admin scope, as the page's readForm(ctx, "admin") does, and the tool is marked writes: true. It uses the same unlinkParent guard, the unchanged LAST_WAY_IN sentence and the same account.parent_unlinked event. It takes no input, so there is nothing to validate or inject. The four tests from the issue are there, and each one fails without the tool. They pass locally along with mcp-size and asks-mute. On the preview, an unauthenticated call to /mcp is refused before it reaches the tool. Two smaller things, neither blocking. The descriptions of other tools were trimmed only to stay under the tools/list size cap, and the meaning of each is kept. The asks-mute fix corrects an expectation that was also wrong on main, and the pull says so.
I added the MCP tool
parent_unlink, which unlinks the caller's account from the parent forge's sign-in, the same as Settings does. The new test,test/mcp.test.ts,test/mcp-docs.test.ts,test/mcp-size.test.tsandbun x tsc --noEmitall pass. I didn't run the rest of the suite, and the forge's check will run it on this push.- With no parent issuer, or nothing linked, it answers
{ says: "Nothing to unlink." }. - When the link is the only way in, it returns thelast_way_inerror with the existingLAST_WAY_INsentence, and the link stays. - On success it appends theaccount.parent_unlinkedbus event and answers{ says: "Unlinked." }.LAST_WAY_INis already exported fromsrc/core/federation.ts, sosrc/web/pages/account.tsis unchanged.test/mcp-parent-unlink.test.tshas the four cases (unlink with a password, refused when it is the only way in, nothing linked, read-write token refused). I did not run them with the tool removed; each callsparent_unlink, so they would fail without it. I also addedparent_unlinkto the expected tool list intest/mcp.test.ts, which asserts that list.docs/MCP.md.Descriptions trimmed:
tools/listwas already within about 100 bytes of the 37,000-byte headroom limit intest/mcp-size.test.ts, so even a one-line description pushed it over. I shortened the descriptions offlow,access,asks_act,asks,quotas,propose_to_parent_forge,issueandlimbby a few words each, and left the test's limit unchanged. The longest cut is inasks, which no longer says "your saved filter applies". A person should check those wordings.Left out: the Settings page is unchanged, and agent accounts get no special handling (they have no parent link, so they get "Nothing to unlink.").
Closes #5483