athena-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +477 -0
  2. package/install.js +327 -0
  3. package/mcp/servers.json +100 -0
  4. package/mcp/tools/README.md +64 -0
  5. package/mcp/tools/__init__.py +1 -0
  6. package/mcp/tools/aderyn_runner.py +226 -0
  7. package/mcp/tools/eas_attest.py +404 -0
  8. package/mcp/tools/evidence_chain.py +363 -0
  9. package/mcp/tools/exploit_simulator.py +545 -0
  10. package/mcp/tools/fuzz_runner.py +440 -0
  11. package/mcp/tools/gev_analyzer.py +362 -0
  12. package/mcp/tools/halmos_runner.py +408 -0
  13. package/mcp/tools/incremental_auditor.py +441 -0
  14. package/mcp/tools/knowledge_base.py +378 -0
  15. package/mcp/tools/poc_generator.py +479 -0
  16. package/mcp/tools/protocol_scanner.py +456 -0
  17. package/mcp/tools/repair_validator.py +421 -0
  18. package/mcp/tools/slither_runner.py +221 -0
  19. package/package.json +52 -0
  20. package/requirements.txt +20 -0
  21. package/skills/glm-audit-skill/SKILL.md +73 -0
  22. package/skills/glm-audit-skill/references/audit-agents/access-control-agent.md +42 -0
  23. package/skills/glm-audit-skill/references/audit-agents/asymmetry-agent.md +42 -0
  24. package/skills/glm-audit-skill/references/audit-agents/boundary-agent.md +42 -0
  25. package/skills/glm-audit-skill/references/audit-agents/economic-security-agent.md +42 -0
  26. package/skills/glm-audit-skill/references/audit-agents/execution-trace-agent.md +42 -0
  27. package/skills/glm-audit-skill/references/audit-agents/first-principles-agent.md +42 -0
  28. package/skills/glm-audit-skill/references/audit-agents/flow-gap-agent.md +38 -0
  29. package/skills/glm-audit-skill/references/audit-agents/invariant-agent.md +37 -0
  30. package/skills/glm-audit-skill/references/audit-agents/math-precision-agent.md +37 -0
  31. package/skills/glm-audit-skill/references/audit-agents/numerical-gap-agent.md +37 -0
  32. package/skills/glm-audit-skill/references/audit-agents/periphery-agent.md +37 -0
  33. package/skills/glm-audit-skill/references/audit-agents/shared-rules.md +37 -0
  34. package/skills/glm-audit-skill/references/audit-agents/trust-gap-agent.md +39 -0
  35. package/skills/glm-audit-skill/references/judging.md +45 -0
  36. package/skills/glm-audit-skill/references/report-formatting.md +22 -0
  37. package/skills/glm-audit-skill/references/senior-auditor-sop.md +34 -0
@@ -0,0 +1,440 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP Server: Fuzz Runner
4
+ Runs Foundry fuzz tests and extracts A1 signals (profitability, execution trace, revert reason).
5
+ """
6
+ import sys
7
+ import json
8
+ import asyncio
9
+ import tempfile
10
+ import subprocess
11
+ import logging
12
+ import os
13
+ import re
14
+ import shutil
15
+
16
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
17
+ logger = logging.getLogger("fuzz_runner")
18
+
19
+ TOOL_NAME = "fuzz_runner"
20
+ TOOL_VERSION = "1.0.0"
21
+
22
+ FOUNDRY_TOML_TEMPLATE = """[profile.default]
23
+ src = "src"
24
+ out = "out"
25
+ libs = ["lib"]
26
+ solc = "/usr/local/bin/solc"
27
+ optimizer = true
28
+ optimizer_runs = 200
29
+
30
+ [profile.default.fuzz]
31
+ runs = 256
32
+ max_test_rejects = 65536
33
+ """
34
+
35
+ TEST_FILE_TEMPLATE = """// SPDX-License-Identifier: MIT
36
+ pragma solidity ^0.8.20;
37
+
38
+ import "forge-std/Test.sol";
39
+
40
+ {source_contract}
41
+
42
+ {test_contract}
43
+ """
44
+
45
+
46
+ def build_tool_definitions() -> list:
47
+ return [
48
+ {
49
+ "name": "run_fuzz_test",
50
+ "description": "Run Foundry fuzz tests on a smart contract. Creates a temporary Foundry project, compiles, and executes fuzz tests. Returns execution traces, revert reasons, and profitability analysis.",
51
+ "inputSchema": {
52
+ "type": "object",
53
+ "properties": {
54
+ "contract_code": {
55
+ "type": "string",
56
+ "description": "The Solidity source code of the contract to test"
57
+ },
58
+ "test_code": {
59
+ "type": "string",
60
+ "description": "The Foundry test contract code (must inherit Test and have test_ functions)"
61
+ },
62
+ "timeout": {
63
+ "type": "integer",
64
+ "description": "Timeout in seconds for the fuzz test (default: 300)",
65
+ "default": 300
66
+ }
67
+ },
68
+ "required": ["contract_code", "test_code"]
69
+ }
70
+ }
71
+ ]
72
+
73
+
74
+ async def run_foundry_fuzz(contract_code: str, test_code: str, timeout: int = 300) -> dict:
75
+ """Create a temp Foundry project, run fuzz tests, parse results."""
76
+ tmp_dir = tempfile.mkdtemp(prefix="fuzz_")
77
+
78
+ try:
79
+ # Initialize Foundry project
80
+ src_dir = os.path.join(tmp_dir, "src")
81
+ lib_dir = os.path.join(tmp_dir, "lib")
82
+ test_dir = os.path.join(tmp_dir, "test")
83
+ os.makedirs(src_dir)
84
+ os.makedirs(lib_dir)
85
+ os.makedirs(test_dir)
86
+
87
+ # Write foundry.toml
88
+ with open(os.path.join(tmp_dir, "foundry.toml"), "w") as f:
89
+ f.write(FOUNDRY_TOML_TEMPLATE)
90
+
91
+ # Install forge-std: try local cache first, then forge install
92
+ logger.info("Setting up forge-std...")
93
+ forge_std_src = os.path.join(lib_dir, "forge-std", "src")
94
+ os.makedirs(forge_std_src, exist_ok=True)
95
+
96
+ # Create minimal Test.sol stub (no network required)
97
+ with open(os.path.join(forge_std_src, "Test.sol"), "w") as f:
98
+ f.write('''// SPDX-License-Identifier: MIT
99
+ pragma solidity ^0.8.20;
100
+
101
+ abstract contract Test {
102
+ event log(string);
103
+ event log_named_uint(string key, uint val);
104
+ event log_named_address(string key, address val);
105
+ event log_named_bytes32(string key, bytes32 val);
106
+ event log_named_string(string key, string val);
107
+
108
+ function assertEq(uint a, uint b) internal {
109
+ if (a != b) {
110
+ revert(string(abi.encodePacked("assertEq failed: ", uint2str(a), " != ", uint2str(b))));
111
+ }
112
+ }
113
+ function assertEq(address a, address b) internal {
114
+ if (a != b) revert("assertEq failed: addresses not equal");
115
+ }
116
+ function assertTrue(bool b) internal {
117
+ if (!b) revert("assertTrue failed");
118
+ }
119
+ function assertFalse(bool b) internal {
120
+ if (b) revert("assertFalse failed");
121
+ }
122
+ function assertGt(uint a, uint b) internal {
123
+ if (a <= b) revert("assertGt failed");
124
+ }
125
+ function assertLt(uint a, uint b) internal {
126
+ if (a >= b) revert("assertLt failed");
127
+ }
128
+ function assertGe(uint a, uint b) internal {
129
+ if (a < b) revert("assertGe failed");
130
+ }
131
+ function assertLe(uint a, uint b) internal {
132
+ if (a > b) revert("assertLe failed");
133
+ }
134
+ function fail() internal pure {
135
+ revert("fail()");
136
+ }
137
+ function uint2str(uint _i) internal pure returns (string memory _uintAsString) {
138
+ if (_i == 0) return "0";
139
+ uint j = _i;
140
+ uint len;
141
+ while (j != 0) { len++; j /= 10; }
142
+ bytes memory bstr = new bytes(len);
143
+ uint k = len;
144
+ while (_i != 0) { k = k - 1; uint8 temp = (48 + uint8(_i - (_i / 10) * 10)); bytes1 b1 = bytes1(temp); bstr[k] = b1; _i /= 10; }
145
+ return string(bstr);
146
+ }
147
+ }
148
+ ''')
149
+ logger.info("Using local forge-std stub (no network required)")
150
+
151
+ # Write source contract
152
+ source_path = os.path.join(src_dir, "Target.sol")
153
+ with open(source_path, "w") as f:
154
+ f.write(contract_code)
155
+
156
+ # Write test contract
157
+ test_path = os.path.join(test_dir, "FuzzTest.t.sol")
158
+ with open(test_path, "w") as f:
159
+ f.write(test_code)
160
+
161
+ # Compile
162
+ logger.info("Compiling contracts...")
163
+ proc = await asyncio.create_subprocess_exec(
164
+ "forge", "build", "--force",
165
+ cwd=tmp_dir,
166
+ stdout=asyncio.subprocess.PIPE,
167
+ stderr=asyncio.subprocess.PIPE
168
+ )
169
+ stdout, stderr = await proc.communicate()
170
+
171
+ if proc.returncode != 0:
172
+ stderr_text = stderr.decode("utf-8", errors="replace")
173
+ return {
174
+ "success": False,
175
+ "error": "Compilation failed",
176
+ "compile_error": stderr_text,
177
+ "stdout": stdout.decode("utf-8", errors="replace")
178
+ }
179
+
180
+ # Run fuzz tests with maximum verbosity
181
+ logger.info(f"Running fuzz tests (timeout: {timeout}s)...")
182
+ proc = await asyncio.create_subprocess_exec(
183
+ "forge", "test", "--json", "-vvvvv", "--fuzz-runs", "256",
184
+ cwd=tmp_dir,
185
+ stdout=asyncio.subprocess.PIPE,
186
+ stderr=asyncio.subprocess.PIPE
187
+ )
188
+
189
+ try:
190
+ stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
191
+ except asyncio.TimeoutError:
192
+ proc.kill()
193
+ return {"success": False, "error": f"Fuzz test timed out after {timeout}s"}
194
+
195
+ stdout_text = stdout.decode("utf-8", errors="replace")
196
+ stderr_text = stderr.decode("utf-8", errors="replace")
197
+
198
+ # Parse JSON output
199
+ test_results = parse_test_output(stdout_text)
200
+
201
+ # Extract A1 signals
202
+ a1_signals = extract_a1_signals(test_results, stderr_text)
203
+
204
+ return {
205
+ "success": True,
206
+ "exit_code": proc.returncode,
207
+ "tests_passed": proc.returncode == 0,
208
+ "test_results": test_results,
209
+ "a1_signals": a1_signals,
210
+ "stderr": stderr_text[:2000] if stderr_text else None
211
+ }
212
+
213
+ except Exception as e:
214
+ logger.exception("Error in fuzz runner")
215
+ return {"success": False, "error": f"Unexpected error: {str(e)}"}
216
+ finally:
217
+ shutil.rmtree(tmp_dir, ignore_errors=True)
218
+
219
+
220
+ def parse_test_output(stdout_text: str) -> list:
221
+ """Parse forge test JSON output."""
222
+ results = []
223
+
224
+ try:
225
+ # forge test --json outputs one JSON object per line
226
+ for line in stdout_text.strip().split("\n"):
227
+ line = line.strip()
228
+ if not line:
229
+ continue
230
+ try:
231
+ data = json.loads(line)
232
+ test_name = data.get("test", data.get("name", ""))
233
+
234
+ # Extract contract and function name
235
+ parts = test_name.split("::")
236
+ contract = parts[0] if len(parts) > 0 else ""
237
+ function = parts[1] if len(parts) > 1 else test_name
238
+
239
+ result = {
240
+ "test_name": test_name,
241
+ "contract": contract,
242
+ "function": function,
243
+ "status": "PASS" if data.get("result", {}).get("status") == "Success" else "FAIL",
244
+ "reason": data.get("result", {}).get("reason", None),
245
+ "duration": data.get("result", {}).get("duration", None),
246
+ "decoded_logs": data.get("decoded_logs", []),
247
+ "trace": data.get("result", {}).get("traces", [])
248
+ }
249
+ results.append(result)
250
+ except json.JSONDecodeError:
251
+ continue
252
+
253
+ # Fallback: parse non-JSON output
254
+ if not results:
255
+ for line in stdout_text.split("\n"):
256
+ if "[PASS]" in line:
257
+ results.append({"test_name": line.strip(), "status": "PASS"})
258
+ elif "[FAIL]" in line:
259
+ results.append({"test_name": line.strip(), "status": "FAIL", "reason": line.strip()})
260
+
261
+ except Exception as e:
262
+ logger.warning(f"Error parsing test output: {e}")
263
+
264
+ return results
265
+
266
+
267
+ def extract_a1_signals(test_results: list, stderr_text: str) -> dict:
268
+ """Extract A1 analysis signals from fuzz test results."""
269
+ signals = {
270
+ "profitability": None,
271
+ "execution_traces": [],
272
+ "revert_reasons": [],
273
+ "fuzz_statistics": {},
274
+ "summary": ""
275
+ }
276
+
277
+ failed_tests = [r for r in test_results if r.get("status") == "FAIL"]
278
+
279
+ # Signal 1: Profitability analysis
280
+ # Look for balance changes, ETH transfers, or value-related assertions
281
+ profitability_hints = []
282
+ for result in test_results:
283
+ logs = result.get("decoded_logs", [])
284
+ for log in logs:
285
+ if any(kw in log.lower() for kw in ["balance", "profit", "stolen", "eth", "wei"]):
286
+ profitability_hints.append(log)
287
+
288
+ if profitability_hints:
289
+ signals["profitability"] = {
290
+ "detected": True,
291
+ "hints": profitability_hints
292
+ }
293
+ else:
294
+ signals["profitability"] = {"detected": False, "hints": []}
295
+
296
+ # Signal 2: Execution traces
297
+ for result in test_results:
298
+ if result.get("trace"):
299
+ signals["execution_traces"].append({
300
+ "test": result["test_name"],
301
+ "trace_summary": summarize_trace(result["trace"])
302
+ })
303
+
304
+ # Signal 3: Revert reasons
305
+ for result in failed_tests:
306
+ reason = result.get("reason")
307
+ if reason:
308
+ signals["revert_reasons"].append({
309
+ "test": result["test_name"],
310
+ "reason": reason
311
+ })
312
+
313
+ # Fuzz statistics from stderr
314
+ fuzz_stats = parse_fuzz_statistics(stderr_text)
315
+ if fuzz_stats:
316
+ signals["fuzz_statistics"] = fuzz_stats
317
+
318
+ # Summary
319
+ total = len(test_results)
320
+ passed = sum(1 for r in test_results if r.get("status") == "PASS")
321
+ failed = total - passed
322
+ signals["summary"] = f"{total} tests: {passed} passed, {failed} failed"
323
+
324
+ return signals
325
+
326
+
327
+ def summarize_trace(trace_data) -> str:
328
+ """Summarize execution trace into readable format."""
329
+ if isinstance(trace_data, str):
330
+ return trace_data[:500]
331
+ if isinstance(trace_data, list):
332
+ return f"{len(trace_data)} trace entries"
333
+ return str(trace_data)[:500]
334
+
335
+
336
+ def parse_fuzz_statistics(stderr_text: str) -> dict:
337
+ """Extract fuzz run statistics from stderr."""
338
+ stats = {}
339
+
340
+ # Pattern: "Fuzz runs: 256"
341
+ runs_match = re.search(r'fuzz runs[:\s]+(\d+)', stderr_text, re.IGNORECASE)
342
+ if runs_match:
343
+ stats["runs"] = int(runs_match.group(1))
344
+
345
+ # Pattern: "Seed: 12345"
346
+ seed_match = re.search(r'seed[:\s]+(\d+)', stderr_text, re.IGNORECASE)
347
+ if seed_match:
348
+ stats["seed"] = int(seed_match.group(1))
349
+
350
+ # Pattern: "Max test rejects: 65536"
351
+ rejects_match = re.search(r'max test rejects[:\s]+(\d+)', stderr_text, re.IGNORECASE)
352
+ if rejects_match:
353
+ stats["max_rejects"] = int(rejects_match.group(1))
354
+
355
+ return stats
356
+
357
+
358
+ async def execute_tool(tool_name: str, arguments: dict) -> dict:
359
+ if tool_name == "run_fuzz_test":
360
+ contract_code = arguments.get("contract_code", "")
361
+ test_code = arguments.get("test_code", "")
362
+ timeout = arguments.get("timeout", 300)
363
+
364
+ if not contract_code:
365
+ return {"success": False, "error": "contract_code is required"}
366
+ if not test_code:
367
+ return {"success": False, "error": "test_code is required"}
368
+
369
+ return await run_foundry_fuzz(contract_code, test_code, timeout)
370
+
371
+ return {"success": False, "error": f"Unknown tool: {tool_name}"}
372
+
373
+
374
+ async def handle_request(request: dict) -> dict:
375
+ method = request.get("method")
376
+ params = request.get("params", {})
377
+
378
+ if method == "initialize":
379
+ return {
380
+ "protocolVersion": "2024-11-05",
381
+ "capabilities": {"tools": {}},
382
+ "serverInfo": {"name": TOOL_NAME, "version": TOOL_VERSION}
383
+ }
384
+ elif method == "tools/list":
385
+ return {"tools": build_tool_definitions()}
386
+ elif method == "tools/call":
387
+ tool_name = params.get("name")
388
+ arguments = params.get("arguments", {})
389
+ result = await execute_tool(tool_name, arguments)
390
+ return {"content": [{"type": "text", "text": json.dumps(result, indent=2)}]}
391
+ elif method == "ping":
392
+ return {}
393
+ return {"error": {"code": -32601, "message": f"Method not found: {method}"}}
394
+
395
+
396
+ async def main():
397
+ reader = asyncio.StreamReader()
398
+ protocol = asyncio.StreamReaderProtocol(reader)
399
+ loop = asyncio.get_event_loop()
400
+ await loop.connect_read_pipe(lambda: protocol, sys.stdin)
401
+
402
+ logger.info(f"{TOOL_NAME} MCP server started")
403
+
404
+ while True:
405
+ line = await reader.readline()
406
+ if not line:
407
+ break
408
+
409
+ line_str = line.decode("utf-8").strip()
410
+ if not line_str:
411
+ continue
412
+
413
+ try:
414
+ request = json.loads(line_str)
415
+ response = await handle_request(request)
416
+ response["jsonrpc"] = "2.0"
417
+ response["id"] = request.get("id")
418
+ sys.stdout.write(json.dumps(response) + "\n")
419
+ sys.stdout.flush()
420
+ except json.JSONDecodeError:
421
+ error_resp = {
422
+ "jsonrpc": "2.0",
423
+ "id": None,
424
+ "error": {"code": -32700, "message": "Parse error"}
425
+ }
426
+ sys.stdout.write(json.dumps(error_resp) + "\n")
427
+ sys.stdout.flush()
428
+ except Exception as e:
429
+ logger.exception("Error handling request")
430
+ error_resp = {
431
+ "jsonrpc": "2.0",
432
+ "id": request.get("id") if "request" in dir() else None,
433
+ "error": {"code": -32603, "message": f"Internal error: {str(e)}"}
434
+ }
435
+ sys.stdout.write(json.dumps(error_resp) + "\n")
436
+ sys.stdout.flush()
437
+
438
+
439
+ if __name__ == "__main__":
440
+ asyncio.run(main())