Files
5403657dbf test: drive the MCP server the way its clients do (#1608)
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]>
2026-09-29 17:10:51 -03:00

2.6 KiB

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.