MCP without sessions: connecting an AI assistant to company data under the new spec
The Model Context Protocol is the standard an AI assistant uses to ask your systems for data: orders, invoices, tickets. The 28 July 2026 revision of the spec changes its foundation: an MCP server no longer remembers the client between requests. For the team deploying it, that means one thing: you can now run an MCP server like any other API.

Until now a conversation with an MCP server began with a handshake: the client sent initialize, the server replied and issued a session ID in the Mcp-Session-Id header. Every later request had to reach wherever that session was stored. With one instance that is fine. With three behind a load balancer you had to pin clients to a machine or keep sessions in a shared store.
What the 2026‑07‑28 revision changed
The spec's authors say plainly that MCP is moving from a bidirectional stateful protocol to a stateless request and response one. Every request now carries the protocol version and the client's capabilities in its _meta field, so a server can sit, in their words, behind a plain round robin load balancer without shared storage. Of the long list of changes, five matter to a company:
- The
initializehandshake and theMcp-Session-Idheader are gone. A server that needs state across calls mints its own handle and takes it as an ordinary tool argument. - Every POST must carry an
Mcp-Methodheader, and tool, resource and prompt calls anMcp-Nameheader too. A gateway can route and meter on them without reading the body. The server must reject a request whose headers disagree with its body, with HTTP 400 and error -32020. - Every server must implement
server/discover, which reports the protocol versions it supports, its capabilities and its identity. - Sampling, Roots and Logging are deprecated. They keep working, and the new lifecycle policy guarantees at least twelve months before anything is removed. New code should not depend on them.
- Dynamic Client Registration (RFC 7591) is deprecated in favour of Client ID Metadata Documents. Clients must also validate the RFC 9207
issparameter when the authorization server sends it, and credentials are now bound to the authorization server that issued them.
Most of the authorization changes land on clients, meaning the assistants and their libraries. On the server side the big win is deployment: a container, a few replicas, a load balancer with no sticky sessions, and no Redis just for sessions. The official Python SDK 2.0.0 shipped the same day as the spec, and its release notes say one server handles both the new protocol revision and every 2025‑era client.
Statelessness has a price. The server can no longer turn to the client in the middle of a call. If a tool needs a user's decision, say to confirm a refund, it returns an input_required result carrying the question, and the client retries the same call with the answer. The spec calls this pattern Multi Round‑Trip Requests. Read-only tools, the sensible place to start, never need it.
The server: one tool, one table, read only
Below is a complete server. It creates a small SQLite database with two orders and exposes one tool, get_order_status, which returns an order's status by its number. The MCPServer class is what used to be FastMCP. In 2.0.0 you import it from mcp.server, which we confirmed by running this code.
Python
import sqlite3
from mcp.server import MCPServer
DB_PATH = "orders.db"
# Demo data: one table, two orders.
with sqlite3.connect(DB_PATH) as db:
db.execute("CREATE TABLE IF NOT EXISTS orders (id TEXT PRIMARY KEY, status TEXT)")
db.executemany("INSERT OR REPLACE INTO orders VALUES (?, ?)",
[("A-1001", "shipped"), ("A-1002", "awaiting payment")])
mcp = MCPServer("orders")
@mcp.tool()
def get_order_status(order_id: str) -> str:
"""Return the status of one order by its ID."""
# Read-only connection: the tool cannot change data even by mistake.
with sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True) as db:
row = db.execute("SELECT status FROM orders WHERE id = ?", (order_id,)).fetchone()
return row[0] if row else f"No order {order_id}"
if __name__ == "__main__":
mcp.run("streamable-http", port=8765, json_response=True)server.py. Needs Python 3.10 or later and pip install mcp==2.0.0. Run it with python server.py.The tool description the model sees comes from the docstring, and the argument schema from the type hints. The database connection opens with mode=ro, so even a bad query cannot change data. By default the server listens only on 127.0.0.1, as the spec recommends for local work. The json_response=True option returns a single JSON object instead of an SSE stream, which keeps the test simple.
A call with no session
Now one curl request to a freshly started process. No initialize, no session ID. The protocol version travels in the MCP-Protocol-Version header and in _meta; the method and tool name in the Mcp-Method and Mcp-Name headers.
Bash
curl -s http://127.0.0.1:8765/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: get_order_status" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": "get_order_status",
"arguments": {"order_id": "A-1001"},
"_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}}}}'server.py from the previous listing running.HTTP
HTTP/1.1 200 OK
content-type: application/json
{"jsonrpc":"2.0","id":1,"result":{"content":[{"text":"shipped","type":"text"}],
"isError":false,"resultType":"complete","structuredContent":{"result":"shipped"},
"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"orders","version":""}}}}date, server and content-length headers. Lines wrapped for reading.That was the first request the process ever handled, and the response carries no session header. Two copies of this server behind a load balancer would answer it the same way. The server also added the resultType field the new revision requires and the serverInfo it recommends, with no code on our side.
We also checked what happens when the headers lie. With Mcp-Name changed to delete_order and the body left alone, the SDK rejects the request exactly as the spec says it must:
HTTP
HTTP/1.1 400 Bad Request
{"jsonrpc":"2.0","id":1,"error":{"code":-32020,
"message":"mcp-name header does not match the request body's 'name' parameter"}}Mcp-Name: delete_order.Leaving out Mcp-Method ends with the same -32020 error. One thing behaves differently from what the changelog might suggest: a request without the MCP-Protocol-Version header is treated by the SDK as traffic from an older client, answered with Missing session ID, and given an mcp-session-id header. The spec allows this, since a server may keep serving clients from before 2026‑07‑28. In practice you only get statelessness once your assistant speaks the new revision too. While older clients connect, sessions in the old mode still exist.
Before you connect real data
The example keeps two orders in a file. A company database holds tens of thousands of records and customer data. Before an assistant touches it, go through this list:
Five things to check
- 01Read only at the database level, not in the tool's name. A separate database user with
SELECTon chosen tables, or a replica. Themode=roin the example is the same rule in miniature. - 02Narrow tools.
get_order_status(order_id)answers one question and returns one value. A tool likerun_sql(query)hands the model the whole database. Cap the number of rows a tool returns as well. - 03Authentication from day one. The spec recommends it for every connection and requires servers to validate the Origin header, so that a web page in a browser cannot talk to your local server. When choosing an authorization server, check for Client ID Metadata Documents support, since Dynamic Client Registration is deprecated, and on the client side check that
issis validated. - 04Log every call at the edge. The
Mcp-MethodandMcp-Nameheaders let a proxy record who called which tool and pass only names on an allow list. The spec warns intermediaries not to trust those headers on requests without the new protocol version, because nothing checks them against the body there. - 05Build nothing new on Sampling, Roots or Logging. Pass directories and files as tool parameters, call the model directly through your provider's API, and send logs to
stderror OpenTelemetry.
Sources
- 01Model Context Protocol, Specification 2026‑07‑28: Key Changespublished 28 July 2026
- 02Model Context Protocol, Specification 2026‑07‑28: Streamable HTTPpublished 28 July 2026
- 03Model Context Protocol Blog, The 2026‑07‑28 Specificationpublished 28 July 2026
- 04modelcontextprotocol/python-sdk, release v2.0.0published 28 July 2026
- 05PyPI, mcp 2.0.0published 28 July 2026
