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

Abierto Apto para principiantes
#3,821 1 comentario 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
2/5
Tiempo estimado
1-3 horas
Aptitud para principiantes
76/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
python

Línea de trabajo

Empieza leyendo la implementación de stdio_server() y siguiendo dónde se redirige y restaura fd 1; el issue no indica ningún archivo ni prueba. Reproduce el caso informado con una herramienta que llame a print() mientras stdout está canalizado y luego comprueba que el texto pendiente se envía a stderr antes de restaurar fd 1 y no aparece como un mensaje JSON-RPC.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

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.

Lenguaje dominante
Python
Estrellas
24.7k
Forks
4.1k
Merge medio
9 h 45 min
PR fusionados (30 d)
16

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de modelcontextprotocol/python-sdk

Todos los issues de modelcontextprotocol/python-sdk

Issues similares

Más issues de Python

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.