How to Test MCP Servers Without Hanging the Process: A Complete Guide
Use the with_server.py helper script with a --timeout flag for stdio servers, and wrap evaluation calls in asyncio.wait_for() for HTTP/SSE endpoints to ensure hung processes or slow tool calls terminate cleanly.
Testing MCP (Modular Compute Protocol) servers in the ComposioHQ/awesome-codex-skills repository requires careful handling of asynchronous I/O and process lifecycle management. Without proper safeguards, a misbehaving server or blocking tool call can hang your test suite indefinitely, leaving zombie processes and open sockets. This guide explains the three built-in mechanisms that prevent test hangs and provides concrete code examples for both local stdio servers and remote HTTP endpoints.
Why MCP Servers Hang During Testing
An MCP server test involves three sequential stages: launching the server process, running the evaluation harness against it, and cleaning up resources. If any stage blocks—due to a server that never opens its port, a tool call that never returns, or a process that refuses to terminate—the entire test runner hangs.
The repository solves this through three complementary safety layers implemented in webapp-testing/scripts/with_server.py and mcp-builder/scripts/connections.py.
Three Safety Mechanisms to Prevent Test Hangs
Port-Ready Timeout
The is_server_ready function in webapp-testing/scripts/with_server.py (lines 23-32) polls the target port for a configurable duration (default 30 seconds). If the server never opens the port, it raises a RuntimeError that aborts the test before the evaluation even begins.
Graceful Process Termination
The finally block in webapp-testing/scripts/with_server.py (lines 91-102) ensures every child process is terminated. It first calls process.terminate() and forces a kill after a short wait, guaranteeing that stray server processes do not linger after a failed or timed-out test.
Async Context Cleanup
The MCPConnection class in mcp-builder/scripts/connections.py implements __aexit__ (lines 48-53) to close the underlying ClientSession and release network sockets. When used with async with connection:, this guarantees cleanup even if the evaluation coroutine raises an exception or times out.
Testing Stdio-Based MCP Servers
For local stdio-based servers—the most common setup for development—use the with_server.py wrapper. It handles process spawning, port polling, and guaranteed cleanup.
python scripts/with_server.py \
--server "python -m my_mcp_server" \
--port 8080 \
--timeout 15 \
-- python -m mcp_builder.scripts.evaluation tests/eval.xml
--timeout 15activates theis_server_readycheck inwith_server.py, limiting the wait for port 8080 to 15 seconds.- If the server fails to become reachable, the script raises
RuntimeErrorand immediately executes the cleanup block, terminating the child process viaprocess.terminate(). - The
finallyclause (lines 91-102) runs even if the evaluation crashes, preventing zombie processes.
Testing HTTP/SSE MCP Servers
For remote HTTP or SSE servers, you typically connect to an already-running service. Use the evaluation harness directly with asyncio.wait_for() to bound execution time.
import asyncio
from pathlib import Path
from mcp_builder.scripts.evaluation import run_evaluation
from mcp_builder.scripts.connections import create_connection
async def main():
connection = create_connection(
transport="http",
url="https://example.com/mcp",
headers={"Authorization": "Bearer <token>"}
)
async with connection:
report = await asyncio.wait_for(
run_evaluation(Path("tests/eval.xml"), connection, model="gpt-4.1"),
timeout=30
)
print(report)
if __name__ == "__main__":
asyncio.run(main())
async with connection:ensuresMCPConnection.__aexit__closes the session, freeing sockets even on timeout.asyncio.wait_foraborts the evaluation if any task exceeds 30 seconds, preventing indefinite hangs on slow or deadlocked tool calls.- The
asyncio.run(main())pattern (as seen inevaluation.pyline 408) provides a clean event loop that cancels pending I/O when timeouts expire.
Handling Individual Tool Timeouts
For finer-grained control, modify the agent_loop in mcp-builder/scripts/evaluation.py to wrap individual tool calls. This prevents a single misbehaving tool from blocking the entire evaluation suite.
# Inside agent_loop, replace the direct call with a timed version
tool_result = await asyncio.wait_for(
connection.call_tool(tool_name, tool_input),
timeout=10 # seconds per tool call
)
This approach is useful when specific tools in your MCP server have known latency issues or external dependencies that may fail to respond.
Summary
- Use
with_server.pyfor stdio servers to get automatic port polling (is_server_ready) and process cleanup (finallyblock) that prevents hangs and zombie processes. - Wrap evaluations in
asyncio.wait_for()when testing HTTP/SSE servers to enforce hard timeouts on the entire evaluation or individual tasks. - Always use
async with connection:when instantiatingMCPConnectionfrommcp-builder/scripts/connections.pyto ensure__aexit__closes sockets and sessions even during exceptions. - Apply per-tool timeouts inside the agent loop for granular control over individual tool invocations that might block.
Frequently Asked Questions
How do I debug a server that fails the port-ready check?
Check the server logs immediately before the timeout. The is_server_ready function in webapp-testing/scripts/with_server.py polls the port for 30 seconds by default; if your server takes longer to initialize, increase the --timeout value or verify that the server actually binds to the correct port specified in your command.
Can I use these patterns with pytest?
Yes. Wrap the with_server.py logic in a pytest fixture using subprocess.Popen with the same timeout logic, or use asyncio.wait_for in async pytest tests. Ensure you replicate the finally block cleanup pattern to terminate any server process started by the fixture.
What happens if asyncio.wait_for cancels the evaluation?
When asyncio.wait_for raises TimeoutError, the async with connection: block exits, triggering MCPConnection.__aexit__ in mcp-builder/scripts/connections.py (lines 48-53). This closes the underlying MCP session and releases network resources, preventing socket leaks even when the evaluation is forcefully cancelled.
Is there a way to test SSE servers locally without hanging?
Yes. Use the create_connection factory with transport="sse" and run it through the same asyncio.wait_for pattern shown for HTTP servers. Since SSE servers run as a long-running process, ensure you also manage the server process lifecycle using the termination logic from with_server.py if you are spawning the SSE server locally for tests.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →