Skip to content

Commit 115bb4c

Browse files
Sehlani042付皓
authored andcommitted
fix(client): surface streamable HTTP 401 as JSON-RPC error
1 parent 53117cb commit 115bb4c

2 files changed

Lines changed: 29 additions & 0 deletions

File tree

src/mcp/client/streamable_http.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,8 @@ async def _handle_post_request(self, ctx: RequestContext) -> None:
356356
error_data = ErrorData(code=METHOD_NOT_FOUND, message="Not Found")
357357
else:
358358
error_data = ErrorData(code=INVALID_REQUEST, message="Session terminated")
359+
elif response.status_code == 401:
360+
error_data = ErrorData(code=INTERNAL_ERROR, message="Unauthorized")
359361
else:
360362
error_data = ErrorData(code=INTERNAL_ERROR, message="Server returned an error response")
361363
session_message = SessionMessage(JSONRPCError(jsonrpc="2.0", id=message.id, error=error_data))

tests/client/test_streamable_http.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from mcp_types import (
1919
CLIENT_CAPABILITIES_META_KEY,
2020
CLIENT_INFO_META_KEY,
21+
INTERNAL_ERROR,
2122
METHOD_NOT_FOUND,
2223
PROTOCOL_VERSION_META_KEY,
2324
JSONRPCError,
@@ -124,6 +125,32 @@ def handler(request: httpx.Request) -> httpx.Response:
124125
assert reply.message.error.code == METHOD_NOT_FOUND
125126

126127

128+
@pytest.mark.anyio
129+
async def test_bare_401_request_maps_to_unauthorized_jsonrpc_error() -> None:
130+
"""A bare HTTP 401 should reach the caller as a correlated JSON-RPC error.
131+
132+
Authorization failures can be operation-specific. The client transport must
133+
leave room for the agent/session layer to handle the denial instead of
134+
collapsing it into an indistinguishable transport failure.
135+
"""
136+
137+
def handler(request: httpx.Request) -> httpx.Response:
138+
return httpx.Response(401)
139+
140+
with anyio.fail_after(5):
141+
async with (
142+
httpx.AsyncClient(transport=httpx.MockTransport(handler)) as http,
143+
streamable_http_client("http://test/mcp", http_client=http) as (read, write),
144+
):
145+
await write.send(SessionMessage(JSONRPCRequest(jsonrpc="2.0", id=1, method="tools/call", params={})))
146+
reply = await read.receive()
147+
assert isinstance(reply, SessionMessage)
148+
assert isinstance(reply.message, JSONRPCError)
149+
assert reply.message.id == 1
150+
assert reply.message.error.code == INTERNAL_ERROR
151+
assert reply.message.error.message == "Unauthorized"
152+
153+
127154
@pytest.mark.anyio
128155
async def test_initialize_post_clears_cached_pv_header_and_unstamped_posts_read_it() -> None:
129156
"""``initialize`` discards the cached protocol-version header; every other POST reads it.

0 commit comments

Comments
 (0)