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.
- package/README.md +477 -0
- package/install.js +327 -0
- package/mcp/servers.json +100 -0
- package/mcp/tools/README.md +64 -0
- package/mcp/tools/__init__.py +1 -0
- package/mcp/tools/aderyn_runner.py +226 -0
- package/mcp/tools/eas_attest.py +404 -0
- package/mcp/tools/evidence_chain.py +363 -0
- package/mcp/tools/exploit_simulator.py +545 -0
- package/mcp/tools/fuzz_runner.py +440 -0
- package/mcp/tools/gev_analyzer.py +362 -0
- package/mcp/tools/halmos_runner.py +408 -0
- package/mcp/tools/incremental_auditor.py +441 -0
- package/mcp/tools/knowledge_base.py +378 -0
- package/mcp/tools/poc_generator.py +479 -0
- package/mcp/tools/protocol_scanner.py +456 -0
- package/mcp/tools/repair_validator.py +421 -0
- package/mcp/tools/slither_runner.py +221 -0
- package/package.json +52 -0
- package/requirements.txt +20 -0
- package/skills/glm-audit-skill/SKILL.md +73 -0
- package/skills/glm-audit-skill/references/audit-agents/access-control-agent.md +42 -0
- package/skills/glm-audit-skill/references/audit-agents/asymmetry-agent.md +42 -0
- package/skills/glm-audit-skill/references/audit-agents/boundary-agent.md +42 -0
- package/skills/glm-audit-skill/references/audit-agents/economic-security-agent.md +42 -0
- package/skills/glm-audit-skill/references/audit-agents/execution-trace-agent.md +42 -0
- package/skills/glm-audit-skill/references/audit-agents/first-principles-agent.md +42 -0
- package/skills/glm-audit-skill/references/audit-agents/flow-gap-agent.md +38 -0
- package/skills/glm-audit-skill/references/audit-agents/invariant-agent.md +37 -0
- package/skills/glm-audit-skill/references/audit-agents/math-precision-agent.md +37 -0
- package/skills/glm-audit-skill/references/audit-agents/numerical-gap-agent.md +37 -0
- package/skills/glm-audit-skill/references/audit-agents/periphery-agent.md +37 -0
- package/skills/glm-audit-skill/references/audit-agents/shared-rules.md +37 -0
- package/skills/glm-audit-skill/references/audit-agents/trust-gap-agent.md +39 -0
- package/skills/glm-audit-skill/references/judging.md +45 -0
- package/skills/glm-audit-skill/references/report-formatting.md +22 -0
- package/skills/glm-audit-skill/references/senior-auditor-sop.md +34 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MCP Server: Repair Validator
|
|
4
|
+
Multi-round repair verification: audit → fix → re-audit → compare.
|
|
5
|
+
"""
|
|
6
|
+
import sys
|
|
7
|
+
import json
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
12
|
+
logger = logging.getLogger("repair_validator")
|
|
13
|
+
|
|
14
|
+
TOOL_NAME = "repair_validator"
|
|
15
|
+
TOOL_VERSION = "1.0.0"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_tool_definitions() -> list:
|
|
19
|
+
return [
|
|
20
|
+
{
|
|
21
|
+
"name": "compare_audit_results",
|
|
22
|
+
"description": "Compare before/after audit results to measure repair effectiveness",
|
|
23
|
+
"inputSchema": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"properties": {
|
|
26
|
+
"before_findings": {
|
|
27
|
+
"type": "array",
|
|
28
|
+
"items": {"type": "object"},
|
|
29
|
+
"description": "Findings from initial audit"
|
|
30
|
+
},
|
|
31
|
+
"after_findings": {
|
|
32
|
+
"type": "array",
|
|
33
|
+
"items": {"type": "object"},
|
|
34
|
+
"description": "Findings from re-audit after fix"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"required": ["before_findings", "after_findings"]
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "generate_repair_report",
|
|
42
|
+
"description": "Generate a repair verification report",
|
|
43
|
+
"inputSchema": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"properties": {
|
|
46
|
+
"contract_name": {"type": "string"},
|
|
47
|
+
"before_findings": {
|
|
48
|
+
"type": "array",
|
|
49
|
+
"items": {"type": "object"}
|
|
50
|
+
},
|
|
51
|
+
"after_findings": {
|
|
52
|
+
"type": "array",
|
|
53
|
+
"items": {"type": "object"}
|
|
54
|
+
},
|
|
55
|
+
"fixes_applied": {
|
|
56
|
+
"type": "array",
|
|
57
|
+
"items": {"type": "string"},
|
|
58
|
+
"description": "List of fixes applied"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"required": ["contract_name", "before_findings", "after_findings"]
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "suggest_fixes",
|
|
66
|
+
"description": "Suggest fixes for audit findings",
|
|
67
|
+
"inputSchema": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"properties": {
|
|
70
|
+
"findings": {
|
|
71
|
+
"type": "array",
|
|
72
|
+
"items": {"type": "object"},
|
|
73
|
+
"description": "Audit findings to fix"
|
|
74
|
+
},
|
|
75
|
+
"contract_code": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"description": "Original contract code"
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"required": ["findings"]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def compare_audit_results(before_findings: list, after_findings: list) -> dict:
|
|
87
|
+
"""Compare before/after audit results."""
|
|
88
|
+
|
|
89
|
+
# Categorize findings
|
|
90
|
+
before_by_severity = _categorize_by_severity(before_findings)
|
|
91
|
+
after_by_severity = _categorize_by_severity(after_findings)
|
|
92
|
+
|
|
93
|
+
# Find fixed findings
|
|
94
|
+
fixed = []
|
|
95
|
+
for bf in before_findings:
|
|
96
|
+
found_in_after = False
|
|
97
|
+
for af in after_findings:
|
|
98
|
+
if _findings_match(bf, af):
|
|
99
|
+
found_in_after = True
|
|
100
|
+
break
|
|
101
|
+
if not found_in_after:
|
|
102
|
+
fixed.append(bf)
|
|
103
|
+
|
|
104
|
+
# Find new findings
|
|
105
|
+
new = []
|
|
106
|
+
for af in after_findings:
|
|
107
|
+
found_in_before = False
|
|
108
|
+
for bf in before_findings:
|
|
109
|
+
if _findings_match(bf, af):
|
|
110
|
+
found_in_before = True
|
|
111
|
+
break
|
|
112
|
+
if not found_in_before:
|
|
113
|
+
new.append(af)
|
|
114
|
+
|
|
115
|
+
# Find remaining findings
|
|
116
|
+
remaining = []
|
|
117
|
+
for af in after_findings:
|
|
118
|
+
for bf in before_findings:
|
|
119
|
+
if _findings_match(bf, af):
|
|
120
|
+
remaining.append(af)
|
|
121
|
+
break
|
|
122
|
+
|
|
123
|
+
# Calculate metrics
|
|
124
|
+
total_before = len(before_findings)
|
|
125
|
+
total_after = len(after_findings)
|
|
126
|
+
fixed_count = len(fixed)
|
|
127
|
+
new_count = len(new)
|
|
128
|
+
remaining_count = len(remaining)
|
|
129
|
+
|
|
130
|
+
# Calculate severity reduction
|
|
131
|
+
severity_reduction = {}
|
|
132
|
+
for sev in ["critical", "high", "medium", "low"]:
|
|
133
|
+
before_count = before_by_severity.get(sev, 0)
|
|
134
|
+
after_count = after_by_severity.get(sev, 0)
|
|
135
|
+
severity_reduction[sev] = {
|
|
136
|
+
"before": before_count,
|
|
137
|
+
"after": after_count,
|
|
138
|
+
"reduced": before_count - after_count
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Calculate effectiveness score (0-100)
|
|
142
|
+
if total_before == 0:
|
|
143
|
+
effectiveness = 100
|
|
144
|
+
else:
|
|
145
|
+
effectiveness = int((fixed_count / total_before) * 100)
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
"success": True,
|
|
149
|
+
"summary": {
|
|
150
|
+
"total_before": total_before,
|
|
151
|
+
"total_after": total_after,
|
|
152
|
+
"fixed": fixed_count,
|
|
153
|
+
"new": new_count,
|
|
154
|
+
"remaining": remaining_count,
|
|
155
|
+
"effectiveness": effectiveness
|
|
156
|
+
},
|
|
157
|
+
"severity_reduction": severity_reduction,
|
|
158
|
+
"fixed_findings": fixed,
|
|
159
|
+
"new_findings": new,
|
|
160
|
+
"remaining_findings": remaining,
|
|
161
|
+
"verdict": _get_verdict(effectiveness, new_count)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _categorize_by_severity(findings: list) -> dict:
|
|
166
|
+
"""Categorize findings by severity."""
|
|
167
|
+
categories = {}
|
|
168
|
+
for f in findings:
|
|
169
|
+
sev = f.get("severity", "unknown").lower()
|
|
170
|
+
categories[sev] = categories.get(sev, 0) + 1
|
|
171
|
+
return categories
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _findings_match(f1: dict, f2: dict) -> bool:
|
|
175
|
+
"""Check if two findings match (same vulnerability)."""
|
|
176
|
+
# Match by contract + function + type
|
|
177
|
+
return (
|
|
178
|
+
f1.get("contract") == f2.get("contract") and
|
|
179
|
+
f1.get("function") == f2.get("function") and
|
|
180
|
+
f1.get("type") == f2.get("type")
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _get_verdict(effectiveness: int, new_findings: int) -> str:
|
|
185
|
+
"""Get overall verdict."""
|
|
186
|
+
if effectiveness >= 90 and new_findings == 0:
|
|
187
|
+
return "EXCELLENT - Nearly all issues fixed, no new issues"
|
|
188
|
+
elif effectiveness >= 70 and new_findings == 0:
|
|
189
|
+
return "GOOD - Most issues fixed, no new issues"
|
|
190
|
+
elif effectiveness >= 50:
|
|
191
|
+
return "FAIR - Some issues fixed"
|
|
192
|
+
elif new_findings > 0:
|
|
193
|
+
return "CONCERNING - New issues introduced"
|
|
194
|
+
else:
|
|
195
|
+
return "POOR - Few issues fixed"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
async def generate_repair_report(contract_name: str, before_findings: list, after_findings: list, fixes_applied: list = None) -> dict:
|
|
199
|
+
"""Generate a repair verification report."""
|
|
200
|
+
|
|
201
|
+
comparison = await compare_audit_results(before_findings, after_findings)
|
|
202
|
+
|
|
203
|
+
report = f"""# Repair Verification Report
|
|
204
|
+
|
|
205
|
+
## Contract: {contract_name}
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Summary
|
|
210
|
+
|
|
211
|
+
| Metric | Value |
|
|
212
|
+
|--------|-------|
|
|
213
|
+
| Findings Before | {comparison['summary']['total_before']} |
|
|
214
|
+
| Findings After | {comparison['summary']['total_after']} |
|
|
215
|
+
| Fixed | {comparison['summary']['fixed']} |
|
|
216
|
+
| New | {comparison['summary']['new']} |
|
|
217
|
+
| Remaining | {comparison['summary']['remaining']} |
|
|
218
|
+
| Effectiveness | {comparison['summary']['effectiveness']}% |
|
|
219
|
+
| Verdict | {comparison['verdict']} |
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Severity Reduction
|
|
224
|
+
|
|
225
|
+
| Severity | Before | After | Reduced |
|
|
226
|
+
|----------|--------|-------|---------|
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
for sev, data in comparison['severity_reduction'].items():
|
|
230
|
+
report += f"| {sev.capitalize()} | {data['before']} | {data['after']} | {data['reduced']} |\n"
|
|
231
|
+
|
|
232
|
+
if fixes_applied:
|
|
233
|
+
report += "\n---\n\n## Fixes Applied\n\n"
|
|
234
|
+
for i, fix in enumerate(fixes_applied, 1):
|
|
235
|
+
report += f"{i}. {fix}\n"
|
|
236
|
+
|
|
237
|
+
if comparison['fixed_findings']:
|
|
238
|
+
report += "\n---\n\n## Fixed Findings\n\n"
|
|
239
|
+
for f in comparison['fixed_findings']:
|
|
240
|
+
report += f"- **{f.get('type', 'unknown')}** in {f.get('contract', '?')}.{f.get('function', '?')}\n"
|
|
241
|
+
|
|
242
|
+
if comparison['new_findings']:
|
|
243
|
+
report += "\n---\n\n## New Findings (Introduced by Fix)\n\n"
|
|
244
|
+
for f in comparison['new_findings']:
|
|
245
|
+
report += f"- **{f.get('type', 'unknown')}** in {f.get('contract', '?')}.{f.get('function', '?')}\n"
|
|
246
|
+
|
|
247
|
+
if comparison['remaining_findings']:
|
|
248
|
+
report += "\n---\n\n## Remaining Findings\n\n"
|
|
249
|
+
for f in comparison['remaining_findings']:
|
|
250
|
+
report += f"- **{f.get('type', 'unknown')}** in {f.get('contract', '?')}.{f.get('function', '?')}\n"
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
"success": True,
|
|
254
|
+
"report": report,
|
|
255
|
+
"comparison": comparison
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
async def suggest_fixes(findings: list, contract_code: str = None) -> dict:
|
|
260
|
+
"""Suggest fixes for audit findings."""
|
|
261
|
+
suggestions = []
|
|
262
|
+
|
|
263
|
+
for finding in findings:
|
|
264
|
+
finding_type = finding.get("type", "").lower()
|
|
265
|
+
severity = finding.get("severity", "unknown")
|
|
266
|
+
|
|
267
|
+
suggestion = {
|
|
268
|
+
"finding": finding,
|
|
269
|
+
"fixes": []
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
# Reentrancy fix
|
|
273
|
+
if "reentrancy" in finding_type:
|
|
274
|
+
suggestion["fixes"].append({
|
|
275
|
+
"approach": "checks-effects-interactions",
|
|
276
|
+
"description": "Update state before external call",
|
|
277
|
+
"diff": "- (bool success, ) = msg.sender.call{value: balance}(\"\");\n- balances[msg.sender] = 0;\n+ balances[msg.sender] = 0;\n+ (bool success, ) = msg.sender.call{value: balance}(\"\");"
|
|
278
|
+
})
|
|
279
|
+
suggestion["fixes"].append({
|
|
280
|
+
"approach": "reentrancy-guard",
|
|
281
|
+
"description": "Use OpenZeppelin ReentrancyGuard",
|
|
282
|
+
"diff": "+ import \"@openzeppelin/contracts/security/ReentrancyGuard.sol\";\n+ contract Vault is ReentrancyGuard {\n function withdraw() public nonReentrant {"
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
# Access control fix
|
|
286
|
+
elif "access_control" in finding_type or "missing_access" in finding_type:
|
|
287
|
+
suggestion["fixes"].append({
|
|
288
|
+
"approach": "onlyOwner-modifier",
|
|
289
|
+
"description": "Add onlyOwner modifier",
|
|
290
|
+
"diff": "+ modifier onlyOwner() {\n+ require(msg.sender == owner, \"Not owner\");\n+ _;\n+ }\n function adminFunction() public onlyOwner {"
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
# Oracle manipulation fix
|
|
294
|
+
elif "oracle" in finding_type:
|
|
295
|
+
suggestion["fixes"].append({
|
|
296
|
+
"approach": "twap-oracle",
|
|
297
|
+
"description": "Use TWAP instead of spot price",
|
|
298
|
+
"diff": "- uint256 price = oracle.getPrice(token);\n+ uint256 price = oracle.getTWAPPrice(token, 3600); // 1 hour TWAP"
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
# Flash loan fix
|
|
302
|
+
elif "flash_loan" in finding_type:
|
|
303
|
+
suggestion["fixes"].append({
|
|
304
|
+
"approach": "timelock",
|
|
305
|
+
"description": "Add timelock for governance actions",
|
|
306
|
+
"diff": "+ require(block.timestamp >= proposal.timestamp + TIMELOCK_DELAY, \"Timelock not expired\");"
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
# Overflow fix
|
|
310
|
+
elif "overflow" in finding_type:
|
|
311
|
+
suggestion["fixes"].append({
|
|
312
|
+
"approach": "checked-arithmetic",
|
|
313
|
+
"description": "Use Solidity 0.8+ checked arithmetic",
|
|
314
|
+
"diff": "- unchecked { total += amount; }\n+ total += amount; // Solidity 0.8+ checks automatically"
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
# Generic fix
|
|
318
|
+
else:
|
|
319
|
+
suggestion["fixes"].append({
|
|
320
|
+
"approach": "review-and-fix",
|
|
321
|
+
"description": "Manual review required",
|
|
322
|
+
"diff": "See finding details for specific fix recommendations"
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
suggestions.append(suggestion)
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
"success": True,
|
|
329
|
+
"suggestions": suggestions,
|
|
330
|
+
"total_suggestions": len(suggestions)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
async def execute_tool(tool_name: str, arguments: dict) -> dict:
|
|
335
|
+
if tool_name == "compare_audit_results":
|
|
336
|
+
return await compare_audit_results(
|
|
337
|
+
arguments.get("before_findings", []),
|
|
338
|
+
arguments.get("after_findings", [])
|
|
339
|
+
)
|
|
340
|
+
elif tool_name == "generate_repair_report":
|
|
341
|
+
return await generate_repair_report(
|
|
342
|
+
arguments.get("contract_name", ""),
|
|
343
|
+
arguments.get("before_findings", []),
|
|
344
|
+
arguments.get("after_findings", []),
|
|
345
|
+
arguments.get("fixes_applied")
|
|
346
|
+
)
|
|
347
|
+
elif tool_name == "suggest_fixes":
|
|
348
|
+
return await suggest_fixes(
|
|
349
|
+
arguments.get("findings", []),
|
|
350
|
+
arguments.get("contract_code")
|
|
351
|
+
)
|
|
352
|
+
return {"success": False, "error": f"Unknown tool: {tool_name}"}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
async def handle_request(request: dict) -> dict:
|
|
356
|
+
method = request.get("method")
|
|
357
|
+
params = request.get("params", {})
|
|
358
|
+
|
|
359
|
+
if method == "initialize":
|
|
360
|
+
return {
|
|
361
|
+
"protocolVersion": "2024-11-05",
|
|
362
|
+
"capabilities": {"tools": {}},
|
|
363
|
+
"serverInfo": {"name": TOOL_NAME, "version": TOOL_VERSION}
|
|
364
|
+
}
|
|
365
|
+
elif method == "tools/list":
|
|
366
|
+
return {"tools": build_tool_definitions()}
|
|
367
|
+
elif method == "tools/call":
|
|
368
|
+
tool_name = params.get("name")
|
|
369
|
+
arguments = params.get("arguments", {})
|
|
370
|
+
result = await execute_tool(tool_name, arguments)
|
|
371
|
+
return {"content": [{"type": "text", "text": json.dumps(result, indent=2)}]}
|
|
372
|
+
elif method == "ping":
|
|
373
|
+
return {}
|
|
374
|
+
return {"error": {"code": -32601, "message": f"Method not found: {method}"}}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
async def main():
|
|
378
|
+
reader = asyncio.StreamReader()
|
|
379
|
+
protocol = asyncio.StreamReaderProtocol(reader)
|
|
380
|
+
loop = asyncio.get_event_loop()
|
|
381
|
+
await loop.connect_read_pipe(lambda: protocol, sys.stdin)
|
|
382
|
+
|
|
383
|
+
logger.info(f"{TOOL_NAME} MCP server started")
|
|
384
|
+
|
|
385
|
+
while True:
|
|
386
|
+
line = await reader.readline()
|
|
387
|
+
if not line:
|
|
388
|
+
break
|
|
389
|
+
|
|
390
|
+
line_str = line.decode("utf-8").strip()
|
|
391
|
+
if not line_str:
|
|
392
|
+
continue
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
request = json.loads(line_str)
|
|
396
|
+
response = await handle_request(request)
|
|
397
|
+
response["jsonrpc"] = "2.0"
|
|
398
|
+
response["id"] = request.get("id")
|
|
399
|
+
sys.stdout.write(json.dumps(response) + "\n")
|
|
400
|
+
sys.stdout.flush()
|
|
401
|
+
except json.JSONDecodeError:
|
|
402
|
+
error_resp = {
|
|
403
|
+
"jsonrpc": "2.0",
|
|
404
|
+
"id": None,
|
|
405
|
+
"error": {"code": -32700, "message": "Parse error"}
|
|
406
|
+
}
|
|
407
|
+
sys.stdout.write(json.dumps(error_resp) + "\n")
|
|
408
|
+
sys.stdout.flush()
|
|
409
|
+
except Exception as e:
|
|
410
|
+
logger.exception("Error handling request")
|
|
411
|
+
error_resp = {
|
|
412
|
+
"jsonrpc": "2.0",
|
|
413
|
+
"id": request.get("id") if "request" in dir() else None,
|
|
414
|
+
"error": {"code": -32603, "message": f"Internal error: {str(e)}"}
|
|
415
|
+
}
|
|
416
|
+
sys.stdout.write(json.dumps(error_resp) + "\n")
|
|
417
|
+
sys.stdout.flush()
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
if __name__ == "__main__":
|
|
421
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MCP Server: Slither Runner
|
|
4
|
+
Wraps Slither static analysis tool for Solidity smart contracts.
|
|
5
|
+
"""
|
|
6
|
+
import sys
|
|
7
|
+
import json
|
|
8
|
+
import asyncio
|
|
9
|
+
import tempfile
|
|
10
|
+
import subprocess
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
16
|
+
logger = logging.getLogger("slither_runner")
|
|
17
|
+
|
|
18
|
+
TOOL_NAME = "slither_runner"
|
|
19
|
+
TOOL_VERSION = "1.0.0"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_tool_definitions() -> list:
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
"name": "slither_analyze",
|
|
26
|
+
"description": "Run Slither static analysis on a Solidity smart contract. Returns detected vulnerabilities with impact, confidence, and location details.",
|
|
27
|
+
"inputSchema": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"contract_path": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Path to the Solidity contract file to analyze"
|
|
33
|
+
},
|
|
34
|
+
"detectors": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": {"type": "string"},
|
|
37
|
+
"description": "Optional list of specific Slither detector names to run (e.g., ['reentrancy-eth', 'unchecked-transfer']). If omitted, all detectors run."
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"required": ["contract_path"]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def run_slither(contract_path: str, detectors: list = None) -> dict:
|
|
47
|
+
"""Execute Slither and parse JSON output."""
|
|
48
|
+
contract_path = os.path.abspath(contract_path)
|
|
49
|
+
|
|
50
|
+
if not os.path.isfile(contract_path):
|
|
51
|
+
return {"success": False, "error": f"Contract file not found: {contract_path}"}
|
|
52
|
+
|
|
53
|
+
# Create a temp directory and point at a file inside it (slither refuses to overwrite)
|
|
54
|
+
tmp_dir = tempfile.mkdtemp(prefix="slither_")
|
|
55
|
+
json_output_path = os.path.join(tmp_dir, "slither.json")
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
cmd = ["slither", contract_path, "--json", json_output_path]
|
|
59
|
+
|
|
60
|
+
# Add specific detectors if provided
|
|
61
|
+
if detectors:
|
|
62
|
+
for detector in detectors:
|
|
63
|
+
cmd.extend(["--detect", detector])
|
|
64
|
+
|
|
65
|
+
logger.info(f"Running slither: {' '.join(cmd)}")
|
|
66
|
+
|
|
67
|
+
proc = await asyncio.create_subprocess_exec(
|
|
68
|
+
*cmd,
|
|
69
|
+
stdout=asyncio.subprocess.PIPE,
|
|
70
|
+
stderr=asyncio.subprocess.PIPE
|
|
71
|
+
)
|
|
72
|
+
stdout, stderr = await proc.communicate()
|
|
73
|
+
|
|
74
|
+
# Slither returns various non-zero codes when it finds issues
|
|
75
|
+
# 0 = no issues, 1 = issues found, 2 = error, 255 = issues found (newer versions)
|
|
76
|
+
if proc.returncode not in (0, 1, 2, 255):
|
|
77
|
+
return {
|
|
78
|
+
"success": False,
|
|
79
|
+
"error": f"Slither exited with code {proc.returncode}",
|
|
80
|
+
"stderr": stderr.decode("utf-8", errors="replace").strip()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Parse JSON output
|
|
84
|
+
if not os.path.isfile(json_output_path):
|
|
85
|
+
return {
|
|
86
|
+
"success": False,
|
|
87
|
+
"error": "Slither did not produce JSON output",
|
|
88
|
+
"stderr": stderr.decode("utf-8", errors="replace").strip()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
with open(json_output_path, "r") as f:
|
|
92
|
+
raw_output = json.load(f)
|
|
93
|
+
|
|
94
|
+
# Extract and normalize findings
|
|
95
|
+
findings = []
|
|
96
|
+
for detector_result in raw_output.get("results", {}).get("detectors", []):
|
|
97
|
+
finding = {
|
|
98
|
+
"check": detector_result.get("check", "unknown"),
|
|
99
|
+
"impact": detector_result.get("impact", "Unknown"),
|
|
100
|
+
"confidence": detector_result.get("confidence", "Unknown"),
|
|
101
|
+
"description": detector_result.get("description", ""),
|
|
102
|
+
"elements": []
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# Extract source locations
|
|
106
|
+
for element in detector_result.get("elements", []):
|
|
107
|
+
source = element.get("source_mapping", {})
|
|
108
|
+
elem_info = {
|
|
109
|
+
"type": element.get("type", ""),
|
|
110
|
+
"name": element.get("name", ""),
|
|
111
|
+
"file": source.get("filename_relative", ""),
|
|
112
|
+
"lines": source.get("lines", []),
|
|
113
|
+
"start": source.get("start", 0),
|
|
114
|
+
"length": source.get("length", 0)
|
|
115
|
+
}
|
|
116
|
+
finding["elements"].append(elem_info)
|
|
117
|
+
|
|
118
|
+
findings.append(finding)
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
"success": True,
|
|
122
|
+
"contract": contract_path,
|
|
123
|
+
"findings_count": len(findings),
|
|
124
|
+
"findings": findings,
|
|
125
|
+
"slither_version": raw_output.get("meta", {}).get("slither_version", "unknown"),
|
|
126
|
+
"compiler": raw_output.get("meta", {}).get("solc_version", "unknown")
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
except FileNotFoundError:
|
|
130
|
+
return {"success": False, "error": "Slither not found. Install with: pip3 install slither-analyzer"}
|
|
131
|
+
except json.JSONDecodeError as e:
|
|
132
|
+
return {"success": False, "error": f"Failed to parse Slither JSON output: {str(e)}"}
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.exception("Unexpected error in slither_runner")
|
|
135
|
+
return {"success": False, "error": f"Unexpected error: {str(e)}"}
|
|
136
|
+
finally:
|
|
137
|
+
# Cleanup temp directory
|
|
138
|
+
import shutil
|
|
139
|
+
if os.path.exists(json_output_path):
|
|
140
|
+
os.unlink(json_output_path)
|
|
141
|
+
try:
|
|
142
|
+
os.rmdir(tmp_dir)
|
|
143
|
+
except OSError:
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
async def execute_tool(tool_name: str, arguments: dict) -> dict:
|
|
148
|
+
if tool_name == "slither_analyze":
|
|
149
|
+
contract_path = arguments.get("contract_path", "")
|
|
150
|
+
detectors = arguments.get("detectors", None)
|
|
151
|
+
return await run_slither(contract_path, detectors)
|
|
152
|
+
return {"success": False, "error": f"Unknown tool: {tool_name}"}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async def handle_request(request: dict) -> dict:
|
|
156
|
+
method = request.get("method")
|
|
157
|
+
params = request.get("params", {})
|
|
158
|
+
|
|
159
|
+
if method == "initialize":
|
|
160
|
+
return {
|
|
161
|
+
"protocolVersion": "2024-11-05",
|
|
162
|
+
"capabilities": {"tools": {}},
|
|
163
|
+
"serverInfo": {"name": TOOL_NAME, "version": TOOL_VERSION}
|
|
164
|
+
}
|
|
165
|
+
elif method == "tools/list":
|
|
166
|
+
return {"tools": build_tool_definitions()}
|
|
167
|
+
elif method == "tools/call":
|
|
168
|
+
tool_name = params.get("name")
|
|
169
|
+
arguments = params.get("arguments", {})
|
|
170
|
+
result = await execute_tool(tool_name, arguments)
|
|
171
|
+
return {"content": [{"type": "text", "text": json.dumps(result, indent=2)}]}
|
|
172
|
+
elif method == "ping":
|
|
173
|
+
return {}
|
|
174
|
+
return {"error": {"code": -32601, "message": f"Method not found: {method}"}}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def main():
|
|
178
|
+
reader = asyncio.StreamReader()
|
|
179
|
+
protocol = asyncio.StreamReaderProtocol(reader)
|
|
180
|
+
loop = asyncio.get_event_loop()
|
|
181
|
+
await loop.connect_read_pipe(lambda: protocol, sys.stdin)
|
|
182
|
+
|
|
183
|
+
logger.info(f"{TOOL_NAME} MCP server started")
|
|
184
|
+
|
|
185
|
+
while True:
|
|
186
|
+
line = await reader.readline()
|
|
187
|
+
if not line:
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
line_str = line.decode("utf-8").strip()
|
|
191
|
+
if not line_str:
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
request = json.loads(line_str)
|
|
196
|
+
response = await handle_request(request)
|
|
197
|
+
response["jsonrpc"] = "2.0"
|
|
198
|
+
response["id"] = request.get("id")
|
|
199
|
+
sys.stdout.write(json.dumps(response) + "\n")
|
|
200
|
+
sys.stdout.flush()
|
|
201
|
+
except json.JSONDecodeError:
|
|
202
|
+
error_resp = {
|
|
203
|
+
"jsonrpc": "2.0",
|
|
204
|
+
"id": None,
|
|
205
|
+
"error": {"code": -32700, "message": "Parse error"}
|
|
206
|
+
}
|
|
207
|
+
sys.stdout.write(json.dumps(error_resp) + "\n")
|
|
208
|
+
sys.stdout.flush()
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.exception("Error handling request")
|
|
211
|
+
error_resp = {
|
|
212
|
+
"jsonrpc": "2.0",
|
|
213
|
+
"id": request.get("id") if "request" in dir() else None,
|
|
214
|
+
"error": {"code": -32603, "message": f"Internal error: {str(e)}"}
|
|
215
|
+
}
|
|
216
|
+
sys.stdout.write(json.dumps(error_resp) + "\n")
|
|
217
|
+
sys.stdout.flush()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
asyncio.run(main())
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "athena-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Athena — Web3 smart contract security audit MCP tools + Skills for Claude Code",
|
|
5
|
+
"main": "install.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"athena-mcp": "install.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"install-tools": "node install.js",
|
|
11
|
+
"postinstall": "echo 'Run npx athena-mcp install to set up Athena tools'"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mcp",
|
|
15
|
+
"smart-contract",
|
|
16
|
+
"security-audit",
|
|
17
|
+
"solidity",
|
|
18
|
+
"web3",
|
|
19
|
+
"claude",
|
|
20
|
+
"slither",
|
|
21
|
+
"aderyn",
|
|
22
|
+
"foundry",
|
|
23
|
+
"eas",
|
|
24
|
+
"ethereum"
|
|
25
|
+
],
|
|
26
|
+
"author": "tiyadegure",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/tiyadegure/Athena.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/tiyadegure/Athena/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/tiyadegure/Athena#readme",
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=16.0.0"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"install.js",
|
|
41
|
+
"package.json",
|
|
42
|
+
"README.md",
|
|
43
|
+
"mcp/",
|
|
44
|
+
"skills/",
|
|
45
|
+
"requirements.txt"
|
|
46
|
+
],
|
|
47
|
+
"preferGlobal": true,
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public",
|
|
50
|
+
"registry": "https://registry.npmjs.org/"
|
|
51
|
+
}
|
|
52
|
+
}
|