Skip to content

Commit 32e82f7

Browse files
committed
test: achieve 100% coverage on streamable_http.py
Add pragma annotations for 3 remaining uncovered paths: - pragma: no branch on mcp_session_id checks in _create_error_response and initialization handler (always True in stateful manager) - pragma: lax no cover on ClosedResourceError handler (non-deterministic) Add 5 integration tests for transport validation: - POST with invalid Content-Type (400) - POST/GET/DELETE with mismatched session ID (404) - PUT unsupported method (405) All 74 tests pass with 100% coverage and strict-no-cover clean.
1 parent cab7732 commit 32e82f7

File tree

2 files changed

+104
-3
lines changed

2 files changed

+104
-3
lines changed

src/mcp/server/streamable_http.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ def _create_error_response(
294294
if headers:
295295
response_headers.update(headers)
296296

297-
if self.mcp_session_id:
297+
if self.mcp_session_id: # pragma: no branch
298298
response_headers[MCP_SESSION_ID_HEADER] = self.mcp_session_id
299299

300300
# Return a properly formatted JSON error response
@@ -481,7 +481,7 @@ async def _handle_post_request(self, scope: Scope, request: Request, receive: Re
481481

482482
if is_initialization_request:
483483
# Check if the server already has an established session
484-
if self.mcp_session_id:
484+
if self.mcp_session_id: # pragma: no branch
485485
# Check if request has a session ID
486486
request_session_id = self._get_session_id(request)
487487

@@ -1026,7 +1026,7 @@ async def message_router():
10261026
for message. Still processing message as the client
10271027
might reconnect and replay."""
10281028
)
1029-
except anyio.ClosedResourceError:
1029+
except anyio.ClosedResourceError: # pragma: lax no cover
10301030
if self._terminated:
10311031
logger.debug("Read stream closed by client")
10321032
else:

tests/shared/test_streamable_http.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2482,3 +2482,104 @@ async def test_tool_with_standalone_stream_close_no_event_store(
24822482
assert result.content[0].type == "text"
24832483
assert isinstance(result.content[0], TextContent)
24842484
assert result.content[0].text == "Standalone stream close test done"
2485+
2486+
2487+
def test_post_invalid_content_type(basic_server: None, basic_server_url: str) -> None:
2488+
"""Test that POST with invalid Content-Type returns 400 (transport security)."""
2489+
url = f"{basic_server_url}/mcp"
2490+
session = requests.Session()
2491+
2492+
# First initialize to get a valid session
2493+
init_payload = {
2494+
"jsonrpc": "2.0",
2495+
"method": "initialize",
2496+
"params": {
2497+
"protocolVersion": "2025-03-26",
2498+
"capabilities": {},
2499+
"clientInfo": {"name": "test-client", "version": "1.0"},
2500+
},
2501+
"id": "init-1",
2502+
}
2503+
resp = session.post(url, json=init_payload, headers={"Accept": "application/json, text/event-stream"})
2504+
assert resp.status_code == 200
2505+
2506+
# Now POST with invalid Content-Type
2507+
resp = session.post(
2508+
url,
2509+
data="hello",
2510+
headers={"Content-Type": "text/plain", "Accept": "application/json, text/event-stream"},
2511+
)
2512+
assert resp.status_code == 400
2513+
assert "Invalid Content-Type header" in resp.text
2514+
2515+
2516+
def test_post_mismatched_session_id(basic_server: None, basic_server_url: str) -> None:
2517+
"""Test that POST with wrong session ID returns 404 (session manager)."""
2518+
url = f"{basic_server_url}/mcp"
2519+
session = requests.Session()
2520+
2521+
# First initialize
2522+
init_payload = {
2523+
"jsonrpc": "2.0",
2524+
"method": "initialize",
2525+
"params": {
2526+
"protocolVersion": "2025-03-26",
2527+
"capabilities": {},
2528+
"clientInfo": {"name": "test-client", "version": "1.0"},
2529+
},
2530+
"id": "init-1",
2531+
}
2532+
resp = session.post(url, json=init_payload, headers={"Accept": "application/json, text/event-stream"})
2533+
assert resp.status_code == 200
2534+
2535+
# POST with wrong session ID
2536+
resp = session.post(
2537+
url,
2538+
json={"jsonrpc": "2.0", "method": "tools/list", "id": "req-1"},
2539+
headers={
2540+
"Accept": "application/json, text/event-stream",
2541+
"Mcp-Session-Id": "wrong-session-id",
2542+
},
2543+
)
2544+
assert resp.status_code == 404
2545+
assert "Session not found" in resp.text
2546+
2547+
2548+
def test_get_mismatched_session_id(basic_server: None, basic_server_url: str) -> None:
2549+
"""Test that GET with wrong session ID returns 404 (session manager)."""
2550+
url = f"{basic_server_url}/mcp"
2551+
2552+
resp = requests.get(
2553+
url,
2554+
headers={
2555+
"Accept": "text/event-stream",
2556+
"Mcp-Session-Id": "wrong-session-id",
2557+
},
2558+
)
2559+
assert resp.status_code == 404
2560+
assert "Session not found" in resp.text
2561+
2562+
2563+
def test_delete_mismatched_session_id(basic_server: None, basic_server_url: str) -> None:
2564+
"""Test that DELETE with wrong session ID returns 404 (session manager)."""
2565+
url = f"{basic_server_url}/mcp"
2566+
2567+
resp = requests.delete(
2568+
url,
2569+
headers={"Mcp-Session-Id": "wrong-session-id"},
2570+
)
2571+
assert resp.status_code == 404
2572+
assert "Session not found" in resp.text
2573+
2574+
2575+
def test_unsupported_http_method(basic_server: None, basic_server_url: str) -> None:
2576+
"""Test that unsupported HTTP methods (e.g. PUT) return 405."""
2577+
url = f"{basic_server_url}/mcp"
2578+
2579+
resp = requests.put(
2580+
url,
2581+
json={"test": "data"},
2582+
headers={"Accept": "application/json"},
2583+
)
2584+
assert resp.status_code == 405
2585+
assert "Method Not Allowed" in resp.text

0 commit comments

Comments
 (0)