fix(mcp): honor context on stdio spawn and stdin write - #164
Conversation
NewStdio discarded its context and started exec.Command, and write checked ctx.Err() once then blocked on stdin.Write. A child that stopped reading stdin never reached Close(), so sync --source mcp hung through Ctrl-C. Spawn with CommandContext, fail canceled spawn immediately, and write stdin asynchronously so cancel can close the pipe and kill the child. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
|
🦞👀 Pull request received. I will update this pull request when review starts. |
|
Codex review: needs maintainer review before merge. Reviewed August 30, 2026, 7:51 PM ET / 23:51 UTC. ClawSweeper reviewWhat this changesThe PR makes MCP stdio child startup and blocked JSON-RPC stdin writes respond to context cancellation. Merge readinessKeep open: the PR uniquely repairs the current MCP stdio cancellation path and the reviewed head has no actionable correctness or security finding. Priority: P2 Review scores
Verification
How this fits togetherThe MCP sync source launches a configured MCP server over stdio, sends JSON-RPC requests, and normalizes returned Slack data into the archive pipeline. Cancellation must stop the child and unblock request writes so the CLI can exit. flowchart LR
A[CLI sync command] --> B[MCP sync source]
B --> C[Stdio MCP client]
C --> D[Child MCP process]
C --> E[JSON-RPC stdin request]
D --> F[Slack data response]
F --> B
E --> G[Context cancellation]
Before merge
Agent review detailsSecurityNone. Review metrics
Technical reviewBest possible solution: Land the narrow cancellation repair so a non-reading configured MCP subprocess cannot keep an operator's sync command alive after cancellation. Do we have a high-confidence way to reproduce the issue? Yes. The captured before/after terminal run exercises the public stdio client against a real non-reading child, and the introduced tests cover the same blocked-pipe and spawn-cancellation cases. Is this the best way to solve the issue? Yes. Propagating the existing sync context to process creation and making a blocked write cancellable directly fixes the owning stdio boundary without changing JSON-RPC framing or configuration. AGENTS.md: not found in the target repository. Codex review notes: model internal, reasoning high; reviewed against dcbc12d44837. LabelsLabel justifications:
EvidenceWhat I checked:
Likely related people:
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (3 earlier review cycles) |
|
Maintainer triage: recommend LAND. No source repair was needed. The docs/changelog follow-up is on I reproduced the blocked stdin path through the real built CLI. A synthetic stdio MCP server completes initialization, returns a 2 MiB pagination cursor from GOWORK=off go build -o /tmp/slacrawl-main ./cmd/slacrawl # main
GOWORK=off go build -o /tmp/slacrawl-164 ./cmd/slacrawl # PR head
python3 proof-cancel.py mcp /tmp/slacrawl-main /tmp/slacrawl-164Each invocation runs Codex autoreview returned scoped-clean for the source patch and docs follow-up (the skill's default P0 scope). Existing GitHub CI is green. This is real process-and-pipe proof with an injected failure, not a claim that a production Slack MCP server naturally returns such a cursor. |
Co-authored-by: Sebastien Tardif <sebtardif@ncf.ca>
Preserve all Unreleased fixes and maintenance notes after landing openclaw#159 and openclaw#163. Co-authored-by: Sebastien Tardif <sebtardif@ncf.ca>
steipete
left a comment
There was a problem hiding this comment.
Reviewed the integrated MCP stdio cancellation patch and docs/changelog follow-up. Focused mcpclient and slackmcp tests passed locally; the earlier synthetic built-CLI proof covers a full stdin pipe, SIGINT exit, and child cleanup. Codex autoreview found no actionable findings in its default P0 scope. Thanks @SebTardif.
|
Landed as 78545c2 with the docs/changelog follow-up. Thanks @SebTardif! Final head 925ecd4 passed |
What Problem This Solves
Fixes an issue where users running
slacrawl sync --source mcpwithtransport = stdiowould hang forever when the MCP child stopped reading stdin. Ctrl-C canceled the Go context, but spawn usedexec.Commandandstdin.Writeignored that cancel, so the CLI never reachedClose()and never killed the child.Why This Change Was Made
NewStdionow starts the child withCommandContextand fails immediately if the spawn context is already done. Stdin writes run in a goroutine and, on cancel, close the pipe and kill the child so a full pipe cannot park the caller. JSON-RPC framing is unchanged. This does not restack the desktop Node decoder bound (#163), the HTTP client timeout (#126), or the repeated-cursor guard (#159).User Impact
A stuck stdio MCP server no longer holds the CLI after cancel.
sync --source mcpcan return when the user stops the command, matching howprovider.Syncalready treats a hung provider child.Evidence
Call chain:
slacrawl sync --source mcpwithtransport = stdio->slackmcp.Sync->slackmcp.New->mcpclient.NewStdio->Initialize/CallToolText->write. The spawn usedexec.Command(context discarded) since MCP stdio landed in #46 on 2026-06-06 (84 days).writecheckedctx.Err()once, then blocked onstdin.Write.provider.Syncalready usesCommandContextand an async stdin write.Before the patch, a 2MiB stdin write to a child that never reads (
sleep) stayed blocked after a 400ms deadline:After the patch, the same public
NewStdio+CallToolTextpath against/bin/sleepreturns when the 400ms deadline fires, and an already-canceled spawn context does not start a child:Real behavior proof
Behavior or issue addressed: MCP stdio spawn and stdin write ignored cancel, so a child that stopped reading stdin parked
sync --source mcpthrough Ctrl-C.Real environment tested: macOS, Go 1.27.0. Patched slacrawl at
/tmp/oc-pr-slacrawl-F004. Child:/bin/sleep(never reads stdin). Proof binary built from./tmpproofagainst the patched module.Exact steps or command run after this patch: Built
/tmp/f004proof.binfrom the patched module and ranold-write(blockingstdin.Write), thennew-write(CallToolTextwith a 2MiB payload and a 400ms deadline) andnew-spawn(already-canceledNewStdiocontext).Evidence after fix: terminal output from the patched
NewStdio/CallToolTextbinary:Observed result after fix:
CallToolTextreturnedcontext deadline exceededin 403ms instead of staying blocked on a full stdin pipe.NewStdiowith a canceled context returnedcontext canceledimmediately and did not leave a sleep child running.What was not tested: A live Slack MCP npm server (
@modelcontextprotocol/server-slack) that hangs after a real tools/call, and a Ctrl-C against that server from an operator terminal.