Synthetic Orders MCP Server
Enables load testing and validation of an order API by generating synthetic orders, previewing them, and sending batches while enforcing guardrails like bounded counts and fixed seeds for replay.
README
Synthetic Orders MCP Server
An MCP server that triggers synthetic order traffic against a local order API, so an agent can load-test and drill the rejection path of a service without being able to aim that traffic anywhere it likes.
Companion code for Trigger Synthetic Orders from an MCP Server on macOS.
Two processes are involved:
| Process | What it is | How it runs |
|---|---|---|
api/main.py |
The order intake API — the target under test. Validates customers, SKUs, stock, prices, and totals. | make api (uvicorn on port 8000) |
server.py |
The MCP server — three tools that preview and send synthetic orders at that API. | stdio subprocess, launched by the MCP client |
The server exposes three tools and one resource:
| Tool | Read/write | What it does |
|---|---|---|
check_target |
read-only | Reports whether the configured order API is reachable, and its SKU/customer counts. |
preview_order |
read-only | Generates one order and returns it without sending it. |
send_orders |
writes | Generates count orders and POSTs each to the order API; summarizes by status code. |
Resource synthetic://catalog lists the sellable catalog rows the seeded
generator draws from.
The two generator modes
seededsamples the real catalog and customer list, so orders validate and come back201. Use it for traffic that should be accepted.simpleinvents every field from random primitives, so the API rejects it with422. Use it to drill the rejection path.
Guardrails worth knowing before you test
These are the point of the design, and they're what the manual tests below poke at:
- The target URL is not a tool parameter. It comes from
ORDER_API_URLin the server's environment, so no prompt can aim the generator at an arbitrary host. countis bounded byMAX_BATCH(default 100). Exceeding it is a readable error, not a silent clamp.- Every batch reports the seed it used, so any run can be replayed exactly by passing that seed back in.
Requirements (macOS)
- uv —
brew install uv. It manages the Python toolchain and the virtualenv; you don't need to install Python 3.12 yourself. - Python 3.12 — pinned in
.python-version;uv syncfetches it if missing. - Claude Code — only for the "drive it from Claude Code" section.
git clone <this repo>
cd mcp-synthetic-server
make install # uv sync — creates .venv and installs everything
make help lists every target with the variables it honors.
Quick start
Two terminals. The order API has to be up before anything can send traffic to it.
Terminal 1 — the target API:
make api
Serves on http://127.0.0.1:8000 with --reload. Confirm it's alive:
curl -s http://127.0.0.1:8000/health
# {"status":"ok","skus":8,"customers":5}
Interactive API docs are at http://127.0.0.1:8000/docs.
Terminal 2 — drive the MCP server:
make demo # in-process client: lists tools, previews, sends a batch
Expected output:
tools:
check_target read-only Check the order API
preview_order read-only Preview a synthetic order
send_orders writes Send synthetic orders
target: http://127.0.0.1:8000 reachable=True skus=8
preview (seed 1337): {"request_id": "643cb56d-...", "customer_id": "CUST-0005", ...}
sent 25 seeded orders (seed 1337): {'201': 25} accepted_total_cents=715350
sent 3 simple orders: {'422': 3}
422 ['unknown customer C-5093', 'unknown sku SKU-9757', ...]
Running the test suite
make test # uv run pytest -q → 10 passed
The tests need no running API: they monkeypatch server.make_client to a
FastAPI TestClient that drives the ASGI app in-process. They assert the control
surface, not just that the tools run — the annotations each tool advertises, that
no tool accepts a URL or host parameter, seed round-tripping, the batch cap, and
the unreachable-API error path.
You'll see one StarletteDeprecationWarning about httpx; it's upstream, not
your setup.
The stdio smoke test
make demo imports the server object directly, which skips process launch,
transport framing, and JSON round-tripping. The smoke test spawns
python server.py as a real subprocess and talks to it over stdio, so a pass here
means the exact command an MCP client is configured with actually works:
make smoke # needs `make api` running
tools: ['check_target', 'preview_order', 'send_orders']
target: http://127.0.0.1:8000 reachable=True
data type: Root
structured: {"mode": "seeded", "seed": 1337, "requested": 5, "accepted": 5, "rejected": 0,
"status_counts": {"201": 5}, "accepted_total_cents": 73550, "sample_failures": []}
OK
It exits non-zero if the order API isn't up, so it's safe to wire into CI behind a started API.
Driving it from Claude Code
1. Generate .mcp.json
The MCP config needs this project's absolute path. Don't hand-edit it — render it:
make config # sed's $(CURDIR) into .mcp.json.example → .mcp.json
That writes:
{
"mcpServers": {
"synthetic-orders": {
"command": "uv",
"args": ["run", "--directory", "/absolute/path/to/mcp-synthetic-server", "python", "server.py"],
"env": {
"ORDER_API_URL": "http://127.0.0.1:8000",
"MAX_BATCH": "100"
}
}
}
}
.mcp.json is gitignored precisely because that path is machine-specific — the
checked-in .mcp.json.example is the template.
2. Start the API, then start Claude Code
make api # terminal 1, leave it running
claude # terminal 2, from the project root
Claude Code reads .mcp.json at startup and will ask you to approve the
project-scoped server the first time. Verify it connected:
/mcp
You should see synthetic-orders as connected, with three tools. If you
started Claude Code before running make config, restart it — the config is read
at launch.
If you change
server.py, restart Claude Code. The server is a subprocess spawned at connect time; edits don't hot-reload the waymake apidoes.
3. Ask for the tools in plain language
Claude Code exposes them as mcp__synthetic-orders__<tool>. Prompts that work:
| Prompt | Tool it triggers |
|---|---|
| "Check whether the order API is reachable." | check_target |
| "Preview one synthetic order with seed 1337." | preview_order |
| "Send 25 synthetic orders." | send_orders |
| "Send 5 orders in simple mode and show me why they failed." | send_orders (rejection path) |
| "Replay that batch using the seed you got back." | send_orders with seed |
| "Read the synthetic://catalog resource." | resource read |
Because send_orders is annotated readOnlyHint: false, Claude Code prompts for
permission before the first send — preview_order and check_target are marked
read-only and idempotent, so they're cheap to approve.
Manual testing: triggering each tool
Everything below was run against a fresh make api. Seeds are fixed so you can
compare output byte-for-byte.
check_target — is the target up?
In Claude Code: "Check the order API."
{"api_url":"http://127.0.0.1:8000","reachable":true,"skus":8,"customers":5}
Negative case — stop make api (Ctrl-C) and ask again. It should not raise;
it reports the failure as data:
{"api_url":"http://127.0.0.1:8000","reachable":false,"skus":null,"customers":null}
preview_order — generate without sending
In Claude Code: "Preview a synthetic order with seed 1337."
{
"mode": "seeded", "seed": 1337,
"order": {
"request_id": "643cb56d-4ec1-4fc6-bee2-9f53ebf644bb",
"customer_id": "CUST-0005",
"channel": "partner",
"currency": "USD",
"lines": [
{"sku": "SKU-6300", "quantity": 3, "unit_price_cents": 2450},
{"sku": "SKU-5510", "quantity": 4, "unit_price_cents": 1850},
{"sku": "SKU-6301", "quantity": 3, "unit_price_cents": 3900}
],
"total_cents": 26450
},
"line_count": 3, "total_cents": 26450
}
Two things to check by hand: the same seed always yields that exact payload, and
the API's /health counts don't move — preview never leaves the process.
send_orders (seeded) — the accept path
In Claude Code: "Send 5 synthetic orders with seed 1337."
{"mode":"seeded","seed":1337,"requested":5,"accepted":5,"rejected":0,
"status_counts":{"201":5},"accepted_total_cents":73550,"sample_failures":[]}
Watch the make api terminal — five POST /orders 201 Created lines appear.
send_orders (simple) — the reject path
In Claude Code: "Send 3 orders in simple mode with seed 1337."
{"mode":"simple","seed":1337,"requested":3,"accepted":0,"rejected":3,
"status_counts":{"422":3},"accepted_total_cents":0,
"sample_failures":[
"422 ['unknown customer C-5093', 'unknown sku SKU-9757', 'unknown sku SKU-6393', 'unknown sku SKU-1830']",
"422 ['unknown customer C-8975', 'unknown sku SKU-8549']",
"422 ['unknown customer C-5035', 'unknown sku SKU-5650', 'unknown sku SKU-6612', 'unknown sku SKU-0935']"
]}
sample_failures is capped at 5 entries, so a 100-order failure storm still
returns a readable result.
Replay by seed
Send a batch without a seed, note the seed in the response, then ask Claude
Code to send the same count with that seed. accepted_total_cents must match
exactly. This is the property test_omitted_seed_is_reported_back covers.
The MAX_BATCH guardrail
In Claude Code: "Send 500 synthetic orders."
Error: count 500 exceeds the server's MAX_BATCH of 100; send smaller batches or
raise MAX_BATCH in the server environment
count: 0 fails the same readable way (count must be at least 1, got 0). To
verify the bound is really server-side, raise it in .mcp.json's env block and
restart Claude Code — no prompt can change it.
The target-selection guardrail
Ask Claude Code to "send orders to https://example.com instead." It can't — there is no URL or host parameter on any tool. Confirm from the schemas:
uv run python -c "
import json, asyncio
from fastmcp import Client
from server import mcp
async def main():
async with Client(mcp) as c:
for t in await c.list_tools():
print(t.name, list((t.inputSchema or {}).get('properties', {})))
asyncio.run(main())"
check_target []
preview_order ['mode', 'seed']
send_orders ['count', 'mode', 'seed']
Retarget by editing ORDER_API_URL in .mcp.json and restarting Claude Code.
Manual testing without an MCP client
make send calls the tool function directly — no client, no transport. Useful
when you're changing generator logic and don't want to restart Claude Code:
make send COUNT=3 SEED=42
{
"mode": "seeded", "seed": 42, "requested": 3, "accepted": 3, "rejected": 0,
"status_counts": {"201": 3},
"accepted_total_cents": 42800, "sample_failures": []
}
COUNT and SEED are Makefile variables (defaults 25 / 1337). Note this path
bypasses MCP entirely, so it will not catch schema or transport problems — use
make smoke for those.
To poke the target API directly, skipping the MCP server too:
curl -s -X POST http://127.0.0.1:8000/orders \
-H 'content-type: application/json' \
-d '{"request_id":"manual-0001","customer_id":"CUST-0005","channel":"partner",
"currency":"USD","lines":[{"sku":"SKU-5510","quantity":2,"unit_price_cents":1850}],
"total_cents":3700}'
# {"order_id":"ORD-manual-0","customer_id":"CUST-0005","lines":1,"total_cents":3700}
Change total_cents to 9999 and it returns 422 with
total mismatch: sent 9999, expected 3700.
You can also run the MCP server by hand and type JSON-RPC at it:
make serve # stdio; useful only to confirm it starts and stays up
Configuration
Read from the server's environment (via .mcp.json's env block, or exported
before make serve) — never from a tool argument:
| Variable | Default | Meaning |
|---|---|---|
ORDER_API_URL |
http://127.0.0.1:8000 |
Where batches are POSTed. |
MAX_BATCH |
100 |
Hard cap on count. Must be ≥ 1. |
REQUEST_TIMEOUT_S |
10 |
Per-request HTTP timeout. |
Seed data lives in data/catalog.json (8 SKUs, one deliberately out of stock) and
data/customers.json (5 customers, each with allowed channels). Edit those to
change what "valid" means — both the API and the seeded generator read them, so
they stay in agreement.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
/mcp shows the server failed to connect |
.mcp.json has a stale absolute path. Re-run make config, restart Claude Code. |
reachable: false |
make api isn't running, or ORDER_API_URL points elsewhere. curl the /health endpoint. |
cannot reach the order API at … on send |
Same cause, surfaced as a tool error because a send can't degrade gracefully. |
| Tool edits don't take effect | The MCP server is a subprocess spawned at connect time. Restart Claude Code (the API's --reload does not apply to it). |
Address already in use on make api |
Something already holds port 8000: lsof -ti:8000 | xargs kill. |
uv: command not found |
brew install uv. |
| Everything passes but Claude Code sees no tools | You launched claude from outside the project root, so .mcp.json wasn't found. |
Project layout
api/main.py order intake API — the target under test
server.py the MCP server: 3 tools + 1 resource
synth/config.py env-derived settings (target URL, MAX_BATCH, timeout)
synth/seed.py loads data/*.json
synth/simple.py generator: random primitives → rejected orders
synth/seeded.py generator: samples the catalog → accepted orders
client.py in-process demo client (make demo)
scripts/smoke_stdio.py real stdio subprocess smoke test (make smoke)
tests/test_server.py control-surface tests (make test)
data/ catalog + customer seed data
Reference
This project accompanies Trigger Synthetic Orders from an MCP Server on macOS, which walks through the design: wrapping the generators in an MCP server, and the guardrails that keep a model from choosing where the traffic goes or how much of it there is.
The generators in synth/ come from the previous article in the series,
Generate Synthetic JSON Requests to Test an API on macOS,
which builds them as a plain CLI — read that first if you want the payload
generation explained before the MCP control surface wrapped around it.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。