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,456 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP Server: Protocol Scanner
4
+ Auto-discovers all Solidity files in a directory, analyzes call graphs,
5
+ and identifies cross-contract attack surfaces.
6
+ """
7
+ import sys
8
+ import json
9
+ import asyncio
10
+ import os
11
+ import re
12
+ import logging
13
+
14
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
15
+ logger = logging.getLogger("protocol_scanner")
16
+
17
+ TOOL_NAME = "protocol_scanner"
18
+ TOOL_VERSION = "1.0.0"
19
+
20
+
21
+ def build_tool_definitions() -> list:
22
+ return [
23
+ {
24
+ "name": "scan_protocol",
25
+ "description": "Scan a directory of Solidity files to discover contracts and analyze their relationships",
26
+ "inputSchema": {
27
+ "type": "object",
28
+ "properties": {
29
+ "directory": {
30
+ "type": "string",
31
+ "description": "Path to directory containing .sol files"
32
+ },
33
+ "exclude_patterns": {
34
+ "type": "array",
35
+ "items": {"type": "string"},
36
+ "description": "Patterns to exclude (e.g., ['test/', 'mock/'])",
37
+ "default": ["test/", "mock/", "node_modules/"]
38
+ }
39
+ },
40
+ "required": ["directory"]
41
+ }
42
+ },
43
+ {
44
+ "name": "analyze_call_graph",
45
+ "description": "Analyze contract call relationships and identify attack surfaces",
46
+ "inputSchema": {
47
+ "type": "object",
48
+ "properties": {
49
+ "contracts": {
50
+ "type": "array",
51
+ "items": {"type": "object"},
52
+ "description": "Contract data from scan_protocol"
53
+ }
54
+ },
55
+ "required": ["contracts"]
56
+ }
57
+ },
58
+ {
59
+ "name": "find_cross_contract_vulnerabilities",
60
+ "description": "Identify potential cross-contract vulnerabilities",
61
+ "inputSchema": {
62
+ "type": "object",
63
+ "properties": {
64
+ "contracts": {
65
+ "type": "array",
66
+ "items": {"type": "object"},
67
+ "description": "Contract data from scan_protocol"
68
+ },
69
+ "call_graph": {
70
+ "type": "object",
71
+ "description": "Call graph from analyze_call_graph"
72
+ }
73
+ },
74
+ "required": ["contracts"]
75
+ }
76
+ }
77
+ ]
78
+
79
+
80
+ async def scan_protocol(directory: str, exclude_patterns: list = None) -> dict:
81
+ """Scan directory for Solidity files and analyze contracts."""
82
+ if exclude_patterns is None:
83
+ exclude_patterns = ["test/", "mock/", "node_modules/", ".git/"]
84
+
85
+ directory = os.path.abspath(directory)
86
+
87
+ if not os.path.isdir(directory):
88
+ return {"success": False, "error": f"Directory not found: {directory}"}
89
+
90
+ # Find all .sol files
91
+ sol_files = []
92
+ for root, dirs, files in os.walk(directory):
93
+ # Skip excluded directories
94
+ skip = False
95
+ for pattern in exclude_patterns:
96
+ if pattern in root:
97
+ skip = True
98
+ break
99
+ if skip:
100
+ continue
101
+
102
+ for file in files:
103
+ if file.endswith(".sol"):
104
+ sol_files.append(os.path.join(root, file))
105
+
106
+ # Analyze each file
107
+ contracts = []
108
+ for sol_file in sol_files:
109
+ try:
110
+ with open(sol_file, 'r') as f:
111
+ content = f.read()
112
+
113
+ # Extract contract info
114
+ file_contracts = _extract_contracts(content, sol_file)
115
+ contracts.extend(file_contracts)
116
+ except Exception as e:
117
+ logger.warning(f"Failed to read {sol_file}: {e}")
118
+
119
+ return {
120
+ "success": True,
121
+ "directory": directory,
122
+ "total_files": len(sol_files),
123
+ "total_contracts": len(contracts),
124
+ "contracts": contracts,
125
+ "files": [os.path.relpath(f, directory) for f in sol_files]
126
+ }
127
+
128
+
129
+ def _extract_contracts(content: str, filepath: str) -> list:
130
+ """Extract contract information from Solidity source."""
131
+ contracts = []
132
+
133
+ # Find contract declarations
134
+ contract_pattern = r'contract\s+(\w+)(?:\s+is\s+([^{]+))?\s*\{'
135
+ for match in re.finditer(contract_pattern, content):
136
+ name = match.group(1)
137
+ inherits = match.group(2).strip() if match.group(2) else ""
138
+ inherit_list = [i.strip() for i in inherits.split(",")] if inherits else []
139
+
140
+ # Find contract body (simplified - doesn't handle nested braces perfectly)
141
+ start_pos = match.end()
142
+ brace_count = 1
143
+ pos = start_pos
144
+ while pos < len(content) and brace_count > 0:
145
+ if content[pos] == '{':
146
+ brace_count += 1
147
+ elif content[pos] == '}':
148
+ brace_count -= 1
149
+ pos += 1
150
+ body = content[start_pos:pos-1]
151
+
152
+ # Extract functions
153
+ functions = _extract_functions(body)
154
+
155
+ # Extract imports
156
+ imports = re.findall(r'import\s+.*?from\s+["\'](.+?)["\']', content)
157
+ import_pattern = r'import\s+["\'](.+?)["\']'
158
+ imports.extend(re.findall(import_pattern, content))
159
+
160
+ # Extract state variables
161
+ state_vars = _extract_state_variables(body)
162
+
163
+ # Check for external calls
164
+ external_calls = re.findall(r'\.call\{|\.call\.value\(|\.delegatecall\(|\.staticcall\(', body)
165
+
166
+ # Check for modifiers
167
+ modifiers = re.findall(r'modifier\s+(\w+)', body)
168
+
169
+ contracts.append({
170
+ "name": name,
171
+ "file": filepath,
172
+ "inherits": inherit_list,
173
+ "functions": functions,
174
+ "state_variables": state_vars,
175
+ "external_calls": len(external_calls),
176
+ "modifiers": modifiers,
177
+ "imports": imports
178
+ })
179
+
180
+ return contracts
181
+
182
+
183
+ def _extract_functions(body: str) -> list:
184
+ """Extract function information from contract body."""
185
+ functions = []
186
+
187
+ # Function pattern
188
+ func_pattern = r'function\s+(\w+)\s*\(([^)]*)\)(?:\s+(?:public|private|internal|external|view|pure|payable|nonpayable|virtual|override))*\s*(?:returns\s*\(([^)]*)\))?\s*\{?'
189
+
190
+ for match in re.finditer(func_pattern, body):
191
+ name = match.group(1)
192
+ params = match.group(2)
193
+ returns = match.group(3) or ""
194
+
195
+ # Determine visibility
196
+ func_text = match.group(0)
197
+ visibility = "internal"
198
+ if "public" in func_text:
199
+ visibility = "public"
200
+ elif "external" in func_text:
201
+ visibility = "external"
202
+ elif "private" in func_text:
203
+ visibility = "private"
204
+
205
+ # Check for payable
206
+ payable = "payable" in func_text
207
+
208
+ # Check for modifiers (simplified)
209
+ modifiers = []
210
+
211
+ functions.append({
212
+ "name": name,
213
+ "visibility": visibility,
214
+ "payable": payable,
215
+ "params": params.strip(),
216
+ "returns": returns.strip(),
217
+ "modifiers": modifiers
218
+ })
219
+
220
+ return functions
221
+
222
+
223
+ def _extract_state_variables(body: str) -> list:
224
+ """Extract state variable declarations."""
225
+ variables = []
226
+
227
+ # Simple pattern for state variables
228
+ var_pattern = r'(?:uint256|uint|address|bool|string|bytes32|mapping)\s+(?:public|private|internal)?\s*(\w+)'
229
+
230
+ for match in re.finditer(var_pattern, body):
231
+ variables.append(match.group(1))
232
+
233
+ return variables
234
+
235
+
236
+ async def analyze_call_graph(contracts: list) -> dict:
237
+ """Analyze call relationships between contracts."""
238
+ # Build contract name to info mapping
239
+ contract_map = {c["name"]: c for c in contracts}
240
+
241
+ # Build call graph
242
+ call_graph = {}
243
+ cross_calls = []
244
+
245
+ for contract in contracts:
246
+ contract_calls = []
247
+
248
+ # Check for calls to other contracts
249
+ for func in contract.get("functions", []):
250
+ # Look for external contract calls in function params/returns
251
+ params = func.get("params", "")
252
+ for other_name in contract_map:
253
+ if other_name != contract["name"]:
254
+ if other_name.lower() in params.lower():
255
+ contract_calls.append({
256
+ "caller": contract["name"],
257
+ "callee": other_name,
258
+ "function": func["name"],
259
+ "type": "parameter"
260
+ })
261
+
262
+ # Check imports for dependencies
263
+ for imp in contract.get("imports", []):
264
+ for other in contracts:
265
+ if other["name"] in imp or other["file"] in imp:
266
+ contract_calls.append({
267
+ "caller": contract["name"],
268
+ "callee": other["name"],
269
+ "function": "import",
270
+ "type": "dependency"
271
+ })
272
+
273
+ if contract_calls:
274
+ call_graph[contract["name"]] = contract_calls
275
+ cross_calls.extend(contract_calls)
276
+
277
+ # Identify attack surface
278
+ attack_surface = []
279
+ for contract in contracts:
280
+ if contract.get("external_calls", 0) > 0:
281
+ attack_surface.append({
282
+ "contract": contract["name"],
283
+ "external_calls": contract["external_calls"],
284
+ "risk": "high" if contract["external_calls"] > 2 else "medium"
285
+ })
286
+
287
+ return {
288
+ "success": True,
289
+ "call_graph": call_graph,
290
+ "cross_calls": cross_calls,
291
+ "attack_surface": attack_surface,
292
+ "total_cross_calls": len(cross_calls)
293
+ }
294
+
295
+
296
+ async def find_cross_contract_vulnerabilities(contracts: list, call_graph: dict = None) -> dict:
297
+ """Identify potential cross-contract vulnerabilities."""
298
+ vulnerabilities = []
299
+
300
+ # Check for oracle dependency
301
+ for contract in contracts:
302
+ for func in contract.get("functions", []):
303
+ if "price" in func["name"].lower() or "oracle" in func["name"].lower():
304
+ vulnerabilities.append({
305
+ "type": "oracle_dependency",
306
+ "contract": contract["name"],
307
+ "function": func["name"],
308
+ "risk": "high",
309
+ "description": f"{contract['name']}.{func['name']} depends on external oracle"
310
+ })
311
+
312
+ # Check for reentrancy vectors
313
+ for contract in contracts:
314
+ if contract.get("external_calls", 0) > 0:
315
+ # Check if state is updated after external call
316
+ has_state_update = False
317
+ for var in contract.get("state_variables", []):
318
+ if "balance" in var.lower() or "amount" in var.lower():
319
+ has_state_update = True
320
+ break
321
+
322
+ if has_state_update:
323
+ vulnerabilities.append({
324
+ "type": "potential_reentrancy",
325
+ "contract": contract["name"],
326
+ "risk": "high",
327
+ "description": f"{contract['name']} has external calls and state variables"
328
+ })
329
+
330
+ # Check for access control issues
331
+ for contract in contracts:
332
+ public_funcs = [f for f in contract.get("functions", []) if f["visibility"] == "public"]
333
+ admin_funcs = [f for f in public_funcs if "admin" in f["name"].lower() or "owner" in f["name"].lower()]
334
+
335
+ if admin_funcs:
336
+ # Check for onlyOwner modifier
337
+ has_modifier = any("onlyOwner" in str(f.get("modifiers", [])) or "onlyAdmin" in str(f.get("modifiers", [])) for f in admin_funcs)
338
+ if not has_modifier:
339
+ vulnerabilities.append({
340
+ "type": "missing_access_control",
341
+ "contract": contract["name"],
342
+ "functions": [f["name"] for f in admin_funcs],
343
+ "risk": "critical",
344
+ "description": f"Admin functions without access control in {contract['name']}"
345
+ })
346
+
347
+ # Check for cross-contract call chains
348
+ if call_graph:
349
+ for caller, calls in call_graph.items():
350
+ for call in calls:
351
+ if call.get("type") == "parameter":
352
+ vulnerabilities.append({
353
+ "type": "cross_contract_call",
354
+ "caller": caller,
355
+ "callee": call.get("callee"),
356
+ "function": call.get("function"),
357
+ "risk": "medium",
358
+ "description": f"Cross-contract call from {caller} to {call.get('callee')}"
359
+ })
360
+
361
+ return {
362
+ "success": True,
363
+ "vulnerabilities": vulnerabilities,
364
+ "total_vulnerabilities": len(vulnerabilities),
365
+ "by_risk": {
366
+ "critical": len([v for v in vulnerabilities if v["risk"] == "critical"]),
367
+ "high": len([v for v in vulnerabilities if v["risk"] == "high"]),
368
+ "medium": len([v for v in vulnerabilities if v["risk"] == "medium"]),
369
+ "low": len([v for v in vulnerabilities if v["risk"] == "low"])
370
+ }
371
+ }
372
+
373
+
374
+ async def execute_tool(tool_name: str, arguments: dict) -> dict:
375
+ if tool_name == "scan_protocol":
376
+ return await scan_protocol(
377
+ arguments.get("directory", ""),
378
+ arguments.get("exclude_patterns")
379
+ )
380
+ elif tool_name == "analyze_call_graph":
381
+ return await analyze_call_graph(arguments.get("contracts", []))
382
+ elif tool_name == "find_cross_contract_vulnerabilities":
383
+ return await find_cross_contract_vulnerabilities(
384
+ arguments.get("contracts", []),
385
+ arguments.get("call_graph")
386
+ )
387
+ return {"success": False, "error": f"Unknown tool: {tool_name}"}
388
+
389
+
390
+ async def handle_request(request: dict) -> dict:
391
+ method = request.get("method")
392
+ params = request.get("params", {})
393
+
394
+ if method == "initialize":
395
+ return {
396
+ "protocolVersion": "2024-11-05",
397
+ "capabilities": {"tools": {}},
398
+ "serverInfo": {"name": TOOL_NAME, "version": TOOL_VERSION}
399
+ }
400
+ elif method == "tools/list":
401
+ return {"tools": build_tool_definitions()}
402
+ elif method == "tools/call":
403
+ tool_name = params.get("name")
404
+ arguments = params.get("arguments", {})
405
+ result = await execute_tool(tool_name, arguments)
406
+ return {"content": [{"type": "text", "text": json.dumps(result, indent=2)}]}
407
+ elif method == "ping":
408
+ return {}
409
+ return {"error": {"code": -32601, "message": f"Method not found: {method}"}}
410
+
411
+
412
+ async def main():
413
+ reader = asyncio.StreamReader()
414
+ protocol = asyncio.StreamReaderProtocol(reader)
415
+ loop = asyncio.get_event_loop()
416
+ await loop.connect_read_pipe(lambda: protocol, sys.stdin)
417
+
418
+ logger.info(f"{TOOL_NAME} MCP server started")
419
+
420
+ while True:
421
+ line = await reader.readline()
422
+ if not line:
423
+ break
424
+
425
+ line_str = line.decode("utf-8").strip()
426
+ if not line_str:
427
+ continue
428
+
429
+ try:
430
+ request = json.loads(line_str)
431
+ response = await handle_request(request)
432
+ response["jsonrpc"] = "2.0"
433
+ response["id"] = request.get("id")
434
+ sys.stdout.write(json.dumps(response) + "\n")
435
+ sys.stdout.flush()
436
+ except json.JSONDecodeError:
437
+ error_resp = {
438
+ "jsonrpc": "2.0",
439
+ "id": None,
440
+ "error": {"code": -32700, "message": "Parse error"}
441
+ }
442
+ sys.stdout.write(json.dumps(error_resp) + "\n")
443
+ sys.stdout.flush()
444
+ except Exception as e:
445
+ logger.exception("Error handling request")
446
+ error_resp = {
447
+ "jsonrpc": "2.0",
448
+ "id": request.get("id") if "request" in dir() else None,
449
+ "error": {"code": -32603, "message": f"Internal error: {str(e)}"}
450
+ }
451
+ sys.stdout.write(json.dumps(error_resp) + "\n")
452
+ sys.stdout.flush()
453
+
454
+
455
+ if __name__ == "__main__":
456
+ asyncio.run(main())