Skip to main content

mcp-fuzz

Runtime behavioral testing for MCP servers.

mcp-doctor reads an MCP server's source code and checks whether its tools are well-documented. mcp-fuzz does the opposite: it actually launches the server and calls its tools, with inputs derived from each tool's own declared JSON schema, and checks whether the server behaves the way that schema and its description claim — does a missing required field get a structured error back, or does the server crash? Does a wrong-typed field get rejected cleanly, or does it hang?

Static analysis can't see any of that. Only running the code can.

Install

pip install mcp-runtime-check

(The PyPI distribution name is mcp-runtime-checkmcp-fuzz and close variants were blocked by PyPI's anti-typosquat check as too similar to existing packages, same naming friction mcp-doctor hit. The installed CLI command is still mcp-fuzz, and the repo/import package are unchanged.)

Use

mcp-fuzz -- python server.py
mcp-fuzz -- npx -y some-mcp-server

mcp-fuzz launches the command you give it as an MCP server over stdio, lists its tools, and for each one runs three kinds of calls built purely from that tool's own inputSchema — no LLM, no network calls of its own:

  • valid — one plausible value per property (respecting type, enum, minimum/maximum, format, ...) — should succeed.
  • missing required — the valid call, with one required property removed at a time — should come back as a structured MCP error, not a crash.
  • wrong type — the valid call, with one property swapped to a value of a different JSON type — same expectation.

Any call that crashes the server, hangs past --timeout (default 15s), or leaves the connection unusable triggers a full reconnect before the next case runs, so one bad tool doesn't invalidate the rest of the report.

Safety — read this before pointing it at anything real

mcp-fuzz actually executes tool calls. Unlike mcp-doctor, it has real side effects if a tool does. By default, only tools annotated readOnlyHint: true are tested — everything else is skipped and listed as such in the report. Pass --include-destructive to test everything, but only against a server you're confident is safe to call blindly (a local sandbox, a test/staging backend) — never a server wired to production data, a real inbox, a real payment system, etc. Many real-world servers don't set readOnlyHint accurately or at all, in which case those tools are conservatively skipped rather than assumed safe.

What the score means

The reported "crash resilience" percentage covers only the missing-required and wrong-type cases — the fraction that came back as a structured error instead of a crash or hang. It does not grade whether the tool's "valid" call produced a correct result: a synthetic, schema-only-derived value (a placeholder string where the field really expects a real arXiv ID, or a URL that has to actually resolve) often isn't realistic enough for a failure there to be a fair judgment. A failed "valid" call is reported separately, flagged explicitly as "may be a synthetic-input false positive, not a confirmed bug" — worth a manual look, not proof of a bug.

JSON output / CI

mcp-fuzz --json -- python server.py
mcp-fuzz --fail-under 90 -- python server.py   # non-zero exit if crash resilience < 90%

Real-world spot check

Repo Stars Lang What mcp-fuzz found
modelcontextprotocol/server-everything TS Official reference server, run cross-language via npx. Clean pass — 9/9 read-only tools handled every bad-input case cleanly (100%/A). trigger-long-running-operation's valid call correctly timed out — it's deliberately a long-running operation, exactly the kind of result the report's own "may be a false positive" framing exists for.
blazickjp/arxiv-mcp-server 3.1k Python Found a real bug in mcp-fuzz itself, not the target: installing this repo (which pins mcp<2.0) into the same environment downgraded the shared mcp package from 2.1.1 to 1.29.1. mcp<2.0 exposes several fields under their raw camelCase wire name (isError, inputSchema, readOnlyHint); mcp>=2.0 renamed them to snake_case. Every hardcoded snake_case attribute access broke with an AttributeError the moment an older mcp happened to be installed. Fixed with a small compatibility helper that tries the current name first, falls back to the old one. 11 tools tested cleanly afterward (100%/A) — the several "valid call errored" flags are exactly the documented synthetic-input false-positive case (a placeholder "paper_id": "test" isn't a real arXiv ID).
punitarani/fli Python Clean pass — all 4 read-only tools (Google Flights MCP) handled every bad-input case cleanly, and even the synthetic "valid" inputs succeeded without error (100%/A, no "worth investigating" flags at all).
modelcontextprotocol/server-sequential-thinking TS Official reference server. Clean pass, tested with --include-destructive (its one tool isn't read-only-annotated but has no real side effects) — 100%/A.
modelcontextprotocol/server-filesystem TS Official reference server, run against a throwaway sandbox directory. Clean pass — 6 of 10 read-only tools "valid call errored" on ENOENT: no such file, exactly the documented synthetic-input false positive: a generic placeholder string isn't a real path that exists in the sandbox. The 4 write/edit/move/create tools were correctly skipped as not read-only.
antvis/mcp-server-chart 4.3k TS The strongest real finding yet — 37.85%/F. All 27 read-only chart/diagram tools crash (not a graceful structured error) on realistic bad input: 133 of 214 missing-required/wrong-type calls raised a raw internal exception through as a protocol-level error instead. generate_bar_chart alone: omitting the required data array → Cannot read properties of null (reading 'map'); passing the wrong type for datae.map is not a function; a wrong-typed title even crashed a downstream call to a remote rendering API with an HTTP 500. Every one of these is a completely realistic mistake a real LLM agent could make (a hallucinated missing or wrong-typed argument), not an artifact of unrealistic synthetic data — the strongest, most legitimate signal this tool has produced. Filed upstream — traced to an already-merged-but-unreleased fix (main's 9fd0bb4/#292), confirmed and commented.
haris-musa/excel-mcp-server 4.1k Python Clean pass — 6 read-only tools handled all 31 bad-input cases cleanly (100%/A). 19 write tools correctly skipped as not read-only.
czlonkowski/n8n-mcp 23k TS Clean pass, run via npx — 7/7 tools, 44 bad-input cases, 100%/A. Several "valid call errored" flags are the documented synthetic-input false positive (a placeholder node/workflow name that doesn't exist).
mendableai/firecrawl-mcp-server 7k+ TS Found a real bug in mcp-fuzz itself, not the target. Run keyless (no FIRECRAWL_API_KEY, its two free tools hit the real Firecrawl cloud). Every one of 93 bad-input calls came back a false 0%/F — firecrawl validates arguments with zod and has the SDK raise a well-formed JSON-RPC -32602 INVALID_PARAMS error for a bad call, rather than a CallToolResult with isError=true content; mcp-fuzz's blanket except Exception treated that identically to a real crash. Fixed by classifying MCPError on its actual code: -32602 (real schema validation) is graceful, everything else stays a crash — see "A bug in mcp-fuzz itself" below. Re-verified: 93/93 handled cleanly, 100%/A.
official reference servers: fetch, time, git Python/TS All clean passes (100%/A). fetch's one tool isn't annotated readOnlyHint despite genuinely being read-only GET-style — re-tested with --include-destructive, still clean; a real but minor annotation gap, not worth filing upstream on Anthropic's own reference implementation.
modelcontextprotocol/server-memory TS Official reference server (in-memory knowledge-graph store). Clean pass — 100%/A, 0 crashes across 4 bad-input calls. Only 3 of 9 tools are annotated readOnlyHint/tested by default (read_graph, search_nodes, open_nodes); the 6 mutating tools (create_entities, add_observations, etc.) are correctly skipped without --include-destructive, exactly as documented.
qdrant/mcp-server-qdrant Python Clean pass — 100%/A, 0 crashes across 8 bad-input calls on both tools, tested with --include-destructive against a fully local embedded store (QDRANT_LOCAL_PATH, no external service). Both "valid" calls errored with "All connection attempts failed" — likely qdrant-client's AsyncQdrantClient not fully supporting its own embedded local-storage mode, not confirmed as an mcp-server-qdrant or mcp-fuzz bug, and not chased further (out of scope for today's pass; the bad-input crash-resilience score itself is unaffected).
upstash/context7 61k TS Widely-used documentation-lookup server (@upstash/context7-mcp). Clean pass — 100%/A, 0 crashes across 8 bad-input calls on both of its tools (resolve-library-id, query-docs), both annotated read-only and tested. Largest-star repo checked so far.

A bug in mcp-fuzz itself, and the two-step fix it took to get right

The firecrawl false-0%/F above led to a genuinely tricky classification bug, worth documenting honestly rather than glossing over. The client SDK raises the same exception type, MCPError, for two completely different situations:

  1. A real, well-formed JSON-RPC error response actually received from the server — e.g. -32602 INVALID_PARAMS when schema validation (zod, pydantic, ...) rejects a bad call. This is exactly the "structured error back" a well-behaved server is supposed to return — not a crash.
  2. A client-synthesized error for a transport failure (-32000 CONNECTION_CLOSED) or the SDK's own internal request timeout (-32001 REQUEST_TIMEOUT) — no real response was ever received. Still a crash/timeout.

The first fix attempt only excluded case 2 and treated every other MCPError as graceful. That over-corrected: re-running it against antvis/mcp-server-chart (the 37.85%/F finding above) silently flipped it to a false 100%/A. The real cause: many frameworks use a third code, -32603 INTERNAL_ERROR, to wrap an unhandled exception from the tool's own business logic (a raw TypeError: Cannot read properties of null) so it doesn't kill the whole process — a completed round trip, but not the server "behaving the way its schema claims" either. The correct rule, verified against both repos simultaneously (each has the other as its regression test): only -32602 INVALID_PARAMS is trusted as "properly handled"; every other code, including -32603, stays a crash. Re-checked against both real repos afterward: firecrawl-mcp-server 100%/A, antvis/mcp-server-chart back to the original, correct 37.85%/F (exactly 133/214) — the fix removed the false positive without touching the real finding.

Known limitations

  • Input generation is schema-only. A property with no type (or a genuinely ambiguous anyOf) is skipped from the wrong-type test set rather than guessed at.
  • No semantic check of what a successful response actually contains — that's a deliberately separate, opt-in, LLM-backed capability planned for a later release, not v1.
  • stdio transport only for now; no HTTP/SSE servers yet.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mcp_runtime_check-0.1.2.tar.gz (20.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mcp_runtime_check-0.1.2-py3-none-any.whl (18.1 kB view details)

Uploaded Python 3

File details

Details for the file mcp_runtime_check-0.1.2.tar.gz.

File metadata

  • Download URL: mcp_runtime_check-0.1.2.tar.gz
  • Upload date:
  • Size: 20.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for mcp_runtime_check-0.1.2.tar.gz
Algorithm Hash digest
SHA256 524b1eeb5976e9b9be54129a76f98b664fd24c6eb3b47ee70c20e2c6dcaca79d
MD5 394018fef13a0b47573948e48f81d902
BLAKE2b-256 d5d7c2a0cf6023e92cef85e0c05bd03a9df319e724a3af9ab9c3a13c8b313b71

See more details on using hashes here.

Provenance

The following attestation bundles were made for mcp_runtime_check-0.1.2.tar.gz:

Publisher: publish.yml on vishalhabib99/mcp-fuzz

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file mcp_runtime_check-0.1.2-py3-none-any.whl.

File metadata

File hashes

Hashes for mcp_runtime_check-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 e50f9191a2af286eddf2cd297ac92fb17473960fba54eb6f8cff461381220afb
MD5 e12882b5ac532ef9e0c1ee690d9fccbb
BLAKE2b-256 23595c1857b1a1361ea3549cc387061cfacbc157846d79a835b251424e644f6b

See more details on using hashes here.

Provenance

The following attestation bundles were made for mcp_runtime_check-0.1.2-py3-none-any.whl:

Publisher: publish.yml on vishalhabib99/mcp-fuzz

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 files

0.1.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page