Skip to content

fix(rmcp): surface JSON-RPC error bodies on HTTP 4xx responses#748

Open
kakarot-dev wants to merge 3 commits intomodelcontextprotocol:mainfrom
kakarot-dev:fix/http-4xx-jsonrpc-error-body
Open

fix(rmcp): surface JSON-RPC error bodies on HTTP 4xx responses#748
kakarot-dev wants to merge 3 commits intomodelcontextprotocol:mainfrom
kakarot-dev:fix/http-4xx-jsonrpc-error-body

Conversation

@kakarot-dev
Copy link
Contributor

When a server returns a 4xx status with Content-Type: application/json, attempt to deserialize the body as a ServerJsonRpcMessage before falling back to UnexpectedServerResponse. This allows JSON-RPC error payloads carried on HTTP error responses to be surfaced as McpError instead of being lost in a transport-level error string.

Fixes #724

Attempt to deserialize HTTP 4xx response bodies as ServerJsonRpcMessage when Content-Type: application/json, surfacing JSON-RPC errors as McpError instead of discarding them as UnexpectedServerResponse.

How Has This Been Tested?

Tested locally with a mock axum server against three scenarios: valid JSON-RPC error body on 400, non-JSON 400, and malformed JSON on 400. All pass.

Breaking Changes

None.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

I created a test file locally, Test file available if needed

When a server returns a 4xx status with Content-Type: application/json,
attempt to deserialize the body as a ServerJsonRpcMessage before falling
back to UnexpectedServerResponse. This allows JSON-RPC error payloads
carried on HTTP error responses to be surfaced as McpError instead of
being lost in a transport-level error string.

Fixes modelcontextprotocol#724
When a server returns a 4xx status with Content-Type: application/json,
attempt to deserialize the body as a ServerJsonRpcMessage before falling
back to UnexpectedServerResponse. This allows JSON-RPC error payloads
carried on HTTP error responses to be surfaced as McpError instead of
being lost in a transport-level error string.

Fixes modelcontextprotocol#724
@kakarot-dev kakarot-dev requested a review from a team as a code owner March 12, 2026 06:36
@github-actions github-actions bot added T-core Core library changes T-transport Transport layer changes labels Mar 12, 2026
Copy link
Member

@DaleSeo DaleSeo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tackling this, @kakarot-dev! I have a couple of comments below. It looks like you've done some manual testing, but it would be great if you could add some automated tests to avoid any regressions.

.as_deref()
.is_some_and(|ct| ct.as_bytes().starts_with(JSON_MIME_TYPE.as_bytes()))
{
match serde_json::from_str::<ServerJsonRpcMessage>(&body) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ServerJsonRpcMessage can parse any valid JSON-RPC message including Request, Response, Notification, not just Error. The deserialization should verify the parsed message is actually a JsonRpcMessage::Error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry about that, updated it so that it now only matches JsonRpcMessage::Error

Comment on lines +202 to +205
// For non-success responses, attempt to parse JSON-RPC error bodies
// before falling back to a transport error. HTTP 4xx responses with
// Content-Type: application/json may carry valid JSON-RPC error
// payloads that should be surfaced as McpError, not TransportSend.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says "HTTP 4xx responses" but the guard is !status.is_success(), which also covers 1xx, 3xx, and 5xx. This could be clearer and simpler.

Suggested change
// For non-success responses, attempt to parse JSON-RPC error bodies
// before falling back to a transport error. HTTP 4xx responses with
// Content-Type: application/json may carry valid JSON-RPC error
// payloads that should be surfaced as McpError, not TransportSend.
// Non-success responses may carry valid JSON-RPC error payloads that
// should be surfaced as McpError rather than lost in TransportSend.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated the comments, thank you

@github-actions github-actions bot added T-dependencies Dependencies related changes T-test Testing related changes T-config Configuration file changes labels Mar 13, 2026
@kakarot-dev kakarot-dev requested a review from DaleSeo March 13, 2026 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-config Configuration file changes T-core Core library changes T-dependencies Dependencies related changes T-test Testing related changes T-transport Transport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

reqwest streamable HTTP client drops JSON-RPC error bodies on HTTP 4xx into TransportSend

2 participants