mcp-as-code 0.1.1__tar.gz → 0.1.2__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.1 → mcp_as_code-0.1.2}/.gitignore +1 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/PKG-INFO +8 -18
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/README.md +7 -17
- mcp_as_code-0.1.2/VERSION.txt +1 -0
- mcp_as_code-0.1.2/docs/mcp-config.md +270 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/examples/serve-mcp/README.md +1 -1
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/pyproject.toml +1 -0
- mcp_as_code-0.1.2/src/maco/_build_info.py +4 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/cli.py +4 -1
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/config.py +85 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/mcp_manager.py +33 -13
- mcp_as_code-0.1.2/src/maco/oauth.py +640 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/sandbox/__init__.py +4 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/sandbox/core.py +12 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/serve_mcp.py +5 -3
- mcp_as_code-0.1.1/VERSION.txt +0 -1
- mcp_as_code-0.1.1/src/maco/_build_info.py +0 -4
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/LICENSE +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/images/sandbox/README.md +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/__init__.py +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/codegen.py +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/gateway.py +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/runner.py +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/sandbox/providers/__init__.py +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/sandbox/providers/base.py +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/sandbox/providers/docker.py +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/sandbox/providers/local.py +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/sandbox/providers/matchlock.py +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/bash_description.j2 +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/code_execute_description.j2 +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/codegen/client.py.j2 +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/codegen/model.py.j2 +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/codegen/package_init.py.j2 +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/codegen/pyproject.toml.j2 +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/codegen/root_model.py.j2 +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/codegen/server_init.py.j2 +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/codegen/tool.py.j2 +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/codegen/type_alias.py.j2 +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/serve_mcp_instructions.j2 +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/server_catalog.j2 +0 -0
- {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-as-code
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Execute MCP tools through generated Python code interfaces
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -141,32 +141,22 @@ If you are using the source checkout directly, the script wrapper is equivalent:
|
|
|
141
141
|
|
|
142
142
|
## MCP config
|
|
143
143
|
|
|
144
|
-
`maco` expects Claude-style JSON with a top-level `mcpServers` object.
|
|
144
|
+
`maco` expects Claude-style JSON with a top-level `mcpServers` object. Supported upstream transports are `stdio`, `http`/`streamable_http`, and `sse`.
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
Minimal stdio example:
|
|
147
147
|
|
|
148
148
|
```json
|
|
149
149
|
{
|
|
150
150
|
"mcpServers": {
|
|
151
|
-
"
|
|
152
|
-
"command": "
|
|
153
|
-
"args": [
|
|
154
|
-
"run",
|
|
155
|
-
"-i",
|
|
156
|
-
"--rm",
|
|
157
|
-
"-e",
|
|
158
|
-
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
|
159
|
-
"ghcr.io/github/github-mcp-server"
|
|
160
|
-
],
|
|
161
|
-
"env": {
|
|
162
|
-
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
|
|
163
|
-
}
|
|
151
|
+
"filesystem": {
|
|
152
|
+
"command": "npx",
|
|
153
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
|
|
164
154
|
}
|
|
165
155
|
}
|
|
166
156
|
}
|
|
167
157
|
```
|
|
168
158
|
|
|
169
|
-
|
|
159
|
+
Minimal Streamable HTTP example:
|
|
170
160
|
|
|
171
161
|
```json
|
|
172
162
|
{
|
|
@@ -180,7 +170,7 @@ HTTP-style servers can use URL and header fields:
|
|
|
180
170
|
}
|
|
181
171
|
```
|
|
182
172
|
|
|
183
|
-
|
|
173
|
+
For remote HTTP/SSE servers without a static `Authorization` header, maco can perform OAuth from the upstream server's HTTP `401 Bearer` challenge. See [`docs/mcp-config.md`](docs/mcp-config.md) for the full config reference, including environment expansion, headers, OAuth hints, token caching, and tool filtering.
|
|
184
174
|
|
|
185
175
|
## Sandbox providers
|
|
186
176
|
|
|
@@ -128,32 +128,22 @@ If you are using the source checkout directly, the script wrapper is equivalent:
|
|
|
128
128
|
|
|
129
129
|
## MCP config
|
|
130
130
|
|
|
131
|
-
`maco` expects Claude-style JSON with a top-level `mcpServers` object.
|
|
131
|
+
`maco` expects Claude-style JSON with a top-level `mcpServers` object. Supported upstream transports are `stdio`, `http`/`streamable_http`, and `sse`.
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
Minimal stdio example:
|
|
134
134
|
|
|
135
135
|
```json
|
|
136
136
|
{
|
|
137
137
|
"mcpServers": {
|
|
138
|
-
"
|
|
139
|
-
"command": "
|
|
140
|
-
"args": [
|
|
141
|
-
"run",
|
|
142
|
-
"-i",
|
|
143
|
-
"--rm",
|
|
144
|
-
"-e",
|
|
145
|
-
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
|
146
|
-
"ghcr.io/github/github-mcp-server"
|
|
147
|
-
],
|
|
148
|
-
"env": {
|
|
149
|
-
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
|
|
150
|
-
}
|
|
138
|
+
"filesystem": {
|
|
139
|
+
"command": "npx",
|
|
140
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
|
|
151
141
|
}
|
|
152
142
|
}
|
|
153
143
|
}
|
|
154
144
|
```
|
|
155
145
|
|
|
156
|
-
|
|
146
|
+
Minimal Streamable HTTP example:
|
|
157
147
|
|
|
158
148
|
```json
|
|
159
149
|
{
|
|
@@ -167,7 +157,7 @@ HTTP-style servers can use URL and header fields:
|
|
|
167
157
|
}
|
|
168
158
|
```
|
|
169
159
|
|
|
170
|
-
|
|
160
|
+
For remote HTTP/SSE servers without a static `Authorization` header, maco can perform OAuth from the upstream server's HTTP `401 Bearer` challenge. See [`docs/mcp-config.md`](docs/mcp-config.md) for the full config reference, including environment expansion, headers, OAuth hints, token caching, and tool filtering.
|
|
171
161
|
|
|
172
162
|
## Sandbox providers
|
|
173
163
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.2
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# MCP configuration
|
|
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`.
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
{
|
|
7
|
+
"mcpServers": {
|
|
8
|
+
"server-name": {
|
|
9
|
+
"command": "..."
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Supported transports
|
|
16
|
+
|
|
17
|
+
| Transport | When to use it | Required fields |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| `stdio` | Local subprocess MCP servers | `command` |
|
|
20
|
+
| `http` / `streamable_http` | Remote Streamable HTTP MCP servers | `url` or `base_url` |
|
|
21
|
+
| `sse` | Deprecated SSE MCP servers | `url` or `base_url` |
|
|
22
|
+
|
|
23
|
+
If `type`, `server_type`, or `transport` is omitted, maco infers `http` when `url`/`base_url` is present and `stdio` otherwise.
|
|
24
|
+
|
|
25
|
+
## Common fields
|
|
26
|
+
|
|
27
|
+
| Field | Type | Applies to | Description |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `type`, `server_type`, `transport` | string | all | Transport name. `streamable-http` and `streamablehttp` are normalized to `streamable_http`. |
|
|
30
|
+
| `command` | string | stdio | Executable used to start the MCP server. |
|
|
31
|
+
| `args` | string array | stdio | Arguments passed to `command`. |
|
|
32
|
+
| `env` | object | stdio | Environment variables passed to the subprocess. Values expand `$VAR`, `${VAR}`, and `~`. |
|
|
33
|
+
| `cwd` | string | stdio | Working directory for the subprocess. |
|
|
34
|
+
| `url`, `base_url` | string | HTTP/SSE | MCP endpoint URL. Values expand `$VAR`, `${VAR}`, and `~`. |
|
|
35
|
+
| `headers` | object | HTTP/SSE | Static HTTP headers. Values expand environment variables. |
|
|
36
|
+
| `oauth` | object | HTTP/SSE | Optional OAuth hints and interaction settings. See [OAuth](#oauth). |
|
|
37
|
+
| `tools`, `tool_white_list`, `tool_whitelist` | string array | all | Optional allow-list of upstream tool names to expose. |
|
|
38
|
+
|
|
39
|
+
## Stdio servers
|
|
40
|
+
|
|
41
|
+
Use stdio for MCP servers that run as local subprocesses.
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"filesystem": {
|
|
47
|
+
"command": "npx",
|
|
48
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Environment variables
|
|
55
|
+
|
|
56
|
+
Prefer listing required environment variables under `env`. maco expands `$VAR` and `${VAR}` using the environment of the maco process, then passes the resolved values to the upstream MCP server.
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"mcpServers": {
|
|
61
|
+
"github": {
|
|
62
|
+
"command": "docker",
|
|
63
|
+
"args": [
|
|
64
|
+
"run",
|
|
65
|
+
"-i",
|
|
66
|
+
"--rm",
|
|
67
|
+
"-e",
|
|
68
|
+
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
|
69
|
+
"ghcr.io/github/github-mcp-server"
|
|
70
|
+
],
|
|
71
|
+
"env": {
|
|
72
|
+
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Arguments and string fields are expanded too, so this is also valid:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"mcpServers": {
|
|
84
|
+
"custom": {
|
|
85
|
+
"command": "uv",
|
|
86
|
+
"args": ["run", "server.py", "--token", "$TOKEN"],
|
|
87
|
+
"env": {"TOKEN": "$TOKEN"}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Streamable HTTP servers
|
|
94
|
+
|
|
95
|
+
Use `type: "http"` or `type: "streamable_http"` for remote Streamable HTTP MCP servers.
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"mcpServers": {
|
|
100
|
+
"remote": {
|
|
101
|
+
"type": "http",
|
|
102
|
+
"url": "https://example.com/mcp"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Static headers
|
|
109
|
+
|
|
110
|
+
Use `headers` for API-key or static bearer-token servers. Static `Authorization` headers take precedence over OAuth and disable OAuth for that server.
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"mcpServers": {
|
|
115
|
+
"remote": {
|
|
116
|
+
"type": "http",
|
|
117
|
+
"url": "https://example.com/mcp",
|
|
118
|
+
"headers": {
|
|
119
|
+
"Authorization": "Bearer ${MCP_TOKEN}"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## SSE servers
|
|
127
|
+
|
|
128
|
+
SSE is supported for compatibility with older MCP servers. Set `type: "sse"` explicitly.
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"mcpServers": {
|
|
133
|
+
"legacy": {
|
|
134
|
+
"type": "sse",
|
|
135
|
+
"url": "https://example.com/sse"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## OAuth
|
|
142
|
+
|
|
143
|
+
For remote HTTP/SSE servers without a static `Authorization` header, maco can automatically perform OAuth when the upstream server returns an HTTP `401` with a Bearer challenge. The flow is:
|
|
144
|
+
|
|
145
|
+
1. maco sends the MCP request without a bearer token.
|
|
146
|
+
2. The upstream server responds with `WWW-Authenticate: Bearer ...`.
|
|
147
|
+
3. maco discovers protected-resource and authorization-server metadata.
|
|
148
|
+
4. maco uses configured client hints or dynamic client registration.
|
|
149
|
+
5. maco opens or prints an authorization URL.
|
|
150
|
+
6. maco receives the loopback callback, exchanges the code with PKCE, caches the token, and retries the MCP request.
|
|
151
|
+
|
|
152
|
+
You can omit `oauth` entirely when the provider supports standard discovery and dynamic client registration.
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"mcpServers": {
|
|
157
|
+
"remote": {
|
|
158
|
+
"type": "http",
|
|
159
|
+
"url": "https://example.com/mcp"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Add `oauth` when the provider needs hints such as a pre-registered client, scopes, a fixed callback URL, or non-standard metadata discovery.
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"mcpServers": {
|
|
170
|
+
"remote": {
|
|
171
|
+
"type": "http",
|
|
172
|
+
"url": "https://example.com/mcp",
|
|
173
|
+
"oauth": {
|
|
174
|
+
"client_id": "${MCP_CLIENT_ID}",
|
|
175
|
+
"client_secret": "${MCP_CLIENT_SECRET}",
|
|
176
|
+
"scopes": ["mcp.read", "mcp.write"],
|
|
177
|
+
"redirect_uri": "http://127.0.0.1:1456/mcp/oauth/callback",
|
|
178
|
+
"auth_server_metadata_url": "https://auth.example.com/.well-known/oauth-authorization-server",
|
|
179
|
+
"interactive": "auto",
|
|
180
|
+
"open_browser": true,
|
|
181
|
+
"callback_timeout": "2m"
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### OAuth fields
|
|
189
|
+
|
|
190
|
+
| Field | Type | Description |
|
|
191
|
+
| --- | --- | --- |
|
|
192
|
+
| `client_id` | string | Pre-registered OAuth client ID. Omit when dynamic client registration is supported. |
|
|
193
|
+
| `client_secret` | string | Optional client secret. When present, token requests use `client_secret_post`. |
|
|
194
|
+
| `scopes` | string array | Requested OAuth scopes. If omitted, maco uses scopes from the Bearer challenge or protected-resource metadata. |
|
|
195
|
+
| `redirect_uri` | string | Loopback callback URI. Defaults to an ephemeral `http://127.0.0.1:<port>/mcp/oauth/callback`. `:0` is allowed and is replaced with the actual bound port. |
|
|
196
|
+
| `auth_server_metadata_url` | string | Optional direct authorization-server metadata URL for providers with non-standard discovery. |
|
|
197
|
+
| `interactive` | string | `auto`, `always`, or `never`. `auto` allows browser authorization only in an interactive terminal. |
|
|
198
|
+
| `open_browser` | boolean | Whether to attempt opening the default browser. The URL is always printed as a fallback. |
|
|
199
|
+
| `callback_timeout` | number or string | Time to wait for the callback. Supports seconds as a number or strings like `30s`, `2m`, `1h`. |
|
|
200
|
+
|
|
201
|
+
### OAuth cache and environment overrides
|
|
202
|
+
|
|
203
|
+
OAuth tokens and client registration metadata are cached under:
|
|
204
|
+
|
|
205
|
+
```text
|
|
206
|
+
~/.maco/mcp/oauth/
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Credential files are written with `0600` permissions.
|
|
210
|
+
|
|
211
|
+
Environment variables can override interaction behavior:
|
|
212
|
+
|
|
213
|
+
| Variable | Description |
|
|
214
|
+
| --- | --- |
|
|
215
|
+
| `MACO_MCP_OAUTH_INTERACTIVE` | Overrides `oauth.interactive`. Use `never` in CI/headless runs to fail fast. |
|
|
216
|
+
| `MACO_MCP_OAUTH_OPEN_BROWSER` | Overrides `oauth.open_browser`. Accepts values like `true`, `false`, `1`, `0`. |
|
|
217
|
+
| `MACO_MCP_OAUTH_CALLBACK_TIMEOUT` | Overrides `oauth.callback_timeout`. Accepts seconds or duration strings like `2m`. |
|
|
218
|
+
|
|
219
|
+
## Tool filtering
|
|
220
|
+
|
|
221
|
+
Use `tools`, `tool_white_list`, or `tool_whitelist` to expose only selected upstream tools through generated wrappers and the maco gateway.
|
|
222
|
+
|
|
223
|
+
```json
|
|
224
|
+
{
|
|
225
|
+
"mcpServers": {
|
|
226
|
+
"remote": {
|
|
227
|
+
"type": "http",
|
|
228
|
+
"url": "https://example.com/mcp",
|
|
229
|
+
"tools": ["search", "fetch"]
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Filtering uses the upstream MCP tool names, not generated Python function names.
|
|
236
|
+
|
|
237
|
+
## Complete mixed example
|
|
238
|
+
|
|
239
|
+
```json
|
|
240
|
+
{
|
|
241
|
+
"mcpServers": {
|
|
242
|
+
"filesystem": {
|
|
243
|
+
"command": "npx",
|
|
244
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
|
|
245
|
+
},
|
|
246
|
+
"github": {
|
|
247
|
+
"command": "docker",
|
|
248
|
+
"args": [
|
|
249
|
+
"run",
|
|
250
|
+
"-i",
|
|
251
|
+
"--rm",
|
|
252
|
+
"-e",
|
|
253
|
+
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
|
254
|
+
"ghcr.io/github/github-mcp-server"
|
|
255
|
+
],
|
|
256
|
+
"env": {
|
|
257
|
+
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
"remote": {
|
|
261
|
+
"type": "http",
|
|
262
|
+
"url": "https://example.com/mcp",
|
|
263
|
+
"oauth": {
|
|
264
|
+
"scopes": ["mcp.read"]
|
|
265
|
+
},
|
|
266
|
+
"tools": ["search", "fetch"]
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
@@ -36,7 +36,7 @@ MCP client ──HTTP──▶ maco serve-mcp ──sandbox──▶ generated P
|
|
|
36
36
|
If you are already authenticated with the GitHub CLI, export a token directly:
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
export
|
|
39
|
+
export GITHUB_TOKEN=$(gh auth token)
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
## 1. Start `maco serve-mcp`
|
|
@@ -133,7 +133,10 @@ def _add_serve_mcp_options(command: argparse.ArgumentParser) -> None:
|
|
|
133
133
|
)
|
|
134
134
|
command.add_argument(
|
|
135
135
|
"--matchlock-gateway-ip",
|
|
136
|
-
help=
|
|
136
|
+
help=(
|
|
137
|
+
"IP for --add-host <gateway-host>:<ip> inside matchlock "
|
|
138
|
+
"(managed default: 192.168.64.1 on macOS, 192.168.100.1 elsewhere)"
|
|
139
|
+
),
|
|
137
140
|
)
|
|
138
141
|
command.add_argument(
|
|
139
142
|
"--matchlock-allow-host",
|
|
@@ -13,6 +13,20 @@ class ConfigError(ValueError):
|
|
|
13
13
|
"""Raised when an MCP configuration file is invalid."""
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class OAuthConfig:
|
|
18
|
+
"""Optional OAuth hints for remote HTTP/SSE MCP servers."""
|
|
19
|
+
|
|
20
|
+
client_id: str | None = None
|
|
21
|
+
client_secret: str | None = None
|
|
22
|
+
scopes: list[str] = field(default_factory=list)
|
|
23
|
+
redirect_uri: str | None = None
|
|
24
|
+
auth_server_metadata_url: str | None = None
|
|
25
|
+
interactive: str | None = None
|
|
26
|
+
open_browser: bool | None = None
|
|
27
|
+
callback_timeout: float | None = None
|
|
28
|
+
|
|
29
|
+
|
|
16
30
|
@dataclass(frozen=True)
|
|
17
31
|
class ServerConfig:
|
|
18
32
|
"""Configuration for one MCP server."""
|
|
@@ -25,6 +39,7 @@ class ServerConfig:
|
|
|
25
39
|
cwd: str | None = None
|
|
26
40
|
base_url: str | None = None
|
|
27
41
|
headers: dict[str, str] = field(default_factory=dict)
|
|
42
|
+
oauth: OAuthConfig | None = None
|
|
28
43
|
tool_white_list: list[str] = field(default_factory=list)
|
|
29
44
|
|
|
30
45
|
@property
|
|
@@ -104,6 +119,7 @@ def _parse_server(name: str, raw: dict[str, Any]) -> ServerConfig:
|
|
|
104
119
|
|
|
105
120
|
env = _string_map(raw.get("env") or {})
|
|
106
121
|
headers = _string_map(raw.get("headers") or {})
|
|
122
|
+
oauth = _parse_oauth(name, raw.get("oauth"))
|
|
107
123
|
args = raw.get("args") or []
|
|
108
124
|
if not isinstance(args, list) or not all(isinstance(arg, str) for arg in args):
|
|
109
125
|
raise ConfigError(f"server {name!r} args must be a list of strings")
|
|
@@ -137,10 +153,79 @@ def _parse_server(name: str, raw: dict[str, Any]) -> ServerConfig:
|
|
|
137
153
|
cwd=cwd,
|
|
138
154
|
base_url=base_url,
|
|
139
155
|
headers=headers,
|
|
156
|
+
oauth=oauth,
|
|
140
157
|
tool_white_list=white_list,
|
|
141
158
|
)
|
|
142
159
|
|
|
143
160
|
|
|
161
|
+
def _parse_oauth(name: str, raw: Any) -> OAuthConfig | None:
|
|
162
|
+
if raw is None or raw is False:
|
|
163
|
+
return None
|
|
164
|
+
if raw is True:
|
|
165
|
+
raw = {}
|
|
166
|
+
if not isinstance(raw, dict):
|
|
167
|
+
raise ConfigError(f"server {name!r} oauth must be an object")
|
|
168
|
+
|
|
169
|
+
scopes = raw.get("scopes") or []
|
|
170
|
+
if not isinstance(scopes, list) or not all(isinstance(scope, str) for scope in scopes):
|
|
171
|
+
raise ConfigError(f"server {name!r} oauth scopes must be a list of strings")
|
|
172
|
+
|
|
173
|
+
interactive = _optional_expanded(raw.get("interactive"))
|
|
174
|
+
if interactive is not None:
|
|
175
|
+
interactive = interactive.strip().lower()
|
|
176
|
+
|
|
177
|
+
return OAuthConfig(
|
|
178
|
+
client_id=_optional_expanded(raw.get("client_id")),
|
|
179
|
+
client_secret=_optional_expanded(raw.get("client_secret")),
|
|
180
|
+
scopes=[_expand_value(scope) for scope in scopes],
|
|
181
|
+
redirect_uri=_optional_expanded(raw.get("redirect_uri")),
|
|
182
|
+
auth_server_metadata_url=_optional_expanded(raw.get("auth_server_metadata_url")),
|
|
183
|
+
interactive=interactive,
|
|
184
|
+
open_browser=_optional_bool(name, raw.get("open_browser")),
|
|
185
|
+
callback_timeout=_optional_duration_seconds(name, raw.get("callback_timeout")),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _optional_bool(name: str, value: Any) -> bool | None:
|
|
190
|
+
if value is None:
|
|
191
|
+
return None
|
|
192
|
+
if isinstance(value, bool):
|
|
193
|
+
return value
|
|
194
|
+
if isinstance(value, str):
|
|
195
|
+
normalized = _expand_value(value).strip().lower()
|
|
196
|
+
if normalized in {"1", "true", "yes", "on"}:
|
|
197
|
+
return True
|
|
198
|
+
if normalized in {"0", "false", "no", "off"}:
|
|
199
|
+
return False
|
|
200
|
+
raise ConfigError(f"server {name!r} oauth open_browser must be a boolean")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _optional_duration_seconds(name: str, value: Any) -> float | None:
|
|
204
|
+
if value is None:
|
|
205
|
+
return None
|
|
206
|
+
if isinstance(value, int | float):
|
|
207
|
+
seconds = float(value)
|
|
208
|
+
elif isinstance(value, str):
|
|
209
|
+
seconds = _parse_duration_seconds(_expand_value(value).strip())
|
|
210
|
+
else:
|
|
211
|
+
raise ConfigError(
|
|
212
|
+
f"server {name!r} oauth callback_timeout must be a number or duration string"
|
|
213
|
+
)
|
|
214
|
+
if seconds <= 0:
|
|
215
|
+
raise ConfigError(f"server {name!r} oauth callback_timeout must be positive")
|
|
216
|
+
return seconds
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _parse_duration_seconds(value: str) -> float:
|
|
220
|
+
if not value:
|
|
221
|
+
raise ConfigError("oauth callback_timeout must not be empty")
|
|
222
|
+
multipliers = {"ms": 0.001, "s": 1.0, "m": 60.0, "h": 3600.0}
|
|
223
|
+
for suffix, multiplier in multipliers.items():
|
|
224
|
+
if value.endswith(suffix):
|
|
225
|
+
return float(value[: -len(suffix)]) * multiplier
|
|
226
|
+
return float(value)
|
|
227
|
+
|
|
228
|
+
|
|
144
229
|
def _infer_server_type(raw: dict[str, Any]) -> str:
|
|
145
230
|
if raw.get("base_url") or raw.get("url"):
|
|
146
231
|
return "http"
|
|
@@ -4,14 +4,19 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import contextlib
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
-
from typing import Any, AsyncIterator
|
|
7
|
+
from typing import Any, AsyncIterator, Protocol, cast
|
|
8
8
|
|
|
9
9
|
from mcp import ClientSession
|
|
10
10
|
from mcp.client.sse import sse_client
|
|
11
11
|
from mcp.client.stdio import StdioServerParameters, stdio_client
|
|
12
|
-
from mcp.client.streamable_http import streamable_http_client
|
|
12
|
+
from mcp.client.streamable_http import create_mcp_http_client, streamable_http_client
|
|
13
13
|
|
|
14
14
|
from .config import MacoConfig, ServerConfig
|
|
15
|
+
from .oauth import make_oauth_auth
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _Closeable(Protocol):
|
|
19
|
+
def close(self) -> None: ...
|
|
15
20
|
|
|
16
21
|
|
|
17
22
|
@dataclass
|
|
@@ -123,15 +128,21 @@ async def _client_streams(server: ServerConfig) -> AsyncIterator[tuple[Any, Any]
|
|
|
123
128
|
return
|
|
124
129
|
|
|
125
130
|
if server.is_streamable_http:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
auth = make_oauth_auth(server)
|
|
132
|
+
if server.headers or auth:
|
|
133
|
+
try:
|
|
134
|
+
async with create_mcp_http_client(
|
|
135
|
+
headers=server.headers or None,
|
|
136
|
+
auth=auth,
|
|
137
|
+
) as http_client:
|
|
138
|
+
async with streamable_http_client(
|
|
139
|
+
server.base_url or "",
|
|
140
|
+
http_client=http_client,
|
|
141
|
+
) as (read_stream, write_stream, _get_session_id):
|
|
142
|
+
yield read_stream, write_stream
|
|
143
|
+
finally:
|
|
144
|
+
if hasattr(auth, "close"):
|
|
145
|
+
cast(_Closeable, auth).close()
|
|
135
146
|
else:
|
|
136
147
|
async with streamable_http_client(server.base_url or "") as (
|
|
137
148
|
read_stream,
|
|
@@ -142,8 +153,17 @@ async def _client_streams(server: ServerConfig) -> AsyncIterator[tuple[Any, Any]
|
|
|
142
153
|
return
|
|
143
154
|
|
|
144
155
|
if server.is_sse:
|
|
145
|
-
|
|
146
|
-
|
|
156
|
+
auth = make_oauth_auth(server)
|
|
157
|
+
try:
|
|
158
|
+
async with sse_client(
|
|
159
|
+
server.base_url or "",
|
|
160
|
+
headers=server.headers or None,
|
|
161
|
+
auth=auth,
|
|
162
|
+
) as streams:
|
|
163
|
+
yield streams
|
|
164
|
+
finally:
|
|
165
|
+
if hasattr(auth, "close"):
|
|
166
|
+
cast(_Closeable, auth).close()
|
|
147
167
|
return
|
|
148
168
|
|
|
149
169
|
raise ValueError(f"unsupported MCP server transport for {server.name}: {server.server_type}")
|