Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion tests/test_mcp_remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,32 @@ def do_POST(self):
self._json(200, {"jsonrpc": "2.0", "id": rid, "error": {"message": "unknown method"}})


class _QuietHTTPServer(ThreadingHTTPServer):
"""HTTP fixture server that stays off stderr on expected disconnects.

Timeout tests (e.g. test_http_timeout: 1s client vs 30s sleep) abandon
the connection on purpose; the handler thread wakes later and writes
to a dead socket. BaseServer.handle_error (invoked on the SERVER, not
the handler) prints that BrokenPipeError traceback to the process
stderr, where under a full-suite run it lands inside a later test's
CliRunner capture window and corrupts assertions on stdout purity
(test_run_json's json.loads saw "Extra data: line 2" on macOS CI).
Anything else still prints — a real handler bug must stay loud.
"""

def handle_error(self, request, client_address):
exc = sys.exc_info()[1]
if isinstance(exc, (BrokenPipeError, ConnectionResetError)):
return
super().handle_error(request, client_address)


@pytest.fixture()
def http_server():
servers = []

def _make(mode="json"):
srv = ThreadingHTTPServer(("127.0.0.1", 0), _Handler)
srv = _QuietHTTPServer(("127.0.0.1", 0), _Handler)
srv.mode = mode
srv.seen = []
srv.flaked = False
Expand All @@ -128,6 +148,10 @@ def _make(mode="json"):
srv.shutdown()
except Exception:
pass
try:
srv.server_close() # shutdown() leaves the listen socket open;
except Exception: # unclosed it reaches GC as stderr noise (see above)
pass


@pytest.fixture(autouse=True)
Expand Down Expand Up @@ -184,6 +208,23 @@ def test_http_headers_passthrough(http_server):
assert srv.seen[0]["headers"].get("Authorization") == "Bearer x"


def test_abandoned_connection_stays_off_stderr(capsys):
"""The timeout test abandons a 30s-sleeping handler on purpose; when it
wakes and writes to the dead socket, the BrokenPipeError traceback must
not reach stderr — under a full-suite run it lands inside a later test's
CliRunner capture and corrupts stdout-purity assertions (test_run_json
saw 'Extra data: line 2' on macOS CI twice)."""
srv = _QuietHTTPServer(("127.0.0.1", 0), _Handler)
try:
try:
raise BrokenPipeError(32, "Broken pipe")
except BrokenPipeError:
srv.handle_error(None, ("127.0.0.1", 1))
assert capsys.readouterr().err == ""
finally:
srv.server_close()


def test_http_timeout(http_server):
url = _url(http_server("sleep"))
client = mc.MCPHttpClient("web", url, {}, 1)
Expand Down
25 changes: 24 additions & 1 deletion tests/test_mcp_tool_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,31 @@ def do_POST(self):
self._json(200, {"jsonrpc": "2.0", "id": rid, "error": {"message": "unknown"}})


class _QuietHTTPServer(ThreadingHTTPServer):
"""HTTP fixture server that stays off stderr on expected disconnects.

A client that times out mid-request leaves the handler thread writing
to a dead socket; BaseServer.handle_error (invoked on the SERVER, not
the handler) prints that BrokenPipeError traceback to the process
stderr, where under a full-suite run it lands inside a later test's
CliRunner capture window and corrupts assertions on stdout purity
(test_run_json's json.loads saw "Extra data: line 2" on macOS CI).
Anything else still prints — a real handler bug must stay loud.
"""

def handle_error(self, request, client_address):
exc = sys.exc_info()[1]
if isinstance(exc, (BrokenPipeError, ConnectionResetError)):
return
super().handle_error(request, client_address)


@pytest.fixture()
def http_server():
servers = []

def _make(mode="json"):
srv = ThreadingHTTPServer(("127.0.0.1", 0), _Handler)
srv = _QuietHTTPServer(("127.0.0.1", 0), _Handler)
srv.mode = mode
srv.seen = []
srv.daemon_threads = True
Expand All @@ -99,6 +118,10 @@ def _make(mode="json"):
srv.shutdown()
except Exception:
pass
try:
srv.server_close() # shutdown() leaves the listen socket open;
except Exception: # unclosed it reaches GC as stderr noise (see above)
pass


def _url(srv):
Expand Down
Loading