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,226 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP Server: Aderyn Runner
4
+ Wraps Aderyn (Rust-based) static analysis 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("aderyn_runner")
17
+
18
+ TOOL_NAME = "aderyn_runner"
19
+ TOOL_VERSION = "1.0.0"
20
+
21
+
22
+ def build_tool_definitions() -> list:
23
+ return [
24
+ {
25
+ "name": "aderyn_analyze",
26
+ "description": "Run Aderyn static analysis on a Solidity project. Returns findings grouped by severity (high, low, informational, optimization).",
27
+ "inputSchema": {
28
+ "type": "object",
29
+ "properties": {
30
+ "project_path": {
31
+ "type": "string",
32
+ "description": "Path to the Solidity project root directory (must contain foundry.toml or hardhat.config)"
33
+ }
34
+ },
35
+ "required": ["project_path"]
36
+ }
37
+ }
38
+ ]
39
+
40
+
41
+ async def run_aderyn(project_path: str) -> dict:
42
+ """Execute Aderyn and parse JSON output."""
43
+ project_path = os.path.abspath(project_path)
44
+
45
+ if not os.path.isdir(project_path):
46
+ return {"success": False, "error": f"Project directory not found: {project_path}"}
47
+
48
+ tmp_dir = tempfile.mkdtemp()
49
+ json_output_path = os.path.join(tmp_dir, "report.json")
50
+
51
+ try:
52
+ cmd = ["aderyn", project_path, "--output", json_output_path]
53
+
54
+ logger.info(f"Running aderyn: {' '.join(cmd)}")
55
+
56
+ proc = await asyncio.create_subprocess_exec(
57
+ *cmd,
58
+ stdout=asyncio.subprocess.PIPE,
59
+ stderr=asyncio.subprocess.PIPE
60
+ )
61
+ stdout, stderr = await proc.communicate()
62
+
63
+ if proc.returncode != 0:
64
+ return {
65
+ "success": False,
66
+ "error": f"Aderyn exited with code {proc.returncode}",
67
+ "stderr": stderr.decode("utf-8", errors="replace").strip()
68
+ }
69
+
70
+ # Parse JSON output
71
+ if not os.path.isfile(json_output_path):
72
+ # Aderyn may output with a different name
73
+ possible_names = ["report.json", "aderyn-report.json", "output.json"]
74
+ for name in possible_names:
75
+ alt_path = os.path.join(tmp_dir, name)
76
+ if os.path.isfile(alt_path):
77
+ json_output_path = alt_path
78
+ break
79
+ else:
80
+ return {
81
+ "success": False,
82
+ "error": "Aderyn did not produce JSON output",
83
+ "stderr": stderr.decode("utf-8", errors="replace").strip(),
84
+ "stdout": stdout.decode("utf-8", errors="replace").strip()
85
+ }
86
+
87
+ with open(json_output_path, "r") as f:
88
+ raw_output = json.load(f)
89
+
90
+ # Normalize Aderyn output structure
91
+ # Aderyn groups issues by severity
92
+ def extract_issues(issue_list):
93
+ issues = []
94
+ for issue in (issue_list or []):
95
+ issues.append({
96
+ "title": issue.get("title", ""),
97
+ "description": issue.get("description", ""),
98
+ "detector_name": issue.get("detector_name", ""),
99
+ "severity": issue.get("severity", ""),
100
+ "instances": [
101
+ {
102
+ "contract_path": inst.get("contract_path", ""),
103
+ "line_no": inst.get("line_no", 0),
104
+ "src": inst.get("src", ""),
105
+ "code_snippet": inst.get("code_snippet", "")
106
+ }
107
+ for inst in issue.get("instances", [])
108
+ ],
109
+ "instance_count": len(issue.get("instances", []))
110
+ })
111
+ return issues
112
+
113
+ high_issues = extract_issues(raw_output.get("high_issues", {}).get("issues", []))
114
+ low_issues = extract_issues(raw_output.get("low_issues", {}).get("issues", []))
115
+ informational_issues = extract_issues(raw_output.get("informational_issues", {}).get("issues", []))
116
+ optimization_issues = extract_issues(raw_output.get("optimization_issues", {}).get("issues", []))
117
+
118
+ total_issues = (
119
+ len(high_issues) + len(low_issues) +
120
+ len(informational_issues) + len(optimization_issues)
121
+ )
122
+
123
+ return {
124
+ "success": True,
125
+ "project": project_path,
126
+ "summary": {
127
+ "total_issues": total_issues,
128
+ "high_severity": len(high_issues),
129
+ "low_severity": len(low_issues),
130
+ "informational": len(informational_issues),
131
+ "optimization": len(optimization_issues)
132
+ },
133
+ "high_issues": high_issues,
134
+ "low_issues": low_issues,
135
+ "informational_issues": informational_issues,
136
+ "optimization_issues": optimization_issues
137
+ }
138
+
139
+ except FileNotFoundError:
140
+ return {"success": False, "error": "Aderyn not found. Install with: cargo install aderyn"}
141
+ except json.JSONDecodeError as e:
142
+ return {"success": False, "error": f"Failed to parse Aderyn JSON output: {str(e)}"}
143
+ except Exception as e:
144
+ logger.exception("Unexpected error in aderyn_runner")
145
+ return {"success": False, "error": f"Unexpected error: {str(e)}"}
146
+ finally:
147
+ # Cleanup temp dir
148
+ import shutil
149
+ if os.path.exists(tmp_dir):
150
+ shutil.rmtree(tmp_dir, ignore_errors=True)
151
+
152
+
153
+ async def execute_tool(tool_name: str, arguments: dict) -> dict:
154
+ if tool_name == "aderyn_analyze":
155
+ project_path = arguments.get("project_path", "")
156
+ return await run_aderyn(project_path)
157
+ return {"success": False, "error": f"Unknown tool: {tool_name}"}
158
+
159
+
160
+ async def handle_request(request: dict) -> dict:
161
+ method = request.get("method")
162
+ params = request.get("params", {})
163
+
164
+ if method == "initialize":
165
+ return {
166
+ "protocolVersion": "2024-11-05",
167
+ "capabilities": {"tools": {}},
168
+ "serverInfo": {"name": TOOL_NAME, "version": TOOL_VERSION}
169
+ }
170
+ elif method == "tools/list":
171
+ return {"tools": build_tool_definitions()}
172
+ elif method == "tools/call":
173
+ tool_name = params.get("name")
174
+ arguments = params.get("arguments", {})
175
+ result = await execute_tool(tool_name, arguments)
176
+ return {"content": [{"type": "text", "text": json.dumps(result, indent=2)}]}
177
+ elif method == "ping":
178
+ return {}
179
+ return {"error": {"code": -32601, "message": f"Method not found: {method}"}}
180
+
181
+
182
+ async def main():
183
+ reader = asyncio.StreamReader()
184
+ protocol = asyncio.StreamReaderProtocol(reader)
185
+ loop = asyncio.get_event_loop()
186
+ await loop.connect_read_pipe(lambda: protocol, sys.stdin)
187
+
188
+ logger.info(f"{TOOL_NAME} MCP server started")
189
+
190
+ while True:
191
+ line = await reader.readline()
192
+ if not line:
193
+ break
194
+
195
+ line_str = line.decode("utf-8").strip()
196
+ if not line_str:
197
+ continue
198
+
199
+ try:
200
+ request = json.loads(line_str)
201
+ response = await handle_request(request)
202
+ response["jsonrpc"] = "2.0"
203
+ response["id"] = request.get("id")
204
+ sys.stdout.write(json.dumps(response) + "\n")
205
+ sys.stdout.flush()
206
+ except json.JSONDecodeError:
207
+ error_resp = {
208
+ "jsonrpc": "2.0",
209
+ "id": None,
210
+ "error": {"code": -32700, "message": "Parse error"}
211
+ }
212
+ sys.stdout.write(json.dumps(error_resp) + "\n")
213
+ sys.stdout.flush()
214
+ except Exception as e:
215
+ logger.exception("Error handling request")
216
+ error_resp = {
217
+ "jsonrpc": "2.0",
218
+ "id": request.get("id") if "request" in dir() else None,
219
+ "error": {"code": -32603, "message": f"Internal error: {str(e)}"}
220
+ }
221
+ sys.stdout.write(json.dumps(error_resp) + "\n")
222
+ sys.stdout.flush()
223
+
224
+
225
+ if __name__ == "__main__":
226
+ asyncio.run(main())
@@ -0,0 +1,404 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP Server: EAS Attestation
4
+ Handles on-chain attestation via Ethereum Attestation Service (EAS) on Sepolia testnet.
5
+ """
6
+ import sys
7
+ import json
8
+ import asyncio
9
+ import logging
10
+ import os
11
+ import hashlib
12
+ from datetime import datetime
13
+
14
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
15
+ logger = logging.getLogger("eas_attest")
16
+
17
+ TOOL_NAME = "eas_attest"
18
+ TOOL_VERSION = "1.0.0"
19
+
20
+ # EAS Sepolia configuration
21
+ EAS_CONTRACT_ADDRESS = "0xC2679fBD37d54388Ce493F1DB75320D236e1815e"
22
+ EAS_SCHEMA_REGISTRY = "0x0a7E2Ff94F05A4a6dCa5eF0D47c1e39e8F8e5fC0"
23
+ SEPOLIA_CHAIN_ID = 11155111
24
+
25
+ # Audit result schema UID (example - replace with actual registered schema)
26
+ AUDIT_RESULT_SCHEMA_UID = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
27
+
28
+ # Common vulnerability severities
29
+ SEVERITY_MAP = {
30
+ "critical": 4,
31
+ "high": 3,
32
+ "medium": 2,
33
+ "low": 1,
34
+ "informational": 0
35
+ }
36
+
37
+
38
+ def build_tool_definitions() -> list:
39
+ return [
40
+ {
41
+ "name": "submit_attestation",
42
+ "description": "Submit an audit attestation to the Ethereum Attestation Service (EAS) on Sepolia testnet. Creates an on-chain record of the audit results including found vulnerabilities and their severities.",
43
+ "inputSchema": {
44
+ "type": "object",
45
+ "properties": {
46
+ "contract_address": {
47
+ "type": "string",
48
+ "description": "The Ethereum address of the audited smart contract"
49
+ },
50
+ "vulnerabilities": {
51
+ "type": "array",
52
+ "items": {
53
+ "type": "object",
54
+ "properties": {
55
+ "type": {"type": "string", "description": "Vulnerability type (e.g., 'reentrancy', 'overflow')"},
56
+ "severity": {"type": "string", "enum": ["critical", "high", "medium", "low", "informational"]},
57
+ "description": {"type": "string"},
58
+ "location": {"type": "string", "description": "File and line number"},
59
+ "remediation": {"type": "string", "description": "Suggested fix"}
60
+ },
61
+ "required": ["type", "severity"]
62
+ },
63
+ "description": "List of vulnerabilities found during the audit"
64
+ },
65
+ "audit_mode": {
66
+ "type": "string",
67
+ "enum": ["full", "quick", "targeted"],
68
+ "description": "Type of audit performed (default: full)",
69
+ "default": "full"
70
+ }
71
+ },
72
+ "required": ["contract_address", "vulnerabilities"]
73
+ }
74
+ }
75
+ ]
76
+
77
+
78
+ def compute_audit_hash(contract_address: str, vulnerabilities: list) -> str:
79
+ """Compute deterministic hash of audit results for verification."""
80
+ audit_data = {
81
+ "contract": contract_address.lower(),
82
+ "vulnerabilities": sorted(
83
+ [
84
+ {
85
+ "type": v.get("type", ""),
86
+ "severity": v.get("severity", ""),
87
+ "description": v.get("description", "")
88
+ }
89
+ for v in vulnerabilities
90
+ ],
91
+ key=lambda x: x["type"]
92
+ ),
93
+ "timestamp": datetime.utcnow().strftime("%Y-%m-%d")
94
+ }
95
+ data_str = json.dumps(audit_data, sort_keys=True)
96
+ return "0x" + hashlib.sha256(data_str.encode()).hexdigest()
97
+
98
+
99
+ def compute_audit_grade(vulnerabilities: list) -> str:
100
+ """Compute audit grade based on vulnerability severities."""
101
+ if not vulnerabilities:
102
+ return "A+"
103
+
104
+ max_severity = 0
105
+ for vuln in vulnerabilities:
106
+ severity = vuln.get("severity", "informational").lower()
107
+ sev_value = SEVERITY_MAP.get(severity, 0)
108
+ max_severity = max(max_severity, sev_value)
109
+
110
+ # Count by severity
111
+ counts = {}
112
+ for vuln in vulnerabilities:
113
+ severity = vuln.get("severity", "informational").lower()
114
+ counts[severity] = counts.get(severity, 0) + 1
115
+
116
+ # Grade calculation
117
+ if max_severity >= 4: # critical
118
+ return "F"
119
+ elif max_severity >= 3: # high
120
+ if counts.get("high", 0) >= 3:
121
+ return "D"
122
+ return "C"
123
+ elif max_severity >= 2: # medium
124
+ if counts.get("medium", 0) >= 3:
125
+ return "C"
126
+ return "B"
127
+ elif max_severity >= 1: # low
128
+ return "B+"
129
+ else: # informational only
130
+ return "A"
131
+
132
+
133
+ async def submit_eas_attestation(
134
+ contract_address: str,
135
+ vulnerabilities: list,
136
+ audit_mode: str = "full"
137
+ ) -> dict:
138
+ """Submit attestation to EAS on Sepolia."""
139
+
140
+ # Validate contract address
141
+ if not contract_address.startswith("0x") or len(contract_address) != 42:
142
+ return {"success": False, "error": "Invalid contract address format"}
143
+
144
+ # Compute audit hash and grade
145
+ audit_hash = compute_audit_hash(contract_address, vulnerabilities)
146
+ grade = compute_audit_grade(vulnerabilities)
147
+
148
+ # Build attestation data
149
+ attestation_data = {
150
+ "schema": AUDIT_RESULT_SCHEMA_UID,
151
+ "recipient": contract_address,
152
+ "expirationTime": 0,
153
+ "revocable": True,
154
+ "refUID": "0x0000000000000000000000000000000000000000000000000000000000000000",
155
+ "data": {
156
+ "contract_address": contract_address,
157
+ "audit_hash": audit_hash,
158
+ "grade": grade,
159
+ "audit_mode": audit_mode,
160
+ "vulnerability_count": len(vulnerabilities),
161
+ "vulnerabilities": vulnerabilities,
162
+ "timestamp": datetime.utcnow().isoformat(),
163
+ "auditor": "GLM-Audit-Agent-v1"
164
+ }
165
+ }
166
+
167
+ # Check for private key in environment
168
+ private_key = os.environ.get("SEPOLIA_PRIVATE_KEY", "")
169
+ rpc_url = os.environ.get("SEPOLIA_RPC_URL", "https://rpc.sepolia.org")
170
+
171
+ if private_key:
172
+ # Real on-chain attestation
173
+ try:
174
+ return await _send_onchain_attestation(attestation_data, private_key, rpc_url)
175
+ except Exception as e:
176
+ logger.error(f"On-chain attestation failed: {e}")
177
+ return {
178
+ "success": False,
179
+ "error": f"On-chain attestation failed: {str(e)}",
180
+ "attestation_data": attestation_data
181
+ }
182
+ else:
183
+ # Mock mode - generate attestation without sending to chain
184
+ logger.info("No private key configured, generating mock attestation")
185
+ return _generate_mock_attestation(attestation_data)
186
+
187
+
188
+ async def _send_onchain_attestation(attestation_data: dict, private_key: str, rpc_url: str) -> dict:
189
+ """Send real attestation to EAS on Sepolia."""
190
+ try:
191
+ from web3 import Web3
192
+ from eth_account import Account
193
+
194
+ w3 = Web3(Web3.HTTPProvider(rpc_url))
195
+ if not w3.is_connected():
196
+ return {"success": False, "error": f"Cannot connect to {rpc_url}"}
197
+
198
+ account = Account.from_key(private_key)
199
+ balance = w3.eth.get_balance(account.address)
200
+ if balance < w3.to_wei(0.001, "ether"):
201
+ return {
202
+ "success": False,
203
+ "error": f"Insufficient balance: {w3.from_wei(balance, 'ether')} ETH. Need at least 0.001 ETH for gas."
204
+ }
205
+
206
+ # EAS contract ABI (minimal for attest)
207
+ eas_abi = [
208
+ {
209
+ "inputs": [
210
+ {
211
+ "components": [
212
+ {"name": "schema", "type": "bytes32"},
213
+ {"name": "recipient", "type": "address"},
214
+ {"name": "expirationTime", "type": "uint64"},
215
+ {"name": "revocable", "type": "bool"},
216
+ {"name": "refUID", "type": "bytes32"},
217
+ {"name": "data", "type": "bytes"},
218
+ {"name": "value", "type": "uint256"}
219
+ ],
220
+ "name": "request",
221
+ "type": "tuple"
222
+ }
223
+ ],
224
+ "name": "attest",
225
+ "outputs": [{"name": "", "type": "bytes32"}],
226
+ "stateMutability": "payable",
227
+ "type": "function"
228
+ }
229
+ ]
230
+
231
+ eas_contract = w3.eth.contract(
232
+ address=Web3.to_checksum_address(EAS_CONTRACT_ADDRESS),
233
+ abi=eas_abi
234
+ )
235
+
236
+ # Encode attestation data
237
+ data_bytes = json.dumps(attestation_data["data"]).encode("utf-8")
238
+
239
+ # Build transaction
240
+ attestation_request = (
241
+ bytes.fromhex(attestation_data["schema"][2:]),
242
+ Web3.to_checksum_address(attestation_data["recipient"]),
243
+ attestation_data["expirationTime"],
244
+ attestation_data["revocable"],
245
+ bytes.fromhex(attestation_data["refUID"][2:]),
246
+ data_bytes,
247
+ 0
248
+ )
249
+
250
+ tx = eas_contract.functions.attest(attestation_request).build_transaction({
251
+ "from": account.address,
252
+ "nonce": w3.eth.get_transaction_count(account.address),
253
+ "gas": 200000,
254
+ "gasPrice": w3.eth.gas_price,
255
+ "value": 0
256
+ })
257
+
258
+ signed_tx = w3.eth.account.sign_transaction(tx, private_key)
259
+ tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
260
+
261
+ # Wait for receipt
262
+ receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
263
+
264
+ return {
265
+ "success": True,
266
+ "mode": "onchain",
267
+ "tx_hash": receipt.transactionHash.hex(),
268
+ "block_number": receipt.blockNumber,
269
+ "gas_used": receipt.gasUsed,
270
+ "contract_address": contract_address,
271
+ "grade": attestation_data["data"]["grade"],
272
+ "audit_hash": attestation_data["data"]["audit_hash"],
273
+ "eas_contract": EAS_CONTRACT_ADDRESS,
274
+ "network": "sepolia",
275
+ "explorer_url": f"https://sepolia.etherscan.io/tx/{receipt.transactionHash.hex()}"
276
+ }
277
+
278
+ except ImportError:
279
+ return {
280
+ "success": False,
281
+ "error": "web3 and eth-account packages required for on-chain attestation. Install with: pip install web3 eth-account"
282
+ }
283
+
284
+
285
+ def _generate_mock_attestation(attestation_data: dict) -> dict:
286
+ """Generate mock attestation for testing."""
287
+ mock_tx_hash = hashlib.sha256(
288
+ json.dumps(attestation_data, sort_keys=True).encode()
289
+ ).hexdigest()
290
+
291
+ data = attestation_data["data"]
292
+
293
+ return {
294
+ "success": True,
295
+ "mode": "mock",
296
+ "note": "This is a mock attestation. Set SEPOLIA_PRIVATE_KEY and SEPOLIA_RPC_URL for on-chain attestation.",
297
+ "tx_hash": f"0x{mock_tx_hash}",
298
+ "contract_address": data["contract_address"],
299
+ "grade": data["grade"],
300
+ "audit_hash": data["audit_hash"],
301
+ "audit_mode": data["audit_mode"],
302
+ "vulnerability_summary": {
303
+ "total": data["vulnerability_count"],
304
+ "by_severity": _count_by_severity(data["vulnerabilities"])
305
+ },
306
+ "eas_contract": EAS_CONTRACT_ADDRESS,
307
+ "network": "sepolia",
308
+ "timestamp": data["timestamp"]
309
+ }
310
+
311
+
312
+ def _count_by_severity(vulnerabilities: list) -> dict:
313
+ """Count vulnerabilities by severity level."""
314
+ counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "informational": 0}
315
+ for vuln in vulnerabilities:
316
+ severity = vuln.get("severity", "informational").lower()
317
+ if severity in counts:
318
+ counts[severity] += 1
319
+ return counts
320
+
321
+
322
+ async def execute_tool(tool_name: str, arguments: dict) -> dict:
323
+ if tool_name == "submit_attestation":
324
+ contract_address = arguments.get("contract_address", "")
325
+ vulnerabilities = arguments.get("vulnerabilities", [])
326
+ audit_mode = arguments.get("audit_mode", "full")
327
+
328
+ if not contract_address:
329
+ return {"success": False, "error": "contract_address is required"}
330
+ if not vulnerabilities:
331
+ return {"success": False, "error": "vulnerabilities array is required"}
332
+
333
+ return await submit_eas_attestation(contract_address, vulnerabilities, audit_mode)
334
+
335
+ return {"success": False, "error": f"Unknown tool: {tool_name}"}
336
+
337
+
338
+ async def handle_request(request: dict) -> dict:
339
+ method = request.get("method")
340
+ params = request.get("params", {})
341
+
342
+ if method == "initialize":
343
+ return {
344
+ "protocolVersion": "2024-11-05",
345
+ "capabilities": {"tools": {}},
346
+ "serverInfo": {"name": TOOL_NAME, "version": TOOL_VERSION}
347
+ }
348
+ elif method == "tools/list":
349
+ return {"tools": build_tool_definitions()}
350
+ elif method == "tools/call":
351
+ tool_name = params.get("name")
352
+ arguments = params.get("arguments", {})
353
+ result = await execute_tool(tool_name, arguments)
354
+ return {"content": [{"type": "text", "text": json.dumps(result, indent=2)}]}
355
+ elif method == "ping":
356
+ return {}
357
+ return {"error": {"code": -32601, "message": f"Method not found: {method}"}}
358
+
359
+
360
+ async def main():
361
+ reader = asyncio.StreamReader()
362
+ protocol = asyncio.StreamReaderProtocol(reader)
363
+ loop = asyncio.get_event_loop()
364
+ await loop.connect_read_pipe(lambda: protocol, sys.stdin)
365
+
366
+ logger.info(f"{TOOL_NAME} MCP server started")
367
+
368
+ while True:
369
+ line = await reader.readline()
370
+ if not line:
371
+ break
372
+
373
+ line_str = line.decode("utf-8").strip()
374
+ if not line_str:
375
+ continue
376
+
377
+ try:
378
+ request = json.loads(line_str)
379
+ response = await handle_request(request)
380
+ response["jsonrpc"] = "2.0"
381
+ response["id"] = request.get("id")
382
+ sys.stdout.write(json.dumps(response) + "\n")
383
+ sys.stdout.flush()
384
+ except json.JSONDecodeError:
385
+ error_resp = {
386
+ "jsonrpc": "2.0",
387
+ "id": None,
388
+ "error": {"code": -32700, "message": "Parse error"}
389
+ }
390
+ sys.stdout.write(json.dumps(error_resp) + "\n")
391
+ sys.stdout.flush()
392
+ except Exception as e:
393
+ logger.exception("Error handling request")
394
+ error_resp = {
395
+ "jsonrpc": "2.0",
396
+ "id": request.get("id") if "request" in dir() else None,
397
+ "error": {"code": -32603, "message": f"Internal error: {str(e)}"}
398
+ }
399
+ sys.stdout.write(json.dumps(error_resp) + "\n")
400
+ sys.stdout.flush()
401
+
402
+
403
+ if __name__ == "__main__":
404
+ asyncio.run(main())