MCP client drops stdio transport on unknown progressToken instead of ignoring it
Draft: Claude Code CLI MCP client tears down stdio transport on unknown progressToken
Target repo: anthropics/claude-code
Draft author: Yale (bajman) — drafted 2026-04-14
---
Title
MCP client drops stdio transport when receiving a notifications/progress for an unknown token, instead of ignoring it
Body
Summary
When a stdio MCP server emits a notifications/progress message whose progressToken is no longer in the client's in-flight request map, Claude Code CLI's MCP client treats the unknown-token notification as a fatal stdio transport error, logs Closing transport (stdio transport error: Error), tears down the pipe, and respawns the server process. This happens at least once per tool call against any server that emits a terminal progress marker (progress >= total) after dispatching its response, because terminal progress races the tool-call response on the wire.
The MCP spec progress notifications section allows progress notifications to arrive at any time during a long-running request, but it does not specify what a client should do when a progress notification arrives for a token it does not recognize. The pragmatic behavior — adopted by most MCP SDK implementations — is to drop the notification with a debug/warning log and continue, because unknown tokens are an expected race condition and cannot by themselves indicate a transport corruption.
Claude Code's current behavior is strictly over-eager and causes observable user-facing failures.
Environment
- Claude Code CLI version: (fill in —
claude --version) - OS: macOS Darwin 25.5.0
- Node: (fill in)
- MCP transport: stdio
- Affected servers (observed):
fmp-modeling-prep(custom Python server, ai-toolkit); likely any server that emitsnotifications/progresswithprogress == total.
Repro
- Run any stdio MCP server that emits a terminal progress notification after dispatching its response.
- From Claude Code, issue a tool call against that server carrying a
_meta.progressTokenin the request. - The server's response arrives and is consumed.
- The terminal progress notification (same token) arrives next.
- Claude Code closes the transport and respawns the server. If a second tool call is in flight to the same server, it dies with "MCP server disconnected" mid-response.
Observed log (from ~/Library/Caches/claude-cli-nodejs/-Users-yaleleber/mcp-logs-fmp-modeling-prep/2026-04-14T05-39-09-548Z.jsonl)
{"debug":"Calling MCP tool: fmp_sec_filings_sec_filings_8k","timestamp":"2026-04-14T05:41:34.761Z"}
{"debug":"STDIO connection dropped after 139s uptime","timestamp":"2026-04-14T05:41:34.880Z"}
{"debug":"Connection error: Received a progress notification for an unknown token: {\"method\":\"notifications/progress\",\"params\":{\"_meta\":{\"requestId\":\"5\"},\"progress\":1,\"total\":1,\"message\":\"Completed fmp.sec_filings.sec_filings_8k\",\"progressToken\":5}}","timestamp":"2026-04-14T05:41:34.880Z"}
{"debug":"Closing transport (stdio transport error: Error)"}
{"debug":"Tool 'fmp_sec_filings_sec_filings_8k' completed successfully in 119ms"}
{"debug":"Starting connection with timeout of 30000ms"}
Note that the tool call did complete successfully — the response is in the client. The transport is nonetheless torn down purely because of the trailing progress notification.
Expected behavior
When the MCP client receives a notifications/progress message for a progressToken that is not in its in-flight map, it should:
- Log the event at debug level with the token and message, and
- Drop the notification, and
- Leave the transport open.
This matches the behavior of most reference MCP SDK implementations and follows the general MCP design principle that notifications are best-effort and must not be load-bearing for transport health.
Proposed fix
In the stdio-transport MCP client's message dispatcher, when a notifications/progress arrives and progressToken is not present in the active-request map, swallow the notification instead of raising. Example (pseudocode):
case "notifications/progress": {
const token = msg.params?.progressToken;
const handler = this.progressHandlers.get(token);
if (!handler) {
this.log.debug(
`Received progress notification for unknown token ${token}; ` +
`dropping (token may have completed already).`
);
return; // do NOT throw; do NOT close the transport
}
handler(msg.params);
return;
}
Related context
This was triggered on my side by a bug in my own custom MCP server (it was emitting a terminal progress == total notification after the response — that is a server bug and I have fixed it on my end). But the CLI's reaction — tearing down the entire transport — means a single malformed notification from any third-party MCP server can silently break a user's tool-call pipeline. The client-side fix is defense-in-depth against a wide class of well-meaning-but-buggy MCP servers.
Happy to submit a PR if you point me at the dispatcher file.
---
Ready to file
If this looks right, post with:
gh issue create \
--repo anthropics/claude-code \
--title "MCP client drops stdio transport on unknown progressToken instead of ignoring it" \
--body-file /Users/yaleleber/Code/ai-toolkit/docs/claude-code-mcp-progress-token-issue.mdThis issue has 3 comments on GitHub. Read the full discussion on GitHub ↗