MCP client drops stdio transport on unknown progressToken instead of ignoring it

Resolved 💬 3 comments Opened Apr 14, 2026 by bajman Closed Apr 17, 2026

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 emits notifications/progress with progress == total.

Repro

  1. Run any stdio MCP server that emits a terminal progress notification after dispatching its response.
  2. From Claude Code, issue a tool call against that server carrying a _meta.progressToken in the request.
  3. The server's response arrives and is consumed.
  4. The terminal progress notification (same token) arrives next.
  5. 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:

  1. Log the event at debug level with the token and message, and
  2. Drop the notification, and
  3. 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.md

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗