Skip to content

Commit 10cc846

Browse files
Claude/assess agent tier readiness w q od r (#36)
Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7b5191e commit 10cc846

4 files changed

Lines changed: 324 additions & 6 deletions

File tree

crates/echidna-mcp/README.md

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
<!-- SPDX-License-Identifier: PMPL-1.0-or-later -->
2+
<!-- SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> -->
3+
4+
# echidna-mcp
5+
6+
Model Context Protocol server that exposes [ECHIDNA](https://github.com/hyperpolymath/echidna)'s
7+
105-prover portfolio to AI coding agents (Claude Code, Claude API, etc.) as first-class
8+
tool-use actions over stdio.
9+
10+
## Installation
11+
12+
```sh
13+
# From source (workspace root)
14+
cargo install --path crates/echidna-mcp
15+
16+
# Or build without installing
17+
cargo build -p echidna-mcp --release
18+
```
19+
20+
The `echidna` binary must be on `PATH` (or set `ECHIDNA_BIN=/abs/path/echidna`).
21+
22+
## Claude Code integration
23+
24+
Add to your project's `.claude/settings.json`:
25+
26+
```json
27+
{
28+
"mcpServers": {
29+
"echidna": {
30+
"command": "echidna-mcp"
31+
}
32+
}
33+
}
34+
```
35+
36+
Or with an explicit binary path:
37+
38+
```json
39+
{
40+
"mcpServers": {
41+
"echidna": {
42+
"command": "echidna-mcp",
43+
"env": { "ECHIDNA_BIN": "/usr/local/bin/echidna" }
44+
}
45+
}
46+
}
47+
```
48+
49+
## Tools
50+
51+
### `prove`
52+
53+
Prove a theorem from a file using one of ECHIDNA's 105 backends.
54+
55+
| Parameter | Type | Required | Description |
56+
|-----------|------|----------|-------------|
57+
| `file` | string | yes | Absolute path to the proof file |
58+
| `prover` | string | no | Backend name (e.g. `Z3`, `Coq`, `Lean`, `Isabelle`). Auto-detected from extension if omitted. |
59+
| `timeout_secs` | integer | no | Prover timeout in seconds. Default: 300. |
60+
61+
**Returns** a JSON object:
62+
```json
63+
{
64+
"verified": true,
65+
"message": "Goal discharged.",
66+
"prover": "Z3",
67+
"raw_output": "...",
68+
"stderr": ""
69+
}
70+
```
71+
72+
**Accepted file formats** (by prover):
73+
74+
| Extension | Prover |
75+
|-----------|--------|
76+
| `.smt2` | Z3, CVC5, Alt-Ergo |
77+
| `.lean` | Lean 4 |
78+
| `.v` | Coq / Rocq |
79+
| `.agda` | Agda |
80+
| `.thy` | Isabelle/HOL |
81+
| `.miz` | Mizar |
82+
| `.fst` | F* |
83+
| `.dfg` | SPASS |
84+
| `.mm` | Metamath |
85+
86+
### `check_prover`
87+
88+
Check whether a named prover backend is installed and reachable on the current system.
89+
90+
| Parameter | Type | Required | Description |
91+
|-----------|------|----------|-------------|
92+
| `prover` | string | yes | Prover backend name (case-sensitive). Examples: `Z3`, `Lean`, `Coq`, `Vampire`. |
93+
94+
**Returns** a JSON object:
95+
```json
96+
{
97+
"available": true,
98+
"message": "Z3 is available"
99+
}
100+
```
101+
102+
### `list_provers`
103+
104+
List all 105 prover backends supported by ECHIDNA. Takes no parameters.
105+
106+
**Returns** a JSON object:
107+
```json
108+
{
109+
"total": 105,
110+
"provers": {
111+
"Z3": "SMT solver (Microsoft Research)",
112+
"Lean": "Lean 4 interactive proof assistant",
113+
"...": "..."
114+
}
115+
}
116+
```
117+
118+
## Example JSON-RPC exchanges
119+
120+
### Prove a trivial SMT goal with Z3
121+
122+
Request:
123+
```json
124+
{
125+
"jsonrpc": "2.0",
126+
"id": 1,
127+
"method": "tools/call",
128+
"params": {
129+
"name": "prove",
130+
"arguments": {
131+
"file": "/tmp/reflexivity.smt2",
132+
"prover": "Z3",
133+
"timeout_secs": 10
134+
}
135+
}
136+
}
137+
```
138+
139+
`/tmp/reflexivity.smt2`:
140+
```smt2
141+
(declare-const x Int)
142+
(assert (not (= x x)))
143+
(check-sat)
144+
```
145+
146+
Response:
147+
```json
148+
{
149+
"jsonrpc": "2.0",
150+
"id": 1,
151+
"result": {
152+
"content": [{
153+
"type": "text",
154+
"text": "{\n \"verified\": true,\n \"message\": \"unsat\",\n \"prover\": \"Z3\",\n \"raw_output\": \"unsat\\n\",\n \"stderr\": \"\"\n}"
155+
}]
156+
}
157+
}
158+
```
159+
160+
### Check that Z3 is installed
161+
162+
Request:
163+
```json
164+
{
165+
"jsonrpc": "2.0",
166+
"id": 2,
167+
"method": "tools/call",
168+
"params": {
169+
"name": "check_prover",
170+
"arguments": {
171+
"prover": "Z3"
172+
}
173+
}
174+
}
175+
```
176+
177+
Response:
178+
```json
179+
{
180+
"jsonrpc": "2.0",
181+
"id": 2,
182+
"result": {
183+
"content": [{
184+
"type": "text",
185+
"text": "{\n \"available\": true,\n \"message\": \"Z3 is available\"\n}"
186+
}]
187+
}
188+
}
189+
```
190+
191+
## Troubleshooting
192+
193+
**`Failed to invoke echidna: No such file or directory`**
194+
: The `echidna` binary is not on PATH. Either install it or set `ECHIDNA_BIN=/full/path/echidna` in the MCP server env config.
195+
196+
**`verified: false` with empty `message`**
197+
: The prover binary itself is missing. Confirm the target prover is installed: `which z3`, `which coqc`, etc. The `check_prover` tool also reports availability.
198+
199+
**Timeout / `verified: false` with partial output**
200+
: Increase `timeout_secs`. Complex goals in interactive provers (Isabelle, Coq) may need 60–600 s. Default is 300 s.
201+
202+
**Wrong prover selected**
203+
: Pass `"prover": "Z3"` (or whichever backend) explicitly rather than relying on auto-detection.
204+
205+
## License
206+
207+
PMPL-1.0-or-later — see [LICENSE](../../LICENSE).

crates/echidna-mcp/src/main.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use rmcp::{
2828
ServerHandler, ServiceExt,
2929
};
3030
use serde::{Deserialize, Serialize};
31+
use std::collections::HashMap;
3132
use tokio::process::Command;
3233

3334
fn echidna_bin() -> String {
@@ -63,6 +64,29 @@ struct ProveResult {
6364
stderr: String,
6465
}
6566

67+
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
68+
struct CheckProverParams {
69+
/// Prover backend name to check (case-sensitive). Examples: `Z3`, `Lean`,
70+
/// `Coq`, `Agda`, `Isabelle`, `Vampire`, `CVC5`, `Metamath`.
71+
pub prover: String,
72+
}
73+
74+
#[derive(Debug, Serialize)]
75+
struct CheckProverResult {
76+
/// Whether the prover binary was found and is executable.
77+
available: bool,
78+
/// Human-readable status message.
79+
message: String,
80+
}
81+
82+
#[derive(Debug, Serialize)]
83+
struct ListProversResult {
84+
/// Total number of supported prover backends.
85+
total: usize,
86+
/// Map from prover name to a brief description.
87+
provers: HashMap<String, String>,
88+
}
89+
6690
#[derive(Clone)]
6791
struct EchidnaMcp {
6892
tool_router: ToolRouter<Self>,
@@ -137,6 +161,96 @@ impl EchidnaMcp {
137161
};
138162
serde_json::to_string_pretty(&result).unwrap_or_default()
139163
}
164+
165+
/// Check whether a named prover backend is available on this system.
166+
#[tool(
167+
name = "check_prover",
168+
description = "Check whether a named prover backend is installed and \
169+
reachable. Returns {available: bool, message: string}. \
170+
Example: check_prover({\"prover\": \"Z3\"})."
171+
)]
172+
pub async fn check_prover(&self, params: Parameters<CheckProverParams>) -> String {
173+
let prover = params.0.prover;
174+
175+
let mut cmd = Command::new(echidna_bin());
176+
cmd.arg("check-prover").arg(&prover);
177+
178+
let result = match cmd.output().await {
179+
Ok(output) => {
180+
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
181+
let available = output.status.success()
182+
|| stdout.contains("available")
183+
|| stdout.contains("found");
184+
let message = if stdout.trim().is_empty() {
185+
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
186+
if stderr.trim().is_empty() {
187+
if available {
188+
format!("{prover} is available")
189+
} else {
190+
format!("{prover} is not available")
191+
}
192+
} else {
193+
stderr.lines().take(3).collect::<Vec<_>>().join("\n")
194+
}
195+
} else {
196+
stdout.trim().to_string()
197+
};
198+
CheckProverResult { available, message }
199+
}
200+
Err(e) => CheckProverResult {
201+
available: false,
202+
message: format!("Failed to invoke echidna: {e}"),
203+
},
204+
};
205+
206+
serde_json::to_string_pretty(&result).unwrap_or_default()
207+
}
208+
209+
/// List all 105 prover backends supported by ECHIDNA.
210+
#[tool(
211+
name = "list_provers",
212+
description = "List all 105 prover backends supported by ECHIDNA, \
213+
grouped by category. Returns {total: int, provers: {name: description}}."
214+
)]
215+
pub async fn list_provers(&self, _params: Parameters<()>) -> String {
216+
let mut cmd = Command::new(echidna_bin());
217+
cmd.arg("list-provers").arg("--format").arg("json");
218+
219+
match cmd.output().await {
220+
Ok(output) if output.status.success() => {
221+
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
222+
// Return the raw JSON from the CLI if it looks like JSON, else wrap it.
223+
if stdout.trim_start().starts_with('{') || stdout.trim_start().starts_with('[') {
224+
stdout
225+
} else {
226+
// Wrap plain-text listing as a simple result object.
227+
let lines: Vec<&str> = stdout.lines().filter(|l| !l.trim().is_empty()).collect();
228+
let provers: HashMap<String, String> = lines
229+
.iter()
230+
.map(|l| (l.trim().to_string(), String::new()))
231+
.collect();
232+
let result = ListProversResult {
233+
total: provers.len(),
234+
provers,
235+
};
236+
serde_json::to_string_pretty(&result).unwrap_or_default()
237+
}
238+
}
239+
Ok(output) => {
240+
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
241+
let result = serde_json::json!({
242+
"error": format!("echidna list-provers failed: {}", stderr.trim())
243+
});
244+
serde_json::to_string_pretty(&result).unwrap_or_default()
245+
}
246+
Err(e) => {
247+
let result = serde_json::json!({
248+
"error": format!("Failed to invoke echidna: {e}")
249+
});
250+
serde_json::to_string_pretty(&result).unwrap_or_default()
251+
}
252+
}
253+
}
140254
}
141255

142256
#[tool_handler(router = self.tool_router)]

examples/aspect_tagging_demo.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ fn main() {
104104
// were dead. Removed 2026-04-25. Re-add when a real neural tagger
105105
// ships (likely via the Julia /predict endpoint or src/rust/neural.rs).
106106
let composite = CompositeTagger::new(AggregationStrategy::WeightedVoting)
107-
.add_tagger(Box::new(RuleBasedTagger::new()), 1.0);
107+
.add_tagger(Box::new(RuleBasedTagger::new()), 1.0)
108+
.add_tagger(Box::new(RuleBasedTagger::with_threshold(0.3)), 0.5);
108109

109110
let theorem_name = "functor_category_theory";
110111
let statement = Term::Const("functor".to_string());

tests/common/generators.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,7 @@ pub fn arb_theorem() -> impl Strategy<Value = Theorem> {
112112

113113
/// Strategy for generating a variable
114114
pub fn arb_variable() -> impl Strategy<Value = Variable> {
115-
(var_name(), arb_term()).prop_map(|(name, ty)| Variable {
116-
name,
117-
ty,
118-
type_info: None,
119-
})
115+
(var_name(), arb_term()).prop_map(|(name, ty)| Variable { name, ty, type_info: None })
120116
}
121117

122118
/// Strategy for generating contexts

0 commit comments

Comments
 (0)