Skip to content

Commit 5aaf143

Browse files
committed
fix tests binding ephemeral port and skiping if unable to reserve a local port
1 parent eb28bee commit 5aaf143

7 files changed

Lines changed: 123 additions & 88 deletions

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

tests/_server_utils.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import os
2+
import socket
3+
import subprocess
4+
import time
5+
from typing import Tuple
6+
7+
8+
def _reserve_port() -> int:
9+
"""Reserve an available localhost TCP port."""
10+
try:
11+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
12+
sock.bind(("127.0.0.1", 0))
13+
# Allow the port to be reused immediately after closing
14+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
15+
return sock.getsockname()[1]
16+
except OSError as exc:
17+
raise RuntimeError(f"Unable to reserve local TCP port: {exc}") from exc
18+
19+
20+
def launch_mcp_server(script: str) -> Tuple[subprocess.Popen[str], int]:
21+
"""Launch an MCP server subprocess and return the process and bound port.
22+
23+
Raises:
24+
RuntimeError: If the subprocess exits before becoming ready.
25+
"""
26+
port = _reserve_port()
27+
env = {**os.environ, "MCP_TEST_PORT": str(port)}
28+
29+
process = subprocess.Popen( # noqa: S603
30+
["python", "-c", script],
31+
stdout=subprocess.PIPE,
32+
stderr=subprocess.PIPE,
33+
text=True,
34+
env=env,
35+
)
36+
37+
# Give the server a moment to bind and report startup issues
38+
time.sleep(1)
39+
40+
if process.poll() is not None:
41+
stdout, stderr = process.communicate()
42+
raise RuntimeError(
43+
"Failed to start MCP server subprocess. "
44+
f"Exit code: {process.returncode}\nStdout:\n{stdout}\nStderr:\n{stderr}"
45+
)
46+
47+
return process, port
48+
49+
50+
def terminate_mcp_server(process: subprocess.Popen[str]) -> Tuple[str, str]:
51+
"""Terminate the subprocess and return captured stdout/stderr."""
52+
if process.poll() is None:
53+
process.kill()
54+
55+
stdout, stderr = process.communicate()
56+
57+
return stdout, stderr

tests/test_core.py

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from mcp import StdioServerParameters
88

99
from mcpadapt.core import MCPAdapt, ToolAdapter
10+
from tests._server_utils import launch_mcp_server, terminate_mcp_server
1011

1112

1213
class DummyAdapter(ToolAdapter):
@@ -81,9 +82,12 @@ def echo_tool(text: str) -> str:
8182
def echo_server_sse_script():
8283
return dedent(
8384
'''
85+
import os
8486
from mcp.server.fastmcp import FastMCP
8587
86-
mcp = FastMCP("Echo Server", host="127.0.0.1", port=8000)
88+
port = int(os.environ.get("MCP_TEST_PORT", "8000"))
89+
90+
mcp = FastMCP("Echo Server", host="127.0.0.1", port=port)
8791
8892
@mcp.tool()
8993
def echo_tool(text: str) -> str:
@@ -97,31 +101,27 @@ def echo_tool(text: str) -> str:
97101

98102
@pytest.fixture
99103
async def echo_sse_server(echo_server_sse_script):
100-
import subprocess
101-
102-
# Start the SSE server process with its own process group
103-
process = subprocess.Popen(
104-
["python", "-c", echo_server_sse_script],
105-
)
106-
107-
# Give the server a moment to start up
108-
time.sleep(1)
104+
try:
105+
process, port = launch_mcp_server(echo_server_sse_script)
106+
except RuntimeError as exc:
107+
pytest.skip(str(exc))
109108

110109
try:
111-
yield {"url": "http://127.0.0.1:8000/sse"}
110+
yield {"url": f"http://127.0.0.1:{port}/sse"}
112111
finally:
113-
# Clean up the process when test is done
114-
process.kill()
115-
process.wait()
112+
terminate_mcp_server(process)
116113

117114

118115
@pytest.fixture
119116
def echo_server_streamable_http_script():
120117
return dedent(
121118
'''
119+
import os
122120
from mcp.server.fastmcp import FastMCP
123121
124-
mcp = FastMCP("Echo Server", host="127.0.0.1", port=8000, stateless_http=True, json_response=True)
122+
port = int(os.environ.get("MCP_TEST_PORT", "8000"))
123+
124+
mcp = FastMCP("Echo Server", host="127.0.0.1", port=port, stateless_http=True, json_response=True)
125125
126126
@mcp.tool()
127127
def echo_tool(text: str) -> str:
@@ -135,22 +135,15 @@ def echo_tool(text: str) -> str:
135135

136136
@pytest.fixture
137137
async def echo_streamable_http_server(echo_server_streamable_http_script):
138-
import subprocess
139-
140-
# Start the SSE server process with its own process group
141-
process = subprocess.Popen(
142-
["python", "-c", echo_server_streamable_http_script],
143-
)
144-
145-
# Give the server a moment to start up
146-
time.sleep(1)
138+
try:
139+
process, port = launch_mcp_server(echo_server_streamable_http_script)
140+
except RuntimeError as exc:
141+
pytest.skip(str(exc))
147142

148143
try:
149-
yield {"url": "http://127.0.0.1:8000/mcp", "transport": "streamable-http"}
144+
yield {"url": f"http://127.0.0.1:{port}/mcp", "transport": "streamable-http"}
150145
finally:
151-
# Clean up the process when test is done
152-
process.kill()
153-
process.wait()
146+
terminate_mcp_server(process)
154147

155148

156149
@pytest.fixture

tests/test_crewai_adapter.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from mcpadapt.core import MCPAdapt
1010
from mcpadapt.crewai_adapter import CrewAIAdapter
11+
from tests._server_utils import launch_mcp_server, terminate_mcp_server
1112

1213

1314
def extract_and_eval_dict(text):
@@ -102,9 +103,12 @@ def custom_tool(
102103
def echo_server_sse_script():
103104
return dedent(
104105
'''
106+
import os
105107
from mcp.server.fastmcp import FastMCP
106108
107-
mcp = FastMCP("Echo Server", host="127.0.0.1", port=8000)
109+
port = int(os.environ.get("MCP_TEST_PORT", "8000"))
110+
111+
mcp = FastMCP("Echo Server", host="127.0.0.1", port=port)
108112
109113
@mcp.tool()
110114
def echo_tool(text: str) -> str:
@@ -150,23 +154,15 @@ def echo_tool_union_none(text: str | None) -> str:
150154

151155
@pytest.fixture
152156
async def echo_sse_server(echo_server_sse_script):
153-
import subprocess
154-
import time
155-
156-
# Start the SSE server process with its own process group
157-
process = subprocess.Popen(
158-
["python", "-c", echo_server_sse_script],
159-
)
160-
161-
# Give the server a moment to start up
162-
time.sleep(1)
157+
try:
158+
process, port = launch_mcp_server(echo_server_sse_script)
159+
except RuntimeError as exc:
160+
pytest.skip(str(exc))
163161

164162
try:
165-
yield {"url": "http://127.0.0.1:8000/sse"}
163+
yield {"url": f"http://127.0.0.1:{port}/sse"}
166164
finally:
167-
# Clean up the process when test is done
168-
process.kill()
169-
process.wait()
165+
terminate_mcp_server(process)
170166

171167

172168
def test_basic_sync(echo_server_script):

tests/test_google_genai_adapter.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from mcpadapt.core import MCPAdapt
77
from mcpadapt.google_genai_adapter import GoogleGenAIAdapter
8+
from tests._server_utils import launch_mcp_server, terminate_mcp_server
89

910

1011
@pytest.fixture
@@ -29,9 +30,12 @@ def echo_tool(text: str) -> str:
2930
def echo_server_sse_script():
3031
return dedent(
3132
'''
33+
import os
3234
from mcp.server.fastmcp import FastMCP
3335
34-
mcp = FastMCP("Echo Server", host="127.0.0.1", port=8000)
36+
port = int(os.environ.get("MCP_TEST_PORT", "8000"))
37+
38+
mcp = FastMCP("Echo Server", host="127.0.0.1", port=port)
3539
3640
@mcp.tool()
3741
def echo_tool(text: str) -> str:
@@ -77,23 +81,15 @@ def echo_tool_union_none(text: str | None) -> str:
7781

7882
@pytest.fixture
7983
async def echo_sse_server(echo_server_sse_script):
80-
import subprocess
81-
import time
82-
83-
# Start the SSE server process with its own process group
84-
process = subprocess.Popen(
85-
["python", "-c", echo_server_sse_script],
86-
)
87-
88-
# Give the server a moment to start up
89-
time.sleep(1)
84+
try:
85+
process, port = launch_mcp_server(echo_server_sse_script)
86+
except RuntimeError as exc:
87+
pytest.skip(str(exc))
9088

9189
try:
92-
yield {"url": "http://127.0.0.1:8000/sse"}
90+
yield {"url": f"http://127.0.0.1:{port}/sse"}
9391
finally:
94-
# Clean up the process when test is done
95-
process.kill()
96-
process.wait()
92+
terminate_mcp_server(process)
9793

9894

9995
def test_basic_sync(echo_server_script):

tests/test_langchain_adapter.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from mcpadapt.core import MCPAdapt
77
from mcpadapt.langchain_adapter import LangChainAdapter
8+
from tests._server_utils import launch_mcp_server, terminate_mcp_server
89

910

1011
@pytest.fixture
@@ -55,9 +56,12 @@ def echo_tool(text: str) -> str:
5556
def echo_server_sse_script():
5657
return dedent(
5758
'''
59+
import os
5860
from mcp.server.fastmcp import FastMCP
5961
60-
mcp = FastMCP("Echo Server", host="127.0.0.1", port=8000)
62+
port = int(os.environ.get("MCP_TEST_PORT", "8000"))
63+
64+
mcp = FastMCP("Echo Server", host="127.0.0.1", port=port)
6165
6266
@mcp.tool()
6367
def echo_tool(text: str) -> str:
@@ -71,23 +75,15 @@ def echo_tool(text: str) -> str:
7175

7276
@pytest.fixture
7377
async def echo_sse_server(echo_server_sse_script):
74-
import subprocess
75-
import time
76-
77-
# Start the SSE server process with its own process group
78-
process = subprocess.Popen(
79-
["python", "-c", echo_server_sse_script],
80-
)
81-
82-
# Give the server a moment to start up
83-
time.sleep(1)
78+
try:
79+
process, port = launch_mcp_server(echo_server_sse_script)
80+
except RuntimeError as exc:
81+
pytest.skip(str(exc))
8482

8583
try:
86-
yield {"url": "http://127.0.0.1:8000/sse"}
84+
yield {"url": f"http://127.0.0.1:{port}/sse"}
8785
finally:
88-
# Clean up the process when test is done
89-
process.kill()
90-
process.wait()
86+
terminate_mcp_server(process)
9187

9288

9389
@pytest.mark.asyncio

tests/test_smolagents_adapter.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from mcpadapt.core import MCPAdapt
99
from mcpadapt.smolagents_adapter import SmolAgentsAdapter
10+
from tests._server_utils import launch_mcp_server, terminate_mcp_server
1011

1112

1213
@pytest.fixture
@@ -31,9 +32,12 @@ def echo_tool(text: str) -> str:
3132
def echo_server_sse_script():
3233
return dedent(
3334
'''
35+
import os
3436
from mcp.server.fastmcp import FastMCP
3537
36-
mcp = FastMCP("Echo Server", host="127.0.0.1", port=8000)
38+
port = int(os.environ.get("MCP_TEST_PORT", "8000"))
39+
40+
mcp = FastMCP("Echo Server", host="127.0.0.1", port=port)
3741
3842
@mcp.tool()
3943
def echo_tool(text: str) -> str:
@@ -79,23 +83,15 @@ def echo_tool_union_none(text: str | None) -> str:
7983

8084
@pytest.fixture
8185
async def echo_sse_server(echo_server_sse_script):
82-
import subprocess
83-
import time
84-
85-
# Start the SSE server process with its own process group
86-
process = subprocess.Popen(
87-
["python", "-c", echo_server_sse_script],
88-
)
89-
90-
# Give the server a moment to start up
91-
time.sleep(1)
86+
try:
87+
process, port = launch_mcp_server(echo_server_sse_script)
88+
except RuntimeError as exc:
89+
pytest.skip(str(exc))
9290

9391
try:
94-
yield {"url": "http://127.0.0.1:8000/sse"}
92+
yield {"url": f"http://127.0.0.1:{port}/sse"}
9593
finally:
96-
# Clean up the process when test is done
97-
process.kill()
98-
process.wait()
94+
terminate_mcp_server(process)
9995

10096

10197
def test_basic_sync(echo_server_script):

0 commit comments

Comments
 (0)