Skip to content

Commit 0f7f517

Browse files
committed
test(mcp): cover in-flight request failing on subprocess EOF
Stub completes initialize then exits on the first tool call, closing stdout mid-request; assert call_tool raises MCPError rather than hanging.
1 parent 6993d83 commit 0f7f517

1 file changed

Lines changed: 43 additions & 1 deletion

File tree

tests/test_mcp.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
No real MCP server dependency.
66
"""
77

8+
import asyncio
89
import os
910
import sys
1011
import tempfile
1112

1213
import pytest
1314

14-
from zhub.mcp import MCPClient
15+
from zhub.mcp import MCPClient, MCPError
1516

1617

1718
# A minimal stub MCP server. Reads JSON-RPC lines from stdin, writes responses
@@ -108,3 +109,44 @@ async def test_mcp_client_unknown_tool_raises(stub_script_path):
108109
assert "unknown tool" in str(exc_info.value).lower() or "32601" in str(exc_info.value)
109110
finally:
110111
await client.close()
112+
113+
114+
# A stub that completes the handshake, then exits (closing stdout) on the first
115+
# non-initialize request — i.e. the server dies mid-call without responding.
116+
DYING_STUB_SCRIPT = '''
117+
import json, sys
118+
for line in sys.stdin:
119+
line = line.strip()
120+
if not line:
121+
continue
122+
req = json.loads(line)
123+
rid = req.get("id")
124+
if req.get("method") == "initialize":
125+
sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": rid, "result": {}}) + "\\n")
126+
sys.stdout.flush()
127+
else:
128+
sys.exit(0)
129+
'''
130+
131+
132+
@pytest.fixture
133+
def dying_stub_path():
134+
fd, path = tempfile.mkstemp(suffix="_dying_stub.py")
135+
with os.fdopen(fd, "w") as f:
136+
f.write(DYING_STUB_SCRIPT)
137+
yield path
138+
os.unlink(path)
139+
140+
141+
@pytest.mark.asyncio
142+
async def test_in_flight_request_fails_when_subprocess_exits(dying_stub_path):
143+
# Regression: if the MCP server closes stdout (EOF) while a request is in
144+
# flight, the reader loop used to just stop, leaving the request future
145+
# unresolved forever. It must fail fast instead.
146+
client = MCPClient([sys.executable, dying_stub_path])
147+
await client.start()
148+
try:
149+
with pytest.raises(MCPError):
150+
await asyncio.wait_for(client.call_tool("echo", {"text": "x"}), timeout=5.0)
151+
finally:
152+
await client.close()

0 commit comments

Comments
 (0)