Comparing the two MCP lifecycle eras: the legacy flow with the initialize handshake and session state based on Mcp-Session-Id, versus the modern stateless 2026-07-28 flow that sends version, identity, and capabilities per request and uses server/discover for bootstrapping.

In episode 2 we covered MCP's three-layer architecture and primitives. Now the question is: how does a connection start, stay alive, and end? The answer is what we call the lifecycle — and this is where MCP underwent its biggest overhaul on 2026-07-28.
This episode dissects two opposing eras: the legacy era that required an initialize handshake and stored session state, versus the modern era that is fully stateless. You'll understand why modern MCP servers can be deployed behind a load balancer without sticky sessions — something that was painful in the legacy era.
In specifications up to 2025-11-25, every MCP connection began with a mandatory initialize handshake:
initialize request containing the protocol version, client name, and capabilities.initialized notification to mark the handshake complete.tools/list.{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": { "name": "my-host", "version": "1.0.0" }
}
}The key to this era is session state: the server remembers the handshake information per connection and identifies the connection via the Mcp-Session-Id header. All subsequent requests must carry this Mcp-Session-Id, because the server needs to know who is speaking and which version is in use.
Session state creates a problem that's uncomfortable in production: the same request must always land on the same server instance. If there are two server instances behind a load balancer, the second instance won't recognize the session from the first — the result is an error or a re-handshake.
The usual workaround is sticky sessions on the load balancer. The problem is that sticky sessions limit load distribution, complicate rolling restarts, and constrain horizontal scaling. That's the big task the stateless era answered.
On 2026-07-28, the protocol core was reworked to be stateless. The principle is simple: no more handshake, no more session state. Every request stands on its own and carries all the information the server needs:
Because there's no state on the server, any request can be routed to any server. You can even place several different servers behind a single endpoint — a request is answered by whichever one is available. To try this stateless pattern, make sure your SDK is the latest version with npm install @modelcontextprotocol/sdk@latest.
Legacy : [initialize handshake] -> [session A] -> [request pakai Mcp-Session-Id]
Modern : [request mandiri + versi + identity + capabilities] -> [response]If there's no handshake, how does the client know the server's version and capabilities? The answer is server/discover — a new method that replaces the handshake's role for bootstrapping, not for establishing a session.
{
"jsonrpc": "2.0",
"id": 1,
"method": "server/discover",
"params": {
"protocolVersion": "2026-07-28",
"clientInfo": { "name": "my-host", "version": "1.0.0" }
}
}The server replies with the list of supported protocols, its capabilities, and the chosen version. The client then stores the result as context — but there's no "live" session on the server. Subsequent requests still carry their own version and identity information.
Info
Don't confuse this with episode 8 on MRTR: server/discover is a one-way bootstrap for negotiating version, whereas MRTR (Multi-Round-Trip Requests) is a mechanism for interactive mid-call requests via messageId and routingId. Both were born in the same era (2026-07-28) but serve different purposes.
The stateless decision opens the door wide for production operations:
| Capability | Legacy Era | Modern Era |
|---|---|---|
| Load balancer | Requires sticky sessions | Free (round-robin) |
| Rolling restarts | Complicated (sessions lost) | Safe (no sessions) |
| Horizontal scaling | Limited | Free |
| Health checks | Need to know the instance | Stateless, easy |
With statelessness, requests can be routed to any server — a round-robin load balancer simply distributes requests evenly without tracking sessions. Server down? The next request is served by another instance with no re-handshake. This is the foundation that makes large-scale MCP deployments (episode 9) and fleet management (episode 16) feasible.
Because the ecosystem still has old servers, modern clients are advised to be dual-era compatible: knowing how to talk to both legacy and modern servers. The pattern is simple:
initialize handshake (legacy era).const discovered = await tryStatelessDiscover();
if (discovered.failed) {
await initializeLegacyHandshake();
}Note: this is just a flow illustration. The full implementation using the official SDKs will be covered in episode 6 (server) and episode 7 (client), and the two-era compatibility strategy in more depth in episode 17.
In this episode 3 you've understood the two MCP lifecycle eras: the legacy era using the initialize handshake plus Mcp-Session-Id session state, and the modern 2026-07-28 era which is stateless with version, identity, and capabilities sent per request, supported by server/discover for bootstrapping.
Key takeaways:
initialize handshake and used Mcp-Session-Id to store state.server/discover replaces the handshake for version and capability bootstrapping.In the next episode 4 we'll cover Tools in depth — how a tool is defined with JSON Schema, called via tools/call, produces structured output, and what tool annotations are along with their limitations. See you in episode 4!