mcp-as-code 0.1.4__tar.gz → 0.1.5__tar.gz

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 (42) hide show
  1. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/PKG-INFO +4 -2
  2. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/README.md +1 -1
  3. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/docs/mcp-config.md +1 -1
  4. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/examples/serve-mcp/README.md +6 -6
  5. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/images/sandbox/README.md +3 -3
  6. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/pyproject.toml +3 -3
  7. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/__init__.py +1 -1
  8. mcp_as_code-0.1.5/src/maco/cli.py +445 -0
  9. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/gateway.py +14 -5
  10. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/oauth.py +23 -7
  11. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/sandbox/__init__.py +1 -1
  12. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/sandbox/core.py +9 -17
  13. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/serve_mcp.py +94 -92
  14. mcp_as_code-0.1.5/src/maco/service.py +589 -0
  15. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/codegen/client.py.j2 +1 -2
  16. mcp_as_code-0.1.5/src/maco/version.py +22 -0
  17. mcp_as_code-0.1.4/VERSION.txt +0 -1
  18. mcp_as_code-0.1.4/src/maco/_build_info.py +0 -4
  19. mcp_as_code-0.1.4/src/maco/cli.py +0 -261
  20. mcp_as_code-0.1.4/src/maco/version.py +0 -72
  21. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/.gitignore +0 -0
  22. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/LICENSE +0 -0
  23. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/codegen.py +0 -0
  24. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/config.py +0 -0
  25. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/mcp_manager.py +0 -0
  26. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/runner.py +0 -0
  27. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/sandbox/providers/__init__.py +0 -0
  28. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/sandbox/providers/base.py +0 -0
  29. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/sandbox/providers/docker.py +0 -0
  30. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/sandbox/providers/local.py +0 -0
  31. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/sandbox/providers/matchlock.py +0 -0
  32. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/bash_description.j2 +0 -0
  33. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/code_execute_description.j2 +0 -0
  34. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/codegen/model.py.j2 +0 -0
  35. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/codegen/package_init.py.j2 +0 -0
  36. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/codegen/pyproject.toml.j2 +0 -0
  37. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/codegen/root_model.py.j2 +0 -0
  38. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/codegen/server_init.py.j2 +0 -0
  39. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/codegen/tool.py.j2 +0 -0
  40. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/codegen/type_alias.py.j2 +0 -0
  41. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/serve_mcp_instructions.j2 +0 -0
  42. {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/server_catalog.j2 +0 -0
@@ -1,10 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-as-code
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Execute MCP tools through generated Python code interfaces
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.11
8
+ Requires-Dist: click>=8.1
9
+ Requires-Dist: httpx>=0.28
8
10
  Requires-Dist: jinja2>=3.1
9
11
  Requires-Dist: matchlock==0.2.15
10
12
  Requires-Dist: mcp>=1.24.0
@@ -102,7 +104,7 @@ This config needs `npx` (for Playwright MCP), a GitHub token in `GITHUB_TOKEN`,
102
104
  Start the `maco` MCP server:
103
105
 
104
106
  ```bash
105
- maco serve-mcp --config mcp.json --provider docker
107
+ maco up --config mcp.json --provider docker
106
108
  ```
107
109
 
108
110
  Use `--provider local` for a faster, non-isolated local feedback loop.
@@ -89,7 +89,7 @@ This config needs `npx` (for Playwright MCP), a GitHub token in `GITHUB_TOKEN`,
89
89
  Start the `maco` MCP server:
90
90
 
91
91
  ```bash
92
- maco serve-mcp --config mcp.json --provider docker
92
+ maco up --config mcp.json --provider docker
93
93
  ```
94
94
 
95
95
  Use `--provider local` for a faster, non-isolated local feedback loop.
@@ -1,6 +1,6 @@
1
1
  # MCP configuration
2
2
 
3
- `maco` reads Claude-style MCP configuration files with a top-level `mcpServers` object. The same config is used by `maco gen`, `maco serve`, and `maco serve-mcp`.
3
+ `maco` reads Claude-style MCP configuration files with a top-level `mcpServers` object. The same config is used by `maco gen` and `maco up`.
4
4
 
5
5
  ```json
6
6
  {
@@ -1,6 +1,6 @@
1
- # `maco serve-mcp` example
1
+ # `maco up` example
2
2
 
3
- This example shows how to expose several upstream MCP servers through one compact `maco serve-mcp` endpoint. The upstream servers here are:
3
+ This example shows how to expose several upstream MCP servers through one compact `maco up` endpoint. The upstream servers here are:
4
4
 
5
5
  - [Playwright MCP](https://playwright.dev/mcp/introduction), launched with `npx -y @playwright/mcp@latest`
6
6
  - [GitHub MCP server](https://github.com/github/github-mcp-server), using GitHub's hosted Streamable HTTP endpoint at `https://api.githubcopilot.com/mcp/`
@@ -19,20 +19,20 @@ If you are already authenticated with the GitHub CLI, export a token directly:
19
19
  export GITHUB_TOKEN=$(gh auth token)
20
20
  ```
21
21
 
22
- ## 1. Start `maco serve-mcp`
22
+ ## 1. Start `maco up`
23
23
 
24
24
  The short path is to run from this example directory so the defaults line up with the local files:
25
25
 
26
26
  ```bash
27
27
  cd examples/serve-mcp
28
- uv run maco serve-mcp --provider local
28
+ uv run maco up --provider local
29
29
  ```
30
30
 
31
- This uses `mcp.json`, writes `.maco/gateway.json`, uses `maco-serve-mcp/` as scratch, starts the gateway, and serves HTTP MCP at `http://127.0.0.1:8789/mcp`. Add `--clean` only when you want to recreate the local generated SDK from scratch.
31
+ This uses `mcp.json`, writes `.maco/gateway.json`, uses `.maco/scratch` as the default scratch directory, starts the gateway, and serves HTTP MCP at `http://127.0.0.1:8789/mcp`. Add `--clean` only when you want to recreate the local generated SDK from scratch.
32
32
 
33
33
  ## 2. Connect an agent to the MCP gateway
34
34
 
35
- Configure your MCP client with `mcp-client.json` to connect to `maco serve-mcp`. For example, in your MCP client's settings, set the MCP server URL to `http://127.0.0.1:8789/mcp`.
35
+ Configure your MCP client with `mcp-client.json` to connect to `maco up`. For example, in your MCP client's settings, set the MCP server URL to `http://127.0.0.1:8789/mcp`.
36
36
 
37
37
  If you are using codex you may connect to it via:
38
38
 
@@ -1,20 +1,20 @@
1
1
  # maco sandbox image
2
2
 
3
- This directory contains the Docker image used by the Docker and Matchlock `maco serve-mcp` sandbox providers.
3
+ This directory contains the Docker image used by the Docker and Matchlock `maco up` sandbox providers.
4
4
 
5
5
  The image extends the pinned Alpine uv/Python image with the `maco` CLI, `ripgrep` (`rg`), and `fd`. Remote sandbox providers use the CLI to bootstrap `/workspace/macosdk/tools` from the live gateway catalog at sandbox startup.
6
6
 
7
7
  ## Build
8
8
 
9
9
  ```bash
10
- VERSION="$(cat VERSION.txt)"
10
+ VERSION="$(scripts/package-version)"
11
11
  docker build -f images/sandbox/Dockerfile -t "ghcr.io/jingkaihe/maco:${VERSION}-alpine" .
12
12
  ```
13
13
 
14
14
  ## Import into Matchlock
15
15
 
16
16
  ```bash
17
- VERSION="$(cat VERSION.txt)"
17
+ VERSION="$(scripts/package-version)"
18
18
  docker save "ghcr.io/jingkaihe/maco:${VERSION}-alpine" \
19
19
  | matchlock image import "ghcr.io/jingkaihe/maco:${VERSION}-alpine"
20
20
  ```
@@ -6,6 +6,8 @@ readme = "README.md"
6
6
  license = "Apache-2.0"
7
7
  requires-python = ">=3.11"
8
8
  dependencies = [
9
+ "click>=8.1",
10
+ "httpx>=0.28",
9
11
  "jinja2>=3.1",
10
12
  "matchlock==0.2.15",
11
13
  "mcp>=1.24.0",
@@ -27,8 +29,7 @@ dev = [
27
29
  ]
28
30
 
29
31
  [tool.hatch.version]
30
- path = "VERSION.txt"
31
- pattern = "(?P<version>.+)"
32
+ path = "src/maco/__init__.py"
32
33
 
33
34
  [tool.hatch.build.targets.wheel]
34
35
  packages = ["src/maco"]
@@ -39,6 +40,5 @@ include = [
39
40
  "docs",
40
41
  "README.md",
41
42
  "LICENSE",
42
- "VERSION.txt",
43
43
  "pyproject.toml",
44
44
  ]
@@ -1,3 +1,3 @@
1
1
  """maco Python interface generator and MCP code-execution server."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.5"
@@ -0,0 +1,445 @@
1
+ """Command line interface for maco."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from types import SimpleNamespace
8
+ from typing import Any
9
+
10
+ import click
11
+ from jinja2 import Template
12
+
13
+ from .codegen import generate, generate_sandbox_sdk_from_gateway
14
+ from .config import ConfigError, load_config
15
+ from .gateway import ServeOptions, serve
16
+ from .runner import RunnerError, exit_with_error, run_code
17
+ from .sandbox import DEFAULT_SANDBOX_IMAGE, SANDBOX_SDK_ROOT
18
+ from .serve_mcp import ServeMcpOptions, serve_mcp
19
+ from .service import (
20
+ DEFAULT_MCP_PORT,
21
+ ServiceError,
22
+ SERVICE_ID_ENV,
23
+ SERVICE_TOKEN_ENV,
24
+ ensure_no_detached_service,
25
+ find_available_port,
26
+ list_services,
27
+ show_status,
28
+ start_detached,
29
+ stop_detached,
30
+ )
31
+ from .version import get_version_info
32
+
33
+
34
+ _CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
35
+ _INTERNAL_CONTEXT_SETTINGS = {"help_option_names": []}
36
+ _RUN_CONTEXT_SETTINGS = {
37
+ **_CONTEXT_SETTINGS,
38
+ "allow_extra_args": True,
39
+ "allow_interspersed_args": False,
40
+ }
41
+ _GENERATION_SUMMARY_TEMPLATE = Template(
42
+ """\
43
+ {{ label }} {{ tool_count }} tools from {{ server_count }} servers
44
+ Workspace: {{ workspace }}{% if explore_path %}
45
+ Explore: rg --files {{ explore_path }}{% endif %}
46
+ """
47
+ )
48
+
49
+
50
+ def _serve_mcp_options(*, port_default: int | None = DEFAULT_MCP_PORT) -> Any:
51
+ port_help = (
52
+ "HTTP MCP bind port (default: auto)"
53
+ if port_default is None
54
+ else f"HTTP MCP bind port (default: {port_default})"
55
+ )
56
+
57
+ def decorator(command: Any) -> Any:
58
+ for option in reversed(
59
+ [
60
+ click.option("--config", default="mcp.json", help="MCP config path", show_default=True),
61
+ click.option(
62
+ "--provider",
63
+ type=click.Choice(["local", "docker", "matchlock"]),
64
+ default="local",
65
+ help="sandbox provider to use",
66
+ show_default=True,
67
+ ),
68
+ click.option("--workspace", default=".maco", help="generated workspace directory", show_default=True),
69
+ click.option("--clean", is_flag=True, help="remove the workspace before generating"),
70
+ click.option("--scratch", help="writable scratch directory for sandbox code"),
71
+ click.option("--gateway-host", help="host for the managed gateway started by maco up"),
72
+ click.option(
73
+ "--gateway-port",
74
+ default=0,
75
+ type=int,
76
+ help="port for the managed gateway started by maco up",
77
+ show_default=True,
78
+ ),
79
+ click.option("--gateway-token", help="explicit bearer token for the managed gateway"),
80
+ click.option(
81
+ "--no-gateway-token",
82
+ is_flag=True,
83
+ help="disable bearer-token protection for the managed gateway",
84
+ ),
85
+ click.option("--host", default="127.0.0.1", help="HTTP MCP bind host", show_default=True),
86
+ click.option("--port", default=port_default, type=int, help=port_help),
87
+ click.option("--timeout", default=60, type=int, help="default command timeout in seconds", show_default=True),
88
+ click.option("--debug", is_flag=True, help="log provider command summaries to server stderr"),
89
+ click.option(
90
+ "--image",
91
+ help=f"container image for docker/matchlock providers (default: {DEFAULT_SANDBOX_IMAGE})",
92
+ ),
93
+ click.option("--python-command", help="guest command prefix used by code_execute"),
94
+ click.option("--docker-binary", default="docker", help="docker binary path/name", show_default=True),
95
+ click.option("--docker-network", help="docker network passed to `docker run --network`"),
96
+ click.option(
97
+ "--docker-gateway-host",
98
+ default="host.docker.internal",
99
+ help="hostname inside docker that reaches the host gateway",
100
+ show_default=True,
101
+ ),
102
+ click.option(
103
+ "--docker-gateway-ip",
104
+ help="explicit host gateway IP to map inside Docker; usually auto-detected",
105
+ ),
106
+ click.option(
107
+ "--matchlock-binary",
108
+ default="matchlock",
109
+ help="matchlock binary path/name",
110
+ show_default=True,
111
+ ),
112
+ click.option(
113
+ "--matchlock-gateway-host",
114
+ default="maco-gateway.internal",
115
+ help="hostname inside matchlock that reaches the host gateway",
116
+ show_default=True,
117
+ ),
118
+ click.option(
119
+ "--matchlock-gateway-ip",
120
+ help=(
121
+ "IP for --add-host <gateway-host>:<ip> inside matchlock "
122
+ "(managed default: 192.168.64.1 on macOS, 192.168.100.1 elsewhere)"
123
+ ),
124
+ ),
125
+ click.option(
126
+ "--matchlock-allow-host",
127
+ multiple=True,
128
+ help="extra host to allow from the matchlock sandbox (repeatable)",
129
+ ),
130
+ ]
131
+ ):
132
+ command = option(command)
133
+ return command
134
+
135
+ return decorator
136
+
137
+
138
+ @click.group(
139
+ name="maco",
140
+ context_settings=_CONTEXT_SETTINGS,
141
+ help="Generate and execute Python code interfaces for MCP tools.",
142
+ no_args_is_help=True,
143
+ )
144
+ def app() -> None:
145
+ """maco command group."""
146
+
147
+
148
+ @app.command("version", help="print maco version and release metadata")
149
+ def version_command() -> int:
150
+ return _cmd_version(_namespace(command="version"))
151
+
152
+
153
+ @app.command("gen", help="generate Python wrappers for configured MCP tools")
154
+ @click.option("--config", default="mcp.json", help="MCP config path", show_default=True)
155
+ @click.option("--workspace", default=".maco", help="generated workspace directory", show_default=True)
156
+ @click.option("--server", help="only generate wrappers for one configured server")
157
+ @click.option("--clean", is_flag=True, help="remove the workspace before generating")
158
+ def gen_command(config: str, workspace: str, server: str | None, clean: bool) -> int:
159
+ return _cmd_gen(_namespace(command="gen", config=config, workspace=workspace, server=server, clean=clean))
160
+
161
+
162
+ @app.command("run", context_settings=_RUN_CONTEXT_SETTINGS, help="run a Python file with generated wrappers available")
163
+ @click.option("--workspace", help="generated workspace directory (default: auto-detect)")
164
+ @click.option("--cwd", help="working directory for the script")
165
+ @click.option("--python", help="Python version/interpreter to pass to uv run")
166
+ @click.argument("code_path")
167
+ @click.argument("script_args", nargs=-1, type=click.UNPROCESSED)
168
+ def run_command(
169
+ workspace: str | None,
170
+ cwd: str | None,
171
+ python: str | None,
172
+ code_path: str,
173
+ script_args: tuple[str, ...],
174
+ ) -> int:
175
+ return _cmd_run(
176
+ _namespace(
177
+ command="run",
178
+ workspace=workspace,
179
+ cwd=cwd,
180
+ python=python,
181
+ code_path=code_path,
182
+ script_args=list(script_args),
183
+ )
184
+ )
185
+
186
+
187
+ @app.command("up", help="start the sandboxed maco MCP server")
188
+ @_serve_mcp_options(port_default=None)
189
+ @click.option("-d", "--detach", is_flag=True, help="start maco in the background for this project")
190
+ def up_command(**kwargs: Any) -> int:
191
+ return _cmd_up(_serve_mcp_namespace("up", kwargs))
192
+
193
+
194
+ @app.command("status", help="show detached maco status for this project")
195
+ @click.option("--workspace", default=".maco", help="generated workspace directory", show_default=True)
196
+ def status_command(workspace: str) -> int:
197
+ return _cmd_status(_namespace(command="status", workspace=workspace))
198
+
199
+
200
+ @app.command("down", help="stop detached maco for this project")
201
+ @click.option("--workspace", default=".maco", help="generated workspace directory", show_default=True)
202
+ def down_command(workspace: str) -> int:
203
+ return _cmd_down(_namespace(command="down", workspace=workspace))
204
+
205
+
206
+ @app.command("ls", help="list detached maco processes")
207
+ def ls_command() -> int:
208
+ return _cmd_ls(_namespace(command="ls"))
209
+
210
+
211
+ @app.command("_mcp-server", hidden=True, add_help_option=False, context_settings=_INTERNAL_CONTEXT_SETTINGS)
212
+ @_serve_mcp_options()
213
+ def internal_mcp_server_command(**kwargs: Any) -> int:
214
+ return _cmd_mcp_server(_serve_mcp_namespace("_mcp-server", kwargs))
215
+
216
+
217
+ @app.command("_gateway", hidden=True, add_help_option=False, context_settings=_INTERNAL_CONTEXT_SETTINGS)
218
+ @click.option("--config", default="mcp.json")
219
+ @click.option("--workspace", default=".maco")
220
+ @click.option("--clean", is_flag=True)
221
+ @click.option("--host", default="127.0.0.1")
222
+ @click.option("--port", default=0, type=int)
223
+ @click.option("--token")
224
+ @click.option("--no-token", is_flag=True)
225
+ def internal_gateway_command(
226
+ config: str,
227
+ workspace: str,
228
+ clean: bool,
229
+ host: str,
230
+ port: int,
231
+ token: str | None,
232
+ no_token: bool,
233
+ ) -> int:
234
+ return _cmd_serve(
235
+ _namespace(
236
+ command="_gateway",
237
+ config=config,
238
+ workspace=workspace,
239
+ clean=clean,
240
+ host=host,
241
+ port=port,
242
+ token=token,
243
+ no_token=no_token,
244
+ )
245
+ )
246
+
247
+
248
+ @app.command("sandbox-bootstrap", hidden=True, add_help_option=False, context_settings=_INTERNAL_CONTEXT_SETTINGS)
249
+ @click.option("--gateway-url")
250
+ @click.option("--gateway-token")
251
+ @click.option("--workspace", default=SANDBOX_SDK_ROOT)
252
+ @click.option("--no-clean", is_flag=True)
253
+ def sandbox_bootstrap_command(
254
+ gateway_url: str | None,
255
+ gateway_token: str | None,
256
+ workspace: str,
257
+ no_clean: bool,
258
+ ) -> int:
259
+ return _cmd_sandbox_bootstrap(
260
+ _namespace(
261
+ command="sandbox-bootstrap",
262
+ gateway_url=gateway_url,
263
+ gateway_token=gateway_token,
264
+ workspace=workspace,
265
+ no_clean=no_clean,
266
+ )
267
+ )
268
+
269
+
270
+ def build_cli() -> click.Group:
271
+ return app
272
+
273
+
274
+ def _namespace(**values: Any) -> SimpleNamespace:
275
+ return SimpleNamespace(**values)
276
+
277
+
278
+ def _serve_mcp_namespace(command: str, values: dict[str, Any]) -> SimpleNamespace:
279
+ normalized = dict(values)
280
+ normalized["command"] = command
281
+ normalized["matchlock_allow_host"] = list(normalized.get("matchlock_allow_host") or [])
282
+ return _namespace(**normalized)
283
+
284
+
285
+ def main(argv: list[str] | None = None) -> int:
286
+ try:
287
+ result = app.main(args=argv, prog_name="maco", standalone_mode=False)
288
+ except click.exceptions.Exit as exc:
289
+ return exc.exit_code
290
+ except click.ClickException as exc:
291
+ exc.show(file=sys.stderr)
292
+ return exc.exit_code
293
+ except (ConfigError, RunnerError, ServiceError, OSError, ValueError) as exc:
294
+ exit_with_error(exc)
295
+ return int(result or 0)
296
+
297
+
298
+ def _cmd_version(args: Any) -> int:
299
+ info = get_version_info()
300
+ print(f"version: {info.version}")
301
+ return 0
302
+
303
+
304
+ def _cmd_gen(args: Any) -> int:
305
+ config = load_config(args.config)
306
+ stats = generate(
307
+ config,
308
+ workspace=args.workspace,
309
+ server_filter=args.server,
310
+ clean=args.clean,
311
+ )
312
+ print(
313
+ _GENERATION_SUMMARY_TEMPLATE.render(
314
+ label="Generated",
315
+ tool_count=stats.tool_count,
316
+ server_count=stats.server_count,
317
+ workspace=stats.workspace,
318
+ explore_path=f"{stats.workspace}/maco_generated/servers",
319
+ ).strip()
320
+ )
321
+ return 0
322
+
323
+
324
+ def _cmd_serve(args: Any) -> int:
325
+ config = load_config(args.config)
326
+ stats = generate(config, workspace=args.workspace, clean=args.clean)
327
+ print(
328
+ _GENERATION_SUMMARY_TEMPLATE.render(
329
+ label="Generated",
330
+ tool_count=stats.tool_count,
331
+ server_count=stats.server_count,
332
+ workspace=stats.workspace,
333
+ explore_path=None,
334
+ ).strip()
335
+ )
336
+ serve(
337
+ config,
338
+ ServeOptions(
339
+ host=args.host,
340
+ port=args.port,
341
+ workspace=args.workspace,
342
+ token=args.token,
343
+ use_token=not args.no_token,
344
+ ),
345
+ )
346
+ return 0
347
+
348
+
349
+ def _cmd_run(args: Any) -> int:
350
+ script_args = list(args.script_args or [])
351
+ if script_args and script_args[0] == "--":
352
+ script_args = script_args[1:]
353
+ return run_code(
354
+ args.code_path,
355
+ script_args,
356
+ workspace=args.workspace,
357
+ cwd=args.cwd,
358
+ python=args.python,
359
+ )
360
+
361
+
362
+ def _cmd_up(args: Any) -> int:
363
+ if args.detach:
364
+ start_detached(args)
365
+ return 0
366
+ ensure_no_detached_service(args)
367
+ if args.port is None:
368
+ args.port = find_available_port(args.host, DEFAULT_MCP_PORT)
369
+ return _cmd_mcp_server(args)
370
+
371
+
372
+ def _cmd_mcp_server(args: Any) -> int:
373
+ if args.port is None:
374
+ args.port = DEFAULT_MCP_PORT
375
+ serve_mcp(
376
+ ServeMcpOptions(
377
+ config=args.config,
378
+ provider=args.provider,
379
+ workspace=args.workspace,
380
+ clean=args.clean,
381
+ scratch=args.scratch,
382
+ gateway_host=args.gateway_host,
383
+ gateway_port=args.gateway_port,
384
+ gateway_token=args.gateway_token,
385
+ gateway_use_token=not args.no_gateway_token,
386
+ host=args.host,
387
+ port=args.port,
388
+ timeout=args.timeout,
389
+ debug=args.debug,
390
+ image=args.image,
391
+ python_command=args.python_command,
392
+ docker_binary=args.docker_binary,
393
+ docker_network=args.docker_network,
394
+ docker_gateway_host=args.docker_gateway_host,
395
+ docker_gateway_ip=args.docker_gateway_ip,
396
+ matchlock_binary=args.matchlock_binary,
397
+ matchlock_gateway_host=args.matchlock_gateway_host,
398
+ matchlock_gateway_ip=args.matchlock_gateway_ip,
399
+ matchlock_allow_host=tuple(args.matchlock_allow_host or []),
400
+ detached_service_id=os.environ.get(SERVICE_ID_ENV),
401
+ detached_service_token=os.environ.get(SERVICE_TOKEN_ENV),
402
+ )
403
+ )
404
+ return 0
405
+
406
+
407
+ def _cmd_status(args: Any) -> int:
408
+ show_status(args)
409
+ return 0
410
+
411
+
412
+ def _cmd_down(args: Any) -> int:
413
+ stop_detached(args)
414
+ return 0
415
+
416
+
417
+ def _cmd_ls(args: Any) -> int:
418
+ list_services()
419
+ return 0
420
+
421
+
422
+ def _cmd_sandbox_bootstrap(args: Any) -> int:
423
+ gateway_url = args.gateway_url or os.environ.get("MACO_GATEWAY_URL")
424
+ if not gateway_url:
425
+ raise ValueError("sandbox bootstrap requires --gateway-url or MACO_GATEWAY_URL")
426
+ stats = generate_sandbox_sdk_from_gateway(
427
+ gateway_url,
428
+ token=args.gateway_token or os.environ.get("MACO_GATEWAY_TOKEN"),
429
+ workspace=args.workspace,
430
+ clean=not args.no_clean,
431
+ )
432
+ print(
433
+ _GENERATION_SUMMARY_TEMPLATE.render(
434
+ label="Generated sandbox SDK with",
435
+ tool_count=stats.tool_count,
436
+ server_count=stats.server_count,
437
+ workspace=stats.workspace,
438
+ explore_path=f"{stats.workspace}/tools",
439
+ ).strip()
440
+ )
441
+ return 0
442
+
443
+
444
+ if __name__ == "__main__": # pragma: no cover
445
+ raise SystemExit(main(sys.argv[1:]))
@@ -18,10 +18,23 @@ from typing import Any
18
18
  from http import HTTPStatus
19
19
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
20
20
 
21
+ from jinja2 import Template
22
+
21
23
  from .config import MacoConfig
22
24
  from .mcp_manager import MCPManager
23
25
 
24
26
 
27
+ _GATEWAY_STARTED_TEMPLATE = Template(
28
+ """\
29
+ maco gateway started
30
+ URL: {{ gateway.url }}
31
+ workspace: {{ gateway.workspace }}
32
+ gateway file: {{ gateway.gateway_file }}
33
+ press Ctrl+C to stop
34
+ """
35
+ )
36
+
37
+
25
38
  @dataclass(frozen=True)
26
39
  class ServeOptions:
27
40
  host: str = "127.0.0.1"
@@ -180,11 +193,7 @@ def serve(config: MacoConfig, options: ServeOptions) -> None:
180
193
  old_sigint = signal.signal(signal.SIGINT, _request_shutdown)
181
194
  old_sigterm = signal.signal(signal.SIGTERM, _request_shutdown)
182
195
  try:
183
- print("maco gateway started")
184
- print(f" URL: {gateway.url}")
185
- print(f" workspace: {gateway.workspace}")
186
- print(f" gateway file: {gateway.gateway_file}")
187
- print(" press Ctrl+C to stop")
196
+ print(_GATEWAY_STARTED_TEMPLATE.render(gateway=gateway).strip())
188
197
  stop_event.wait()
189
198
  finally:
190
199
  signal.signal(signal.SIGINT, old_sigint)
@@ -21,6 +21,7 @@ from urllib.parse import parse_qs, urlparse
21
21
  import webbrowser
22
22
 
23
23
  import httpx
24
+ from jinja2 import Template
24
25
  from mcp.client.auth.utils import (
25
26
  build_oauth_authorization_server_metadata_discovery_urls,
26
27
  build_protected_resource_metadata_discovery_urls,
@@ -45,6 +46,17 @@ from pydantic import AnyUrl
45
46
  from .config import OAuthConfig, ServerConfig
46
47
 
47
48
 
49
+ _OAUTH_AUTHORIZATION_TEMPLATE = Template(
50
+ """\
51
+ {{ name }} requires OAuth authorization.{% if opening_browser %} Opening your browser...{% endif %}
52
+ {% if browser_failed %}Could not open browser automatically.
53
+ {% endif %}If your browser did not open, visit this URL:
54
+
55
+ {{ auth_url }}
56
+ """
57
+ )
58
+
59
+
48
60
  CALLBACK_PATH = "/mcp/oauth/callback"
49
61
  DEFAULT_CALLBACK_TIMEOUT = 120.0
50
62
  TokenEndpointAuthMethod = Literal[
@@ -139,13 +151,17 @@ def _redirect_handler(server_name: str, oauth: OAuthConfig) -> Callable[[str], A
139
151
  )
140
152
 
141
153
  name = f"MCP server {server_name!r}" if server_name else "MCP server"
142
- if open_browser(oauth):
143
- print(f"{name} requires OAuth authorization. Opening your browser...", file=sys.stderr)
144
- if not webbrowser.open(auth_url):
145
- print("Could not open browser automatically.", file=sys.stderr)
146
- else:
147
- print(f"{name} requires OAuth authorization.", file=sys.stderr)
148
- print(f"If your browser did not open, visit this URL:\n\n {auth_url}\n", file=sys.stderr)
154
+ opening_browser = open_browser(oauth)
155
+ browser_failed = opening_browser and not webbrowser.open(auth_url)
156
+ print(
157
+ _OAUTH_AUTHORIZATION_TEMPLATE.render(
158
+ name=name,
159
+ opening_browser=opening_browser,
160
+ browser_failed=browser_failed,
161
+ auth_url=auth_url,
162
+ ).strip(),
163
+ file=sys.stderr,
164
+ )
149
165
 
150
166
  return redirect_handler
151
167
 
@@ -1,4 +1,4 @@
1
- """Sandbox providers for maco serve-mcp execution."""
1
+ """Sandbox providers for maco MCP server execution."""
2
2
 
3
3
  from .core import (
4
4
  DEFAULT_MATCHLOCK_DARWIN_GATEWAY_IP,