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
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Compile consensus specs into executable Python modules."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ..logging import get_logger
|
|
9
|
+
|
|
10
|
+
logger = get_logger("compiler")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class CompiledSpec:
|
|
15
|
+
"""A compiled spec module with extracted metadata."""
|
|
16
|
+
|
|
17
|
+
fork: str
|
|
18
|
+
module_path: Path
|
|
19
|
+
constants: dict[str, str | int]
|
|
20
|
+
functions: list[str]
|
|
21
|
+
types: list[str]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def compile_specs(consensus_dir: Path, output_dir: Path) -> list[CompiledSpec]:
|
|
25
|
+
"""
|
|
26
|
+
Compile consensus specs using the spec build process.
|
|
27
|
+
|
|
28
|
+
The consensus-specs repo has a build system that compiles
|
|
29
|
+
the markdown + python into executable modules.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
consensus_dir: Path to cloned consensus-specs repo
|
|
33
|
+
output_dir: Where to store compiled output
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
List of compiled spec metadata
|
|
37
|
+
"""
|
|
38
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
|
|
40
|
+
# Check if pyspec can be built
|
|
41
|
+
setup_py = consensus_dir / "setup.py"
|
|
42
|
+
if not setup_py.exists():
|
|
43
|
+
logger.warning("setup.py not found, using fallback extraction")
|
|
44
|
+
return _extract_specs_fallback(consensus_dir, output_dir)
|
|
45
|
+
|
|
46
|
+
# Try to build pyspec
|
|
47
|
+
try:
|
|
48
|
+
result = subprocess.run(
|
|
49
|
+
["pip", "install", "-e", ".[test]"],
|
|
50
|
+
cwd=consensus_dir,
|
|
51
|
+
capture_output=True,
|
|
52
|
+
text=True,
|
|
53
|
+
timeout=300,
|
|
54
|
+
)
|
|
55
|
+
if result.returncode != 0:
|
|
56
|
+
logger.warning("pyspec build failed: %s", result.stderr)
|
|
57
|
+
return _extract_specs_fallback(consensus_dir, output_dir)
|
|
58
|
+
|
|
59
|
+
return _extract_from_pyspec(consensus_dir, output_dir)
|
|
60
|
+
|
|
61
|
+
except subprocess.TimeoutExpired:
|
|
62
|
+
logger.warning("pyspec build timed out, using fallback")
|
|
63
|
+
return _extract_specs_fallback(consensus_dir, output_dir)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.warning("pyspec build error: %s, using fallback", e)
|
|
66
|
+
return _extract_specs_fallback(consensus_dir, output_dir)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _extract_from_pyspec(consensus_dir: Path, output_dir: Path) -> list[CompiledSpec]:
|
|
70
|
+
"""Extract metadata from built pyspec modules."""
|
|
71
|
+
compiled = []
|
|
72
|
+
|
|
73
|
+
# Import and inspect pyspec modules
|
|
74
|
+
try:
|
|
75
|
+
import importlib
|
|
76
|
+
import inspect
|
|
77
|
+
|
|
78
|
+
forks = ["phase0", "altair", "bellatrix", "capella", "deneb", "electra", "fulu"]
|
|
79
|
+
|
|
80
|
+
for fork in forks:
|
|
81
|
+
try:
|
|
82
|
+
module = importlib.import_module(f"eth2spec.{fork}.mainnet")
|
|
83
|
+
|
|
84
|
+
constants = {}
|
|
85
|
+
functions = []
|
|
86
|
+
types = []
|
|
87
|
+
|
|
88
|
+
for name, obj in inspect.getmembers(module):
|
|
89
|
+
if name.startswith("_"):
|
|
90
|
+
continue
|
|
91
|
+
if inspect.isfunction(obj):
|
|
92
|
+
functions.append(name)
|
|
93
|
+
elif inspect.isclass(obj):
|
|
94
|
+
types.append(name)
|
|
95
|
+
elif name.isupper(): # Constants are uppercase
|
|
96
|
+
constants[name] = obj
|
|
97
|
+
|
|
98
|
+
# Save to JSON
|
|
99
|
+
spec_data = {
|
|
100
|
+
"fork": fork,
|
|
101
|
+
"constants": {k: str(v) for k, v in constants.items()},
|
|
102
|
+
"functions": functions,
|
|
103
|
+
"types": types,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
output_file = output_dir / f"{fork}_spec.json"
|
|
107
|
+
with open(output_file, "w") as f:
|
|
108
|
+
json.dump(spec_data, f, indent=2)
|
|
109
|
+
|
|
110
|
+
compiled.append(
|
|
111
|
+
CompiledSpec(
|
|
112
|
+
fork=fork,
|
|
113
|
+
module_path=output_file,
|
|
114
|
+
constants=constants,
|
|
115
|
+
functions=functions,
|
|
116
|
+
types=types,
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
logger.info(
|
|
120
|
+
"Extracted %s: %d constants, %d functions",
|
|
121
|
+
fork, len(constants), len(functions)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
except ImportError:
|
|
125
|
+
logger.warning("Could not import %s spec", fork)
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
except ImportError:
|
|
129
|
+
logger.warning("eth2spec not available, using fallback")
|
|
130
|
+
return _extract_specs_fallback(consensus_dir, output_dir)
|
|
131
|
+
|
|
132
|
+
return compiled
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _extract_specs_fallback(consensus_dir: Path, output_dir: Path) -> list[CompiledSpec]:
|
|
136
|
+
"""
|
|
137
|
+
Fallback: extract specs directly from markdown/python files.
|
|
138
|
+
|
|
139
|
+
This doesn't give us executable code but extracts constants,
|
|
140
|
+
function signatures, and type definitions.
|
|
141
|
+
"""
|
|
142
|
+
import re
|
|
143
|
+
|
|
144
|
+
compiled = []
|
|
145
|
+
specs_dir = consensus_dir / "specs"
|
|
146
|
+
|
|
147
|
+
# Known forks in order
|
|
148
|
+
forks = ["phase0", "altair", "bellatrix", "capella", "deneb", "electra", "fulu"]
|
|
149
|
+
|
|
150
|
+
for fork in forks:
|
|
151
|
+
fork_dir = specs_dir / fork
|
|
152
|
+
if not fork_dir.exists():
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
constants = {}
|
|
156
|
+
functions = []
|
|
157
|
+
types = []
|
|
158
|
+
|
|
159
|
+
# Parse all markdown files in fork directory
|
|
160
|
+
for md_file in fork_dir.glob("*.md"):
|
|
161
|
+
content = md_file.read_text()
|
|
162
|
+
|
|
163
|
+
# Extract constants from tables (| Name | Value | pattern)
|
|
164
|
+
const_pattern = r"\|\s*`?([A-Z][A-Z0-9_]+)`?\s*\|\s*`?([^|`]+)`?\s*\|"
|
|
165
|
+
for match in re.finditer(const_pattern, content):
|
|
166
|
+
name, value = match.groups()
|
|
167
|
+
constants[name.strip()] = value.strip()
|
|
168
|
+
|
|
169
|
+
# Extract function definitions
|
|
170
|
+
func_pattern = r"^def\s+([a-z_][a-z0-9_]*)\s*\("
|
|
171
|
+
for match in re.finditer(func_pattern, content, re.MULTILINE):
|
|
172
|
+
functions.append(match.group(1))
|
|
173
|
+
|
|
174
|
+
# Extract class/type definitions
|
|
175
|
+
class_pattern = r"^class\s+([A-Z][a-zA-Z0-9_]*)"
|
|
176
|
+
for match in re.finditer(class_pattern, content, re.MULTILINE):
|
|
177
|
+
types.append(match.group(1))
|
|
178
|
+
|
|
179
|
+
if constants or functions or types:
|
|
180
|
+
spec_data = {
|
|
181
|
+
"fork": fork,
|
|
182
|
+
"constants": constants,
|
|
183
|
+
"functions": list(set(functions)),
|
|
184
|
+
"types": list(set(types)),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
output_file = output_dir / f"{fork}_spec.json"
|
|
188
|
+
with open(output_file, "w") as f:
|
|
189
|
+
json.dump(spec_data, f, indent=2)
|
|
190
|
+
|
|
191
|
+
compiled.append(
|
|
192
|
+
CompiledSpec(
|
|
193
|
+
fork=fork,
|
|
194
|
+
module_path=output_file,
|
|
195
|
+
constants=constants,
|
|
196
|
+
functions=list(set(functions)),
|
|
197
|
+
types=list(set(types)),
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
logger.info(
|
|
201
|
+
"Extracted %s (fallback): %d constants, %d functions",
|
|
202
|
+
fork, len(constants), len(functions)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return compiled
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def get_function_source(consensus_dir: Path, fork: str, function_name: str) -> str | None:
|
|
209
|
+
"""
|
|
210
|
+
Extract the full source code of a function from spec markdown.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
consensus_dir: Path to consensus-specs repo
|
|
214
|
+
fork: Fork name (e.g., 'electra')
|
|
215
|
+
function_name: Name of function to find
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Function source code or None if not found
|
|
219
|
+
"""
|
|
220
|
+
import re
|
|
221
|
+
|
|
222
|
+
# Prevent path traversal - ensure fork doesn't escape specs directory
|
|
223
|
+
specs_dir = (consensus_dir / "specs" / fork).resolve()
|
|
224
|
+
base_specs = (consensus_dir / "specs").resolve()
|
|
225
|
+
if not str(specs_dir).startswith(str(base_specs)):
|
|
226
|
+
logger.warning("Path traversal attempt blocked: %s", fork)
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
if not specs_dir.exists():
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
for md_file in specs_dir.glob("*.md"):
|
|
233
|
+
content = md_file.read_text()
|
|
234
|
+
|
|
235
|
+
# Find function definition and extract full body
|
|
236
|
+
# Functions in specs are in ```python blocks
|
|
237
|
+
# Escape function_name to prevent regex injection (defense in depth)
|
|
238
|
+
safe_name = re.escape(function_name)
|
|
239
|
+
pattern = rf"```python\n(def\s+{safe_name}\s*\([^)]*\)[^`]*?)```"
|
|
240
|
+
match = re.search(pattern, content, re.DOTALL)
|
|
241
|
+
|
|
242
|
+
if match:
|
|
243
|
+
return match.group(1).strip()
|
|
244
|
+
|
|
245
|
+
return None
|