mcp-stdio 0.1.0__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.
@@ -0,0 +1,58 @@
1
+ name: Release to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v6
13
+
14
+ - name: Set up Python
15
+ uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+
19
+ - name: Install build tools
20
+ run: pip install build
21
+
22
+ - name: Build package
23
+ run: python -m build
24
+
25
+ - name: Upload artifacts
26
+ uses: actions/upload-artifact@v4
27
+ with:
28
+ name: dist
29
+ path: dist/
30
+
31
+ publish:
32
+ needs: build
33
+ runs-on: ubuntu-latest
34
+ environment: pypi
35
+ permissions:
36
+ id-token: write
37
+ steps:
38
+ - name: Download artifacts
39
+ uses: actions/download-artifact@v4
40
+ with:
41
+ name: dist
42
+ path: dist/
43
+
44
+ - name: Publish to PyPI
45
+ uses: pypa/gh-action-pypi-publish@release/v1
46
+
47
+ github-release:
48
+ needs: publish
49
+ runs-on: ubuntu-latest
50
+ permissions:
51
+ contents: write
52
+ steps:
53
+ - uses: actions/checkout@v6
54
+
55
+ - name: Create GitHub Release
56
+ env:
57
+ GH_TOKEN: ${{ github.token }}
58
+ run: gh release create "${{ github.ref_name }}" --generate-notes
@@ -0,0 +1,5 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ *.egg-info/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shigechika AIKAWA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-stdio
3
+ Version: 0.1.0
4
+ Summary: Stdio-to-HTTP relay for MCP servers
5
+ Project-URL: Homepage, https://github.com/shigechika/mcp-stdio
6
+ Project-URL: Issues, https://github.com/shigechika/mcp-stdio/issues
7
+ Author: Shigechika AIKAWA
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: claude,mcp,proxy,relay,stdio
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: httpx>=0.25.0
19
+ Description-Content-Type: text/markdown
20
+
21
+ # mcp-stdio
22
+
23
+ English | [日本語](README.ja.md)
24
+
25
+ Stdio-to-HTTP relay for MCP servers — bridges Claude Desktop/Code to remote Streamable HTTP endpoints.
26
+
27
+ ## Why?
28
+
29
+ [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:
30
+
31
+ ```mermaid
32
+ graph LR
33
+ A[Claude Desktop / Code] -- stdio --> B[mcp-stdio]
34
+ B -- HTTPS --> C[Remote MCP Server]
35
+ ```
36
+
37
+ It also works around known issues with HTTP transport in Claude Code ([#28293](https://github.com/anthropics/claude-code/issues/28293)) where custom headers are not forwarded on tool calls.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install mcp-stdio
43
+ ```
44
+
45
+ Or with [uv](https://docs.astral.sh/uv/):
46
+
47
+ ```bash
48
+ uv tool install mcp-stdio
49
+ ```
50
+
51
+ Or run directly without installing:
52
+
53
+ ```bash
54
+ uvx mcp-stdio https://your-server.example.com:8080/mcp
55
+ ```
56
+
57
+ ## Quick Start
58
+
59
+ ```bash
60
+ mcp-stdio https://your-server.example.com:8080/mcp
61
+ ```
62
+
63
+ With Bearer token authentication:
64
+
65
+ ```bash
66
+ mcp-stdio https://your-server.example.com:8080/mcp --bearer-token YOUR_TOKEN
67
+ ```
68
+
69
+ With custom headers:
70
+
71
+ ```bash
72
+ mcp-stdio https://your-server.example.com:8080/mcp -H "X-API-Key: YOUR_KEY"
73
+ ```
74
+
75
+ ## Claude Desktop Configuration
76
+
77
+ Add to `claude_desktop_config.json`:
78
+
79
+ ```json
80
+ {
81
+ "mcpServers": {
82
+ "my-remote-server": {
83
+ "command": "mcp-stdio",
84
+ "args": [
85
+ "https://your-server.example.com:8080/mcp",
86
+ "--bearer-token", "YOUR_TOKEN"
87
+ ]
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ Config file locations:
94
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
95
+ - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
96
+ - Linux: `~/.config/Claude/claude_desktop_config.json`
97
+
98
+ ## Claude Code Configuration
99
+
100
+ ```bash
101
+ claude mcp add my-remote-server -- \
102
+ mcp-stdio https://your-server.example.com:8080/mcp \
103
+ --bearer-token YOUR_TOKEN
104
+ ```
105
+
106
+ ## Usage
107
+
108
+ ```
109
+ mcp-stdio [OPTIONS] URL
110
+
111
+ Arguments:
112
+ URL Remote MCP server URL
113
+
114
+ Options:
115
+ --bearer-token TOKEN Bearer token (or set MCP_BEARER_TOKEN env var)
116
+ -H 'Key: Value' Custom header (can be repeated)
117
+ --timeout-connect SEC Connection timeout (default: 10)
118
+ --timeout-read SEC Read timeout (default: 120)
119
+ -V, --version Show version
120
+ -h, --help Show help
121
+ ```
122
+
123
+ ## Features
124
+
125
+ - **Retry with backoff** — retries up to 3 times on connection errors
126
+ - **Session recovery** — resets MCP session ID on 404 and retries
127
+ - **Bearer token auth** — via `--bearer-token` flag or `MCP_BEARER_TOKEN` env var
128
+ - **Custom headers** — pass any header with `-H` (workaround for [#28293](https://github.com/anthropics/claude-code/issues/28293))
129
+ - **Graceful shutdown** — handles SIGTERM/SIGINT
130
+ - **Minimal dependencies** — only [httpx](https://www.python-httpx.org/)
131
+
132
+ ## How It Works
133
+
134
+ 1. Reads JSON-RPC messages from stdin (sent by Claude Desktop/Code)
135
+ 2. Forwards each message as HTTP POST to the remote MCP server
136
+ 3. Parses the response (JSON or SSE) and writes it to stdout
137
+ 4. Maintains the `Mcp-Session-Id` header across requests
138
+
139
+ ## License
140
+
141
+ MIT
@@ -0,0 +1,121 @@
1
+ # mcp-stdio
2
+
3
+ [English](README.md) | 日本語
4
+
5
+ MCP サーバー向け stdio-to-HTTP リレー — Claude Desktop/Code とリモート Streamable HTTP エンドポイントを橋渡しします。
6
+
7
+ ## なぜ必要?
8
+
9
+ [MCP](https://modelcontextprotocol.io/) クライアント(Claude Desktop, Claude Code)に対してローカルで稼働するセルフホスト MCP サーバのように振る舞いつつ、リモート MCP サーバへの Streamable HTTP 接続を橋渡しします:
10
+
11
+ ```mermaid
12
+ graph LR
13
+ A[Claude Desktop / Code] -- stdio --> B[mcp-stdio]
14
+ B -- HTTPS --> C[Remote MCP Server]
15
+ ```
16
+
17
+ Claude Code の HTTP transport でカスタムヘッダーが送れないバグ([#28293](https://github.com/anthropics/claude-code/issues/28293))のワークアラウンドとしても有用です。
18
+
19
+ ## インストール
20
+
21
+ ```bash
22
+ pip install mcp-stdio
23
+ ```
24
+
25
+ [uv](https://docs.astral.sh/uv/) を使う場合:
26
+
27
+ ```bash
28
+ uv tool install mcp-stdio
29
+ ```
30
+
31
+ インストールせずに直接実行:
32
+
33
+ ```bash
34
+ uvx mcp-stdio https://your-server.example.com:8080/mcp
35
+ ```
36
+
37
+ ## クイックスタート
38
+
39
+ ```bash
40
+ mcp-stdio https://your-server.example.com:8080/mcp
41
+ ```
42
+
43
+ Bearer token 認証付き:
44
+
45
+ ```bash
46
+ mcp-stdio https://your-server.example.com:8080/mcp --bearer-token YOUR_TOKEN
47
+ ```
48
+
49
+ カスタムヘッダー付き:
50
+
51
+ ```bash
52
+ mcp-stdio https://your-server.example.com:8080/mcp -H "X-API-Key: YOUR_KEY"
53
+ ```
54
+
55
+ ## Claude Desktop の設定
56
+
57
+ `claude_desktop_config.json` に追加:
58
+
59
+ ```json
60
+ {
61
+ "mcpServers": {
62
+ "my-remote-server": {
63
+ "command": "mcp-stdio",
64
+ "args": [
65
+ "https://your-server.example.com:8080/mcp",
66
+ "--bearer-token", "YOUR_TOKEN"
67
+ ]
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ 設定ファイルの場所:
74
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
75
+ - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
76
+ - Linux: `~/.config/Claude/claude_desktop_config.json`
77
+
78
+ ## Claude Code の設定
79
+
80
+ ```bash
81
+ claude mcp add my-remote-server -- \
82
+ mcp-stdio https://your-server.example.com:8080/mcp \
83
+ --bearer-token YOUR_TOKEN
84
+ ```
85
+
86
+ ## 使い方
87
+
88
+ ```
89
+ mcp-stdio [OPTIONS] URL
90
+
91
+ 引数:
92
+ URL リモート MCP サーバーの URL
93
+
94
+ オプション:
95
+ --bearer-token TOKEN Bearer token(MCP_BEARER_TOKEN 環境変数でも指定可)
96
+ -H 'Key: Value' カスタムヘッダー(複数指定可)
97
+ --timeout-connect SEC 接続タイムアウト(デフォルト: 10秒)
98
+ --timeout-read SEC 読み取りタイムアウト(デフォルト: 120秒)
99
+ -V, --version バージョン表示
100
+ -h, --help ヘルプ表示
101
+ ```
102
+
103
+ ## 機能
104
+
105
+ - **バックオフ付きリトライ** — 接続エラー時に最大3回リトライ
106
+ - **セッション回復** — 404 でセッション ID をリセットして再試行
107
+ - **Bearer token 認証** — `--bearer-token` フラグまたは `MCP_BEARER_TOKEN` 環境変数
108
+ - **カスタムヘッダー** — `-H` で任意のヘッダーを送信([#28293](https://github.com/anthropics/claude-code/issues/28293) のワークアラウンド)
109
+ - **グレースフルシャットダウン** — SIGTERM/SIGINT ハンドリング
110
+ - **最小依存** — [httpx](https://www.python-httpx.org/) のみ
111
+
112
+ ## 仕組み
113
+
114
+ 1. stdin から JSON-RPC メッセージを読み取り(Claude Desktop/Code が送信)
115
+ 2. HTTP POST でリモート MCP サーバーに転送
116
+ 3. レスポンス(JSON または SSE)をパースして stdout に書き出し
117
+ 4. `Mcp-Session-Id` ヘッダーをリクエスト間で維持
118
+
119
+ ## ライセンス
120
+
121
+ MIT
@@ -0,0 +1,121 @@
1
+ # mcp-stdio
2
+
3
+ English | [日本語](README.ja.md)
4
+
5
+ Stdio-to-HTTP relay for MCP servers — bridges Claude Desktop/Code to remote Streamable HTTP endpoints.
6
+
7
+ ## Why?
8
+
9
+ [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:
10
+
11
+ ```mermaid
12
+ graph LR
13
+ A[Claude Desktop / Code] -- stdio --> B[mcp-stdio]
14
+ B -- HTTPS --> C[Remote MCP Server]
15
+ ```
16
+
17
+ It also works around known issues with HTTP transport in Claude Code ([#28293](https://github.com/anthropics/claude-code/issues/28293)) where custom headers are not forwarded on tool calls.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install mcp-stdio
23
+ ```
24
+
25
+ Or with [uv](https://docs.astral.sh/uv/):
26
+
27
+ ```bash
28
+ uv tool install mcp-stdio
29
+ ```
30
+
31
+ Or run directly without installing:
32
+
33
+ ```bash
34
+ uvx mcp-stdio https://your-server.example.com:8080/mcp
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```bash
40
+ mcp-stdio https://your-server.example.com:8080/mcp
41
+ ```
42
+
43
+ With Bearer token authentication:
44
+
45
+ ```bash
46
+ mcp-stdio https://your-server.example.com:8080/mcp --bearer-token YOUR_TOKEN
47
+ ```
48
+
49
+ With custom headers:
50
+
51
+ ```bash
52
+ mcp-stdio https://your-server.example.com:8080/mcp -H "X-API-Key: YOUR_KEY"
53
+ ```
54
+
55
+ ## Claude Desktop Configuration
56
+
57
+ Add to `claude_desktop_config.json`:
58
+
59
+ ```json
60
+ {
61
+ "mcpServers": {
62
+ "my-remote-server": {
63
+ "command": "mcp-stdio",
64
+ "args": [
65
+ "https://your-server.example.com:8080/mcp",
66
+ "--bearer-token", "YOUR_TOKEN"
67
+ ]
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ Config file locations:
74
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
75
+ - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
76
+ - Linux: `~/.config/Claude/claude_desktop_config.json`
77
+
78
+ ## Claude Code Configuration
79
+
80
+ ```bash
81
+ claude mcp add my-remote-server -- \
82
+ mcp-stdio https://your-server.example.com:8080/mcp \
83
+ --bearer-token YOUR_TOKEN
84
+ ```
85
+
86
+ ## Usage
87
+
88
+ ```
89
+ mcp-stdio [OPTIONS] URL
90
+
91
+ Arguments:
92
+ URL Remote MCP server URL
93
+
94
+ Options:
95
+ --bearer-token TOKEN Bearer token (or set MCP_BEARER_TOKEN env var)
96
+ -H 'Key: Value' Custom header (can be repeated)
97
+ --timeout-connect SEC Connection timeout (default: 10)
98
+ --timeout-read SEC Read timeout (default: 120)
99
+ -V, --version Show version
100
+ -h, --help Show help
101
+ ```
102
+
103
+ ## Features
104
+
105
+ - **Retry with backoff** — retries up to 3 times on connection errors
106
+ - **Session recovery** — resets MCP session ID on 404 and retries
107
+ - **Bearer token auth** — via `--bearer-token` flag or `MCP_BEARER_TOKEN` env var
108
+ - **Custom headers** — pass any header with `-H` (workaround for [#28293](https://github.com/anthropics/claude-code/issues/28293))
109
+ - **Graceful shutdown** — handles SIGTERM/SIGINT
110
+ - **Minimal dependencies** — only [httpx](https://www.python-httpx.org/)
111
+
112
+ ## How It Works
113
+
114
+ 1. Reads JSON-RPC messages from stdin (sent by Claude Desktop/Code)
115
+ 2. Forwards each message as HTTP POST to the remote MCP server
116
+ 3. Parses the response (JSON or SSE) and writes it to stdout
117
+ 4. Maintains the `Mcp-Session-Id` header across requests
118
+
119
+ ## License
120
+
121
+ MIT
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "mcp-stdio"
3
+ version = "0.1.0"
4
+ description = "Stdio-to-HTTP relay for MCP servers"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.10"
8
+ authors = [{ name = "Shigechika AIKAWA" }]
9
+ keywords = ["mcp", "stdio", "proxy", "relay", "claude"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Environment :: Console",
13
+ "Intended Audience :: Developers",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3",
16
+ "Topic :: Software Development :: Libraries",
17
+ ]
18
+ dependencies = [
19
+ "httpx>=0.25.0",
20
+ ]
21
+
22
+ [project.scripts]
23
+ mcp-stdio = "mcp_stdio.cli:main"
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/shigechika/mcp-stdio"
27
+ Issues = "https://github.com/shigechika/mcp-stdio/issues"
28
+
29
+ [build-system]
30
+ requires = ["hatchling"]
31
+ build-backend = "hatchling.build"
@@ -0,0 +1,3 @@
1
+ """mcp-stdio: Stdio-to-HTTP relay for MCP servers."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,83 @@
1
+ """Command-line interface for mcp-stdio."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+
9
+ from . import __version__
10
+ from .relay import run
11
+
12
+
13
+ def _parse_header(header: str) -> tuple[str, str]:
14
+ """Parse a header string 'Key: Value' into a tuple."""
15
+ if ":" not in header:
16
+ print(f"error: invalid header format (expected 'Key: Value'): {header}", file=sys.stderr)
17
+ sys.exit(1)
18
+ key, _, value = header.partition(":")
19
+ return key.strip(), value.strip()
20
+
21
+
22
+ def main() -> None:
23
+ """Entry point for mcp-stdio CLI."""
24
+ parser = argparse.ArgumentParser(
25
+ prog="mcp-stdio",
26
+ description="Stdio-to-HTTP relay for MCP servers. "
27
+ "Bridges Claude Desktop/Code (stdio) to remote Streamable HTTP MCP endpoints.",
28
+ )
29
+ parser.add_argument(
30
+ "url",
31
+ help="Remote MCP server URL (e.g., https://example.com:8080/mcp)",
32
+ )
33
+ parser.add_argument(
34
+ "--bearer-token",
35
+ default=os.environ.get("MCP_BEARER_TOKEN", ""),
36
+ help="Bearer token for authentication (or set MCP_BEARER_TOKEN env var)",
37
+ )
38
+ parser.add_argument(
39
+ "-H",
40
+ "--header",
41
+ action="append",
42
+ default=[],
43
+ dest="headers",
44
+ metavar="'Key: Value'",
45
+ help="Custom header to send (can be specified multiple times)",
46
+ )
47
+ parser.add_argument(
48
+ "--timeout-connect",
49
+ type=float,
50
+ default=10,
51
+ help="Connection timeout in seconds (default: 10)",
52
+ )
53
+ parser.add_argument(
54
+ "--timeout-read",
55
+ type=float,
56
+ default=120,
57
+ help="Read timeout in seconds (default: 120)",
58
+ )
59
+ parser.add_argument(
60
+ "-V",
61
+ "--version",
62
+ action="version",
63
+ version=f"%(prog)s {__version__}",
64
+ )
65
+ args = parser.parse_args()
66
+
67
+ # Build headers
68
+ headers: dict[str, str] = {
69
+ "Content-Type": "application/json",
70
+ "Accept": "application/json, text/event-stream",
71
+ }
72
+ if args.bearer_token:
73
+ headers["Authorization"] = f"Bearer {args.bearer_token}"
74
+ for h in args.headers:
75
+ key, value = _parse_header(h)
76
+ headers[key] = value
77
+
78
+ run(
79
+ url=args.url,
80
+ headers=headers,
81
+ timeout_connect=args.timeout_connect,
82
+ timeout_read=args.timeout_read,
83
+ )
@@ -0,0 +1,146 @@
1
+ """Core relay logic: stdin JSON-RPC -> HTTP POST -> stdout."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import signal
7
+ import sys
8
+ import time
9
+ from typing import Any
10
+
11
+ import httpx
12
+
13
+ MAX_RETRIES = 3
14
+ RETRY_DELAY = 1 # seconds
15
+
16
+
17
+ def log(msg: str) -> None:
18
+ """Log to stderr (visible in Claude Desktop/Code logs)."""
19
+ print(f"[mcp-stdio] {msg}", file=sys.stderr, flush=True)
20
+
21
+
22
+ def _extract_id(line: str) -> Any:
23
+ """Extract JSON-RPC id from request line."""
24
+ try:
25
+ return json.loads(line).get("id")
26
+ except (json.JSONDecodeError, AttributeError):
27
+ return None
28
+
29
+
30
+ def _error_response(message: str, req_id: Any = None) -> str:
31
+ """Build a JSON-RPC error response."""
32
+ return json.dumps(
33
+ {
34
+ "jsonrpc": "2.0",
35
+ "error": {"code": -32000, "message": message},
36
+ "id": req_id,
37
+ }
38
+ )
39
+
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
+ def run(
61
+ url: str,
62
+ headers: dict[str, str],
63
+ *,
64
+ timeout_connect: float = 10,
65
+ timeout_read: float = 120,
66
+ timeout_write: float = 30,
67
+ ) -> None:
68
+ """Run the stdio-to-HTTP relay loop.
69
+
70
+ Reads JSON-RPC messages from stdin, sends them as HTTP POST to the
71
+ remote MCP server, and writes responses to stdout.
72
+
73
+ Args:
74
+ url: Remote MCP server URL
75
+ headers: HTTP headers to send with each request
76
+ timeout_connect: Connection timeout in seconds
77
+ timeout_read: Read timeout in seconds
78
+ timeout_write: Write timeout in seconds
79
+ """
80
+ # Graceful shutdown on SIGTERM/SIGINT
81
+ def _shutdown(signum: int, _: Any) -> None:
82
+ log(f"received signal {signum}, shutting down")
83
+ sys.exit(0)
84
+
85
+ signal.signal(signal.SIGTERM, _shutdown)
86
+ signal.signal(signal.SIGINT, _shutdown)
87
+
88
+ log(f"connecting to {url}")
89
+
90
+ session_id: str | None = None
91
+ client = httpx.Client(
92
+ timeout=httpx.Timeout(
93
+ connect=timeout_connect,
94
+ read=timeout_read,
95
+ write=timeout_write,
96
+ pool=10,
97
+ )
98
+ )
99
+
100
+ try:
101
+ for line in sys.stdin:
102
+ line = line.strip()
103
+ if not line:
104
+ continue
105
+
106
+ req_id = _extract_id(line)
107
+
108
+ req_headers = dict(headers)
109
+ if session_id:
110
+ req_headers["Mcp-Session-Id"] = session_id
111
+
112
+ try:
113
+ resp = send_request(client, url, line, req_headers)
114
+ except Exception as e:
115
+ log(f"request failed after retries: {e}")
116
+ session_id = None
117
+ print(_error_response(str(e), req_id), flush=True)
118
+ continue
119
+
120
+ # Session expired (404) — reset and retry
121
+ if resp.status_code == 404 and session_id:
122
+ log("session expired, resetting and retrying")
123
+ session_id = None
124
+ req_headers = dict(headers)
125
+ try:
126
+ resp = send_request(client, url, line, req_headers)
127
+ except Exception as e:
128
+ log(f"retry after session reset failed: {e}")
129
+ print(_error_response(str(e), req_id), flush=True)
130
+ continue
131
+
132
+ # Track session ID
133
+ if "mcp-session-id" in resp.headers:
134
+ session_id = resp.headers["mcp-session-id"]
135
+
136
+ # Parse response
137
+ content_type = resp.headers.get("content-type", "")
138
+ if "text/event-stream" in content_type:
139
+ for event_line in resp.text.splitlines():
140
+ if event_line.startswith("data: "):
141
+ print(event_line[6:], flush=True)
142
+ else:
143
+ if resp.text.strip():
144
+ print(resp.text.strip(), flush=True)
145
+ finally:
146
+ client.close()
@@ -0,0 +1,104 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.10"
4
+
5
+ [[package]]
6
+ name = "anyio"
7
+ version = "4.13.0"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ dependencies = [
10
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
11
+ { name = "idna" },
12
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
13
+ ]
14
+ sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
15
+ wheels = [
16
+ { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
17
+ ]
18
+
19
+ [[package]]
20
+ name = "certifi"
21
+ version = "2026.2.25"
22
+ source = { registry = "https://pypi.org/simple" }
23
+ sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
24
+ wheels = [
25
+ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
26
+ ]
27
+
28
+ [[package]]
29
+ name = "exceptiongroup"
30
+ version = "1.3.1"
31
+ source = { registry = "https://pypi.org/simple" }
32
+ dependencies = [
33
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
34
+ ]
35
+ sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
36
+ wheels = [
37
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
38
+ ]
39
+
40
+ [[package]]
41
+ name = "h11"
42
+ version = "0.16.0"
43
+ source = { registry = "https://pypi.org/simple" }
44
+ sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
45
+ wheels = [
46
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
47
+ ]
48
+
49
+ [[package]]
50
+ name = "httpcore"
51
+ version = "1.0.9"
52
+ source = { registry = "https://pypi.org/simple" }
53
+ dependencies = [
54
+ { name = "certifi" },
55
+ { name = "h11" },
56
+ ]
57
+ sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
58
+ wheels = [
59
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
60
+ ]
61
+
62
+ [[package]]
63
+ name = "httpx"
64
+ version = "0.28.1"
65
+ source = { registry = "https://pypi.org/simple" }
66
+ dependencies = [
67
+ { name = "anyio" },
68
+ { name = "certifi" },
69
+ { name = "httpcore" },
70
+ { name = "idna" },
71
+ ]
72
+ sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
73
+ wheels = [
74
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
75
+ ]
76
+
77
+ [[package]]
78
+ name = "idna"
79
+ version = "3.11"
80
+ source = { registry = "https://pypi.org/simple" }
81
+ sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
82
+ wheels = [
83
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
84
+ ]
85
+
86
+ [[package]]
87
+ name = "mcp-stdio"
88
+ version = "0.1.0"
89
+ source = { editable = "." }
90
+ dependencies = [
91
+ { name = "httpx" },
92
+ ]
93
+
94
+ [package.metadata]
95
+ requires-dist = [{ name = "httpx", specifier = ">=0.25.0" }]
96
+
97
+ [[package]]
98
+ name = "typing-extensions"
99
+ version = "4.15.0"
100
+ source = { registry = "https://pypi.org/simple" }
101
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
102
+ wheels = [
103
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
104
+ ]