MCP initialize Removed: How Do You Replace Mcp-Session-Id in a Remote Server?
Evidence is based on the locked MCP 2026-07-28 RC, official draft documentation, Python mcp==2.0.0b2, and a real localhost round-robin HTTP fixture. The wire lab was rerun on July 26, 2026 with 6/6 cases passing, and the SDK import smoke test completed. The experiment makes no claim about final-spec conformance, production Kubernetes behavior, commercial-client interoperability, or performance improvement.
MCP initialize Removed: How Do You Replace Mcp-Session-Id in a Remote Server?
What do you do after MCP initialize is removed, and what replaces Mcp-Session-Id? The answer is direct: MCP 2026-07-28 removes the protocol session, not application state. Every remote request must carry the protocol, client, capability, and routing information needed to evaluate that call. Baskets, approvals, browser sessions, and long-running jobs continue through explicit handles such as basket_id, approval_id, browser_id, and task_id, backed by a database or task system. Installing SDK v2 alone does not complete that migration.
Why is the change necessary? Nothing changed at the endpoint, yet the first request returned HTTP 200 and the next returned HTTP 400. No network timeout or malformed tool schema was involved: initialize reached Replica A and stored a session in local memory; a following tools/call reached Replica B, which had never seen the identifier and returned Unknown MCP session.
This failure is easy to miss while developing a remote MCP server on one process. During local development, the client, connection, and server instance stay together, so instance-local protocol state appears harmless. Behind Kubernetes, a serverless gateway, or an ordinary round-robin proxy, the URL can remain stable while the process changes on every request. MCP’s locked 2026-07-28 release candidate therefore removes initialize / initialized and protocol-level Mcp-Session-Id, moving protocol facts onto each request so execution no longer depends on one process’s memory.
To test that boundary, the XBSTACK fixture runs two legacy replicas, two draft-shaped replicas, a real localhost round-robin proxy, and an external SQLite store. Legacy traffic produced a deterministic 200/400 result, while draft-shaped requests alternated between draft-a and draft-b successfully. No model or API key is involved.
An intentionally small setup avoids Kubernetes, Redis, and commercial clients while isolating the core failure. Two independent processes establish the session-to-replica dependency; the same proxy then tests self-contained requests; only the basket record moves to external storage. Each step answers one question and can be rerun after the final specification ships without turning a local result into a production guarantee.
The last pre-publication verification completed on July 26, 2026: the wire lab passed 6/6 cases, and the Python mcp==2.0.0b2 import smoke test confirmed the new module layout and the missing legacy mcp.server.fastmcp path. On July 28, 2026, the official GitHub Releases page still labels this revision RC / pre-release, and the final-release milestone is not fully closed. The article is therefore published as a locked-RC experiment and migration plan: it separates official facts, local evidence, and work that still requires final-spec validation instead of claiming a final-spec test. See the official MCP releases.

Why does an MCP server return Unknown MCP session across replicas?
Because Mcp-Session-Id points to session state held by one replica, while a load balancer may send the next request to another. Replica B cannot read the session created by A, so it returns Unknown MCP session.
Legacy Streamable HTTP follows a straightforward sequence. A client sends initialize; the server returns capabilities and server information and may allocate Mcp-Session-Id. Later requests carry the identifier. If the remote endpoint or transport is not stable yet, start with the complete path from local stdio to remote MCP Streamable HTTP deployment.
When both calls return to one process, the flow works:
POST /mcp initialize
-> Replica A
<- 200
<- Mcp-Session-Id: session_a_123
POST /mcp tools/call
Mcp-Session-Id: session_a_123
-> Replica A
<- 200
A second replica exposes the hidden assumption. When the proxy sends the tool call to B, its local session map does not contain session_a_123:
POST /mcp tools/call
Mcp-Session-Id: session_a_123
-> Replica B
<- 400 Unknown MCP session
This is not only a synthetic concern. In June 2026, a Stack Overflow question described the same shape in a Spring AI MCP MVC deployment on Kubernetes: initialize and the follow-up call reached different pods, leaving the team to choose among sticky routing, a shared session store, or architectural change. See the deployment question.
Sticky sessions can stop the immediate failure, but they only make the proxy try to return traffic to A. If A restarts, is drained, or disappears during a rollout, its memory disappears as well. Moving the whole session into Redis can preserve compatibility, but it often turns one opaque blob into the home for client capabilities, identity, temporary files, domain objects, and recovery data.
Misdiagnosis as model instability is also common. A gateway sees HTTP 400, the client initializes again, and the agent runtime retries the tool. If the tool has side effects, one transport problem can become two business operations. Logs then show several sessions and attempts even though the root cause was simply a request reaching another process.
Before changing dependencies, force this path in a fixture: initialize on one replica, call on another, restart the server, drop the response, and repeat the request. A single-instance success test cannot reveal how much hidden work the session is doing.
Next, inspect what the session actually contains. Some values are protocol facts such as client name, version, and capabilities. Others belong to authenticated identity, including user, tenant, and scopes. A third group already consists of business facts: an approval amount, a partially uploaded file, or a ticket created before the response was lost. When all three categories share one session record, teams often preserve the wrong thing—replicating sensitive workflow data merely to keep a tool-list cache, or continuing to bind an approval draft to a pod because that was where the session lived.
A useful question is whether the data can be reconstructed after the process is killed. Client capabilities can arrive again with a request, and a tool list can be rediscovered. Payment results, approval decisions, and whether a task already executed cannot be guessed. They require an explicit authority outside the server process.
That is why a shared session store is a transition rather than a complete design. It solves “B cannot read A’s record” without answering what the record means. If it contains only short-lived legacy compatibility context, give it a short TTL and retire it with the old protocol. If it contains orders or tasks, migrate those facts into domain records first. Otherwise the stateless protocol launches while the product still depends on a legacy session table no one dares to delete.

How can you reproduce MCP session failure behind a load balancer?
legacy-a and legacy-b are independent HTTP servers. Each keeps its own sessions: set[str]. A strict round-robin proxy sends request one to A and request two to B.
As a control, both calls go directly to legacy-a:
initialize_status = 200
tool_call_status = 200
session_id = session_legacy-a_0748e8f4
served_by = legacy-a
Legacy behavior is not designed to fail unconditionally. Put the round-robin proxy back in the path and the record changes:
initialize_status = 200
initialize_replica = legacy-a
call_status = 400
call_replica = legacy-b
error.code = -32001
error.message = Unknown MCP session
A created the session. B could not find it. A shared hostname, healthy replicas, and an available load balancer do not detect that dependency.
| Scenario | Request placement | Result | Hidden dependency |
|---|---|---|---|
| Single replica | initialize → A, call → A | 200 / 200 | A’s memory survives |
| Round robin | initialize → A, call → B | 200 / 400 | B cannot read A’s session |
| Sticky routing | calls prefer A | Usually works | A must remain alive |
| Shared session store | A and B read one store | Compatible path | Protocol sessions still require governance |
| Self-contained requests | any call may reach A or B | Independent success | Domain state is stored separately |
What matters is not whether the older specification is “broken,” but whether the server meets a sharper scaling target: any replica should be able to interpret the protocol request from the request itself.
Proxy route history provides stronger evidence than the final status code alone. It rules out a common false positive in which traffic appears to pass through a load balancer but connection reuse or proxy policy keeps both calls on one upstream. Here the same record contains initialize_replica = legacy-a and call_replica = legacy-b, tying the failure to the replica change.
Production diagnostics need the same context. Record protocol version, an irreversible session or handle hash, destination replica, tool name, and attempt. Without the replica, Unknown MCP session only says that a lookup failed somewhere. With route evidence, operations can distinguish expiration, a shared-store read failure, and a genuine cross-instance miss. An agent runtime that retries automatically needs that distinction to decide whether to renegotiate, reload domain state, or stop before duplicating a side effect.
Session and handle identifiers are generated randomly on every run, so the article does not treat one identifier as a fixed test fixture. Verification checks status, replica switching, object lifecycle, and final data, then writes redacted exchanges and a result summary to JSON. This keeps the run reproducible without hard-coding a “real-looking” session that never changes.
Why did MCP 2026-07-28 remove initialize and Mcp-Session-Id?
Stateless Core is the main line of the locked RC. initialize / initialized leave the core flow, and protocol sessions stop linking requests. server/discover probes supported revisions and capabilities; protocol version, client information, and client capabilities travel with requests; HTTP gains routing metadata such as Mcp-Method and Mcp-Name. Current TypeScript SDK guidance also moves server identity away from the legacy handshake and a fixed Discover body field: modern responses expose it through _meta['io.modelcontextprotocol/serverInfo']. See the draft changelog.
| Legacy flow | 2026-07-28 RC direction | Common migration mistake |
|---|---|---|
| Initialize, then keep negotiated state | Carry required protocol context per request | A handler still reads old session context |
Use Mcp-Session-Id to relate calls | Core requests do not require protocol sessions | Domain state is deleted as well |
| Return server information during initialization | Probe versions/capabilities with server/discover; read server identity from response _meta | Code still reads the legacy handshake or a fixed Discover body field |
| Route mainly from the JSON-RPC body | Expose method/name routing metadata | Trust the header without checking the body |
| Keep Tasks wire methods in the old core | Move long-running work to an extension | Assume every client and server supports Tasks |
One idea makes the design easier to understand: protocol facts that used to live in a connection or a process become visible, verifiable request data. The MCP protocol-boundary guide covers how client, server, tool, and resource responsibilities should remain separated.
SDK v2 is not the same thing as the new wire protocol. Official TypeScript SDK guidance explicitly says that adopting v2 does not automatically put 2026-07-28 on the wire; the application still needs to opt in and inspect actual traffic. See TypeScript SDK v2 support for 2026-07-28.
Specification date, SDK major version, package publication, and commercial-client support are separate timelines. 2.0.0b2 proves that a prerelease package exists. It does not prove what a production request is sending.
Removing initialization also changes where incompatibility appears. In the legacy flow, a version or capability mismatch usually surfaced during initialize. In the new shape, each request can carry protocol and capability information, so the server must evaluate compatibility before entering the tool. A long-lived client that changes request shape during an upgrade cannot rely on negotiation cached hours earlier.
That separation matters in multi-tenant systems. A legacy session may have represented both “what this client supports” and “who this user is,” even though the lifecycles differ. Client version changes with an application deployment, an access token may expire in minutes, and a domain object may remain for months. Moving them into request context, authenticated identity, and domain storage lets each one change or expire independently rather than sharing one session TTL. For token, Resource Indicator, and tool-scope design, continue with the MCP OAuth authentication guide.
server/discover should not become a new long-lived session entry point. It returns supported revisions and a current capability snapshot, while current SDK guidance reads server identity from response _meta; a client may use ttlMs or related hints to decide when to refresh. Tool execution still depends on the current request and current authorization. Seeing a tool during discovery does not guarantee that the user remains authorized later, or that another server pool in a canary exposes the same registry.
Without initialize, how can any replica process an MCP request?
A draft-shaped fixture creates no protocol session. Every request provides its protocol version and routing metadata, and the server validates the current request before execution.
A simplified tools/call looks like this:
POST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: add_item
{
"jsonrpc": "2.0",
"id": 13,
"method": "tools/call",
"params": {
"name": "add_item",
"arguments": {
"basket_id": "basket_33be3dcbe25d",
"item": "mcp-book"
},
"_meta": {
"io.modelcontextprotocol/clientInfo": {
"name": "xbstack-fixture",
"version": "1.0"
},
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}
}
}
Routing headers help a gateway with early policy checks, rate limits, and telemetry, but they do not permit skipping the JSON-RPC body. To test that boundary, the fixture sends Mcp-Method: tools/list and Mcp-Name: wrong-tool while the body calls create_basket, then rejects the request before tool execution:
{
"code": -32602,
"message": "Routing headers do not match the JSON-RPC body"
}
That exact message belongs to the fixture, not the final specification. Production guidance is narrower: routing metadata can accelerate a decision, but the gateway still needs a consistent body, token, and tool authorization decision.
In the replica test, server/discover reached draft-a and tools/list reached draft-b:
discover_status = 200
discover_replica = draft-a
list_status = 200
list_replica = draft-b
tool_count = 3
ttl_ms = 30000
Neither request shared a session or depended on A’s memory. Next, create_basket on A and add_item on B also succeeded. That separates two responsibilities cleanly: self-contained requests solve protocol routing; an explicit handle plus external storage preserves the business workflow.
Self-contained does not mean copying the entire workflow into every request. Protocol version, client metadata, and current arguments belong on the request. Order details, file contents, and approval records should still travel by reference. Replacing a small session with tens of kilobytes of repeated domain context would increase cost and expose sensitive data to more proxies and logs.
Gateway responsibilities become easier to define. Protocol and method headers can select a server pool before the body is fully processed; parsing then verifies the body, authenticated identity, and tool authorization. If the two views disagree, reject the call rather than guessing which one to trust. When an edge proxy rewrites headers, logs should retain both received and normalized values so the conflict can be traced to the client or an intermediary.
Idempotency ordering also needs a fixed rule. For a side-effecting tool, validate protocol and authorization first, then derive the idempotency key from the normalized tool name and arguments. Otherwise different headers with the same body can create several keys, or a rejected request can occupy the key needed by the legitimate call.

Where should application state go after Mcp-Session-Id is removed?
Removing a protocol session does not forbid server-side state. Orders, approvals, baskets, and task progress still need durable storage. They should not be hidden inside a transport-level session.
A deliberately small basket workflow keeps the state boundary visible:
create_basket
<- basket_id
add_item(basket_id, "mcp-book")
<- updated basket
create_basket writes the record through draft-a. add_item reaches draft-b, which reads and updates the same external SQLite database:
{
"basket_id": "basket_33be3dcbe25d",
"status": "active",
"items": ["mcp-book"]
}
basket_id differs from Mcp-Session-Id in more than name. A basket handle points to one domain object, allowing the server to validate owner, tenant, state, and expiration on every use. A protocol session is only an association identifier, yet implementations often turn it into a container for identity, capabilities, temporary files, and draft business data.
An expiration test calls expire_basket, then uses the same identifier through another replica. The fixture returns -32602 Invalid or expired application handle. That code is an application choice; the important behavior is that a syntactically valid identifier does not remain trusted after expiration.
For a first migration, three handle rules matter most:
- Ownership is part of the lookup. Query by handle, tenant, and allowed state together rather than fetching by ID and adding authorization later.
- Lifecycle states are distinct. Active, completed, cancelled, and expired should not collapse into one generic “not found.”
- Concurrent writes have a rule. Mutable approvals or drafts need a version or ETag so one agent cannot silently overwrite another.
Side-effecting tools also need an idempotency key. Stateless protocol handling does not mean every retry is a new business action. A gateway or worker may repeat a payment, email, ticket, or database write after a timeout unless the application can recognize the previous attempt.
A small handle model is sufficient. Its record needs the object type, tenant, owner, state, version, expiration, and a reference to the authoritative domain record. The model sees a random identifier; the server resolves it under authenticated context. This design is easier to revoke and audit than encoding the whole object into a long signed token: a permission change takes effect at the database lookup instead of waiting for every old token to expire.
A globally unique handle is not authorization. Randomness only reduces guessing. Logs, prompt injection, browser history, or another tool can expose a real identifier to the wrong user. Including tenant and owner in the actual query makes the leaked ID insufficient by itself. Use the MCP security-governance checklist to review tool scope, read-only identities, roots, and audit evidence.
Concurrency deserves a real negative test. Let two clients read the same approval_id and submit different changes. If the later write silently overwrites the first, the new explicit state is still unreliable. A version conflict should force a fresh read and, when amount or beneficiary changes, another human confirmation.
Approval records can also store a hash of critical creation-time arguments. When execution resumes, the server recomputes the hash and rejects the old approval if the target account or path changed while the agent was waiting. That protection belongs to the domain record and continues to work after a replica switch.

How do you migrate to Python MCP SDK v2, and why is a version bump not enough?
Do not replace mcp>=1 with mcp>=2 and keep the old entry point. Install the target release in isolation, verify imports and startup, inspect the actual wire protocol, test the target client, and only then change the production dependency.
As of July 23, 2026, the Python MCP v2 prerelease visible on PyPI was mcp==2.0.0b2, published July 14. See PyPI: mcp 2.0.0b2.
Installation used an isolated target directory:
python3 -m pip install \
--target /tmp/xbstack-mcp-sdk-b2 \
mcp==2.0.0b2
Installation succeeded, as did imports of mcp and mcp.server. A legacy path used by older examples did not:
from mcp.server.fastmcp import FastMCP
This wheel returned:
ModuleNotFoundError: No module named 'mcp.server.fastmcp'
That failure is more informative than a successful install. It shows that package layout, server abstraction, startup code, and protocol activation may all move. Changing:
mcp>=1.x
to:
mcp>=2
is not a migration plan.
A practical dependency setup is simple: keep a <2 upper bound on the production branch; pin the exact beta on a migration branch; after the final release, pin a concrete 2.x.y only after import, startup, and interoperability checks pass. Broad prerelease ranges create changes that are difficult to reproduce.
There is also no reason to begin with a large benchmark. First confirm four things: the package installs, a minimal server starts, the actual wire protocol is the target version, and the target client can discover and call a tool. Performance testing matters after the basic contract works.
“Starts” should mean more than a process remaining alive. A minimal server should register one side-effect-free tool and complete discovery, listing, invocation, and an intentional error. Capture the real HTTP exchange and verify the version header, routing metadata, and _meta. Many migrations do not fail at import time; the SDK starts in a compatibility mode, or the server launches without the transport and protocol shape the team thought it enabled.
Keep a small wire Golden File in the migration branch. One server/discover, one tools/list, one successful call, and one failed call are enough to expose many beta-to-final changes. Compare those samples before rereading an entire SDK README whenever a new package appears.
Runtime details belong in the evidence as well. This b2 result came from Python 3.10.2 and an isolated target directory. Another interpreter, lockfile, wheel, or operating system may expose a different module layout. Final verification should record Python version, resolved dependencies, and wheel hash so “we both installed 2.0.0” becomes reproducible evidence rather than a vague claim.

Can old MCP clients connect directly to 2026-07-28 servers?
Familiar fields do not make the protocols compatible. A legacy client begins with initialize and expects a session. A new client sends a self-contained request. A legacy server needs an established session; a new server may not accept the initialization flow at all.
A minimal compatibility fixture produces this matrix:
| Client | Server | Direct result | Rollout treatment |
|---|---|---|---|
| 2025-11-25 | 2025-11-25 | Pass | Keep the legacy pool |
| 2025-11-25 | 2026-07-28 | Fail | Route through the legacy adapter |
| 2026-07-28 | 2025-11-25 | Fail | Route to a new server |
| 2026-07-28 | 2026-07-28 | Pass | Begin low-volume validation |
This is not a universal interoperability statement. Production still needs the actual SDKs, commercial clients, transports, OAuth flow, and extensions in its test matrix.
During rollout, explicit gateway routing is easier to reason about than one handler guessing every protocol:
MCP-Protocol-Version
├── 2025-11-25 -> legacy adapter / legacy pool
├── 2026-07-28 -> stateless adapter / new pool
└── missing or unknown -> explicit error and upgrade guidance
An adapter can translate initialization output into an internal client profile and select the matching server pool. It should not invent a capability that the client never declared. It should also never switch protocols and blindly retry a side-effecting tool after failure.
To keep domain tools from understanding two wire formats, place a normalized internal request behind the adapters. It needs only the fields the business layer uses: external protocol version, original method, normalized tool name, authenticated context, domain handle, idempotency key, and allowed fallback policy. Legacy input contributes still-valid session information; new input contributes headers and _meta. Payment, file, or ticket tools read neither wire format directly.
At that boundary, translation errors become visible before execution. The gateway can record the incoming and normalized shapes, while future protocol upgrades replace an adapter instead of rewriting every domain tool. A dangerous mismatch also surfaces early: the same business call should not produce different idempotency keys merely because it entered through different protocols.
Fallback policy belongs to the normalized request before execution begins. A read-only tools/list may fall back to the legacy pool when the new path is unavailable. A payment call that may already have reached its backend cannot be retried through another protocol after a timeout. Gateway logic must distinguish “not executed” from “possibly executed, response lost”; the latter requires an idempotency lookup or external operation reconciliation.
Compatibility windows need exit conditions. Track old-protocol traffic by client version and tenant, contact low-frequency callers, and remove the adapter only after both legacy traffic and fallbacks remain at zero through a meaningful observation window. Calendar time since SDK release is not evidence that a monthly job has upgraded.

What is the difference between MRTR, Tasks, and application handles?
MRTR supplies missing input during one tool call, Tasks represent long-running execution, and application handles identify domain objects such as baskets, approvals, or browser sessions. They are separate lifecycles and should not be collapsed into one generic session ID.
Removing the protocol session does not remove approvals, missing user input, or long-running work. It removes the assumption that those states can live in one connection object.
MRTR, or Multi-Round Tool Results, fits a tool call that cannot proceed without another piece of input. The server can return InputRequiredResult with requestState; the client gathers input and submits inputResponses with that state.
execute_payment
-> InputRequiredResult
requestState = opaque_state_abc
user confirms
-> tools/call
requestState = opaque_state_abc
inputResponses = {...}
The server still validates caller, original parameters, expiration, and whether the state has already been consumed. An opaque string is not a bearer credential for unlimited continuation.
Long-running work is handled by the independent io.modelcontextprotocol/tasks extension. The 2026 core handler does not process tasks/* by itself. Only after both sides declare and enable the Tasks extension does the extension implementation accept tasks/get, tasks/update, and tasks/cancel, and only then may a supported request return CreateTaskResult. See the MCP Tasks extension.
| Scenario | Appropriate state |
|---|---|
| One tool call needs more user input | MRTR requestState |
| Work runs for seconds or hours | Task handle plus queue/task store |
| Basket, draft, or approval | Domain handle plus database |
| Pure query tool | Self-contained request, usually no session state |
A recoverable task must answer three questions: what state it is in, who may advance it, and whether the previous attempt already produced a side effect. running and completed are not enough. A task may be queued, waiting for input, waiting for approval, executing, retrying, cancellation requested, cancelled, or externally successful while the local result has not been written.
A worker should not own that task forever. A lease works better across replicas: the worker claims the record until a deadline and renews it while active. After a crash, another worker waits for lease expiry, reads the previous attempt and external operation ID, and then decides whether to continue. Queryable third-party systems can be reconciled first; non-queryable actions need idempotency or manual review.
Cancellation is not deletion. When the client requests it, the task may still be queued or may already be inside a third-party API whose response was lost. Record both “cancellation requested” and “execution confirmed stopped.” A user interface should not display a final cancelled state while an external payment may already have completed.
MRTR and a Task can appear in one workflow. A long-running job may pause for a human document: the Task handle represents the overall execution, while requestState represents only the missing input round. The input state can expire after submission while the task continues.
One identifier should not mean “waiting for user input,” “background task,” and “domain object” at the same time. That simply recreates the universal session under a new name.

How do you roll out MCP 2026-07-28 without breaking production?
Use MCP-Protocol-Version to route legacy clients to a legacy pool and modern clients to a stateless pool. Start with low-risk tools, keep idempotency and authorization checks consistent, and preserve both data rollback and semantic rollback. The remote MCP production-governance guide adds OAuth, multi-tenant isolation, observability, and permission gates.
A complete production-governance framework can become much larger than this migration. Four gates determine whether this particular change is ready to receive traffic.
1. Protocol routing is explainable
Each request records client, protocol version, method, tool, destination pool, and fallback. Unknown versions fail explicitly instead of entering a handler that guesses.
2. Application state survives replica changes
A handle remains usable after a replica switch, process restart, or disconnected response. Expiration, ownership failure, and version conflict produce stable outcomes. Critical state does not live only in worker memory or a temporary directory.
3. Retries do not duplicate side effects
Payments, emails, database writes, and ticket creation use stable idempotency keys. Logs distinguish the request, tool call, and attempt. After a timeout, the system checks whether the previous attempt completed before executing again.
4. Legacy traffic can return immediately
The gateway keeps a version switch. New data remains readable by the fallback path, or the canary is limited to low-risk flows that can be discarded or rebuilt. Rolling back code while leaving unreadable handles and tasks behind is not a real rollback.
A sensible order is read-only tools first, idempotent writes next, and payments, permission changes, or destructive file operations last. Success rate alone is not enough; watch authorization rejection, fallback, duplicated side effects, and invalid handles. A high fallback success rate can mean the new path is failing continuously.
Suppose the first cohort contains tools/list and one read-only query. The new path returns the same data, but authorization rejection rises. That may be a correction rather than a regression if the new gateway finally validates audience or tool scope. Conversely, an overall HTTP success rate of 99.9% is meaningless if half the requests reached that success only after falling back to the legacy pool. The dashboard needs the first-attempt outcome, fallback reason, and final outcome together.
Write canaries care about how many times execution occurred. Give the incoming request an internal Request ID, the tool invocation a Tool Call ID, and every retry an Attempt ID. Correlate them with traceparent, but do not merge them. When a duplicate email appears, operations must distinguish one request retried twice from two independent requests executed once each.
Rehearse data rollback before the canary. The new path may have created handles, tasks, or approval records that the old path cannot read. If backward interpretation is unavailable, restrict the first cohort to rebuildable data and label the migration as forward-only. A database restore cannot unsend an email, reverse a payment, or delete a third-party ticket; side-effect recovery depends on idempotency, reconciliation, or manual action.
The observation window must include real usage cycles. Month-end finance, Monday batches, and low-frequency administrator tools do not pass simply because they were quiet for three days. Synthetic requests can exercise the route, but the adapter should remain until at least one relevant business cycle shows declining legacy traffic and fallback.
| Stage | Pass condition | Rollback |
|---|---|---|
| Fixture | Legacy/new requests and errors repeat deterministically | No production traffic changes |
| Dual protocol | No legacy-client regression | Route everything to the old pool |
| Read-only canary | Result differences are explained | Stop new routing |
| Write canary | Idempotency, handles, and audit records remain correct | Roll back by tenant or tool |
| Legacy retirement | No old traffic in the observation window | Extend compatibility |
After the final specification ships, rerun the six local cases and add target-SDK and real-client interoperability. Any change to fields, errors, or extension negotiation belongs in both the implementation and this article.

What has this MCP migration test verified, and what remains untested?
The local evidence currently supports these observations:
- a legacy session works when both requests stay on one replica;
- the same flow returns
Unknown MCP sessionwhen the next call reaches another replica; - self-contained draft-shaped requests alternate between
draft-aanddraft-b; basket_idpreserves domain state across replicas;- expired handles and header/body conflicts are rejected before execution;
mcp==2.0.0b2installs, but the legacy FastMCP import path fails.
The fixture does not cover production Kubernetes ingress, service mesh behavior, SSE, cross-region traffic, commercial clients, OAuth interoperability, Tasks extension interoperability, or performance comparison. SQLite demonstrates state outside the process; it is not a recommendation to share a local SQLite file across production replicas.
A real Kubernetes run adds several variables: whether the ingress reuses upstream connections, whether the service mesh retries automatically, how a streaming response ends during pod drain, and whether old and new protocols coexist during a rolling deployment. The localhost proxy proves the instance-local failure mechanism; it cannot stand in for those components. Run the same fixture inside the target infrastructure and retain both proxy and replica logs.
Performance cannot be inferred from the word stateless. Per-request metadata, external handle reads, schema validation, and authorization may add latency, while removing session coordination and sticky routing may reduce complexity. Only a controlled comparison with the same payload, concurrency, and storage can show whether time moved into protocol parsing, the database, or the tool backend. This article has no such benchmark and therefore makes no speed or cost claim.
Commercial clients are a separate variable. Some negotiate compatibility, some keep the legacy initialization flow, and some hide retries behind their UI. The minimal matrix proves only that wire shapes should not be assumed compatible. Release validation still needs the actual supported clients with recorded version, protocol, capabilities, and recovery behavior.
The final specification has not shipped yet. Method names, fields, SDK layout, errors, and extension negotiation still require a new check against the final changelog and SDK tags. Even if Final matches the RC, rerun the fixture against the final tag instead of changing only the date on the July 23 result.
Experiment directory:
experiments/mcp-2026-07-28-stateless-migration-demo/
Run the wire fixture:
python3 run_experiment.py
Recorded result:
{"passed": 6, "total": 6, "all_passed": true}
Which MCP servers should migrate now?
A local stdio server used by one developer, with no remote multi-tenancy or replicas, does not need an emergency production rewrite. Pin the current dependency, build a minimal new-protocol fixture, and wait for the final text and target SDK before choosing a release date.
A remote Streamable HTTP server behind Kubernetes, serverless infrastructure, a gateway, or multi-tenant routing should start preparing now if its session lives in process memory. Preparation means finding hidden state, adding version-aware routing, moving domain state to explicit handles, and running real old/new client-server combinations. It does not mean sending all traffic to the new path before July 28.
Projects with payments, approvals, email, or database writes should examine idempotency and ownership before celebrating the protocol upgrade. The new core can remove a request’s dependency on one pod. It cannot prevent a duplicate payment or decide who owns a leaked approval_id.
Migration priority follows what the current session decides. If it stores only negotiation data, the work is mainly client/server compatibility and routing. If a tool reads user, tenant, draft, or task progress from the session, those facts need a new authority before the handshake disappears. If the session records whether the previous attempt already paid someone, domain state and idempotency take priority over the protocol cutover.
Team and client topology matter as well. With one internal client, both sides can upgrade together and the compatibility window may be short. With desktop clients, partners, and low-frequency automations, dual-protocol routing may remain for weeks or months. An adapter is not a failure in that situation; an adapter with no traffic metrics or retirement date is.
The migration plan should answer one concrete question: what happens to the next request if the current replica is killed now? The desired answer is not “the load balancer will try to find the old pod.” A new replica should validate the protocol, load authoritative domain state, identify the previous attempt, and either continue or reject the call explicitly. If session affinity is still the only answer, the migration is unfinished.
The final judgment is simple: MCP 2026-07-28 is worth migrating to, but installing SDK v2 is only the first step. The migration is real when any replica can interpret the current request, business state no longer hides in a protocol session, old and new versions have an explicit route, and a failure report can explain how many side effects actually happened.

Experiment code and official sources
First-party experiment files:
experiments/mcp-2026-07-28-stateless-migration-demo/README.mdexperiments/mcp-2026-07-28-stateless-migration-demo/results/verification.jsonexperiments/mcp-2026-07-28-stateless-migration-demo/results/sdk-smoke.jsonexperiments/mcp-2026-07-28-stateless-migration-demo/IMAGE_PLAN.md
Official and problem sources:
- MCP 2026-07-28 Release Candidate
- MCP Draft Changelog
- SEP-2567: Sessionless MCP
- TypeScript SDK: Support for MCP 2026-07-28
- Official Python SDK repository
- PyPI: mcp 2.0.0b2
- Stack Overflow: MCP Streamable HTTP session behavior across replicas
All tables are original XBSTACK material, and experiment data comes from the repository fixture. The cover and architecture diagrams were generated with GPT rather than copied from official documentation or third-party articles.
Continue from protocol details to production MCP governance
The MCP hub connects protocol fundamentals, transports, authentication, security, JSON-RPC debugging and production deployment without splitting the search intent across isolated guides.
Next Reading
View Hub →Practical AI Trading Agents: Market Monitoring, Strategy Backtesting, Risk Control, and Human-in-the-Loop Verification
This article breaks down the production-grade design of AI trading agents, covering market monitoring, news and announcement parsing, technical signals, strategy backtesting, risk control, position limits, paper trading validation, human confirmation, trade logs, and review metrics. It helps developers build controllable trading assistance systems rather than blindly executing automated orders.
OpenAI Agents SDK RunState: Resume Tool Approvals Across Processes
A reproducible openai-agents 0.18.3 case study covering Tool Approval interruption, RunState serialization, cross-process approve/reject and worker resume, plus duplicate side effects, context-secret leakage, idempotency, and version gates.
Vercel AI SDK 7 Migration: Interrupted Streams, Cloudflare 524 Boundaries, and Tool-Call Recovery
A production AI SDK 6-to-7 migration backed by deterministic fixtures and real localhost HTTP/SSE disconnect tests, covering partial persistence, idempotent tool recovery, request-bound versus detached execution, SDK timeouts, and Cloudflare 524 proxy boundaries.
AI Agent Protocol and Framework Selection: How to Choose Between MCP, Function Calling, A2A, LangGraph, AutoGen, and CrewAI?
A systematic overview of protocol and framework selection for AI Agent development, covering Function Calling, MCP, A2A, LangGraph, AutoGen, CrewAI, LangChain, custom workflows, multi-agent collaboration, tool invocation, state management, and production deployment boundaries. Helps developers choose the appropriate tech stack based on specific scenarios.
Xiaobai
Full-Stack AI Engineer
Xiaobai, a full-stack AI engineer building production Agent systems, product tools and independent software assets.
About Xiaobai & XBSTACK →Liked this article?
Join the newsletter
Every issue condenses production AI engineering changes, real failures, reproducible experiments, useful tools and new XBSTACK assets. No generic news digest and no filler.
DISCUSSION
Questions, verification and corrections
Sign in to comment. Every new comment is reviewed before publication; while pending, it is visible only to you and the administrator.