MCP Goes Stateless: No Handshake, No Sticky Sessions, Better AI Gateways
TL;DR
MCP 2026-07-28 removes the
initializehandshake and protocol-level sessions, so each request can reach any server replica. For Azure gateways, the useful part is routing and rate limiting per tool from validated headers, without parsing JSON bodies.Keep application state explicit, protect retry state and check streaming and tracing before removing affinity. Azure’s dedicated AI Gateway is still a preview, and its October resource change matters if you’re planning a pilot.
Running a remote MCP server behind a load balancer used to come with an awkward question: which instance remembers this client? If the server issued an Mcp-Session-Id and kept its session in memory, you needed affinity, shared storage or a deployment small enough to avoid the problem.
The 28 July specification release changes that model. Every request describes itself. A tool call no longer needs a handshake before it can do useful work, and the next call doesn’t have to find the same process.
I spend a lot of my time in the layer between agents and the things they call. From there, the interesting question isn’t just what changed in MCP. It’s what we can simplify in the gateway, what we can finally do per tool, and which responsibilities haven’t gone away.
In a hurry? Jump to the gateway checklist or the eight checks before go-live.
Updated 4 November 2026: the SDK sign-in guidance and AI Gateway migration notes below reflect documentation checked on that date.
One request, before and after
The biggest change is easiest to see on the wire. This compares a session-based deployment of the previous revision, 2025-11-25, with the new one.
sequenceDiagram
accTitle: One tool call before and after MCP 2026-07-28
accDescr: In a legacy deployment with in-memory sessions, the agent initialises a session tied to one instance. In the new revision, a self-describing POST can land on any instance.
autonumber
participant AG as Agent
participant LB as Load balancer
participant A as Instance A
participant B as Instance B
rect rgb(246, 248, 250)
Note over AG,B: Before: 2025-11-25 with an in-memory session
AG->>LB: POST initialize
LB->>A: route
A-->>AG: InitializeResult and Mcp-Session-Id abc
AG->>LB: POST notifications/initialized, session abc
LB->>A: must reach A
AG->>LB: POST tools/call, session abc
LB->>A: must reach A again
A-->>AG: result
Note over LB,B: If A disappears, B doesn't know session abc
end
rect rgb(230, 255, 237)
Note over AG,B: After: 2026-07-28
AG->>LB: POST tools/call
version, method, name and _meta
LB->>B: any ready instance
B-->>AG: resultType complete
endIn the older deployment, the first tool call takes three POSTs: initialize, notifications/initialized and the call itself. Once the server creates an in-memory session, later requests have to find that state.
Older revisions already allowed servers to omit session IDs. The distinction is that 2026-07-28 removes both the handshake and protocol-level sessions, rather than leaving session behaviour to each server.
Here’s a complete example of the new request shape. The protocol version and client capabilities travel in params._meta; the method and tool name are mirrored into HTTP headers.
1 | POST /mcp |
Three details in the Streamable HTTP specification matter at the gateway:
MCP-Protocol-Versionmust match the version in_meta.Mcp-Methodis required on requests, andMcp-Nameis required ontools/call,resources/readandprompts/get.- A server must reject missing, malformed or mismatched required headers with HTTP
400and JSON-RPC error-32020,HeaderMismatch. - Servers must implement
server/discoverfor clients that want capabilities up front. Clients don’t have to call it before doing useful work.
Client identity in clientInfo is descriptive metadata, not authentication. Don’t turn a caller-supplied agent name into an authorisation decision.
The release on one page
The TypeScript, Python, Go and C# SDKs supported the new revision on release day. The change is broader than removing one session header.
mindmap
root((MCP 2026-07-28))
Stateless core
No initialize handshake
No Mcp-Session-Id
Version and capabilities per request
Optional server/discover call
Routing headers
Mcp-Method
Mcp-Name
Mcp-Param headers via x-mcp-header
Cache hints
ttlMs freshness
cacheScope public or private
Deterministic tools/list order
Trace context
traceparent, tracestate, baggage
Carried in _meta
Server to client
Multi Round-Trip Requests
subscriptions/listen stream
No resumable SSE
Authorisation
Validate iss when present
Credentials bound to issuer
DCR deprecated for CIMD
Extensions and deprecations
Tasks move to an extension
Roots, Sampling, Logging deprecated
Legacy HTTP+SSE deprecatedTasks move out of the core into the formal extensions framework, alongside extensions such as MCP Apps. Roots, Sampling and Logging are deprecated, not immediately removed; new implementations shouldn’t build on them.
For gateway operators, the practical changes are independent requests, visible routing information, explicit cache hints and a different way to continue work when a tool needs more input.
What you can remove, and what you can’t
A server keeping transport sessions in memory had three common deployment choices.
Session affinity kept requests on one replica. In Azure Container Apps, affinity is cookie-based and requires single revision mode with HTTP ingress. API Management backend-pool session awareness also uses cookies. Neither is automatically a routing mechanism for Mcp-Session-Id, so the client had to preserve the right cookies too.
A shared session store let replicas find the same state, with an event store if you also wanted resumable streams. That solved a problem, but added infrastructure, latency and another access boundary.
One replica avoided cross-replica coordination altogether. It also left the workload dependent on that replica.
%%{init: {"htmlLabels": false, "flowchart": {"htmlLabels": false, "nodeSpacing": 15, "rankSpacing": 30, "padding": 10}}}%%
flowchart TB
accTitle: Removing protocol session infrastructure
accDescr: Legacy deployments needed affinity or shared session storage to recover session state. The new protocol lets a load balancer send each request to any ready replica without a protocol session store.
subgraph OLDWAY["Before: find the session state"]
direction TB
C1["Agent"] --> LB1["Load balancer
session-aware routing"]
LB1 --> A1["Instance A"]
LB1 --> B1["Instance B"]
A1 --- S1[("Shared session store
if replicas share state")]
B1 --- S1
end
subgraph NEWWAY["After: any request, any ready instance"]
direction TB
C2["Agent"] --> LB2["Load balancer
plain round robin"]
LB2 --> A2["Instance A"]
LB2 --> B2["Instance B"]
LB2 --> D2["Instance C"]
end
classDef historical fill:#f6f8fa,stroke:#8c959f,color:#000,stroke-dasharray:5 5
classDef focus fill:#cce5ff,stroke:#1f6feb,color:#000
classDef output fill:#e6ffed,stroke:#2da44e,color:#000
classDef data fill:#fff4c2,stroke:#b7950b,color:#000
class C1,LB1,A1,B1 historical
class S1 data
class C2,LB2 focus
class A2,B2,D2 outputWith 2026-07-28, you can remove affinity and storage that existed solely for MCP transport sessions. Container Apps can scale in without stranding a protocol session, multiple revisions can participate in traffic splitting, and API Management pools can use ordinary round robin, weighted or priority routing.
That doesn’t make an application stateless. A basket, browser session or report still needs somewhere to live. Return an explicit handle from one tool and accept it as an argument on the next. Treat the handle as a name, not a credential: authorise access on every call, and put its lifetime in the tool description so the model can see it.
There’s also no promise that an interrupted request did nothing. A broken response stream loses the in-flight response, and a retry is a new request with a new ID. Side-effecting tools need an application-level idempotency key or a way to check the original outcome before repeating the action.
Route and govern each tool from the headers
This is the change I’m most interested in. Mcp-Method and Mcp-Name let a gateway make per-tool decisions without buffering and parsing the JSON body.
A server can also mark selected tool parameters with x-mcp-header. Conforming clients mirror those values into Mcp-Param-{Name} headers. For example, "x-mcp-header": "Region" on a region parameter produces Mcp-Param-Region.
Don’t mirror passwords, tokens or personal data. These values become visible to network intermediaries.
The trust boundary matters more than the convenience. A client can put a harmless tool name in a header and a destructive one in the body. The backend must compare them before execution, and the gateway must only rely on those headers for protocol revisions whose servers enforce that comparison.
%%{init: {"htmlLabels": false, "flowchart": {"htmlLabels": false, "nodeSpacing": 15, "rankSpacing": 30, "padding": 10}}}%%
flowchart TB
accTitle: Per-tool routing from validated MCP request headers
accDescr: The gateway checks the protocol version before using method and name headers for routing. The selected MCP backend must validate the headers against the body before executing a tool.
REQ["POST /mcp
version, method and name"]
VER{"Validated revision
2026-07-28?"}
LEGACY["Reject or use a legacy endpoint
without header-based policy"]
METHOD{"Mcp-Method"}
LIST["tools/list
client caching"]
LISTEN["subscriptions/listen
streaming settings"]
NAME{"Mcp-Name"}
HEAVY["run_report
report pool, tighter limit"]
STD["Other tools
standard pool and limit"]
CHECK["MCP server
validate headers against body
before execution"]
REQ --> VER
VER -->|"no"| LEGACY
VER -->|"yes"| METHOD
METHOD -->|"tools/list"| LIST
METHOD -->|"subscriptions/listen"| LISTEN
METHOD -->|"tools/call"| NAME
NAME -->|"run_report"| HEAVY
NAME -->|"other tool"| STD
HEAVY --> CHECK
STD --> CHECK
classDef focus fill:#cce5ff,stroke:#1f6feb,color:#000
classDef output fill:#e6ffed,stroke:#2da44e,color:#000
classDef data fill:#fff4c2,stroke:#b7950b,color:#000
classDef warning fill:#ffd6d6,stroke:#c0392b,color:#000
class REQ,VER,METHOD,NAME focus
class LIST,LISTEN,HEAVY,STD output
class CHECK data
class LEGACY warningHere’s the core pattern as an API Management policy at MCP server scope on supported classic or v2 tiers. Replace the backend ID and tool name with your own. The inherited inbound policy must already enforce your authentication and authorisation requirements; the counter key below is a throttling bucket, not proof of identity.
1 | <policies> |
This deliberately limited example uses plain ASCII names and rejects Base64-encoded names. A general implementation must decode the specification’s =?base64?...?= sentinel format before comparing Mcp-Name or mirrored parameters. Otherwise an encoded run_report could miss the stricter branch.
The report pool must expose the same tool contract advertised by discovery and enforce the same header and body validation. Keep incompatible protocol revisions in separate pools. If you serve older clients, give them a compatible endpoint rather than weakening the version gate.
At MCP server scope, policies apply across the server’s tools. Per-tool behaviour comes from conditions like these, not from assuming that each tool has its own REST operation policy.
Keep the response body out of that policy. Microsoft’s MCP guidance warns that accessing context.Response.Body forces buffering and can break streaming. Check that the inherited forward-request uses buffer-response="false" for SSE; the example inherits backend policy rather than replacing it.
Cache hints aren’t an HTTP response cache
Results from tools/list, prompts/list, resources/list, resources/read and resources/templates/list now carry ttlMs and cacheScope. The first is a freshness hint in milliseconds; the second controls whether shared intermediaries may cache the result.
Tool lists may vary with the caller’s authorisation, even though they must no longer vary with connection state. Keep caller-specific results private, partition caches by authorisation context and return tools in a deterministic order.
API Management’s built-in cache-lookup policy only performs response lookups for GET requests. MCP messages use POST. It won’t automatically become an MCP cache because the result contains ttlMs.
For this design, let the client honour the hints and leave the gateway out of response caching. A protocol-aware shared cache is a separate implementation, not something cacheScope: "public" creates for you.
When a tool needs more input
Sometimes a tool needs a confirmation, a missing parameter or an answer from the client’s model. Older revisions sent server-initiated requests such as elicitation/create, sampling/createMessage or roots/list down a held-open stream.
Multi Round-Trip Requests, or MRTR, replace that exchange. The server returns resultType: "input_required", its questions in inputRequests and, optionally, opaque requestState. The client gathers the answers and retries the original call with a new JSON-RPC ID, the answers in inputResponses and exactly the state it received.
MRTR applies to tools/call, resources/read and prompts/get.
sequenceDiagram accTitle: A multi round-trip request across two instances accDescr: Instance A asks for user input and returns protected request state. The client gathers consent, then sends a new request with the answer and unchanged state. Instance B verifies the state and completes the tool call. autonumber participant U as User participant AG as Agent participant GW as Gateway participant A as Instance A participant B as Instance B AG->>GW: tools/call delete_files, id 1 GW->>A: any ready instance A-->>GW: input_required
inputRequests and requestState GW-->>AG: passed back unchanged AG->>U: Delete 3 files? U-->>AG: Yes AG->>GW: tools/call delete_files, id 2
inputResponses and unchanged requestState GW->>B: another ready instance Note over B: Verify integrity, caller,
expiry and original request B-->>GW: resultType complete GW-->>AG: result
The first request can land on A and the retry on B. In a self-contained implementation, the replicas don’t need to share the original request’s in-memory state.
They do need to agree on how to trust requestState. The MRTR specification treats it as attacker-controlled input. If it influences authorisation, resource access or business logic, protect its integrity, for example with HMAC or AEAD, and reject failed verification.
Bind it to the authenticated caller, a short expiry and the original request. Those checks limit replay; they don’t guarantee single use. A one-time approval or redemption still needs server-side enforcement of that invariant.
On Azure, I’d keep the protection key in Key Vault, let each replica access it through managed identity and overlap current and previous keys during rotation. A retry routed to the reporting pool must reach a replica that understands the state and can verify it.
Don’t confuse a follow-up request for consent with proof of consent. The client must actually obtain the user’s answer, and the server still has to authorise the operation.
One trace from agent to tool
The specification now reserves traceparent, tracestate and baggage in _meta, using W3C Trace Context and Baggage formats. Several implementations already used those names; the convention now has a standard home.
There are still two propagation paths. MCP carries context in the body, while HTTP intermediaries work with headers. API Management’s Application Insights diagnostic supports W3C correlation, which injects Trace Context headers.
I’d instrument the agent’s HTTP client and propagate the same trace ID through both the HTTP header and MCP metadata. That’s the same trace ID, not a requirement to freeze the complete traceparent value: parent span IDs change as services create child spans.
%%{init: {"htmlLabels": false, "flowchart": {"htmlLabels": false, "nodeSpacing": 15, "rankSpacing": 30, "padding": 10}}}%%
flowchart TB
accTitle: Trace propagation through API Management to an MCP server
accDescr: The agent supplies HTTP and MCP trace context. API Management creates correlated gateway telemetry and forwards HTTP context. The MCP server continues the downstream HTTP parent, with all telemetry exported to Application Insights.
AG["Agent
starts trace 0af7..."]
GW["API Management
W3C correlation
gateway and dependency spans"]
SRV["MCP server on Container Apps
OpenTelemetry instrumentation"]
DS["Downstream API or database"]
AI[("Application Insights
shared trace ID")]
AG -->|"traceparent header
and _meta.traceparent"| GW
GW -->|"downstream traceparent
and x-correlation-id"| SRV
SRV -->|"child trace context"| DS
AG -.->|"agent spans"| AI
GW -.->|"gateway telemetry"| AI
SRV -.->|"server and tool spans"| AI
classDef focus fill:#cce5ff,stroke:#1f6feb,color:#000
classDef output fill:#e6ffed,stroke:#2da44e,color:#000
classDef data fill:#fff4c2,stroke:#b7950b,color:#000
class GW focus
class AG,SRV,DS output
class AI dataMake the server’s parent-selection behaviour explicit. To preserve the gateway as part of the parent chain, HTTP server instrumentation should continue the downstream HTTP context rather than replacing it with the older agent parent still in _meta. Check how your MCP instrumentation handles both, and avoid creating duplicate root spans.
I keep a correlation ID alongside tracing because it’s useful in a support ticket. These sections belong inside an existing <policies> document, merged with its current inbound and outbound sections.
1 | <inbound> |
Treat a caller-supplied correlation ID as untrusted diagnostic text, never an identity or an access token. The trace record reaches Application Insights only when its severity meets the diagnostic’s configured verbosity.
For the Application Insights tables, this KQL query brings the journey back in time order:
1 | union requests, dependencies, traces |
Use the corresponding AppRequests, AppDependencies and AppTraces schema if you query the Log Analytics workspace tables instead.
One MCP-specific setting is easy to miss: if Application Insights or Azure Monitor diagnostics are enabled at All APIs scope, set frontend response payload bytes to log to 0. Logging the response body can interfere with streaming, just as reading it in a policy can.
Azure AI Gateway isn’t just another policy editor
Microsoft introduced the AI Gateway tier in public preview on 27 July. It grouped model connections, federated MCP servers, governance and telemetry into an AI-focused experience rather than a general API catalogue.
The October change makes the naming more important: the dedicated AI Gateway product and AI gateway capabilities in API Management service are related, but they aren’t the same resource or management model.
With the dedicated product, applications use model aliases and MCP endpoints while the platform team manages backend connections. A federated MCP server can combine remote MCP servers, OpenAPI-described APIs and built-in connectors.
%%{init: {"htmlLabels": false, "flowchart": {"htmlLabels": false, "nodeSpacing": 15, "rankSpacing": 30, "padding": 10}}}%%
flowchart TB
accTitle: A dedicated AI Gateway for models and tools
accDescr: Applications authenticate to AI Gateway. Model aliases route to registered model backends, while federated MCP endpoints combine remote servers, OpenAPI APIs and connectors. Gateway telemetry goes to a configured destination.
APP["Agents and apps"]
GW["AI Gateway preview
caller access and AI policies"]
MOD["Model aliases
registered model backends"]
TS["Federated MCP server
one governed endpoint"]
R1["Remote MCP server"]
R2["OpenAPI API"]
R3["Built-in connector"]
OT[("Configured telemetry
Application Insights or OTLP")]
APP -->|"Entra runtime access
or gateway runtime key"| GW
GW --> MOD
GW --> TS
TS --> R1
TS --> R2
TS --> R3
GW -.-> OT
classDef focus fill:#cce5ff,stroke:#1f6feb,color:#000
classDef output fill:#e6ffed,stroke:#2da44e,color:#000
classDef data fill:#fff4c2,stroke:#b7950b,color:#000
class GW focus
class APP,MOD,TS,R1,R2,R3 output
class OT dataBackend authentication is separate from caller access. A managed identity or stored backend credential determines what the gateway can do upstream; it doesn’t automatically limit each caller to that caller’s own downstream permissions.
The current concepts guide also makes an important boundary explicit: runtime API keys grant gateway-wide access. Separate keys help attribution and rotation, but don’t create per-tool least privilege. Use separate gateways where trust boundaries require them.
The AI-focused policy cards aren’t the XML policies shown earlier. Check the controls supported by your actual resource type rather than assuming a card from the July launch is available on the replacement.
The October resource migration
From 14 October, AI Gateway moved from the AIGateway SKU of Microsoft.ApiManagement/service to Microsoft.ApiManagement/aigateways.
The migration guidance, checked on 4 November, says existing AIGateway SKU resources work until 15 January 2027 and are then permanently removed. New resources using that older representation can’t be created.
Migration creates a new resource, not an in-place type change. Applications need its new runtime hostname under ai.gateway.azure.com and a new runtime key. Key-based requests change from api-key to Authorization: Bearer <key>. Read the hostname from the resource; don’t construct it yourself.
Resource-scoped role assignments must be recreated. A new system-assigned identity also needs its own permissions to backends and telemetry destinations.
At the same check date, the migration guide lists no private endpoint support, no content safety policy support and no ability to change the virtual network or subnet after creation. Those aren’t small details if your pilot assumes private ingress or a particular safety control.
The documented preview regions remain East US 2 and Sweden Central. The July launch offered the preview at no cost with pricing to follow; don’t treat that launch statement as a permanent price or an availability commitment.
Where should your MCP servers sit?
I’d start with workload requirements, not with the newest portal.
%%{init: {"htmlLabels": false, "flowchart": {"htmlLabels": false, "nodeSpacing": 15, "rankSpacing": 30, "padding": 10}}}%%
flowchart TB
accTitle: Choosing between API Management service and dedicated AI Gateway
accDescr: A required production SLA, an unsuitable preview region or custom XML policy logic points to an appropriate API Management service tier. A compatible AI-focused pilot can evaluate the dedicated AI Gateway preview.
Q1{"Need a supported
production SLA?"}
Q2{"Can the request path use
East US 2 or Sweden Central?"}
Q3{"Need custom policy logic
or unsupported preview controls?"}
CL["API Management service
appropriate production tier
MCP support and XML policies"]
AGT["Dedicated AI Gateway preview
evaluate with suitable pilot data"]
Q1 -->|"yes"| CL
Q1 -->|"no, a pilot"| Q2
Q2 -->|"no"| CL
Q2 -->|"yes"| Q3
Q3 -->|"yes"| CL
Q3 -->|"no"| AGT
classDef focus fill:#cce5ff,stroke:#1f6feb,color:#000
classDef output fill:#e6ffed,stroke:#2da44e,color:#000
classDef data fill:#fff4c2,stroke:#b7950b,color:#000
class Q1,Q2,Q3 focus
class CL output
class AGT dataFor New Zealand and Australian customers, the region question can settle it before policy features do. A US or European gateway in front of Australian backends still puts prompts and tool payloads through that region. Review the whole request path against your residency obligations.
For production MCP traffic that needs established API management controls, I’d keep the supported classic or v2 service tier that meets those requirements. The Developer tier is not a production SLA choice.
For an AI-focused pilot, the dedicated gateway is worth evaluating, provided its regions, networking, controls and preview terms fit. Microsoft’s product comparison confirms that existing AI workloads in API Management remain supported; adopting the dedicated product doesn’t require moving everything.
Clients must know who issued their credentials
The authorisation changes are less visible than the new transport, but matter just as much for agents connecting to several servers.
Authorisation servers should include iss in their responses, following RFC 9207. When present, clients must check it against the recorded issuer before redeeming an authorisation code.
Stored client credentials belong to the issuer that minted them. Clients must key them by issuer, must not reuse them with another authorisation server and must register again when that server changes.
Clients also declare an appropriate application_type during Dynamic Client Registration. DCR itself is deprecated in favour of Client ID Metadata Documents, while remaining available for backwards compatibility.
sequenceDiagram accTitle: Credentials stay bound to their issuer accDescr: A client compares discovered authorisation metadata with the issuer recorded for its credentials. It uses those credentials only with the matching issuer and never sends them to a different server. autonumber participant C as MCP client participant M as MCP server participant ISS as Recorded issuer participant OTHER as Other issuer C->>M: tools/call M-->>C: 401 and resource metadata Note over C: Compare discovered issuer
with recorded credential issuer alt issuer matches C->>ISS: token request with bound credentials ISS-->>C: access token else issuer differs C--xOTHER: stored credentials never sent Note over C: A different issuer needs
a separate trusted registration end
A governed gateway protected by a known Entra issuer can reduce the number of issuers an agent needs to trust. That only helps if clients validate the issuer and the gateway validates tokens and tool access correctly.
SDK sign-in fixes: update checked 4 November 2026
The SDK advisories make this concrete. The official Python and TypeScript SDKs had OAuth clients that let a malicious or compromised MCP server influence where credentials were sent.
For Python’s issuer-binding advisory, the fixes are mcp 1.30.0 and 2.2.0 or later. Upgrading alone isn’t enough for ClientCredentialsOAuthProvider or PrivateKeyJWTOAuthProvider: configure issuer= as well. Clear registrations stored without an issuer so the client registers again, or bind pre-registered records to their legitimate issuer.
For TypeScript’s issuer-binding advisory, the fixes start at @modelcontextprotocol/sdk 1.31.0 and @modelcontextprotocol/client 2.2.0. Bundled providers need expectedIssuer; stored credentials need issuer, or must be cleared. Custom OAuthClientProvider implementations must persist the issuer passed to their save methods. If you import @modelcontextprotocol/core directly, update that too.
A separate TypeScript cross-origin redirect advisory is fixed in SDK 1.32.0 and client 2.3.0. Those higher versions address both advisory sets, but don’t remove the issuer configuration requirements. Configure final endpoints rather than restoring unrestricted redirect following.
Only complete new interactive sign-ins for servers you trust. If an affected client may have sent credentials to an untrusted destination, rotate the exposed secret or signing key and revoke its tokens. These advisories concern affected HTTP clients, not every MCP server built with those SDKs.
Eight checks before go-live
API Management’s existing-server guide specifies MCP 2025-06-18 or later over Streamable HTTP or SSE. That minimum is not evidence that every 2026-07-28 interaction works through your chosen configuration. The guide currently supports tools and resources for external servers, but not prompts, and doesn’t support MCP server capabilities in workspaces.
Run these checks through the actual endpoint, policies and backends you intend to use. For a federating gateway, test its public MCP surface as well as each upstream connection; federation isn’t necessarily transparent proxying.
1. A call gets through without a handshake
Send tools/call with the required headers and _meta, without initialize. Expect a successful response with resultType: "complete" and no transport session minted along the path.
2. Required headers reach the validating component
Verify MCP-Protocol-Version, Mcp-Method, Mcp-Name and applicable Mcp-Param-* values. On a proxy path, confirm they arrive unchanged. If a federation layer rewrites a request, verify that it creates matching upstream headers and metadata.
3. Header and body disagreements are refused
Send Mcp-Name: get_weather with a body calling another tool. Expect HTTP 400 and the server’s -32020 error. Also test missing required headers, mismatched versions and encoded names. No tool should execute when validation fails.
4. A continuation can reach another replica
Use three replicas without affinity and a tool returning input_required. Confirm the retry succeeds on a different replica. Also try modified, expired and wrong-caller requestState; those must fail before the action runs.
5. Progress isn’t buffered
Use a slow tool that supports notifications/progress, and request progress with _meta.progressToken. Events should arrive while the tool runs, not in a burst at the end.
API Management documents buffer-response="false" on forward-request for SSE backends. Check inherited policies and disable frontend response body logging at All APIs scope.
6. A listen stream survives quiet periods
Where the server supports subscriptions, open subscriptions/listen and leave it idle beyond four minutes. Confirm keep-alive comments preserve the stream, or the client reconnects and re-establishes the subscription correctly.
The forward-request documentation notes that network infrastructure can drop idle connections after about 240 seconds. The MCP specification encourages periodic SSE comments on long-lived streams. This is not resumable SSE with Last-Event-ID.
7. The trace joins up
Send one trace ID through the HTTP and MCP contexts. Check operation_Id across agent, gateway and server, then inspect the parent-child relationships. Sharing a trace ID is necessary, but it doesn’t prove the span tree is correct.
8. Cache hints survive at the public surface
Check tools/list for sensible ttlMs and cacheScope. For caller-filtered catalogues, verify private scope and cache isolation. A federated server may generate its own catalogue and hints rather than copy a backend’s values.
For self-hosted deployments, Microsoft’s open-source MCP Gateway now documents a
2026-07-28-only path, with no legacy handshake or transport sessions. Its migration guide is a useful implementation reference, not a compatibility guarantee for managed API Management or dedicated AI Gateway.
Put numbers behind the scaling argument
I haven’t run a new benchmark for this article. This is the harness I’d use to measure the difference rather than assume a latency improvement.
%%{init: {"htmlLabels": false, "flowchart": {"htmlLabels": false, "nodeSpacing": 15, "rankSpacing": 30, "padding": 10}}}%%
flowchart TB
accTitle: A benchmark design for session-based and stateless MCP
accDescr: A k6 client runs the same tool-call mix against session-based and stateless servers with the same replica count, equivalent gateway limits and comparable instrumentation. A replica is removed during each run to measure recovery.
K6["k6 load generator
same tool-call mix"]
APIM["API Management
equivalent routing and limits"]
subgraph RUN1["Run 1: 2025-11-25 with in-memory sessions"]
ACA1["Container Apps, 3 replicas
client preserves affinity cookies"]
end
subgraph RUN2["Run 2: 2026-07-28"]
ACA2["Container Apps, 3 replicas
no affinity"]
end
AI[("Application Insights
throughput, p95, errors
and replica distribution")]
K6 --> APIM
APIM --> ACA1
APIM --> ACA2
APIM -.-> AI
ACA1 -.-> AI
ACA2 -.-> AI
classDef historical fill:#f6f8fa,stroke:#8c959f,color:#000,stroke-dasharray:5 5
classDef focus fill:#cce5ff,stroke:#1f6feb,color:#000
classDef output fill:#e6ffed,stroke:#2da44e,color:#000
classDef data fill:#fff4c2,stroke:#b7950b,color:#000
class K6,APIM focus
class ACA1 historical
class ACA2 output
class AI dataKeep the tool implementation, load, replica count, limits and telemetry settings comparable. Use a version-aware comparison policy, not the earlier example’s single-revision gate, and confirm the legacy client actually preserves affinity cookies and session IDs.
Warm the backends first. As with the APIM fan-out benchmark, cold scale can tell a convincing but misleading story.
Partway through each run, remove a replica. Measure throughput, p95 latency, error rate, request distribution and recovery time.
With sessions held only on that replica, legacy clients have to establish new sessions. The stateless side has no transport session to recover, but in-flight calls can still fail and side effects can still have an uncertain outcome. That’s the distinction to measure, not a claim that stateless means zero disruption.
What to change at your gateway
- Remove affinity and storage used solely for
2026-07-28transport sessions, after checking application state and draining legacy traffic. - Gate mirrored-header policy by supported protocol version, and validate or decode header values before using them.
- Route and rate-limit per tool, while keeping header and body validation on every executing backend.
- Preserve streaming: no response body access in MCP policy, no frontend response body logging at All APIs scope, and no response buffering for SSE.
- Protect
requestStateacross replicas, overlap keys during rotation and enforce one-time consumption where required. - Join HTTP and MCP trace propagation deliberately, then inspect the span tree rather than only the trace ID.
- Give
subscriptions/listenkeep-alives and reconnection handling; don’t assume old SSE resumability still exists. - Let clients honour cache hints, isolate caller-specific catalogues and bind OAuth credentials to their issuer.
- Run the eight checks through the real gateway before go-live.
My take
The best part of this release is what it lets us stop doing. A remote MCP tool server can behave much more like an ordinary HTTP workload, without transport sessions deciding which replica gets the next request.
For gateways, the headers are the real prize. They make routing, metering and policy per tool practical without buffering bodies. That only works as a trust boundary when the server enforces the header and body contract.
The remaining state is more visible, not magically gone. Application handles, retry protection, consent and idempotency still deserve proper design.
I’d keep production traffic on the gateway that meets its actual operational requirements and evaluate dedicated AI Gateway where the preview fits. The protocol simplification is useful either way.
Key takeaways
- MCP
2026-07-28removes the handshake and protocol sessions, not application state or the possibility of interrupted calls. - Header-based policy needs a compatible revision, correct value decoding and backend validation before execution.
- MRTR can continue on another replica, but integrity protection alone doesn’t make an approval single-use.
- Streaming, trace parentage, cache isolation and issuer-bound credentials belong in the go-live gate.
- Dedicated AI Gateway is a separate preview resource with an October migration path; existing API Management AI workloads remain supported.
References
- MCP 2026-07-28 changelog
- Streamable HTTP: headers, validation, streaming and compatibility
- Multi Round-Trip Requests and request-state security
- MCP tools: catalogues, caching and mirrored parameters
- Expose and govern an existing MCP server in API Management
- API Management forward-request policy
- API Management and Application Insights integration
- AI Gateway concepts: federation, runtime access and identities
- Migrate from the AI Gateway tier to the dedicated resource
- Python SDK OAuth issuer-binding advisory
- TypeScript SDK OAuth issuer-binding advisory
- TypeScript SDK cross-origin redirect advisory
Related posts
- APIM Backend Fan-out: Shared Backend + rewrite-uri vs One Backend Per API (A Real Benchmark)
- Governance for AI Agents: The Gap, the OWASP Agentic Top 10, and Where AGT Fits
- Microsoft Foundry Cross-Region with Private Endpoints, Part 1
Image Credits
- Cover image generated by Copilot
MCP Goes Stateless: No Handshake, No Sticky Sessions, Better AI Gateways




