Skip to content

Browse catalog

Search catalog

[READY] Type a title, tag, or description.

MCP

MCP connected, zero tools: Claude Code's 2026-07-28 switch

AUTHOR
Bartłomiej Krupa
PUBLISHED
2026.10.13
UPDATED
2026.10.13
READ_TIME
8 min

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:

VersionRelease dateWhat started negotiating 2026-07-28 by default
2.1.2742026-09-16Direct 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.2922026-10-06Local stdio servers, on every install including Bedrock, Vertex, and Foundry
2.1.2952026-10-08claude.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. legacy skips 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 lineWhat happenedFix
Failed to fetch tools: Request timed out after 30 s, with subscriptions/listen in the trafficServer acknowledged the subscription stream, then stopped answering anything on stdioUpgrade 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 sProbe error reply carried a non-standard message, and Claude Code logged the server as 2026-07-28Server: reply -32601 Method not found; you: legacy
Invalid result for tools/list with paths ttlMs / cacheScopeList result failed Claude Code’s strict validation of the new cache fieldsServer must send valid values (Roblox Studio patched in 0.740.19)
Received a response for an unknown message ID, then STDIO connection dropped after 0sOlder HTTP server’s error reply had no matching request ID, so the connection was torn downlegacy; 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".

ChangeWhat to do
initialize handshake and Mcp-Session-Id removedEvery 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 requiredImplement it; until you do, answer it with a plain -32601 Method not found
ttlMs + cacheScope required on list resultsReturn a number and "public" or "private"
subscriptions/listen replaces the GET streamAcknowledge it and keep serving other requests on the same connection
Multi round-trip requests (MRTR) replace server-initiated requestsInstead of sending elicitation/create, sampling/createMessage, or roots/list, return resultType: "input_required" with inputRequests; the client retries with inputResponses
Sessions removedKeep cross-call state in handles: IDs the server creates and the client passes back as ordinary tool arguments
Roots, Sampling, Logging deprecatedStill 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.

FAQ

How do I make Claude Code use the old MCP protocol?
Set the environment variable `MCP_PROTOCOL_NEGOTIATION=legacy`, either in your shell (`MCP_PROTOCOL_NEGOTIATION=legacy claude`) or in the `env` block of `settings.json`. Claude Code then stops probing servers for protocol 2026-07-28 and connects every server with the older `initialize` handshake. It works on the v2 MCP client runtime from Claude Code v2.1.221; it became relevant for stdio servers in 2.1.292 and claude.ai connectors in 2.1.295, when those defaults flipped. It pins every server, so you also miss 2026-07-28 on servers that support it until you remove it.
My MCP server says connected but has no tools after a Claude Code update - why?
Most likely Claude Code negotiated MCP protocol 2026-07-28 and the server mishandles part of it. Open the server's MCP log (on macOS, `~/Library/Caches/claude-cli-nodejs/<project>/mcp-logs-<server>/*.jsonl`) and check `negotiatedProtocolVersion`. If it reads `2026-07-28` next to `Failed to fetch tools`, the cause is one of four known problems. Two are server bugs: an unanswered `subscriptions/listen` and invalid `ttlMs`/`cacheScope` fields. Two are server replies that Claude Code's client mishandles: the `server/discover` probe error and an error reply with no matching request ID. `MCP_PROTOCOL_NEGOTIATION=legacy` unblocks you while the server gets fixed.
What does an MCP server author have to change for protocol 2026-07-28?
Implement `server/discover` (required), stop relying on the `initialize` handshake and `Mcp-Session-Id` sessions, return `ttlMs` (a number) and `cacheScope` (`public` or `private`) on list results, keep answering requests after acknowledging `subscriptions/listen`, and replace server-initiated elicitation, sampling, and roots requests with the multi round-trip pattern (`resultType: "input_required"`). Roots, Sampling, and Logging are deprecated with a minimum twelve-month window.

Sources

  1. Claude Code changelog - 2.1.274, 2.1.292, 2.1.295Anthropic
  2. Claude Code docs - MCP client runtimesAnthropic
  3. Claude Code docs - Environment variablesAnthropic
  4. MCP specification 2026-07-28 - Key changesModel Context Protocol
  5. MCP stdio server lists no tools since protocol 2026-07-28 is negotiated (#97189)GitHub
  6. A -32601 reply to the server/discover probe puts a 2025-11-25 stdio server on 2026-07-28 (#97391)GitHub
  7. MCP client rejects tools/list response on ttlMs/cacheScope validation (#97319)GitHub
  8. Error reply to server/discover probe is unroutable and kills the connection (#100504)GitHub