What changed, and who has work to do
The Model Context Protocol specification version 2026-07-28 was finalised on 28 July 2026, following a release candidate. It is the largest structural change the protocol has had. In one sentence: MCP stopped being a bidirectional, stateful protocol and became a request/response, stateless one.
Three removals carry most of the consequence. The initialize and initialized handshake is gone (SEP-2575 and SEP-2567). The Mcp-Session-Id header is gone, and with it protocol-level sessions as a concept. In their place, every request self-describes — the protocol version, the client's identity and the client's capabilities all travel inside the request's _meta field, on every single call. The specification blog states the payoff plainly: "Any request can now land on any server instance behind a plain round-robin load balancer without needing shared storage."
Alongside the removals come replacements and additions. Multi Round-Trip Requests (MRTR, SEP-2322) replace the server-initiated requests that previously needed an open stream. Header-based routing (SEP-2243) makes the Mcp-Method and Mcp-Name HTTP headers mandatory. Cacheable list results (SEP-2549) add ttlMs and cacheScope to tools/list, prompts/list, resources/list and resources/read. Authorisation gets hardened, Tasks moves out into an extension, extensions themselves become a first-class versioned mechanism, and a formal deprecation policy arrives with a stated minimum window of "at least twelve months".
Who actually has work to do? Roughly three groups, in descending order of pain.
- Remote server operators who held per-session state. Cached token exchanges, in-flight multi-step forms, subscription registries, pagination cursors in a dictionary. This is the real migration, and the specification blog acknowledges the cost "especially for developers that did depend on session identifiers".
- Anyone using server-initiated requests. If your server ever asked the client a question mid-call, that flow becomes MRTR, and MRTR changes the shape of your handler rather than just its transport.
- Everyone else. A server that is a set of pure tool handlers hitting a database is mostly a dependency bump plus a header check. As of July 2026 the Tier 1 SDKs supporting
2026-07-28are TypeScript, Python, Go and C#, with the Rust SDK in beta, and the SDK releases ship migration notes.
If you are new to writing servers at all, start with the fundamentals before the migration — our walkthroughs on building your first MCP server with tools, resources and security and the twelve-step FastMCP build both assume a stateless-friendly shape already, which is the easier place to start from.
Prerequisites and a migration audit
Do not start by upgrading the SDK. Start by finding out how much state you are actually holding, because that number determines whether this is an afternoon or a quarter.
Prerequisites. You want three things in place before touching code: a staging environment running at least two instances of the server; the ability to turn session affinity off at the load balancer; and request-level tracing so you can see a single logical operation spread across multiple HTTP requests. If you have not instrumented the server yet, do that first — our guide to agent observability with OpenTelemetry covers the span structure that makes a multi-round-trip call legible instead of a scattering of unrelated POSTs.
The audit. Work through this checklist against your codebase and your infrastructure. Every "yes" is a migration task.
- In-memory session maps. Any module-level dictionary,
Map, or cache keyed by a session identifier. Grep forMcp-Session-Id,sessionId,session_id,sessions[and your SDK's session object type. - Sticky-session load balancer configuration. AWS ALB target-group stickiness, Cloudflare session affinity,
ip_hashorstickydirectives in nginx, Envoy hash policies, KubernetessessionAffinity: ClientIP. - Auth state cached per session. Token exchange results, downstream API credentials, resolved scopes or tenant lookups computed once at
initializeand reused. This is the most common one and the most dangerous to move carelessly. - Subscription and streaming handlers. Resource subscriptions, change notifications, anything that assumed a long-lived connection existed to push down.
- Server-initiated requests. Sampling calls, elicitation-style prompts, roots queries — everything that inverted the direction of the conversation.
- Long-running operations. Anything built against the experimental Tasks in core, which has moved to the
io.modelcontextprotocol/tasksextension. - Transport. Any code path still on legacy HTTP+SSE.
- Client-side assumptions. If you also ship a client or a proxy, it must now emit
Mcp-MethodandMcp-Nameon every request and populate_metaon every call.
The fastest audit is not a code review. Deploy your current server to staging with three replicas, turn session affinity off, and run your integration suite. Every failure is a piece of state you did not know you were holding. In our experience this finds more than grep does, because it also catches state hidden inside libraries and connection pools rather than only in your own handlers.
There is an operational prize at the end of this, and it is worth naming before the work starts. Session affinity forces you to over-provision. A team running a small MCP fleet in AWS Mumbai, or in London, sizes each instance for its pinned clients rather than for aggregate load, so utilisation stays low and autoscaling reacts late — a scaled-out instance receives no traffic until new sessions arrive, while the pinned ones stay hot. Remove affinity and that distortion disappears. It also makes a two-region deployment materially simpler: with no shared session storage to replicate between ap-south-1 and eu-west-2, the two regions can front one logical pool with ordinary latency-based routing, rather than each region maintaining its own sticky world and a cross-region replication story you have to reason about during incidents.
The stateless request shape, before and after
Here is what disappeared. The old flow opened with a handshake that established the protocol version and exchanged capabilities, and every subsequent request carried a session header that the server used to look that context back up. All code in this guide is simplified and illustrative; exact field names come from your SDK's constants.
// BEFORE — illustrative, pre-2026-07-28
// 1. Handshake
POST /mcp
{ "jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"clientInfo": { "name": "acme-agent", "version": "3.2.0" },
"capabilities": { "sampling": {}, "roots": { "listChanged": true } }
} }
// Response set the session
HTTP/1.1 200 OK
Mcp-Session-Id: 8f2c1e5a-...
{ "jsonrpc": "2.0", "id": 1,
"result": { "protocolVersion": "2025-06-18",
"serverInfo": { "name": "billing-mcp", "version": "1.4.0" },
"capabilities": { "tools": {}, "resources": {} } } }
// 2. Confirmation
POST /mcp Mcp-Session-Id: 8f2c1e5a-...
{ "jsonrpc": "2.0", "method": "notifications/initialized" }
// 3. Every later call rode the same session
POST /mcp Mcp-Session-Id: 8f2c1e5a-...
{ "jsonrpc": "2.0", "id": 7, "method": "tools/call",
"params": { "name": "create_invoice", "arguments": { "customer": "c_9912" } } }
And here is the replacement. There is no step one. The first request a client ever sends is a real request, and it carries everything the server needs to interpret it.
// AFTER — illustrative, MCP 2026-07-28
POST /mcp
Mcp-Method: tools/call
Mcp-Name: create_invoice
Content-Type: application/json
{ "jsonrpc": "2.0", "id": 7, "method": "tools/call",
"params": {
"name": "create_invoice",
"arguments": { "customer": "c_9912", "amount_minor": 4500000,
"currency": "INR" },
"_meta": {
"protocolVersion": "2026-07-28",
"client": { "name": "acme-agent", "version": "3.2.0" },
"capabilities": { /* what this client can do, per request */ }
}
} }
// No Mcp-Session-Id. No prior handshake. No shared storage required
// for the server to understand this request.
Read the difference in terms of what the server is allowed to assume. Previously, a handler could assume that some earlier code path had run on the same process and left context behind. Now it cannot assume any prior interaction happened at all. That is the whole migration in one sentence, and every remaining section is a consequence of it.
_meta is client-supplied input on every request, which means it is untrusted input on every request. Negotiating capabilities once at handshake time gave a comfortable illusion that the values had been vetted; per-request delivery removes it. Validate the protocol version, validate the capability shape, and never treat a client-declared identity in _meta as an authorisation decision — that belongs to your token, not to a JSON field the caller controls.
Where your state actually goes now
This is the section that decides how long your migration takes. "The protocol is stateless" does not mean your application is. It means the protocol will no longer carry your state for you, so you have to place it deliberately. There are exactly three honest destinations, and most real servers use all three.
Option A — genuinely stateless: recompute per request
The state was derived, so derive it again. Tenant lookup from the token's subject claim, permission resolution, feature flags, schema introspection. Each request pays a small recomputation cost and the server holds nothing.
This is the right default more often than teams expect, because the thing being cached is frequently cheap and the caching existed only because the handshake gave a convenient place to put it. The cost is latency and load on whatever you are recomputing against. The mitigation is not a session — it is an ordinary process-local cache keyed on the token subject with a short TTL, which is safe precisely because it is a cache rather than a source of truth: a cold instance simply recomputes.
Option B — externalise to a shared store
The state is genuinely durable and genuinely server-side: a half-finished workflow, an uploaded file being processed, an idempotency ledger. It moves to Redis, Postgres, a Durable Object, or whatever your stack already runs.
The key question is what you key it on. There is no protocol session identifier any more, so the key must come from somewhere you control: the authenticated subject from the access token, plus an application-level identifier the client supplies and re-sends — a workflow ID, a draft ID, an idempotency key. Derive the key from both. Keying purely on a client-supplied string is how one tenant reads another tenant's draft.
The costs are real and worth stating: a network round trip on requests that previously hit memory; a new operational dependency in your availability calculation; and consistency exposure. That last one bites hardest in multi-region setups. If your store is a single primary in eu-west-2 and a request lands in ap-south-1 immediately after a write, read-after-write is no longer free. In practice we would recommend either pinning the store to one region and accepting the cross-region latency, or scoping state so that a workflow's requests naturally stay within a region — but be explicit about which, rather than discovering it during an incident.
Option C — hoist state to the client
Hand the state back to the caller and let it return the state on the next call. Pagination cursors, partially-completed argument sets, and MRTR answers are the natural fits. The server stays pure and the store disappears entirely.
The costs are token cost — this state travels through a model's context, so it is not free the way a Redis value is — a size ceiling, and a trust boundary. Anything the client can hold, the client can modify. If the hoisted state carries authority (an approved amount, a resolved account, a granted scope), sign it or encode only an opaque reference and keep the authoritative copy server-side under Option B.
| State type | Where it belongs | Cost | Failure mode if you get it wrong |
|---|---|---|---|
| Resolved tenant, scopes, feature flags | A — recompute, with a short-TTL process cache | Small per-request latency and lookup load | Cached against the wrong key; a cold instance behaves differently from a warm one |
| Downstream API tokens obtained by exchange | B — shared store, keyed on the authenticated subject | Store round trip; a real secrets-handling requirement | Token leaks across tenants, or re-exchange storms when the cache is cold |
| Pagination cursor | C — hoist to the client as an opaque cursor | Tokens in context; cursor must be version-tolerant | Client replays a stale cursor against changed data and silently skips rows |
| In-flight MRTR answers | C for the answers, B for anything already committed | Repeated payload on each round trip | Duplicate side effects when the handler re-runs — see the next section |
| Expensive computed index or embedding cache | B — shared store, keyed on content hash | Storage cost; invalidation logic | Every instance recomputes the same index; cost multiplies by replica count |
| Resource subscriptions and change notifications | B, or redesign around client polling | Registry to maintain; delivery is now your problem | Subscriptions registered on an instance that later dies; silent notification loss |
| Rate-limit and quota counters | B — shared store, or enforce at the gateway | Hot-key contention on the counter | Per-instance counters let a client exceed the limit by the replica count |
| Idempotency ledger for committed operations | B — always; this one cannot be hoisted | A write on the critical path of every mutating call | The same invoice raised twice because the retry landed elsewhere |
Re-implementing sessions on top of the stateless protocol by inventing your own x-my-session-id header and a sticky routing rule to match. It gives you every constraint the specification just removed — affinity, shared storage, instance-pinned state — with none of the interoperability, and no other client or gateway in the ecosystem will honour it.
Rewriting interactive flows as Multi Round-Trip Requests
Server-initiated requests were how a server asked the client for something mid-call: sample this prompt, confirm this action, tell me your roots. They required an open stream in the client-to-server direction, which is exactly what a stateless request/response protocol cannot provide. MRTR (SEP-2322) is the replacement, and the inversion is clean.
The server no longer pushes. It returns. When a handler discovers it needs something it does not have, it returns a result with resultType set to "input_required", enumerating the requests it needs satisfied. The client gathers the answers and retries the original call, carrying them in inputResponses.
// Server response — illustrative shape
{ "jsonrpc": "2.0", "id": 7,
"result": {
"resultType": "input_required",
"requests": [
{ "id": "confirm_amount",
"type": "confirmation",
"prompt": "Raise an invoice for GBP 3,600.00 against Northgate Ltd?" },
{ "id": "po_number",
"type": "text",
"prompt": "Purchase order number, if the customer requires one." }
]
} }
// Client retry — the ORIGINAL call, resent with answers
POST /mcp
Mcp-Method: tools/call
Mcp-Name: create_invoice
{ "jsonrpc": "2.0", "id": 8, "method": "tools/call",
"params": {
"name": "create_invoice",
"arguments": { "customer": "c_9912", "amount_minor": 360000,
"currency": "GBP" },
"inputResponses": {
"confirm_amount": true,
"po_number": "NG-2026-4471"
},
"_meta": { "protocolVersion": "2026-07-28",
"client": { "name": "acme-agent", "version": "3.2.0" } }
} }
Now the part that breaks migrations. Your handler is re-entrant whether you designed for it or not. It runs once, returns input_required, and runs again from the top with partial answers. If a flow needs two questions asked at different points, it runs three times. If the client abandons the flow and retries later, it runs again. Any side effect performed before the point where the handler asked for input will happen more than once.
This is the single most common way an MCP migration breaks, and it breaks quietly: the tool works in testing, where the first pass happens to have all its inputs, and duplicates in production, where it does not.
The structural fix is to split every interactive handler into two phases. A plan phase that is pure — it validates arguments, resolves references, computes what would happen, and decides which inputs are still missing — and a commit phase that runs exactly once, at the end, guarded by an idempotency key.
# Illustrative Python sketch — re-entrant MRTR handler
def create_invoice(arguments: dict, input_responses: dict, ctx) -> dict:
# ---- PLAN PHASE: pure. Safe to run any number of times. ----
customer = load_customer(arguments["customer"]) # read-only
total = compute_total(arguments) # read-only
missing = []
if "confirm_amount" not in input_responses:
missing.append({
"id": "confirm_amount", "type": "confirmation",
"prompt": f"Raise an invoice for {fmt(total)} against "
f"{customer.name}?",
})
if customer.requires_po and "po_number" not in input_responses:
missing.append({
"id": "po_number", "type": "text",
"prompt": "Purchase order number.",
})
if missing:
# Return, don't push. The client will resend the whole call.
return {"resultType": "input_required", "requests": missing}
if input_responses.get("confirm_amount") is not True:
return {"resultType": "error",
"error": "Invoice cancelled by the user."}
# ---- COMMIT PHASE: runs once, guarded. ----
# The key must be stable across retries of the SAME logical
# operation and different across genuinely different ones.
key = idempotency_key(
subject=ctx.auth.subject, # from the access token
operation="create_invoice",
payload=arguments, # NOT input_responses
)
existing = ledger.get(key)
if existing:
return {"resultType": "ok", "invoice": existing} # replay
invoice = billing.create(customer, total,
po=input_responses.get("po_number"))
ledger.put(key, invoice, ttl_seconds=86_400)
return {"resultType": "ok", "invoice": invoice}
Two details in that sketch matter more than they look. First, the idempotency key is derived from the arguments, not from the arguments plus the answers — otherwise a user who changes a purchase order number and retries creates a second invoice. Second, the ledger write and the billing call want to be atomic; where your store allows it, write the ledger row in the same transaction as the effect, and where it does not, write the key first with a pending marker and reconcile. This is standard distributed-systems hygiene, but MCP servers largely did not need it before, because the session made a handler look like it ran once.
If your interactive flows were previously built on Sampling or elicitation-style prompts, our earlier guide to Sampling and elicitation for human-in-the-loop servers now describes deprecated mechanisms — Sampling is deprecated under the new policy — but the human-in-the-loop design questions it works through, such as what to confirm and how to phrase a confirmation, transfer directly onto MRTR. Read it for the product thinking, not the wire format.
Add a test that invokes every interactive handler twice with the same arguments and the full set of answers, and asserts that the side effect occurred once. Run it in CI. It is three lines per handler and it catches the failure mode that manual testing structurally cannot, because a human tester naturally supplies answers in the order the handler asks for them.
While you are in this code, it is a good moment to revisit tool design generally — the tighter your schemas, the fewer round trips MRTR needs in the first place. Our guide to designing tools for AI agents: schemas, errors and retries covers the argument-shaping that keeps input_required rare.
Every article here is written by a Verified Builder. Want your name on the next one?
AI Tech Connect lists AI engineers, founders and researchers across India and the UK — and the people hiring browse it to find them. Adding your profile is free.
Become a Verified Builder →Gateway routing and cacheable lists: the operational payoff
Two additions in 2026-07-28 exist mainly to make MCP behave like ordinary HTTP infrastructure expects. They are the cheapest wins in the whole release.
Header-based routing (SEP-2243). The Mcp-Method and Mcp-Name headers are mandatory. Mcp-Method carries the JSON-RPC method — tools/call, tools/list, resources/read — and Mcp-Name carries the specific tool, prompt or resource name. Both are now visible to every hop before anything parses a JSON body.
That sounds minor and is not. Consider what an edge proxy could previously do with an MCP request: almost nothing, because every call was a POST to the same path with an opaque body. Now the edge can route tools/list to a cache and tools/call to the fleet; apply a strict rate limit to Mcp-Name: run_migration and a loose one to Mcp-Name: search_docs; block a compromised tool at the gateway during an incident without a deploy; emit per-tool metrics from access logs alone; and enforce a body-size limit that varies by tool. None of it requires buffering and parsing the body at the edge, which is what made all of it impractical before.
# Illustrative gateway sketch (Envoy-style route matching)
routes:
# Manifest reads: serve from the edge cache.
- match:
path: /mcp
headers:
- name: Mcp-Method
string_match: { exact: "tools/list" }
route: { cluster: mcp_fleet }
response_headers_to_add:
- header: { key: Cache-Control, value: "public, max-age=300" }
# Dangerous tool: strict per-subject rate limit, tight body cap.
- match:
path: /mcp
headers:
- name: Mcp-Method
string_match: { exact: "tools/call" }
- name: Mcp-Name
string_match: { exact: "run_migration" }
route:
cluster: mcp_fleet
rate_limits: [{ actions: [{ header_value_match: { descriptor_key: tool } }] }]
per_request_buffer_limit_bytes: 16384
# Everything else: plain round robin. No session affinity, by design.
- match: { path: /mcp }
route: { cluster: mcp_fleet }
Cacheable list results (SEP-2549). tools/list, prompts/list, resources/list and resources/read can now return ttlMs and cacheScope, telling the client how long the result stays valid and how widely it may be shared.
// tools/list response — illustrative
{ "jsonrpc": "2.0", "id": 1,
"result": {
"tools": [ /* ... tool definitions ... */ ],
"ttlMs": 300000,
"cacheScope": "global"
} }
// A per-user manifest MUST narrow the scope, or one user's
// tool list will be served to another.
{ "jsonrpc": "2.0", "id": 1,
"result": {
"tools": [ /* tools this subject is entitled to */ ],
"ttlMs": 60000,
"cacheScope": "user"
} }
Tool manifests are re-fetched constantly and change rarely, so this removes a genuinely large share of chatty traffic. It also cuts tokens: a manifest that is cached is a manifest the client is not rebuilding. If you are already thinking about the token cost of large tool surfaces, this pairs well with the approach in our guide to code execution with MCP to cut agent tokens, which attacks the same bill from the other direction.
cacheScope is a security control, not a performance knob. If your tool list varies by user, tenant or granted scope and you declare it globally cacheable, you have built a cross-tenant disclosure into a caching layer that is working exactly as instructed. Audit every list endpoint for whether its output depends on the caller before you set a scope, and during a rollout keep ttlMs short — long TTLs mean clients keep calling a tool you removed hours ago.
Authorisation: from DCR to CIMD, and issuer validation
The authorisation changes are smaller in code but higher in consequence, because they close real attack classes.
Issuer validation per RFC 9207 (SEP-2468). The authorisation response must be validated against the issuer that produced it. This is the standard mitigation for authorisation-server mix-up attacks, where a client that talks to more than one issuer can be induced to redeem a code at the wrong one. If you have written your own OAuth client code rather than leaning on a library, this is the change to read carefully — and if you are using an SDK's built-in client, confirm the version you are pinning implements it rather than assuming.
application_type at Dynamic Client Registration (SEP-837) lets a registration declare whether it is a native or web client, so the authorisation server can apply the appropriate redirect-URI rules rather than treating every client identically.
Issuer-bound client credentials (SEP-2352) tie credentials to the issuer that minted them, so a credential obtained from one authorisation server cannot be presented to another.
DCR is deprecated in favour of Client ID Metadata Documents (CIMD). This is the structural change. Rather than registering with every authorisation server and storing a client ID and secret per server, a client publishes a metadata document at an HTTPS URL and uses that URL as its client identifier. The authorisation server fetches the document to learn the client's redirect URIs, name and properties.
The client-side work this implies is worth planning explicitly: you need somewhere stable to host the metadata document, with a URL you are prepared to treat as a long-lived identifier — changing it changes your client's identity everywhere. You need that host to be genuinely reliable, since it now sits in the authorisation path. And you get to delete the registration-storage code and the per-server credential table, which is usually a net simplification. For servers, the corresponding work is accepting a URL-shaped client identifier, fetching and caching those documents sensibly, and not letting an unavailable metadata host become an outage.
Everything here rides on top of ordinary credential hygiene, which the specification cannot do for you: scope the tokens your server holds to the narrowest set of downstream permissions that works, and rotate them. Our guide to least-privilege credentials for AI agents covers the pattern, and it matters more now that a cached token exchange has moved out of process memory and into a store with its own access-control surface.
Planning the deprecations
2026-07-28 introduces a formal deprecation policy with a stated minimum window of "at least twelve months". That policy is itself the most valuable thing in this part of the release: it converts "will this break?" into a scheduling question. Deprecated is not removed. Sequence accordingly.
| What | Status in 2026-07-28 | Window | Recommended sequencing |
|---|---|---|---|
initialize / initialized handshake |
Removed (SEP-2575, SEP-2567) | None — it is gone | Now. Non-negotiable to speak the new version. |
Mcp-Session-Id and protocol sessions |
Removed | None | Now. This is the state migration above. |
Mcp-Method / Mcp-Name headers |
Mandatory (SEP-2243) | None | Now, on both server and any client or proxy you ship. |
| Legacy HTTP+SSE transport | Deprecated | "A year-long offramp" | Schedule first. It is the deprecation with an operational deadline and the most code attached. |
| Tasks | Moved to the io.modelcontextprotocol/tasks extension (SEP-2663), with poll-based tasks/get and a new tasks/update |
Not a deprecation — a relocation | Now, if you use it. Re-point to the extension while you are already in the code. |
| Sampling | Deprecated | At least twelve months | Schedule. Fold into the MRTR rewrite if the same handlers are affected. |
| Roots | Deprecated | At least twelve months | Schedule. Move to explicit tool arguments rather than an ambient query. |
| Logging | Deprecated | At least twelve months | Leave, then replace. Most teams should be on OpenTelemetry for this anyway. |
| Dynamic Client Registration | Deprecated in favour of CIMD | Per the deprecation policy | Schedule. Needs a hosting decision before it needs code. |
Our reading of that ordering, offered as engineering judgement rather than as anything the specification mandates: the removals are a single release, the transport migration is the next quarter's work because it has a real end date, and the three deprecated features are ordinary backlog items with a twelve-month floor under them. The extensions mechanism becoming first-class and versioned is the quiet enabler here — capabilities can now leave core without leaving the ecosystem, which is exactly what happened to Tasks, and it is a reasonable bet that more will follow. Designing your server so that extension-provided capabilities are behind an interface, rather than called directly from handlers, is cheap insurance.
Six ways this migration breaks
These are the failure patterns to expect, with the symptom you will actually observe first and the fix.
- Duplicate side effects from MRTR re-entry. Symptom: two invoices, two emails, two rows — always for the operations that asked the user a question, never for the ones that did not. Fix: the plan/commit split with an idempotency ledger, plus the double-invocation CI test.
- Surviving in-memory state. Symptom: works locally and in single-instance staging; intermittent "not found" or, far worse, one user seeing another's data once affinity is removed. Fix: the multi-replica no-affinity staging run before production, and place every survivor deliberately using the decision table above.
- Sticky sessions left switched on. Symptom: the migration ships and nothing improves — utilisation stays lopsided, autoscaling still lags. Fix: removing affinity is a deliberate step, not a consequence. It is also the step that reveals bugs, so do it in staging first and production second, never both at once.
- Client-supplied identifiers used as an authorisation boundary. Symptom: nothing, until someone changes a workflow ID by hand. Fix: compose every store key from the authenticated subject plus the client identifier, and verify ownership on read rather than trusting the key.
- Over-broad
cacheScopeor over-longttlMs. Symptom: clients calling a tool you deleted, or a user seeing tools they are not entitled to. Fix: scope by whether the output depends on the caller; keep TTLs short during rollout and lengthen once the manifest is stable. - Missing
Mcp-MethodorMcp-Nameat an intermediate hop. Symptom: requests that work against the server directly and fail through your proxy, usually as a 4xx with an unhelpful body. Fix: ensure every hop preserves the headers, and add a smoke test that asserts they arrive at the origin. - Treating
_metaas trusted. Symptom: no symptom until it is a security finding. Fix: validate version and capability shape on every request; take identity from the token. - Silent subscription loss. Symptom: notifications simply stop for some clients after a deploy. Fix: subscriptions cannot live in instance memory any more — externalise the registry or redesign around client polling, which is the direction the Tasks extension itself took.
"The handshake was doing more work than anyone documented. Every server I have looked at had at least one thing cached at initialize that nobody would have chosen to cache if they had been asked. Deleting the handshake mostly deletes accidental design."
Where to start on Monday
A sensible order of operations, for a team with a remote MCP server in production as of July 2026.
- Run the audit. Three replicas in staging, affinity off, integration suite green or not. Write down every failure.
- Place each piece of surviving state against the decision table — recompute, externalise, or hoist — and be honest about which. The temptation to externalise everything is strong and usually wrong; recomputation is cheaper than an operational dependency more often than it looks.
- Upgrade to an SDK release supporting
2026-07-28and read its migration notes, which the SDK releases ship precisely because the specification blog acknowledges the cost for anyone who depended on session identifiers. - Rewrite interactive handlers as plan/commit, add the idempotency ledger, add the double-invocation test. Budget more time here than anywhere else.
- Emit and preserve
Mcp-MethodandMcp-Nameend to end, then take the gateway wins: per-tool rate limits, edge-cached manifests, per-tool metrics. - Set
ttlMsandcacheScopeon your list endpoints, conservatively at first. - Schedule the transport migration off HTTP+SSE, and the CIMD work behind it.
- Only then remove session affinity in production — and watch your utilisation graphs, because that is where the return on all of the above shows up.
The broader direction of travel is worth keeping in view while you do this: the changes tracked in our coverage of the MCP 2026 roadmap, server cards and enterprise auth all point the same way, towards MCP servers behaving like ordinary HTTP services that ordinary infrastructure can operate. A stateless protocol is a precondition for that, and this release is the version where it arrived. The migration is real work, particularly around re-entrancy, but what you get at the end is a server your platform team already knows how to run — in Mumbai, in London, or in both at once behind one pool.