Skip to content

Runner hangs when using MCPServerStdio after successful tools/list #434

Closed
@lighteternal

Description

@lighteternal

Please read this first

  • Have you read the docs?Agents SDK docs --Yes
  • Have you searched for related issues? Others may have faced similar issues. --Yes, could not find an exact match.

Describe the bug

When running an Agent that uses a tool provided via MCPServerStdio, the Runner.run() call hangs indefinitely.

The hang occurs after the MCPServerStdio subprocess successfully responds to the initialize and tools/list requests, but before the first call to the LLM (e.g., OpenAI API /chat/completions) that should decide whether to use the tool. Logs show the tools/list response is sent by the server and the trace ingest call succeeds on the client, but no further progress is made and no tools/call request is ever sent to the server.

The agent runs correctly if the MCPServerStdio is removed!!!

Debug information

Agents SDK version: 0.0.8 (tried also with 0.0.7)
Python version: Python 3.12 (also tested with 3.10) via Conda
OS: WSL 2 (Ubuntu 22.04) on Windows

Repro steps

I had to create a dummy mcp server as the original one is based on an internal API:

  1. Save the following files:
    agent_app.py (Client application)
    generic_mcp_server.py (dummy)
import asyncio
import logging
import os
import sys
from dotenv import load_dotenv

# Basic path setup assumes script is run from its directory
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(script_dir) # Adjust if structure differs
src_path = os.path.join(project_root, 'src') # Path to openai-agents src
if src_path not in sys.path:
    sys.path.insert(0, src_path)

try:
    from agents import Agent
    from agents.mcp import MCPServerStdio
    from agents import Runner, gen_trace_id, trace
except ImportError as e:
    print(f"ImportError: {e}. Make sure 'openai-agents' is installed and src path is correct if running from source.")
    sys.exit(1)

# Setup logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger("ReproApp")
logging.getLogger("httpx").setLevel(logging.INFO) # Use INFO or DEBUG
logging.getLogger("agents").setLevel(logging.DEBUG) # Enable SDK debug logs

# Load environment variables (.env file in same directory)
load_dotenv()

async def main():
    logger.info("Starting Agent Application...")

    mcp_server_script = os.path.join(script_dir, "generic_mcp_server.py")

    if not os.path.exists(mcp_server_script):
        logger.error(f"MCP server script not found at: {mcp_server_script}")
        return

    if not os.getenv("OPENAI_API_KEY"):
        logger.error("Missing OPENAI_API_KEY environment variable.")
        return

    python_executable = sys.executable
    logger.info(f"Using Python executable for MCP server: {python_executable}")
    
    mcp_server = MCPServerStdio(
        name="GenericServer",
        params={
            "command": python_executable,
            "args": [mcp_server_script],
        },
        cache_tools_list=False
    )

    try:
        async with mcp_server:
            logger.info("MCPServerStdio context entered.")
            
            logger.info("Initializing Agent (with MCP)...")
            simple_agent = Agent(
                name="TestAssistant",
                mcp_servers=[mcp_server],
                model="gpt-4o" # Or gpt-3.5-turbo
            )
            logger.info("Agent initialized.")

            user_query = "Use the tool to get dummy data."
            logger.info(f"Attempting to run agent with query: '{user_query}'")

            trace_id = gen_trace_id()
            logger.info(f"Trace ID generated: {trace_id}")
            print(f"\nTrace URL: https://platform.openai.com/traces/trace?trace_id={trace_id}\n")

            try:
                with trace(workflow_name="ReproMCPHang", trace_id=trace_id):
                    logger.info("Calling Runner.run()... Waiting for LLM/tool...")
                    result = await Runner.run(starting_agent=simple_agent, input=user_query)
                    logger.info("Runner finished.")

                    final_response = result.final_output if result else "No response."
                    print("\n" + "-"*30 + "\nAgent Response:\n" + final_response + "\n" + "-"*30 + "\n")

            except Exception as agent_run_err:
                logger.error(f"An error occurred during agent execution: {agent_run_err}", exc_info=True)

    except Exception as outer_err:
        logger.error(f"An error occurred outside agent run: {outer_err}", exc_info=True)
    finally:
        logger.info("Agent Application finished.")

if __name__ == "__main__":
    asyncio.run(main())`
    import sys
    import asyncio
    import json
    import logging

    # Setup logging to stderr for the server script itself
    logging.basicConfig(level=logging.INFO, stream=sys.stderr, format='%(asctime)s [Generic MCP Server] %(levelname)s: %(message)s')
    logger = logging.getLogger(__name__)

    # Define a dummy tool schema
    DUMMY_TOOL_SCHEMA = {
        "type": "function",
        "function": {
            "name": "generic_search",
            "description": "A generic dummy tool.",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "A query string."}
                },
                "required": ["query"]
            }
        }
    }

    def create_jsonrpc_response(id, result):
        return json.dumps({"jsonrpc": "2.0", "id": id, "result": result}, separators=(',', ':')) + "\n"

    def create_jsonrpc_error(id, code, message, data=None):
        error_obj = {"code": code, "message": message}
        if data: error_obj["data"] = data
        return json.dumps({"jsonrpc": "2.0", "id": id, "error": error_obj}, separators=(',', ':')) + "\n"

    async def handle_request(request_data):
        try:
            request = json.loads(request_data)
            request_id = request.get("id")
            method = request.get("method")
            params = request.get("params")

            logger.info(f"Received request: id={request_id}, method={method}")
            logger.debug(f"Params: {params}")

            if method is None: return None
            
            if method == "initialize":
                logger.info(f"Handling initialize (id={request_id})")
                cv = params.get('protocolVersion') if params else None
                if not cv: return create_jsonrpc_error(request_id, -32602, "Invalid params", "Missing protocolVersion")
                resp = {"serverInfo": {"name": "GenericMCPServer", "version": "0.1.0"}, "protocolVersion": cv, "capabilities": {}}
                logger.info(f"Sending initialize response")
                return create_jsonrpc_response(request_id, resp)

            elif method == "notifications/initialized":
                logger.info(f"Received initialized notification")
                return None 

            if request_id is None: return None

            if method == "tools/list":
                logger.info("Handling tools/list request")
                response = create_jsonrpc_response(request_id, [DUMMY_TOOL_SCHEMA]) 
                logger.info("Sending tools/list response (dummy tool)")
                return response

            elif method == "tools/call":
                logger.info("Handling tools/call request")
                tool_name = params.get("name")
                if tool_name == DUMMY_TOOL_SCHEMA["function"]["name"]:
                     logger.info(f"Simulating call to '{tool_name}'")
                     # Return a dummy success result
                     dummy_result = {"status": "success", "data": f"dummy result for query: {params.get('arguments',{}).get('query')}"}
                     response = create_jsonrpc_response(request_id, dummy_result)
                     logger.info("Sending tools/call dummy success response")
                     return response
                else:
                     logger.error(f"Unknown tool name: {tool_name}")
                     return create_jsonrpc_error(request_id, -32601, "Method not found", f"Tool '{tool_name}' not supported.")

            else:
                logger.error(f"Unknown method: {method}")
                return create_jsonrpc_error(request_id, -32601, "Method not found", f"Method '{method}' not supported.")

        except json.JSONDecodeError:
            logger.exception(f"JSON Decode Error: {request_data}")
            return create_jsonrpc_error(None, -32700, "Parse error")
        except Exception as e:
            logger.exception(f"Error handling request: {request_data}")
            req_id = None
            try: req_id = json.loads(request_data).get("id")
            except: pass 
            return create_jsonrpc_error(req_id, -32603, "Internal error", str(e))

    async def main():
        logger.info("Generic MCP Server starting...")
        
        reader = asyncio.StreamReader()
        protocol = asyncio.StreamReaderProtocol(reader)
        loop = asyncio.get_event_loop()
        await loop.connect_read_pipe(lambda: protocol, sys.stdin)

        writer_transport, writer_protocol = await loop.connect_write_pipe(asyncio.streams.FlowControlMixin, sys.stdout)
        writer = asyncio.StreamWriter(writer_transport, writer_protocol, None, loop)

        logger.info("Ready for requests on stdin.")
        while not reader.at_eof():
            try:
                line = await reader.readline()
                if not line: break 
                request_data = line.decode('utf-8').strip()
                if not request_data: continue
                
                logger.info(f"Raw line: {request_data}")
                response_json = await handle_request(request_data)
                
                if response_json:
                    logger.info(f"Sending response: {response_json.strip()}")
                    writer.write(response_json.encode('utf-8'))
                    await writer.drain()
                else:
                    logger.warning(f"No response for: {request_data}")

            except asyncio.CancelledError: break
            except Exception: logger.exception("Error in main loop")

        logger.info("Generic MCP Server shutting down.")
        writer.close()
        try: await writer.wait_closed()
        except Exception: pass

    if __name__ == "__main__":
        try: asyncio.run(main())
        except KeyboardInterrupt: logger.info("Server stopped by user.")
        except Exception: logger.exception("Unhandled exception.")`

and run python agent_app.py

Expected behavior

The application should:

  1. Start the MCP server subprocess.
  2. Complete the initialize and tools/list handshake.
  3. Make a call to the OpenAI API
  4. Receive a response from the LLM indicating it should use the generic_search tool.
  5. Send a tools/call request to the MCP server subprocess.
  6. Receive the dummy result from the MCP server.
  7. Send the tool result back to the LLM.
  8. Receive the final response from the LLM.
  9. Print the final response.

Instead:
The application hangs after step 2 (and after a trace ingest call). Logs show the tools/list response is successfully sent by the server, but the agent_app.py never proceeds to make the expected OpenAI API call or send a tools/call request.

Sample logs (had to remove with some dummy data):

2025-04-04 HH:MM:SS,ms - ReproApp - INFO - MCPServerStdio context entered.
2025-04-04 HH:MM:SS,ms - ReproApp - INFO - Initializing Agent (with MCP)...
2025-04-04 HH:MM:SS,ms - ReproApp - INFO - Agent initialized.
2025-04-04 HH:MM:SS,ms - ReproApp - INFO - Attempting to run agent with query: 'Use the tool to get dummy data.'
2025-04-04 HH:MM:SS,ms - ReproApp - INFO - Trace ID generated: trace_...
Trace URL: https://platform.openai.com/traces/trace?trace_id=trace_...
2025-04-04 HH:MM:SS,ms - ReproApp - INFO - Calling Runner.run()... Waiting for LLM/tool...
2025-04-04 HH:MM:SS,ms - httpx - INFO - HTTP Request: POST https://api.openai.com/v1/traces/ingest "HTTP/1.1 204 No Content"
<<< HANGS HERE INDEFINITELY >>>

(Simultaneously, generic_mcp_server.py logs show it successfully sent the tools/list response but receives nothing further)

Additional Context / Troubleshooting

  • The agent_app.py runs correctly and gets a response from the LLM if the MCPServerStdio and mcp_servers parameters are removed/commented out.
  • The generic_mcp_server.py script (and my original, more complex version) responds correctly to manual JSONRPC messages (initialize, tools/list, tools/call) sent via stdin.
  • The hang persists even if generic_mcp_server.py is modified to return an empty list [] for the tools/list request.
  • The hang persists with/without tool list caching enabled (cache_tools_list).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions