mcp-stdio 0.3.2__tar.gz → 0.3.3__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.2 → mcp_stdio-0.3.3}/.github/workflows/release.yml +10 -0
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/PKG-INFO +2 -1
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/README.ja.md +2 -1
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/README.md +1 -0
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/src/mcp_stdio/__init__.py +1 -1
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/src/mcp_stdio/relay.py +74 -9
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/tests/test_relay.py +120 -18
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/.github/workflows/test.yml +0 -0
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/.gitignore +0 -0
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/CLAUDE.md +0 -0
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/LICENSE +0 -0
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/pyproject.toml +0 -0
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/server.json +0 -0
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/src/mcp_stdio/__main__.py +0 -0
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/src/mcp_stdio/cli.py +0 -0
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/src/mcp_stdio/oauth.py +0 -0
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/src/mcp_stdio/token_store.py +0 -0
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/tests/__init__.py +0 -0
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/tests/test_cli.py +0 -0
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/tests/test_oauth.py +0 -0
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/tests/test_token_store.py +0 -0
- {mcp_stdio-0.3.2 → mcp_stdio-0.3.3}/uv.lock +0 -0
|
@@ -11,6 +11,16 @@ jobs:
|
|
|
11
11
|
steps:
|
|
12
12
|
- uses: actions/checkout@v6
|
|
13
13
|
|
|
14
|
+
- name: Verify __version__ matches tag
|
|
15
|
+
run: |
|
|
16
|
+
TAG="${GITHUB_REF_NAME#v}"
|
|
17
|
+
PKG_VERSION=$(python3 -c "import re; print(re.search(r'__version__ = \"([^\"]+)\"', open('src/mcp_stdio/__init__.py').read()).group(1))")
|
|
18
|
+
if [ "$TAG" != "$PKG_VERSION" ]; then
|
|
19
|
+
echo "::error::Tag v$TAG does not match __version__ $PKG_VERSION in src/mcp_stdio/__init__.py"
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
echo "Verified: tag v$TAG matches __version__ $PKG_VERSION"
|
|
23
|
+
|
|
14
24
|
- name: Set up Python
|
|
15
25
|
uses: actions/setup-python@v6
|
|
16
26
|
with:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-stdio
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
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
|
|
@@ -162,6 +162,7 @@ Works around known issues in Claude Code's HTTP transport:
|
|
|
162
162
|
- **Missing Accept header** — servers return 406, misinterpreted as auth failure ([#42470](https://github.com/anthropics/claude-code/issues/42470))
|
|
163
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
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))
|
|
165
|
+
- **OAuth scope omitted** — Claude Code sends no `scope` parameter in authorization requests, causing strict OAuth servers to reject the flow ([#4540](https://github.com/anthropics/claude-code/issues/4540)); mcp-stdio sends scopes via `--oauth-scope`
|
|
165
166
|
- **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
|
|
166
167
|
|
|
167
168
|
## Features
|
|
@@ -134,7 +134,8 @@ Claude Code の HTTP transport の既知の問題を回避できます:
|
|
|
134
134
|
- **Accept ヘッダーの欠落** — サーバーが 406 を返し、認証エラーと誤認される([#42470](https://github.com/anthropics/claude-code/issues/42470))
|
|
135
135
|
- **OAuth フォールバックループ** — OAuth 不要なサーバーでも OAuth 検出が走る([#34008](https://github.com/anthropics/claude-code/issues/34008), [#39271](https://github.com/anthropics/claude-code/issues/39271))
|
|
136
136
|
- **切断後にセッションが失われる** — mcp-stdio は 404 で MCP セッションを自動回復([#34498](https://github.com/anthropics/claude-code/issues/34498), [#38631](https://github.com/anthropics/claude-code/issues/38631))
|
|
137
|
-
-
|
|
137
|
+
- **OAuth scope が送信されない** — 認可リクエストに `scope` パラメータが含まれず、厳格な OAuth サーバーがフローを拒否する([#4540](https://github.com/anthropics/claude-code/issues/4540)); mcp-stdio は `--oauth-scope` でスコープを送信
|
|
138
|
+
- **プロキシ設定が無視される** — Claude Code が `NO_PROXY` を尊重しない([#34804](https://github.com/anthropics/claude-code/issues/34804)); mcp-stdio は httpx 経由でプロキシ設定を継承
|
|
138
139
|
|
|
139
140
|
## 機能
|
|
140
141
|
|
|
@@ -136,6 +136,7 @@ Works around known issues in Claude Code's HTTP transport:
|
|
|
136
136
|
- **Missing Accept header** — servers return 406, misinterpreted as auth failure ([#42470](https://github.com/anthropics/claude-code/issues/42470))
|
|
137
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
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))
|
|
139
|
+
- **OAuth scope omitted** — Claude Code sends no `scope` parameter in authorization requests, causing strict OAuth servers to reject the flow ([#4540](https://github.com/anthropics/claude-code/issues/4540)); mcp-stdio sends scopes via `--oauth-scope`
|
|
139
140
|
- **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
|
|
140
141
|
|
|
141
142
|
## Features
|
|
@@ -10,6 +10,8 @@ from typing import Any
|
|
|
10
10
|
|
|
11
11
|
import httpx
|
|
12
12
|
|
|
13
|
+
from mcp_stdio import __version__
|
|
14
|
+
|
|
13
15
|
MAX_RETRIES = 3
|
|
14
16
|
RETRY_DELAY = 1 # seconds
|
|
15
17
|
|
|
@@ -99,6 +101,66 @@ def _post_and_stream(
|
|
|
99
101
|
return None
|
|
100
102
|
|
|
101
103
|
|
|
104
|
+
def _reinitialize(
|
|
105
|
+
client: httpx.Client,
|
|
106
|
+
url: str,
|
|
107
|
+
headers: dict[str, str],
|
|
108
|
+
) -> str | None:
|
|
109
|
+
"""Send an initialize handshake to establish a new MCP session.
|
|
110
|
+
|
|
111
|
+
Used to recover after a session expires (server returns 404 on the
|
|
112
|
+
next request). Performs the full MCP initialize handshake:
|
|
113
|
+
|
|
114
|
+
1. POST an ``initialize`` request to get a new session ID
|
|
115
|
+
2. POST a ``notifications/initialized`` notification to signal
|
|
116
|
+
readiness (required by the MCP spec before any other requests)
|
|
117
|
+
|
|
118
|
+
Returns the new session ID on success, or None on failure. The
|
|
119
|
+
initialize response payload is discarded — the caller only needs
|
|
120
|
+
the session ID for subsequent requests.
|
|
121
|
+
"""
|
|
122
|
+
initialize_msg = json.dumps(
|
|
123
|
+
{
|
|
124
|
+
"jsonrpc": "2.0",
|
|
125
|
+
"method": "initialize",
|
|
126
|
+
"id": 0,
|
|
127
|
+
"params": {
|
|
128
|
+
"protocolVersion": "2024-11-05",
|
|
129
|
+
"capabilities": {},
|
|
130
|
+
"clientInfo": {"name": "mcp-stdio", "version": __version__},
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
try:
|
|
135
|
+
resp = client.post(url, content=initialize_msg, headers=headers)
|
|
136
|
+
except httpx.HTTPError as e:
|
|
137
|
+
log(f"re-initialize request failed: {e}")
|
|
138
|
+
return None
|
|
139
|
+
if resp.status_code != 200:
|
|
140
|
+
log(f"re-initialize returned HTTP {resp.status_code}")
|
|
141
|
+
return None
|
|
142
|
+
new_session_id = resp.headers.get("mcp-session-id")
|
|
143
|
+
if not new_session_id:
|
|
144
|
+
log("re-initialize response missing mcp-session-id header")
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
# MCP spec: send notifications/initialized before any other requests
|
|
148
|
+
initialized_msg = json.dumps(
|
|
149
|
+
{"jsonrpc": "2.0", "method": "notifications/initialized"}
|
|
150
|
+
)
|
|
151
|
+
initialized_headers = dict(headers)
|
|
152
|
+
initialized_headers["Mcp-Session-Id"] = new_session_id
|
|
153
|
+
try:
|
|
154
|
+
resp = client.post(url, content=initialized_msg, headers=initialized_headers)
|
|
155
|
+
except httpx.HTTPError as e:
|
|
156
|
+
log(f"notifications/initialized failed: {e}")
|
|
157
|
+
return None
|
|
158
|
+
if resp.status_code not in (200, 202):
|
|
159
|
+
log(f"notifications/initialized returned HTTP {resp.status_code}")
|
|
160
|
+
return None
|
|
161
|
+
return new_session_id
|
|
162
|
+
|
|
163
|
+
|
|
102
164
|
def test_connection(
|
|
103
165
|
url: str,
|
|
104
166
|
headers: dict[str, str],
|
|
@@ -118,15 +180,13 @@ def test_connection(
|
|
|
118
180
|
"params": {
|
|
119
181
|
"protocolVersion": "2024-11-05",
|
|
120
182
|
"capabilities": {},
|
|
121
|
-
"clientInfo": {"name": "mcp-stdio", "version":
|
|
183
|
+
"clientInfo": {"name": "mcp-stdio", "version": __version__},
|
|
122
184
|
},
|
|
123
185
|
}
|
|
124
186
|
)
|
|
125
187
|
|
|
126
188
|
client = httpx.Client(
|
|
127
|
-
timeout=httpx.Timeout(
|
|
128
|
-
connect=timeout_connect, read=timeout_read, write=30, pool=10
|
|
129
|
-
)
|
|
189
|
+
timeout=httpx.Timeout(connect=timeout_connect, read=timeout_read, write=30, pool=10)
|
|
130
190
|
)
|
|
131
191
|
|
|
132
192
|
try:
|
|
@@ -169,9 +229,7 @@ def test_connection(
|
|
|
169
229
|
tools = "yes" if caps.get("tools") else "no"
|
|
170
230
|
resources = "yes" if caps.get("resources") else "no"
|
|
171
231
|
prompts = "yes" if caps.get("prompts") else "no"
|
|
172
|
-
log(
|
|
173
|
-
f"✓ Capabilities: tools={tools}, resources={resources}, prompts={prompts}"
|
|
174
|
-
)
|
|
232
|
+
log(f"✓ Capabilities: tools={tools}, resources={resources}, prompts={prompts}")
|
|
175
233
|
elif result_data and "error" in result_data:
|
|
176
234
|
err = result_data["error"]
|
|
177
235
|
log(f"✗ MCP error: {err.get('message', err)}")
|
|
@@ -273,11 +331,18 @@ def run(
|
|
|
273
331
|
)
|
|
274
332
|
continue
|
|
275
333
|
|
|
276
|
-
# Session expired (404) — reset
|
|
334
|
+
# Session expired (404) — reset, re-initialize, then retry
|
|
277
335
|
if result.status_code == 404 and session_id:
|
|
278
|
-
log("session expired,
|
|
336
|
+
log("session expired, re-initializing and retrying")
|
|
279
337
|
session_id = None
|
|
338
|
+
new_session_id = _reinitialize(client, url, dict(headers))
|
|
339
|
+
if new_session_id is None:
|
|
340
|
+
log("re-initialize failed, dropping request")
|
|
341
|
+
print(_error_response("session lost", req_id), flush=True)
|
|
342
|
+
continue
|
|
343
|
+
session_id = new_session_id
|
|
280
344
|
req_headers = dict(headers)
|
|
345
|
+
req_headers["Mcp-Session-Id"] = session_id
|
|
281
346
|
result = _post_and_stream(client, url, line, req_headers, req_id)
|
|
282
347
|
if result is None:
|
|
283
348
|
continue
|
|
@@ -83,9 +83,7 @@ class TestPostAndStream:
|
|
|
83
83
|
httpx_mock.add_exception(httpx.ConnectError("refused"))
|
|
84
84
|
client = httpx.Client()
|
|
85
85
|
with patch("mcp_stdio.relay.time.sleep"):
|
|
86
|
-
result = _post_and_stream(
|
|
87
|
-
client, "https://example.com/mcp", '{"id":1}', {}, 1
|
|
88
|
-
)
|
|
86
|
+
result = _post_and_stream(client, "https://example.com/mcp", '{"id":1}', {}, 1)
|
|
89
87
|
assert result is None
|
|
90
88
|
|
|
91
89
|
def test_non_200_returns_status(self, httpx_mock):
|
|
@@ -120,9 +118,7 @@ class TestRun:
|
|
|
120
118
|
text=body,
|
|
121
119
|
headers={"content-type": "application/json"},
|
|
122
120
|
)
|
|
123
|
-
output = self._run_with_stdin(
|
|
124
|
-
httpx_mock, ['{"jsonrpc":"2.0","method":"init","id":1}']
|
|
125
|
-
)
|
|
121
|
+
output = self._run_with_stdin(httpx_mock, ['{"jsonrpc":"2.0","method":"init","id":1}'])
|
|
126
122
|
assert json.loads(output.strip()) == json.loads(body)
|
|
127
123
|
|
|
128
124
|
def test_sse_response(self, httpx_mock):
|
|
@@ -131,9 +127,7 @@ class TestRun:
|
|
|
131
127
|
text=sse_body,
|
|
132
128
|
headers={"content-type": "text/event-stream"},
|
|
133
129
|
)
|
|
134
|
-
output = self._run_with_stdin(
|
|
135
|
-
httpx_mock, ['{"jsonrpc":"2.0","method":"init","id":1}']
|
|
136
|
-
)
|
|
130
|
+
output = self._run_with_stdin(httpx_mock, ['{"jsonrpc":"2.0","method":"init","id":1}'])
|
|
137
131
|
assert json.loads(output.strip())["id"] == 1
|
|
138
132
|
|
|
139
133
|
def test_empty_lines_skipped(self, httpx_mock):
|
|
@@ -173,8 +167,16 @@ class TestRun:
|
|
|
173
167
|
req2 = httpx_mock.get_requests()[1]
|
|
174
168
|
assert req2.headers["mcp-session-id"] == "sess-123"
|
|
175
169
|
|
|
176
|
-
def
|
|
177
|
-
|
|
170
|
+
def test_session_expired_triggers_reinitialize_then_retry(self, httpx_mock):
|
|
171
|
+
"""Reproduces the 404 -> 400 hang from FastMCP StreamableHTTP.
|
|
172
|
+
|
|
173
|
+
Before the fix, mcp-stdio cleared the session_id on 404 and just
|
|
174
|
+
re-sent the original request — but FastMCP requires an initialize
|
|
175
|
+
handshake on each new session, so the retry came back 400 and the
|
|
176
|
+
caller hung. The fix sends an initialize to establish a new
|
|
177
|
+
session first, then replays the original request with it.
|
|
178
|
+
"""
|
|
179
|
+
# Request 1: init — server assigns sess-old
|
|
178
180
|
httpx_mock.add_response(
|
|
179
181
|
text='{"jsonrpc":"2.0","result":{},"id":1}',
|
|
180
182
|
headers={
|
|
@@ -182,17 +184,28 @@ class TestRun:
|
|
|
182
184
|
"mcp-session-id": "sess-old",
|
|
183
185
|
},
|
|
184
186
|
)
|
|
185
|
-
#
|
|
187
|
+
# Request 2: tool call with sess-old — server returns 404 (expired)
|
|
186
188
|
httpx_mock.add_response(
|
|
187
189
|
status_code=404,
|
|
188
190
|
text="",
|
|
189
191
|
headers={"content-type": "application/json"},
|
|
190
192
|
)
|
|
191
|
-
#
|
|
193
|
+
# Request 3: _reinitialize sends a fresh initialize — server assigns sess-new
|
|
194
|
+
httpx_mock.add_response(
|
|
195
|
+
text='{"jsonrpc":"2.0","result":{"protocolVersion":"2024-11-05"},"id":0}',
|
|
196
|
+
headers={
|
|
197
|
+
"content-type": "application/json",
|
|
198
|
+
"mcp-session-id": "sess-new",
|
|
199
|
+
},
|
|
200
|
+
)
|
|
201
|
+
# Request 4: _reinitialize sends notifications/initialized with sess-new
|
|
202
|
+
httpx_mock.add_response(status_code=202, text="")
|
|
203
|
+
# Request 5: original tool call replayed with sess-new — server returns result
|
|
192
204
|
httpx_mock.add_response(
|
|
193
205
|
text='{"jsonrpc":"2.0","result":{"ok":true},"id":2}',
|
|
194
206
|
headers={"content-type": "application/json"},
|
|
195
207
|
)
|
|
208
|
+
|
|
196
209
|
output = self._run_with_stdin(
|
|
197
210
|
httpx_mock,
|
|
198
211
|
[
|
|
@@ -201,19 +214,108 @@ class TestRun:
|
|
|
201
214
|
],
|
|
202
215
|
)
|
|
203
216
|
lines = [x for x in output.strip().splitlines() if x]
|
|
217
|
+
# stdout gets the two original responses (init + call); the re-initialize
|
|
218
|
+
# handshake is internal and should not leak to stdout.
|
|
204
219
|
assert len(lines) == 2
|
|
205
|
-
|
|
220
|
+
assert json.loads(lines[1])["result"] == {"ok": True}
|
|
221
|
+
|
|
206
222
|
requests = httpx_mock.get_requests()
|
|
207
|
-
assert len(requests) ==
|
|
223
|
+
assert len(requests) == 5
|
|
224
|
+
|
|
225
|
+
# Request 2 (the call) still carried sess-old before the 404
|
|
226
|
+
assert requests[1].headers.get("mcp-session-id") == "sess-old"
|
|
227
|
+
|
|
228
|
+
# Request 3 is the re-initialize: no session header, body is initialize
|
|
208
229
|
assert "mcp-session-id" not in requests[2].headers
|
|
230
|
+
init_body = json.loads(requests[2].content)
|
|
231
|
+
assert init_body["method"] == "initialize"
|
|
232
|
+
|
|
233
|
+
# Request 4 is notifications/initialized with sess-new
|
|
234
|
+
assert requests[3].headers.get("mcp-session-id") == "sess-new"
|
|
235
|
+
notif_body = json.loads(requests[3].content)
|
|
236
|
+
assert notif_body["method"] == "notifications/initialized"
|
|
237
|
+
assert "id" not in notif_body # notifications must not carry an id
|
|
238
|
+
|
|
239
|
+
# Request 5 is the replayed tool call, now with sess-new
|
|
240
|
+
assert requests[4].headers.get("mcp-session-id") == "sess-new"
|
|
241
|
+
replay_body = json.loads(requests[4].content)
|
|
242
|
+
assert replay_body["method"] == "call"
|
|
243
|
+
assert replay_body["id"] == 2
|
|
244
|
+
|
|
245
|
+
def test_reinitialize_failure_returns_error(self, httpx_mock):
|
|
246
|
+
"""If the post-404 re-initialize fails, we surface a JSON-RPC error
|
|
247
|
+
instead of silently dropping the original request."""
|
|
248
|
+
# Request 1: init
|
|
249
|
+
httpx_mock.add_response(
|
|
250
|
+
text='{"jsonrpc":"2.0","result":{},"id":1}',
|
|
251
|
+
headers={
|
|
252
|
+
"content-type": "application/json",
|
|
253
|
+
"mcp-session-id": "sess-old",
|
|
254
|
+
},
|
|
255
|
+
)
|
|
256
|
+
# Request 2: tool call -> 404
|
|
257
|
+
httpx_mock.add_response(status_code=404, text="")
|
|
258
|
+
# Request 3: re-initialize also fails (server still broken)
|
|
259
|
+
httpx_mock.add_response(status_code=500, text="")
|
|
260
|
+
|
|
261
|
+
output = self._run_with_stdin(
|
|
262
|
+
httpx_mock,
|
|
263
|
+
[
|
|
264
|
+
'{"jsonrpc":"2.0","method":"init","id":1}',
|
|
265
|
+
'{"jsonrpc":"2.0","method":"call","id":2}',
|
|
266
|
+
],
|
|
267
|
+
)
|
|
268
|
+
lines = [x for x in output.strip().splitlines() if x]
|
|
269
|
+
# First response goes through, second is an error reply (not a hang)
|
|
270
|
+
assert len(lines) == 2
|
|
271
|
+
err = json.loads(lines[1])
|
|
272
|
+
assert err["id"] == 2
|
|
273
|
+
assert err["error"]["code"] == -32000
|
|
274
|
+
assert "session lost" in err["error"]["message"]
|
|
275
|
+
|
|
276
|
+
def test_reinitialize_notifications_initialized_failure_returns_error(
|
|
277
|
+
self, httpx_mock
|
|
278
|
+
):
|
|
279
|
+
"""If the initialize succeeds but the notifications/initialized step
|
|
280
|
+
fails, we treat the whole re-init as failed and surface an error."""
|
|
281
|
+
httpx_mock.add_response(
|
|
282
|
+
text='{"jsonrpc":"2.0","result":{},"id":1}',
|
|
283
|
+
headers={
|
|
284
|
+
"content-type": "application/json",
|
|
285
|
+
"mcp-session-id": "sess-old",
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
httpx_mock.add_response(status_code=404, text="")
|
|
289
|
+
# Initialize succeeds — server assigns sess-new
|
|
290
|
+
httpx_mock.add_response(
|
|
291
|
+
text='{"jsonrpc":"2.0","result":{},"id":0}',
|
|
292
|
+
headers={
|
|
293
|
+
"content-type": "application/json",
|
|
294
|
+
"mcp-session-id": "sess-new",
|
|
295
|
+
},
|
|
296
|
+
)
|
|
297
|
+
# notifications/initialized fails
|
|
298
|
+
httpx_mock.add_response(status_code=500, text="")
|
|
299
|
+
|
|
300
|
+
output = self._run_with_stdin(
|
|
301
|
+
httpx_mock,
|
|
302
|
+
[
|
|
303
|
+
'{"jsonrpc":"2.0","method":"init","id":1}',
|
|
304
|
+
'{"jsonrpc":"2.0","method":"call","id":2}',
|
|
305
|
+
],
|
|
306
|
+
)
|
|
307
|
+
lines = [x for x in output.strip().splitlines() if x]
|
|
308
|
+
assert len(lines) == 2
|
|
309
|
+
err = json.loads(lines[1])
|
|
310
|
+
assert err["id"] == 2
|
|
311
|
+
assert err["error"]["code"] == -32000
|
|
312
|
+
assert "session lost" in err["error"]["message"]
|
|
209
313
|
|
|
210
314
|
def test_request_failure_returns_error(self, httpx_mock):
|
|
211
315
|
for _ in range(3):
|
|
212
316
|
httpx_mock.add_exception(httpx.ConnectError("refused"))
|
|
213
317
|
with patch("mcp_stdio.relay.time.sleep"):
|
|
214
|
-
output = self._run_with_stdin(
|
|
215
|
-
httpx_mock, ['{"jsonrpc":"2.0","method":"init","id":5}']
|
|
216
|
-
)
|
|
318
|
+
output = self._run_with_stdin(httpx_mock, ['{"jsonrpc":"2.0","method":"init","id":5}'])
|
|
217
319
|
result = json.loads(output.strip())
|
|
218
320
|
assert result["error"]["code"] == -32000
|
|
219
321
|
assert result["id"] == 5
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|