Claude Desktop (Windows): stdio MCP `tools/call` silently dropped after successful handshake — host-side dispatch never delivers the call to the subprocess

Resolved 💬 4 comments Opened May 19, 2026 by BorekSaheli Closed May 19, 2026

Summary

On Claude Desktop for Windows, any stdio MCP server's tools/call is silently
dropped before reaching the subprocess. The handshake (initialize +
tools/list + resources/list) completes successfully — tools appear in the
LLM's available-tools list (Claude in chat confirms it can see them) — but
when the LLM emits a tool_use for any of those tools, the request never
arrives at the stdio subprocess. The client surfaces a ~4-minute timeout
error claiming "the local MCP server was unresponsive", while the subprocess
log shows zero tools/call entries during that window.

This is not the same as #60224 (slow initialize causing the tool list
to be dropped from ToolSearch). Here the tool list registers correctly and
is visible to the LLM; the break is one stage downstream — between the LLM
emitting tool_use and Claude Desktop forwarding it as JSON-RPC tools/call
to the subprocess.

A trivial 60-line, zero-dependency stdio server reproduces the bug, ruling
out SDK / slow-init / dependency-related causes.

Environment

  • App: Claude Desktop (Windows)
  • Versions reproduced on: 1.7196.3, 1.7196.0, 1.6608.0 (all via winget downgrade)
  • OS: Windows 11 Enterprise (Build 26200)
  • MCP server type: stdio
  • CCD version logged: 2.1.142
  • Repros on: fresh install with no userdata, and restored userdata backup
  • Network: works fine for tools/list traffic — no general transport issue

Minimal repro (60 lines, no dependencies)

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "hello-test": {
      "command": "C:\\Program Files\\nodejs\\node.exe",
      "args": ["C:\\path\\to\\server.mjs"]
    }
  }
}

server.mjs — single trivial tool, no I/O, ~2 ms handshake:

import { createInterface } from 'node:readline';
const rl = createInterface({ input: process.stdin });
const send = (m) => process.stdout.write(JSON.stringify(m) + '\n');
const TOOL = { name: 'hello_test', description: 'Trivial diagnostic tool.', inputSchema: { type: 'object', properties: {} } };
rl.on('line', (line) => {
  if (!line.trim()) return;
  const msg = JSON.parse(line);
  const handlers = {
    initialize: () => ({ protocolVersion: msg.params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: 'hello-test', version: '0.1.0' } }),
    'tools/list': () => ({ tools: [TOOL] }),
    'tools/call': () => ({ content: [{ type: 'text', text: 'It works.' }] }),
  };
  const fn = handlers[msg.method];
  if (fn && msg.id != null) send({ jsonrpc: '2.0', id: msg.id, result: fn() });
});
process.stderr.write('hello-test ready over stdio\n');

Restart Claude Desktop. Ask Claude to invoke hello_test.

Expected: tool returns "It works." within seconds.
Actual: ~4-minute client-side timeout; subprocess log shows the handshake completed but no tools/call was ever delivered.

Server-log evidence

2026-05-19T13:49:39.229Z [hello-test] Server started and connected successfully
2026-05-19T13:49:39.760Z [hello-test] Message from client: {"method":"initialize",...,"id":0}
2026-05-19T13:49:39.763Z [hello-test] Message from server: {"jsonrpc":"2.0","id":0,"result":...}     # 3 ms
2026-05-19T13:49:40.052Z [hello-test] Message from client: {"method":"notifications/initialized",...}
2026-05-19T13:49:40.056Z [hello-test] Message from client: {"method":"tools/list",...,"id":1}
2026-05-19T13:49:40.058Z [hello-test] Message from server: {"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"hello_test",...}]}}
# user asks Claude to invoke hello_test at ~13:50 UTC
# (4 min timeout, no further entries)

grep "tools/call" mcp-server-hello-test.log0 matches.

What I ruled out

| Hypothesis | Tested | Result |
|---|---|---|
| Specific to one MCP server | Reproduces on 3 distinct servers (incl. trivial hello-test) | ❌ Not server-specific |
| Slow initialize (sibling of #60224) | Handshake completes in 2-5 ms on hello-test | ❌ Not init-timeout |
| Claude Desktop binary version regression | Reproduces on 1.7196.3, 1.7196.0, 1.6608.0 via winget downgrade | ❌ Same bug on older binaries |
| Corrupted local userdata | Repro on fresh install with empty %APPDATA%\Roaming\Claude\ | ❌ Persists across wipe |
| Corrupted MCP cache (mcp-needs-auth-cache.json) | Deleted manually | ❌ No change |
| Orphan Code-tab session files | All deleted; tested with empty claude-code-sessions/ | ❌ No change |
| Config path / command typo | Both tools/list and tools/call would fail if so; only tools/call fails | ❌ Path/command are fine |
| Corporate proxy interference | All other MCP traffic and Anthropic API calls succeed | ❌ Network layer fine |

Correlated host-side anomalies

%APPDATA%\Roaming\Claude\logs\main.log shows the structured-log line:

[LocalMcpServerManager] Connected to <server> (N tools)

…fires correctly on early sessions today (07:51, 08:58, 09:03, 09:32, 09:56)
and stops firing entirely after a triggering event at 10:11:01, despite
the per-server logs continuing to show successful handshakes on every
subsequent launch.

The triggering event was a Code-daemon subprocess crash:

2026-05-19 10:11:01 [error] Session local_e94d8111-... query error:
  Claude Code process exited with code 1073807364
    at BPi.getProcessExitError (...\app-1.7196.3\resources\app.asar\.vite\build\index.js:406:8122)
    at ChildProcess.i (...\index.js:406:11166)

(Exit 1073807364 = 0x40010004 = DBG_TERMINATE_PROCESS.) The crash
occurred while trying to spawn the bundled Code daemon for an archived Code
session whose cwd no longer existed on disk. Triggered by a Sentry.caught
flood (50+ entries in two seconds), after which LocalMcpServerManager
never reports Connected (N tools) again — but per-server handshakes
continue completing successfully. Tool dispatch is broken from that moment
on, persistently, across launches / versions / a full uninstall +
reinstall + userdata wipe.

This suggests the degraded state is server-side / account-tied: a fresh
Claude Desktop install with empty local data reproduces the same tools/call
black-hole behaviour. Local-state interventions (cache wipe, session-file
delete, version downgrade, full reinstall) do not recover dispatch.

Hypothesis

The host's tool_use → tools/call dispatcher uses a registry whose
populate-once code path is gated on LocalMcpServerManager.Connected events.
After the 1073807364 cascade the manager logs Closing all (0 servers) on
every quit cleanup and never re-runs whatever code emits Connected (N tools)
— so for the rest of the install's lifetime, the registry has the tools
listed (tools/list works → LLM sees them) but no entry in the dispatch
table (LLM-emitted tool_use for those names has no route, silently
discarded, eventually timing out client-side).

If that mapping is mirrored to account-side state, it explains why fresh
installs inherit the bug.

Suggested fixes (any of these would resolve it)

  1. Make tool_use → tools/call dispatch fall back to looking up the MCP

server by name from the tools/list registry when the per-tool dispatch
table has no entry. Currently the silent drop suggests it doesn't.

  1. Emit [LocalMcpServerManager] Connected (N tools) independent of the

Code-daemon process state — the protocol-level handshake on the
subprocess is what should drive this event, not whatever component
crashed at 1073807364.

  1. Add a recovery path that rebuilds the dispatch table from the current

handshake state on every fresh launch, so a transient crash doesn't
permanently brick the host for the account.

  1. Surface the missing-dispatch-route condition as a visible error in the

chat (rather than a 4-min timeout with no diagnostic), so users can tell
the difference between "server hung" and "host didn't route the call".

Related

  • #60224 — sibling bug at one stage upstream (tool list dropped from

ToolSearch when initialize is slow). Same area (area:mcp), same
platform (platform:windows), different precise failure mode.

View original on GitHub ↗

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