eth-mcp 0.2.0__py3-none-any.whl
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.
- eth_mcp-0.2.0.dist-info/METADATA +332 -0
- eth_mcp-0.2.0.dist-info/RECORD +21 -0
- eth_mcp-0.2.0.dist-info/WHEEL +4 -0
- eth_mcp-0.2.0.dist-info/entry_points.txt +3 -0
- ethereum_mcp/__init__.py +3 -0
- ethereum_mcp/cli.py +589 -0
- ethereum_mcp/clients.py +363 -0
- ethereum_mcp/config.py +324 -0
- ethereum_mcp/expert/__init__.py +1 -0
- ethereum_mcp/expert/guidance.py +300 -0
- ethereum_mcp/indexer/__init__.py +8 -0
- ethereum_mcp/indexer/chunker.py +563 -0
- ethereum_mcp/indexer/client_compiler.py +725 -0
- ethereum_mcp/indexer/compiler.py +245 -0
- ethereum_mcp/indexer/downloader.py +521 -0
- ethereum_mcp/indexer/embedder.py +627 -0
- ethereum_mcp/indexer/manifest.py +411 -0
- ethereum_mcp/logging.py +85 -0
- ethereum_mcp/models.py +126 -0
- ethereum_mcp/server.py +555 -0
- ethereum_mcp/tools/__init__.py +1 -0
ethereum_mcp/server.py
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
"""MCP server for Ethereum specs search."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from fastmcp import FastMCP
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
|
|
9
|
+
from .clients import (
|
|
10
|
+
get_client,
|
|
11
|
+
get_client_diversity,
|
|
12
|
+
get_recommended_pairs,
|
|
13
|
+
list_all_clients,
|
|
14
|
+
list_consensus_clients,
|
|
15
|
+
list_execution_clients,
|
|
16
|
+
)
|
|
17
|
+
from .expert.guidance import get_expert_guidance, list_guidance_topics
|
|
18
|
+
from .indexer.compiler import get_function_source
|
|
19
|
+
from .indexer.embedder import EmbeddingSearcher
|
|
20
|
+
from .logging import get_logger
|
|
21
|
+
from .models import (
|
|
22
|
+
ClientListInput,
|
|
23
|
+
ClientLookupInput,
|
|
24
|
+
ConstantLookupInput,
|
|
25
|
+
EipSearchInput,
|
|
26
|
+
FunctionAnalysisInput,
|
|
27
|
+
GuidanceInput,
|
|
28
|
+
SearchInput,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
logger = get_logger("server")
|
|
32
|
+
|
|
33
|
+
# Initialize MCP server
|
|
34
|
+
mcp = FastMCP("ethereum-mcp")
|
|
35
|
+
|
|
36
|
+
# Default paths
|
|
37
|
+
DEFAULT_DATA_DIR = Path.home() / ".ethereum-mcp"
|
|
38
|
+
DEFAULT_DB_PATH = DEFAULT_DATA_DIR / "lancedb"
|
|
39
|
+
DEFAULT_SPECS_DIR = DEFAULT_DATA_DIR / "consensus-specs"
|
|
40
|
+
|
|
41
|
+
# Fork information
|
|
42
|
+
FORKS = {
|
|
43
|
+
"phase0": {"epoch": 0, "date": "2020-12-01", "description": "Beacon chain genesis"},
|
|
44
|
+
"altair": {"epoch": 74240, "date": "2021-10-27", "description": "Light clients, sync cttes"},
|
|
45
|
+
"bellatrix": {"epoch": 144896, "date": "2022-09-06", "description": "Merge preparation"},
|
|
46
|
+
"capella": {"epoch": 194048, "date": "2023-04-12", "description": "Withdrawals enabled"},
|
|
47
|
+
"deneb": {"epoch": 269568, "date": "2024-03-13", "description": "Blobs (EIP-4844)"},
|
|
48
|
+
"electra": {"epoch": 364032, "date": "2025-05-07", "description": "MaxEB, consolidations"},
|
|
49
|
+
"fulu": {"epoch": 411392, "date": "2025-11-15", "description": "PeerDAS, verkle prep"},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
CURRENT_FORK = "electra" # Pectra = Prague + Electra
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_searcher() -> EmbeddingSearcher:
|
|
56
|
+
"""Get or create embedding searcher."""
|
|
57
|
+
return EmbeddingSearcher(DEFAULT_DB_PATH)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _safe_path(base: Path, *parts: str) -> Path:
|
|
61
|
+
"""
|
|
62
|
+
Safely construct a path, preventing directory traversal.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
base: Base directory that must contain the result
|
|
66
|
+
*parts: Path components to join
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Resolved path guaranteed to be under base
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
ValueError: If the resulting path would escape base directory
|
|
73
|
+
"""
|
|
74
|
+
result = (base / "/".join(parts)).resolve()
|
|
75
|
+
base_resolved = base.resolve()
|
|
76
|
+
if not str(result).startswith(str(base_resolved)):
|
|
77
|
+
raise ValueError(f"Path traversal attempt: {parts}")
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@mcp.tool()
|
|
82
|
+
def eth_health() -> dict:
|
|
83
|
+
"""
|
|
84
|
+
Check health of the Ethereum MCP server.
|
|
85
|
+
|
|
86
|
+
Verifies that the index exists and is queryable.
|
|
87
|
+
Call this to diagnose issues with search functionality.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Health status with component checks
|
|
91
|
+
"""
|
|
92
|
+
status = {
|
|
93
|
+
"healthy": True,
|
|
94
|
+
"checks": {},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Check index exists
|
|
98
|
+
if DEFAULT_DB_PATH.exists():
|
|
99
|
+
status["checks"]["index_exists"] = "ok"
|
|
100
|
+
else:
|
|
101
|
+
status["checks"]["index_exists"] = "missing"
|
|
102
|
+
status["healthy"] = False
|
|
103
|
+
|
|
104
|
+
# Check specs directory
|
|
105
|
+
if DEFAULT_SPECS_DIR.exists():
|
|
106
|
+
status["checks"]["specs_dir"] = "ok"
|
|
107
|
+
else:
|
|
108
|
+
status["checks"]["specs_dir"] = "missing"
|
|
109
|
+
|
|
110
|
+
# Check compiled specs
|
|
111
|
+
compiled_dir = DEFAULT_DATA_DIR / "compiled"
|
|
112
|
+
if compiled_dir.exists() and any(compiled_dir.glob("*.json")):
|
|
113
|
+
status["checks"]["compiled_specs"] = "ok"
|
|
114
|
+
else:
|
|
115
|
+
status["checks"]["compiled_specs"] = "missing"
|
|
116
|
+
|
|
117
|
+
# Try a test query if index exists
|
|
118
|
+
if status["checks"]["index_exists"] == "ok":
|
|
119
|
+
try:
|
|
120
|
+
searcher = get_searcher()
|
|
121
|
+
searcher.search("test", limit=1) # Just verify search works
|
|
122
|
+
status["checks"]["search"] = "ok"
|
|
123
|
+
except Exception as e:
|
|
124
|
+
status["checks"]["search"] = f"error: {e}"
|
|
125
|
+
status["healthy"] = False
|
|
126
|
+
|
|
127
|
+
# Add paths for debugging
|
|
128
|
+
status["paths"] = {
|
|
129
|
+
"data_dir": str(DEFAULT_DATA_DIR),
|
|
130
|
+
"db_path": str(DEFAULT_DB_PATH),
|
|
131
|
+
"specs_dir": str(DEFAULT_SPECS_DIR),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return status
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@mcp.tool()
|
|
138
|
+
def eth_get_current_fork() -> dict:
|
|
139
|
+
"""
|
|
140
|
+
Get the current active Ethereum fork.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Current fork name and metadata
|
|
144
|
+
"""
|
|
145
|
+
return {
|
|
146
|
+
"fork": CURRENT_FORK,
|
|
147
|
+
"info": FORKS[CURRENT_FORK],
|
|
148
|
+
"note": "Pectra = Prague (EL) + Electra (CL)",
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@mcp.tool()
|
|
153
|
+
def eth_list_forks() -> list[dict]:
|
|
154
|
+
"""
|
|
155
|
+
List all major Ethereum network upgrades with dates and epochs.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
List of forks in chronological order
|
|
159
|
+
"""
|
|
160
|
+
return [
|
|
161
|
+
{"fork": name, **info, "current": name == CURRENT_FORK}
|
|
162
|
+
for name, info in FORKS.items()
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@mcp.tool()
|
|
167
|
+
def eth_search(query: str, fork: str | None = None, limit: int = 5) -> list[dict]:
|
|
168
|
+
"""
|
|
169
|
+
Search Ethereum specs and EIPs together.
|
|
170
|
+
|
|
171
|
+
This is the primary search tool - it searches across all indexed
|
|
172
|
+
content including consensus specs and EIPs.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
query: Search query (e.g., "slashing penalty calculation")
|
|
176
|
+
fork: Optional fork to filter by (defaults to current fork context)
|
|
177
|
+
limit: Maximum results to return
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
List of relevant chunks with source and similarity score
|
|
181
|
+
"""
|
|
182
|
+
try:
|
|
183
|
+
validated = SearchInput(query=query, fork=fork, limit=limit)
|
|
184
|
+
except ValidationError as e:
|
|
185
|
+
return [{"error": str(e)}]
|
|
186
|
+
|
|
187
|
+
searcher = get_searcher()
|
|
188
|
+
|
|
189
|
+
# Default to current fork context if not specified
|
|
190
|
+
fork = validated.fork or CURRENT_FORK
|
|
191
|
+
|
|
192
|
+
results = searcher.search(validated.query, limit=validated.limit, fork=fork)
|
|
193
|
+
|
|
194
|
+
# Also search without fork filter to catch EIPs
|
|
195
|
+
eip_results = searcher.search_eip(query)
|
|
196
|
+
|
|
197
|
+
# Merge and dedupe
|
|
198
|
+
seen = set()
|
|
199
|
+
merged = []
|
|
200
|
+
for r in results + eip_results:
|
|
201
|
+
key = (r["source"], r["section"])
|
|
202
|
+
if key not in seen:
|
|
203
|
+
seen.add(key)
|
|
204
|
+
merged.append(r)
|
|
205
|
+
|
|
206
|
+
return sorted(merged, key=lambda x: x["score"], reverse=True)[:limit]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@mcp.tool()
|
|
210
|
+
def eth_search_specs(query: str, fork: str | None = None, limit: int = 5) -> list[dict]:
|
|
211
|
+
"""
|
|
212
|
+
Search consensus specs only (no EIPs).
|
|
213
|
+
|
|
214
|
+
Use this when you specifically want spec content without EIP results.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
query: Search query
|
|
218
|
+
fork: Optional fork filter
|
|
219
|
+
limit: Maximum results
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
List of spec chunks
|
|
223
|
+
"""
|
|
224
|
+
try:
|
|
225
|
+
validated = SearchInput(query=query, fork=fork, limit=limit)
|
|
226
|
+
except ValidationError as e:
|
|
227
|
+
return [{"error": str(e)}]
|
|
228
|
+
|
|
229
|
+
searcher = get_searcher()
|
|
230
|
+
fork = validated.fork or CURRENT_FORK
|
|
231
|
+
|
|
232
|
+
# Exclude EIP chunk type
|
|
233
|
+
results = searcher.search(validated.query, limit=validated.limit * 2, fork=fork)
|
|
234
|
+
return [r for r in results if r["chunk_type"] != "eip"][:validated.limit]
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@mcp.tool()
|
|
238
|
+
def eth_grep_constant(constant_name: str, fork: str | None = None) -> dict | None:
|
|
239
|
+
"""
|
|
240
|
+
Look up a specific constant value from the specs.
|
|
241
|
+
|
|
242
|
+
Optimized for finding constants like MAX_EFFECTIVE_BALANCE,
|
|
243
|
+
MIN_SLASHING_PENALTY_QUOTIENT, etc.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
constant_name: Name of constant (e.g., "MAX_EFFECTIVE_BALANCE")
|
|
247
|
+
fork: Fork to look up (defaults to current)
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Constant definition with value and context
|
|
251
|
+
"""
|
|
252
|
+
try:
|
|
253
|
+
validated = ConstantLookupInput(constant_name=constant_name, fork=fork)
|
|
254
|
+
except ValidationError as e:
|
|
255
|
+
return {"error": str(e)}
|
|
256
|
+
|
|
257
|
+
fork = validated.fork or CURRENT_FORK
|
|
258
|
+
|
|
259
|
+
# Try compiled specs first - use safe path construction
|
|
260
|
+
try:
|
|
261
|
+
spec_file = _safe_path(DEFAULT_DATA_DIR, "compiled", f"{fork}_spec.json")
|
|
262
|
+
except ValueError:
|
|
263
|
+
return {"error": "Invalid fork name"}
|
|
264
|
+
|
|
265
|
+
if spec_file.exists():
|
|
266
|
+
with open(spec_file) as f:
|
|
267
|
+
spec_data = json.load(f)
|
|
268
|
+
if validated.constant_name in spec_data.get("constants", {}):
|
|
269
|
+
return {
|
|
270
|
+
"constant": validated.constant_name,
|
|
271
|
+
"value": spec_data["constants"][validated.constant_name],
|
|
272
|
+
"fork": fork,
|
|
273
|
+
"source": "compiled_spec",
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
# Fall back to embedding search
|
|
277
|
+
searcher = get_searcher()
|
|
278
|
+
results = searcher.search_constant(validated.constant_name)
|
|
279
|
+
|
|
280
|
+
for r in results:
|
|
281
|
+
if validated.constant_name.upper() in r["content"].upper():
|
|
282
|
+
return {
|
|
283
|
+
"constant": validated.constant_name,
|
|
284
|
+
"context": r["content"],
|
|
285
|
+
"fork": r["fork"] or fork,
|
|
286
|
+
"source": r["source"],
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@mcp.tool()
|
|
293
|
+
def eth_analyze_function(function_name: str, fork: str | None = None) -> dict | None:
|
|
294
|
+
"""
|
|
295
|
+
Get the actual Python implementation of a spec function.
|
|
296
|
+
|
|
297
|
+
Returns the complete function source code from the consensus specs.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
function_name: Name of function (e.g., "process_slashings")
|
|
301
|
+
fork: Fork version (defaults to current)
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Function source code and metadata
|
|
305
|
+
"""
|
|
306
|
+
try:
|
|
307
|
+
validated = FunctionAnalysisInput(function_name=function_name, fork=fork)
|
|
308
|
+
except ValidationError as e:
|
|
309
|
+
return {"error": str(e)}
|
|
310
|
+
|
|
311
|
+
fork = validated.fork or CURRENT_FORK
|
|
312
|
+
|
|
313
|
+
# Try to get from source files
|
|
314
|
+
source = get_function_source(DEFAULT_SPECS_DIR, fork, validated.function_name)
|
|
315
|
+
|
|
316
|
+
if source:
|
|
317
|
+
return {
|
|
318
|
+
"function": validated.function_name,
|
|
319
|
+
"fork": fork,
|
|
320
|
+
"source": source,
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
# Fall back to embedding search
|
|
324
|
+
searcher = get_searcher()
|
|
325
|
+
results = searcher.search_function(validated.function_name, fork=fork)
|
|
326
|
+
|
|
327
|
+
if results:
|
|
328
|
+
return {
|
|
329
|
+
"function": validated.function_name,
|
|
330
|
+
"fork": results[0]["fork"] or fork,
|
|
331
|
+
"source": results[0]["content"],
|
|
332
|
+
"file": results[0]["source"],
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@mcp.tool()
|
|
339
|
+
def eth_get_spec_version() -> dict:
|
|
340
|
+
"""
|
|
341
|
+
Get metadata about indexed spec versions.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Version info for indexed specs and EIPs
|
|
345
|
+
"""
|
|
346
|
+
import subprocess
|
|
347
|
+
|
|
348
|
+
version_info = {
|
|
349
|
+
"indexed_forks": list(FORKS.keys()),
|
|
350
|
+
"current_fork": CURRENT_FORK,
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
# Try to get git commit info
|
|
354
|
+
if DEFAULT_SPECS_DIR.exists():
|
|
355
|
+
try:
|
|
356
|
+
result = subprocess.run(
|
|
357
|
+
["git", "rev-parse", "--short", "HEAD"],
|
|
358
|
+
cwd=DEFAULT_SPECS_DIR,
|
|
359
|
+
capture_output=True,
|
|
360
|
+
text=True,
|
|
361
|
+
)
|
|
362
|
+
if result.returncode == 0:
|
|
363
|
+
version_info["consensus_specs_commit"] = result.stdout.strip()
|
|
364
|
+
else:
|
|
365
|
+
logger.debug("Git rev-parse failed: %s", result.stderr)
|
|
366
|
+
except Exception as e:
|
|
367
|
+
logger.debug("Failed to get git commit info: %s", e)
|
|
368
|
+
|
|
369
|
+
# Check if index exists
|
|
370
|
+
db_path = DEFAULT_DB_PATH
|
|
371
|
+
if db_path.exists():
|
|
372
|
+
version_info["index_path"] = str(db_path)
|
|
373
|
+
version_info["index_exists"] = True
|
|
374
|
+
else:
|
|
375
|
+
version_info["index_exists"] = False
|
|
376
|
+
|
|
377
|
+
return version_info
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@mcp.tool()
|
|
381
|
+
def eth_expert_guidance(topic: str) -> dict | None:
|
|
382
|
+
"""
|
|
383
|
+
Get curated expert interpretations on Ethereum topics.
|
|
384
|
+
|
|
385
|
+
This provides nuanced guidance that goes beyond what's explicitly
|
|
386
|
+
in the specs - including common gotchas, implementation notes,
|
|
387
|
+
and insights from EF discussions.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
topic: Topic to get guidance on (e.g., "slashing", "churn", "withdrawals")
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Expert guidance with key points and references
|
|
394
|
+
"""
|
|
395
|
+
try:
|
|
396
|
+
validated = GuidanceInput(topic=topic)
|
|
397
|
+
except ValidationError as e:
|
|
398
|
+
return {"error": str(e)}
|
|
399
|
+
|
|
400
|
+
guidance = get_expert_guidance(validated.topic)
|
|
401
|
+
|
|
402
|
+
if guidance:
|
|
403
|
+
return {
|
|
404
|
+
"topic": validated.topic,
|
|
405
|
+
"guidance": guidance,
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
# List available topics if not found
|
|
409
|
+
available = list_guidance_topics()
|
|
410
|
+
return {
|
|
411
|
+
"topic": validated.topic,
|
|
412
|
+
"error": f"No guidance found for '{validated.topic}'",
|
|
413
|
+
"available_topics": available,
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
@mcp.tool()
|
|
418
|
+
def eth_search_eip(query: str, eip_number: str | None = None, limit: int = 5) -> list[dict]:
|
|
419
|
+
"""
|
|
420
|
+
Search EIPs specifically.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
query: Search query
|
|
424
|
+
eip_number: Optional specific EIP number to search within
|
|
425
|
+
limit: Maximum results
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
List of relevant EIP chunks
|
|
429
|
+
"""
|
|
430
|
+
try:
|
|
431
|
+
validated = EipSearchInput(query=query, eip_number=eip_number, limit=limit)
|
|
432
|
+
except ValidationError as e:
|
|
433
|
+
return [{"error": str(e)}]
|
|
434
|
+
|
|
435
|
+
searcher = get_searcher()
|
|
436
|
+
results = searcher.search_eip(validated.query)
|
|
437
|
+
|
|
438
|
+
if validated.eip_number:
|
|
439
|
+
results = [r for r in results if r.get("eip") == validated.eip_number]
|
|
440
|
+
|
|
441
|
+
return results[:validated.limit]
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@mcp.tool()
|
|
445
|
+
def eth_list_clients(layer: str | None = None) -> list[dict]:
|
|
446
|
+
"""
|
|
447
|
+
List Ethereum client implementations.
|
|
448
|
+
|
|
449
|
+
Ethereum has separate execution layer (EL) and consensus layer (CL) clients
|
|
450
|
+
that run together. Client diversity is critical for network security.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
layer: Filter by "execution" or "consensus" (default: both)
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
List of clients with their details
|
|
457
|
+
"""
|
|
458
|
+
try:
|
|
459
|
+
validated = ClientListInput(layer=layer)
|
|
460
|
+
except ValidationError as e:
|
|
461
|
+
return [{"error": str(e)}]
|
|
462
|
+
|
|
463
|
+
if validated.layer == "execution":
|
|
464
|
+
clients = list_execution_clients()
|
|
465
|
+
elif validated.layer == "consensus":
|
|
466
|
+
clients = list_consensus_clients()
|
|
467
|
+
else:
|
|
468
|
+
clients = list_all_clients()
|
|
469
|
+
|
|
470
|
+
return [
|
|
471
|
+
{
|
|
472
|
+
"name": c.name,
|
|
473
|
+
"layer": c.layer,
|
|
474
|
+
"organization": c.organization,
|
|
475
|
+
"language": c.language,
|
|
476
|
+
"repo": c.repo,
|
|
477
|
+
"status": c.mainnet_status,
|
|
478
|
+
"percentage": c.node_percentage or c.stake_percentage,
|
|
479
|
+
}
|
|
480
|
+
for c in clients
|
|
481
|
+
]
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@mcp.tool()
|
|
485
|
+
def eth_get_client(name: str) -> dict | None:
|
|
486
|
+
"""
|
|
487
|
+
Get details about a specific Ethereum client.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
name: Client name (e.g., "geth", "reth", "lighthouse", "prysm")
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
Full client details including features and notes
|
|
494
|
+
"""
|
|
495
|
+
try:
|
|
496
|
+
validated = ClientLookupInput(name=name)
|
|
497
|
+
except ValidationError as e:
|
|
498
|
+
return {"error": str(e)}
|
|
499
|
+
|
|
500
|
+
client = get_client(validated.name)
|
|
501
|
+
if not client:
|
|
502
|
+
all_clients = list_all_clients()
|
|
503
|
+
return {
|
|
504
|
+
"error": f"Client '{validated.name}' not found",
|
|
505
|
+
"available": [c.name for c in all_clients],
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
"name": client.name,
|
|
510
|
+
"layer": client.layer,
|
|
511
|
+
"organization": client.organization,
|
|
512
|
+
"language": client.language,
|
|
513
|
+
"repo": client.repo,
|
|
514
|
+
"description": client.description,
|
|
515
|
+
"status": client.mainnet_status,
|
|
516
|
+
"percentage": client.node_percentage or client.stake_percentage,
|
|
517
|
+
"key_features": client.key_features,
|
|
518
|
+
"notes": client.notes,
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
@mcp.tool()
|
|
523
|
+
def eth_get_client_diversity() -> dict:
|
|
524
|
+
"""
|
|
525
|
+
Get Ethereum client diversity statistics.
|
|
526
|
+
|
|
527
|
+
Client diversity is critical for network security. If any client has >66% share,
|
|
528
|
+
a bug in that client could cause incorrect finalization.
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
Diversity statistics for EL and CL, health assessment, and recommendations
|
|
532
|
+
"""
|
|
533
|
+
return get_client_diversity()
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
@mcp.tool()
|
|
537
|
+
def eth_get_recommended_client_pairs() -> list[dict]:
|
|
538
|
+
"""
|
|
539
|
+
Get recommended execution + consensus client pairings.
|
|
540
|
+
|
|
541
|
+
Validators need both an EL and CL client. Some combinations work better together.
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
List of recommended EL+CL pairs with notes
|
|
545
|
+
"""
|
|
546
|
+
return get_recommended_pairs()
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def run():
|
|
550
|
+
"""Run the MCP server."""
|
|
551
|
+
mcp.run()
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
if __name__ == "__main__":
|
|
555
|
+
run()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP tools for Ethereum specs search."""
|