Skip to content
Conduit

mcp server

a2a-fuzzer

Conformance and robustness fuzzing for A2A, MCP and JSON-RPC agent endpoints you own. Finds crashes, info leaks, validation and trust-boundary bugs.

kirtanbhatt10/a2a-fuzzercommunitylistedunreviewed

Set up

Runs on your computer. Pick your agent and operating system.

Before you start

  • uv (`curl -LsSf https://astral.sh/uv/install.sh | sh`)
~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "a2a-fuzzer": {
      "command": "uvx",
      "args": [
        "a2a-fuzzer"
      ]
    }
  }
}

same format Cursor, GitHub Copilot CLI, Junie, JetBrains AI Assistant, Augment (Auggie CLI), Factory Droid, Kiro, Devin CLI +12

Values in angle brackets are placeholders. Restart Claude Desktop after editing its config.Claude Desktop docs

README

synced Oct 29, 2026

a2a-fuzzer

Bounded conformance and robustness fuzzing for agent-to-agent protocols.

CI Version Python License: MIT SARIF

Point a2afuzz at an A2A, MCP, or plain JSON-RPC 2.0 HTTP endpoint you own. It sends a small, deterministic set of malformed and edge-case requests and tells you where the server:

  • crashes on bad input,
  • leaks internals such as stack traces, file paths, or credentials,
  • accepts input it should reject, or
  • trusts data a client should never control.

Every run writes JSON, HTML, and SARIF reports, so it drops straight into CI and GitHub code scanning.

[!IMPORTANT] Use this tool only on systems you own or have explicit written permission to test. It refuses any non-loopback target unless you pass --i-am-authorized. Requests and captured responses are capped at 256 KiB each; this is robustness testing, not load testing.

Contents

Features

Deterministic and finite26 generic JSON-RPC cases, plus 6 A2A or 5 MCP trust-boundary cases. The same inputs every run, so results are reproducible and diffable. Preview them with a2afuzz list without sending anything.
Six response oraclescrash, availability, info-leak, trust-boundary, validation, and conformance, each mapped to a severity.
Safe by defaultLoopback-only unless explicitly authorized, URL validation before any network I/O, no redirects followed, bounded payloads and captures, configurable rate limit and concurrency.
CI-ready--fail-on <severity> sets the exit code; SARIF output uploads to GitHub code scanning.
Small footprintTwo runtime dependencies (httpx, typer). Python 3.10+ on Linux, macOS, and Windows.

Install

The package is not on PyPI yet. Install it straight from GitHub:

pip install "git+https://github.com/kirtanbhatt10/a2a-fuzzer.git"

Or from a clone, with the development tools:

git clone https://github.com/kirtanbhatt10/a2a-fuzzer.git
cd a2a-fuzzer
python -m venv .venv && source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -e ".[dev]"

Check the install:

a2afuzz --version

Quick start

The repository ships a small demo agent (standard library only) with a deliberately vulnerable default mode and a hardened --secure mode.

# Terminal 1: the vulnerable target
python examples/demo_agent.py

# Terminal 2: fuzz it
a2afuzz run http://127.0.0.1:9999/ -p a2a

You will see something like:

Completed 32 cases: 35 findings (high=12, medium=22, low=1, info=0).
Reports: reports/a2afuzz.report.json, reports/a2afuzz.report.html, reports/a2afuzz.sarif

Open reports/a2afuzz.report.html in a browser for the full breakdown. Exact counts can vary slightly by Python version, because the demo relies on the standard-library JSON parser.

Now run the hardened mode and watch every finding disappear:

python examples/demo_agent.py --secure --port 9998
a2afuzz run http://127.0.0.1:9998/ -p a2a
# Completed 32 cases: 0 findings (high=0, medium=0, low=0, info=0).

The --secure code path in examples/demo_agent.py is a compact reference for the controls each check expects: strict parsing, envelope and ID validation, size and depth limits, and trust-boundary rejection.

Usage

a2afuzz run URL [OPTIONS]      Run the probes and write reports
a2afuzz list [-p PROTOCOL]     Print the case set without sending anything
a2afuzz --version              Print the version
python -m a2afuzz ...          Same CLI, without the console script

URL is the JSON-RPC HTTP endpoint. Each case is sent as one POST with Content-Type: application/json.

OptionDefaultDescription
-p, --protocola2aa2a, mcp, or jsonrpc
-o, --outreports/Directory for the JSON, HTML, and SARIF reports (created if missing)
--concurrency4Maximum requests in flight
--rate10Maximum requests per second; 0 means unlimited
--timeout10Per-request timeout in seconds; responses slower than 80% of it are flagged
-H, --headernoneExtra request header as 'Name: value'; repeatable (for example an auth token)
--onlyallCase-ID prefix or category; repeatable or comma-separated
--fail-onnoneExit 1 if any finding is at or above info, low, medium, or high
--i-am-authorizedoffRequired before probing any non-loopback host

Examples

# Only the trust-boundary and parsing cases
a2afuzz run http://127.0.0.1:8080/rpc --only trust-boundary,parsing

# An authenticated staging endpoint you are authorized to test
a2afuzz run https://agent.staging.example.com/a2a -p a2a \
  -H "Authorization: Bearer $TOKEN" --i-am-authorized --rate 2

# An MCP server that answers JSON-RPC over plain HTTP
a2afuzz run http://127.0.0.1:3000/mcp -p mcp --fail-on medium

Exit codes

CodeMeaning
0The run completed and no finding met the --fail-on threshold (or no threshold was set)
1At least one finding met the --fail-on threshold
2Usage error: bad option, malformed or unauthorized target, --only matched nothing, or the report directory is not writable
130Interrupted with Ctrl-C; no reports were written

What it checks

Run a2afuzz list -p <protocol> to see every case. In total that is 26 cases for jsonrpc, 32 for a2a, and 31 for mcp.

CategoryCasesExamples
parsing6Truncated or empty body, UTF-8 BOM, invalid UTF-8, NaN, duplicate keys
envelope6Missing or wrong jsonrpc version, missing or non-string method, scalar params
ids4Object IDs, 40-digit integers, 10 KiB strings, bidi and zero-width characters
methods4Unknown, reserved rpc.*, path-like, and dunder method names
notifications1A notification must not get a response
batches2Empty batch, batch with a malformed member
resource-limits32,000-level nesting, 240 KiB string, 20,000-key object
trust-boundary (A2A)6Client claiming the agent or system role, file:// and ../ URIs, foreign or wildcard task IDs
trust-boundary, validation (MCP)5Unsupported protocol version, traversal and sensitive resources/read URIs, malformed tools/call and initialize

Each response is evaluated by these oracles:

OracleSeverityDetects
crashhighHTTP 5xx in response to malformed input
availabilityhigh / lowTimeouts (high); dropped connections, slow responses, oversized responses (low)
info-leakhigh / mediumCredential-like tokens or stack traces (high); server file paths (medium)
trust-boundarymediumA success result for a role, URI, or task-ownership probe
validationmediumA success result for malformed or unsupported input
conformancelowReplies to notifications, non-JSON success bodies, wrong jsonrpc version, mismatched response IDs

A JSON-RPC error object, an HTTP 4xx, or a clean refusal is always a pass. For the ids category a null response ID is accepted, because the specification allows it when the request ID cannot be read.

Reports

Every run writes three files to --out:

FilePurpose
a2afuzz.report.jsonComplete machine-readable record: every request, response status, headers, body, timing, and finding. Bodies that are not valid UTF-8 are stored as base64.
a2afuzz.report.htmlSelf-contained report for humans, with light and dark themes. All endpoint content is HTML-escaped.
a2afuzz.sarifSARIF 2.1.0, ready for GitHub code scanning or any SARIF viewer

[!WARNING] Reports can contain whatever the target sent back, including leaked secrets that triggered an info-leak finding. Treat them as sensitive. Request headers you pass with -H are never written to the reports.

Using it in CI

A GitHub Actions job that starts your agent, fuzzes it, fails the build on medium or worse, and uploads the results to code scanning:

jobs:
  fuzz:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write   # required for the SARIF upload
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-python@v6
        with:
          python-version: "3.12"
      - run: pip install "git+https://github.com/kirtanbhatt10/a2a-fuzzer.git"
      - name: Start the agent under test
        run: |
          python -m my_agent --port 8080 &
          sleep 2
      - name: Fuzz
        run: a2afuzz run http://127.0.0.1:8080/ -p a2a --fail-on medium
      - uses: github/codeql-action/upload-sarif@v4
        if: always()
        with:
          sarif_file: reports/a2afuzz.sarif

Pin the install to a tag or commit (...a2a-fuzzer.git@<sha>) so the case set only changes when you choose to upgrade.

Python API

The CLI is a thin wrapper around a small async API:

import asyncio

from a2afuzz import FuzzRunner, write_reports


async def main() -> None:
    runner = FuzzRunner(protocol="a2a", concurrency=4, rate=10, timeout=10)
    result = await runner.run("http://127.0.0.1:9999/", only=["a2a-trust"])
    for case in result.cases:
        for finding in case.findings:
            print(case.case.id, finding.severity, finding.message)
    write_reports(result, "reports")


asyncio.run(main())

FuzzRunner.run() raises TargetAuthorizationError before any network I/O if the URL is malformed, or non-local without authorized=True. You can pass your own transport object (anything with async send(url, body) -> TransportResponse) to test non-HTTP transports or to unit-test your own oracles.

Limitations

  • Supports JSON-RPC over HTTP POST with a JSON response. Server-sent-event streams (for example A2A message/stream), WebSockets, and MCP stdio servers are not exercised.
  • MCP Streamable HTTP servers that require an initialize handshake, an Mcp-Session-Id, or Accept: text/event-stream will reject most probes with HTTP 4xx, which counts as a pass. Supply what the server needs with -H (user headers replace the defaults) and check the status codes in the JSON report before trusting a clean result.
  • Authentication is limited to static headers passed with -H.
  • The suite is a fixed, reviewable set of probes, not a coverage-guided or mutation fuzzer. A clean run means none of these known mistakes were found, not that the server is bug-free.
  • Trust-boundary checks are heuristics. If your server legitimately accepts one of those inputs (for example an operator-only endpoint), triage the finding instead of suppressing the whole category; use --only to scope runs.

Development

pip install -e ".[dev]"
python -m pytest                  # unit, CLI, and end-to-end tests against the demo agent
python -m ruff check .
python -m ruff format --check .

Project layout:

a2afuzz/
  cli.py          Typer CLI (run, list, --version)
  generators.py   Deterministic case sets for jsonrpc, a2a, and mcp
  runner.py       Target validation, concurrency, rate limiting
  transport.py    httpx transport with timeouts and capped capture
  checks.py       Response oracles and severity thresholds
  report.py       JSON, HTML, and SARIF writers
  models.py       Dataclasses shared across modules
examples/
  demo_agent.py   Vulnerable and hardened demo target
tests/            Unit, CLI, and end-to-end tests

CI runs lint and the full test suite on Python 3.10 to 3.14 (plus macOS and Windows), then builds the wheel, installs it into a clean environment, and checks that the hardened demo passes and the vulnerable demo is caught. See CHANGELOG.md for release notes.

Responsible use

This tool is for builders testing their own services, local and staging deployments, or targets covered by explicit written permission. Do not use it against third-party agents you do not control. Keep the default rate limit unless you own the infrastructure.

License

MIT © Kirtan Bhatt. See LICENSE.

↑↓ navigate openesc close