stdio_server: text a tool prints still reaches the client, because sys.stdout is flushed after fd 1 is restored

Open Beginner friendly
#3,821 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
76/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python

Research direction

Start by reading the stdio_server() implementation and tracing where fd 1 is diverted and restored; the issue does not name a file or test. Reproduce the reported case with a tool that calls print() while stdout is piped, then check that pending text goes to stderr before fd 1 is restored and does not appear as a JSON-RPC message.

Written by the indexing model from the issue text.

Description

bug v2
Summary

Since #3117, stdio_server() points fd 1 at stderr while it serves, so that stray output from handlers misses the wire. Text written with print() still reaches the client. It goes into sys.stdout's own buffer, which is block-buffered when stdout is a pipe. That buffer is flushed when the interpreter exits, after stdio_server() has given fd 1 back to the client's pipe. The client then reads the text as a JSON-RPC message.

Reproduction

mcp 2.3.0, Python 3.13.16, macOS 26.3.

import asyncio
import sys

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.server.mcpserver import MCPServer

if sys.argv[1:] == ["server"]:
    server = MCPServer("repro")

    @server.tool()
    def noisy() -> str:
        print("stray output from a tool")  # e.g. a library that prints
        return "done"

    server.run("stdio")
else:
    async def main():
        params = StdioServerParameters(command=sys.executable, args=[__file__, "server"])
        async with stdio_client(params) as (read, write):
            async with ClientSession(read, write) as session:
                await session.initialize()
                result = await session.call_tool("noisy", {})
                print("result:", result.content[0].text)

    asyncio.run(main())

Running python repro.py, the call succeeds, and the client also logs:

pydantic_core._pydantic_core.ValidationError: 1 validation error for union[JSONRPCRequest,JSONRPCNotification,JSONRPCResponse,JSONRPCError]
  Invalid JSON: expected value at line 1 column 1 [type=json_invalid, input_value='stray output from a tool', input_type=str]
Cause

While fd 1 is diverted, the printed text never reaches it: it sits in sys.stdout's buffer. The buffer is written out at interpreter exit, when fd 1 is the protocol pipe again. Adding sys.stdout.flush() right after the print() in the tool removes the error, and the text appears on stderr, which confirms this.

Suggestion

Flush sys.stdout before restoring fd 1, so that pending text goes to the diversion. Making sys.stdout line-buffered while serving (sys.stdout.reconfigure(line_buffering=True)) would also send printed text to stderr when it is printed, rather than at exit.

As a workaround we call sys.stdout.reconfigure(line_buffering=True) before server.run("stdio") and flush after each tool.

Dominant language
Python
Stars
24.7k
Forks
4.1k
Avg merge
9h 45m
Merged PRs (30d)
16

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from modelcontextprotocol/python-sdk

All issues in modelcontextprotocol/python-sdk

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.