Skip to content

Commit 67634ec

Browse files
committed
test(mcp): cover per-request timeout on a silent-but-alive server
The existing EOF regression test only covers a subprocess that closes stdout. Add a stub that completes initialize then ignores every subsequent request without responding or exiting; with no internal timeout the call would hang forever, so the test deliberately omits an external watchdog and asserts the client raises a 'timed out' MCPError.
1 parent 30ee156 commit 67634ec

1 file changed

Lines changed: 43 additions & 0 deletions

File tree

tests/test_mcp.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,46 @@ async def test_in_flight_request_fails_when_subprocess_exits(dying_stub_path):
150150
await asyncio.wait_for(client.call_tool("echo", {"text": "x"}), timeout=5.0)
151151
finally:
152152
await client.close()
153+
154+
155+
# A stub that completes the handshake then stays alive but silently ignores
156+
# every subsequent request — no response, no EOF. The exact case the EOF fix
157+
# doesn't cover: a hung/unresponsive-but-living server.
158+
SILENT_STUB_SCRIPT = '''
159+
import json, sys
160+
for line in sys.stdin:
161+
line = line.strip()
162+
if not line:
163+
continue
164+
req = json.loads(line)
165+
rid = req.get("id")
166+
if req.get("method") == "initialize":
167+
sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": rid, "result": {}}) + "\\n")
168+
sys.stdout.flush()
169+
# any other method: read it and never answer, keeping the process alive
170+
'''
171+
172+
173+
@pytest.fixture
174+
def silent_stub_path():
175+
fd, path = tempfile.mkstemp(suffix="_silent_stub.py")
176+
with os.fdopen(fd, "w") as f:
177+
f.write(SILENT_STUB_SCRIPT)
178+
yield path
179+
os.unlink(path)
180+
181+
182+
@pytest.mark.asyncio
183+
async def test_request_times_out_when_server_silent(silent_stub_path):
184+
# Regression: a subprocess that stays alive but never responds to a
185+
# request (no EOF, no crash) used to hang the caller forever — only
186+
# initialize was time-bounded. The per-request timeout must fail it.
187+
# No external asyncio.wait_for here on purpose: the client must bound
188+
# the wait itself, otherwise this test would hang the whole suite.
189+
client = MCPClient([sys.executable, silent_stub_path], request_timeout=1.0)
190+
await client.start()
191+
try:
192+
with pytest.raises(MCPError, match="timed out"):
193+
await client.call_tool("echo", {"text": "x"})
194+
finally:
195+
await client.close()

0 commit comments

Comments
 (0)