Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/mcp/client/streamable_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from mcp.shared._httpx_utils import create_mcp_http_client
from mcp.shared.message import ClientMessageMetadata, SessionMessage
from mcp.types import (
INTERNAL_ERROR,
INVALID_REQUEST,
PARSE_ERROR,
ErrorData,
Expand Down Expand Up @@ -273,7 +274,13 @@ async def _handle_post_request(self, ctx: RequestContext) -> None:
await ctx.read_stream_writer.send(session_message)
return

response.raise_for_status()
if response.status_code >= 400:
if isinstance(message, JSONRPCRequest):
error_data = ErrorData(code=INTERNAL_ERROR, message="Server returned an error response")
session_message = SessionMessage(JSONRPCError(jsonrpc="2.0", id=message.id, error=error_data))
await ctx.read_stream_writer.send(session_message)
return

if is_initialization:
self._maybe_extract_session_id_from_response(response)

Expand Down
34 changes: 34 additions & 0 deletions tests/client/test_notification_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,40 @@ async def test_unexpected_content_type_sends_jsonrpc_error() -> None:
await session.list_tools()


def _create_http_error_app(error_status: int) -> Starlette:
"""Create a server that returns an HTTP error for non-init requests."""

async def handle_mcp_request(request: Request) -> Response:
body = await request.body()
data = json.loads(body)

if data.get("method") == "initialize":
return _init_json_response(data)

if "id" not in data:
return Response(status_code=202)

return Response(status_code=error_status)

return Starlette(debug=True, routes=[Route("/mcp", handle_mcp_request, methods=["POST"])])


async def test_http_error_status_sends_jsonrpc_error() -> None:
"""Verify HTTP 5xx errors unblock the pending request with an MCPError.

When a server returns a non-2xx status code (e.g. 500), the client should
send a JSONRPCError so the pending request resolves immediately instead of
raising an unhandled httpx.HTTPStatusError that causes the caller to hang.
"""
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=_create_http_error_app(500))) as client:
async with streamable_http_client("http://localhost/mcp", http_client=client) as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session: # pragma: no branch
await session.initialize()

with pytest.raises(MCPError, match="Server returned an error response"): # pragma: no branch
await session.list_tools()


def _create_invalid_json_response_app() -> Starlette:
"""Create a server that returns invalid JSON for requests."""

Expand Down
Loading