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.
Files changed (41) hide show
  1. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/.gitignore +1 -0
  2. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/PKG-INFO +8 -18
  3. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/README.md +7 -17
  4. mcp_as_code-0.1.2/VERSION.txt +1 -0
  5. mcp_as_code-0.1.2/docs/mcp-config.md +270 -0
  6. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/examples/serve-mcp/README.md +1 -1
  7. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/pyproject.toml +1 -0
  8. mcp_as_code-0.1.2/src/maco/_build_info.py +4 -0
  9. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/cli.py +4 -1
  10. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/config.py +85 -0
  11. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/mcp_manager.py +33 -13
  12. mcp_as_code-0.1.2/src/maco/oauth.py +640 -0
  13. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/sandbox/__init__.py +4 -0
  14. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/sandbox/core.py +12 -0
  15. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/serve_mcp.py +5 -3
  16. mcp_as_code-0.1.1/VERSION.txt +0 -1
  17. mcp_as_code-0.1.1/src/maco/_build_info.py +0 -4
  18. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/LICENSE +0 -0
  19. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/images/sandbox/README.md +0 -0
  20. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/__init__.py +0 -0
  21. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/codegen.py +0 -0
  22. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/gateway.py +0 -0
  23. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/runner.py +0 -0
  24. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/sandbox/providers/__init__.py +0 -0
  25. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/sandbox/providers/base.py +0 -0
  26. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/sandbox/providers/docker.py +0 -0
  27. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/sandbox/providers/local.py +0 -0
  28. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/sandbox/providers/matchlock.py +0 -0
  29. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/bash_description.j2 +0 -0
  30. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/code_execute_description.j2 +0 -0
  31. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/codegen/client.py.j2 +0 -0
  32. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/codegen/model.py.j2 +0 -0
  33. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/codegen/package_init.py.j2 +0 -0
  34. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/codegen/pyproject.toml.j2 +0 -0
  35. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/codegen/root_model.py.j2 +0 -0
  36. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/codegen/server_init.py.j2 +0 -0
  37. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/codegen/tool.py.j2 +0 -0
  38. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/codegen/type_alias.py.j2 +0 -0
  39. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/serve_mcp_instructions.j2 +0 -0
  40. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/templates/server_catalog.j2 +0 -0
  41. {mcp_as_code-0.1.1 → mcp_as_code-0.1.2}/src/maco/version.py +0 -0
@@ -49,3 +49,4 @@ gateway.json
49
49
  examples/**/.maco/
50
50
  examples/**/scratch/
51
51
  examples/**/maco-serve-mcp/
52
+ examples/**/.playwright-mcp/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-as-code
3
- Version: 0.1.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
- For environment variables, put them under `env`. `maco` expands `$VAR` and `${VAR}` using the environment of the `maco` process, then passes the resolved values to the upstream MCP server:
146
+ Minimal stdio example:
147
147
 
148
148
  ```json
149
149
  {
150
150
  "mcpServers": {
151
- "github": {
152
- "command": "docker",
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
- HTTP-style servers can use URL and header fields:
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
- Supported transports are `stdio`, `http`/`streamable_http`, and `sse`.
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
- For environment variables, put them under `env`. `maco` expands `$VAR` and `${VAR}` using the environment of the `maco` process, then passes the resolved values to the upstream MCP server:
133
+ Minimal stdio example:
134
134
 
135
135
  ```json
136
136
  {
137
137
  "mcpServers": {
138
- "github": {
139
- "command": "docker",
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
- HTTP-style servers can use URL and header fields:
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
- Supported transports are `stdio`, `http`/`streamable_http`, and `sse`.
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 GITHUB_PERSONAL_ACCESS_TOKEN=$(gh auth token)
39
+ export GITHUB_TOKEN=$(gh auth token)
40
40
  ```
41
41
 
42
42
  ## 1. Start `maco serve-mcp`
@@ -36,6 +36,7 @@ packages = ["src/maco"]
36
36
  [tool.hatch.build.targets.sdist]
37
37
  include = [
38
38
  "src/maco",
39
+ "docs",
39
40
  "README.md",
40
41
  "LICENSE",
41
42
  "VERSION.txt",
@@ -0,0 +1,4 @@
1
+ """Build metadata embedded during release builds."""
2
+
3
+ COMMIT_SHA = "e680ca8b394a20cf951bc0645e452696710ed7de"
4
+ RELEASE_DATE = "2026-06-15"
@@ -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="IP for --add-host <gateway-host>:<ip> inside matchlock (default: 192.168.100.1 for managed gateways)",
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
- if server.headers:
127
- import httpx
128
-
129
- async with httpx.AsyncClient(headers=server.headers, follow_redirects=True) as http_client:
130
- async with streamable_http_client(
131
- server.base_url or "",
132
- http_client=http_client,
133
- ) as (read_stream, write_stream, _get_session_id):
134
- yield read_stream, write_stream
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
- async with sse_client(server.base_url or "", headers=server.headers or None) as streams:
146
- yield streams
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}")