Skip to main content
Glama
jobvancreij

Demo MCP Stateless

by jobvancreij
README.md
# 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.

```bash
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). |

## The five acts

1. **Old pattern** — `mode="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 pattern** — `mode="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 death** — `book_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 `print`s 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 Apps** — `boarding_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.