Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions src/mcp/client/streamable_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ async def _handle_post_request(self, ctx: RequestContext) -> None:
error_data = ErrorData(code=METHOD_NOT_FOUND, message="Not Found")
else:
error_data = ErrorData(code=INVALID_REQUEST, message="Session terminated")
elif response.status_code == 401:
error_data = ErrorData(code=INTERNAL_ERROR, message="Unauthorized")
else:
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))
Expand Down
35 changes: 34 additions & 1 deletion tests/client/test_streamable_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@
import httpx
import pytest
from inline_snapshot import snapshot
from mcp_types import METHOD_NOT_FOUND, JSONRPCError, JSONRPCNotification, JSONRPCRequest, JSONRPCResponse
from mcp_types import (
INTERNAL_ERROR,
METHOD_NOT_FOUND,
JSONRPCError,
JSONRPCNotification,
JSONRPCRequest,
JSONRPCResponse,
)

from mcp.client.streamable_http import streamable_http_client
from mcp.shared.inbound import MCP_PROTOCOL_VERSION_HEADER, encode_header_value
Expand Down Expand Up @@ -103,6 +110,32 @@ def handler(request: httpx.Request) -> httpx.Response:
assert reply.message.error.code == METHOD_NOT_FOUND


@pytest.mark.anyio
async def test_bare_401_request_maps_to_unauthorized_jsonrpc_error() -> None:
"""A bare HTTP 401 should reach the caller as a correlated JSON-RPC error.

Authorization failures can be operation-specific. The client transport must
leave room for the agent/session layer to handle the denial instead of
collapsing it into an indistinguishable transport failure.
"""

def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(401)

with anyio.fail_after(5):
async with (
httpx.AsyncClient(transport=httpx.MockTransport(handler)) as http,
streamable_http_client("http://test/mcp", http_client=http) as (read, write),
):
await write.send(SessionMessage(JSONRPCRequest(jsonrpc="2.0", id=1, method="tools/call", params={})))
reply = await read.receive()
assert isinstance(reply, SessionMessage)
assert isinstance(reply.message, JSONRPCError)
assert reply.message.id == 1
assert reply.message.error.code == INTERNAL_ERROR
assert reply.message.error.message == "Unauthorized"


@pytest.mark.anyio
async def test_initialize_post_clears_cached_pv_header_and_unstamped_posts_read_it() -> None:
"""``initialize`` discards the cached protocol-version header; every other POST reads it.
Expand Down
Loading