What is Stateless MCP?
Photo by Mohamed B. via Pexels
Stateless MCP is the new stateless protocol core for MCP introduced in the 2026-07-28 Specification.
The original MCP specification described a handshake protocol that started with an initialize request, which the client and the server used to agree on the protocol version and capabilities, and finished with an initialized notification.
That's all gone now in the new spec.
Clients can just call the MCP tools they want immediately. If they need to know what capabilities a server has, there's a new optional server/discover RPC method.
Basically, stateless MCP looks a lot more like a typical JSON-RPC API, but with a few helpful standards for server and tool discovery, multi-round-trip requests and optional extensions for long-running tasks. It also supports Streamable HTTP, where the client sends each MCP message as a separate POST request.
Stateless servers are a lot easier to host and manage. For one thing, they don't require sticky load balancing or shared session storage.

The protocol is stateless, but the application doesn't have to be. A tool can return an explicit state handle and the model can pass it to later calls.
The Python SDK uses /mcp as the default endpoint, and the message body uses JSON-RPC. There are Mcp-Method and Mcp-Name headers to identify the request and help with routing and authorisation.
Building a stateless MCP server
I'm going to construct a basic MCP server and client so we can see exactly what it looks like.
Server
This example uses version 2 of the official MCP Python SDK for the server. It uses curl for the client.
Let's start with a trivial example of a calculator that can only add numbers.
I'll create a new instance of an MCPServer, add a single tool called add and then set a few server options:
stateless_http=Truedisables transport session tracking.json_response=Truemakes the server return a JSON object instead of an SSE stream.
This starts the server in the background:
uv run --with 'mcp>=2,<3' python - >/tmp/stateless-mcp.log 2>&1 <<'PY' &
from mcp.server import MCPServer
mcp = MCPServer("Calculator")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
mcp.run(
transport="streamable-http",
port=3001,
stateless_http=True,
json_response=True,
)
PY
sleep 2
echo "Server running at http://127.0.0.1:3001/mcp"
Server running at http://127.0.0.1:3001/mcp
Note that uv run lets us run a simple command without manually creating an environment.
Client
The tools/call method allows us to call a known tool directly.
curl --silent --show-error http://127.0.0.1:3001/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/call' \
-H 'Mcp-Name: add' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "add",
"arguments": {"a": 2, "b": 3},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "curl",
"version": "1.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}' | python3 -m json.tool
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"text": "5",
"type": "text"
}
],
"isError": false,
"resultType": "complete",
"structuredContent": {
"result": 5
},
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "Calculator",
"version": ""
}
}
}
}
The server returns 5 in an MCP tool result.
We can also use the server/discover method to see what the server supports. The request uses the same minimal _meta object as the tool call.
curl --silent --show-error http://127.0.0.1:3001/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
--data '{
"jsonrpc": "2.0",
"id": 2,
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "curl",
"version": "1.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}' | python3 -m json.tool
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"cacheScope": "private",
"capabilities": {
"prompts": {
"listChanged": true
},
"resources": {
"listChanged": true,
"subscribe": true
},
"tools": {
"listChanged": true
}
},
"resultType": "complete",
"supportedVersions": [
"2026-07-28"
],
"ttlMs": 0,
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "Calculator",
"version": ""
}
}
}
}
This tells us that the server supports tools. It does not list the individual add tool. A client can call tools/list if it needs the tool names and schemas.
curl --silent --show-error http://127.0.0.1:3001/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/list' \
--data '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "curl",
"version": "1.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}' | python3 -m json.tool
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"cacheScope": "private",
"resultType": "complete",
"tools": [
{
"description": "Add two numbers.",
"inputSchema": {
"type": "object",
"properties": {
"a": {
"title": "A",
"type": "integer"
},
"b": {
"title": "B",
"type": "integer"
}
},
"required": [
"a",
"b"
],
"title": "addArguments"
},
"name": "add",
"outputSchema": {
"properties": {
"result": {
"title": "Result",
"type": "integer"
}
},
"required": [
"result"
],
"title": "addOutput",
"type": "object"
}
}
],
"ttlMs": 0,
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "Calculator",
"version": ""
}
}
}
}
There is our add tool, including the input and output schemas generated from the Python types.
Stop the server
kill "$(lsof -tiTCP:3001 -sTCP:LISTEN)"
echo "Server stopped"
Server stopped
An MCP request is now a self-contained unit of work. Any compatible server instance can process the request and the server does not need hidden transport state, so it can be load-balanced easily.
Much better.
References
Comments
Reply to this post on Bluesky or Mastodon to join the conversation.