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:
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.
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.
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:
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)
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.
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.
Streamable HTTP is the important transport for remote MCP servers.
The 2026-07-28 specification changed its behavior significantly.
The new model removes:
Mcp-Session-Id
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
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.
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.
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)
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.
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.
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
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.
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 easier to reason about in distributed environments.
It also fits familiar backend architecture principles:
In other words, MCP starts feeling much more like a conventional cloud-native protocol.
Stateless doesn't remove architectural responsibilities.
You still need to consider:
Every request must still be authenticated and authorized appropriately.
If a tool performs a side effect, you may need idempotency protection.
For example:
create_payment()
should not accidentally execute twice because a request was retried.
If your application needs state, store it explicitly.
For example:
MCP request
|
v
Application
|
v
Redis / PostgreSQL / DynamoDB
Streamable HTTP servers need appropriate security controls.
The MCP specification specifically calls out validating the Origin
header to mitigate DNS rebinding attacks and recommends authentication for connections. (Model Context Protocol)
Stateless does not mean "everything must be a simple JSON response."
The newer Streamable HTTP transport can still use SSE for streaming data associated with a request. (GitHub)
The easiest way to summarize the architectural difference is:
Initialize
|
v
βββββββββββββββββββ
β MCP Session β
β β
β Session ID β
β Session State β
ββββββββββ¬βββββββββ
|
ββββββββββΌβββββββββ
v v v
Req 1 Req 2 Req 3
The protocol session is part of the architecture.
Request 1 βββββββ> Server
Request 2 βββββββ> Server
Request 3 βββββββ> Server
Request 4 βββββββ> Server
Each request can be independently routed and processed.
If state is required:
Request
|
v
MCP Server
|
+----> Application State
|
v
Database
The state belongs to the application rather than the MCP transport.
The move toward a stateless MCP protocol might initially look like a relatively small protocol change.
I think its implications are much bigger.
Removing protocol-level sessions makes MCP easier to deploy in environments where stateless HTTP is already the default architectural model.
It reduces the need for:
And it encourages a cleaner separation:
MCP
β
Communication protocol
Application
β
Business logic
Database
β
Application state
My currency converter server was a relatively simple project, but it was a useful way to understand this change in practice.
If you're building remote MCP servers today, I think it's worth understanding the new stateless modelβnot just from an MCP perspective, but from a distributed systems and deployment architecture perspective.
The interesting question is no longer:
"How do I maintain an MCP session?"
It becomes:
"Can I design my MCP server so that every request can be independently processed?"
For many MCP tools, the answer is yes.
And when it is, the resulting architecture can be considerably simpler.
I implemented the stateless behavior in my currency converter MCP server as a practical example of the concepts discussed in this article.
The project is available on GitHub:
In NPM repository:
If you're learning MCP, I recommend starting with a small tool server like this and then experimenting with:
That's where the protocol changes become much more interesting from an architecture perspective.