MCP Goes Stateless: Building a Python Server with the v2 SDK
What actually changed in the protocol
Three things, and they compound.
First, there is no session handshake before requests. A client does not need to establish a protocol session before it can call a tool. Second, servers no longer rely on Mcp-Session-Id for ordinary requests, so the header that once pinned a conversation to a specific backend process is simply not part of the normal path. Third, requests are self-contained: everything needed to process a call travels with the call.
The practical consequence is that ordinary load balancing suffices. You can put MCP behind the same HTTP infrastructure you already run, scale instances horizontally, and let a request land wherever it lands. No sticky routing, no shared session infrastructure, no coordination layer that exists purely to remember which process a client happened to hit first.
That is a meaningful simplification. Anyone who has tried to run stateful agent tooling in a container scheduler that treats instances as disposable knows how much of that machinery existed only to paper over the session model.
The Python SDK v2 at a glance
The official Python SDK has moved to v2 as its current stable line, a change documented in the SDK’s release notes and reflected in the current Python SDK README. The same documentation sets the Python version floor at 3.10 or newer, which is the number to check before you plan an upgrade. Two server APIs matter.
MCPServer is the recommended high-level API. You define tools, resources, and prompts as regular Python functions, and the SDK handles the protocol surface. The lower-level Server class exists for cases where you need exact control over schemas, protocol metadata, or custom methods. Most servers should start with MCPServer and only drop down when the abstraction genuinely gets in the way.
The SDK documentation also recommends installing the CLI extra. That gives you the development command and the MCP Inspector workflow, which is the basic loop the maintainers suggest for building and checking a server.
Tools, resources, and prompts without a manifest
There is no manually written tool manifest. The SDK derives the tool definition from the Python function itself. Type hints become the MCP input schema, and defaults make parameters optional in the generated schema. If you write def search_notes(query: str, limit: int = 10), the schema follows from the signature.
Resources expose information the host application can load into context, and a client can read them without invoking a tool. The SDK documentation roughly compares resources with GET-like behavior and tools with action-oriented POST-like behavior, which is a useful mental model when you are deciding where a piece of functionality belongs.
Prompts are reusable templates defined as a Python function plus a decorator. They are generally initiated by the user or host rather than autonomously invoked by the model. All three primitives are supported through the same decorator-oriented server interface.
Running and inspecting locally
The SDK includes a development command that launches the server with MCP Inspector support, giving you a UI for listing and invoking tools. That is the recommended basic development loop, and it is worth using before you wire anything into a real host.
The SDK’s Streamable HTTP server uses /mcp as its default endpoint, per the SDK documentation. If you are deploying behind a reverse proxy, that is the path to route.
Stateless protocol, stateful applications
Here is the part that trips people up. Stateless protocol does not mean applications cannot maintain state. It means MCP no longer hides application state inside a protocol session.
The MCP maintainers recommend an explicit-handle pattern for application-level state. Their example is a shopping server that passes an explicit handle rather than relying on a protocol session to remember the cart. The handle travels with the request, the server resolves it, and nothing depends on which process handled the previous call.
This is a design constraint, not a limitation. It forces you to be honest about where state lives. If your agent needs continuity across calls, that continuity becomes an explicit object you manage, version, and expire, rather than an implicit side effect of a connection. Anyone who has debugged a distributed system knows which of those two is easier to reason about. The same tension shows up across agent infrastructure right now, including oversight assumptions that break down in practice, as explored in Human-in-the-Loop AI Is Broken: Hugging Face Researchers Say Safeguards Push People Out.
Tutorial: a developer knowledge-base server
The clearest way to see the explicit-handle pattern is a small server with no user session state at all, where every request contains everything required to process it. In the example below, NOTES is a stand-in for a database or cache you would supply in production. It is deliberately process-local and read-only, so it does not reintroduce the shared mutable state the stateless core is meant to eliminate.
from mcp.server import MCPServer
server = MCPServer("dev-kb")
# Stand-in for a database or cache. Swap for your own store in production.
NOTES = {
"mcp-stateless": "The 28 July 2026 spec makes the protocol core stateless.",
"streamable-http": "The SDK's Streamable HTTP server defaults to /mcp.",
}
@server.tool()
def search_notes(query: str, limit: int = 5) -> list[str]:
"""Search the knowledge base by keyword."""
hits = [k for k in NOTES if query.lower() in k.lower()]
return hits[:limit]
@server.tool()
def read_note(note_id: str) -> str:
"""Read a single note by id."""
return NOTES.get(note_id, "not found")
@server.resource("kb://index")
def index() -> str:
"""Return the list of available note ids."""
return "n".join(NOTES)
The tool definitions come straight from the signatures. limit is optional because it has a default. There is no manifest to keep in sync, and no session object to look up.
For Streamable HTTP, the SDK gives you a standard ASGI app:
app = server.streamable_http_app()
That is the whole deployment surface. Every request carries its own arguments, so any instance can serve it.
Deploying alongside FastAPI or Starlette
streamable_http_app() returns a standard Starlette-compatible ASGI app, which is what makes MCP easy to mount as one component inside a larger FastAPI or Starlette deployment. You can serve your existing API routes and the MCP endpoint from the same process, or split them across services, without changing how either is scaled.
On the client side, the v2 Client accepts an HTTP URL directly and automatically uses Streamable HTTP. It also exposes the negotiated protocol version, which is the detail to check when you are debugging a mismatch between a client and a server that are not speaking the same revision.
What the stateless core means for existing servers
If you are running an MCP server today, the migration question is mostly about what you were storing in a session. Anything that lived implicitly in protocol state needs an explicit home: a handle, a token, a database row, a cache key. Move it, and the rest of the change is largely subtractive.
Reach for MCPServer unless you need exact schema control, protocol metadata, or custom methods, in which case the lower-level Server class is there. Install the CLI extra and use the Inspector loop while you build. Route /mcp and let ordinary load balancing do the rest.
The broader lesson is worth carrying past this spec revision. When agents operate at scale, implicit shared state becomes a liability, whether it is a session store holding a conversation together or a swarm of agents coordinating in ways nobody designed for, the territory mapped in When AI Agents Talk Behind Our Backs: Inside the 2026 Agent Collusion Incidents. Making state explicit is not just a scaling convenience. It is how you keep a system legible to the people running it.
2 Comments
Comments are closed.