Connecting Claude to a Real STARK Prover via MCP
A cryptographic prover exposed as two MCP tools: prove and verify. Built against rmcp's real API, verified against its own examples rather than guessed — and the same never-trust-the-prover's-own-claim discipline applies here too.
MCP (Model Context Protocol) is how an AI assistant calls tools directly rather than a human copy-pasting CLI output into a chat window. This project exposes exactly two tools over it — prove and verify — running on their own port, separate from the plain HTTP API.
Why a separate port
The obvious design would nest the MCP handler onto the same axum router as /v1/proofs. That doesn't actually work cleanly: rmcp's StreamableHttpService implements tower::Service, but its response body type doesn't line up with axum::Router::nest_service without writing an adapter — and rmcp's own upstream examples serve it via a raw hyper accept loop for that exact reason. Rather than claim an integration that wasn't actually verified to work, the MCP server runs on port 4478, the HTTP API on 4477, and the doc comment in the source says why, instead of presenting a workaround as if it were the obvious design.
The two tools
#[tool(description = "Generate a real STARK proof that a .zkasm program \
executes to its claimed result. Returns the base64-encoded proof and \
the (initial, result) public inputs.")]
async fn prove(&self, Parameters(args): Parameters<ProveArgs>)
-> Result<CallToolResult, McpError>
#[tool(description = "Verify a proof (from the prove tool) against the \
exact .zkasm program it claims to be for. Re-executes the program \
locally to know what to check -- it never trusts the proof's own \
claims.")]
async fn verify(&self, Parameters(args): Parameters<VerifyArgs>)
-> Result<CallToolResult, McpError>
The tool descriptions aren't just documentation for a human reading the source — they're what an MCP client (Claude Desktop, Cursor, anything speaking the protocol) actually sees when deciding whether and how to call the tool. Getting them accurate matters the same way an API's actual behavior does.
verify doesn't trust prove's answer
The verify tool takes a program and a base64-encoded proof, and does exactly what the CLI's verify command and the HTTP server's /v1/verify endpoint do: parse the program, re-execute it locally, build the expected public inputs from that re-execution, and only then check the proof against them:
let padded = program.padded();
let pub_inputs = public_inputs_for_program(&padded);
match verify_program(proof, pub_inputs) { ... }
An MCP client calling verify isn't trusting whatever the prove call (possibly from a different, untrusted source) claimed — it's getting an independent check, the same trust boundary the rest of this project relies on everywhere else.
Verified, not assumed
The claim "this actually works as an MCP server" isn't just "it compiles" — it's confirmed by capturing a real transcript against a running server: a real initialize handshake, followed by real tools/call invocations for both prove and verify, via scripts/mcp_demo.sh. That script exists specifically so "the MCP integration works" is a thing that gets exercised, not just declared.