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/__init__.py +3 -0
- maco/_build_info.py +4 -0
- maco/cli.py +258 -0
- maco/codegen.py +680 -0
- maco/config.py +177 -0
- maco/gateway.py +305 -0
- maco/mcp_manager.py +157 -0
- maco/runner.py +104 -0
- maco/sandbox/__init__.py +43 -0
- maco/sandbox/core.py +216 -0
- maco/sandbox/providers/__init__.py +7 -0
- maco/sandbox/providers/base.py +69 -0
- maco/sandbox/providers/docker.py +228 -0
- maco/sandbox/providers/local.py +46 -0
- maco/sandbox/providers/matchlock.py +224 -0
- maco/serve_mcp.py +527 -0
- maco/templates/bash_description.j2 +8 -0
- maco/templates/code_execute_description.j2 +14 -0
- maco/templates/codegen/client.py.j2 +104 -0
- maco/templates/codegen/model.py.j2 +6 -0
- maco/templates/codegen/package_init.py.j2 +2 -0
- maco/templates/codegen/pyproject.toml.j2 +8 -0
- maco/templates/codegen/root_model.py.j2 +3 -0
- maco/templates/codegen/server_init.py.j2 +11 -0
- maco/templates/codegen/tool.py.j2 +38 -0
- maco/templates/codegen/type_alias.py.j2 +2 -0
- maco/templates/serve_mcp_instructions.j2 +17 -0
- maco/templates/server_catalog.j2 +8 -0
- maco/version.py +72 -0
- mcp_as_code-0.1.0.dist-info/METADATA +212 -0
- mcp_as_code-0.1.0.dist-info/RECORD +34 -0
- mcp_as_code-0.1.0.dist-info/WHEEL +4 -0
- mcp_as_code-0.1.0.dist-info/entry_points.txt +2 -0
- mcp_as_code-0.1.0.dist-info/licenses/LICENSE +203 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Local subprocess sandbox provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from ..core import SandboxContext, SandboxExec, SandboxRunResult
|
|
10
|
+
from .base import BaseSandboxProvider
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LocalSandboxProvider(BaseSandboxProvider):
|
|
14
|
+
"""Run commands as local subprocesses with maco env injected."""
|
|
15
|
+
|
|
16
|
+
default_python_command = "uv run python"
|
|
17
|
+
guest_workspace: str
|
|
18
|
+
guest_scratch: str
|
|
19
|
+
|
|
20
|
+
def __init__(self, context: SandboxContext) -> None:
|
|
21
|
+
super().__init__(context)
|
|
22
|
+
self.guest_workspace = str(self.context.workspace)
|
|
23
|
+
self.guest_scratch = str(self.context.scratch)
|
|
24
|
+
|
|
25
|
+
def run(self, request: SandboxExec) -> SandboxRunResult:
|
|
26
|
+
self.context.scratch.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
env = os.environ.copy()
|
|
28
|
+
injected = self._guest_env(request.env, gateway_url=self.context.gateway.url)
|
|
29
|
+
existing_pythonpath = env.get("PYTHONPATH")
|
|
30
|
+
if existing_pythonpath:
|
|
31
|
+
injected["PYTHONPATH"] = os.pathsep.join([self.guest_workspace, existing_pythonpath])
|
|
32
|
+
env.update(injected)
|
|
33
|
+
command = ["sh", "-lc", request.command]
|
|
34
|
+
completed = subprocess.run(
|
|
35
|
+
command,
|
|
36
|
+
cwd=str(self.context.scratch),
|
|
37
|
+
env=env,
|
|
38
|
+
text=True,
|
|
39
|
+
stdout=subprocess.PIPE,
|
|
40
|
+
stderr=subprocess.PIPE,
|
|
41
|
+
timeout=self._timeout(request),
|
|
42
|
+
check=False,
|
|
43
|
+
)
|
|
44
|
+
if self.context.debug:
|
|
45
|
+
print(f"maco local command: {command!r}", file=sys.stderr)
|
|
46
|
+
return SandboxRunResult(completed.returncode, completed.stdout, completed.stderr)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Matchlock sandbox provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shlex
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
9
|
+
|
|
10
|
+
from ..core import (
|
|
11
|
+
SANDBOX_SDK_ROOT,
|
|
12
|
+
SANDBOX_USER,
|
|
13
|
+
SandboxContext,
|
|
14
|
+
SandboxError,
|
|
15
|
+
SandboxExec,
|
|
16
|
+
SandboxRunResult,
|
|
17
|
+
translate_loopback_url,
|
|
18
|
+
)
|
|
19
|
+
from .base import RemoteSandboxProvider
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MatchlockSandboxProvider(RemoteSandboxProvider):
|
|
23
|
+
"""Run commands inside one long-lived Matchlock micro-VM."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
context: SandboxContext,
|
|
28
|
+
*,
|
|
29
|
+
image: str,
|
|
30
|
+
matchlock_binary: str = "matchlock",
|
|
31
|
+
gateway_host: str = "maco-gateway.internal",
|
|
32
|
+
gateway_ip: str | None = None,
|
|
33
|
+
extra_allow_hosts: list[str] | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
super().__init__(context)
|
|
36
|
+
self.image = image
|
|
37
|
+
self.matchlock_binary = matchlock_binary
|
|
38
|
+
self.gateway_host = gateway_host
|
|
39
|
+
self.gateway_ip = gateway_ip
|
|
40
|
+
self.extra_allow_hosts = extra_allow_hosts or []
|
|
41
|
+
self.client: Any | None = None
|
|
42
|
+
self.gateway_url = ""
|
|
43
|
+
self.allowed_hosts: list[str] = []
|
|
44
|
+
self.gateway_mapping: tuple[str, str] | None = None
|
|
45
|
+
|
|
46
|
+
def start(self) -> None:
|
|
47
|
+
if self.client is not None:
|
|
48
|
+
return
|
|
49
|
+
Client, Config, Sandbox = _load_matchlock_sdk()
|
|
50
|
+
self.gateway_url = _matchlock_gateway_url(
|
|
51
|
+
self.context.gateway.url,
|
|
52
|
+
gateway_host=self.gateway_host,
|
|
53
|
+
gateway_ip=self.gateway_ip,
|
|
54
|
+
)
|
|
55
|
+
env = self._guest_env({}, gateway_url=self.gateway_url)
|
|
56
|
+
gateway_policy_host = _url_host(self.gateway_url) or self.gateway_host
|
|
57
|
+
self.gateway_mapping = (gateway_policy_host, self.gateway_ip) if self.gateway_ip else None
|
|
58
|
+
if self.gateway_mapping is not None and self.extra_allow_hosts:
|
|
59
|
+
raise SandboxError(
|
|
60
|
+
"matchlock extra allow hosts cannot be combined with a mapped local gateway yet; "
|
|
61
|
+
"Matchlock currently proxies all HTTP traffic when allow-hosts are configured"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Matchlock applies the image-config user from launch to later exec
|
|
65
|
+
# calls when no per-exec user is supplied, so every client.exec below
|
|
66
|
+
# runs as the same unprivileged sandbox user.
|
|
67
|
+
spec = (
|
|
68
|
+
Sandbox(self.image)
|
|
69
|
+
.with_workspace(self.guest_scratch)
|
|
70
|
+
.with_user(SANDBOX_USER)
|
|
71
|
+
.with_env_map(env)
|
|
72
|
+
)
|
|
73
|
+
if self.gateway_mapping is not None:
|
|
74
|
+
spec.add_host(*self.gateway_mapping)
|
|
75
|
+
self.allowed_hosts = [*self.extra_allow_hosts]
|
|
76
|
+
if self.gateway_mapping is None:
|
|
77
|
+
self.allowed_hosts.append(gateway_policy_host)
|
|
78
|
+
for host in sorted(set(self.allowed_hosts)):
|
|
79
|
+
spec.allow_host(host)
|
|
80
|
+
if self.context.gateway.token and self.gateway_mapping is None:
|
|
81
|
+
placeholder = "MACO_GATEWAY_TOKEN_PLACEHOLDER"
|
|
82
|
+
spec.with_env("MACO_GATEWAY_TOKEN", placeholder)
|
|
83
|
+
spec.add_secret_with_placeholder(
|
|
84
|
+
"MACO_GATEWAY_TOKEN",
|
|
85
|
+
self.context.gateway.token,
|
|
86
|
+
placeholder,
|
|
87
|
+
gateway_policy_host,
|
|
88
|
+
)
|
|
89
|
+
spec.mount_memory(self.guest_scratch)
|
|
90
|
+
|
|
91
|
+
config = Config(binary_path=self.matchlock_binary)
|
|
92
|
+
client = Client(config)
|
|
93
|
+
try:
|
|
94
|
+
client.start()
|
|
95
|
+
client.launch(spec)
|
|
96
|
+
self.client = client
|
|
97
|
+
self._bootstrap_sdk()
|
|
98
|
+
except BaseException:
|
|
99
|
+
try:
|
|
100
|
+
client.close()
|
|
101
|
+
finally:
|
|
102
|
+
try:
|
|
103
|
+
client.remove()
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
self.client = None
|
|
107
|
+
raise
|
|
108
|
+
|
|
109
|
+
def stop(self) -> None:
|
|
110
|
+
if self.client is None:
|
|
111
|
+
return
|
|
112
|
+
client = self.client
|
|
113
|
+
self.client = None
|
|
114
|
+
try:
|
|
115
|
+
client.close()
|
|
116
|
+
finally:
|
|
117
|
+
try:
|
|
118
|
+
client.remove()
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
def run(self, request: SandboxExec) -> SandboxRunResult:
|
|
123
|
+
self.start()
|
|
124
|
+
assert self.client is not None
|
|
125
|
+
result = self.client.exec(
|
|
126
|
+
request.command,
|
|
127
|
+
working_dir=self.guest_scratch,
|
|
128
|
+
timeout=self._timeout(request),
|
|
129
|
+
)
|
|
130
|
+
if self.context.debug:
|
|
131
|
+
summary = _sdk_command_summary(
|
|
132
|
+
self.matchlock_binary,
|
|
133
|
+
self.image,
|
|
134
|
+
request.command,
|
|
135
|
+
gateway_url=self.gateway_url,
|
|
136
|
+
allowed_hosts=sorted(set(self.allowed_hosts)),
|
|
137
|
+
gateway_mapping=self.gateway_mapping,
|
|
138
|
+
)
|
|
139
|
+
print(
|
|
140
|
+
f"maco matchlock command: {summary!r}",
|
|
141
|
+
file=sys.stderr,
|
|
142
|
+
)
|
|
143
|
+
return SandboxRunResult(result.exit_code, result.stdout, result.stderr)
|
|
144
|
+
|
|
145
|
+
def write_file(self, relative_path: str, content: str) -> str:
|
|
146
|
+
self.start()
|
|
147
|
+
assert self.client is not None
|
|
148
|
+
guest_path = self._guest_scratch_path(relative_path)
|
|
149
|
+
parent = guest_path.rsplit("/", 1)[0]
|
|
150
|
+
self.client.exec(
|
|
151
|
+
f"mkdir -p {shlex.quote(parent)}",
|
|
152
|
+
working_dir=self.guest_scratch,
|
|
153
|
+
timeout=self.context.timeout,
|
|
154
|
+
)
|
|
155
|
+
self.client.write_file(guest_path, content)
|
|
156
|
+
return guest_path
|
|
157
|
+
|
|
158
|
+
def _bootstrap_sdk(self) -> None:
|
|
159
|
+
assert self.client is not None
|
|
160
|
+
result = self.client.exec(
|
|
161
|
+
f"maco sandbox-bootstrap --workspace {shlex.quote(SANDBOX_SDK_ROOT)}",
|
|
162
|
+
working_dir=self.guest_scratch,
|
|
163
|
+
timeout=self.context.timeout,
|
|
164
|
+
)
|
|
165
|
+
if result.exit_code != 0:
|
|
166
|
+
raise SandboxError(f"failed to bootstrap Matchlock sandbox SDK: {result.stderr.strip()}")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _sdk_command_summary(
|
|
170
|
+
binary: str,
|
|
171
|
+
image: str,
|
|
172
|
+
command: str,
|
|
173
|
+
*,
|
|
174
|
+
gateway_url: str,
|
|
175
|
+
allowed_hosts: list[str],
|
|
176
|
+
gateway_mapping: tuple[str, str] | None = None,
|
|
177
|
+
) -> list[str]:
|
|
178
|
+
"""Return a non-secret summary for debug logs."""
|
|
179
|
+
|
|
180
|
+
summary: list[str] = [
|
|
181
|
+
binary,
|
|
182
|
+
"rpc",
|
|
183
|
+
"launch",
|
|
184
|
+
image,
|
|
185
|
+
"exec",
|
|
186
|
+
command,
|
|
187
|
+
f"MACO_GATEWAY_URL={gateway_url}",
|
|
188
|
+
]
|
|
189
|
+
if gateway_mapping is not None:
|
|
190
|
+
host, ip = gateway_mapping
|
|
191
|
+
summary.append(f"hosts={ip}:{host}")
|
|
192
|
+
for host in allowed_hosts:
|
|
193
|
+
summary.append(f"allow_host={host}")
|
|
194
|
+
return summary
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _url_host(url: str) -> str | None:
|
|
198
|
+
return urlsplit(url).hostname
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _matchlock_gateway_url(url: str, *, gateway_host: str, gateway_ip: str | None) -> str:
|
|
202
|
+
translated = translate_loopback_url(url, gateway_host)
|
|
203
|
+
if gateway_ip and _url_host(translated) in {gateway_ip, "0.0.0.0"}:
|
|
204
|
+
return _replace_url_host(translated, gateway_host)
|
|
205
|
+
return translated
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _replace_url_host(url: str, host: str) -> str:
|
|
209
|
+
parts = urlsplit(url)
|
|
210
|
+
netloc = host
|
|
211
|
+
if parts.port is not None:
|
|
212
|
+
netloc = f"{host}:{parts.port}"
|
|
213
|
+
return urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _load_matchlock_sdk() -> tuple[type[Any], type[Any], type[Any]]:
|
|
217
|
+
try:
|
|
218
|
+
from matchlock import Client, Config, Sandbox
|
|
219
|
+
except ImportError as exc: # pragma: no cover - depends on optional package availability
|
|
220
|
+
raise SandboxError(
|
|
221
|
+
"matchlock provider requires the Matchlock Python SDK; "
|
|
222
|
+
"install `maco-sandbox[matchlock]` or `matchlock`"
|
|
223
|
+
) from exc
|
|
224
|
+
return Client, Config, Sandbox
|