arc-builder-kit 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.
- arc_builder_kit/__init__.py +4 -0
- arc_builder_kit/__main__.py +6 -0
- arc_builder_kit/_paths.py +47 -0
- arc_builder_kit/cli.py +277 -0
- arc_builder_kit/config/arc_testnet.facts.json +31 -0
- arc_builder_kit/doctor.py +936 -0
- arc_builder_kit/examples/agent-commerce-components/components.js +200 -0
- arc_builder_kit/examples/agent-commerce-components/index.html +120 -0
- arc_builder_kit/examples/agent-commerce-flows/flows.js +271 -0
- arc_builder_kit/examples/agent-commerce-flows/index.html +114 -0
- arc_builder_kit/examples/agent-commerce-live/commerce-live.js +190 -0
- arc_builder_kit/examples/agent-commerce-live/index.html +105 -0
- arc_builder_kit/examples/agent-commerce-review-packet/index.html +96 -0
- arc_builder_kit/examples/agent-commerce-review-packet/packet.js +125 -0
- arc_builder_kit/examples/agent-identity-profile-preview/identity.js +126 -0
- arc_builder_kit/examples/agent-identity-profile-preview/index.html +104 -0
- arc_builder_kit/examples/arc-agent-treasury-lab/index.html +152 -0
- arc_builder_kit/examples/arc-agent-treasury-lab/treasury.js +532 -0
- arc_builder_kit/examples/arc-testnet-operator-evidence/evidence.example.json +47 -0
- arc_builder_kit/examples/arc-testnet-wallet-send-gate/index.html +233 -0
- arc_builder_kit/examples/arc-testnet-wallet-send-gate/live-infrastructure-policy.example.json +59 -0
- arc_builder_kit/examples/arc-testnet-wallet-send-gate/wallet-send-gate.js +472 -0
- arc_builder_kit/examples/circle-wallet-integration/index.html +155 -0
- arc_builder_kit/examples/circle-wallet-integration/wallet-lab.js +91 -0
- arc_builder_kit/examples/job-escrow-simulator/index.html +121 -0
- arc_builder_kit/examples/job-escrow-simulator/simulator.js +162 -0
- arc_builder_kit/examples/payment-intent-demo/index.html +132 -0
- arc_builder_kit/examples/payment-intent-playground/index.html +301 -0
- arc_builder_kit/examples/payment-intent-playground/playground.js +835 -0
- arc_builder_kit/examples/payment-intent-receipt-matcher/index.html +157 -0
- arc_builder_kit/examples/payment-intent-receipt-matcher/matcher.js +877 -0
- arc_builder_kit/examples/receipt-verifier-playground/index.html +120 -0
- arc_builder_kit/examples/receipt-verifier-playground/verifier.js +226 -0
- arc_builder_kit/examples/receipt-viewer/index.html +138 -0
- arc_builder_kit/examples/receipt-viewer/receipt-viewer.js +472 -0
- arc_builder_kit/examples/transaction-status-playground/index.html +135 -0
- arc_builder_kit/examples/transaction-status-playground/status.js +518 -0
- arc_builder_kit/examples/x402-local-challenge-server/.env.example +25 -0
- arc_builder_kit/examples/x402-local-challenge-server/README.md +111 -0
- arc_builder_kit/examples/x402-local-challenge-server/server.py +711 -0
- arc_builder_kit/mcp_server.py +463 -0
- arc_builder_kit/release_packet.py +469 -0
- arc_builder_kit/templates/README.md +25 -0
- arc_builder_kit/templates/job-escrow-starter/README.md +25 -0
- arc_builder_kit/templates/job-escrow-starter/index.html +41 -0
- arc_builder_kit/templates/job-escrow-starter/index.js +14 -0
- arc_builder_kit/templates/payment-intent-starter/README.md +25 -0
- arc_builder_kit/templates/payment-intent-starter/index.html +42 -0
- arc_builder_kit/templates/payment-intent-starter/index.js +7 -0
- arc_builder_kit/templates/x402-agent-starter/README.md +29 -0
- arc_builder_kit/templates/x402-agent-starter/server.py +201 -0
- arc_builder_kit/validate_repo.py +2212 -0
- arc_builder_kit-0.2.0.dist-info/METADATA +543 -0
- arc_builder_kit-0.2.0.dist-info/RECORD +58 -0
- arc_builder_kit-0.2.0.dist-info/WHEEL +5 -0
- arc_builder_kit-0.2.0.dist-info/entry_points.txt +3 -0
- arc_builder_kit-0.2.0.dist-info/licenses/LICENSE +21 -0
- arc_builder_kit-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Arc Builder MCP server (stdio).
|
|
3
|
+
|
|
4
|
+
Exposes the Arc MCP Builder Assistant kit as MCP tools over stdio JSON-RPC.
|
|
5
|
+
The server is dependency-free and stays local-only by default. It does not
|
|
6
|
+
connect wallets, sign, broadcast, or handle secrets. Network calls happen only
|
|
7
|
+
when a tool argument explicitly requests an opt-in read-only check.
|
|
8
|
+
|
|
9
|
+
Supported JSON-RPC methods:
|
|
10
|
+
- initialize
|
|
11
|
+
- tools/list
|
|
12
|
+
- tools/call
|
|
13
|
+
|
|
14
|
+
Supported tools:
|
|
15
|
+
- arc_builder_doctor
|
|
16
|
+
- list_templates
|
|
17
|
+
- scaffold_project
|
|
18
|
+
- validate_repo
|
|
19
|
+
- get_arc_testnet_facts
|
|
20
|
+
- x402_manifest
|
|
21
|
+
- generate_release_packet
|
|
22
|
+
- list_examples
|
|
23
|
+
|
|
24
|
+
Each tool result contains both human-readable `content` and `structuredContent`
|
|
25
|
+
so MCP clients can render text or consume JSON.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import shutil
|
|
32
|
+
import subprocess
|
|
33
|
+
import sys
|
|
34
|
+
from contextlib import redirect_stderr, redirect_stdout
|
|
35
|
+
from io import StringIO
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Any, Callable
|
|
38
|
+
|
|
39
|
+
from arc_builder_kit import __version__
|
|
40
|
+
from arc_builder_kit._paths import (
|
|
41
|
+
CONFIG_DIR,
|
|
42
|
+
DEFAULT_OUTPUT_ROOT,
|
|
43
|
+
EXAMPLES_DIR,
|
|
44
|
+
TEMPLATES_DIR,
|
|
45
|
+
)
|
|
46
|
+
from arc_builder_kit.doctor import main as doctor_main
|
|
47
|
+
from arc_builder_kit.release_packet import main as release_packet_main
|
|
48
|
+
from arc_builder_kit.validate_repo import main as validate_main
|
|
49
|
+
|
|
50
|
+
X402_SERVER = EXAMPLES_DIR / "x402-local-challenge-server" / "server.py"
|
|
51
|
+
|
|
52
|
+
PROTOCOL_VERSION = "2024-11-05"
|
|
53
|
+
SERVER_NAME = "arc-builder-mcp"
|
|
54
|
+
SERVER_VERSION = __version__
|
|
55
|
+
MAX_REQUEST_BYTES = 1_000_000
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class McpError(Exception):
|
|
59
|
+
def __init__(self, code: int, message: str, data: Any = None) -> None:
|
|
60
|
+
super().__init__(message)
|
|
61
|
+
self.code = code
|
|
62
|
+
self.message = message
|
|
63
|
+
self.data = data
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _json_response(id: Any, result: Any) -> dict[str, Any]:
|
|
67
|
+
return {"jsonrpc": "2.0", "id": id, "result": result}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _json_error(id: Any, code: int, message: str, data: Any = None) -> dict[str, Any]:
|
|
71
|
+
error: dict[str, Any] = {"code": code, "message": message}
|
|
72
|
+
if data is not None:
|
|
73
|
+
error["data"] = data
|
|
74
|
+
return {"jsonrpc": "2.0", "id": id, "error": error}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _run_script(
|
|
78
|
+
script: Path,
|
|
79
|
+
args: list[str],
|
|
80
|
+
*,
|
|
81
|
+
timeout: float = 120,
|
|
82
|
+
check: bool = False,
|
|
83
|
+
) -> subprocess.CompletedProcess[str]:
|
|
84
|
+
if not script.exists():
|
|
85
|
+
raise McpError(-32603, f"script not found: {script}")
|
|
86
|
+
return subprocess.run(
|
|
87
|
+
[sys.executable, str(script), *args],
|
|
88
|
+
capture_output=True,
|
|
89
|
+
text=True,
|
|
90
|
+
timeout=timeout,
|
|
91
|
+
check=check,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _load_json(path: Path) -> Any:
|
|
96
|
+
if not path.exists():
|
|
97
|
+
raise McpError(-32603, f"file not found: {path}")
|
|
98
|
+
try:
|
|
99
|
+
with path.open("r", encoding="utf-8") as fh:
|
|
100
|
+
return json.load(fh)
|
|
101
|
+
except json.JSONDecodeError as exc:
|
|
102
|
+
raise McpError(-32603, f"invalid JSON: {exc}") from exc
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _list_templates() -> list[str]:
|
|
106
|
+
if not TEMPLATES_DIR.exists():
|
|
107
|
+
return []
|
|
108
|
+
return sorted(
|
|
109
|
+
d.name for d in TEMPLATES_DIR.iterdir() if d.is_dir() and (d / "README.md").exists()
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _tool_text(text: str, structured: Any) -> dict[str, Any]:
|
|
114
|
+
return {
|
|
115
|
+
"content": [{"type": "text", "text": text}],
|
|
116
|
+
"structuredContent": structured,
|
|
117
|
+
"isError": False,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _tool_error(text: str, structured: Any | None = None) -> dict[str, Any]:
|
|
122
|
+
return {
|
|
123
|
+
"content": [{"type": "text", "text": text}],
|
|
124
|
+
"structuredContent": structured,
|
|
125
|
+
"isError": True,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def tool_initialize(_params: dict[str, Any]) -> dict[str, Any]:
|
|
130
|
+
return {
|
|
131
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
132
|
+
"serverInfo": {
|
|
133
|
+
"name": SERVER_NAME,
|
|
134
|
+
"version": SERVER_VERSION,
|
|
135
|
+
},
|
|
136
|
+
"capabilities": {"tools": {}},
|
|
137
|
+
"safety": {
|
|
138
|
+
"localOnlyDefault": True,
|
|
139
|
+
"noWallet": True,
|
|
140
|
+
"noSigning": True,
|
|
141
|
+
"noBroadcast": True,
|
|
142
|
+
"testnetOnly": True,
|
|
143
|
+
"noSecrets": True,
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def tool_list_templates(_params: dict[str, Any]) -> dict[str, Any]:
|
|
149
|
+
names = _list_templates()
|
|
150
|
+
titles: dict[str, str] = {}
|
|
151
|
+
for name in names:
|
|
152
|
+
readme = TEMPLATES_DIR / name / "README.md"
|
|
153
|
+
if readme.exists():
|
|
154
|
+
first = readme.read_text(encoding="utf-8").splitlines()[0].lstrip("# ").strip()
|
|
155
|
+
titles[name] = first
|
|
156
|
+
return _tool_text(
|
|
157
|
+
f"Available templates: {', '.join(names) if names else '(none)'}",
|
|
158
|
+
{"templates": names, "titles": titles, "count": len(names)},
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def tool_scaffold_project(params: dict[str, Any]) -> dict[str, Any]:
|
|
163
|
+
template = params.get("template")
|
|
164
|
+
output = params.get("output")
|
|
165
|
+
force = bool(params.get("force", False))
|
|
166
|
+
if not isinstance(template, str) or not template:
|
|
167
|
+
return _tool_error("missing or invalid 'template' argument")
|
|
168
|
+
if not isinstance(output, str) or not output:
|
|
169
|
+
return _tool_error("missing or invalid 'output' argument")
|
|
170
|
+
available = _list_templates()
|
|
171
|
+
if template not in available:
|
|
172
|
+
return _tool_error(
|
|
173
|
+
f"unknown template: {template}; available: {', '.join(available)}",
|
|
174
|
+
{"available": available},
|
|
175
|
+
)
|
|
176
|
+
source = TEMPLATES_DIR / template
|
|
177
|
+
dest = Path(output).expanduser().resolve()
|
|
178
|
+
if dest.exists() and not force:
|
|
179
|
+
return _tool_error(
|
|
180
|
+
f"output already exists: {dest}; set force=true to overwrite",
|
|
181
|
+
{"exists": str(dest)},
|
|
182
|
+
)
|
|
183
|
+
if dest.exists() and force:
|
|
184
|
+
shutil.rmtree(dest)
|
|
185
|
+
shutil.copytree(source, dest)
|
|
186
|
+
return _tool_text(
|
|
187
|
+
f"Scaffolded '{template}' to {dest}",
|
|
188
|
+
{"template": template, "output": str(dest), "force": force},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def tool_validate_repo(_params: dict[str, Any]) -> dict[str, Any]:
|
|
193
|
+
try:
|
|
194
|
+
validate_main()
|
|
195
|
+
ok = True
|
|
196
|
+
text = "Repository validation passed."
|
|
197
|
+
except SystemExit as exc:
|
|
198
|
+
ok = False
|
|
199
|
+
text = f"Repository validation failed: {exc}"
|
|
200
|
+
return _tool_text(text, {"ok": ok})
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def tool_arc_builder_doctor(params: dict[str, Any]) -> dict[str, Any]:
|
|
204
|
+
args = ["--json"]
|
|
205
|
+
if params.get("full"):
|
|
206
|
+
args.append("--full")
|
|
207
|
+
if params.get("include_arc_rpc"):
|
|
208
|
+
args.append("--include-arc-rpc")
|
|
209
|
+
if params.get("include_public_site"):
|
|
210
|
+
args.append("--include-public-site")
|
|
211
|
+
old_stdout = sys.stdout
|
|
212
|
+
try:
|
|
213
|
+
sys.stdout = StringIO()
|
|
214
|
+
rc = doctor_main(args)
|
|
215
|
+
stdout = sys.stdout.getvalue()
|
|
216
|
+
finally:
|
|
217
|
+
sys.stdout = old_stdout
|
|
218
|
+
try:
|
|
219
|
+
report = json.loads(stdout) if stdout.strip() else {}
|
|
220
|
+
except json.JSONDecodeError:
|
|
221
|
+
report = {"raw": stdout, "error": "doctor output was not valid JSON"}
|
|
222
|
+
ok = rc == 0 and report.get("status") in ("pass", "warn")
|
|
223
|
+
return _tool_text(
|
|
224
|
+
f"Doctor report status: {report.get('status', 'unknown')}",
|
|
225
|
+
{"ok": ok, "report": report},
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def tool_get_arc_testnet_facts(_params: dict[str, Any]) -> dict[str, Any]:
|
|
230
|
+
facts = _load_json(CONFIG_DIR / "arc_testnet.facts.json")
|
|
231
|
+
return _tool_text(
|
|
232
|
+
f"Arc Testnet chain ID: {facts.get('chainId', 'unknown')}",
|
|
233
|
+
facts,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def tool_x402_manifest(_params: dict[str, Any]) -> dict[str, Any]:
|
|
238
|
+
result = _run_script(X402_SERVER, ["--print-manifest"], timeout=60)
|
|
239
|
+
try:
|
|
240
|
+
manifest = json.loads(result.stdout) if result.stdout.strip() else {}
|
|
241
|
+
except json.JSONDecodeError:
|
|
242
|
+
manifest = {"raw": result.stdout}
|
|
243
|
+
ok = result.returncode == 0
|
|
244
|
+
return _tool_text(
|
|
245
|
+
"x402 manifest retrieved." if ok else "x402 manifest failed.",
|
|
246
|
+
{"ok": ok, "manifest": manifest, "stderr": result.stderr},
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def tool_generate_release_packet(params: dict[str, Any]) -> dict[str, Any]:
|
|
251
|
+
output = params.get("output")
|
|
252
|
+
force = bool(params.get("force", False))
|
|
253
|
+
out = (
|
|
254
|
+
Path(output).expanduser().resolve()
|
|
255
|
+
if isinstance(output, str) and output
|
|
256
|
+
else DEFAULT_OUTPUT_ROOT / ".arc-release-packet"
|
|
257
|
+
)
|
|
258
|
+
argv = ["--out", str(out)]
|
|
259
|
+
if force:
|
|
260
|
+
argv.append("--force")
|
|
261
|
+
stdout_buffer = StringIO()
|
|
262
|
+
stderr_buffer = StringIO()
|
|
263
|
+
with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer):
|
|
264
|
+
try:
|
|
265
|
+
rc = release_packet_main(argv)
|
|
266
|
+
except SystemExit as exc:
|
|
267
|
+
rc = int(exc.code) if isinstance(exc.code, int) else 1
|
|
268
|
+
stdout = stdout_buffer.getvalue()
|
|
269
|
+
stderr = stderr_buffer.getvalue()
|
|
270
|
+
ok = rc == 0
|
|
271
|
+
structured: dict[str, Any] = {"ok": ok, "output": str(out), "force": force}
|
|
272
|
+
if ok:
|
|
273
|
+
files = sorted(p.name for p in out.iterdir()) if out.exists() else []
|
|
274
|
+
structured["files"] = files
|
|
275
|
+
return _tool_text(
|
|
276
|
+
f"Generated release packet in {out} ({len(files)} files).",
|
|
277
|
+
structured,
|
|
278
|
+
)
|
|
279
|
+
structured["stdout"] = stdout
|
|
280
|
+
structured["stderr"] = stderr
|
|
281
|
+
detail = stderr.strip() or stdout.strip() or f"exit {rc}"
|
|
282
|
+
return _tool_error(f"Release packet generation failed: {detail}", structured)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def tool_list_examples(_params: dict[str, Any]) -> dict[str, Any]:
|
|
286
|
+
if not EXAMPLES_DIR.exists():
|
|
287
|
+
return _tool_text("No examples directory found.", {"examples": [], "count": 0})
|
|
288
|
+
examples: list[dict[str, str]] = []
|
|
289
|
+
for path in sorted(EXAMPLES_DIR.iterdir()):
|
|
290
|
+
if path.is_dir() and (path / "index.html").exists():
|
|
291
|
+
readme = path / "README.md"
|
|
292
|
+
title = readme.read_text(encoding="utf-8").splitlines()[0].lstrip("# ").strip() if readme.exists() else path.name
|
|
293
|
+
examples.append({"id": path.name, "title": title})
|
|
294
|
+
return _tool_text(
|
|
295
|
+
f"Available examples: {', '.join(e['id'] for e in examples) if examples else '(none)'}",
|
|
296
|
+
{"examples": examples, "count": len(examples)},
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
TOOLS: dict[str, dict[str, Any]] = {
|
|
301
|
+
"arc_builder_doctor": {
|
|
302
|
+
"description": "Run Arc Builder Doctor and return a structured report.",
|
|
303
|
+
"inputSchema": {
|
|
304
|
+
"type": "object",
|
|
305
|
+
"properties": {
|
|
306
|
+
"full": {"type": "boolean", "description": "Run full local verification."},
|
|
307
|
+
"include_arc_rpc": {
|
|
308
|
+
"type": "boolean",
|
|
309
|
+
"description": "Opt-in read-only Arc Testnet RPC check.",
|
|
310
|
+
},
|
|
311
|
+
"include_public_site": {
|
|
312
|
+
"type": "boolean",
|
|
313
|
+
"description": "Opt-in public GitHub Pages health check.",
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
"additionalProperties": False,
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
"list_templates": {
|
|
320
|
+
"description": "List available Arc builder starter templates.",
|
|
321
|
+
"inputSchema": {"type": "object", "additionalProperties": False},
|
|
322
|
+
},
|
|
323
|
+
"scaffold_project": {
|
|
324
|
+
"description": "Copy a starter template into a new project directory.",
|
|
325
|
+
"inputSchema": {
|
|
326
|
+
"type": "object",
|
|
327
|
+
"properties": {
|
|
328
|
+
"template": {"type": "string", "description": "Template name."},
|
|
329
|
+
"output": {"type": "string", "description": "Destination directory."},
|
|
330
|
+
"force": {"type": "boolean", "description": "Overwrite existing directory."},
|
|
331
|
+
},
|
|
332
|
+
"required": ["template", "output"],
|
|
333
|
+
"additionalProperties": False,
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
"validate_repo": {
|
|
337
|
+
"description": "Run repository validation checks.",
|
|
338
|
+
"inputSchema": {"type": "object", "additionalProperties": False},
|
|
339
|
+
},
|
|
340
|
+
"get_arc_testnet_facts": {
|
|
341
|
+
"description": "Return the reviewed Arc Testnet facts object.",
|
|
342
|
+
"inputSchema": {"type": "object", "additionalProperties": False},
|
|
343
|
+
},
|
|
344
|
+
"x402_manifest": {
|
|
345
|
+
"description": "Return the local x402 paid-agent manifest.",
|
|
346
|
+
"inputSchema": {"type": "object", "additionalProperties": False},
|
|
347
|
+
},
|
|
348
|
+
"generate_release_packet": {
|
|
349
|
+
"description": "Generate a local maintainer release packet for PR/release review.",
|
|
350
|
+
"inputSchema": {
|
|
351
|
+
"type": "object",
|
|
352
|
+
"properties": {
|
|
353
|
+
"output": {"type": "string", "description": "Output directory (default: .arc-release-packet/)."},
|
|
354
|
+
"force": {"type": "boolean", "description": "Overwrite existing packet directory."},
|
|
355
|
+
},
|
|
356
|
+
"additionalProperties": False,
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
"list_examples": {
|
|
360
|
+
"description": "List available browser-facing examples in the kit.",
|
|
361
|
+
"inputSchema": {"type": "object", "additionalProperties": False},
|
|
362
|
+
},
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
TOOL_HANDLERS: dict[str, Callable[[dict[str, Any]], dict[str, Any]]] = {
|
|
366
|
+
"arc_builder_doctor": tool_arc_builder_doctor,
|
|
367
|
+
"list_templates": tool_list_templates,
|
|
368
|
+
"scaffold_project": tool_scaffold_project,
|
|
369
|
+
"validate_repo": tool_validate_repo,
|
|
370
|
+
"get_arc_testnet_facts": tool_get_arc_testnet_facts,
|
|
371
|
+
"x402_manifest": tool_x402_manifest,
|
|
372
|
+
"generate_release_packet": tool_generate_release_packet,
|
|
373
|
+
"list_examples": tool_list_examples,
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def handle_initialize(id: Any, _params: Any) -> dict[str, Any]:
|
|
378
|
+
return _json_response(id, tool_initialize({}))
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def handle_tools_list(id: Any, _params: Any) -> dict[str, Any]:
|
|
382
|
+
tools = [
|
|
383
|
+
{"name": name, "description": spec["description"], "inputSchema": spec["inputSchema"]}
|
|
384
|
+
for name, spec in TOOLS.items()
|
|
385
|
+
]
|
|
386
|
+
return _json_response(id, {"tools": tools})
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def handle_tools_call(id: Any, params: Any) -> dict[str, Any]:
|
|
390
|
+
if not isinstance(params, dict):
|
|
391
|
+
return _json_error(id, -32602, "invalid params: expected object")
|
|
392
|
+
name = params.get("name")
|
|
393
|
+
arguments = params.get("arguments") or {}
|
|
394
|
+
if not isinstance(name, str) or not name:
|
|
395
|
+
return _json_error(id, -32602, "missing or invalid tool name")
|
|
396
|
+
if not isinstance(arguments, dict):
|
|
397
|
+
return _json_error(id, -32602, "invalid arguments: expected object")
|
|
398
|
+
if name not in TOOL_HANDLERS:
|
|
399
|
+
return _json_error(id, -32601, f"unknown tool: {name}")
|
|
400
|
+
try:
|
|
401
|
+
result = TOOL_HANDLERS[name](arguments)
|
|
402
|
+
except McpError as exc:
|
|
403
|
+
return _json_error(id, exc.code, exc.message, exc.data)
|
|
404
|
+
except subprocess.TimeoutExpired:
|
|
405
|
+
return _json_error(id, -32000, f"tool '{name}' timed out")
|
|
406
|
+
except Exception as exc: # noqa: BLE001 - catch-all for tool safety
|
|
407
|
+
return _json_error(id, -32603, f"tool '{name}' failed: {exc}")
|
|
408
|
+
return _json_response(id, result)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def handle_request(request: dict[str, Any]) -> dict[str, Any] | None:
|
|
412
|
+
if request.get("jsonrpc") != "2.0":
|
|
413
|
+
return _json_error(request.get("id"), -32600, "invalid JSON-RPC version")
|
|
414
|
+
id = request.get("id")
|
|
415
|
+
method = request.get("method")
|
|
416
|
+
params = request.get("params", {})
|
|
417
|
+
if not isinstance(method, str):
|
|
418
|
+
return _json_error(id, -32600, "invalid method")
|
|
419
|
+
if method == "initialize":
|
|
420
|
+
return handle_initialize(id, params)
|
|
421
|
+
if method == "tools/list":
|
|
422
|
+
return handle_tools_list(id, params)
|
|
423
|
+
if method == "tools/call":
|
|
424
|
+
return handle_tools_call(id, params)
|
|
425
|
+
return _json_error(id, -32601, f"method not found: {method}")
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def main() -> int:
|
|
429
|
+
while True:
|
|
430
|
+
try:
|
|
431
|
+
line = sys.stdin.readline()
|
|
432
|
+
except KeyboardInterrupt:
|
|
433
|
+
break
|
|
434
|
+
if not line:
|
|
435
|
+
break
|
|
436
|
+
line = line.strip()
|
|
437
|
+
if not line:
|
|
438
|
+
continue
|
|
439
|
+
try:
|
|
440
|
+
if len(line.encode("utf-8")) > MAX_REQUEST_BYTES:
|
|
441
|
+
response = _json_error(None, -32700, "request too large")
|
|
442
|
+
print(json.dumps(response), flush=True)
|
|
443
|
+
continue
|
|
444
|
+
request = json.loads(line)
|
|
445
|
+
except json.JSONDecodeError as exc:
|
|
446
|
+
response = _json_error(None, -32700, f"parse error: {exc}")
|
|
447
|
+
print(json.dumps(response), flush=True)
|
|
448
|
+
continue
|
|
449
|
+
if not isinstance(request, dict):
|
|
450
|
+
response = _json_error(None, -32600, "invalid request")
|
|
451
|
+
print(json.dumps(response), flush=True)
|
|
452
|
+
continue
|
|
453
|
+
if "id" not in request:
|
|
454
|
+
# JSON-RPC notification: consume silently.
|
|
455
|
+
continue
|
|
456
|
+
response = handle_request(request)
|
|
457
|
+
if response is not None:
|
|
458
|
+
print(json.dumps(response), flush=True)
|
|
459
|
+
return 0
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
if __name__ == "__main__":
|
|
463
|
+
sys.exit(main())
|