# How to Test MCP Servers Without Hanging the Process: A Complete Guide

> Learn how to test MCP servers without hanging processes. Use scripts and asyncio to ensure clean termination of hung processes and slow tool calls.

- Repository: [Composio/awesome-codex-skills](https://github.com/composiohq/awesome-codex-skills)
- Tags: how-to-guide
- Published: 2026-04-26

---

**Use the [`with_server.py`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/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`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/webapp-testing/scripts/with_server.py) and [`mcp-builder/scripts/connections.py`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/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`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/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`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/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`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/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`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/with_server.py) wrapper. It handles process spawning, port polling, and guaranteed cleanup.

```bash
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 15` activates the `is_server_ready` check in [`with_server.py`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/with_server.py), limiting the wait for port 8080 to 15 seconds.
- If the server fails to become reachable, the script raises `RuntimeError` and immediately executes the cleanup block, terminating the child process via `process.terminate()`.
- The `finally` clause (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.

```python
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:` ensures `MCPConnection.__aexit__` closes the session, freeing sockets even on timeout.
- `asyncio.wait_for` aborts the evaluation if any task exceeds 30 seconds, preventing indefinite hangs on slow or deadlocked tool calls.
- The `asyncio.run(main())` pattern (as seen in [`evaluation.py`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/evaluation.py) line 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`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/mcp-builder/scripts/evaluation.py) to wrap individual tool calls. This prevents a single misbehaving tool from blocking the entire evaluation suite.

```python

# 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.py`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/with_server.py)** for stdio servers to get automatic port polling (`is_server_ready`) and process cleanup (`finally` block) 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 instantiating `MCPConnection` from [`mcp-builder/scripts/connections.py`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/mcp-builder/scripts/connections.py) to 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`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/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`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/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`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/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`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/with_server.py) if you are spawning the SSE server locally for tests.