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.
Files changed (58) hide show
  1. arc_builder_kit/__init__.py +4 -0
  2. arc_builder_kit/__main__.py +6 -0
  3. arc_builder_kit/_paths.py +47 -0
  4. arc_builder_kit/cli.py +277 -0
  5. arc_builder_kit/config/arc_testnet.facts.json +31 -0
  6. arc_builder_kit/doctor.py +936 -0
  7. arc_builder_kit/examples/agent-commerce-components/components.js +200 -0
  8. arc_builder_kit/examples/agent-commerce-components/index.html +120 -0
  9. arc_builder_kit/examples/agent-commerce-flows/flows.js +271 -0
  10. arc_builder_kit/examples/agent-commerce-flows/index.html +114 -0
  11. arc_builder_kit/examples/agent-commerce-live/commerce-live.js +190 -0
  12. arc_builder_kit/examples/agent-commerce-live/index.html +105 -0
  13. arc_builder_kit/examples/agent-commerce-review-packet/index.html +96 -0
  14. arc_builder_kit/examples/agent-commerce-review-packet/packet.js +125 -0
  15. arc_builder_kit/examples/agent-identity-profile-preview/identity.js +126 -0
  16. arc_builder_kit/examples/agent-identity-profile-preview/index.html +104 -0
  17. arc_builder_kit/examples/arc-agent-treasury-lab/index.html +152 -0
  18. arc_builder_kit/examples/arc-agent-treasury-lab/treasury.js +532 -0
  19. arc_builder_kit/examples/arc-testnet-operator-evidence/evidence.example.json +47 -0
  20. arc_builder_kit/examples/arc-testnet-wallet-send-gate/index.html +233 -0
  21. arc_builder_kit/examples/arc-testnet-wallet-send-gate/live-infrastructure-policy.example.json +59 -0
  22. arc_builder_kit/examples/arc-testnet-wallet-send-gate/wallet-send-gate.js +472 -0
  23. arc_builder_kit/examples/circle-wallet-integration/index.html +155 -0
  24. arc_builder_kit/examples/circle-wallet-integration/wallet-lab.js +91 -0
  25. arc_builder_kit/examples/job-escrow-simulator/index.html +121 -0
  26. arc_builder_kit/examples/job-escrow-simulator/simulator.js +162 -0
  27. arc_builder_kit/examples/payment-intent-demo/index.html +132 -0
  28. arc_builder_kit/examples/payment-intent-playground/index.html +301 -0
  29. arc_builder_kit/examples/payment-intent-playground/playground.js +835 -0
  30. arc_builder_kit/examples/payment-intent-receipt-matcher/index.html +157 -0
  31. arc_builder_kit/examples/payment-intent-receipt-matcher/matcher.js +877 -0
  32. arc_builder_kit/examples/receipt-verifier-playground/index.html +120 -0
  33. arc_builder_kit/examples/receipt-verifier-playground/verifier.js +226 -0
  34. arc_builder_kit/examples/receipt-viewer/index.html +138 -0
  35. arc_builder_kit/examples/receipt-viewer/receipt-viewer.js +472 -0
  36. arc_builder_kit/examples/transaction-status-playground/index.html +135 -0
  37. arc_builder_kit/examples/transaction-status-playground/status.js +518 -0
  38. arc_builder_kit/examples/x402-local-challenge-server/.env.example +25 -0
  39. arc_builder_kit/examples/x402-local-challenge-server/README.md +111 -0
  40. arc_builder_kit/examples/x402-local-challenge-server/server.py +711 -0
  41. arc_builder_kit/mcp_server.py +463 -0
  42. arc_builder_kit/release_packet.py +469 -0
  43. arc_builder_kit/templates/README.md +25 -0
  44. arc_builder_kit/templates/job-escrow-starter/README.md +25 -0
  45. arc_builder_kit/templates/job-escrow-starter/index.html +41 -0
  46. arc_builder_kit/templates/job-escrow-starter/index.js +14 -0
  47. arc_builder_kit/templates/payment-intent-starter/README.md +25 -0
  48. arc_builder_kit/templates/payment-intent-starter/index.html +42 -0
  49. arc_builder_kit/templates/payment-intent-starter/index.js +7 -0
  50. arc_builder_kit/templates/x402-agent-starter/README.md +29 -0
  51. arc_builder_kit/templates/x402-agent-starter/server.py +201 -0
  52. arc_builder_kit/validate_repo.py +2212 -0
  53. arc_builder_kit-0.2.0.dist-info/METADATA +543 -0
  54. arc_builder_kit-0.2.0.dist-info/RECORD +58 -0
  55. arc_builder_kit-0.2.0.dist-info/WHEEL +5 -0
  56. arc_builder_kit-0.2.0.dist-info/entry_points.txt +3 -0
  57. arc_builder_kit-0.2.0.dist-info/licenses/LICENSE +21 -0
  58. 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())