Skip to main content
Glama
subodhkhanger

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