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

> Since Claude Code 2.1.292, stdio MCP servers get protocol 2026-07-28 by default. If tools vanish with 'Failed to fetch tools: Request timed out', read the MCP log, set MCP_PROTOCOL_NEGOTIATION=legacy, then fix the server or wait for the client fix.

URL: https://bartlomiejkrupa.dev/articles/claude-code-mcp-2026-07-28/
Published: 2026-10-13
Updated: 2026-10-13
Tags: mcp, claude-code, mcp-servers, protocol, debugging, migration
Summary: Since Claude Code 2.1.292, stdio MCP servers get protocol 2026-07-28 by default. If tools vanish with 'Failed to fetch tools: Request timed out', read the MCP log, set MCP_PROTOCOL_NEGOTIATION=legacy, then fix the server or wait for the client fix.

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](https://github.com/anthropics/claude-code/issues/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:

```text
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:

```bash
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:

```text
   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:

```bash
MCP_PROTOCOL_NEGOTIATION=legacy claude
```

Persistent, in `~/.claude/settings.json`:

```json
{
  "env": {
    "MCP_PROTOCOL_NEGOTIATION": "legacy"
  }
}
```

In a comment on [#97391](https://github.com/anthropics/claude-code/issues/97391), the author of [issue #100504](https://github.com/anthropics/claude-code/issues/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](/articles/claude-code-channels) 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 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](https://github.com/anthropics/claude-code/issues/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:

```bash
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](https://github.com/anthropics/claude-code/issues/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](https://github.com/anthropics/claude-code/issues/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](https://github.com/anthropics/claude-code/issues/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](/articles/claude-code-tools) and the [startup tax](/notes/startup-tax) note.

Sources:
- Claude Code changelog - 2.1.274, 2.1.292, 2.1.295: https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md
- Claude Code docs - MCP client runtimes: https://code.claude.com/docs/en/mcp
- Claude Code docs - Environment variables: https://code.claude.com/docs/en/env-vars
- MCP specification 2026-07-28 - Key changes: https://modelcontextprotocol.io/specification/2026-07-28/changelog
- MCP stdio server lists no tools since protocol 2026-07-28 is negotiated (#97189): https://github.com/anthropics/claude-code/issues/97189
- A -32601 reply to the server/discover probe puts a 2025-11-25 stdio server on 2026-07-28 (#97391): https://github.com/anthropics/claude-code/issues/97391
- MCP client rejects tools/list response on ttlMs/cacheScope validation (#97319): https://github.com/anthropics/claude-code/issues/97319
- Error reply to server/discover probe is unroutable and kills the connection (#100504): https://github.com/anthropics/claude-code/issues/100504
