DEV Community

MCP Is Going Stateless: What Changed and How I Migrated My Currency Converter Server

The Model Context Protocol (MCP) has been evolving quickly. One of the most interesting changes in the latest MCP specification is the move toward a stateless protocol model. I recently updated my MCP currency converter server to work with the newer stateless behavior and the new split TypeScript SDK packages, particularly @modelcontextprotocol/server . In this article, I'll explain: - What MCP sessions were doing - What "stateless MCP" actually means - Why the change matters for production systems - How Streamable HTTP changes with the new specification - How I migrated my currency converter MCP server - What this means for scaling MCP servers What Is MCP? If you're new to MCP, the Model Context Protocol is a standard for connecting AI applications to external tools, resources, and data. Instead of building custom integrations between every AI application and every external service, MCP provides a common protocol. For example, an AI assistant can use an MCP server exposing a tool like: convert_currency The model can then request: Convert 100 USD to EUR. The MCP client communicates with the MCP server, which performs the actual operation and returns the result. MCP servers can expose several primitives, including tools, resources, and prompts. For my example, the server is intentionally simple: it exposes currency-conversion functionality. The Old Mental Model: MCP Sessions Before the stateless changes, Streamable HTTP could maintain a protocol-level session. Conceptually, the flow looked something like this: Client | | POST /mcp | initialize v MCP Server | | Mcp-Session-Id v Client | | POST /mcp | Mcp-Session-Id: abc123 v MCP Server The server creates a session during initialization. Subsequent requests contain the session identifier. That means the server can associate requests with the session that was established earlier. This isn't necessarily bad. Session state can be useful when an application genuinely needs conversational or connection-level state. But it creates an architectural problem when we want MCP servers to behave like ordinary horizontally scalable HTTP services. Why Stateful HTTP Can Become a Problem Imagine deploying three MCP server instances: โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Load โ”‚ โ”‚ Balancer โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ | โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” | | | v v v Server A Server B Server C Suppose the initialization request reaches Server A. The session now belongs to Server A. What happens when the next request reaches Server B? Server B doesn't necessarily know anything about that session. You now need mechanisms such as: - Sticky sessions - Shared session storage - Session replication - Distributed coordination That adds complexity to the deployment architecture. The latest MCP direction removes this protocol-level session dependency. The MCP team describes the goal as allowing requests to reach any server instance without requiring sticky routing or a shared protocol session store. (Model Context Protocol Blog) Enter Stateless MCP The key idea is surprisingly simple: Each request should be understandable independently. Instead of relying on a protocol-level session established earlier, the request contains the information necessary for the server to process it. Conceptually: Request 1 | v Server A Request 2 | v Server C Request 3 | v Server B There is no requirement that all three requests reach the same instance. This is a much more natural fit for cloud-native architectures. Stateless Does NOT Mean Your Application Cannot Have State This is an important distinction. Stateless MCP does not mean your entire application must be stateless. Suppose you have a shopping-cart tool: create_cart() The server can return: { "cart_id": "cart_123" } The next tool call can explicitly provide: { "cart_id": "cart_123", "product_id": "product_456" } The application still has state. But that state is represented explicitly by application data rather than being hidden inside an MCP protocol session. The MCP team explicitly recommends this type of application-level state when state needs to persist between calls. (Model Context Protocol Blog) That's a subtle but important architectural improvement. What Changed in Streamable HTTP? Streamable HTTP is the important transport for remote MCP servers. The 2026-07-28 specification changed its behavior significantly. The new model removes: - Protocol-level sessions - The Mcp-Session-Id - The standalone GET stream endpoint Instead, the server exposes a single MCP endpoint that accepts POST requests. Each JSON-RPC request is sent as its own HTTP request. The server can return either: application/json or an SSE stream associated with that request. (GitHub) So the mental model becomes: POST /mcp | v +----------------+ | MCP Server | | | | Process request| | | | Return result | +----------------+ Rather than: Initialize session | v Maintain session | v Process requests | v Eventually close session My Currency Converter MCP Server To understand the change practically, I used my currency converter MCP server. The server exposes currency conversion as an MCP tool. The architecture is roughly: AI Application | | MCP v โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Currency MCP โ”‚ โ”‚ Server โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ | v Exchange Rate API The server doesn't need conversational state. For example: Convert 100 USD to EUR is completely independent from: Convert 500 GBP to USD There is no reason for the server to maintain a protocol session between these requests. That makes the currency converter a good candidate for a stateless MCP server. The New TypeScript SDK Packages Another change I encountered while updating the project was the move to the split TypeScript SDK packages. Instead of relying on the older monolithic package structure, the SDK now provides separate packages such as: @modelcontextprotocol/server @modelcontextprotocol/client The server package is responsible for building MCP servers, while the client package provides client functionality. (GitHub) For my server, I use: npm install @modelcontextprotocol/server along with the required schema library. This separation makes the dependency boundaries much clearer. Stateless Transport Configuration One of the important concepts when working with Streamable HTTP is the absence of a session ID generator. In the earlier SDK transport model, a stateful server could configure something like: const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), }); The transport then generates and manages a session identifier. For stateless operation, the session ID generator is omitted: const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); The SDK documentation describes this as stateless mode: no session ID is returned and no session validation is performed. (GitHub) However, there is an important nuance here. Don't confuse this older SDK configuration with the new 2026 protocol itself. The 2026 specification removes protocol-level sessions altogether. The SDK is evolving to provide higher-level APIs around the new protocol behavior as well. (GitHub) Why Stateless MCP Is Interesting for Cloud Deployments This is probably the biggest reason I find this change interesting. Consider a Kubernetes deployment: Load Balancer | โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” | | | v v v Pod 1 Pod 2 Pod 3 With protocol-level sessions, routing could become: Client A โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> Pod 1 Client A โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> Pod 1 Client A โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> Pod 1 because the session belongs to Pod 1. With a stateless protocol, requests can be distributed naturally: Client A โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> Pod 1 Client A โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> Pod 3 Client A โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> Pod 2 Client A โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> Pod 1 The load balancer doesn't need to understand MCP session ownership. This makes horizontal scaling significantly simpler. Stateless MCP Fits Serverless Architectures Better This is another area where the change becomes interesting. Consider AWS Lambda. A typical architecture might look like: MCP Client | v API Gateway | v Lambda | v Exchange Rate API With stateless requests, each invocation can independently process an MCP request. There is less reason to keep a warm process specifically because it owns a particular MCP session. This aligns nicely with the serverless execution model. Of course, long-running streaming operations and application-specific state can still introduce additional considerations. Stateless doesn't magically eliminate all stateful requirements. What About Stateful Applications? Suppose I later extend my currency server with something like: create_conversion_watch A user might create a watch: create_conversion_watch( from = USD, to = EUR, threshold = 0.95 ) The server could return: { "watch_id": "watch_123" } The watch itself could be persisted in: PostgreSQL or: Redis or another durable store. Later: get_conversion_watch( watch_id = "watch_123" ) The MCP protocol doesn't need to maintain a session. The application owns the state. That's a much cleaner separation of responsibilities: MCP | | Protocol communication | v Application | | Business state | v Database Stateless Doesn't Mean "No Context" This is another misconception worth addressing. An MCP request can still contain metadata and application parameters. The difference is that we're no longer relying on an implicit protocol session to reconstruct the context. The newer protocol explicitly moves toward per-request information. The TypeScript SDK documentation describes the 2026 protocol as carrying request-level information such as protocol version and client information rather than relying on an initialization-scoped identity. (GitHub) This makes each request more self-contained. What I Like About This Change After migrating my currency converter server, the biggest architectural benefit I see is simplification. Instead of thinking: Where does this client's MCP session live? I can think: Can this request be processed independently? For many tools, the answer is yes. That makes MCP much ea

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.