Skip to content

Commit e6cdb23

Browse files
test
1 parent 3a98694 commit e6cdb23

File tree

1 file changed

+169
-0
lines changed

1 file changed

+169
-0
lines changed
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
"""
2+
Tests for StreamableHTTP client transport with non-SDK servers.
3+
4+
These tests verify client behavior when interacting with servers
5+
that don't follow SDK conventions.
6+
"""
7+
8+
import json
9+
import multiprocessing
10+
import socket
11+
import time
12+
from collections.abc import Generator
13+
14+
import pytest
15+
import uvicorn
16+
from starlette.applications import Starlette
17+
from starlette.requests import Request
18+
from starlette.responses import JSONResponse, Response
19+
from starlette.routing import Route
20+
21+
from mcp.client.session import ClientSession
22+
from mcp.client.streamable_http import streamablehttp_client
23+
from mcp.types import ClientNotification, Implementation, RootsListChangedNotification
24+
25+
26+
def create_non_sdk_server_app() -> Starlette:
27+
"""Create a minimal server that doesn't follow SDK conventions."""
28+
29+
async def handle_mcp_request(request: Request) -> Response:
30+
"""Handle MCP requests with non-standard responses."""
31+
try:
32+
body = await request.body()
33+
data = json.loads(body)
34+
35+
# Handle initialize request normally
36+
if data.get("method") == "initialize":
37+
response_data = {
38+
"jsonrpc": "2.0",
39+
"id": data["id"],
40+
"result": {
41+
"serverInfo": {
42+
"name": "test-non-sdk-server",
43+
"version": "1.0.0"
44+
},
45+
"protocolVersion": "2024-11-05",
46+
"capabilities": {}
47+
}
48+
}
49+
return JSONResponse(response_data)
50+
51+
# For notifications, return 204 No Content (non-SDK behavior)
52+
if "id" not in data:
53+
return Response(status_code=204)
54+
55+
# Default response for other requests
56+
return JSONResponse({
57+
"jsonrpc": "2.0",
58+
"id": data.get("id"),
59+
"error": {
60+
"code": -32601,
61+
"message": "Method not found"
62+
}
63+
})
64+
65+
except Exception as e:
66+
return JSONResponse(
67+
{"error": f"Server error: {str(e)}"},
68+
status_code=500
69+
)
70+
71+
app = Starlette(
72+
debug=True,
73+
routes=[
74+
Route("/mcp", handle_mcp_request, methods=["POST"]),
75+
],
76+
)
77+
return app
78+
79+
80+
def run_non_sdk_server(port: int) -> None:
81+
"""Run the non-SDK server in a separate process."""
82+
app = create_non_sdk_server_app()
83+
config = uvicorn.Config(
84+
app=app,
85+
host="127.0.0.1",
86+
port=port,
87+
log_level="error", # Reduce noise in tests
88+
)
89+
server = uvicorn.Server(config=config)
90+
server.run()
91+
92+
93+
@pytest.fixture
94+
def non_sdk_server_port() -> int:
95+
"""Get an available port for the test server."""
96+
with socket.socket() as s:
97+
s.bind(("127.0.0.1", 0))
98+
return s.getsockname()[1]
99+
100+
101+
@pytest.fixture
102+
def non_sdk_server(non_sdk_server_port: int) -> Generator[None, None, None]:
103+
"""Start a non-SDK server for testing."""
104+
proc = multiprocessing.Process(
105+
target=run_non_sdk_server,
106+
kwargs={"port": non_sdk_server_port},
107+
daemon=True
108+
)
109+
proc.start()
110+
111+
# Wait for server to be ready
112+
start_time = time.time()
113+
while time.time() - start_time < 10:
114+
try:
115+
with socket.create_connection(("127.0.0.1", non_sdk_server_port), timeout=0.1):
116+
break
117+
except (TimeoutError, ConnectionRefusedError):
118+
time.sleep(0.1)
119+
else:
120+
proc.kill()
121+
proc.join(timeout=2)
122+
pytest.fail("Server failed to start within 10 seconds")
123+
124+
yield
125+
126+
proc.kill()
127+
proc.join(timeout=2)
128+
129+
130+
@pytest.mark.anyio
131+
async def test_notification_with_204_response(
132+
non_sdk_server: None,
133+
non_sdk_server_port: int
134+
) -> None:
135+
"""Test that client handles 204 responses to notifications correctly.
136+
137+
This test verifies the fix for the issue where non-SDK servers
138+
might return 204 No Content for notifications instead of 202 Accepted.
139+
The client should handle this gracefully without trying to parse
140+
the response body.
141+
"""
142+
server_url = f"http://127.0.0.1:{non_sdk_server_port}/mcp"
143+
144+
async with streamablehttp_client(server_url) as (read_stream, write_stream, get_session_id):
145+
async with ClientSession(
146+
read_stream,
147+
write_stream,
148+
client_info=Implementation(name="test-client", version="1.0.0")
149+
) as session:
150+
# Initialize should work normally
151+
await session.initialize()
152+
153+
# Send a notification - this should not raise an error
154+
# even though the server returns 204 instead of 202
155+
notification_sent = False
156+
try:
157+
await session.send_notification(
158+
ClientNotification(
159+
RootsListChangedNotification(
160+
method="notifications/roots/list_changed",
161+
params={}
162+
)
163+
)
164+
)
165+
notification_sent = True
166+
except Exception as e:
167+
pytest.fail(f"Notification failed with 204 response: {e}")
168+
169+
assert notification_sent, "Notification should have been sent successfully"

0 commit comments

Comments
 (0)