mcp-stdio 0.3.0__tar.gz → 0.3.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_stdio-0.3.0 → mcp_stdio-0.3.2}/.github/workflows/release.yml +6 -0
- {mcp_stdio-0.3.0 → mcp_stdio-0.3.2}/PKG-INFO +12 -4
- {mcp_stdio-0.3.0 → mcp_stdio-0.3.2}/README.ja.md +9 -4
- {mcp_stdio-0.3.0 → mcp_stdio-0.3.2}/README.md +8 -3
- {mcp_stdio-0.3.0 → mcp_stdio-0.3.2}/pyproject.toml +3 -0
- {mcp_stdio-0.3.0 → mcp_stdio-0.3.2}/server.json +2 -2
- {mcp_stdio-0.3.0 → mcp_stdio-0.3.2}/src/mcp_stdio/__init__.py +1 -1
- {mcp_stdio-0.3.0 → mcp_stdio-0.3.2}/src/mcp_stdio/oauth.py +14 -11
- {mcp_stdio-0.3.0 → mcp_stdio-0.3.2}/src/mcp_stdio/relay.py +1 -22
- {mcp_stdio-0.3.0 → mcp_stdio-0.3.2}/src/mcp_stdio/token_store.py +47 -5
- {mcp_stdio-0.3.0 → mcp_stdio-0.3.2}/tests/test_cli.py +52 -23
- {mcp_stdio-0.3.0 → mcp_stdio-0.3.2}/tests/test_oauth.py +142 -74
- {mcp_stdio-0.3.0 → mcp_stdio-0.3.2}/tests/test_relay.py +61 -51
- mcp_stdio-0.3.2/tests/test_token_store.py +203 -0
- mcp_stdio-0.3.2/uv.lock +244 -0
- mcp_stdio-0.3.0/tests/test_token_store.py +0 -106
- mcp_stdio-0.3.0/uv.lock +0 -104
- {mcp_stdio-0.3.0 → mcp_stdio-0.3.2}/.github/workflows/test.yml +0 -0
- {mcp_stdio-0.3.0 → mcp_stdio-0.3.2}/.gitignore +0 -0
- {mcp_stdio-0.3.0 → mcp_stdio-0.3.2}/CLAUDE.md +0 -0
- {mcp_stdio-0.3.0 → mcp_stdio-0.3.2}/LICENSE +0 -0
- {mcp_stdio-0.3.0 → mcp_stdio-0.3.2}/src/mcp_stdio/__main__.py +0 -0
- {mcp_stdio-0.3.0 → mcp_stdio-0.3.2}/src/mcp_stdio/cli.py +0 -0
- {mcp_stdio-0.3.0 → mcp_stdio-0.3.2}/tests/__init__.py +0 -0
|
@@ -91,6 +91,12 @@ jobs:
|
|
|
91
91
|
run: |
|
|
92
92
|
VERSION="${GITHUB_REF_NAME#v}"
|
|
93
93
|
jq --arg v "$VERSION" '.version = $v | .packages[0].version = $v' server.json > server.tmp && mv server.tmp server.json
|
|
94
|
+
# Sanity check: ensure placeholder was replaced
|
|
95
|
+
if jq -e '.version == "0.0.0" or .packages[0].version == "0.0.0"' server.json > /dev/null; then
|
|
96
|
+
echo "::error::server.json still contains placeholder 0.0.0 after patching"
|
|
97
|
+
exit 1
|
|
98
|
+
fi
|
|
99
|
+
echo "Patched server.json to version $VERSION"
|
|
94
100
|
|
|
95
101
|
- name: Install mcp-publisher
|
|
96
102
|
run: |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-stdio
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Stdio-to-HTTP relay for MCP servers
|
|
5
5
|
Project-URL: Homepage, https://github.com/shigechika/mcp-stdio
|
|
6
6
|
Project-URL: Issues, https://github.com/shigechika/mcp-stdio/issues
|
|
@@ -13,6 +13,9 @@ Classifier: Environment :: Console
|
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
19
|
Classifier: Topic :: Software Development :: Libraries
|
|
17
20
|
Requires-Python: >=3.10
|
|
18
21
|
Requires-Dist: httpx>=0.25.0
|
|
@@ -34,13 +37,15 @@ Stdio-to-HTTP relay for MCP servers — bridges Claude Desktop/Code to remote St
|
|
|
34
37
|
[MCP](https://modelcontextprotocol.io/) clients like Claude Desktop and Claude Code see mcp-stdio as a locally running self-hosted MCP server, while it relays all requests to a remote MCP server over Streamable HTTP:
|
|
35
38
|
|
|
36
39
|
```mermaid
|
|
37
|
-
flowchart
|
|
40
|
+
flowchart BT
|
|
38
41
|
A[Claude<br>CLI/Desktop] <-- stdio --> B(mcp-stdio)
|
|
39
|
-
B <== "HTTPS<br>
|
|
42
|
+
B <== "<b>HTTPS</b><br>Bearer Token<br>Header<br>OAuth" ==> C[Remote<br>MCP Server]
|
|
43
|
+
B -. "OAuth 2.1<br>(PKCE)" .-> D[Authorization<br>Server]
|
|
44
|
+
D -. callback .-> B
|
|
40
45
|
style B fill:#4a5,stroke:#333,color:#fff
|
|
41
46
|
```
|
|
42
47
|
|
|
43
|
-
Bearer tokens
|
|
48
|
+
Bearer tokens, custom headers, and OAuth 2.1 credentials are forwarded to the remote server.
|
|
44
49
|
|
|
45
50
|
## Install
|
|
46
51
|
|
|
@@ -156,6 +161,7 @@ Works around known issues in Claude Code's HTTP transport:
|
|
|
156
161
|
- **Bearer token not sent** — Claude Code ignores `Authorization` header on tool calls ([#28293](https://github.com/anthropics/claude-code/issues/28293), [#33817](https://github.com/anthropics/claude-code/issues/33817))
|
|
157
162
|
- **Missing Accept header** — servers return 406, misinterpreted as auth failure ([#42470](https://github.com/anthropics/claude-code/issues/42470))
|
|
158
163
|
- **OAuth fallback loop** — Claude Code enters OAuth discovery even when not needed ([#34008](https://github.com/anthropics/claude-code/issues/34008), [#39271](https://github.com/anthropics/claude-code/issues/39271))
|
|
164
|
+
- **Session lost after disconnect** — mcp-stdio recovers MCP sessions automatically on 404 ([#34498](https://github.com/anthropics/claude-code/issues/34498), [#38631](https://github.com/anthropics/claude-code/issues/38631))
|
|
159
165
|
- **Proxy settings ignored** — Claude Code does not respect `NO_PROXY` ([#34804](https://github.com/anthropics/claude-code/issues/34804)); mcp-stdio inherits proxy settings from httpx
|
|
160
166
|
|
|
161
167
|
## Features
|
|
@@ -186,6 +192,8 @@ Works around known issues in Claude Code's HTTP transport:
|
|
|
186
192
|
5. Maintains the `Mcp-Session-Id` header across requests
|
|
187
193
|
6. On 401, refreshes the OAuth token and retries; on 404, resets the session
|
|
188
194
|
|
|
195
|
+
OAuth tokens are stored in `~/.config/mcp-stdio/tokens.json` (permissions 0600).
|
|
196
|
+
|
|
189
197
|
## License
|
|
190
198
|
|
|
191
199
|
MIT
|
|
@@ -9,13 +9,15 @@ MCP サーバー向け stdio-to-HTTP リレー — Claude Desktop/Code とリモ
|
|
|
9
9
|
[MCP](https://modelcontextprotocol.io/) クライアント(Claude Desktop, Claude Code)に対してローカルで稼働するセルフホスト MCP サーバのように振る舞いつつ、リモート MCP サーバへの Streamable HTTP 接続を橋渡しします:
|
|
10
10
|
|
|
11
11
|
```mermaid
|
|
12
|
-
flowchart
|
|
12
|
+
flowchart BT
|
|
13
13
|
A[Claude<br>CLI/Desktop] <-- stdio --> B(mcp-stdio)
|
|
14
|
-
B <== "HTTPS<br>
|
|
14
|
+
B <== "<b>HTTPS</b><br>Bearer Token<br>Header<br>OAuth" ==> C[Remote<br>MCP Server]
|
|
15
|
+
B -. "OAuth 2.1<br>(PKCE)" .-> D[Authorization<br>Server]
|
|
16
|
+
D -. callback .-> B
|
|
15
17
|
style B fill:#4a5,stroke:#333,color:#fff
|
|
16
18
|
```
|
|
17
19
|
|
|
18
|
-
Bearer token
|
|
20
|
+
Bearer token、カスタムヘッダー、OAuth 2.1 認証情報をリモートサーバーへ転送します。
|
|
19
21
|
|
|
20
22
|
## インストール
|
|
21
23
|
|
|
@@ -131,7 +133,8 @@ Claude Code の HTTP transport の既知の問題を回避できます:
|
|
|
131
133
|
- **Bearer token が送信されない** — ツール呼び出し時に `Authorization` ヘッダーが無視される([#28293](https://github.com/anthropics/claude-code/issues/28293), [#33817](https://github.com/anthropics/claude-code/issues/33817))
|
|
132
134
|
- **Accept ヘッダーの欠落** — サーバーが 406 を返し、認証エラーと誤認される([#42470](https://github.com/anthropics/claude-code/issues/42470))
|
|
133
135
|
- **OAuth フォールバックループ** — OAuth 不要なサーバーでも OAuth 検出が走る([#34008](https://github.com/anthropics/claude-code/issues/34008), [#39271](https://github.com/anthropics/claude-code/issues/39271))
|
|
134
|
-
-
|
|
136
|
+
- **切断後にセッションが失われる** — mcp-stdio は 404 で MCP セッションを自動回復([#34498](https://github.com/anthropics/claude-code/issues/34498), [#38631](https://github.com/anthropics/claude-code/issues/38631))
|
|
137
|
+
- **プロキシ設定が無視される** — Claude Code が `NO_PROXY` を尊重しない([#34804](https://github.com/anthropics/claude-code/issues/34804)); mcp-stdio は httpx 経由でプロキシ設定を��承
|
|
135
138
|
|
|
136
139
|
## 機能
|
|
137
140
|
|
|
@@ -161,6 +164,8 @@ Claude Code の HTTP transport の既知の問題を回避できます:
|
|
|
161
164
|
5. `Mcp-Session-Id` ヘッダーをリクエスト間で維持
|
|
162
165
|
6. 401 で OAuth トークンをリフレッシュしてリトライ、404 でセッションをリセット
|
|
163
166
|
|
|
167
|
+
OAuth トークンは `~/.config/mcp-stdio/tokens.json` に保存されます(パーミッション 0600)。
|
|
168
|
+
|
|
164
169
|
## ライセンス
|
|
165
170
|
|
|
166
171
|
MIT
|
|
@@ -11,13 +11,15 @@ Stdio-to-HTTP relay for MCP servers — bridges Claude Desktop/Code to remote St
|
|
|
11
11
|
[MCP](https://modelcontextprotocol.io/) clients like Claude Desktop and Claude Code see mcp-stdio as a locally running self-hosted MCP server, while it relays all requests to a remote MCP server over Streamable HTTP:
|
|
12
12
|
|
|
13
13
|
```mermaid
|
|
14
|
-
flowchart
|
|
14
|
+
flowchart BT
|
|
15
15
|
A[Claude<br>CLI/Desktop] <-- stdio --> B(mcp-stdio)
|
|
16
|
-
B <== "HTTPS<br>
|
|
16
|
+
B <== "<b>HTTPS</b><br>Bearer Token<br>Header<br>OAuth" ==> C[Remote<br>MCP Server]
|
|
17
|
+
B -. "OAuth 2.1<br>(PKCE)" .-> D[Authorization<br>Server]
|
|
18
|
+
D -. callback .-> B
|
|
17
19
|
style B fill:#4a5,stroke:#333,color:#fff
|
|
18
20
|
```
|
|
19
21
|
|
|
20
|
-
Bearer tokens
|
|
22
|
+
Bearer tokens, custom headers, and OAuth 2.1 credentials are forwarded to the remote server.
|
|
21
23
|
|
|
22
24
|
## Install
|
|
23
25
|
|
|
@@ -133,6 +135,7 @@ Works around known issues in Claude Code's HTTP transport:
|
|
|
133
135
|
- **Bearer token not sent** — Claude Code ignores `Authorization` header on tool calls ([#28293](https://github.com/anthropics/claude-code/issues/28293), [#33817](https://github.com/anthropics/claude-code/issues/33817))
|
|
134
136
|
- **Missing Accept header** — servers return 406, misinterpreted as auth failure ([#42470](https://github.com/anthropics/claude-code/issues/42470))
|
|
135
137
|
- **OAuth fallback loop** — Claude Code enters OAuth discovery even when not needed ([#34008](https://github.com/anthropics/claude-code/issues/34008), [#39271](https://github.com/anthropics/claude-code/issues/39271))
|
|
138
|
+
- **Session lost after disconnect** — mcp-stdio recovers MCP sessions automatically on 404 ([#34498](https://github.com/anthropics/claude-code/issues/34498), [#38631](https://github.com/anthropics/claude-code/issues/38631))
|
|
136
139
|
- **Proxy settings ignored** — Claude Code does not respect `NO_PROXY` ([#34804](https://github.com/anthropics/claude-code/issues/34804)); mcp-stdio inherits proxy settings from httpx
|
|
137
140
|
|
|
138
141
|
## Features
|
|
@@ -163,6 +166,8 @@ Works around known issues in Claude Code's HTTP transport:
|
|
|
163
166
|
5. Maintains the `Mcp-Session-Id` header across requests
|
|
164
167
|
6. On 401, refreshes the OAuth token and retries; on 404, resets the session
|
|
165
168
|
|
|
169
|
+
OAuth tokens are stored in `~/.config/mcp-stdio/tokens.json` (permissions 0600).
|
|
170
|
+
|
|
166
171
|
## License
|
|
167
172
|
|
|
168
173
|
MIT
|
|
@@ -13,6 +13,9 @@ classifiers = [
|
|
|
13
13
|
"Intended Audience :: Developers",
|
|
14
14
|
"License :: OSI Approved :: MIT License",
|
|
15
15
|
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.14",
|
|
16
19
|
"Topic :: Software Development :: Libraries",
|
|
17
20
|
]
|
|
18
21
|
dependencies = [
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"url": "https://github.com/shigechika/mcp-stdio",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "0.0.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"registryBaseUrl": "https://pypi.org",
|
|
15
15
|
"identifier": "mcp-stdio",
|
|
16
|
-
"version": "0.
|
|
16
|
+
"version": "0.0.0",
|
|
17
17
|
"runtimeHint": "uvx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
|
@@ -62,8 +62,7 @@ def _fetch_authorization_server_metadata(
|
|
|
62
62
|
return OAuthMetadata(
|
|
63
63
|
authorization_endpoint=data.get("authorization_endpoint")
|
|
64
64
|
or f"{auth_server_url}/authorize",
|
|
65
|
-
token_endpoint=data.get("token_endpoint")
|
|
66
|
-
or f"{auth_server_url}/token",
|
|
65
|
+
token_endpoint=data.get("token_endpoint") or f"{auth_server_url}/token",
|
|
67
66
|
registration_endpoint=data.get("registration_endpoint") or None,
|
|
68
67
|
)
|
|
69
68
|
except Exception:
|
|
@@ -71,9 +70,7 @@ def _fetch_authorization_server_metadata(
|
|
|
71
70
|
return None
|
|
72
71
|
|
|
73
72
|
|
|
74
|
-
def discover_oauth_metadata(
|
|
75
|
-
server_url: str, client: httpx.Client
|
|
76
|
-
) -> OAuthMetadata:
|
|
73
|
+
def discover_oauth_metadata(server_url: str, client: httpx.Client) -> OAuthMetadata:
|
|
77
74
|
"""Discover OAuth authorization server metadata.
|
|
78
75
|
|
|
79
76
|
Follows the MCP spec discovery flow:
|
|
@@ -224,10 +221,7 @@ def _make_callback_handler(
|
|
|
224
221
|
msg = html.escape(result.error)
|
|
225
222
|
body = f"<h1>Authorization failed</h1><p>{msg}</p>"
|
|
226
223
|
else:
|
|
227
|
-
body =
|
|
228
|
-
"<h1>Authorization successful</h1>"
|
|
229
|
-
"<p>You can close this tab.</p>"
|
|
230
|
-
)
|
|
224
|
+
body = "<h1>Authorization successful</h1><p>You can close this tab.</p>"
|
|
231
225
|
self.wfile.write(body.encode())
|
|
232
226
|
|
|
233
227
|
def log_message(self, format: str, *args: Any) -> None:
|
|
@@ -407,7 +401,10 @@ def ensure_token(
|
|
|
407
401
|
registration_endpoint=cached.registration_endpoint,
|
|
408
402
|
)
|
|
409
403
|
data = _token_response_to_data(
|
|
410
|
-
raw,
|
|
404
|
+
raw,
|
|
405
|
+
metadata,
|
|
406
|
+
cached.client_id,
|
|
407
|
+
cached.client_secret,
|
|
411
408
|
previous_refresh_token=cached.refresh_token,
|
|
412
409
|
)
|
|
413
410
|
save_token(server_url, data)
|
|
@@ -504,7 +501,13 @@ def ensure_token(
|
|
|
504
501
|
# Token exchange (RFC 8707: include resource indicator)
|
|
505
502
|
log("exchanging authorization code for token")
|
|
506
503
|
raw = exchange_code(
|
|
507
|
-
metadata,
|
|
504
|
+
metadata,
|
|
505
|
+
cid,
|
|
506
|
+
csecret,
|
|
507
|
+
code,
|
|
508
|
+
code_verifier,
|
|
509
|
+
redirect_uri,
|
|
510
|
+
client,
|
|
508
511
|
resource=server_url,
|
|
509
512
|
)
|
|
510
513
|
data = _token_response_to_data(raw, metadata, cid, csecret)
|
|
@@ -38,25 +38,6 @@ def _error_response(message: str, req_id: Any = None) -> str:
|
|
|
38
38
|
)
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
def send_request(
|
|
42
|
-
client: httpx.Client,
|
|
43
|
-
url: str,
|
|
44
|
-
content: str,
|
|
45
|
-
headers: dict[str, str],
|
|
46
|
-
) -> httpx.Response:
|
|
47
|
-
"""Send a request with retry on transient errors."""
|
|
48
|
-
last_error: Exception | None = None
|
|
49
|
-
for attempt in range(1, MAX_RETRIES + 1):
|
|
50
|
-
try:
|
|
51
|
-
return client.post(url, content=content, headers=headers)
|
|
52
|
-
except (httpx.ConnectError, httpx.ReadTimeout, httpx.WriteTimeout) as e:
|
|
53
|
-
last_error = e
|
|
54
|
-
log(f"attempt {attempt}/{MAX_RETRIES} failed: {e}")
|
|
55
|
-
if attempt < MAX_RETRIES:
|
|
56
|
-
time.sleep(RETRY_DELAY * attempt)
|
|
57
|
-
raise last_error # type: ignore[misc]
|
|
58
|
-
|
|
59
|
-
|
|
60
41
|
class _StreamResult:
|
|
61
42
|
"""Result of a streaming request."""
|
|
62
43
|
|
|
@@ -281,9 +262,7 @@ def run(
|
|
|
281
262
|
req_headers = dict(headers)
|
|
282
263
|
if session_id:
|
|
283
264
|
req_headers["Mcp-Session-Id"] = session_id
|
|
284
|
-
result = _post_and_stream(
|
|
285
|
-
client, url, line, req_headers, req_id
|
|
286
|
-
)
|
|
265
|
+
result = _post_and_stream(client, url, line, req_headers, req_id)
|
|
287
266
|
if result is None:
|
|
288
267
|
continue
|
|
289
268
|
else:
|
|
@@ -5,14 +5,18 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
7
|
import stat
|
|
8
|
-
from dataclasses import asdict, dataclass
|
|
8
|
+
from dataclasses import asdict, dataclass
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
_STORE_DIR = Path.home() / ".mcp-stdio"
|
|
13
|
+
_STORE_DIR = Path.home() / ".config" / "mcp-stdio"
|
|
14
14
|
_STORE_FILE = _STORE_DIR / "tokens.json"
|
|
15
15
|
|
|
16
|
+
# Legacy path (v0.3.0 and earlier)
|
|
17
|
+
_LEGACY_STORE_DIR = Path.home() / ".mcp-stdio"
|
|
18
|
+
_LEGACY_STORE_FILE = _LEGACY_STORE_DIR / "tokens.json"
|
|
19
|
+
|
|
16
20
|
|
|
17
21
|
@dataclass
|
|
18
22
|
class TokenData:
|
|
@@ -37,8 +41,27 @@ def _ensure_store_dir() -> None:
|
|
|
37
41
|
_STORE_DIR.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
38
42
|
|
|
39
43
|
|
|
44
|
+
def _migrate_legacy_store() -> None:
|
|
45
|
+
"""Migrate tokens from ~/.mcp-stdio/ to ~/.config/mcp-stdio/ if needed."""
|
|
46
|
+
if not _LEGACY_STORE_FILE.exists():
|
|
47
|
+
return
|
|
48
|
+
if _STORE_FILE.exists():
|
|
49
|
+
# New file already exists — just remove legacy
|
|
50
|
+
_LEGACY_STORE_FILE.unlink()
|
|
51
|
+
else:
|
|
52
|
+
_ensure_store_dir()
|
|
53
|
+
_LEGACY_STORE_FILE.rename(_STORE_FILE)
|
|
54
|
+
os.chmod(_STORE_FILE, stat.S_IRUSR | stat.S_IWUSR)
|
|
55
|
+
# Remove legacy directory if empty
|
|
56
|
+
try:
|
|
57
|
+
_LEGACY_STORE_DIR.rmdir()
|
|
58
|
+
except OSError:
|
|
59
|
+
pass # Not empty or already gone
|
|
60
|
+
|
|
61
|
+
|
|
40
62
|
def _read_store() -> dict[str, Any]:
|
|
41
63
|
"""Read the token store file."""
|
|
64
|
+
_migrate_legacy_store()
|
|
42
65
|
if not _STORE_FILE.exists():
|
|
43
66
|
return {}
|
|
44
67
|
try:
|
|
@@ -48,10 +71,29 @@ def _read_store() -> dict[str, Any]:
|
|
|
48
71
|
|
|
49
72
|
|
|
50
73
|
def _write_store(data: dict[str, Any]) -> None:
|
|
51
|
-
"""Write the token store file with secure permissions.
|
|
74
|
+
"""Write the token store file atomically with secure permissions.
|
|
75
|
+
|
|
76
|
+
Uses a temp file created with 0o600 from the start (no umask window),
|
|
77
|
+
then atomically renames it over the target file so a crash mid-write
|
|
78
|
+
cannot corrupt existing tokens.
|
|
79
|
+
"""
|
|
52
80
|
_ensure_store_dir()
|
|
53
|
-
|
|
54
|
-
|
|
81
|
+
payload = json.dumps(data, indent=2).encode("utf-8")
|
|
82
|
+
tmp_path = _STORE_FILE.with_suffix(_STORE_FILE.suffix + f".tmp.{os.getpid()}")
|
|
83
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
|
|
84
|
+
fd = os.open(tmp_path, flags, stat.S_IRUSR | stat.S_IWUSR) # 0o600
|
|
85
|
+
try:
|
|
86
|
+
with os.fdopen(fd, "wb") as f:
|
|
87
|
+
f.write(payload)
|
|
88
|
+
f.flush()
|
|
89
|
+
os.fsync(f.fileno())
|
|
90
|
+
os.replace(tmp_path, _STORE_FILE)
|
|
91
|
+
except Exception:
|
|
92
|
+
try:
|
|
93
|
+
tmp_path.unlink()
|
|
94
|
+
except OSError:
|
|
95
|
+
pass
|
|
96
|
+
raise
|
|
55
97
|
|
|
56
98
|
|
|
57
99
|
def load_token(server_url: str) -> TokenData | None:
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Tests for mcp_stdio.cli module."""
|
|
2
2
|
|
|
3
|
-
import sys
|
|
4
3
|
from unittest.mock import patch
|
|
5
4
|
|
|
6
5
|
import pytest
|
|
@@ -45,15 +44,27 @@ class TestMain:
|
|
|
45
44
|
assert exc_info.value.code == 2
|
|
46
45
|
|
|
47
46
|
def test_headers_and_bearer_token(self):
|
|
48
|
-
with
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
with (
|
|
48
|
+
patch(
|
|
49
|
+
"sys.argv",
|
|
50
|
+
[
|
|
51
|
+
"mcp-stdio",
|
|
52
|
+
"https://example.com/mcp",
|
|
53
|
+
"--bearer-token",
|
|
54
|
+
"tok123",
|
|
55
|
+
"-H",
|
|
56
|
+
"X-Custom: val",
|
|
57
|
+
],
|
|
58
|
+
),
|
|
59
|
+
patch("mcp_stdio.cli.run") as mock_run,
|
|
60
|
+
):
|
|
54
61
|
main()
|
|
55
62
|
call_kwargs = mock_run.call_args
|
|
56
|
-
headers =
|
|
63
|
+
headers = (
|
|
64
|
+
call_kwargs.kwargs["headers"]
|
|
65
|
+
if call_kwargs.kwargs
|
|
66
|
+
else call_kwargs[1]["headers"]
|
|
67
|
+
)
|
|
57
68
|
# If called positionally
|
|
58
69
|
if not headers:
|
|
59
70
|
headers = call_kwargs[0][1]
|
|
@@ -62,31 +73,49 @@ class TestMain:
|
|
|
62
73
|
|
|
63
74
|
def test_bearer_token_from_env(self, monkeypatch):
|
|
64
75
|
monkeypatch.setenv("MCP_BEARER_TOKEN", "env-token")
|
|
65
|
-
with
|
|
66
|
-
|
|
76
|
+
with (
|
|
77
|
+
patch("sys.argv", ["mcp-stdio", "https://example.com/mcp"]),
|
|
78
|
+
patch("mcp_stdio.cli.run") as mock_run,
|
|
79
|
+
):
|
|
67
80
|
main()
|
|
68
|
-
headers =
|
|
81
|
+
headers = (
|
|
82
|
+
mock_run.call_args[1]["headers"]
|
|
83
|
+
if mock_run.call_args[1]
|
|
84
|
+
else mock_run.call_args[0][1]
|
|
85
|
+
)
|
|
69
86
|
assert headers["Authorization"] == "Bearer env-token"
|
|
70
87
|
|
|
71
88
|
def test_custom_timeouts(self):
|
|
72
|
-
with
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
89
|
+
with (
|
|
90
|
+
patch(
|
|
91
|
+
"sys.argv",
|
|
92
|
+
[
|
|
93
|
+
"mcp-stdio",
|
|
94
|
+
"https://example.com/mcp",
|
|
95
|
+
"--timeout-connect",
|
|
96
|
+
"5",
|
|
97
|
+
"--timeout-read",
|
|
98
|
+
"60",
|
|
99
|
+
],
|
|
100
|
+
),
|
|
101
|
+
patch("mcp_stdio.cli.run") as mock_run,
|
|
102
|
+
):
|
|
78
103
|
main()
|
|
79
104
|
kwargs = mock_run.call_args
|
|
80
105
|
assert kwargs.kwargs["timeout_connect"] == 5.0
|
|
81
106
|
assert kwargs.kwargs["timeout_read"] == 60.0
|
|
82
107
|
|
|
83
108
|
def test_oauth_and_bearer_token_mutually_exclusive(self):
|
|
84
|
-
with patch(
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
109
|
+
with patch(
|
|
110
|
+
"sys.argv",
|
|
111
|
+
[
|
|
112
|
+
"mcp-stdio",
|
|
113
|
+
"https://example.com/mcp",
|
|
114
|
+
"--oauth",
|
|
115
|
+
"--bearer-token",
|
|
116
|
+
"tok",
|
|
117
|
+
],
|
|
118
|
+
):
|
|
90
119
|
with pytest.raises(SystemExit) as exc_info:
|
|
91
120
|
main()
|
|
92
121
|
assert exc_info.value.code == 1
|