From 9b171b7fbe0798f09a31d056fae1da0b58ae3672 Mon Sep 17 00:00:00 2001 From: guptaishaan Date: Tue, 13 Oct 2026 13:36:50 -0700 Subject: [PATCH] Fix quadratic time reading large stdio responses stdout_reader rebuilt "buffer + chunk" for every chunk, so a single large JSON line was re-copied once per chunk read. Receiving N bytes cost O(N^2). Keep the pieces of the unfinished line in a list and join them once when a chunk contains a newline. --- src/mcp/client/stdio.py | 9 ++++++--- tests/client/test_stdio.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/mcp/client/stdio.py b/src/mcp/client/stdio.py index 6a3ad12111..664086677a 100644 --- a/src/mcp/client/stdio.py +++ b/src/mcp/client/stdio.py @@ -147,10 +147,13 @@ async def stdout_reader() -> None: async with read_stream_writer: try: # One line at a time; no read-ahead while a delivery is blocked. - buffer = "" + parts: list[str] = [] async for chunk in stdout: - lines = (buffer + chunk).split("\n") - buffer = lines.pop() + if "\n" not in chunk: + parts.append(chunk) # joined once the line ends, not re-copied per chunk + continue + lines = ("".join(parts) + chunk).split("\n") + parts = [lines.pop()] for line in lines: try: await read_stream_writer.send(_parse_line(line)) diff --git a/tests/client/test_stdio.py b/tests/client/test_stdio.py index b64eb01902..8b494c5e84 100644 --- a/tests/client/test_stdio.py +++ b/tests/client/test_stdio.py @@ -277,6 +277,26 @@ async def test_messages_split_and_packed_across_chunks_are_reframed(monkeypatch: await read_stream.receive() +@pytest.mark.anyio +async def test_a_large_message_in_many_small_chunks_is_reframed_in_linear_time( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Receiving one huge line must not re-copy everything received so far for every chunk.""" + big = JSONRPCResponse(jsonrpc="2.0", id=1, result={"text": "x" * (8 * 1024 * 1024)}) + wire = _line(big) + process = FakeProcess(on_stdin_close=lambda: process.exit(0)) + + install_fake_process(monkeypatch, process) + + # Quadratic reframing of this takes tens of seconds; linear takes well under one. + with anyio.fail_after(5): + async with stdio_client(FAKE_PARAMS) as (read_stream, _): + for start in range(0, len(wire), 1024): + await process.feed(wire[start : start + 1024]) + + assert await _next_message(read_stream) == big + + @pytest.mark.anyio async def test_each_outgoing_message_is_written_as_exactly_one_line(monkeypatch: pytest.MonkeyPatch) -> None: """Client -> server framing writes one line per message.