Streamable HTTP client: a 401 or 403 with a JSON-RPC error body and no WWW-Authenticate loses its HTTP status
Maintainers usually reply within 3 days
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 68/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- rust
- Domain
- authentication, backend
Research direction
Read crates/rmcp/src/transport/common/reqwest/streamable_http_client.rs, especially the non-success response branch around lines 276–306 and the earlier 401/403 checks. Compare its handling with legacy_discover_response and the plain-text error path. Done when a 401 or 403 with a JSON-RPC error body preserves the HTTP status for callers; run the relevant Streamable HTTP client tests.
Written by the indexing model from the issue text.
Description
When a Streamable HTTP server answers a POST with 401 Unauthorized (or 403 Forbidden) without a WWW-Authenticate header and with a JSON-RPC error body, the reqwest client returns Ok(StreamableHttpPostResponse::Json(..)). The caller then gets an ordinary JSON-RPC error, ClientInitializeError::JsonRpcError during initialize or ServiceError::McpError after it, and cannot tell that the server rejected the credentials.
Where
The non-success branch parses the body with parse_json_rpc_error before it looks at the status:
AuthRequired and InsufficientScope come only from the checks above it, which require WWW-Authenticate. The same 401 with a plain-text body becomes UnexpectedServerResponse("HTTP 401 Unauthorized: …"), so the status survives on that path but not on the JSON one.
Reproduce
rmcp 3.5.1 (main has the same code). The server answers initialize with:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{"jsonrpc":"2.0","id":0,"error":{"code":-32001,"message":"Unauthorized"}}
serve_client over StreamableHttpClientTransport::with_client(reqwest::Client, config) fails with ClientInitializeError::JsonRpcError (code -32001, message "Unauthorized"). Nothing in the error says HTTP 401.
Why it matters
A client that sends a static token through custom_headers wants to tell the user that the token was rejected. An OAuth client wants to start or refresh authorization. Both need the status. The MCP authorization spec asks servers that implement authorization to send WWW-Authenticate with a 401, but a server that only takes a static token is not bound by that and may answer with a JSON-RPC error body instead.
Possible fixes
- Handle 401 and 403 before the JSON-RPC branch, as
legacy_discover_responsealready does ("Keep authentication failures and server errors on their original paths"). For example, returnAuthRequiredorInsufficientScopefor every 401 or 403, with the challenge optional, or returnUnexpectedServerResponse("HTTP 401 …")as the plain-text path does. - Or keep the JSON-RPC error and carry the HTTP status with it.
- Dominant language
- Rust
- Stars
- 4k
- Forks
- 654
- Avg merge
- 3d 4h
- Merged PRs (30d)
- 37
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from modelcontextprotocol/rust-sdk
-
bug P2 ready for work T-config
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
modelcontextprotocol/rust-sdk#1335 ·
Maintainers usually reply within 3 days
-
auth: refresh_token() requests offline_access that was never granted, so refreshes fail with invalid_scopePossibly taken @jstar0 claimed this 2 days ago. Openbug P1 ready for work T-security T-transport
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
modelcontextprotocol/rust-sdk#1330 ·
Maintainers usually reply within 3 days
-
P3 question Stale T-documentation T-enhancement
Difficulty 1/5 Under an hour Newbie friendliness 86/100
modelcontextprotocol/rust-sdk#1155 · 1 comment ·
Maintainers usually reply within 3 days
-
enhancement P2 ready for work T-service
Difficulty 4/5 3-5 days Newbie friendliness 48/100
modelcontextprotocol/rust-sdk#1338 · 1 comment ·
Maintainers usually reply within 3 days
-
Non-object outputSchema and structuredContent are sent to clients that negotiated 2025-06-18 / 2025-11-25, where an object is requiredPossibly taken @Cassian433 claimed this today. Openbug P1 ready for work T-handler T-model
Difficulty 3/5 1-2 days Newbie friendliness 28/100
modelcontextprotocol/rust-sdk#1337 ·
Maintainers usually reply within 3 days
All issues in modelcontextprotocol/rust-sdk
Similar issues
-
Progress difficulty filter lists Hard before MediumPossibly taken @Pandamachi claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
sysprog21/codetrial#281 · 1 comment ·
Maintainers usually reply within 1 day
-
C-bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
rust-lang/rust-analyzer#23501 ·
Maintainers usually reply within 1 day
-
scripts/gen-gallery.py:118: a ready session now reports in_progress, so SESSION_READY_OLD can goOpennightly-audit
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
antithesishq/snouty#396 ·
Maintainers usually reply within 1 day
-
French BIP39 wordlist starts with a UTF-8 BOM, so generated French mnemonics carry U+FEFF and derive a non-canonical seedPossibly taken @Kshot3000 claimed this today. Open
Difficulty 1/5 Under an hour Newbie friendliness 91/100
ergoplatform/sigma-rust#976 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day