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.
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/PKG-INFO +4 -2
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/README.md +1 -1
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/docs/mcp-config.md +1 -1
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/examples/serve-mcp/README.md +6 -6
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/images/sandbox/README.md +3 -3
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/pyproject.toml +3 -3
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/__init__.py +1 -1
- mcp_as_code-0.1.5/src/maco/cli.py +445 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/gateway.py +14 -5
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/oauth.py +23 -7
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/sandbox/__init__.py +1 -1
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/sandbox/core.py +9 -17
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/serve_mcp.py +94 -92
- mcp_as_code-0.1.5/src/maco/service.py +589 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/codegen/client.py.j2 +1 -2
- mcp_as_code-0.1.5/src/maco/version.py +22 -0
- mcp_as_code-0.1.4/VERSION.txt +0 -1
- mcp_as_code-0.1.4/src/maco/_build_info.py +0 -4
- mcp_as_code-0.1.4/src/maco/cli.py +0 -261
- mcp_as_code-0.1.4/src/maco/version.py +0 -72
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/.gitignore +0 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/LICENSE +0 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/codegen.py +0 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/config.py +0 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/mcp_manager.py +0 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/runner.py +0 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/sandbox/providers/__init__.py +0 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/sandbox/providers/base.py +0 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/sandbox/providers/docker.py +0 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/sandbox/providers/local.py +0 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/sandbox/providers/matchlock.py +0 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/bash_description.j2 +0 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/code_execute_description.j2 +0 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/codegen/model.py.j2 +0 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/codegen/package_init.py.j2 +0 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/codegen/pyproject.toml.j2 +0 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/codegen/root_model.py.j2 +0 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/codegen/server_init.py.j2 +0 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/codegen/tool.py.j2 +0 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/codegen/type_alias.py.j2 +0 -0
- {mcp_as_code-0.1.4 → mcp_as_code-0.1.5}/src/maco/templates/serve_mcp_instructions.j2 +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1
|
+
# `maco up` example
|
|
2
2
|
|
|
3
|
-
This example shows how to expose several upstream MCP servers through one compact `maco
|
|
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
|
|
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
|
|
28
|
+
uv run maco up --provider local
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
This uses `mcp.json`, writes `.maco/gateway.json`, uses `
|
|
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
|
|
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
|
|
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="$(
|
|
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="$(
|
|
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 = "
|
|
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
|
]
|
|
@@ -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(
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|