Book a call
REFERENCE14mVERIFIED 2026-08-03 · CLAUDE CODE 2.1.193 · CODEX CLI 0.146.0 · GITHUB COPILOT CLI 1.0.77 · ANTIGRAVITY CLI 1.1.9 · KIMI CODE CLI docs only, local binary broken

MCP in 2026: stateless, standardised, and what got deprecated

The 2026-07-28 revision made MCP stateless and gave every deprecated feature a removal date. What changed, what is on the clock, and what your harness actually speaks today.

Why a spec revision is your problem

Most of the time it isn't. You install a server, your harness lists its tools, work happens — the protocol underneath is somebody else's concern.

It stops being somebody else's concern in three situations, and they're the reason this page exists:

  • If you wrote a server. The 2026-07-28 revision removed the handshake your server is built around, and newly deprecated four features with removal dates attached: Roots, Sampling, Logging and Dynamic Client Registration.

  • If you're choosing between servers or gateways. When people say something "supports MCP" they now mean one of at least three different things, and the difference shows up in what you can actually deploy.

  • If you hit an error mentioning a protocol version. Then you want a table, not an article.

The short version: MCP is now a stateless request/response protocol, governance moved to a foundation in December 2025, and there's finally a written policy saying how long a deprecated feature survives. Nothing broke on 28 July. Some of it now has an expiry date.

The stateless core

The headline change is that MCP went from a stateful two-way protocol to a request/response one, by deleting everything that made a connection mean something.

Removed in 2026-07-28What takes over
The initialize / notifications/initialized handshakeio.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities, in _meta, on every request
Protocol-level sessions and the Mcp-Session-Id headerHandles minted by the server, passed as ordinary tool arguments
The HTTP GET endpoint, resources/subscribe and resources/unsubscribesubscriptions/listen — one long-lived POST-response stream you opt into per notification type
pingNothing. The changelog names no replacement
logging/setLevelio.modelcontextprotocol/logLevel in a request's _meta
notifications/roots/list_changedNothing, and Roots itself is deprecated anyway
SSE resumability: the Last-Event-ID header and SSE event IDsRe-send the request with a new request ID
notifications/elicitation/complete and elicitationIdThe client sends the original request again; a server that needs to correlate embeds its own identifier in requestState
Tasks as an experimental core feature, including tasks/result and tasks/listThe io.modelcontextprotocol/tasks extension, with tasks/get polling and tasks/update

One method arrives with all this, and servers MUST implement it: server/discover, which reports the protocol versions and capabilities a server supports along with its identity. A client can call it up front to pick a version, or use it to probe compatibility over stdio.

This is what a request looks like once the handshake is gone. The _meta key names are the normative part:

JSON
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "search_invoices",
    "arguments": { "query": "overdue" },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {},
      "io.modelcontextprotocol/clientInfo": { "name": "your-client", "version": "1.0.0" }
    }
  }
}

With nothing to pin a client to, an MCP server behaves like any other HTTP workload: round-robin load balancing, no sticky sessions, no shared session store. The mandatory Mcp-Method and Mcp-Name headers on Streamable HTTP POSTs address the same thing from the other side — a gateway can route and authorise on headers without parsing the JSON-RPC body.

The pattern that replaced server-initiated calls

A stateless design leaves no channel for a request that starts at the server and targets the client, which is what sat behind roots/list, sampling/createMessage and elicitation/create. All three involved interrupting a client mid-flight.

Multi Round-Trip Requests is the answer, and it works like a retry rather than an interrupt. The server returns a result with resultType: "input_required" and what it needs in inputRequests; the client re-sends the original request with inputResponses filled in.

That's also why every result now carries a required resultType"complete" for an ordinary one, "input_required" for the interim kind. If a server on an older protocol omits the field, clients MUST treat the result as "complete", and that's what keeps old servers working.

If you ever wrote a server that asks the client to sample from its model, this is your paragraph: the mechanism is deprecated, and the pattern replacing it has no sampling equivalent. The documented migration is to call an LLM provider's API directly.

What a 2026 server now has to do

Past the removals, the revision added obligations that are easy to miss because none of them announce themselves:

  • ttlMs and cacheScope are mandatory in results from tools/list, prompts/list, resources/list, resources/read and resources/templates/list, via a new CacheableResult interface. ttlMs is a freshness hint in milliseconds; cacheScope is "public" or "private" and says whether a shared intermediary may cache the response.

  • tools/list should return a deterministic order. The stated reason is client-side caching and prompt-cache hit rates — a cost line, not tidiness.

  • The error code range got split. -32000 to -32019 stays implementation-defined and existing SDK usage is grandfathered; -32020 to -32099 is reserved for the specification. Three codes moved with it: HeaderMismatch from -32001 to -32020, MissingRequiredClientCapability from -32003 to -32021, UnsupportedProtocolVersion from -32004 to -32022. Separately, resource-not-found went from -32002 to -32602, to line up with JSON-RPC's own Invalid Params.

  • Schema constraints got more permissive. inputSchema and outputSchema accept any JSON Schema 2020-12 keyword, and structuredContent accepts any JSON value.

  • Authorization got tighter. Authorization servers should send iss per RFC 9207, and clients MUST validate a present iss against their record of the issuer before redeeming the code. Client credentials are bound to the authorization server that issued them: key them by issuer, don't reuse them elsewhere, re-register when the authorization server changes.

  • _meta is now documented for trace context. OpenTelemetry propagation for traceparent, tracestate and baggage — the standard way to carry context through a gateway into a server.

What is deprecated, and when it can actually go

Bookmark this part, because "deprecated" used to be a label with no defined outcome and now it has one. The specification's own registry is the source of truth:

FeatureDeprecated inMigrationEarliest removal
Roots2026-07-28Provide files or directories through tool parameters, resource URIs or server configurationFirst revision released on or after 2027-07-28
Sampling2026-07-28Call LLM provider APIs directlyFirst revision released on or after 2027-07-28
Logging2026-07-28Write to stderr under stdio; OpenTelemetry for observabilityFirst revision released on or after 2027-07-28
Dynamic Client Registration (RFC 7591)2026-07-28Client ID Metadata DocumentsFirst revision released on or after 2027-07-28
includeContext values "thisServer" and "allServers"2025-11-25Omit the field or set it to "none"Tied to Sampling's timeline
HTTP+SSE transport2025-03-26Streamable HTTPThree months after SEP-2596 reaches Final

Two things in that table are easy to read past.

Roots, Sampling and Logging are the interesting rows. They're not obscure corners — they're what people mean when they talk about the client-side capabilities that make MCP more than a tool schema. All three keep working throughout the deprecation window. If you're writing new code, just don't reach for them.

HTTP+SSE is on the shortest window. Deprecated since the 2025-03-26 revision, reclassified under the formal policy in 2026-07-28, and eligible for removal three months after SEP-2596 reaches Final rather than the twelve months everything else gets. So if you're configuring a remote server today and the transport list offers SSE, that's the legacy path, not the modern one.

Nothing has actually been removed yet. The registry's "Removed" section is empty.

The rule that makes those dates mean something

The 2026-07-28 revision adopted a feature lifecycle policy, which is where every earliest-removal date above comes from. A feature sits in exactly one of three states:

  • Active — part of the current revision. Implement it.

  • Deprecated — still in the specification, removal scheduled, migration documented. New adopters should avoid it; existing ones should migrate before the earliest removal date.

  • Removed — dropped from the draft and absent from the next current revision, but still documented in the last final revision that contained it.

A deprecated feature can't be removed sooner than twelve months after the release of the revision that marked it Deprecated — counted from that release, not from the date the proposal reached Final. Eligibility isn't removal, either: the actual deletion is a maintainer decision taken during release preparation, and a feature can sit Deprecated far longer than the minimum.

There's one escape hatch. The twelve-month floor can be shortened for a feature with an active security risk — a published advisory, or documented exploitation with no in-place mitigation — and even then ninety days is as low as it goes.

The policy binds the SDKs too. Tier 1 SDKs must mark a deprecated surface with their language's native mechanism in their next release, and should warn at runtime when it's exercised; an SDK that consistently doesn't is subject to a tier relegation process. Which means your first warning about a deprecation should arrive in your build output rather than from a page like this one.

Who owns MCP now

On 9 December 2025 MCP became a founding project of the Agentic AI Foundation, a directed fund under the Linux Foundation, co-founded by Anthropic, Block and OpenAI with support from Google, Microsoft, AWS, Cloudflare and Bloomberg. The other two inaugural projects were Block's goose and OpenAI's AGENTS.md.

This is the "standardised" half of the picture, and it's the part that should change how you plan. A protocol owned by one vendor can turn on that vendor's product decision. A protocol with a written deprecation policy, a public SEP process and a registry of what's on its way out is one you can build a two-year roadmap against — which is exactly what the twelve-month window is for.

The announcement cited over 97 million monthly SDK downloads and 10,000 active servers. Those are vendor-supplied figures, so treat them as such; the direction is real.

Does any of this break what you run today?

No, and the compatibility story is better than the size of the changelog suggests.

  • Servers answer both. A v2 server handles server/discover and the old initialize handshake, so an older client keeps working unchanged.

  • Clients fall back. A new client that reaches a server on 2025-11-25 or earlier drops back to the initialize handshake.

  • Stateless is opt-in. The TypeScript and Go SDKs need explicit configuration for it; Python v2 servers answer both protocol revisions from a single endpoint.

Beta SDK support landed as Python mcp v2.0.0b1, TypeScript v2, Go v1.7.0-pre.1 and C# v2.0.0-preview.1, with Rust also named in the release announcement.

So if you're asking when you have to rewrite, that's not really the shape of it. The question is narrower: do you use Roots, Sampling or Logging, and is your remote transport SSE? Those answers tell you whether you have work in the next twelve months.

Where the harnesses actually are

This is the part that ages fastest, so we checked it on the wire instead of in a docs page. We connected three CLIs to a stdio server that writes down everything it receives, on 3 August 2026:

TEXT
Claude Code 2.1.193             initialize   protocolVersion 2025-11-25
Cursor CLI 2026.07.23-e383d2b   initialize   protocolVersion 2025-11-25
Codex CLI 0.146.0               initialize   protocolVersion 2025-06-18

Every one of them opened with the handshake the 2026-07-28 revision deletes, and none asked for 2026-07-28. That's not a fault — the SDKs implementing the revision are still in beta, and the compatibility rules above exist precisely so clients can migrate gradually. It does mean something practical though: a server you write today still has to understand initialize, because that's what every client we could observe sent it.

IN YOUR HARNESS

In Claude Code

Server registration is done with claude mcp add, and the transport flag is where this lesson lands. Two add commands — one pointing at a remote Streamable HTTP server, one at a local subprocess with an env var passed in:

BASH
# Streamable HTTP — the current remote transport
claude mcp add --transport http sentry https://mcp.sentry.dev/mcp

# stdio — the default, so the flag is optional
claude mcp add my-server -e API_KEY=xxx -- npx my-mcp-server

The -t / --transport flag accepts three values — stdio, sse and http — and defaults to stdio when you don't pass it. Keep in mind that sse is the deprecated HTTP+SSE transport, the one on the shortest window in the registry above. If a vendor hands you an /sse endpoint, check whether they publish Streamable HTTP as well — most do by now, and that's the one with a future.

The -s / --scope flag can be set to local, user or project. The project scope is backed by the .mcp.json file, which is the one you put under version control, and which Claude Code holds behind an approval before it will connect.

The gotcha: claude mcp list and claude mcp get create actual connections, which is why their output distinguishes a server that passed its health check from an unapproved .mcp.json entry — the latter shows as pending approval, with no connection established. That makes list the cheapest way to find out whether a server actually works. Just FYI, when we pointed Claude Code 2.1.193 at a logging server, claude mcp list did the entire handshake for us — no approval prompt, no model call, zero tokens — and what it sent was initialize with protocolVersion set to 2025-11-25.

In Codex CLI

Configuration is kept in ~/.codex/config.toml, in [mcp_servers.<name>] tables, and codex mcp add writes them for you. Anything after -- is treated as the command to run, env variables go in via --env, and for a remote server the URL is meant to point at a Streamable HTTP endpoint with the token read from an environment variable you name:

BASH
# stdio — everything after `--` is the command
codex mcp add my-server --env API_KEY=xxx -- npx my-mcp-server

# remote — `--url` is documented as "URL for a streamable HTTP MCP server"
codex mcp add sentry --url https://mcp.sentry.dev/mcp --bearer-token-env-var SENTRY_TOKEN

Which produces TOML simple enough to edit by hand:

TOML
[mcp_servers.my-server]
command = "npx"
args = ["my-mcp-server"]

[mcp_servers.my-server.env]
API_KEY = "xxx"

The point is that Codex has no SSE escape hatch, and that's on purpose — --url means Streamable HTTP, and there's no flag anywhere that points at the deprecated HTTP+SSE transport some vendors still run. So if a vendor only publishes an /sse endpoint, Codex is where you find that out.

The gotcha: codex mcp list can report a server as enabled when it isn't working at all, because it only reads configuration and never opens a connection. To actually exercise one you need a run — and -c gets you there without touching your config file:

BASH
codex exec --skip-git-repo-check \
  -c 'mcp_servers.probe={command="node",args=["/tmp/mcp-probe/mcp-probe.js"]}' \
  'Reply with the single word: ok'

--skip-git-repo-check is in there because a scratch directory isn't a repository, and codex exec refuses to run outside one by default. That's how we captured Codex CLI 0.146.0's opening message: initialize with protocolVersion set to 2025-06-18 — older than the revision before the current one, and the oldest of every client we probed.

In GitHub Copilot CLI

There are three sources of configuration from Copilot's point of view, and copilot mcp --help names all three:

TEXT
User       ~/.copilot/mcp-config.json
Workspace  .mcp.json or .github/mcp.json
Plugin     Installed plugins with MCP servers

So if you want configuration versioned alongside your codebase, create a .github/mcp.json. If you want it local to your machine and not tied to a particular project, set it up in ~/.copilot/mcp-config.json — though if you've set COPILOT_HOME, that file lives at $COPILOT_HOME/mcp-config.json instead. The .mcp.json files are searched for starting from the current directory and going up to the root of the repository.

Structurally, a config file looks like this:

JSON
{
  "mcpServers": {
    "playwright": {
      "type": "local",
      "command": "npx",
      "args": ["@playwright/mcp@latest"],
      "env": {},
      "tools": ["*"]
    }
  }
}

Two properties are worth knowing about:

  • type takes local or stdio for a process running on your machine, http for a remote Streamable HTTP endpoint, and sse for Server-Sent Events — which GitHub's own documentation marks as deprecated but supported. That lines up with the specification: HTTP+SSE is on the shortest window in the registry above, so treat it as the legacy option rather than a choice.

  • tools controls what the server is allowed to expose: "*" for everything, a comma-separated list to narrow it, an empty string for none. A crowded tool list costs you, and this is the cheapest place to cut one.

The gotcha: copilot mcp list and copilot mcp get show you what's defined in configuration and nothing more — they don't run anything, so you won't see traffic from them. Our test server logs every byte it receives and it logged nothing at all while we ran both commands. The "Enabled" label next to a server is about its configuration status, not its availability. To observe traffic you need an active session, which is why there's no wire capture for this harness in the table above: getting one non-interactively needs a flag that opts out of the permission prompt, and that isn't a habit worth building.

In Cursor

The Cursor CLI has two configuration sources:

  • a project-level file at .cursor/mcp.json

  • a user-level file at ~/.cursor/mcp.json, holding servers you want in every project

The configuration looks like this:

JSON
{
  "mcpServers": {
    "my-server": {
      "command": "npx",
      "args": ["my-mcp-server"],
      "env": { "API_KEY": "xxx" }
    }
  }
}

It's a different kind of harness from the others here, in that it puts an explicit approval in front of MCP — so its subcommands are built around that approval rather than around the underlying job:

BASH
agent mcp enable my-server        # adds it to the local approved list
agent mcp list                    # configured servers and their status
agent mcp list-tools my-server    # tool names and argument names
agent mcp login my-server         # authenticate against a configured server
agent mcp disable my-server       # never loaded, never prompted for

list-tools is the interesting one, because it actually connects. It's the fastest way to check what a server really provides, as opposed to what its README says it provides.

The gotcha: enable adds the server to a local approved list, and --approve-mcps approves everything for a run. Fine in a throwaway directory; put it in a shell alias you carry into everyday work and you've nullified the point of the gate.

We pointed Cursor CLI 2026.07.23-e383d2b at a server that logs what it receives, and saw two things:

  • the first command called the server's initialize method with protocolVersion set to 2025-11-25

  • every command after that did the whole handshake again, because each run of the CLI starts a fresh server process. Worth knowing if your server is slow to start.

In Antigravity CLI

There's no agy mcp subcommand — we checked, and the subcommand list has no MCP entry. To set MCP up you create an mcp_config.json file, in one of three places:

  • ~/.gemini/config/mcp_config.json — applies to every session

  • .agents/mcp_config.json — applies only to the project you're currently working on

  • plugins/<plugin_name>/mcp_config.json — applies only while that plugin is enabled

The global and plugin locations come from the documentation shipped inside the CLI itself; the workspace one is on Google's website but isn't named in the copy that ships with version 1.1.9. The practical consequence: if a server in .agents/ doesn't show up during a headless run, it may simply be getting ignored — ours was, so that's the first thing to check.

Here's what the file looks like:

JSON
{
  "mcpServers": {
    "sqlite-helper": {
      "command": "sqlite-mcp-server",
      "args": ["/path/to/database.db"],
      "env": { "DB_READONLY": "true" }
    },
    "remote-service": {
      "serverUrl": "https://mcp.mycompany.com/sse"
    }
  }
}

That covers both shapes: a local stdio server via command, args and env, and a remote one via serverUrl.

This is the harness where the deprecation table needs the most care. The documentation shipped inside version 1.1.9 says it supports exactly two transports — Stdio for local commands, and SSE for remote services through serverUrl, with an example URL ending in /sse. SSE is the transport the specification has now formally reclassified as Deprecated, on the shortest window in the registry above. Google's public documentation is broader: it says servers can be stdio, SSE or HTTP, and that serverUrl covers "remote SSE, Streamable HTTP, or websocket-based MCP connections". The two disagree, so confirm what your build actually opens before you rely on a remote server here.

The gotcha: that third location means an MCP server can arrive as part of a plugin rather than through a file you wrote — so a tool can show up in a session you never configured. agy plugin list is where you look when that happens.

We have no wire capture for this harness, for two reasons: the workspace-local config we put in a scratch directory wasn't picked up during a headless run, and the other two locations are global or plugin-scoped, neither of which we were willing to write into on someone else's machine.

In Kimi Code CLI

The kimi mcp command group covers the whole lifecycle of a server, and the list of transports you can pick from is a short one:

BASH
kimi mcp add my-server -- npx my-mcp-server      # stdio, the default
kimi mcp add sentry --transport http https://mcp.sentry.dev/mcp
kimi mcp auth sentry                             # authorise an OAuth server
kimi mcp test sentry                             # connect and enumerate tools
kimi mcp list
kimi mcp remove sentry

--transport supports stdio or http, and nothing else. There's no sse value, which puts Kimi Code on the right side of the shortest deprecation window in the registry above without you having to do anything about it. OAuth credentials are cached under ~/.kimi/mcp-oauth/; if a server starts refusing you for no visible reason, kimi mcp reset-auth clears them and you authorise again.

The test command is worth calling out. Most of the other harnesses make you start a session before you can find out whether a server works; here there's a command whose entire job is to connect and list the available tools. Run it after every config change and you'll catch a broken server before it costs you a turn.

The gotcha: there are two different programs called kimi — the old Python kimi-cli and the rebuilt Kimi Code CLI. They're separate tools whose MCP subcommands happen to look alike, which is exactly what makes it easy to believe you're driving one when you're driving the other. Search results for one will mislead you about the other, so check which binary you have before you trust a flag.

That's a thing we struggled with on our end: we couldn't get the Kimi Code binary to run on our machine, so this variant is checked against Moonshot's documentation rather than a captured session. Verify the transport flags yourself with kimi mcp add --help before relying on them.

Read it off the wire yourself

Everything in the section above came out of one small JavaScript file. The version numbers in this lesson will age faster than the shape of the answer will, so the durable move is to re-run the check rather than trust the table.

Save the artifact below, register it in your harness as a stdio server, and trigger anything that makes the harness connect — a health check, a tool listing, a one-line prompt. The log file gets the raw JSON-RPC in both directions.

BASH
mkdir -p /tmp/mcp-probe && cd /tmp/mcp-probe
# save mcp-probe.js here, then add it as a stdio server in your harness
tail -f /tmp/mcp-probe/probe.log

Three things worth looking for.

The opening message. initialize means your client is on the old protocol. A first request carrying io.modelcontextprotocol/protocolVersion in _meta, or a call to server/discover, means it has moved.

The protocolVersion value. It tells you which revision's rules your client is playing by, which is the fact you want when a server misbehaves.

Whether it reconnects. Some CLIs start a fresh server process per invocation, which shows up as a repeated handshake in the log and matters if your server is expensive to start.

Run it in a scratch directory rather than a repository you care about, and take the server out of your configuration afterwards.

The one-screen version

  • The current revision is 2026-07-28. The one before it was 2025-11-25.

  • MCP is stateless: no handshake, no session header, no ping. Version and capabilities travel in _meta on every request.

  • Server-initiated requests are gone. The replacement is Multi Round-Trip Requests — the server returns resultType: "input_required" and the client retries with inputResponses.

  • Deprecated with a date: Roots, Sampling, Logging and Dynamic Client Registration, none of them removable before 2027-07-28. HTTP+SSE is on a shorter window than any of them.

  • The deprecation floor is twelve months, or ninety days where there's an active security risk.

  • Governance sits with the Agentic AI Foundation under the Linux Foundation, since 9 December 2025.

  • Nothing breaks today. New servers answer the old handshake, new clients fall back to it, and stateless mode is opt-in.

  • No harness we probed speaks 2026-07-28 yet. Check yours instead of assuming.

THE FILEmcp-probe.js
JAVASCRIPT
#!/usr/bin/env node
// A stdio MCP server that does nothing except record what your harness sends it.
//
//   1. Save this file somewhere disposable, e.g. /tmp/mcp-probe/mcp-probe.js
//   2. Add it to your harness as a stdio server: `node /tmp/mcp-probe/mcp-probe.js`
//   3. Make the harness connect (a health check, a tool listing, a one-line prompt)
//   4. Read /tmp/mcp-probe/probe.log
//
// It answers the minimum needed for a health check to pass, so nothing hangs. It advertises no
// tools, so the model cannot call anything. Remove it from your config when you are done.

const fs = require('fs');
const LOG = process.env.PROBE_LOG || '/tmp/mcp-probe/probe.log';

let buffer = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
  buffer += chunk;
  let newline;
  while ((newline = buffer.indexOf('\n')) >= 0) {
    const line = buffer.slice(0, newline).trim();
    buffer = buffer.slice(newline + 1);
    if (!line) continue;

    fs.appendFileSync(LOG, 'IN  ' + line + '\n');

    let message;
    try {
      message = JSON.parse(line);
    } catch {
      continue;
    }
    // No id means a notification: nothing to answer.
    if (message.id === undefined || message.id === null) continue;

    let result;
    switch (message.method) {
      // Pre-2026-07-28 clients open with this.
      case 'initialize':
        result = {
          protocolVersion:
            (message.params && message.params.protocolVersion) || '2025-06-18',
          capabilities: { tools: {} },
          serverInfo: { name: 'probe', version: '0.0.1' },
        };
        break;
      // 2026-07-28 clients may open with this instead.
      case 'server/discover':
        result = {
          protocolVersions: ['2026-07-28'],
          capabilities: { tools: {} },
          serverInfo: { name: 'probe', version: '0.0.1' },
        };
        break;
      case 'tools/list':
        result = { tools: [] };
        break;
      case 'resources/list':
        result = { resources: [] };
        break;
      case 'prompts/list':
        result = { prompts: [] };
        break;
      default:
        result = {};
    }

    const response = JSON.stringify({ jsonrpc: '2.0', id: message.id, result });
    fs.appendFileSync(LOG, 'OUT ' + response + '\n');
    process.stdout.write(response + '\n');
  }
});
j / k to move between lessons