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,441 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MCP Server: Incremental Auditor
|
|
4
|
+
Detects code changes and audits only the diff, reusing previous audit context.
|
|
5
|
+
Demonstrates GLM-5.1's 200K context window for long-horizon tasks.
|
|
6
|
+
"""
|
|
7
|
+
import sys
|
|
8
|
+
import json
|
|
9
|
+
import asyncio
|
|
10
|
+
import hashlib
|
|
11
|
+
import logging
|
|
12
|
+
import difflib
|
|
13
|
+
|
|
14
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
15
|
+
logger = logging.getLogger("incremental_auditor")
|
|
16
|
+
|
|
17
|
+
TOOL_NAME = "incremental_auditor"
|
|
18
|
+
TOOL_VERSION = "1.0.0"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_tool_definitions() -> list:
|
|
22
|
+
return [
|
|
23
|
+
{
|
|
24
|
+
"name": "detect_changes",
|
|
25
|
+
"description": "Detect changes between old and new contract versions",
|
|
26
|
+
"inputSchema": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"properties": {
|
|
29
|
+
"old_code": {"type": "string", "description": "Original contract code"},
|
|
30
|
+
"new_code": {"type": "string", "description": "Updated contract code"},
|
|
31
|
+
"contract_name": {"type": "string", "description": "Contract name"}
|
|
32
|
+
},
|
|
33
|
+
"required": ["old_code", "new_code"]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "generate_incremental_report",
|
|
38
|
+
"description": "Generate incremental audit report based on changes",
|
|
39
|
+
"inputSchema": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"properties": {
|
|
42
|
+
"contract_name": {"type": "string"},
|
|
43
|
+
"old_code": {"type": "string"},
|
|
44
|
+
"new_code": {"type": "string"},
|
|
45
|
+
"previous_findings": {
|
|
46
|
+
"type": "array",
|
|
47
|
+
"items": {"type": "object"},
|
|
48
|
+
"description": "Findings from previous audit"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"required": ["contract_name", "old_code", "new_code"]
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "build_audit_context",
|
|
56
|
+
"description": "Build audit context for 200K context window utilization",
|
|
57
|
+
"inputSchema": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"properties": {
|
|
60
|
+
"contracts": {
|
|
61
|
+
"type": "array",
|
|
62
|
+
"items": {"type": "object"},
|
|
63
|
+
"description": "All contracts in the protocol"
|
|
64
|
+
},
|
|
65
|
+
"previous_audits": {
|
|
66
|
+
"type": "array",
|
|
67
|
+
"items": {"type": "object"},
|
|
68
|
+
"description": "Previous audit results"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"required": ["contracts"]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def detect_changes(old_code: str, new_code: str, contract_name: str = None) -> dict:
|
|
78
|
+
"""Detect changes between old and new contract versions."""
|
|
79
|
+
|
|
80
|
+
old_lines = old_code.splitlines()
|
|
81
|
+
new_lines = new_code.splitlines()
|
|
82
|
+
|
|
83
|
+
# Generate diff
|
|
84
|
+
diff = list(difflib.unified_diff(old_lines, new_lines, lineterm='', n=3))
|
|
85
|
+
|
|
86
|
+
# Analyze changes
|
|
87
|
+
added_lines = []
|
|
88
|
+
removed_lines = []
|
|
89
|
+
changed_functions = []
|
|
90
|
+
|
|
91
|
+
for line in diff:
|
|
92
|
+
if line.startswith('+') and not line.startswith('+++'):
|
|
93
|
+
added_lines.append(line[1:])
|
|
94
|
+
elif line.startswith('-') and not line.startswith('---'):
|
|
95
|
+
removed_lines.append(line[1:])
|
|
96
|
+
|
|
97
|
+
# Detect changed functions
|
|
98
|
+
old_functions = _extract_functions(old_code)
|
|
99
|
+
new_functions = _extract_functions(new_code)
|
|
100
|
+
|
|
101
|
+
for func_name in set(old_functions.keys()) | set(new_functions.keys()):
|
|
102
|
+
old_func = old_functions.get(func_name)
|
|
103
|
+
new_func = new_functions.get(func_name)
|
|
104
|
+
|
|
105
|
+
if old_func and new_func:
|
|
106
|
+
if old_func != new_func:
|
|
107
|
+
changed_functions.append({
|
|
108
|
+
"name": func_name,
|
|
109
|
+
"change_type": "modified",
|
|
110
|
+
"old_hash": hashlib.sha256(old_func.encode()).hexdigest()[:16],
|
|
111
|
+
"new_hash": hashlib.sha256(new_func.encode()).hexdigest()[:16]
|
|
112
|
+
})
|
|
113
|
+
elif new_func and not old_func:
|
|
114
|
+
changed_functions.append({
|
|
115
|
+
"name": func_name,
|
|
116
|
+
"change_type": "added"
|
|
117
|
+
})
|
|
118
|
+
elif old_func and not new_func:
|
|
119
|
+
changed_functions.append({
|
|
120
|
+
"name": func_name,
|
|
121
|
+
"change_type": "removed"
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
# Calculate change magnitude
|
|
125
|
+
total_lines = max(len(old_lines), len(new_lines))
|
|
126
|
+
changed_lines = len(added_lines) + len(removed_lines)
|
|
127
|
+
change_percentage = (changed_lines / total_lines * 100) if total_lines > 0 else 0
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
"success": True,
|
|
131
|
+
"contract_name": contract_name,
|
|
132
|
+
"summary": {
|
|
133
|
+
"old_lines": len(old_lines),
|
|
134
|
+
"new_lines": len(new_lines),
|
|
135
|
+
"added_lines": len(added_lines),
|
|
136
|
+
"removed_lines": len(removed_lines),
|
|
137
|
+
"change_percentage": round(change_percentage, 2)
|
|
138
|
+
},
|
|
139
|
+
"changed_functions": changed_functions,
|
|
140
|
+
"diff": diff[:100], # First 100 lines of diff
|
|
141
|
+
"affected_areas": _identify_affected_areas(changed_functions)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _extract_functions(code: str) -> dict:
|
|
146
|
+
"""Extract function bodies from code."""
|
|
147
|
+
functions = {}
|
|
148
|
+
|
|
149
|
+
import re
|
|
150
|
+
pattern = r'function\s+(\w+)\s*\([^)]*\)[^{]*\{'
|
|
151
|
+
|
|
152
|
+
for match in re.finditer(pattern, code):
|
|
153
|
+
func_name = match.group(1)
|
|
154
|
+
start_pos = match.end()
|
|
155
|
+
|
|
156
|
+
# Find function end (simplified)
|
|
157
|
+
brace_count = 1
|
|
158
|
+
pos = start_pos
|
|
159
|
+
while pos < len(code) and brace_count > 0:
|
|
160
|
+
if code[pos] == '{':
|
|
161
|
+
brace_count += 1
|
|
162
|
+
elif code[pos] == '}':
|
|
163
|
+
brace_count -= 1
|
|
164
|
+
pos += 1
|
|
165
|
+
|
|
166
|
+
functions[func_name] = code[match.start():pos]
|
|
167
|
+
|
|
168
|
+
return functions
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _identify_affected_areas(changed_functions: list) -> list:
|
|
172
|
+
"""Identify which areas of the contract are affected."""
|
|
173
|
+
areas = set()
|
|
174
|
+
|
|
175
|
+
for func in changed_functions:
|
|
176
|
+
name = func["name"].lower()
|
|
177
|
+
|
|
178
|
+
if "deposit" in name or "withdraw" in name or "transfer" in name:
|
|
179
|
+
areas.add("token_operations")
|
|
180
|
+
if "borrow" in name or "repay" in name or "liquidat" in name:
|
|
181
|
+
areas.add("lending")
|
|
182
|
+
if "swap" in name or "addliquidity" in name or "removeliquidity" in name:
|
|
183
|
+
areas.add("amm")
|
|
184
|
+
if "vote" in name or "propose" in name or "execute" in name:
|
|
185
|
+
areas.add("governance")
|
|
186
|
+
if "oracle" in name or "price" in name:
|
|
187
|
+
areas.add("oracle")
|
|
188
|
+
if "admin" in name or "owner" in name or "set" in name:
|
|
189
|
+
areas.add("admin")
|
|
190
|
+
|
|
191
|
+
return list(areas)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async def generate_incremental_report(contract_name: str, old_code: str, new_code: str, previous_findings: list = None) -> dict:
|
|
195
|
+
"""Generate incremental audit report."""
|
|
196
|
+
|
|
197
|
+
changes = await detect_changes(old_code, new_code, contract_name)
|
|
198
|
+
|
|
199
|
+
if previous_findings is None:
|
|
200
|
+
previous_findings = []
|
|
201
|
+
|
|
202
|
+
# Determine what needs re-auditing
|
|
203
|
+
affected_areas = changes.get("affected_areas", [])
|
|
204
|
+
changed_functions = changes.get("changed_functions", [])
|
|
205
|
+
|
|
206
|
+
# Filter previous findings that might be affected
|
|
207
|
+
potentially_affected = []
|
|
208
|
+
for finding in previous_findings:
|
|
209
|
+
finding_func = finding.get("function", "").lower()
|
|
210
|
+
for func in changed_functions:
|
|
211
|
+
if func["name"].lower() in finding_func or finding_func in func["name"].lower():
|
|
212
|
+
potentially_affected.append(finding)
|
|
213
|
+
break
|
|
214
|
+
|
|
215
|
+
# Generate report
|
|
216
|
+
report = f"""# Incremental Audit Report
|
|
217
|
+
|
|
218
|
+
## Contract: {contract_name}
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Change Summary
|
|
223
|
+
|
|
224
|
+
| Metric | Value |
|
|
225
|
+
|--------|-------|
|
|
226
|
+
| Old Lines | {changes['summary']['old_lines']} |
|
|
227
|
+
| New Lines | {changes['summary']['new_lines']} |
|
|
228
|
+
| Added Lines | {changes['summary']['added_lines']} |
|
|
229
|
+
| Removed Lines | {changes['summary']['removed_lines']} |
|
|
230
|
+
| Change % | {changes['summary']['change_percentage']}% |
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Changed Functions
|
|
235
|
+
|
|
236
|
+
| Function | Change Type |
|
|
237
|
+
|----------|-------------|
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
for func in changed_functions:
|
|
241
|
+
report += f"| {func['name']} | {func['change_type']} |\n"
|
|
242
|
+
|
|
243
|
+
report += f"""
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Affected Areas
|
|
247
|
+
|
|
248
|
+
{', '.join(affected_areas) if affected_areas else 'None identified'}
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Previous Findings Affected
|
|
253
|
+
|
|
254
|
+
{len(potentially_affected)} findings may be affected by changes:
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
for finding in potentially_affected:
|
|
258
|
+
report += f"- {finding.get('type', 'unknown')} in {finding.get('function', '?')}\n"
|
|
259
|
+
|
|
260
|
+
report += """
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Recommended Actions
|
|
264
|
+
|
|
265
|
+
1. Re-audit changed functions
|
|
266
|
+
2. Verify fixes don't introduce new issues
|
|
267
|
+
3. Run fuzz tests on affected areas
|
|
268
|
+
4. Update audit report with incremental findings
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
"success": True,
|
|
273
|
+
"report": report,
|
|
274
|
+
"changes": changes,
|
|
275
|
+
"affected_findings": potentially_affected,
|
|
276
|
+
"requires_reaudit": len(changed_functions) > 0
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
async def build_audit_context(contracts: list, previous_audits: list = None) -> dict:
|
|
281
|
+
"""Build audit context for 200K context window."""
|
|
282
|
+
|
|
283
|
+
if previous_audits is None:
|
|
284
|
+
previous_audits = []
|
|
285
|
+
|
|
286
|
+
# Build context structure
|
|
287
|
+
context = {
|
|
288
|
+
"protocol_overview": {
|
|
289
|
+
"total_contracts": len(contracts),
|
|
290
|
+
"contract_names": [c.get("name", "unknown") for c in contracts]
|
|
291
|
+
},
|
|
292
|
+
"contract_details": [],
|
|
293
|
+
"audit_history": previous_audits,
|
|
294
|
+
"known_vulnerabilities": [],
|
|
295
|
+
"architecture_notes": []
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# Add contract details
|
|
299
|
+
total_lines = 0
|
|
300
|
+
for contract in contracts:
|
|
301
|
+
code = contract.get("code", "")
|
|
302
|
+
lines = code.count('\n') + 1
|
|
303
|
+
total_lines += lines
|
|
304
|
+
|
|
305
|
+
context["contract_details"].append({
|
|
306
|
+
"name": contract.get("name", "unknown"),
|
|
307
|
+
"lines": lines,
|
|
308
|
+
"functions": len(contract.get("functions", [])),
|
|
309
|
+
"imports": contract.get("imports", [])
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
# Estimate context usage
|
|
313
|
+
# Rough estimate: 1 token ≈ 4 characters
|
|
314
|
+
total_chars = sum(len(c.get("code", "")) for c in contracts)
|
|
315
|
+
estimated_tokens = total_chars // 4
|
|
316
|
+
|
|
317
|
+
context["context_stats"] = {
|
|
318
|
+
"total_lines": total_lines,
|
|
319
|
+
"estimated_tokens": estimated_tokens,
|
|
320
|
+
"context_window": 200000,
|
|
321
|
+
"utilization": f"{(estimated_tokens / 200000 * 100):.1f}%"
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
# Add known vulnerabilities from previous audits
|
|
325
|
+
for audit in previous_audits:
|
|
326
|
+
for finding in audit.get("findings", []):
|
|
327
|
+
context["known_vulnerabilities"].append({
|
|
328
|
+
"contract": finding.get("contract"),
|
|
329
|
+
"type": finding.get("type"),
|
|
330
|
+
"severity": finding.get("severity"),
|
|
331
|
+
"status": finding.get("status", "open")
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
"success": True,
|
|
336
|
+
"context": context,
|
|
337
|
+
"recommendation": _get_context_recommendation(estimated_tokens)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _get_context_recommendation(estimated_tokens: int) -> str:
|
|
342
|
+
"""Get recommendation based on context utilization."""
|
|
343
|
+
if estimated_tokens < 50000:
|
|
344
|
+
return "Full protocol audit in single context - ideal"
|
|
345
|
+
elif estimated_tokens < 100000:
|
|
346
|
+
return "Protocol fits in context with room for analysis"
|
|
347
|
+
elif estimated_tokens < 150000:
|
|
348
|
+
return "Protocol requires most of context - prioritize critical contracts"
|
|
349
|
+
else:
|
|
350
|
+
return "Protocol exceeds comfortable context - use incremental auditing"
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
async def execute_tool(tool_name: str, arguments: dict) -> dict:
|
|
354
|
+
if tool_name == "detect_changes":
|
|
355
|
+
return await detect_changes(
|
|
356
|
+
arguments.get("old_code", ""),
|
|
357
|
+
arguments.get("new_code", ""),
|
|
358
|
+
arguments.get("contract_name")
|
|
359
|
+
)
|
|
360
|
+
elif tool_name == "generate_incremental_report":
|
|
361
|
+
return await generate_incremental_report(
|
|
362
|
+
arguments.get("contract_name", ""),
|
|
363
|
+
arguments.get("old_code", ""),
|
|
364
|
+
arguments.get("new_code", ""),
|
|
365
|
+
arguments.get("previous_findings")
|
|
366
|
+
)
|
|
367
|
+
elif tool_name == "build_audit_context":
|
|
368
|
+
return await build_audit_context(
|
|
369
|
+
arguments.get("contracts", []),
|
|
370
|
+
arguments.get("previous_audits")
|
|
371
|
+
)
|
|
372
|
+
return {"success": False, "error": f"Unknown tool: {tool_name}"}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
async def handle_request(request: dict) -> dict:
|
|
376
|
+
method = request.get("method")
|
|
377
|
+
params = request.get("params", {})
|
|
378
|
+
|
|
379
|
+
if method == "initialize":
|
|
380
|
+
return {
|
|
381
|
+
"protocolVersion": "2024-11-05",
|
|
382
|
+
"capabilities": {"tools": {}},
|
|
383
|
+
"serverInfo": {"name": TOOL_NAME, "version": TOOL_VERSION}
|
|
384
|
+
}
|
|
385
|
+
elif method == "tools/list":
|
|
386
|
+
return {"tools": build_tool_definitions()}
|
|
387
|
+
elif method == "tools/call":
|
|
388
|
+
tool_name = params.get("name")
|
|
389
|
+
arguments = params.get("arguments", {})
|
|
390
|
+
result = await execute_tool(tool_name, arguments)
|
|
391
|
+
return {"content": [{"type": "text", "text": json.dumps(result, indent=2)}]}
|
|
392
|
+
elif method == "ping":
|
|
393
|
+
return {}
|
|
394
|
+
return {"error": {"code": -32601, "message": f"Method not found: {method}"}}
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
async def main():
|
|
398
|
+
reader = asyncio.StreamReader()
|
|
399
|
+
protocol = asyncio.StreamReaderProtocol(reader)
|
|
400
|
+
loop = asyncio.get_event_loop()
|
|
401
|
+
await loop.connect_read_pipe(lambda: protocol, sys.stdin)
|
|
402
|
+
|
|
403
|
+
logger.info(f"{TOOL_NAME} MCP server started")
|
|
404
|
+
|
|
405
|
+
while True:
|
|
406
|
+
line = await reader.readline()
|
|
407
|
+
if not line:
|
|
408
|
+
break
|
|
409
|
+
|
|
410
|
+
line_str = line.decode("utf-8").strip()
|
|
411
|
+
if not line_str:
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
request = json.loads(line_str)
|
|
416
|
+
response = await handle_request(request)
|
|
417
|
+
response["jsonrpc"] = "2.0"
|
|
418
|
+
response["id"] = request.get("id")
|
|
419
|
+
sys.stdout.write(json.dumps(response) + "\n")
|
|
420
|
+
sys.stdout.flush()
|
|
421
|
+
except json.JSONDecodeError:
|
|
422
|
+
error_resp = {
|
|
423
|
+
"jsonrpc": "2.0",
|
|
424
|
+
"id": None,
|
|
425
|
+
"error": {"code": -32700, "message": "Parse error"}
|
|
426
|
+
}
|
|
427
|
+
sys.stdout.write(json.dumps(error_resp) + "\n")
|
|
428
|
+
sys.stdout.flush()
|
|
429
|
+
except Exception as e:
|
|
430
|
+
logger.exception("Error handling request")
|
|
431
|
+
error_resp = {
|
|
432
|
+
"jsonrpc": "2.0",
|
|
433
|
+
"id": request.get("id") if "request" in dir() else None,
|
|
434
|
+
"error": {"code": -32603, "message": f"Internal error: {str(e)}"}
|
|
435
|
+
}
|
|
436
|
+
sys.stdout.write(json.dumps(error_resp) + "\n")
|
|
437
|
+
sys.stdout.flush()
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
if __name__ == "__main__":
|
|
441
|
+
asyncio.run(main())
|