mcp-stateless-lab

mcp-stateless-lab

A comparative lab demonstrating MCP protocol evolution from stateful (2025-11-25) to stateless (2026-07-28) using a procure-to-pay agent, highlighting the implications for load balancing and session management.

Category
访问服务器

README

mcp-stateless-lab

A before/after lab for MCP 2025-11-252026-07-28, told through a procure-to-pay story, answering four questions head-on: what problem showed up · how stateless solves it · why that works · what discipline it demands in production.

繁體中文版:README.zh-TW.md

Two MCP servers, one business logic. The same story runs against both revisions, so every difference you observe comes from the protocol, not the code. Running it produces a machine-generated report: 29 checks, 29 passed.

git clone https://github.com/133institute/mcp-stateless-lab && cd mcp-stateless-lab
./scripts/setup.sh          # Windows: scripts\setup.bat
./scripts/run_experiments.sh

Results land in experiments/report.md — generated from a real run, never hand-written.


The challenge: MCP stopped being a local tool call

The early assumption was simple: one client connects to one server, the connection stays alive, and the server remembers you. The moment agent services went to production, reality stopped cooperating:

  • Instances get swapped. A load balancer or serverless platform sends the next request to a different machine. The process that served the last one may no longer exist.
  • Humans take their time. Before submitting, someone has to ask a manager, legal, or accounting. The answer might come back in 4 seconds — or 4 hours.
  • Work gets handed off. The same job is continued by different people, on different devices, by background schedulers. It does not stay on one connection.

So the challenge was never "have no state." The challenge is that state can no longer be tied to a connection.

The problem: state hidden in the connection makes the system brittle

A purchasing flow shows it plainly. One requisition crosses days, people and tools on its way to payment — but 2025-11-25 records "who you are, how far you got" in the connection (the session). Four everyday moments, four ways to break:

Business moment Why it breaks Evidence
A high-value line needs sign-off before submitting the server must hold the request open while a person decides: too short a timeout loses the operation, too long parks a connection exp02(b): an answer 2 s late loses the submission
"Add one more line to yesterday's requisition" yesterday's session is gone; the business document is still very much alive exp02(c): the row sits in the database, unreachable by anyone
The warehouse receives, accounting adjudicates you cannot make everyone share one connection exp02(a): hit a different instance, get HTTP 404
A three-way match runs for minutes progress checking is chained to the channel that started the job exp01: tasks/result parks a connection for the full duration

The most telling detail in exp02: the documents sit intact in the shared database for the entire duration of every failure. What broke was not the data but the addressing. What the system needs to catch is "the requisition number, the receipt record, the payment task" — not "whichever connection is still alive."

The stateless answer: self-contained requests, named state

2026-07-28 removes protocol sessions and the initialize handshake. Instead:

Old: the connection remembers New: every request carries its own context
Mcp-Session-Id header gone
after initialize, both sides remember _meta states version and capabilities every time
drafts, confirmations, tasks hidden in a process/stream handle: a name for the business object
the answer must find "the process that asked" requestState: continuation data the client carries back
progress chained to the starting channel taskId: a name for the long-running job

The core difference in one sentence: it no longer matters who receives the request — if the name, the authorization and the state machine all check out, the work continues.

Why that works: state goes back where it belongs

Stateless does not mean throwing state away. It means the transport layer stops secretly holding it for you. Each piece goes to its proper home:

  • The request carries its ownprotocolVersion, clientCapabilities, clientInfo in _meta. The server never needs to remember a handshake.
  • The database keeps the rest — document states, authorization, audit trails, idempotency keys. Restarts, instance swaps and overnight gaps cannot break it.
  • The model sees names — document numbers, task ids, result summaries. An agent can quote them, relay them, hand them to the next person.

The payoff: an MCP server scales, restarts and swaps instances like any ordinary HTTP service, while business state stays traceable, controllable and auditable. exp03 proves it the direct way — the same six prompts through the same round-robin cluster, all passing, with the report recording which instance served each step.

How, part 1: business continuity rides on handles

A handle is just the requisition number PR-2026-0001, a receipt record id, a payment task id — the name of "the thing I want to continue," not a permission. Anyone may hold the name; whether they can act is decided by three other things:

  • Authorization: may this person see this document? (re-checked on every request)
  • The state machine: can it be amended / received / paid right now? (fully covered in tests/test_state_machine.py)
  • Idempotency: does a resend double-order?

Evidence: in exp03, a fresh next-day conversation, the warehouse on a new device, and accounting's separate agent all continue the same flow holding nothing but the number — no hand-over protocol, because the name is the hand-over.

How, part 2: human decisions become two requests (MRTR)

The old way: the server holds the original request open and waits. The new way removes the waiting entirely:

  1. A tool realizes it needs a human first
  2. The server answers with input_required, attaching the question and an opaque requestState — then hangs up
  3. The client asks the user at leisure — 4 seconds or 4 hours, nothing is waiting
  4. The client re-sends the original request with the answer and the untouched requestState

The catch: requestState passes through the client's hands, so the server may only verify it, never trust it. This lab implements and tests all four defences (tests/test_protocol_2026.py):

Defence Mechanism On failure
Integrity HMAC signature one flipped character → -32602, rejected
Bound to the user principal sealed into the signature presented by someone else → rejected
Bound to the request parameter digest sealed in replayed onto another document → rejected
Short expiry deadline sealed in expired → rejected

And rejection produces zero side effects — every tamper test checks afterwards that the requisition did not move.

Evidence: exp03 replaces every instance in the cluster between the question and the answer of one confirmation. The submission still completes, because requestState carries everything needed to resume.

How, part 3: long jobs and multi-agent relay ride on taskId

A minutes-long three-way match must not chain "how is it going?" to the channel that started it:

  1. Start the job → get a taskId back immediately (16 ms measured, versus the old blocking call parking a connection for 2+ seconds)
  2. Poll tasks/get — any instance can answer, because task state lives in the database, not in memory
  3. When the job needs a ruling it parks at input_required; answer via tasks/update and it resumes

Three implementation disciplines, none optional:

  • Persist task state — park it in an in-memory future and a tasks/update landing on another instance can never reach it
  • Per-request capability — whether the server may ask a human is declared by this request, not remembered from a connection
  • No tasks/list — a server that cannot identify the requester would be listing other people's tasks (the spec removed it for exactly this reason)

Evidence: exp03 records one job's creation, polls and mid-flight answer being served across all three instances.

The production baseline: stateless means engineering your state

Moving state from the transport layer back into the application buys the scaling — and costs four disciplines this lab tests one by one:

  1. A handle is not a permission. Re-authorize on every request; holding a number ≠ being allowed to act on it.
  2. requestState is not trusted data. Verify everything the client brings back; reject with zero side effects.
  3. A taskId is not a public query ticket. Bind tasks to user, tenant and originating request.
  4. Tool-catalogue caching must not leak. Mark a private listing cacheScope: "public" and you have published it.

The experiments at a glance

exp01 2025-11-25, one instance, one conversation all six prompts pass — proving the old revision's logic is fine
exp02 2025-11-25, 3 instances + round-robin reproduces the three failures above (a pass = the failure reproduced)
exp03 2026-07-28, identical conditions all pass, with per-step instance labels
exp04 the two revisions meeting 12 conformance checks (error codes, headers, capability gating)

The six driving prompts (P1–P6) live in src/client_lab/scenarios.py, identical for both revisions; P2 (continue tomorrow) and P5 (hand over to the warehouse) are the two boundaries the old revision cannot cross.

Four ways in

  • Read: experiments/report.md, the check-by-check record of a real run
  • Step through: notebooks/01_walkthrough.ipynb (the whole story on the new revision in twelve steps) and 02_before_after.ipynb (old and new side by side, showing where it breaks) — both committed with real outputs, readable on GitHub as-is
  • Run: ./scripts/run_experiments.sh starts and stops every server it needs
  • Drive it from a real agent: agents/ (Claude Code / Desktop, ChatGPT, OpenAI API). Run the era probe first to learn which revision your host speaks; verification status in agents/STATUS.md

Deeper reading: docs/comparison.md (the full SEP-by-SEP reasoning), docs/migration.md (the migration checklist), docs/diagrams/ (Mermaid diagrams).

Commands

./scripts/setup.sh                # idempotent; ends by running a real tool call
./scripts/run_2026.sh --http      # :8002/mcp   (--stdio also works)
./scripts/run_2025.sh --stdio     # :8001/mcp with --http
./scripts/run_cluster_2026.sh     # 3 instances :8201-8203 + proxy :8200
./scripts/run_cluster_2025.sh     # 3 instances :8101-8103 + proxy :8100
./scripts/run_experiments.sh      # exp01..exp04 -> experiments/report.md
./scripts/run_tests.sh            # pytest, ruff, mypy, shellcheck
./scripts/stop_all.sh
./scripts/clean.sh                # lists what it would delete; --force to do it

Every one has --help, exits non-zero on failure with reproduction steps, and refuses to silently pick another port when one is busy. Windows equivalents: scripts\*.bat.

Layout

src/procure_core/     business logic and SQLite — knows nothing about MCP
src/mcp_wire/         JSON-RPC, stdio and Streamable HTTP, round-robin proxy
src/server_2025/      the old server: handshake, sessions, blocking tasks
src/server_2026/      the new server: discover, handles, MRTR, tasks extension
src/client_lab/       protocol-level clients for both revisions + the six prompts
experiments/          exp01..exp04 and the generated report
notebooks/            the same material, step by step
tests/                97 tests: state machine, thresholds, both protocols
scripts/              one command each (.sh / .bat share scripts/lab.py)
agents/               four agent-host guides + the era probe
docs/                 comparison, migration checklist, Mermaid diagrams

Two decisions worth knowing

No official SDK. The two revisions live in mcp 1.x and 2.x — same package name, cannot coexist — and the experiments need both revisions in one process, plus the ability to send requests an SDK exists to prevent (wrong headers, a tampered requestState). Everything is implemented directly on JSON-RPC, so one virtual environment suffices. requirements-2025.txt / requirements-2026.txt still pin the real SDKs for separate comparison.

The 50,000 threshold applies to the line total (quantity × unit price), not the unit price. Two laptops at 45,000 form one 90,000 line — which is what makes the walkthrough actually trigger a confirmation and a legal review at all. The boundaries 49,999 / 50,000 / 50,001 are pinned in tests.

Scope and safety

No real ERP, no real payments, no full OAuth chain (the authorization changes are documented, not implemented). Every company, person, document number, amount and supplier is fictional. Nothing in this repository reads, logs or stores any credential, token or personal datum; the one path that exposes anything to the internet (the ChatGPT connector) opens its guide with the safety notes.

Verified on

Python 3.12.7, Windows 11, 2026-07-30: 97 tests pass, ruff and mypy clean, 29 of 29 experiment checks pass. shellcheck was not installed, so it is reported as not run. The four agent-host integrations have not been run against their real hosts — agents/STATUS.md records exactly what was and was not exercised.

Sources

Authoritative:

Also useful: the MRTR pattern, Streamable HTTP, versioning and the tasks extension.

Licence

MIT — see LICENSE.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选