You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
MCP client not able to initiate StdioServerParameters while running in Jupyter Notebook #1103
Same issue. It seems like Google Colab / Jupyter notebook mock the sys.stderr, sys.stdout streams, making it incompatible with MCP. Example of traceback:
For a quick fix, please see this comment from a related issue on langchain. Basically, you can pass errlog=None to mcp.client.stdio.stdio_client to avoid this issue in a Jupyter notebook.
I can reproduce this Windows client failure with a self-contained SDK-only script. It models a non-None stderr object whose fileno() returns -1, matching Textual 8.2.8's _PrintCapture during 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.
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=1
In mcp==1.28.1, stdio_client captures sys.stderr as its default errlog at import time and forwards it to Windows subprocess creation. Python calls stderr.fileno() and msvcrt.get_osfhandle(...); the invalid descriptor raises OSError(9). The errlog=None control avoids that file-object descriptor conversion.
Related #2551 addresses sys.stderr is None, but its proposed errlog=None fallback 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.
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.stderr is ipykernel.iostream.OutStream, but sys.stderr.fileno() returns a real descriptor (60), and Client(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's errlog default is still sys.stderr bound at import time (src/mcp/client/stdio.py#L116) and is passed directly to anyio.open_process(stderr=errlog) (#L346). Replacing sys.stderr with an object whose fileno() raises io.UnsupportedOperation, before importing mcp, 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() raising UnsupportedOperation on Linux.
With fileno() returning -1 instead (@bringfire's Textual case above), default works on macOS, presumably because POSIX subprocess treats -1 as "no redirect".
Possible direction: resolve errlog at call time and fall back to inheriting stderr (errlog=None) when it has no usable fileno(). The trade-off: the server's stderr is then inherited from the host process's fd 2 instead of going through sys.stderr, so where it shows up depends on the host. #2551 falls back to os.devnull when sys.stderr is None, and EvoScientist#423 (below) tries sys.__stderr__ and then os.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.py from the SDK repo root. For the -1 case, change fileno to return -1.
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_client to pick a usable errlog at call time because langchain-mcp-adapters doesn't expose errlog).
AI disclosure: I used Claude to help write the reproduction scripts; I ran all checks myself and reviewed the results.
Activity
Conchylicultor commentedon Jul 16, 2025
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
https://gist.github.com/Conchylicultor/9b31dc47ac99532e783f7c9d9d7518a6
Reproduction using: https://github.com/modelcontextprotocol/servers/tree/main/src/time
Raised by:
savvaki commentedon Sep 9, 2025
I have the same issue
felixweinberger commentedon Oct 3, 2025
Hi thanks for reporting this issue @kdheeraz could you provide a minimal runnable example that isolates the issue you're seeing?
kikaDev404 commentedon Oct 3, 2025
I have the same issue
tomityrrell commentedon Oct 30, 2025
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.bringfire commentedon Jul 16, 2026
I 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
26200.8875, 64-bit3.12.12mcp==1.28.1anyio==4.14.2deepagents-code==0.1.39),textual==8.2.8,langchain-mcp-adapters==0.3.0Isolated setup and invocation
Complete
repro_invalid_errlog.pyResult
The default call fails before the MCP server starts; changing only to
stdio_client(server_params, errlog=None)succeeds and lists one tool.Full sanitized traceback
In
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.
JoeChen0430 commentedon Oct 3, 2026
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: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.pyfake_stderr.pyDownstream 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.