If your MCP server shows as connected in Claude Code but exposes no tools, and its log says Failed to fetch tools: Request timed out, check the protocol version first. Since Claude Code 2.1.292 (released 2026-10-06), every install asks local stdio servers (servers Claude Code launches as a subprocess and talks to over standard input/output) for MCP protocol 2026-07-28. Version 2.1.295 (2026-10-08) does the same for claude.ai connectors (the remote integrations you add in your claude.ai account) on installs that fetch no feature flags. Setting MCP_PROTOCOL_NEGOTIATION=legacy puts every server back on the old handshake in one line.
In every report, “connected” did not mean “working”. The connection opens, the status panel says so, and the tool list never arrives. Four failure patterns are on record so far, each with its own log line. Two are server bugs: the silent subscriptions/listen stall and invalid ttlMs/cacheScope values. The other two are server replies that a Claude Code client quirk turns into a dead connection: the probe error reply and the unknown message ID.
What changed, and when
MCP (Model Context Protocol) is the protocol Claude Code uses to talk to tool servers. It runs on JSON-RPC, a message format where every reply carries the ID of the request it answers. 2026-07-28 is the newest revision of that spec, named by its date. Protocol negotiation is the step where client and server agree on which revision to speak. The probe is a server/discover request the client sends before the old initialize handshake. If the server answers it, the client switches to 2026-07-28. A server that doesn’t know the method should reply with a “method not found” error, and the client falls back to the old handshake.
The rollout came in stages. Claude Code’s v2 MCP client runtime (built on MCP TypeScript SDK 2.0) carries the new revision. Each line below is a default flipping, per the changelog and the docs:
| Version | Release date | What started negotiating 2026-07-28 by default |
|---|---|---|
| 2.1.274 | 2026-09-16 | Direct HTTP servers on Bedrock, Vertex, and Foundry (cloud-provider installs of Claude Code) and on installs with telemetry turned off; the changelog says other installs already negotiated it |
| 2.1.292 | 2026-10-06 | Local stdio servers, on every install including Bedrock, Vertex, and Foundry |
| 2.1.295 | 2026-10-08 | claude.ai connectors, on installs that fetch no feature flags |
Some installs got it earlier through a feature flag (a server-side switch Anthropic controls per install). The reporter of issue #97189 saw stdio servers flip to 2026-07-28 mid-afternoon on 2026-09-25, while the same servers had negotiated 2025-11-25 that morning.
Step 1: Read which protocol you got
Every MCP server gets its own JSONL log (one JSON object per line). The Connection established line records what was negotiated:
Connection established with capabilities: {"hasTools":true,...,"protocolEra":"modern","negotiatedProtocolVersion":"2026-07-28"}
Failed to fetch resources: Request timed out
Failed to fetch tools: Request timed out
protocolEra: "modern" together with 2026-07-28 means the new protocol. legacy with 2025-11-25 (or older) means the old initialize handshake. To pull the field out of every server log on macOS:
grep -ho '"negotiatedProtocolVersion\\*":\\*"[0-9-]*' \
~/Library/Caches/claude-cli-nodejs/*/mcp-logs-*/*.jsonl | sort | uniq -c
On my machine that printed a mix, which is normal while servers catch up:
2 "negotiatedProtocolVersion\":\"2024-11-05
312 "negotiatedProtocolVersion\":\"2025-11-25
451 "negotiatedProtocolVersion\":\"2026-07-28
On Windows, the logs live under %LOCALAPPDATA%\claude-cli-nodejs\Cache\<project>\mcp-logs-<server>\. /mcp (the server status panel) showing “connected” proves only that the connection opened. Every failure below happens after that line, so read the log, not the panel.
Step 2: Opt out while you fix it
MCP_PROTOCOL_NEGOTIATION is documented in the official env-var reference. Set it to legacy and Claude Code probes no servers, so they all connect on the old handshake. auto probes all three server types (HTTP, claude.ai connectors, stdio) regardless of the version defaults. Leave it unset and you get the version-by-version defaults in the table above. It needs Claude Code v2.1.221 or later and applies only on the v2 client runtime.
One session:
MCP_PROTOCOL_NEGOTIATION=legacy claude
Persistent, in ~/.claude/settings.json:
{
"env": {
"MCP_PROTOCOL_NEGOTIATION": "legacy"
}
}
In a comment on #97391, the author of issue #100504 reported that the setting took the related errors in their logs to zero, with a caveat: “it’s a static pin.” legacy holds every server on the old handshake, so a server you later upgrade to 2026-07-28 won’t get it until you remove the variable. Treat it as a workaround and take it out once the server is fixed.
Two more cases where legacy helps:
- Channel servers. The docs state that a channel server (one that pushes messages into your session) can’t deliver channel messages on
2026-07-28, so Claude Code doesn’t register it as a channel. Stdio channel servers get probed by default now. If your Claude Code channel stopped delivering, this is the switch to check. - Slow first connect. A stdio server that ignores the probe costs one slow connect. Since 2.1.292, Claude Code then remembers it for 7 days and connects it the old way.
legacyskips even that first wait.
Step 3: Match the log line to the cause
A server that lists all its tools when driven by hand can still fail under the new protocol. In #97189, #97391, and #97319, the reporters drove the same server by hand over plain JSON-RPC and got its full tool list back: 65 tools “immediately”, 81 in under 0.5 s, and 28 in under 100 ms. In #100504, the reporter captured the server’s raw reply with curl instead. The problem is in how server and client handle the new protocol’s first few requests.
| Log line | What happened | Fix |
|---|---|---|
Failed to fetch tools: Request timed out after 30 s, with subscriptions/listen in the traffic | Server acknowledged the subscription stream, then stopped answering anything on stdio | Upgrade the server (mcp-grafana: v1.6.1, 2026-09-28) |
negotiatedProtocolVersion: 2026-07-28 although initialize returned 2025-11-25, then Failed to fetch tools: Request timed out after 30 s | Probe error reply carried a non-standard message, and Claude Code logged the server as 2026-07-28 | Server: reply -32601 Method not found; you: legacy |
Invalid result for tools/list with paths ttlMs / cacheScope | List result failed Claude Code’s strict validation of the new cache fields | Server must send valid values (Roblox Studio patched in 0.740.19) |
Received a response for an unknown message ID, then STDIO connection dropped after 0s | Older HTTP server’s error reply had no matching request ID, so the connection was torn down | legacy; the client-side fix is still open |
Timeout: the server goes silent after subscriptions/listen
subscriptions/listen is the new way a client asks to be told when a server’s tool list changes. It is one long-lived request, replacing the old HTTP GET stream. In #97189, mcp-grafana acknowledged the listen request and then never answered tools/list or resources/list. The reporter’s verdict: “Root cause found, and it is server side.” They replayed the captured client traffic: with the subscriptions/listen line removed, tools/list returned 65 tools immediately. The cause was a bug in the mark3labs/mcp-go library: after acknowledging the listen request, the server froze and stopped answering anything on its stdio connection. mcp-grafana v1.6.1 fixed it.
If you’re stuck on a server built on an older mcp-go, the reporter’s stopgap was to filter that one request out before it reaches the server. The pipeline below replaces the server’s launch command in your MCP config (wrapped in sh -c '...'), so grep sits between Claude Code and the server:
grep --line-buffered -v 'method":"subscriptions/listen' | mcp-grafana --disable-write
With that request gone, the client cancels the pending listen on its own and lists the tools.
Timeout: a probe error reply picks the wrong protocol
server/discover is a new RPC that the 2026-07-28 spec makes mandatory: servers must use it to advertise which versions and capabilities they support. Claude Code sends it as a probe before initialize. A server that predates the spec should answer with JSON-RPC error -32601, the standard “method not found” code.
In #97391, mcp-grafana 1.6.0 answered with the right code but a custom message: "server/discover" requires protocol version 2026-07-28 or later. Then initialize returned 2025-11-25. Claude Code still logged 2026-07-28, and tools/list timed out after exactly 30 seconds. The reporter put a small proxy in front of the server that changed only the message to Method not found. With the code unchanged, the log flipped to legacy and all 81 tools loaded. That suggests, though the issue doesn’t confirm it, that the client classifies the reply by its message text, not its code. The issue was still open on 2026-10-13.
Server authors can avoid it today: answer an unknown server/discover with {"code": -32601, "message": "Method not found"}.
Validation: ttlMs and cacheScope
The new spec requires two cache fields on results from tools/list, prompts/list, resources/list, resources/read, and resources/templates/list. ttlMs is a freshness hint in milliseconds that tells the client how long it may cache the result. cacheScope is "public" or "private" and says whether a shared proxy may cache it. Both get strict validation. In #97319, Roblox Studio’s MCP bridge returned values that failed the check (ttlMs wasn’t a number, cacheScope was neither allowed value). Claude Code retried at 250, 500, and 1000 ms, then gave up with tool_count: 0 for the rest of the session. Roblox shipped a Studio update that fixed it. Other users in the thread unblocked themselves by setting MCP_PROTOCOL_NEGOTIATION=legacy or by downgrading Claude Code to 2.1.280.
Dropped connection: Received a response for an unknown message ID
This one hits older Streamable HTTP servers (MCP over HTTP POST requests), usually bridged to stdio through mcp-remote, a small local adapter that lets a stdio-only client reach a remote server. In #100504, servers built on the official MCP Python SDK rejected the probe with -32600 Bad Request: Missing session ID. That rejection is correct, since no session exists yet. But the SDK stamps the reply with the ID "server-error" instead of echoing the request’s ID. A JSON-RPC reply must echo the request’s ID so the client can pair them. When a reply’s ID matches nothing, Claude Code’s client treats it as a connection error and closes a transport that had connected milliseconds earlier. tools/list then waits out the full timeout on a dead pipe.
The reporter counted 132 STDIO connection dropped after 0s lines and 132 unknown message ID lines in a month of logs, across all 15 of their servers. By the reporter’s estimate, roughly 20% of connects ended with no tools, each after a 30-120 s stall. The issue was still open on 2026-10-13, so legacy is the fix for now.
What server authors should change
If you maintain an MCP server, these items from the 2026-07-28 spec changelog are the ones Claude Code now exercises. One needs a definition first: MRTR (Multi Round-Trip Requests) is the new way a server asks the user or client for more input. The server no longer sends its own request back up the connection. It returns an interim result saying what it needs, and the client calls again with the answer. Every result now carries resultType: "complete" or "input_required".
| Change | What to do |
|---|---|
initialize handshake and Mcp-Session-Id removed | Every request carries its protocol version and client capabilities in _meta (a metadata object on each request); a version you don’t support gets UnsupportedProtocolVersionError (code -32022) |
server/discover required | Implement it; until you do, answer it with a plain -32601 Method not found |
ttlMs + cacheScope required on list results | Return a number and "public" or "private" |
subscriptions/listen replaces the GET stream | Acknowledge it and keep serving other requests on the same connection |
| Multi round-trip requests (MRTR) replace server-initiated requests | Instead of sending elicitation/create, sampling/createMessage, or roots/list, return resultType: "input_required" with inputRequests; the client retries with inputResponses |
| Sessions removed | Keep cross-call state in handles: IDs the server creates and the client passes back as ordinary tool arguments |
| Roots, Sampling, Logging deprecated | Still work during a minimum twelve-month window; new code should take paths as tool parameters, call the LLM provider directly, and log to stderr or OpenTelemetry (an open tracing and metrics standard) |
Next step
As of 2026-10-13, #97391, #97319, and #100504 are still open on GitHub, and every fresh install now asks stdio servers for 2026-07-28. Run the grep from Step 1 against your MCP logs now, before you file a bug against the wrong project. If any server shows 2026-07-28 next to a fetch failure, add the legacy env block, find its row in the table, and file the fix with the server’s maintainer. Once the fix ships, take the variable out so you get 2026-07-28 on servers that support it. For what loaded MCP tools cost you in context once they work, see the MCP section of Claude Code’s built-in tools and the startup tax note.