Skip to main content
Glama
jobvancreij

Demo MCP Stateless

by jobvancreij

Old MCP vs new MCP, on a real (local) serverless platform

A runnable proof of what the 2026-07-28 MCP revision means by "stateless", built on the official mcp Python SDK (2.0.0). A tiny but honest serverless platform cold-starts real server processes on demand, scales out under load, and kills anything idle for 4 seconds — then a narrated client runs the old protocol and the new protocol against that same platform. The old one dies (Session not found); the new one never notices, including a mid-call user question whose asking instance is dead before the answer arrives.

git clone https://github.com/jobvancreij/demo-mcp-stateless
cd demo-mcp-stateless
./run_demo.sh        # needs uv + Python >= 3.11; creates .venv on first run
.venv/bin/python probes.py   # the four adversarial checks

Binds 127.0.0.1:9000 (the gateway) and 127.0.0.1:9102+ (instances); the script refuses to start if anything already holds those ports. The whole run takes about 36 seconds — most of it deliberate idling, waiting for the reaper. probes.py needs POSIX fcntl (it takes a lock so two copies can't answer each other's checks), so it is Linux/macOS only.

What's in the box

File

Role

server.py

One MCP server on the official SDK. Serves BOTH eras: legacy handshake sessions and stateless 2026-07-28 requests. MRTR via Resolve/Elicit dependency injection, shared RequestStateSecurity key, an MCP Apps ui:// boarding-pass card, SQLite-backed job handles, and the in-memory anti-pattern for contrast.

faas.py

The local serverless platform: zero warm instances, measured cold starts (real uvicorn processes), scale-out to 3, idle-reap to zero. Dispatches to whatever process is free and logs each request from the Mcp-Method/Mcp-Name headers alone — it never parses a body. GET /faas/status exposes the event log.

client.py

The narrated walkthrough (five acts, official SDK Client in both mode="legacy" and mode="2026-07-28").

probes.py

The adversarial checks the walkthrough can't make, each actually executed: header/body mismatch → -32020, the requestState blob decrypted, a cross-instance booking completed, the same blob replayed into a second booking inside its TTL, a tampered-argument replay refused, and a server built with request_state_security= omitted failing across instances. Starts its own servers on :9201-9204; no gateway involved.

run_demo.sh

Starts the platform, runs the client, kills exactly the PID it started.

article.md

Companion Medium article draft.

docs/

The four article figures — fleet lifetimes across one run, old-vs-new request shape, the MRTR timeline across instance death, and the shared-key vs omitted-argument split. SVG sources plus rendered PNGs; docs/render.sh rebuilds the PNGs with headless Chrome (and trims them with Pillow, if it is installed).

Related MCP server: example-mcp-server-streamable-http-stateless

The five acts

  1. Old patternmode="legacy": handshake, session, tool call works; 4s of idleness later the platform reaps the instance and the next call is MCPError: Session not found. That error is the whole reason remote MCP needed sticky, always-on servers.

  2. New patternmode="2026-07-28": first packet is a real request; three concurrent calls scale the fleet to 3; idleness scales it to 0; the same client object keeps working via fresh cold starts.

  3. MRTR across instance deathbook_flight pauses with input_required + AES-GCM-sealed requestState; the user "thinks" for 6s; the asking instance is reaped; the retry cold-starts a new process that unseals the state and completes the booking. Works only because all instances share the RequestStateSecurity key. The two prints in server.py show what re-runs: the resolver fires once per round trip (on both instances), the tool body exactly once.

  4. Durable state — the process-memory job handle dies with its instance; the SQLite-backed handle survives (started on 'i7', fetched from 'i8').

  5. MCP Appsboarding_pass is bound to ui://flight-desk/boarding-pass (text/html;profile=mcp-app) via the SDK's Apps extension; the demo writes boarding-pass.html with the tool's structured result injected the way a host's postMessage bridge would.

Fidelity notes

Server and client are the unmodified official SDK, so the wire format is the SDK's real 2026-07-28 implementation (and its real legacy implementation in Act 1). The platform is the toy part: a sub-300-line process manager standing in for Lambda/Cloud Run/Workers. Out of scope: OAuth, Origin validation hardening, SSE streaming responses, subscriptions/listen — note that last one still exists in this revision, so server-to-client notifications were reshaped, not deleted; what MRTR replaced is the tool-scoped server-initiated request.

Two consequences of json_response=True (every reply a plain JSON object, which keeps the gateway trivially bufferable) are worth naming. book_flight is 2026-07-28-only: a legacy session would need the SSE back-channel to deliver elicitation/create, and answers no back-channel for server-initiated requests without it — so Act 1 uses whoami. And because there is no authenticated transport here, the sealed requestState is bound to its originating request, expiry and audience, but not to a principal; add OAuth and the SDK binds that too.

Related MCP Connectors

Related MCP Servers