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.
- mcp_stdio-0.1.0/.github/workflows/release.yml +58 -0
- mcp_stdio-0.1.0/.gitignore +5 -0
- mcp_stdio-0.1.0/LICENSE +21 -0
- mcp_stdio-0.1.0/PKG-INFO +141 -0
- mcp_stdio-0.1.0/README.ja.md +121 -0
- mcp_stdio-0.1.0/README.md +121 -0
- mcp_stdio-0.1.0/pyproject.toml +31 -0
- mcp_stdio-0.1.0/src/mcp_stdio/__init__.py +3 -0
- mcp_stdio-0.1.0/src/mcp_stdio/cli.py +83 -0
- mcp_stdio-0.1.0/src/mcp_stdio/relay.py +146 -0
- mcp_stdio-0.1.0/uv.lock +104 -0
|
@@ -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
|
mcp_stdio-0.1.0/LICENSE
ADDED
|
@@ -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.
|
mcp_stdio-0.1.0/PKG-INFO
ADDED
|
@@ -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,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()
|
mcp_stdio-0.1.0/uv.lock
ADDED
|
@@ -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
|
+
]
|