MCP is Now a Stateless Protocol

The latest MCP specification, released on the 28th of July 2026, makes a major architectural change to the MCP protocol: it's now stateless.

Previously, an MCP client and server had to complete a handshake before the client could call a tool. They used it to agree on the protocol version and capabilities, and to share session identifiers.

That's all gone now.

Before the 2026-07-28 specification, an MCP client had to exchange initialize, initialize result and initialized messages before calling a tool. Now it can send a self-contained tools/call request immediately, while server/discover is optional.

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 (for clients, at least) 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.

Why does this matter?

Stateless servers are a lot easier to host and manage. For one thing, they don't require sticky load balancing or shared session storage.

A stateless MCP client sends a self-contained JSON-RPC request through a load balancer to any compatible server instance, which returns the result.

However, just because the protocol is stateless doesn't mean the application has to be. If a tool needs to remember something between calls, it can return a state ID, which the client includes in the next request.

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.

This example uses version 2 of the official MCP Python SDK for the server. It uses curl for the client.

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. It also supports Streamable HTTP, where the client sends each MCP message as a separate POST request.

Server

First, install version 2 of the mcp package:

uv add "mcp>=2,<3"
Resolved 182 packages in 0.49ms
Audited 178 packages in 0.05ms

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=True disables transport session tracking.
  • json_response=True makes the server return a JSON object instead of an SSE stream.

This starts the server in the background:

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,
)
INFO:     Started server process [21783]
INFO:     Waiting for application startup.
[08/06/26 14:14:04] INFO     StreamableHTTP       streamable_http_manager.py:151
                             session manager                                    
                             started                                            
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:3001 (Press CTRL+C to quit)

This example is running directly in my Obsidian notebook through my Obsidian Markdown Notebook plugin. To run it outside Obsidian, save the code as mcp_server.py, then run uv run python mcp_server.py. Leave that terminal open while you run the client commands below in another terminal.

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/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.

As mentioned, we can also use the server/discover method to see what the server supports. The request uses the same _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/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. 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/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.

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

添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论