The MCP server is the one part of Echidna that another program talks to, and the Haskell suite cannot say whether that conversation works: it runs in process, while what matters here is what goes over the wire and what the tools answer about a campaign that is actually fuzzing. So this suite starts one. conftest.py launches a real echidna against a fixture contract and waits for the server to accept an MCP initialize rather than merely for the socket to open, then hands the same campaign to every module. Four layers ride on it: test_mcp.py covers the nine tools -- what each one reports about a live campaign, and what it says when it cannot answer. status while the campaign fuzzes, coverage marked up line by line, an LCOV file that is really on disk, an injected sequence that the workers keep fuzzing through, a replayed sequence reported call by call including the revert reason, a sampled function whose counts show up in status a few seconds later. Anything that steers the campaign is undone afterwards, so no test depends on the order it ran in. test_mcp_conformance.py is the regression guard for "real clients can still connect". A notification gets 202 with no body; a GET asking for a server stream gets 405 rather than the JSON blob a strict client tries to read as SSE; initialize negotiates a version and names the server. These are exactly the two behaviours mcp-server 0.2.0.1 fixed, and the reason that version is the floor. test_mcp_codex.py replays Codex's rmcp handshake step by step, and test_mcp_claude.py drives the server with the reference `mcp` SDK through a real session. Both are transport-level and need no API key, so both run in CI; a live-model run is the LangGraph example instead. That example is here too. It reads status, and when the campaign has gone a minute without finding coverage it hands Claude the ABI and the coverage report and injects the sequences that come back -- a demonstration that those tools are enough to close the loop, not a tuned strategy. Some notes on what differs from the version this is adapted from: The tool tests are written against the server as it is rather than the prototype: status answers JSON, show_coverage takes a contract, and a tool that cannot do what it was asked answers with an MCP error result. They assert on all three, where the originals asserted that some substring appeared somewhere in the text. They also go through the shared wire helpers after a handshake, rather than a second raw client that posted tools/call with no handshake and no headers -- asserting leniency next to a suite that asserts strictness said two different things about what the server promises. The `mcp` SDK's 2.0 renamed its transport helper, dropped the third stream it used to yield, and moved its result models to snake_case. Rather than straddle both majors, requirements-test.txt follows the current one and the test skips itself on an older SDK. The other pins are bounded by major version only, since a compatibility suite should be running against what clients actually ship. The fixture contracts lost their spec-kit headers, two properties that could not fail (one returned a literal `true`), and the `test` prefix on their entry points, which means something else in Foundry mode. What is left is a contract that gives the tools something to report on: calls that succeed, calls that revert with a reason, and properties that hold so the campaign keeps running for as long as the session needs it. The workflow now triggers on master and on pull requests against it, like every other workflow here, and only for paths that can change what the server answers -- it builds the whole closure and then fuzzes, which is not something to do on every push to every branch. Its Cachix token is unavailable to pull requests from forks, so those runs read the public cache and push nothing; that is noted where it will be read. tests/mcp/requirements.txt is gone: nothing referenced it, and it contradicted both its sibling and the example's README on what to install. So is pytest.ini, which was only read when pytest was given the directory as an argument -- the one async test carries an explicit marker instead, which works either way. Co-authored-by: Dani Tradito <[email protected]> Co-authored-by: gustavo-grieco <[email protected]>
Echidna MCP agent example
An agent that connects to a live Echidna campaign through its MCP server, watches what the fuzzer is reaching, and aims it at what it is not.
Requirements
pip install langchain-anthropic langgraph httpx
Usage
1. Start Echidna with the MCP server:
echidna MyContract.sol --server 8080 --format text
--format text is required — the interactive TUI otherwise owns the terminal.
2. Run the agent:
export ANTHROPIC_API_KEY=your_key_here
python examples/mcp_agent.py
It polls until you stop it with Ctrl-C. Point it at another campaign with
ECHIDNA_MCP_URL=http://127.0.0.1:9000/mcp.
What it does
Every 30 seconds it calls status. When the campaign has gone a minute without
finding new coverage, it reads target and show_coverage, asks Claude for
call sequences that would reach lines the campaign has not, and injects them
with inject_fuzz_transactions — clearing the previous round first, so the
fuzzer is not left splitting its budget across every ordering ever suggested.
The tools the server exposes
Four report on the campaign:
| Tool | Description |
|---|---|
status |
Corpus size, iterations, coverage, failing tests, optimization values, how long since the last coverage and which functions found it, and whatever sample is recording |
target |
The contract under test and the functions it exposes |
show_coverage |
One contract's source, line by line, marked with what the campaign reached |
dump_lcov |
Write the coverage so far to an LCOV file |
Five steer it:
| Tool | Description |
|---|---|
inject_fuzz_transactions |
Spend part of the fuzzer's budget on a specific ordering of calls |
clear_fuzz_priorities |
Forget every injected ordering and return to the corpus |
execute_sequence |
Run a concrete sequence and report what each call did, without disturbing the campaign |
sample |
Record what one function does as the campaign calls it; results appear in status |
reload_corpus |
Pick up whatever was written to the corpus directory since the campaign started |
inject_fuzz_transactions and execute_sequence take a sequence written the way
the calls would be in Solidity, separated by ;. inject_fuzz_transactions
additionally accepts ? for an argument the fuzzer should choose:
approve(0x10, ?); transferFrom(?, ?, 100)
Testing the server
tests/mcp/ drives a real campaign the way these clients do — tool semantics,
wire-protocol conformance, and the official MCP SDK. See the
README for how to run it.