|
13 | 13 | import httpx |
14 | 14 | import pytest |
15 | 15 | from inline_snapshot import snapshot |
16 | | -from mcp_types import METHOD_NOT_FOUND, JSONRPCError, JSONRPCNotification, JSONRPCRequest, JSONRPCResponse |
| 16 | +from mcp_types import ( |
| 17 | + INTERNAL_ERROR, |
| 18 | + METHOD_NOT_FOUND, |
| 19 | + JSONRPCError, |
| 20 | + JSONRPCNotification, |
| 21 | + JSONRPCRequest, |
| 22 | + JSONRPCResponse, |
| 23 | +) |
17 | 24 |
|
18 | 25 | from mcp.client.streamable_http import streamable_http_client |
19 | 26 | from mcp.shared.inbound import MCP_PROTOCOL_VERSION_HEADER, encode_header_value |
@@ -103,6 +110,32 @@ def handler(request: httpx.Request) -> httpx.Response: |
103 | 110 | assert reply.message.error.code == METHOD_NOT_FOUND |
104 | 111 |
|
105 | 112 |
|
| 113 | +@pytest.mark.anyio |
| 114 | +async def test_bare_401_request_maps_to_unauthorized_jsonrpc_error() -> None: |
| 115 | + """A bare HTTP 401 should reach the caller as a correlated JSON-RPC error. |
| 116 | +
|
| 117 | + Authorization failures can be operation-specific. The client transport must |
| 118 | + leave room for the agent/session layer to handle the denial instead of |
| 119 | + collapsing it into an indistinguishable transport failure. |
| 120 | + """ |
| 121 | + |
| 122 | + def handler(request: httpx.Request) -> httpx.Response: |
| 123 | + return httpx.Response(401) |
| 124 | + |
| 125 | + with anyio.fail_after(5): |
| 126 | + async with ( |
| 127 | + httpx.AsyncClient(transport=httpx.MockTransport(handler)) as http, |
| 128 | + streamable_http_client("http://test/mcp", http_client=http) as (read, write), |
| 129 | + ): |
| 130 | + await write.send(SessionMessage(JSONRPCRequest(jsonrpc="2.0", id=1, method="tools/call", params={}))) |
| 131 | + reply = await read.receive() |
| 132 | + assert isinstance(reply, SessionMessage) |
| 133 | + assert isinstance(reply.message, JSONRPCError) |
| 134 | + assert reply.message.id == 1 |
| 135 | + assert reply.message.error.code == INTERNAL_ERROR |
| 136 | + assert reply.message.error.message == "Unauthorized" |
| 137 | + |
| 138 | + |
106 | 139 | @pytest.mark.anyio |
107 | 140 | async def test_initialize_post_clears_cached_pv_header_and_unstamped_posts_read_it() -> None: |
108 | 141 | """``initialize`` discards the cached protocol-version header; every other POST reads it. |
|
0 commit comments