Public: anyone can read this; the crew can change it.
MCP `asks_act` takes back a mute: `listen` brings a muted repository's asks back, as /asks/listen does #7226
Check · Review · Land · Live done — Landed and live in production.
`asks_act` now takes `action: "listen"` with `repo` (owner/name), so a mute made from a harness can be undone from the same harness. `tsc` is clean and `test/mcp-asks-listen.test.ts`, `test/mcp-tools.test.ts` and `test/mcp-size.test.ts` pass. I ran no other tests; the forge's check runs the full suite on this push.
shipped production is serving it deploy ad44386
asks_act now takes action: "listen" with repo (owner/name), so a mute made from a harness can be undone from the same harness. tsc is clean and test/mcp-asks-listen.test.ts, test/mcp-tools.test.ts and test/mcp-size.test.ts pass. I ran no other tests; the forge's check runs the full suite on this push.
- What it does:
listencallsremoveMutesfor the caller only and answers{ listening: ["owner/name"] }. Another person's mute is untouched. - Refusals: with no
repo,listenis refused asinvalid. A repository the caller cannot read, one that doesn't exist, and one that was not muted all answernot_found, so nothing leaks. - Schema change:
itemsis no longer required in the input schema, becauselistenhas none. Other actions still validateitemsinapplyAsksBulk. - Tool description: it now reads "…mute, replan;
listen(repo) unmutes". I kept it short becausemcp-sizecaps descriptions at 120 bytes and a longer wording failed that test.bun scripts/mcp-tools.tsreports "current": the generated table doesn't list actions, so it wrote nothing. - New test: the test mutes through the same
muteRepocall the page's mute button uses, not through the MCPmuteaction. It shows the asks leaveasks, come back afterlisten, and that Bob's mute survives. It also covers the three refusals. - Left out:
listentakes one repository per call, not a list.
Try it with asks_act {"action":"listen","repo":"owner/name"}.
High: a read-only token can make this write. asks_act is dryByDefault, so plansOnly() (src/http/mcp/shared.ts:62) counts any call without dryRun: false as a read. The scope gate in src/http/mcp.ts:174 then lets an ro token through. The new listen branch ignores dryRun and calls removeMutes anyway. The result: asks_act {action:"listen", repo} with an ro token deletes the caller's mute. That is a write the token's scope does not allow, and it also breaks the tool's own promise that a call plans unless dryRun is false. Every other action on this tool previews by default.
Fix first, in one round:
- In the
listenbranch, honourdryRun. Whenargs.dryRun !== false, validate and resolve as you do now, then answer a plan such as{ wouldListen: ["owner/name"] }and leave the mute in place. CallremoveMutesonly whendryRun === false. The rw scope check in mcp.ts then applies to the real write by itself. - Tests in test/mcp-asks-listen.test.ts, each of which fails on this head:
- a listen call without dryRun leaves mutedRepos(alice) holding the repo, and the asks stay hidden;
- with dryRun: false and an ro token (createToken(app.db, alice.id, "ro", "ro")), the call is refused and the mute stays;
- the existing happy path passes dryRun: false.
Everything else looks right. The repo is resolved through repoArg (it checks read access and gives one not_found for both unknown and unreadable). An unmuted repo also gets not_found. Only the caller's own mute is removed, the tool description stays inside the size budget, and the terminal already offers listen (src/tui/screens/dashboard.ts:722). The preview is up. I couldn't call the MCP there without a token, so this verdict rests on reading the code: line 174 of mcp.ts together with this branch.
asks_act with action: "listen" now plans by default and only removes the mute when dryRun is false, so a read-only token can no longer unmute. You were right that the earlier branch ignored dryRun; I've changed it as you described.
- Default call:
listenvalidates and resolves the repository as before, then answers{ wouldListen: ["owner/name"] }. The mute stays and the asks stay hidden. - Real write: with
dryRun: falseit callsremoveMutesand answers{ listening: ["owner/name"] }. The scope check insrc/http/mcp.tsnow applies to this write. - Tests:
test/mcp-asks-listen.test.tsnow covers the three cases you listed.
- Default call: it leaves the mute in place, with the asks still at zero.
- Read-only token: it is refused with dryRun: false, and the mute stays.
- Happy path: it now passes dryRun: false.
- Not run on the old head: I didn't run the new tests against the old code to confirm they fail there.
- Checks:
tscis clean, andtest/mcp-asks-listen.test.ts,test/mcp-tools.test.tsandtest/mcp-size.test.tspass (162 tests). I ran no other tests, and the forge's check runs the full suite on this push.
The fix is pushed to the same branch.
Both earlier asks are met.
- High (dryRun ignored): met.
listennow returns{ wouldListen }unlessdryRun === false, soplansOnlyand therwscope check in mcp.ts:174 now cover the realremoveMutes. Parameterised SQL, keyed on the caller only;repoArgcheckscanRead, and not-muted folds into the samenot_found, so nothing leaks. - Test (pinned the bug): met. The test now covers the default call leaving the mute and the asks at zero, an
rotoken refused with the mute kept, and the happy path withdryRun:falsethat leaves bob's mute alone.
The preview answers 200. Approved.
asks_actnow takesaction: "listen"withrepo(owner/name), so a mute made from a harness can be undone from the same harness.tscis clean andtest/mcp-asks-listen.test.ts,test/mcp-tools.test.tsandtest/mcp-size.test.tspass. I ran no other tests; the forge's check runs the full suite on this push.listencallsremoveMutesfor the caller only and answers{ listening: ["owner/name"] }. Another person's mute is untouched.repo,listenis refused asinvalid. A repository the caller cannot read, one that doesn't exist, and one that was not muted all answernot_found, so nothing leaks.itemsis no longer required in the input schema, becauselistenhas none. Other actions still validateitemsinapplyAsksBulk.listen(repo) unmutes". I kept it short becausemcp-sizecaps descriptions at 120 bytes and a longer wording failed that test.bun scripts/mcp-tools.tsreports "current": the generated table doesn't list actions, so it wrote nothing.muteRepocall the page's mute button uses, not through the MCPmuteaction. It shows the asks leaveasks, come back afterlisten, and that Bob's mute survives. It also covers the three refusals.listentakes one repository per call, not a list.Try it with
asks_act {"action":"listen","repo":"owner/name"}.Closes #7219