mcp-as-code 0.1.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.
maco/serve_mcp.py ADDED
@@ -0,0 +1,527 @@
1
+ """HTTP MCP server for running maco code inside a sandbox provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import hashlib
7
+ import json
8
+ from pathlib import Path
9
+ import signal
10
+ import subprocess
11
+ import sys
12
+ import threading
13
+ from types import FrameType
14
+ from typing import Annotated, Any
15
+
16
+ from jinja2 import Environment, PackageLoader, StrictUndefined
17
+ from mcp.server.fastmcp import FastMCP
18
+ from pydantic import Field
19
+
20
+ from .codegen import fetch_gateway_tools, generate_sandbox_sdk, server_module_names
21
+ from .config import load_config
22
+ from .gateway import GatewayServer, ServeOptions
23
+ from .sandbox import (
24
+ DEFAULT_MATCHLOCK_GATEWAY_IP,
25
+ GatewayInfo,
26
+ SandboxContext,
27
+ SandboxExec,
28
+ SandboxProvider,
29
+ SandboxRunResult,
30
+ provider_from_name,
31
+ )
32
+
33
+
34
+ _TEMPLATES = Environment(
35
+ loader=PackageLoader("maco", "templates"),
36
+ trim_blocks=True,
37
+ lstrip_blocks=True,
38
+ undefined=StrictUndefined,
39
+ )
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class ServeMcpOptions:
44
+ """Configuration for the sandbox-backed MCP server."""
45
+
46
+ config: str | Path = "mcp.json"
47
+ provider: str = "local"
48
+ workspace: str | Path = ".maco"
49
+ clean: bool = False
50
+ scratch: str | Path | None = None
51
+ gateway_file: str | Path | None = None
52
+ gateway_host: str | None = None
53
+ gateway_port: int = 0
54
+ gateway_token: str | None = None
55
+ gateway_use_token: bool = True
56
+ host: str = "127.0.0.1"
57
+ port: int = 8789
58
+ timeout: int = 60
59
+ debug: bool = False
60
+ image: str | None = None
61
+ python_command: str | None = None
62
+ docker_binary: str = "docker"
63
+ docker_network: str | None = None
64
+ docker_gateway_host: str = "host.docker.internal"
65
+ docker_gateway_ip: str | None = None
66
+ matchlock_binary: str = "matchlock"
67
+ matchlock_gateway_host: str = "maco-gateway.internal"
68
+ matchlock_gateway_ip: str | None = None
69
+ matchlock_allow_host: tuple[str, ...] = ()
70
+
71
+
72
+ class _ServeMcpShutdown(KeyboardInterrupt):
73
+ """Raised from signal handlers so provider cleanup can run."""
74
+
75
+
76
+ def _install_shutdown_signal_handlers() -> tuple[tuple[int, Any], ...]:
77
+ if threading.current_thread() is not threading.main_thread():
78
+ return ()
79
+
80
+ def _request_shutdown(signum: int, _frame: FrameType | None) -> None:
81
+ print(f"\nreceived signal {signum}; stopping maco serve-mcp", file=sys.stderr)
82
+ raise _ServeMcpShutdown
83
+
84
+ return (
85
+ (signal.SIGINT, signal.signal(signal.SIGINT, _request_shutdown)),
86
+ (signal.SIGTERM, signal.signal(signal.SIGTERM, _request_shutdown)),
87
+ )
88
+
89
+
90
+ def _restore_signal_handlers(handlers: tuple[tuple[int, Any], ...]) -> None:
91
+ if threading.current_thread() is not threading.main_thread():
92
+ return
93
+ for signum, handler in handlers:
94
+ signal.signal(signum, handler)
95
+
96
+
97
+ def serve_mcp(options: ServeMcpOptions) -> None:
98
+ """Run a streamable HTTP MCP server exposing sandboxed bash/code tools."""
99
+
100
+ gateway_server: GatewayServer | None = None
101
+ provider: SandboxProvider | None = None
102
+ old_signal_handlers = _install_shutdown_signal_handlers()
103
+ try:
104
+ workspace = Path(options.workspace).expanduser().resolve()
105
+ scratch = (
106
+ Path(options.scratch).expanduser().resolve()
107
+ if options.scratch is not None
108
+ else workspace.parent / "maco-serve-mcp"
109
+ )
110
+ gateway_file = (
111
+ Path(options.gateway_file).expanduser().resolve()
112
+ if options.gateway_file is not None
113
+ else workspace / "gateway.json"
114
+ )
115
+ normalized_provider = _normalize_provider(options.provider)
116
+ managed_gateway = options.gateway_file is None
117
+ docker_gateway_ip = (
118
+ _docker_gateway_ip(
119
+ options.docker_gateway_ip,
120
+ managed_gateway=managed_gateway,
121
+ docker_binary=options.docker_binary,
122
+ docker_network=options.docker_network,
123
+ )
124
+ if normalized_provider == "docker"
125
+ else None
126
+ )
127
+ matchlock_gateway_ip = (
128
+ _matchlock_gateway_ip(
129
+ options.matchlock_gateway_ip,
130
+ managed_gateway=managed_gateway,
131
+ gateway_file=gateway_file,
132
+ )
133
+ if normalized_provider == "matchlock"
134
+ else None
135
+ )
136
+ if options.gateway_file is None:
137
+ config = load_config(options.config)
138
+ gateway_host = options.gateway_host or _default_gateway_host()
139
+ extra_hosts = _gateway_extra_hosts(
140
+ normalized_provider,
141
+ docker_gateway_ip=docker_gateway_ip,
142
+ matchlock_gateway_ip=matchlock_gateway_ip,
143
+ explicit_gateway_host=options.gateway_host,
144
+ )
145
+ gateway_server = GatewayServer(
146
+ config,
147
+ ServeOptions(
148
+ host=gateway_host,
149
+ port=options.gateway_port,
150
+ workspace=workspace,
151
+ token=options.gateway_token,
152
+ use_token=options.gateway_use_token,
153
+ extra_hosts=extra_hosts,
154
+ freebind_hosts=_gateway_freebind_hosts(
155
+ gateway_host,
156
+ extra_hosts=extra_hosts,
157
+ docker_gateway_ip=docker_gateway_ip,
158
+ matchlock_gateway_ip=matchlock_gateway_ip,
159
+ ),
160
+ ),
161
+ ).start()
162
+ gateway_file = gateway_server.gateway_file
163
+ print("maco gateway started")
164
+ print(f" URL: {gateway_server.url}")
165
+ for url in gateway_server.extra_urls:
166
+ print(f" extra URL: {url}")
167
+ print(f" gateway file: {gateway_file}")
168
+ gateway = GatewayInfo.from_file(gateway_file)
169
+ tools_by_server = fetch_gateway_tools(gateway.url, token=gateway.token)
170
+ modules = sorted(server_module_names(tools_by_server.keys()).values())
171
+ if options.provider.replace("_", "-").lower() == "local":
172
+ _clean_local_sdk(workspace, clean=options.clean)
173
+ stats = generate_sandbox_sdk(tools_by_server, workspace=workspace, clean=False)
174
+ print(f"Generated local sandbox SDK with {stats.tool_count} tools from {stats.server_count} servers")
175
+ print(f"SDK workspace: {stats.workspace}")
176
+ context = SandboxContext(
177
+ workspace=workspace,
178
+ scratch=scratch,
179
+ gateway=gateway,
180
+ timeout=options.timeout,
181
+ python_command=options.python_command,
182
+ debug=options.debug,
183
+ )
184
+ provider = provider_from_name(
185
+ options.provider,
186
+ context,
187
+ image=options.image,
188
+ docker_binary=options.docker_binary,
189
+ docker_network=options.docker_network,
190
+ docker_gateway_host=options.docker_gateway_host,
191
+ docker_gateway_ip=docker_gateway_ip,
192
+ matchlock_binary=options.matchlock_binary,
193
+ matchlock_gateway_host=options.matchlock_gateway_host,
194
+ matchlock_gateway_ip=matchlock_gateway_ip,
195
+ matchlock_extra_allow_hosts=list(options.matchlock_allow_host),
196
+ )
197
+ provider.start()
198
+ app = create_serve_mcp_app(provider, context, server_modules=modules, host=options.host, port=options.port)
199
+ print("maco serve-mcp started")
200
+ print(f" URL: http://{options.host}:{options.port}/mcp")
201
+ print(f" provider: {options.provider}")
202
+ print(f" SDK: {provider.guest_workspace}")
203
+ print(f" scratch: {scratch}")
204
+ app.run("streamable-http")
205
+ except _ServeMcpShutdown:
206
+ pass
207
+ finally:
208
+ _restore_signal_handlers(old_signal_handlers)
209
+ try:
210
+ if provider is not None:
211
+ provider.stop()
212
+ finally:
213
+ if gateway_server is not None:
214
+ gateway_server.stop()
215
+
216
+
217
+ def create_serve_mcp_app(
218
+ provider: SandboxProvider,
219
+ context: SandboxContext,
220
+ *,
221
+ server_modules: list[str] | None = None,
222
+ host: str = "127.0.0.1",
223
+ port: int = 8789,
224
+ ) -> FastMCP:
225
+ """Create the MCP app used by ``serve_mcp``.
226
+
227
+ Separated for tests and future embedding. Tools intentionally return plain
228
+ JSON-serializable dictionaries so MCP clients can inspect exit status,
229
+ stdout, and stderr.
230
+ """
231
+
232
+ app = FastMCP(
233
+ "maco-serve-mcp",
234
+ instructions=_mcp_instructions(provider, context, server_modules=server_modules),
235
+ host=host,
236
+ port=port,
237
+ json_response=True,
238
+ stateless_http=True,
239
+ )
240
+
241
+ @app.tool(description=_bash_description(provider, context, server_modules=server_modules))
242
+ def bash(
243
+ command: Annotated[
244
+ str,
245
+ Field(description="Non-interactive shell command to run in the sandbox scratch directory."),
246
+ ],
247
+ timeout: Annotated[
248
+ int | None,
249
+ Field(description="Optional command timeout in seconds. Omit to use the server default."),
250
+ ] = None,
251
+ ) -> dict[str, Any]:
252
+ result = provider.run(SandboxExec(command=command, timeout=timeout))
253
+ return _result_payload(result)
254
+
255
+ @app.tool(description=_code_execute_description(provider, context, server_modules=server_modules))
256
+ def code_execute(
257
+ code: Annotated[
258
+ str,
259
+ Field(
260
+ description=(
261
+ "Python source code to write into the sandbox scratch directory and execute. "
262
+ "Import generated tools from tools.<server>."
263
+ )
264
+ ),
265
+ ],
266
+ filename: Annotated[
267
+ str | None,
268
+ Field(
269
+ description=(
270
+ "Optional relative .py path in scratch. Leave null to use <hash>.py."
271
+ )
272
+ ),
273
+ ] = None,
274
+ args: Annotated[
275
+ list[str] | None,
276
+ Field(
277
+ description=(
278
+ "Optional command-line arguments passed as sys.argv[1:]. "
279
+ "Leave null for no arguments."
280
+ )
281
+ ),
282
+ ] = None,
283
+ timeout: Annotated[
284
+ int | None,
285
+ Field(description="Optional script timeout in seconds. Omit to use the server default."),
286
+ ] = None,
287
+ ) -> dict[str, Any]:
288
+ filename = filename if filename is not None else _content_addressed_script_filename(code)
289
+ guest_script = provider.write_file(filename, code)
290
+ command = provider.python_script_command(guest_script, args or [])
291
+ result = provider.run(SandboxExec(command=command, timeout=timeout))
292
+ payload = _result_payload(result)
293
+ payload["script"] = guest_script
294
+ payload["guest_script"] = guest_script
295
+ return payload
296
+
297
+ return app
298
+
299
+
300
+ def _mcp_instructions(
301
+ provider: SandboxProvider,
302
+ context: SandboxContext,
303
+ *,
304
+ server_modules: list[str] | None = None,
305
+ ) -> str:
306
+ return _render_model_text("serve_mcp_instructions.j2", provider, context, server_modules=server_modules)
307
+
308
+
309
+ def _bash_description(
310
+ provider: SandboxProvider,
311
+ context: SandboxContext,
312
+ *,
313
+ server_modules: list[str] | None = None,
314
+ ) -> str:
315
+ return _render_model_text("bash_description.j2", provider, context, server_modules=server_modules)
316
+
317
+
318
+ def _code_execute_description(
319
+ provider: SandboxProvider,
320
+ context: SandboxContext,
321
+ *,
322
+ server_modules: list[str] | None = None,
323
+ ) -> str:
324
+ return _render_model_text("code_execute_description.j2", provider, context, server_modules=server_modules)
325
+
326
+
327
+ def _render_model_text(
328
+ template_name: str,
329
+ provider: SandboxProvider,
330
+ context: SandboxContext,
331
+ *,
332
+ server_modules: list[str] | None = None,
333
+ ) -> str:
334
+ wrapper_root = _guest_tools_root(provider)
335
+ return _TEMPLATES.get_template(template_name).render(
336
+ server_modules=server_modules if server_modules is not None else _server_modules(context.workspace),
337
+ wrapper_root=wrapper_root,
338
+ ).strip()
339
+
340
+
341
+ def _server_modules(workspace: Path) -> list[str]:
342
+ server_root = workspace / "tools"
343
+ return sorted(
344
+ path.name
345
+ for path in server_root.iterdir()
346
+ if path.is_dir() and (path / "__init__.py").exists()
347
+ ) if server_root.exists() else []
348
+
349
+
350
+ def _guest_tools_root(provider: SandboxProvider) -> str:
351
+ return f"{provider.guest_workspace.rstrip('/')}/tools"
352
+
353
+
354
+ def _clean_local_sdk(workspace: Path, *, clean: bool) -> None:
355
+ if not clean:
356
+ return
357
+ for path in [workspace / "tools", workspace / "manifest.json", workspace / "pyproject.toml"]:
358
+ if path.is_dir():
359
+ import shutil
360
+
361
+ shutil.rmtree(path)
362
+ elif path.exists():
363
+ path.unlink()
364
+
365
+
366
+ def _content_addressed_script_filename(code: str) -> str:
367
+ digest = hashlib.sha256(code.encode("utf-8")).hexdigest()[:16]
368
+ return f"{digest}.py"
369
+
370
+
371
+ def _default_gateway_host() -> str:
372
+ return "127.0.0.1"
373
+
374
+
375
+ def _normalize_provider(provider: str) -> str:
376
+ return provider.replace("_", "-").lower()
377
+
378
+
379
+ def _matchlock_gateway_ip(
380
+ configured_ip: str | None,
381
+ *,
382
+ managed_gateway: bool,
383
+ gateway_file: Path,
384
+ ) -> str | None:
385
+ if configured_ip:
386
+ return configured_ip
387
+ if managed_gateway:
388
+ return DEFAULT_MATCHLOCK_GATEWAY_IP
389
+ try:
390
+ gateway = GatewayInfo.from_file(gateway_file)
391
+ except Exception:
392
+ return None
393
+ host = _url_host(gateway.url)
394
+ if host == DEFAULT_MATCHLOCK_GATEWAY_IP:
395
+ return DEFAULT_MATCHLOCK_GATEWAY_IP
396
+ return None
397
+
398
+
399
+ def _docker_gateway_ip(
400
+ configured_ip: str | None,
401
+ *,
402
+ managed_gateway: bool,
403
+ docker_binary: str,
404
+ docker_network: str | None,
405
+ ) -> str | None:
406
+ if configured_ip:
407
+ return configured_ip
408
+ if not managed_gateway:
409
+ return None
410
+ if _is_docker_desktop(docker_binary):
411
+ return None
412
+ detected_ip = _detect_docker_gateway_ip(docker_binary, docker_network)
413
+ if detected_ip:
414
+ return detected_ip
415
+ network = docker_network or "bridge"
416
+ raise ValueError(
417
+ f"could not detect Docker gateway IP for network {network!r}; "
418
+ "pass --docker-gateway-ip explicitly"
419
+ )
420
+
421
+
422
+ def _is_docker_desktop(docker_binary: str) -> bool:
423
+ if not sys.platform.startswith("linux"):
424
+ return True
425
+ try:
426
+ completed = subprocess.run(
427
+ [docker_binary, "info", "--format", "{{.OperatingSystem}}"],
428
+ text=True,
429
+ stdout=subprocess.PIPE,
430
+ stderr=subprocess.DEVNULL,
431
+ timeout=5,
432
+ check=False,
433
+ )
434
+ except Exception:
435
+ return False
436
+ if completed.returncode != 0:
437
+ return False
438
+ return "docker desktop" in completed.stdout.strip().lower()
439
+
440
+
441
+ def _detect_docker_gateway_ip(docker_binary: str, docker_network: str | None) -> str | None:
442
+ network = docker_network or "bridge"
443
+ try:
444
+ completed = subprocess.run(
445
+ [docker_binary, "network", "inspect", network],
446
+ text=True,
447
+ stdout=subprocess.PIPE,
448
+ stderr=subprocess.DEVNULL,
449
+ timeout=5,
450
+ check=False,
451
+ )
452
+ except Exception:
453
+ return None
454
+ if completed.returncode != 0:
455
+ return None
456
+ try:
457
+ networks = json.loads(completed.stdout)
458
+ except json.JSONDecodeError:
459
+ return None
460
+ if not isinstance(networks, list) or not networks or not isinstance(networks[0], dict):
461
+ return None
462
+ ipam = networks[0].get("IPAM")
463
+ configs = ipam.get("Config") if isinstance(ipam, dict) else None
464
+ if not isinstance(configs, list):
465
+ return None
466
+ for config in configs:
467
+ if not isinstance(config, dict):
468
+ continue
469
+ gateway = config.get("Gateway")
470
+ if isinstance(gateway, str) and gateway and "." in gateway:
471
+ return gateway
472
+ return None
473
+
474
+
475
+ def _gateway_extra_hosts(
476
+ provider: str,
477
+ *,
478
+ docker_gateway_ip: str | None,
479
+ matchlock_gateway_ip: str | None,
480
+ explicit_gateway_host: str | None,
481
+ ) -> tuple[str, ...]:
482
+ gateway_ip = _provider_gateway_ip(provider, docker_gateway_ip, matchlock_gateway_ip)
483
+ if not gateway_ip:
484
+ return ()
485
+ if explicit_gateway_host and explicit_gateway_host not in {"127.0.0.1", "localhost", "::1"}:
486
+ return ()
487
+ return (gateway_ip,)
488
+
489
+
490
+ def _gateway_freebind_hosts(
491
+ gateway_host: str,
492
+ *,
493
+ extra_hosts: tuple[str, ...],
494
+ docker_gateway_ip: str | None,
495
+ matchlock_gateway_ip: str | None,
496
+ ) -> tuple[str, ...]:
497
+ hosts = list(extra_hosts)
498
+ if gateway_host in {docker_gateway_ip, matchlock_gateway_ip}:
499
+ hosts.append(gateway_host)
500
+ return tuple(dict.fromkeys(hosts))
501
+
502
+
503
+ def _provider_gateway_ip(
504
+ provider: str,
505
+ docker_gateway_ip: str | None,
506
+ matchlock_gateway_ip: str | None,
507
+ ) -> str | None:
508
+ if provider == "docker":
509
+ return docker_gateway_ip
510
+ if provider == "matchlock":
511
+ return matchlock_gateway_ip
512
+ return None
513
+
514
+
515
+ def _url_host(url: str) -> str | None:
516
+ from urllib.parse import urlsplit
517
+
518
+ return urlsplit(url).hostname
519
+
520
+
521
+ def _result_payload(result: SandboxRunResult) -> dict[str, Any]:
522
+ return {
523
+ "ok": result.ok,
524
+ "exit_code": result.exit_code,
525
+ "stdout": result.stdout,
526
+ "stderr": result.stderr,
527
+ }
@@ -0,0 +1,8 @@
1
+ Run a non-interactive shell command in the sandbox.
2
+
3
+ Use this to inspect generated tool files, search for available tool exports, or run small shell/Python probes before using code_execute.
4
+
5
+ Useful discovery commands:
6
+ rg --files {{ wrapper_root }}
7
+ fd . {{ wrapper_root }} -t f
8
+ rg "^def " {{ wrapper_root }}/<server>
@@ -0,0 +1,14 @@
1
+ Run Python code in the sandbox. Use this for multi-step MCP work, loops, filtering, joins, or structured output.
2
+
3
+ For most tasks, pass only the code argument. If filename is omitted, maco writes the code to a deterministic content-addressed path like <hash>.py under the writable scratch directory. Use filename only when a readable traceback path or stable named script matters. Use args only when the script explicitly reads command-line arguments.
4
+
5
+ Import generated tools with:
6
+ from tools.<server> import <tool>
7
+
8
+ {% include "server_catalog.j2" %}
9
+
10
+ Inspect wrapper code at:
11
+ {{ wrapper_root }}/<server>/__init__.py
12
+ {{ wrapper_root }}/<server>/<tool>.py
13
+
14
+ Use the bash tool with rg/fd for discovery before writing code when needed.
@@ -0,0 +1,104 @@
1
+ # This file is automatically generated by maco. Do not edit.
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Any
9
+ from urllib.error import HTTPError, URLError
10
+ from urllib.request import Request, urlopen
11
+
12
+
13
+ _CURRENT_DIR = Path(__file__).resolve().parent
14
+ _WORKSPACE_DIR = _CURRENT_DIR.parent
15
+
16
+
17
+ def call_mcp_tool(
18
+ server_name: str,
19
+ tool_name: str,
20
+ arguments: dict[str, Any] | None = None,
21
+ *,
22
+ timeout: float | None = 60.0,
23
+ ) -> Any:
24
+ """Call an MCP tool through a running maco gateway.
25
+
26
+ The gateway is discovered from MACO_GATEWAY_URL or from gateway.json in the
27
+ generated workspace. Structured MCP output is returned directly when
28
+ available; otherwise text content is returned, with a best-effort JSON parse.
29
+ """
30
+
31
+ gateway = _load_gateway()
32
+ url = gateway.get("url")
33
+ if not url:
34
+ raise RuntimeError(
35
+ "maco gateway URL is not configured. Start `maco serve` and run code with `maco run`, "
36
+ "or set MACO_GATEWAY_URL."
37
+ )
38
+
39
+ payload = json.dumps(
40
+ {
41
+ "server": server_name,
42
+ "tool": tool_name,
43
+ "arguments": arguments or {},
44
+ }
45
+ ).encode("utf-8")
46
+ headers = {"Content-Type": "application/json"}
47
+ token = os.environ.get("MACO_GATEWAY_TOKEN") or gateway.get("token")
48
+ if token:
49
+ headers["Authorization"] = f"Bearer {token}"
50
+
51
+ request = Request(url, data=payload, headers=headers, method="POST")
52
+ try:
53
+ with urlopen(request, timeout=timeout) as response:
54
+ raw = response.read().decode("utf-8")
55
+ except HTTPError as exc:
56
+ detail = exc.read().decode("utf-8", errors="replace")
57
+ raise RuntimeError(f"MCP tool {server_name}.{tool_name} failed: HTTP {exc.code}: {detail}") from exc
58
+ except URLError as exc:
59
+ raise RuntimeError(f"failed to connect to maco gateway at {url}: {exc}") from exc
60
+
61
+ result = json.loads(raw)
62
+ if result.get("isError"):
63
+ raise RuntimeError(f"MCP tool {server_name}.{tool_name} returned an error: {_content_text(result)}")
64
+ if "structuredContent" in result:
65
+ return result["structuredContent"]
66
+ text = _content_text(result)
67
+ try:
68
+ return json.loads(text)
69
+ except Exception:
70
+ return text
71
+
72
+
73
+ def _load_gateway() -> dict[str, Any]:
74
+ gateway: dict[str, Any] = {}
75
+ gateway_file = os.environ.get("MACO_GATEWAY_FILE")
76
+ if gateway_file:
77
+ gateway.update(_read_json(Path(gateway_file)))
78
+ else:
79
+ gateway.update(_read_json(_WORKSPACE_DIR / "gateway.json"))
80
+ if os.environ.get("MACO_GATEWAY_URL"):
81
+ gateway["url"] = os.environ["MACO_GATEWAY_URL"]
82
+ if os.environ.get("MACO_GATEWAY_TOKEN"):
83
+ gateway["token"] = os.environ["MACO_GATEWAY_TOKEN"]
84
+ return gateway
85
+
86
+
87
+ def _read_json(path: Path) -> dict[str, Any]:
88
+ try:
89
+ return json.loads(path.read_text(encoding="utf-8"))
90
+ except FileNotFoundError:
91
+ return {}
92
+
93
+
94
+ def _content_text(result: dict[str, Any]) -> str:
95
+ parts: list[str] = []
96
+ for block in result.get("content") or []:
97
+ if isinstance(block, dict) and block.get("type") == "text":
98
+ parts.append(str(block.get("text", "")))
99
+ elif isinstance(block, dict):
100
+ parts.append(json.dumps(block, sort_keys=True))
101
+ else:
102
+ parts.append(str(block))
103
+ return "".join(parts)
104
+
@@ -0,0 +1,6 @@
1
+ class {{ class_name }}(BaseModel):
2
+ model_config = ConfigDict(populate_by_name=True, extra='allow')
3
+
4
+ {% for field in fields %}
5
+ {{ field.name }}: {{ field.type_expr }} = {{ field.field_args }}
6
+ {% endfor %}
@@ -0,0 +1,2 @@
1
+ """{{ docstring }}"""
2
+
@@ -0,0 +1,8 @@
1
+ [project]
2
+ name = "maco-generated-workspace"
3
+ version = "0.0.0"
4
+ requires-python = ">=3.11"
5
+ dependencies = [
6
+ "pydantic>=2.0",
7
+ ]
8
+
@@ -0,0 +1,3 @@
1
+ class {{ class_name }}(RootModel[{{ type_expr }}]):
2
+ pass
3
+
@@ -0,0 +1,11 @@
1
+ # This file is automatically generated by maco. Do not edit.
2
+ {% for name in exports %}
3
+ from .{{ name }} import {{ name }}
4
+ {% endfor %}
5
+
6
+ __all__ = [
7
+ {% for name in exports %}
8
+ {{ name | pyrepr }},
9
+ {% endfor %}
10
+ ]
11
+