Skip to main content
Glama
mitchallen

Synthetic Orders MCP Server

by mitchallen

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

  • seeded samples the real catalog and customer list, so orders validate and come back 201. Use it for traffic that should be accepted.

  • simple invents every field from random primitives, so the API rejects it with 422. 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_URL in the server's environment, so no prompt can aim the generator at an arbitrary host.

  • count is bounded by MAX_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)

  • uvbrew 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 sync fetches 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.


Related MCP server: Korral StoreLink MCP

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 way make api does.

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.

Available Tools

3 tools
check_targetCheck the order APIA
Read-onlyIdempotent

Report whether the configured order API is reachable, and what it holds.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
skusNo
api_urlYes
customersNo
reachableYes

TDQS

A4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint, openWorldHint, and idempotentHint, so the safety profile is clear. The description adds that the tool reports reachability and contents, but does not disclose details like response format, potential errors, or any side effects beyond what annotations already cover.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, front-loaded sentence that directly states the tool's purpose with no filler or redundancy. Every word contributes meaning.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple zero-parameter check tool with rich annotations and an output schema, the description adequately covers the tool's function. It clearly states what is reported (reachability and contents), though it does not elaborate on the meaning of 'what it holds' or offer guidance relative to sibling tools.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has zero parameters, so the baseline is 4. The description correctly avoids describing parameters that do not exist, and no additional parameter semantics are needed.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb ('Report') and a clear resource ('the configured order API'), stating exactly what is checked: reachability and contents. This distinguishes it from siblings 'preview_order' and 'send_orders', which clearly perform different operations.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies using this tool to verify API availability and inspect its contents, which suggests a preflight check before operations like previewing or sending. However, it does not explicitly state when to use it versus the sibling tools or provide any exclusion criteria.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

preview_orderPreview a synthetic orderA
Read-onlyIdempotent

Generate one order and return it without sending it.

seeded samples the catalog and should be accepted; simple invents every field and should be rejected. Pass seed to reproduce an exact payload.

ParametersJSON Schema
NameRequiredDescriptionDefault
modeNoseeded
seedNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
modeYes
seedYes
orderYes
line_countYes
total_centsYes

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description adds significant behavioral context beyond the annotations: it explains that 'seeded' samples the catalog while 'simple' invents fields, and that 'seed' reproduces an exact payload. This is useful, non-obvious information that helps the agent anticipate results. No contradiction with readOnlyHint or idempotentHint.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is three sentences, front-loaded with the core purpose, followed by concise parameter guidance. Each sentence adds necessary information without redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simplicity, the presence of an output schema, and the annotations, the description is complete: it covers the operation's purpose, the mode behavior, and the seed parameter. No gaps remain for the agent to guess.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Despite 0% schema description coverage, the description fully explains both parameters: 'mode' with its two enum values and their implications, and 'seed' for reproducing a payload. This provides meaning beyond the raw schema definitions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states the specific verb 'Generate' and resource 'one order', with the key differentiator 'without sending it'. This clearly distinguishes it from sibling tools like send_orders, which actually sends orders.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear context for when to use the tool (previewing an order before sending) and even gives guidance on which mode to choose ('seeded' should be accepted, 'simple' should be rejected). It doesn't explicitly name sibling tools or state when not to use it, but the 'without sending it' contrast implies the alternative.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

send_ordersSend synthetic ordersA

Generate count orders and POST each one to the configured order API.

Use seeded for traffic that should be accepted and simple to drill the API's rejection path. The result reports the seed, so the same batch can be replayed by passing it back in.

ParametersJSON Schema
NameRequiredDescriptionDefault
modeNoseeded
seedNo
countNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
modeYes
seedYesReplay this batch by passing this seed back in.
acceptedYes
rejectedYes
requestedYes
status_countsYes
sample_failuresYes
accepted_total_centsYes

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the annotations, the description adds behavioral context: orders are POSTed, the result reports the seed, and replay is possible by passing the seed back. This clarifies side effects and error-path drilling, which is useful for an agent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two concise sentences and a follow-up sentence, all front-loaded with the primary action. No redundant text; every sentence adds operational value.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The tool has an output schema, so return values likely are covered there. The description sufficiently covers the tool's purpose, mode distinctions, and replay behavior, making it complete for a low-complexity tool with three optional parameters.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must and does explain parameters: 'count' is the number of orders, 'mode' (seeded/simple) has usage semantics, and 'seed' is returned for replay. All three parameters are meaningfully covered in natural language.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb 'Generate' and 'POST' with a clear resource ('orders') and destination ('configured order API'). It clearly distinguishes itself from sibling tools like check_target and preview_order, which likely handle other aspects of order workflow.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit guidance on when to use each mode ('Use `seeded` for traffic that should be accepted and `simple` to drill the API's rejection path'). It does not explicitly mention when to use this tool over sibling tools, but the purpose is so distinct that no conflict arises.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 3 tool updatesv0.1.0
    • First observedcheck_target
    • First observedpreview_order
    • First observedsend_orders

TDQS

A4.4/5.0

Scored across 3 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: check_target verifies API connectivity, preview_order generates a single order without sending, and send_orders generates and sends multiple orders. There is no overlap or ambiguity between them.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern (check_target, preview_order, send_orders). The naming is predictable and uniform.

Tool Count5/5

With 3 tools, the server is well-scoped for its purpose of generating and sending synthetic orders. Each tool serves a distinct function without redundancy or unnecessary bloat.

Completeness5/5

The tool set covers the essential workflow: checking the target, previewing an order, and sending orders. The ability to replay seeds in send_orders addresses the need for reproducibility, leaving no obvious gaps in the stated domain.

Maintenance

ActivitySlowing
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables stock assessment and replenishment by exposing three deterministic tools: inspect stock positions, raise replenishment orders, and check order status. Includes an auditable local client and follows a security-first design with limited API surface.
    -
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables local auditing of trading strategies through MCP checkers for point-in-time data provenance, pre-trade order validation, and regime-fragility testing, while deferring promotion verdicts to a hosted service.
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables read-only inspection of ad-bid recommendations, scenario analysis, and explanation of threshold decisions using synthetic fixtures, without requiring live ad accounts or credentials.
    -