|
18 | 18 | from mcp_types import ( |
19 | 19 | CLIENT_CAPABILITIES_META_KEY, |
20 | 20 | CLIENT_INFO_META_KEY, |
| 21 | + INTERNAL_ERROR, |
21 | 22 | METHOD_NOT_FOUND, |
22 | 23 | PROTOCOL_VERSION_META_KEY, |
23 | 24 | JSONRPCError, |
@@ -124,6 +125,32 @@ def handler(request: httpx.Request) -> httpx.Response: |
124 | 125 | assert reply.message.error.code == METHOD_NOT_FOUND |
125 | 126 |
|
126 | 127 |
|
| 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 | + |
127 | 154 | @pytest.mark.anyio |
128 | 155 | async def test_initialize_post_clears_cached_pv_header_and_unstamped_posts_read_it() -> None: |
129 | 156 | """``initialize`` discards the cached protocol-version header; every other POST reads it. |
|
0 commit comments