Description
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:
- 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:
- Start the MCP server subprocess.
- Complete the initialize and tools/list handshake.
- Make a call to the OpenAI API
- Receive a response from the LLM indicating it should use the generic_search tool.
- Send a tools/call request to the MCP server subprocess.
- Receive the dummy result from the MCP server.
- Send the tool result back to the LLM.
- Receive the final response from the LLM.
- 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).