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

Aperta Adatta ai principianti
#3,821 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
2/5
Tempo stimato
1-3 ore
Idoneità per principianti
76/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
python

Direzione di ricerca

Inizia leggendo l’implementazione di stdio_server() e seguendo dove fd 1 viene reindirizzato e ripristinato; l’issue non indica alcun file né test. Riproduci il caso segnalato con uno strumento che chiama print() mentre stdout è collegato a una pipe, quindi verifica che il testo in sospeso venga inviato a stderr prima del ripristino di fd 1 e non compaia come messaggio JSON-RPC.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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.

Lingua principale
Python
Stelle
24.7k
Fork
4.1k
Merge medio
9h 45m
PR unite (30g)
16

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di modelcontextprotocol/python-sdk

Tutte le issue di modelcontextprotocol/python-sdk

Issue simili

Altre issue su Python

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.