Skip to content

MCP client not able to initiate StdioServerParameters while running in Jupyter Notebook #1103

Description

@kdheeraz

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

Activity

  1. Conchylicultor commented on Jul 16, 2025

    @Conchylicultor

    Same issue. It seems like Google Colab / Jupyter notebook mock the sys.stderr, sys.stdout streams, making it incompatible with MCP. Example of traceback:

    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"))
    
  2. savvaki commented on Sep 9, 2025

    @savvaki

    I have the same issue

  3. felixweinberger commented on Oct 3, 2025

    @felixweinberger
    Contributor

    Hi thanks for reporting this issue @kdheeraz could you provide a minimal runnable example that isolates the issue you're seeing?

  4. added
    bugSomething isn't working
    needs reproneeds additional information to be able to reproduce bug
    on Oct 3, 2025
  5. kikaDev404 commented on Oct 3, 2025

    @kikaDev404

    I have the same issue

  6. tomityrrell commented on Oct 30, 2025

    @tomityrrell

    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.

  7. bringfire commented on Jul 16, 2026

    @bringfire

    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.

    Environment

    • Windows 11 Pro 25H2, OS build 26200.8875, 64-bit
    • Python 3.12.12
    • mcp==1.28.1
    • anyio==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.py
    from __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=1
    
    Full 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=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-None sys.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.

  8. JoeChen0430 commented on Oct 3, 2026

    @JoeChen0430

    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.

    server.py

    from 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.py

    import 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_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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingneeds reproneeds additional information to be able to reproduce bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions