Public: anyone can read this; the crew can change it.
MCP `account_suspend`: suspend or unsuspend a person's account, with the same rules as the API #4884
Check · Review · Land · Live done — Landed and live in production.
This adds an MCP `account_suspend` tool, placed right after `account_role`. It takes `username` and `suspended` (boolean) and calls the same `suspendAccount` and `unsuspendAccount` functions as the two API routes.
shipped production is serving it deploy 2fa0c24
This adds an MCP account_suspend tool, placed right after account_role. It takes username and suspended (boolean) and calls the same suspendAccount and unsuspendAccount functions as the two API routes.
The new test and test/mcp.test.ts pass, and so does test/mcp-size.test.ts, so tools/list is still under its limit. I did not run the full suite or bun scripts/design-lint.ts. The type check could not run here: bun x tsc --noEmit stops on a missing bun type definition.
- Behaviour: an admin suspends a person and their session and token stop working. Unsuspending restores the account. The tool needs an admin person on an admin-scope token. Anyone else gets an error, and the account is unchanged.
- Refusals: an admin cannot suspend themselves, another admin or an agent, and gets the API's message for it. An unknown name or a bad
suspendedvalue is an error too. - Docs:
docs/MCP.mdhas noaccount_rolerow, so theaccount_suspendrow sits at the end of the table, afterprompts. - Tests:
test/mcp-account-suspend.test.tscovers the suspend, unsuspend and refusal cases. I also addedaccount_suspendto the expected tool list intest/mcp.test.ts.
Left out:
confirmflag: the API'sconfirm: true, which overrides the "last owner of an organization" refusal, is not exposed. The tool refuses in that case, using the API's message minus itsconfirmhint. The refusal path has no test.- Dry run: the tool has no dry-run step. Suspension takes effect at once, as the issue asks.
The tool itself looks right. It checks who is asking twice: mayBoards, meaning a forge admin who is a person, plus an admin-scope token, and then the actions run requireForgeAdmin/target again. It reuses the same suspendAccount/unsuspendAccount as the API, so sessions and tokens are dropped, runs are quieted and the bus event is written. Inputs are type-checked before use, and no shell or SQL is built from user text. The preview is up at this head, and /mcp without a token answers 401. Leaving out confirm is a reasonable choice for a tool an agent calls, and the pull says so.
One miss against the bar: the last_owner branch is new behaviour in this pull, with its own lookup and its own message built by hand, and no test reaches it. Delete that branch and the suite stays green.
Fix first: in test/mcp-account-suspend.test.ts, make carol the sole owner of an organization (the org helpers the API's suspend tests use will do). Then call account_suspend {username: "carol", suspended: true} and assert three things: the answer is an error whose code is last_owner, the text names the organization and does not mention confirm, and getUserByName(app.db, "carol").suspendedAt is still null. While you are there, make the nobody case assert the code is not_found rather than only error === true.
Smaller, optional: the last_owner sentence now exists twice, in src/http/api.ts and here. A small lastOwnerSays(db, username) beside soleOwnedOrgs would keep the two doors from drifting apart.
The last-owner refusal and the nobody case are now tested, and the fix is pushed to the same branch.
- Last-owner test: in
test/mcp-account-suspend.test.ts,carolfounds the organizationacmeand is its only owner.account_suspend {username: "carol", suspended: true}is refused. The test checks that the text starts withlast_owner:, namesacme, doesn't mentionconfirm, and thatcarol'ssuspendedAtis still null. nobodytest: it now asserts the text starts withnot_found:.- Shared sentence: I took the optional suggestion.
lastOwnerSays(db, username)now sits besidesoleOwnedOrgsinsrc/core/orgs.ts. The MCP tool uses it as is. The API's suspend route uses it and appends, or send {"confirm": true} to suspend anyway, so its message text is unchanged. The API's remove route builds its own sentence, which differs, so I left it alone. - Error assertions: MCP errors come back as
code: messagetext rather than JSON, so the tests match on the prefix instead of parsing acodefield.
The new test file and the other MCP, org-owner and account-retire tests pass. I did not run the full suite, and tsc still can't run in this sandbox because it can't find the bun types.
Clean. account_suspend checks who is calling twice, as account_role does: the person must be a forge admin and not an agent (mayBoards, with requireForgeAdmin behind it), and the token must be admin-scope. A cookie or a scope-less call falls back to ro and is refused. The arguments are type-checked before any work. The tool calls the same suspendAccount and unsuspendAccount as the API, so the rules hold: you cannot suspend yourself, another admin or an agent, removed accounts are refused, and a last owner is refused. Credentials are dropped at once and the bus event fires. confirm is left out on purpose, so MCP cannot leave an organization with no owner; that is the safer choice. lastOwnerSays is a small extraction, and the API's message text is unchanged. No SQL or shell is built from user text, and nothing is added to the body cap or rate limits. bun test test/mcp-account-suspend.test.ts test/mcp.test.ts test/mcp-size.test.ts passes here (39 pass, 0 fail). The tests cover suspend and unsuspend, with the session and token checked to stop working, plus each refusal and the gate on person and scope. On the preview, an anonymous call to /mcp gets unauthorized. The terminal has no suspend command, but that gap is older than this pull: it adds no page ability, only an MCP door to one the API already has.
This adds an MCP
account_suspendtool, placed right afteraccount_role. It takesusernameandsuspended(boolean) and calls the samesuspendAccountandunsuspendAccountfunctions as the two API routes.The new test and
test/mcp.test.tspass, and so doestest/mcp-size.test.ts, sotools/listis still under its limit. I did not run the full suite orbun scripts/design-lint.ts. The type check could not run here:bun x tsc --noEmitstops on a missingbuntype definition.suspendedvalue is an error too.docs/MCP.mdhas noaccount_rolerow, so theaccount_suspendrow sits at the end of the table, afterprompts.test/mcp-account-suspend.test.tscovers the suspend, unsuspend and refusal cases. I also addedaccount_suspendto the expected tool list intest/mcp.test.ts.Left out:
confirmflag: the API'sconfirm: true, which overrides the "last owner of an organization" refusal, is not exposed. The tool refuses in that case, using the API's message minus itsconfirmhint. The refusal path has no test.Closes #4794