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.
@@ -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.0
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 LR
40
+ flowchart BT
38
41
  A[Claude<br>CLI/Desktop] <-- stdio --> B(mcp-stdio)
39
- B <== "HTTPS<br>(Bearer/Headers)" ==> C[Remote<br>MCP Server]
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 and custom headers are forwarded to the remote server.
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 LR
12
+ flowchart BT
13
13
  A[Claude<br>CLI/Desktop] <-- stdio --> B(mcp-stdio)
14
- B <== "HTTPS<br>(Bearer/Headers)" ==> C[Remote<br>MCP Server]
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
- - **プロキシ設定が無視される**Claude Code `NO_PROXY` を尊重しない([#34804](https://github.com/anthropics/claude-code/issues/34804)); mcp-stdio は httpx 経由でプロキシ設定を継承
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 LR
14
+ flowchart BT
15
15
  A[Claude<br>CLI/Desktop] <-- stdio --> B(mcp-stdio)
16
- B <== "HTTPS<br>(Bearer/Headers)" ==> C[Remote<br>MCP Server]
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 and custom headers are forwarded to the remote server.
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.2.1",
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.2.1",
16
+ "version": "0.0.0",
17
17
  "runtimeHint": "uvx",
18
18
  "transport": {
19
19
  "type": "stdio"
@@ -1,3 +1,3 @@
1
1
  """mcp-stdio: Stdio-to-HTTP relay for MCP servers."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.3.2"
@@ -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, metadata, cached.client_id, cached.client_secret,
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, cid, csecret, code, code_verifier, redirect_uri, client,
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, field
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
- _STORE_FILE.write_text(json.dumps(data, indent=2))
54
- os.chmod(_STORE_FILE, stat.S_IRUSR | stat.S_IWUSR) # 0o600
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 patch("sys.argv", [
49
- "mcp-stdio",
50
- "https://example.com/mcp",
51
- "--bearer-token", "tok123",
52
- "-H", "X-Custom: val",
53
- ]), patch("mcp_stdio.cli.run") as mock_run:
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 = call_kwargs.kwargs["headers"] if call_kwargs.kwargs else call_kwargs[1]["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 patch("sys.argv", ["mcp-stdio", "https://example.com/mcp"]), \
66
- patch("mcp_stdio.cli.run") as mock_run:
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 = mock_run.call_args[1]["headers"] if mock_run.call_args[1] else mock_run.call_args[0][1]
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 patch("sys.argv", [
73
- "mcp-stdio",
74
- "https://example.com/mcp",
75
- "--timeout-connect", "5",
76
- "--timeout-read", "60",
77
- ]), patch("mcp_stdio.cli.run") as mock_run:
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("sys.argv", [
85
- "mcp-stdio",
86
- "https://example.com/mcp",
87
- "--oauth",
88
- "--bearer-token", "tok",
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