python-sdk icon indicating copy to clipboard operation
python-sdk copied to clipboard

MCP Server Hangs Indefinitely After KqueueSelector Log on macOS (Python 3.12, Both STDIO & SSE)

Open jlcases opened this issue 10 months ago • 2 comments

Describe the bug

When running an MCP server using mcp-sdk (both FastMCP and the low-level Server with mcp.server.stdio.stdio_server) on macOS with Python 3.12, the server process hangs indefinitely immediately after the DEBUG - Using selector: KqueueSelector log message appears. This happens during the server.run() or mcp.run() call.

The hang occurs regardless of whether the STDIO transport (transport="stdio") or the default web/SSE transport is used. It also occurs even with a minimal server configuration with no custom tools, resources, or plugins registered.

Environment:

  • OS: macOS 15 Sequoia (Darwin 24.5.0)
  • Python: 3.12.9 (via Homebrew - Please update if incorrect)
  • mcp-sdk Version: 1.6.1.dev14+babb477 (Installed via pip install -e . from Git main branch using mcp @ git+https://github.com/modelcontextprotocol/python-sdk.git in pyproject.toml with hatchling and allow-direct-references = true)
  • Key Dependencies:
    • uvicorn: 0.30.3
    • starlette: 0.37.2
    • anyio: 4.9.0

To Reproduce

Steps to reproduce the behavior:

  1. Ensure Python 3.12.9 and mcp-sdk (tested with version 1.6.1.dev14+babb477 installed from Git main branch) are installed in your environment on macOS.
  2. Save the following minimal STDIO server example (based on the SDK documentation) as mcp_stdio_test.py:
    # From https://github.com/modelcontextprotocol/python-sdk README (Low-Level Server example)
    import mcp.server.stdio
    import mcp.types as types
    from mcp.server.lowlevel import NotificationOptions, Server
    from mcp.server.models import InitializationOptions
    import logging # Add logging
    import asyncio # Add asyncio
    
    # Configure logging
    logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
    logger = logging.getLogger(__name__)
    
    
    # Create a server instance
    logger.debug("Creating Server instance...")
    server = Server("example-server")
    logger.debug("Server instance created.")
    
    
    @server.list_prompts()
    async def handle_list_prompts() -> list[types.Prompt]:
        logger.debug("Handling list_prompts request...")
        return [
            types.Prompt(
                name="example-prompt",
                description="An example prompt template",
                arguments=[
                    types.PromptArgument(
                        name="arg1", description="Example argument", required=True
                    )
                ],
            )
        ]
    logger.debug("list_prompts handler defined.")
    
    
    @server.get_prompt()
    async def handle_get_prompt(
        name: str, arguments: dict[str, str] | None
    ) -> types.GetPromptResult:
        logger.debug(f"Handling get_prompt request for: {name}")
        if name != "example-prompt":
            raise ValueError(f"Unknown prompt: {name}")
    
        return types.GetPromptResult(
            description="Example prompt",
            messages=[
                types.PromptMessage(
                    role="user",
                    content=types.TextContent(type="text", text="Example prompt text"),
                )
            ],
        )
    logger.debug("get_prompt handler defined.")
    
    
    async def run():
        logger.debug("run() function started.")
        async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
            logger.debug("stdio_server context entered.")
            init_options = InitializationOptions(
                    server_name="example",
                    server_version="0.1.0",
                    capabilities=server.get_capabilities(
                        notification_options=NotificationOptions(),
                        experimental_capabilities={},
                    ),
                )
            logger.debug(f"Initialization options created: {init_options}")
            logger.debug("Attempting server.run()...")
            # Note: Using low-level server.run, not FastMCP's run
            await server.run(
                read_stream,
                write_stream,
                init_options
            )
            logger.debug("server.run() finished.") # This likely won't be reached in normal operation
    
    
    if __name__ == "__main__":
        logger.info("Starting asyncio event loop...")
        try:
            asyncio.run(run())
        except KeyboardInterrupt:
            logger.info("KeyboardInterrupt received, exiting.")
        except Exception as e:
            logger.critical(f"Unhandled exception in main loop: {e}", exc_info=True)
        finally:
            logger.info("asyncio event loop finished.")
    
  3. Run the script from the terminal: python mcp_stdio_test.py
  4. See error: Observe the log output. The script hangs indefinitely after the Attempting server.run()... log message.

Expected behavior

The server should successfully start, remain running, and be ready to accept MCP connections via STDIO (or SSE if run without transport="stdio"). It should not hang during initialization.

Screenshots

N/A - The issue is a process hang, the relevant output is text logs shown below.

Desktop:

  • OS: macOS 15 Sequoia (Darwin 24.5.0)
  • Python Version: 3.12.9 (via Homebrew - Please update if incorrect)
  • mcp-sdk Version: 1.6.1.dev14+babb477 (Installed from Git main branch)
  • Key Dependencies:
    • uvicorn: 0.30.3
    • starlette: 0.37.2
    • anyio: 4.9.0

Smartphone:

N/A

Additional context

  • The issue occurs with both FastMCP().run(...) and the low-level Server().run(...) implementation.
  • The hang happens even when all custom tools, resources, plugins, and prompts defined in the application code are commented out.
  • The issue persists whether using mcp installed from PyPI or directly from the Git main branch.
  • Pinning uvicorn[standard] to 0.30.3 (a version used in a previously working commit) did not resolve the issue.
  • Adding a time.sleep(10) before the run() call (which seemed to help in older commits) no longer prevents the hang.
  • Issue #396 seems potentially related as it also involves problems with the STDIO transport, although it describes the client not detecting server termination rather than the server hanging on startup.

This strongly suggests an underlying issue within mcp-sdk's core run logic or its interaction with asyncio/KqueueSelector on macOS with recent Python versions or recent mcp-sdk versions. The hang point after KqueueSelector seems critical.

Log Output Showing Hang:

2025-04-21 02:17:29,965 - DEBUG - Creating Server instance...
2025-04-21 02:17:29,966 - DEBUG - Initializing server 'example-server'
2025-04-21 02:17:29,966 - DEBUG - Server instance created.
2025-04-21 02:17:29,966 - DEBUG - Registering handler for PromptListRequest
2025-04-21 02:17:29,966 - DEBUG - list_prompts handler defined.
2025-04-21 02:17:29,966 - DEBUG - Registering handler for GetPromptRequest
2025-04-21 02:17:29,966 - DEBUG - get_prompt handler defined.
2025-04-21 02:17:29,966 - INFO - Starting asyncio event loop...
2025-04-21 02:17:29,966 - DEBUG - Using selector: KqueueSelector
2025-04-21 02:17:29,971 - DEBUG - run() function started.
2025-04-21 02:17:29,973 - DEBUG - stdio_server context entered.
2025-04-21 02:17:29,973 - DEBUG - Initialization options created: server_name='example' server_version='0.1.0' capabilities=ServerCapabilities(experimental={}, logging=None, prompts=PromptsCapability(listChanged=False), resources=None, tools=None) instructions=None
2025-04-21 02:17:29,973 - DEBUG - Attempting server.run()...
<-- HANGS HERE INDEFINITELY -->

jlcases avatar Apr 21 '25 00:04 jlcases

Same issue with Python 3.10.17 and 3.12.3; although it hangs even earlier at this stage

...
DEBUG Requirement already installed: mdurl==0.1.2
Audited 28 packages in 0.08ms
DEBUG Using Python 3.10.17 interpreter at: .../.venv/bin/python3
DEBUG Running `python weather.py`
DEBUG Spawned child 23168 in process group 23167
<-- HANGS -->

shreyass-ranganatha avatar May 30 '25 05:05 shreyass-ranganatha

Using fastmcp but I think the problem is coming from deeper. With Python 3.13, fastmcp==2.5.1 running this simple example:

from fastmcp import Client
from src.config.config_manager import get_config_manager
import asyncio
import sys
import os

async def get_tools():
  servers = get_config_manager().get_mcp_config().list_servers()
  print("servers: {servers}".format(servers=servers))

  server = servers[1]
  print("Picking: {server}".format(server=server))

  config = get_config_manager().get_mcp_config().get_server(server)
  final_config = {
    "mcpServers": {
      server: config.to_dict()
    }
  }

  print("Final generated config: {config}".format(config=final_config))
  '''
Output: Final generated config: {'mcpServers': {'everything': {'command': 'npx', 'args': ['-y', '@modelcontextprotocol/server-everything']}}}
'''

  async with Client(final_config, timeout=10) as client:
    print("Blocked here, pid:{pid}".format(pid=os.getpid()))
    tools = await client.list_tools()
    print("Tools output: {tools}".format(tools=tools))



print("Starting the process")
print("Current process pid: {pid}".format(pid=os.getpid()))
asyncio.run(get_tools())

This gets stuck in the stdio prompt

surya-prakash-susarla avatar May 30 '25 05:05 surya-prakash-susarla

Hi thanks for this report, is this still an issue for you? Checking as it's been a while (apologies for the time it took to get back to this)

felixweinberger avatar Oct 06 '25 17:10 felixweinberger