diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index 1c05364..66b440b 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -4,6 +4,7 @@ import os import subprocess import sys +import threading import time import pytest @@ -42,20 +43,56 @@ def test_stdio_initialize_and_tools_list(monkeypatch: pytest.MonkeyPatch) -> Non } initialized = {"jsonrpc": "2.0", "method": "notifications/initialized"} tools_list = {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}} - input_text = "\n".join(json.dumps(m) for m in (init, initialized, tools_list)) + "\n" - proc = subprocess.run( + proc = subprocess.Popen( [sys.executable, "-m", "tiny_ntfy_mcp"], - input=input_text, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, text=True, - capture_output=True, env=os.environ.copy(), - timeout=5, - check=False, ) - assert proc.returncode == 0, proc.stderr + responses: list[dict] = [] + # Signalled by the reader once the tools/list response (id=2) is observed, + # so the main thread waits on a real event rather than racing a join timeout. + saw_tools_list = threading.Event() + + # Stdin is intentionally kept open until the tools/list response arrives + # so that MCP's transport-close handler (mcp>=1.27.0) does not cancel the + # in-flight request before it can respond. + def _collect() -> None: + for raw in proc.stdout: + raw = raw.strip() + if not raw: + continue + msg = json.loads(raw) + responses.append(msg) + if msg.get("id") == 2: + saw_tools_list.set() + return + + t = threading.Thread(target=_collect, daemon=True) + t.start() + try: + for msg in (init, initialized, tools_list): + proc.stdin.write(json.dumps(msg) + "\n") + proc.stdin.flush() - responses = [json.loads(line) for line in proc.stdout.splitlines()] + got_response = saw_tools_list.wait(timeout=5) + finally: + proc.stdin.close() + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait() + # Reader exits naturally once stdout closes; join to ensure no further + # writes to `responses` race the assertions below. + t.join(timeout=5) + + stderr_output = proc.stderr.read() + assert got_response, f"timed out waiting for tools/list response. Got ids: {[r.get('id') for r in responses]}\nstderr: {stderr_output}" + assert proc.returncode == 0, stderr_output init_resp = next(r for r in responses if r.get("id") == 1) tools_resp = next(r for r in responses if r.get("id") == 2)