Skip to content

Commit a2b0f17

Browse files
maxisbeyclaude
andcommitted
fix: Replace arbitrary sleeps with active server readiness checks in tests
Eliminates race conditions in HTTP server tests by replacing fixed sleep delays with deterministic server readiness polling. This makes tests more reliable and often faster by eliminating unnecessary waits. Changes: - Created shared wait_for_server() helper in tests/test_helpers.py - Fixed flaky time.sleep(1) in test_streamable_http_security.py - Replaced duplicate wait_for_server() in test_sse_security.py with shared helper - Improved consistency by updating all HTTP server test fixtures in: - tests/shared/test_ws.py - tests/shared/test_sse.py - tests/shared/test_streamable_http.py The wait_for_server() helper: - Actively polls the server port by attempting TCP connections - Returns immediately when the server accepts connections - Uses 0.01-second intervals between attempts - Has a 5-second timeout that raises TimeoutError on true failures - Makes tests deterministic instead of probabilistic 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f97f7c4 commit a2b0f17

File tree

7 files changed

+45
-98
lines changed

7 files changed

+45
-98
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ venv = ".venv"
9898
# those private functions instead of testing the private functions directly. It makes it easier to maintain the code source
9999
# and refactor code that is not public.
100100
executionEnvironments = [
101-
{ root = "tests", reportUnusedFunction = false, reportPrivateUsage = false },
101+
{ root = "tests", extraPaths = ["."], reportUnusedFunction = false, reportPrivateUsage = false },
102102
{ root = "examples/servers", reportUnusedFunction = false },
103103
]
104104

tests/server/test_sse_security.py

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import logging
44
import multiprocessing
55
import socket
6-
import time
76

87
import httpx
98
import pytest
@@ -17,6 +16,7 @@
1716
from mcp.server.sse import SseServerTransport
1817
from mcp.server.transport_security import TransportSecuritySettings
1918
from mcp.types import Tool
19+
from tests.test_helpers import wait_for_server
2020

2121
logger = logging.getLogger(__name__)
2222
SERVER_NAME = "test_sse_security_server"
@@ -66,26 +66,6 @@ async def handle_sse(request: Request):
6666
uvicorn.run(starlette_app, host="127.0.0.1", port=port, log_level="error")
6767

6868

69-
def wait_for_server(port: int, timeout: float = 5.0) -> None:
70-
"""Wait for server to be ready to accept connections.
71-
72-
Polls the server port until it accepts connections or timeout is reached.
73-
This eliminates race conditions without arbitrary sleeps.
74-
"""
75-
start_time = time.time()
76-
while time.time() - start_time < timeout:
77-
try:
78-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
79-
s.settimeout(0.1)
80-
s.connect(("127.0.0.1", port))
81-
# Server is ready
82-
return
83-
except (ConnectionRefusedError, OSError):
84-
# Server not ready yet, retry quickly
85-
time.sleep(0.01)
86-
raise TimeoutError(f"Server on port {port} did not start within {timeout} seconds")
87-
88-
8969
def start_server_process(port: int, security_settings: TransportSecuritySettings | None = None):
9070
"""Start server in a separate process."""
9171
process = multiprocessing.Process(target=run_server_with_settings, args=(port, security_settings))

tests/server/test_streamable_http_security.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import logging
44
import multiprocessing
55
import socket
6-
import time
76
from collections.abc import AsyncGenerator
87
from contextlib import asynccontextmanager
98

@@ -18,6 +17,7 @@
1817
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
1918
from mcp.server.transport_security import TransportSecuritySettings
2019
from mcp.types import Tool
20+
from tests.test_helpers import wait_for_server
2121

2222
logger = logging.getLogger(__name__)
2323
SERVER_NAME = "test_streamable_http_security_server"
@@ -77,8 +77,8 @@ def start_server_process(port: int, security_settings: TransportSecuritySettings
7777
"""Start server in a separate process."""
7878
process = multiprocessing.Process(target=run_server_with_settings, args=(port, security_settings))
7979
process.start()
80-
# Give server time to start
81-
time.sleep(1)
80+
# Wait for server to be ready to accept connections
81+
wait_for_server(port)
8282
return process
8383

8484

tests/shared/test_sse.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
TextResourceContents,
3333
Tool,
3434
)
35+
from tests.test_helpers import wait_for_server
3536

3637
SERVER_NAME = "test_server_for_SSE"
3738

@@ -123,19 +124,8 @@ def server(server_port: int) -> Generator[None, None, None]:
123124
proc.start()
124125

125126
# Wait for server to be running
126-
max_attempts = 20
127-
attempt = 0
128127
print("waiting for server to start")
129-
while attempt < max_attempts:
130-
try:
131-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
132-
s.connect(("127.0.0.1", server_port))
133-
break
134-
except ConnectionRefusedError:
135-
time.sleep(0.1)
136-
attempt += 1
137-
else:
138-
raise RuntimeError(f"Server failed to start after {max_attempts} attempts")
128+
wait_for_server(server_port)
139129

140130
yield
141131

tests/shared/test_streamable_http.py

Lines changed: 5 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import json
88
import multiprocessing
99
import socket
10-
import time
1110
from collections.abc import Generator
1211
from typing import Any
1312

@@ -43,6 +42,7 @@
4342
from mcp.shared.message import ClientMessageMetadata
4443
from mcp.shared.session import RequestResponder
4544
from mcp.types import InitializeResult, TextContent, TextResourceContents, Tool
45+
from tests.test_helpers import wait_for_server
4646

4747
# Test constants
4848
SERVER_NAME = "test_streamable_http_server"
@@ -344,18 +344,7 @@ def basic_server(basic_server_port: int) -> Generator[None, None, None]:
344344
proc.start()
345345

346346
# Wait for server to be running
347-
max_attempts = 20
348-
attempt = 0
349-
while attempt < max_attempts:
350-
try:
351-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
352-
s.connect(("127.0.0.1", basic_server_port))
353-
break
354-
except ConnectionRefusedError:
355-
time.sleep(0.1)
356-
attempt += 1
357-
else:
358-
raise RuntimeError(f"Server failed to start after {max_attempts} attempts")
347+
wait_for_server(basic_server_port)
359348

360349
yield
361350

@@ -391,18 +380,7 @@ def event_server(
391380
proc.start()
392381

393382
# Wait for server to be running
394-
max_attempts = 20
395-
attempt = 0
396-
while attempt < max_attempts:
397-
try:
398-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
399-
s.connect(("127.0.0.1", event_server_port))
400-
break
401-
except ConnectionRefusedError:
402-
time.sleep(0.1)
403-
attempt += 1
404-
else:
405-
raise RuntimeError(f"Server failed to start after {max_attempts} attempts")
383+
wait_for_server(event_server_port)
406384

407385
yield event_store, f"http://127.0.0.1:{event_server_port}"
408386

@@ -422,18 +400,7 @@ def json_response_server(json_server_port: int) -> Generator[None, None, None]:
422400
proc.start()
423401

424402
# Wait for server to be running
425-
max_attempts = 20
426-
attempt = 0
427-
while attempt < max_attempts:
428-
try:
429-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
430-
s.connect(("127.0.0.1", json_server_port))
431-
break
432-
except ConnectionRefusedError:
433-
time.sleep(0.1)
434-
attempt += 1
435-
else:
436-
raise RuntimeError(f"Server failed to start after {max_attempts} attempts")
403+
wait_for_server(json_server_port)
437404

438405
yield
439406

@@ -1407,18 +1374,7 @@ def context_aware_server(basic_server_port: int) -> Generator[None, None, None]:
14071374
proc.start()
14081375

14091376
# Wait for server to be running
1410-
max_attempts = 20
1411-
attempt = 0
1412-
while attempt < max_attempts:
1413-
try:
1414-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
1415-
s.connect(("127.0.0.1", basic_server_port))
1416-
break
1417-
except ConnectionRefusedError:
1418-
time.sleep(0.1)
1419-
attempt += 1
1420-
else:
1421-
raise RuntimeError(f"Context-aware server failed to start after {max_attempts} attempts")
1377+
wait_for_server(basic_server_port)
14221378

14231379
yield
14241380

tests/shared/test_ws.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
TextResourceContents,
2727
Tool,
2828
)
29+
from tests.test_helpers import wait_for_server
2930

3031
SERVER_NAME = "test_server_for_WS"
3132

@@ -110,19 +111,8 @@ def server(server_port: int) -> Generator[None, None, None]:
110111
proc.start()
111112

112113
# Wait for server to be running
113-
max_attempts = 20
114-
attempt = 0
115114
print("waiting for server to start")
116-
while attempt < max_attempts:
117-
try:
118-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
119-
s.connect(("127.0.0.1", server_port))
120-
break
121-
except ConnectionRefusedError:
122-
time.sleep(0.1)
123-
attempt += 1
124-
else:
125-
raise RuntimeError(f"Server failed to start after {max_attempts} attempts")
115+
wait_for_server(server_port)
126116

127117
yield
128118

tests/test_helpers.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Common test utilities for MCP server tests."""
2+
3+
import socket
4+
import time
5+
6+
7+
def wait_for_server(port: int, timeout: float = 5.0) -> None:
8+
"""Wait for server to be ready to accept connections.
9+
10+
Polls the server port until it accepts connections or timeout is reached.
11+
This eliminates race conditions without arbitrary sleeps.
12+
13+
Args:
14+
port: The port number to check
15+
timeout: Maximum time to wait in seconds (default 5.0)
16+
17+
Raises:
18+
TimeoutError: If server doesn't start within the timeout period
19+
"""
20+
start_time = time.time()
21+
while time.time() - start_time < timeout:
22+
try:
23+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
24+
s.settimeout(0.1)
25+
s.connect(("127.0.0.1", port))
26+
# Server is ready
27+
return
28+
except (ConnectionRefusedError, OSError):
29+
# Server not ready yet, retry quickly
30+
time.sleep(0.01)
31+
raise TimeoutError(f"Server on port {port} did not start within {timeout} seconds")

0 commit comments

Comments
 (0)