Stateless, Streamable & How We Uplifted
Matthew Khouzam | Ericsson Research | August 2026
Why the spec needed to change
SSE was a workaround, not a design choice. It leaked complexity into every deployment.
The headline changes
initialize/initialized handshakeMcp-Session-Id header (removed entirely)2025-03-26 made sessions optional. 2026-07-28 removes them entirely.
_metaserver/discover RPC for up-front capability discovery_meta.io.modelcontextprotocol/protocolVersion_meta.io.modelcontextprotocol/clientCapabilities_meta.io.modelcontextprotocol/clientInfo_meta.io.modelcontextprotocol/serverInfo in results/.well-known/oauth-authorization-serverWWW-AuthenticateDirect response to enterprise feedback: orgs couldn't adopt MCP without central IT control over which servers employees connect to.
InputRequiredResult with resultType: "input_required"inputResponsesresultType fieldServers no longer initiate JSON-RPC requests. Client always drives the interaction.
subscriptions/listen replaces GET endpoint and resource subscriptionsping, logging/setLevel, notifications/roots/list_changedio.modelcontextprotocol/tasks)ttlMs and cacheScope on list resultssubscriptions/listen consolidates all server→client notifications into one stream:
One unified pattern for all server-initiated notifications. No more per-feature subscription APIs.
Spec: modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions
Advanced behaviours modelled as optional, self-contained modules:
postMessage + JSON-RPCtaskIdtasks/gettasks/updateNew headers for infrastructure-level routing:
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
Content-Type: application/json
Mcp-Method and Mcp-Name let load balancers and gateways route traffic by operation without parsing the JSON body.
Use explicit handles instead of hidden session state:
1. Client calls: create_checkout({items: ["widget-a", "widget-b"]})
Server returns: {basket_id: "bsk_8f3a", status: "created"}
2. Client calls: add_shipping(basket_id: "bsk_8f3a", address: {...})
Server returns: {basket_id: "bsk_8f3a", status: "ready_to_pay"}
3. Client calls: confirm_order(basket_id: "bsk_8f3a")
Server returns: {order_id: "ord_91cb", status: "confirmed"}
The LLM can reason about explicit handles, compose them across tools, and hand them off between workflow steps. Hidden session state was invisible to the model.
From sessions to stateless
| Aspect | 2025-03-26 (Old) | 2026-07-28 (New) |
|---|---|---|
| Initialization | initialize/initialized handshake | No handshake; self-describing requests |
| Session | Optional (Mcp-Session-Id MAY) | Removed entirely |
| Capabilities | Exchanged once at init | Carried per-request in _meta |
| Aspect | 2025-03-26 (Old) | 2026-07-28 (New) |
|---|---|---|
| Server discovery | Via initialize response | server/discover RPC |
| Server requests | Server MAY send requests to client | Removed; MRTR pattern instead |
| Transport | Streamable HTTP + stdio | Streamable HTTP + stdio (unchanged) |
// Every request now carries its own context
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "list_experiments",
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {
"name": "MyClient", "version": "1.0"
}
}
},
"id": 1
}
Old (2025-03-26): Server sends request to client
Server → elicitation/create request to client
Client → responds with user input
New (2026-07-28): Server returns InputRequiredResult
Client → POST tools/call
Server → InputRequiredResult (resultType: "input_required")
Client → retries with inputResponses
Server → final result (resultType: "complete")
Python harness-level uplift
Before (SDK 1.27.0)
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp import Image
mcp = FastMCP("tmll-cli-mcp-server")
After (SDK 2.0.0)
from mcp.server import MCPServer
from mcp.server.mcpserver import Image
mcp = MCPServer("tmll-cli-mcp-server")
Core change: FastMCP → MCPServer class rename. API surface identical.
mcp.server.fastmcp → mcp.server / mcp.server.mcpserver_mcp_server internal access@mcp.tool() decorators work the samefrom mcp.types import ToolAnnotations
@mcp.tool(annotations=ToolAnnotations(
readOnlyHint=True,
destructiveHint=False,
openWorldHint=False
))
async def list_experiments() -> str:
"""List all open experiments."""
return await run_cli(["list-experiments"])
@mcp.tool(annotations=ToolAnnotations(
readOnlyHint=True,
destructiveHint=False,
openWorldHint=True
))
async def detect_anomalies(...) -> str:
"""Run anomaly detection on trace data."""
...
14 tools total: most readOnly. Only delete_experiment and ensure_server are destructive. detect_anomalies is openWorld (contacts trace server).
Uplift complexity: LOW
Total: 1 commit, ~20 lines changed
If your server is stdio-only and stateless, the uplift is trivial.
MCP harness (client) uplift
A server only needs to speak one protocol version. A harness must speak all of them.
initialize/mcp endpointBefore
// Session-tracking transport
const transport =
new StreamableHTTPServerTransport({
sessionIdGenerator: () =>
randomUUID(),
});
// Map of active sessions
httpTransports.set(
sessionId, transport
);
After (2026-07-28)
// Stateless no session tracking
const transport =
new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
// Per-request, no map needed
// Transport created and disposed
// within request handler
async doStart(): Promise<void> {
try {
// Try Streamable HTTP first (2026-07-28)
this.transport = new StreamableHTTPClientTransport(
url, { authProvider: this.oauthProvider }
);
await this.client.connect(this.transport);
} catch (e) {
// Fall back to SSE for legacy servers
this.transport = new SSEClientTransport(
url, { authProvider: this.oauthProvider }
);
await this.client.connect(this.transport);
}
}
Graceful degradation: StreamableHTTP → SSE fallback → stdio for local
+8,190 lines for the full OAuth stack
OAuth is the biggest single addition. Required credential store, callback handlers for browser and Electron, token refresh logic, and server lifecycle integration.
// Plugin authors register MCP server definitions
vscode.lm.registerMcpServerDefinitionProvider(
'my-tools', {
provideMcpServerDefinitions():
McpServerDefinition[] {
return [{
type: 'http',
name: 'My Remote Server',
url: 'https://my-server.com/mcp',
// OAuth handled automatically by Theia
}];
}
});
Supports two server definition types:
type: 'http'url server endpointtype: 'stdio'command + argsUplift complexity: HIGH
Total: multiple PRs, ~10K lines changed
Formal framework for protocol evolution
| Feature | Why Deprecated | Migration Path |
|---|---|---|
| Roots | Filesystem assumptions don't generalise to remote/cloud | Pass paths as tool params or server config |
| Sampling | Reverse dependency; complicates trust boundaries | Servers call LLM APIs directly |
| Logging | Redundant with existing observability infra | Use stderr or OpenTelemetry |
| Dynamic Client Reg. | Conflicts with enterprise-managed auth | Explicit OAuth app registration |
| HTTP+SSE Transport | Superseded by Streamable HTTP | Migrate to Streamable HTTP |
Not a removal notice - these still work today. Earliest removal: July 28, 2027.
| Date | Milestone |
|---|---|
| May 2026 | Release candidate published |
| July 28, 2026 | Final specification ships |
| July 28, 2027 | Earliest removal date for deprecated features |
Beta SDKs available for Python, TypeScript, Go, and C#
What you need to do
mcp>=2.0.0 · TS: @modelcontextprotocol/sdk@^1.30.0Mcp-Session-Id is goneserver/discover required RPC for capability advertisementresultType on all results ("complete" or "input_required")_meta on every request (protocol version, capabilities)server/discover for initial capability probingInputRequiredResult retry with inputResponsessubscriptions/listen for server notificationsinitialize handshakeserver/discover returns correct capabilitiesnpx @modelcontextprotocol/inspector test your server interactively
resultType on all tool call responses_meta fields propagate correctly| Dimension | TMLL (Python) | Theia (TypeScript) |
|---|---|---|
| Role | MCP Server only | Client + Server |
| Transport | stdio (unchanged) | StreamableHTTP + SSE + stdio |
| SDK | Python mcp 2.0.0 | TS @mcp/sdk 1.30.0 |
| Session | Already stateless | Removed session tracking |
| OAuth | N/A (local stdio) | Full OAuth 2.1 + PKCE |
| Tools | 14 (decorator-based) | Dynamic (contribution API) |
| Annotations | Added (readOnly, etc.) | Inherited from tool providers |
| Effort | ~1 day | ~2 weeks |
What this means for us
tmll/mcp/server.py branch mcp-2.063218242f stateless protocol uplift