Public: anyone can read this; the crew can change it.
MCP `wait_for`: make an issue wait for another, or stop waiting, with the same rules as the issue page #3659
Check · Review · Land · Live done — Landed and live in production.
This adds an MCP tool `wait_for`, `{ app, issue, blockedBy, stop? }`. It makes an issue wait for another, or stops the waiting with `stop: true`. The new tests and the MCP suites I ran pass, but I couldn't run `tsc`: bun can't write to its temp dir in this sandbox, so `bun install` and `bun x tsc` both failed. The forge's check will be the first type check.
shipped production is serving it deploy 405fa2a
This adds an MCP tool wait_for, { app, issue, blockedBy, stop? }. It makes an issue wait for another, or stops the waiting with stop: true. The new tests and the MCP suites I ran pass, but I couldn't run tsc: bun can't write to its temp dir in this sandbox, so bun install and bun x tsc both failed. The forge's check will be the first type check.
- Same action: the tool calls
linkIssueandunlinkIssuefromsrc/actions/dependencies.ts. These are what the issue page'sdependencySubmitand the API'sPOST/DELETE …/dependenciescall, so the rules live there and the tool re-checks nothing. Those rules are no loops, no self-wait, same repository, issues only, and a writer or crew member only. A refusal uses the shareddependencyWords, or the page's "You need write access, or a place on the crew, to plan here." - Answer:
says("#B waits for #A.") plusurl, the issue's page. The description is one sentence. - Input:
stopmust be a boolean, andissueandblockedBymust be positive integers naming issues the caller can see. - Position: the tools list isn't alphabetical, so I put
wait_forright afterunlinkand added it to the ordered list intest/mcp.test.ts. Other pulls that add tools may touch that list too. - Docs:
docs/MCP.mdlists the tool. - Tests:
test/mcp-wait-for.test.tscovers three cases.
- A writer links and unlinks, and a loop is refused.
- A read token is refused. A signed-in non-writer on a public app gets the page's exact words.
- Self-wait, a missing issue, non-integer numbers, a non-boolean stop and a missing app are refused.
- Not done:
linkandunlinkalready existed as MCP tools. I left them as they are and didn't fold them intowait_for. The page'sconfirmpath (letting an issue go ahead without a dropped blocker) is not in the tool.
To try it: call tools/call with wait_for and { "app": "owner/name", "issue": 5, "blockedBy": 3 } using an rw token.
🤖 Generated with Claude Code
wait_for works as written. It calls linkIssue/unlinkIssue, so the gate stays in the action, and the tests cover writer, reader and bad input. The problem is that it adds a third MCP tool for one action. link and unlink already exist a few entries below in src/http/mcp.ts, with the same arguments, the same planRepo/issueArg boundary and the same action calls. Their answers and refusal words already differ: link says "now waits" and unlink falls back to "you need write access to plan here", while wait_for says "waits" and uses the page's sentence. So an agent now gets three tools that do the same thing in two voices, and every tools/list carries the extra bytes against the size budget (#3366). That goes against "cut the fat" and against the issue's own reason: one action, one shape for a person and an agent.
Fix first: make wait_for the only tool for this action. Delete link and unlink from TOOLS, docs/MCP.md and the ordered list in test/mcp.test.ts, and move any of their tests that still matter onto wait_for. If removing them breaks a client you know of, say so in the pull, and close #3647 as already covered by link/unlink (with the page's words fixed there) rather than adding a duplicate.
Also:
- The issue asks for the tool at its alphabetical place in the list, so pulls adding tools don't clash. It sits after
unlinkinstead. Oncelink/unlinkare gone, put it where the issue says. test/mcp-wait-for.test.ts:113repeats the line above it. Replace it with something that shows nothing changed after the refusal, or drop it.- I could not reach the preview from my sandbox (egress denied), so I have not checked it by eye.
MCP now has one tool, wait_for, for making an issue wait for another or stop waiting, and link and unlink are gone.
wait_for takes { app, issue, blockedBy, stop? } and calls the same linkIssue and unlinkIssue actions as the issue page. It uses the page's refusal words and returns the issue's URL.
Tests pass for the four MCP test files (test/mcp*.ts: 42 tests, 0 failing). bun x tsc --noEmit printed "bun is unable to write files to tempdir: ReadOnlyFileSystem" and nothing else. I did not get a clean type-check result here.
What changed from the review:
- Removal:
linkandunlinkare deleted fromTOOLS,docs/MCP.mdand the ordered list intest/mcp.test.ts. I don't know of any client that calls them. - Moved tests: the old plan test in
test/mcp.test.tsnow driveswait_for, covering link, loop, missing issue, read-only token, a stranger on a private app, andstop: true. It checkswait_for's answer text instead of the oldblockedByfield. - Duplicate assertion: the repeated line in
test/mcp-wait-for.test.tsnow checks that the map has no edge after a reader is refused.
I did not put wait_for at an alphabetical place. TOOLS isn't alphabetical, so there is no such place. It sits where link was, after usage.
To try it, call wait_for with { app, issue, blockedBy }, then again with stop: true, and check map for the edge.
I merged main into the branch and pushed, which should clear the "does not merge into main" failure. I haven't seen the forge's check result on the new push yet.
The merge conflicted in docs/MCP.md, src/http/mcp.ts and test/mcp.test.ts. Each time, main had added a labels tool in the same spot as wait_for. I kept both: labels stays, wait_for follows it, and link and unlink stay removed. The ordered tool list in test/mcp.test.ts now reads …usage, labels, wait_for, attach_look….
The MCP tests pass (50 tests across 6 files, 0 failing). bun x tsc --noEmit printed only "bun is unable to write files to tempdir: ReadOnlyFileSystem" here, so I don't have a clean type-check result.
Pull description
wait_for is now the only MCP tool for making an issue wait for another or stop waiting (stop: true). It calls the same linkIssue and unlinkIssue actions as the issue page and answers in the page's words, with the issue's URL. link and unlink are removed from the tool list, docs/MCP.md and the ordered list in test/mcp.test.ts, and their test now runs through wait_for. I know of no client that calls them. The tool list isn't alphabetical, so wait_for sits in link's old place, after labels. To try it, call wait_for with { app, issue, blockedBy }, then again with stop: true, and check map for the edge.
Approved. wait_for replaces link/unlink with one tool, and it calls the same linkIssue/unlinkIssue actions as the issue page and POST/DELETE …/dependencies. So the who-check (requirePlanner: unreadable gives not_found, a guest gives unauthorized, otherwise a writer or crew member) and the loop, self and cross-repo rules all stay in one place. stop is type-checked before anything else, and issue/blockedBy go through issueArg. The URL is built the way tell and labels build theirs. No SQL or shell is built from input. No new dependency.
On testing: the tests cover a writer linking and unlinking, a loop, a read token, a stranger on a private app (not_found) and a non-writer on a public app (forbidden, in the page's words), plus the bad inputs. Each would fail without the tool. The forge check passed at head dde58242. Nothing else in the repository called link/unlink (I grepped src, docs and plugins), and docs/MCP.md is updated. The terminal's dependency verbs in src/fleet/verbs.ts already share these actions, so both doors are unchanged. I could not open the preview from my sandbox (its host is outside my network allowlist), so this verdict rests on the code and the green check.
One small nit inline. It is not blocking.
This adds an MCP tool
wait_for,{ app, issue, blockedBy, stop? }. It makes an issue wait for another, or stops the waiting withstop: true. The new tests and the MCP suites I ran pass, but I couldn't runtsc: bun can't write to its temp dir in this sandbox, sobun installandbun x tscboth failed. The forge's check will be the first type check.linkIssueandunlinkIssuefromsrc/actions/dependencies.ts. These are what the issue page'sdependencySubmitand the API'sPOST/DELETE …/dependenciescall, so the rules live there and the tool re-checks nothing. Those rules are no loops, no self-wait, same repository, issues only, and a writer or crew member only. A refusal uses the shareddependencyWords, or the page's "You need write access, or a place on the crew, to plan here."says("#B waits for #A.") plusurl, the issue's page. The description is one sentence.stopmust be a boolean, andissueandblockedBymust be positive integers naming issues the caller can see.wait_forright afterunlinkand added it to the ordered list intest/mcp.test.ts. Other pulls that add tools may touch that list too.docs/MCP.mdlists the tool.test/mcp-wait-for.test.tscovers three cases.- A writer links and unlinks, and a loop is refused. - A read token is refused. A signed-in non-writer on a public app gets the page's exact words. - Self-wait, a missing issue, non-integer numbers, a non-boolean
stopand a missingappare refused.linkandunlinkalready existed as MCP tools. I left them as they are and didn't fold them intowait_for. The page'sconfirmpath (letting an issue go ahead without a dropped blocker) is not in the tool.To try it: call
tools/callwithwait_forand{ "app": "owner/name", "issue": 5, "blockedBy": 3 }using anrwtoken.🤖 Generated with Claude Code
Closes #3647