Stateless MCP Is Here: What the 2026-07-28 Spec Changes and How to Host a Fleet of Servers on One Bun Process

TL;DR
Stateless MCP: the 2026-07-28 spec drops sessions, so every request is one self-contained POST. What changed and a Bun + Hono pattern for many servers.
Last updated: August 1, 2026
The Model Context Protocol just went through its biggest revision since launch. The 2026-07-28 spec - informally "MCP 2.0" - removes protocol-level sessions entirely. No more initialize handshake, no more Mcp-Session-Id header, no more GET stream endpoint. Every request is now a single, fully self-contained HTTP POST.
Simon Willison has an excellent writeup on why this matters in Stateless MCP, along with a set of small tools he built against the new spec (a uvx-runnable MCP explorer, a Datasette plugin, and an LLM client integration). His framing is the right one: statelessness "greatly decreases the complexity of implementing both clients and servers." This post covers what actually changed on the wire, and a server pattern the new spec unlocks - hosting a whole fleet of MCP servers on one Bun process.
What changed on the wire#
Under the old Streamable HTTP transport (2025-03-26 through 2025-11-25), a client had to initialize first, hold on to a server-minted session ID, and echo it on every request. Servers had to route requests back to session state, which made horizontal scaling and serverless deployment awkward. That tradeoff is part of why we compared CLIs and MCPs as complementary interfaces rather than interchangeable ones.
Under 2026-07-28, one tools/call is one POST:
POST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": { "location": "Seattle, WA" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
The interesting details:
- Client metadata rides in the body. Protocol version, client info, and capabilities live in
params._metaunderio.modelcontextprotocol/*keys. There is no handshake to carry them anymore. - Selected fields are mirrored into headers.
MCP-Protocol-Version,Mcp-Method, andMcp-Nameduplicate body values so load balancers and gateways can route on them without parsing JSON. Servers must validate that headers match the body and reject mismatches with a400and JSON-RPC error-32020(HeaderMismatch) - otherwise an intermediary routing on a header and a server executing on the body could disagree, which is a security hole. - Tool parameters can become headers too. A tool schema can annotate a parameter with
x-mcp-header, and conforming clients mirror that value into anMcp-Param-{Name}header. That means a gateway can rate-limit or route by tenant without ever reading a request body. - Server-initiated requests are gone. Sampling and elicitation are now handled by the server returning an
InputRequiredResult, and the client retrying the original call with the answers attached (the spec calls this MRTR - multi round-trip requests). Long-lived notifications moved to an explicitsubscriptions/listenSSE stream.
If you have deployed MCP servers behind a load balancer, you already know why this is a big deal: session affinity is gone as a requirement. Any replica can answer any request. This is the same property that made plain REST APIs easy to scale, applied to the agent tool ecosystem.
The pattern this unlocks: an MCP fleet on one process#
Once a server is a pure function of the request, hosting many MCP servers stops being an infrastructure problem and becomes a routing problem. We built this out as a small Bun + Hono + Commander repo, and the core of it is genuinely tiny. It fits the same practical “choose the right server for the job” workflow behind our MCP server guide.
An MCP server is just data - a name and a list of tools:
import { defineMcp } from "../mcp/types.ts";
export const mathMcp = defineMcp({
name: "math",
version: "0.1.0",
description: "Arithmetic tools - add, multiply, power",
tools: [
{
name: "add",
description: "Add two numbers",
inputSchema: {
type: "object",
properties: { a: { type: "number" }, b: { type: "number" } },
required: ["a", "b"],
},
handler: (args) => [{ type: "text", text: String(Number(args.a) + Number(args.b)) }],
},
],
});
A registry maps mount paths to servers - nested paths included:
export const registry: Record<string, McpServerDef> = {
time: timeMcp,
math: mathMcp,
"labs/math": mathMcp,
};
And one Hono app serves the whole fleet:
const app = new Hono();
for (const [path, mcp] of Object.entries(registry)) {
app.all(`/${path}`, (c) => {
const toolsParam = c.req.query("tools");
const allowedTools = toolsParam ? toolsParam.split(",") : undefined;
return handleMcpRequest(mcp, c.req.raw, { allowedTools });
});
}
export default { port: 3100, fetch: app.fetch };
handleMcpRequest is the only part with real spec surface area: it validates the three mirrored headers against the body, answers tools/list and tools/call, returns 404 with -32601 for unknown methods, and 405 for the legacy GET/DELETE verbs. It is around a hundred lines total, with zero session bookkeeping. That is the whole point.
Query parameters as capability scoping#
The ?tools= parameter in that route is the detail we like most. Because every request is self-contained, the URL itself can carry policy:
/mathexposesadd,multiply, andpower/math?tools=addexposes onlyadd- it disappears fromtools/listand calling anything else fails
You can hand different agents different URLs to the same server and get different capability surfaces, with no auth framework and no per-client configuration. Under the stateful spec this would have been fragile - the filter would have had to live in session state. Statelessly, it is just a query string. That kind of deliberately scoped tool surface pairs well with the UI patterns in our Apps SDK and MCP UI guide.
Trying it against a live server#
The repo ships a Commander CLI that speaks the new wire format (correct headers, _meta block, base64 sentinel encoding for non-ASCII tool names):
bun run cli list http://localhost:3100/math
bun run cli call http://localhost:3100/math add -a '{"a":2,"b":3}'
bun run cli call "http://localhost:3100/math?tools=add" power -a '{"a":2,"b":8}'
# -> error: Unknown tool: power
And the header-validation rule in action - send an Mcp-Name that does not match the body and the server must refuse:
curl -s -X POST localhost:3100/math \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/call' \
-H 'Mcp-Name: wrong' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"add","arguments":{"a":1,"b":1}}}'
# -> {"error":{"code":-32020,"message":"Header mismatch: Mcp-Name 'wrong' does not match body value 'add'"}}
Deployment is equally boring, in the best way: one small Dockerfile on the oven/bun image, one container, any number of MCP endpoints. Adding a new experiment is scaffolding a file and adding one registry line.
Should you migrate?#
If you maintain an MCP server today: yes, start now. The spec has a defined backward-compatibility story - modern clients probe with a stateless request first and fall back to initialize on a legacy error - so you can support both eras during the transition. The old HTTP+SSE transport is formally deprecated and eligible for removal. For the production edge, pair the migration with the auth considerations in our zero-touch OAuth guide.
If you are building new agent infrastructure, the calculus is simpler. Stateless MCP servers deploy like ordinary web handlers: they scale horizontally, they work on serverless platforms, and a whole catalog of them can share one process until traffic says otherwise. The protocol finally matches how the rest of the web is built.
Worth reading alongside this: Simon's original post, the 2026-07-28 Streamable HTTP transport spec, and the changelog for the full delta.
FAQ#
What changed in the 2026-07-28 MCP spec?#
It removes protocol-level sessions entirely. There is no more initialize handshake, no Mcp-Session-Id header and no GET stream endpoint. Every request is a single, fully self-contained HTTP POST, with client metadata carried in params._meta.
Why must MCP headers like Mcp-Method and Mcp-Name match the request body?#
MCP-Protocol-Version, Mcp-Method and Mcp-Name mirror body values so load balancers and gateways can route without parsing JSON. Servers must reject a mismatch with a 400 and JSON-RPC error -32020 (HeaderMismatch), otherwise an intermediary routing on the header and a server executing on the body could disagree.
How do sampling and elicitation work without server-initiated requests?#
The server returns an InputRequiredResult, and the client retries the original call with the answers attached - the spec calls this multi round-trip requests (MRTR). Long-lived notifications moved to an explicit subscriptions/listen SSE stream.
Should I migrate my MCP server to the stateless spec?#
If you maintain an MCP server, start now. Modern clients probe with a stateless request first and fall back to initialize on a legacy error, so you can support both eras during the transition. The old HTTP+SSE transport is formally deprecated and eligible for removal.
How can one process host many MCP servers?#
Because each server is a pure function of the request, a registry can map mount paths to server definitions and one Hono app on Bun can serve them all. A ?tools= query parameter can then scope which tools each URL exposes.
Sources#
- Stateless MCP - Simon Willison
- MCP 2026-07-28 Streamable HTTP transport specification
- MCP 2026-07-28 changelog
Continue Reading#
- Zero-Touch OAuth for Enterprise MCP
- Cloudflare Gateway Can Now Detect MCP Traffic - why the per-request headers this post uses became a network security signal
- The Best MCP Servers in 2026
- CLIs Over MCPs
- Apps SDK and MCP UI
- Browse more posts tagged MCP and AI Agents
Get the next deep dive like this in your inbox
One email a week on MCP and the rest of the AI dev stack. Free.
Read next on AI coding tools
MCP Goes Stateless: The 2026-07-28 Migration Guide
MCP 2026-07-28 spec migration guide: sessions and the initialize handshake are gone. What changed, what breaks, and how to move your MCP servers to stateless.
9 min readThe MCP 2026-07-28 Rewrite: What Breaks and How to Migrate
MCP 2026-07-28 breaking changes: a stateless core, deprecated Roots, Sampling and Logging, and tougher OAuth, plus a migration checklist for server authors.
11 min readWhat Is MCP (Model Context Protocol)? The 2026 Developer Guide
MCP (Model Context Protocol) is the open standard connecting AI apps to tools and data. How the 2026-07-28 spec works and how to install or build a server.
14 min readNew here? Start with
Technical content at the intersection of AI and development. Building with AI agents, Claude Code, and modern dev tools - then showing you exactly how it works.






