Repository navigation
MCP client not able to initiate StdioServerParameters while running in Jupyter Notebook #1103
Description
Activity
Same issue. It seems like Google Colab / Jupyter notebook mock the
sys.stderr,sys.stdoutstreams, making it incompatible with MCP. Example of traceback:-
https://gist.github.com/Conchylicultor/b4b4d737da59f1da0d5ef81031279510
UnsupportedOperation: fileno
-
https://gist.github.com/Conchylicultor/9b31dc47ac99532e783f7c9d9d7518a6
AttributeError: 'OutStream' object has no attribute 'buffer'
Reproduction using: https://github.com/modelcontextprotocol/servers/tree/main/src/time
import asyncio import mcp_server_time asyncio.run(mcp_server_time.serve()) # AttributeError: 'OutStream' object has no attribute 'buffer'
Raised by:
/usr/local/lib/python3.11/dist-packages/mcp/server/stdio.py in stdio_server(stdin, stdout) 47 stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8")) 48 if not stdout: ---> 49 stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8"))-
I have the same issue
Hi thanks for reporting this issue @kdheeraz could you provide a minimal runnable example that isolates the issue you're seeing?
- addedbugSomething isn't workingSomething isn't workingneeds reproneeds additional information to be able to reproduce bugneeds additional information to be able to reproduce bug
on Oct 3, 2025 I have the same issue
For a quick fix, please see this comment from a related issue on langchain. Basically, you can pass
errlog=Nonetomcp.client.stdio.stdio_clientto avoid this issue in a Jupyter notebook.Reacted by Carlos BazagaI can reproduce this Windows client failure with a self-contained SDK-only script. It models a non-
Nonestderr object whosefileno()returns-1, matching Textual 8.2.8's_PrintCaptureduring stderr redirection. I first encountered it through dcode/Textual; the reproduction removes dcode, LangChain, and the original MCP server.This establishes the same client-side stderr/descriptor boundary reported here; it does not establish that the dcode/Textual and Jupyter triggers are identical.
Environment
- Windows 11 Pro 25H2, OS build
26200.8875, 64-bit - Python
3.12.12 mcp==1.28.1anyio==4.14.2- Downstream discovery context: dcode (
deepagents-code==0.1.39),textual==8.2.8,langchain-mcp-adapters==0.3.0
Isolated setup and invocation
mkdir mcp-stderr-repro cd mcp-stderr-repro uv venv --python 3.12.12 uv pip install --python .venv\Scripts\python.exe "mcp==1.28.1" "anyio==4.14.2" .venv\Scripts\python.exe .\repro_invalid_errlog.py
Complete
repro_invalid_errlog.pyfrom __future__ import annotations import asyncio import sys import traceback from pathlib import Path if "--server" in sys.argv: from mcp.server.fastmcp import FastMCP server = FastMCP("stderr-repro") @server.tool() def ping() -> str: return "pong" server.run(transport="stdio") raise SystemExit class InvalidStderr: """Small stand-in for a UI-owned stderr with an unusable descriptor.""" def fileno(self) -> int: return -1 def write(self, value: str) -> int: return len(value) def flush(self) -> None: pass def isatty(self) -> bool: return False # Import the stdio client only after replacing stderr. The default value of # stdio_client(errlog=...) is evaluated when mcp.client.stdio is imported. sys.stderr = InvalidStderr() from mcp import ClientSession, StdioServerParameters # noqa: E402 from mcp.client.stdio import stdio_client # noqa: E402 server_params = StdioServerParameters( command=sys.executable, args=[str(Path(__file__).resolve()), "--server"], ) async def probe(label: str, *, use_none: bool) -> bool: try: client = ( stdio_client(server_params, errlog=None) if use_none else stdio_client(server_params) ) async with client as (read_stream, write_stream): async with ClientSession(read_stream, write_stream) as session: await session.initialize() tools = await session.list_tools() print(f"{label}: PASS tools={len(tools.tools)}") return True except Exception as exc: print(f"{label}: FAIL {type(exc).__name__}: {exc!r}") traceback.print_exc(file=sys.stdout) return False async def main() -> None: default_ok = await probe("default", use_none=False) none_ok = await probe("errlog=None", use_none=True) # Success means the default fails and the one-variable control passes. if default_ok or not none_ok: raise SystemExit(1) if __name__ == "__main__": asyncio.run(main())
Result
The default call fails before the MCP server starts; changing only to
stdio_client(server_params, errlog=None)succeeds and lists one tool.default: FAIL OSError: OSError(9, 'Bad file descriptor') ... OSError: [Errno 9] Bad file descriptor errlog=None: PASS tools=1Full sanitized traceback
default: FAIL OSError: OSError(9, 'Bad file descriptor') Traceback (most recent call last): File "<repro-dir>\.venv\Lib\site-packages\mcp\os\win32\utilities.py", line 169, in create_windows_process process = await anyio.open_process( ^^^^^^^^^^^^^^^^^^^^^^^^^ File "<repro-dir>\.venv\Lib\site-packages\anyio\_core\_subprocesses.py", line 184, in open_process return await get_async_backend().open_process( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<repro-dir>\.venv\Lib\site-packages\anyio\_backends\_asyncio.py", line 2745, in open_process transport, protocol = await loop.subprocess_exec( ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<python-install>\Lib\asyncio\base_events.py", line 1756, in subprocess_exec transport = await self._make_subprocess_transport( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<python-install>\Lib\asyncio\windows_events.py", line 400, in _make_subprocess_transport transp = _WindowsSubprocessTransport(self, protocol, args, shell, ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<python-install>\Lib\asyncio\base_subprocess.py", line 36, in __init__ self._start(args=args, shell=shell, stdin=stdin, stdout=stdout, File "<python-install>\Lib\asyncio\windows_events.py", line 878, in _start self._proc = windows_utils.Popen( ^^^^^^^^^^^^^^^^^^^^ File "<python-install>\Lib\asyncio\windows_utils.py", line 153, in __init__ super().__init__(args, stdin=stdin_rfd, stdout=stdout_wfd, File "<python-install>\Lib\subprocess.py", line 992, in __init__ errread, errwrite) = self._get_handles(stdin, stdout, stderr) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<python-install>\Lib\subprocess.py", line 1406, in _get_handles errwrite = msvcrt.get_osfhandle(stderr.fileno()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ OSError: [Errno 9] Bad file descriptor During handling of the above exception, another exception occurred: Traceback (most recent call last): File "<repro-dir>\repro_invalid_errlog.py", line 59, in probe async with client as (read_stream, write_stream): ^^^^^^ File "<python-install>\Lib\contextlib.py", line 210, in __aenter__ return await anext(self.gen) ^^^^^^^^^^^^^^^^^^^^^ File "<repro-dir>\.venv\Lib\site-packages\mcp\client\stdio\__init__.py", line 124, in stdio_client process = await _create_platform_compatible_process( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<repro-dir>\.venv\Lib\site-packages\mcp\client\stdio\__init__.py", line 249, in _create_platform_compatible_process process = await create_windows_process(command, args, env, errlog, cwd) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<repro-dir>\.venv\Lib\site-packages\mcp\os\win32\utilities.py", line 184, in create_windows_process process = await anyio.open_process( ^^^^^^^^^^^^^^^^^^^^^^^^^ File "<repro-dir>\.venv\Lib\site-packages\anyio\_core\_subprocesses.py", line 184, in open_process return await get_async_backend().open_process( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<repro-dir>\.venv\Lib\site-packages\anyio\_backends\_asyncio.py", line 2745, in open_process transport, protocol = await loop.subprocess_exec( ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<python-install>\Lib\asyncio\base_events.py", line 1756, in subprocess_exec transport = await self._make_subprocess_transport( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<python-install>\Lib\asyncio\windows_events.py", line 400, in _make_subprocess_transport transp = _WindowsSubprocessTransport(self, protocol, args, shell, ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<python-install>\Lib\asyncio\base_subprocess.py", line 36, in __init__ self._start(args=args, shell=shell, stdin=stdin, stdout=stdout, File "<python-install>\Lib\asyncio\windows_events.py", line 878, in _start self._proc = windows_utils.Popen( ^^^^^^^^^^^^^^^^^^^^ File "<python-install>\Lib\asyncio\windows_utils.py", line 153, in __init__ super().__init__(args, stdin=stdin_rfd, stdout=stdout_wfd, File "<python-install>\Lib\subprocess.py", line 992, in __init__ errread, errwrite) = self._get_handles(stdin, stdout, stderr) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<python-install>\Lib\subprocess.py", line 1406, in _get_handles errwrite = msvcrt.get_osfhandle(stderr.fileno()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ OSError: [Errno 9] Bad file descriptor errlog=None: PASS tools=1In
mcp==1.28.1,stdio_clientcapturessys.stderras its defaulterrlogat import time and forwards it to Windows subprocess creation. Python callsstderr.fileno()andmsvcrt.get_osfhandle(...); the invalid descriptor raisesOSError(9). Theerrlog=Nonecontrol avoids that file-object descriptor conversion.Related #2551 addresses
sys.stderr is None, but its proposederrlog=Nonefallback selects any non-Nonesys.stderr. Here that would reselect the invalid object, so the PR would neither cover this case nor preserve the working control. I'm flagging the overlap so maintainers can decide the intended semantics.Does this belong on #1103 or in a separate issue? If a fix is wanted, which behavior and branch should it target? I'll wait for maintainer acknowledgment/assignment before coding.
AI assistance disclosure: AI helped isolate and draft this reproduction; I personally ran it, reviewed the results, and understand the report.
- Windows 11 Pro 25H2, OS build
Data point on v2 / macOS, since the existing repros are on v1.
Environment: macOS 26.6.2, Python 3.11.5, ipykernel 7.4.0, mcp from a source checkout of main (
17aaf255).1. Real Jupyter on macOS: does not reproduce.
In JupyterLab,sys.stderrisipykernel.iostream.OutStream, butsys.stderr.fileno()returns a real descriptor (60), andClient(StdioServerParameters(...))connects;call_tool("add", {"a": 1, "b": 2})returns{'result': 3}.2. The v2 code path still fails for any stderr whose
fileno()raises.
stdio_client'serrlogdefault is stillsys.stderrbound at import time (src/mcp/client/stdio.py#L116) and is passed directly toanyio.open_process(stderr=errlog)(#L346). Replacingsys.stderrwith an object whosefileno()raisesio.UnsupportedOperation, before importingmcp, gives:default FAIL UnsupportedOperation('fileno') errlog=None OK {'result': 3}So on v2 the failure isn't platform-specific in the SDK itself; it depends on whether the replaced stderr has a usable descriptor. That fits the reports so far: Windows Jupyter, and Colab, where @Conchylicultor's first gist shows ipykernel's
OutStream.fileno()raisingUnsupportedOperationon Linux.With
fileno()returning-1instead (@bringfire's Textual case above),defaultworks on macOS, presumably because POSIXsubprocesstreats-1as "no redirect".Possible direction: resolve
errlogat call time and fall back to inheriting stderr (errlog=None) when it has no usablefileno(). The trade-off: the server's stderr is then inherited from the host process's fd 2 instead of going throughsys.stderr, so where it shows up depends on the host. #2551 falls back toos.devnullwhensys.stderrisNone, and EvoScientist#423 (below) triessys.__stderr__and thenos.devnull, so it's probably worth settling on one fallback for all these cases.Scripts
Put both files in the same directory, then run
uv run --frozen python path/to/fake_stderr.pyfrom the SDK repo root. For the-1case, changefilenotoreturn -1.server.pyfrom mcp.server import MCPServer mcp = MCPServer("repro") @mcp.tool() def add(a: int, b: int) -> int: return a + b if __name__ == "__main__": mcp.run()
fake_stderr.pyimport io import sys from pathlib import Path import anyio class NoFilenoStderr(io.StringIO): def fileno(self): raise io.UnsupportedOperation("fileno") sys.stderr = NoFilenoStderr() from mcp import Client, StdioServerParameters # noqa: E402 from mcp.client.stdio import stdio_client # noqa: E402 params = StdioServerParameters( command=sys.executable, args=[str(Path(__file__).resolve().parent / "server.py")], ) async def main(): for label, target in [("default", params), ("errlog=None", stdio_client(params, errlog=None))]: try: async with Client(target) as client: r = await client.call_tool("add", {"a": 1, "b": 2}) print(label, "OK", r.structured_content, file=sys.__stdout__) except Exception as e: print(label, "FAIL", repr(e), file=sys.__stdout__) anyio.run(main)
Downstream projects are working around this independently: langchain-ai/deep_research_from_scratch#13 (Windows + Jupyter, mcp 1.29.1) and EvoScientist/EvoScientist#423 (Windows + Textual; it wraps
stdio_clientto pick a usableerrlogat call time because langchain-mcp-adapters doesn't exposeerrlog).AI disclosure: I used Claude to help write the reproduction scripts; I ran all checks myself and reviewed the results.
while initiating StdioServerParameters in jupyter notebook there is an Exception from subprocess.Popen.
Exception is : UnsupportedOperation fileno
Same code works in idle and with quart server.
OS : windows 11
Python version: 3.11