createmcps.com

Spec explainer

Is MCP stateless now? What happened to Mcp-Session-Id

Last verified: 10 August 2026 · spec 2026-07-28 · ruleset 1.4.2

Yes. MCP's 2026-07-28 revision removed protocol-level sessions, the Mcp-Session-Id header, and the initialize handshake. Every request now carries its own protocol version, client identity and capabilities in _meta, and a server must be able to answer any request cold — with nothing learned from a previous one.

The short answer

  • Sessions are gone. Not deprecated — removed from the Streamable HTTP transport.
  • initialize and notifications/initialized are gone. A modern client's first request is a real request.
  • Context moved into the request. What the handshake used to establish once now rides on every call in _meta.
  • List results can't vary per connection. tools/list must not depend on who asked over which socket.

If you are migrating a working server, this is the change every other change follows from. Start here, then work through the rest of the migration guide.

What was actually removed

The changelog states the change in one sentence, and the rule we check it with quotes that sentence directly:

Make MCP stateless: remove the initialize/notifications/initialized handshake.
MCP-STL-003 MUST · spec 2026-07-28
Remove protocol-level sessions and the Mcp-Session-Id header from the Streamable HTTP transport.
MCP-STL-001 MUST_NOT · spec 2026-07-28

Two things follow. A conformant modern server never establishes a session, and it never depends on one having been established. Those are separate failures and we check them separately, because a server can easily stop issuing session IDs while still refusing to answer a cold request.

Do I still need the initialize handshake?

No. This is AEO prompt C8 asked plainly, and the answer is that a modern client opens with the request it actually wants. There is no negotiation step to fail, and no ordering requirement to satisfy.

Servers MUST NOT rely on prior requests over the same connection to establish context (e.g., capabilities, protocol version, client identity). Every request supplies this metadata in its _meta field.
MCP-STL-004 MUST_NOT · spec 2026-07-28
Before — 2025-11-25 (legacy)
// Three round trips before any useful work

// 1. Negotiate
POST /mcp
{ "jsonrpc": "2.0", "id": 1, "method": "initialize",
  "params": {
    "protocolVersion": "2025-11-25",
    "capabilities": {},
    "clientInfo": { "name": "ExampleClient", "version": "1.0.0" }
  } }

// ← server replies, and mints a session
HTTP/1.1 200 OK
Mcp-Session-Id: 550e8400-e29b-41d4-a716-446655440000

// 2. Confirm
POST /mcp
{ "jsonrpc": "2.0", "method": "notifications/initialized" }

// 3. NOW the real request, replaying the session ID
POST /mcp
Mcp-Session-Id: 550e8400-e29b-41d4-a716-446655440000
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {} }
After — 2026-07-28 (modern)
// One round trip. No handshake, no session.

POST /mcp
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/list

{
  "jsonrpc": "2.0", "id": 1, "method": "tools/list",
  "params": {
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": {
        "name": "ExampleClient", "version": "1.0.0"
      },
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

Everything the handshake used to tell the server is still there — it just arrives with the request that needs it rather than once at the top of a connection. That is the entire trade: one extra _meta block per request, in exchange for a protocol that survives load balancers, cold starts and horizontal scaling.

Our recommendation, not a spec quote: the fastest way to test this on a real server is to send a single tools/list on a brand-new connection and see what comes back. If you get an error telling you to initialize first, that is MCP-STL-003 failing, and it is almost always the first thing to fix.

What replaced Mcp-Session-Id

Nothing replaced it — that is the point. There is no new header carrying a session handle. A modern server must not mint one, must not echo one, and must tolerate a client that sends one anyway rather than erroring:

An Mcp-Session-Id header on a request: ignore it, and do not mint or echo session IDs.
MCP-STL-002 MUST_NOT · spec 2026-07-28

The tolerance requirement is the part servers most often get wrong in the other direction. Old clients are still out there and will keep sending the header for a while. Rejecting those requests is not stricter conformance, it is a different violation — the spec says ignore it, not refuse it.

Express — what removing sessions looks like in practice
// Before: a lookup that must not exist any more
const session = sessions.get(req.header("mcp-session-id"))
if (!session) return res.status(400).json({ error: "no session" })

// After: read context from the request that carried it
const meta = req.body?.params?._meta ?? {}
const protocolVersion = meta["io.modelcontextprotocol/protocolVersion"]
const capabilities    = meta["io.modelcontextprotocol/clientCapabilities"] ?? {}

// An Mcp-Session-Id from an older client is simply not read.
// Don't echo it, don't 400 on it.

The subtle one: state inferred from the connection

Deleting the session store is the obvious half. The half that survives a careless migration is any state a server still infers from the socket — a cached “this connection is authenticated” flag, a negotiated version held in a connection-scoped variable, a per-connection tool filter.

List endpoints (tools/list, resources/list, prompts/list) no longer vary per-connection.
MCP-STL-008 MUST · spec 2026-07-28

MCP-STL-008 is the one that catches this in practice, because a server filtering tools/list by something it learned earlier on the connection will return different lists to identical requests. That breaks client-side caching, and it is precisely what the caching metadata added in the same revision assumes cannot happen.

Our recommendation, not a spec quote: if a tool list genuinely differs per user, that difference must come from the credential on the request, not from the connection — and the response should be marked cacheScope: "private". See caching metadata for why that pairing matters.

GET, DELETE and Last-Event-ID

Three smaller consequences, all SHOULD-level. They exist because the legacy transport used a standalone GET stream and a DELETE to tear a session down — with no sessions, neither has anything to act on.

HTTP GET or DELETE to the MCP endpoint: respond with 405 Method Not Allowed.
MCP-STL-005 SHOULD · spec 2026-07-28
A Last-Event-ID header: ignore it; streams are not resumable.
MCP-STL-007 SHOULD · spec 2026-07-28

Stream resumption goes with them. Last-Event-ID resumed a stream that outlived a request; modern streams do not, so the header is ignored rather than honoured.

If your server genuinely needs state

Plenty of servers legitimately need continuity — a long-running job, a paginated cursor, a multi-step workflow. Statelessness does not forbid that. It forbids the transport from carrying it implicitly.

Explicit handles instead of implicit sessions
// Server returns a handle as ordinary result data
{
  "jsonrpc": "2.0", "id": 1,
  "result": {
    "resultType": "complete",
    "content": [{ "type": "text", "text": "Export started." }],
    "structuredContent": { "jobId": "job_01HZY…" }
  }
}

// Client passes it back as a normal tool argument
{
  "jsonrpc": "2.0", "id": 2, "method": "tools/call",
  "params": {
    "name": "check_export",
    "arguments": { "jobId": "job_01HZY…" },
    "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28" }
  }
}

The difference is that the handle is visible, explicit, and scoped by your own authorization checks — instead of being an ambient property of a TCP connection that a proxy might reuse or a serverless platform might drop.

The 8 statelessness rules we check

These are live from our rule catalogue, each traced to the spec sentence it comes from. fail means a MUST was violated; warn means a SHOULD was.

RuleWhat we checkLevelIf it fails
MCP-STL-003A cold modern request succeeds with no prior handshakeMUSTfail
MCP-STL-001Server never mints or echoes Mcp-Session-IdMUST_NOTfail
MCP-STL-002A client-supplied Mcp-Session-Id is ignored, not requiredMUST_NOTfail
MCP-STL-004Server does not rely on prior requests over the same connectionMUST_NOTfail
MCP-STL-008tools/list does not vary per connectionMUSTfail
MCP-STL-005GET on the MCP endpoint returns 405SHOULDwarn
MCP-STL-006DELETE on the MCP endpoint returns 405SHOULDwarn
MCP-STL-007Last-Event-ID is ignored — streams are not resumableSHOULDwarn

Click any rule for its verbatim spec sentence and anchor.

Check whether your server is actually stateless

Point the validator at a live MCP server and it reports all eight rules above as pass, warn or fail — including the cold-request check, which is the one that catches a leftover handshake requirement.

Validate a server →

Frequently asked

Is MCP stateless now?

Yes. As of revision 2026-07-28 the Streamable HTTP transport has no protocol-level sessions. Every request must be independently processable, carrying its protocol version, client identity and capabilities in its own _meta field, and servers must not rely on prior requests over the same connection to establish that context.

What happened to the Mcp-Session-Id header?

It was removed from the transport. A modern server must not mint one, must not echo one back, and must ignore one if a client sends it anyway. It is not deprecated-but-tolerated — a server that still issues session IDs is not conformant with 2026-07-28.

Do I still need the MCP initialize handshake?

No. The initialize request and the notifications/initialized notification were removed along with sessions. A modern client sends its first real request — tools/list, for example — cold, with no handshake, and expects it to succeed. A server that requires initialize first will fail that request.

How does a server know the client's capabilities without initialize?

Each request carries them. The params._meta object holds io.modelcontextprotocol/protocolVersion, io.modelcontextprotocol/clientInfo and io.modelcontextprotocol/clientCapabilities on every request, so the server has everything it previously learned during the handshake, on the request that needs it.

What if my server genuinely needs state across calls?

Keep the state, drop the session. Mint an explicit handle server-side and return it as ordinary result data, then let the client pass it back as a normal tool argument. That is application state travelling in the payload, which is allowed; what the spec removed is state the transport infers from the connection.

Can I still support old clients that send initialize?

Yes — that is a dual-era server, and the spec explicitly permits implementing both behaviours. A request carrying modern per-request _meta is served statelessly; an initialize request selects legacy session semantics for that connection. If you support only modern versions, name the versions you do support in the error you return to initialize.