stateless-mcp-scale-demo
README.md
# Stateless MCP Scale Demo
A small, production-minded demonstration of the **MCP 2026-07-28 stateless protocol core**.
The same multi-step purchase workflow is served by **two independent MCP replicas** behind an ordinary Nginx round-robin load balancer. There are:
- no protocol sessions;
- no sticky routing;
- no shared session database;
- no hidden in-memory workflow state.
Instead, every mutating tool returns an explicit, HMAC-signed `request_handle`. The client passes that handle into the next tool call, and any replica with the shared signing key can validate it and continue the workflow.
> This demo is intentionally small. It illustrates architecture, not a complete authorization or purchasing system.
## Why this exists
The 2026-07-28 MCP specification retired the required `initialize` handshake and `Mcp-Session-Id` for modern requests. A request is self-describing, so it can land on any healthy replica. Application state is still allowed; the important change is that it should be explicit rather than hidden in the transport.
This repository makes that behavior visible:
1. Nginx sends calls to `mcp-a` and `mcp-b` in round-robin order.
2. Each response includes `served_by`.
3. The workflow survives replica changes because its state travels in `request_handle`.
4. Nginx logs `Mcp-Method`, `Mcp-Name`, and `Mcp-Protocol-Version`, showing how a gateway can observe and govern tool traffic without parsing the JSON body.
## Architecture
```mermaid
sequenceDiagram
participant C as MCP client
participant G as Nginx gateway
participant A as MCP replica A
participant B as MCP replica B
C->>G: create_purchase_request
G->>A: tools/call
A-->>C: signed request_handle + served_by=mcp-a
C->>G: add_line_item(request_handle)
G->>B: tools/call
B->>B: verify HMAC and decode state
B-->>C: updated request_handle + served_by=mcp-b
C->>G: review_purchase_request(request_handle)
G->>A: tools/call
A->>A: verify the same explicit state
A-->>C: summary + served_by=mcp-a
```
## Tools
| Tool | Purpose |
|---|---|
| `create_purchase_request` | Creates the workflow and returns its first signed handle. |
| `add_line_item` | Accepts a handle and returns a new handle with an immutable state revision. |
| `review_purchase_request` | Validates and summarizes the handle without changing it. |
| `submit_purchase_request` | Demonstrates a policy boundary: requests above €500 require explicit human approval. |
## Run with two replicas
Requirements: Docker and Docker Compose.
```bash
cp .env.example .env
# Replace STATE_SIGNING_KEY in .env. For example:
# openssl rand -hex 32
docker compose up --build
```
The MCP endpoint is:
```text
http://localhost:8080/mcp
```
In a second terminal, install the project and run the client:
```bash
uv sync --dev
uv run python scripts/demo_client.py
```
Representative output—the exact replica order can vary:
```text
create_purchase_request replica=mcp-a, total=€0.00
add_line_item (GPU) replica=mcp-b, total=€450.00
add_line_item (storage) replica=mcp-a, total=€570.00
review_purchase_request replica=mcp-b, total=€570.00
submit (without approval) replica=mcp-a, total=€570.00, status=approval_required
submit (approved) replica=mcp-b, total=€570.00, status=accepted
```
Now inspect the gateway logs:
```bash
docker compose logs gateway
```
You should see fields such as:
```text
mcp_method=tools/call mcp_name=add_line_item protocol=2026-07-28 upstream=...
```
That is the practical value of header-based routing: an API gateway, WAF, or rate limiter can identify the MCP method and tool name directly from headers.
## Test with MCP Inspector
The reference Inspector supports modern remote MCP servers:
```bash
npx @modelcontextprotocol/inspector --server-url http://localhost:8080/mcp --transport http
```
## Run tests
```bash
uv sync --dev
uv run ruff check .
uv run pytest -q
```
The tests prove that a handle minted by one replica can be verified by another replica with the same key, while tampered, expired, or differently signed handles are rejected.
## Important security notes
- The handle is **signed, not encrypted**. Its contents can be decoded by the client. Never put credentials, secrets, or sensitive personal data in it.
- A real application can return an opaque ID and store state in PostgreSQL, Redis, or another durable system. The MCP transport still remains stateless.
- Tool annotations are behavioral hints, not authorization controls.
- The demo disables SDK DNS-rebinding protection because Nginx controls the inbound Host header locally. A public deployment should configure explicit `allowed_hosts` and `allowed_origins`.
- The `human_approved` field demonstrates a policy checkpoint, not cryptographic proof of a human decision. Production approval should be tied to authenticated identity and authorization.
- The submission is simulated and idempotent; no payment or external order is created.
## What this demonstrates
Stateless MCP does **not** mean “no state.” It means protocol state is not hidden inside a transport session. Once the workflow handle is explicit, the system becomes easier to scale, test, observe, and reason about.
## References
- Model Context Protocol 2026-07-28 specification announcement
- Official MCP Python SDK v2 documentation
- Simon Willison, “Stateless MCP has recaptured my interest”
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues