Skip to content

Answer envelope-invalid requests with errors correlated to the original id - #3365

Closed
Parker-Fawcett wants to merge 1 commit into
modelcontextprotocol:mainfrom
Parker-Fawcett:fix/2848-correlate-envelope-error-ids
Closed

Answer envelope-invalid requests with errors correlated to the original id#3365
Parker-Fawcett wants to merge 1 commit into
modelcontextprotocol:mainfrom
Parker-Fawcett:fix/2848-correlate-envelope-error-ids

Conversation

@Parker-Fawcett

Copy link
Copy Markdown

Answer envelope-invalid requests with errors correlated to the original id

Fixes #2848

What

Three server-side parse-failure paths dropped or de-correlated the original JSON-RPC request id:

Path Before After
Streamable HTTP (2025 era) HTTP 400, id: null, -32602 HTTP 400, original id echoed, -32600
Streamable HTTP (2026-07-28 single-exchange entry) HTTP 400, id: null HTTP 400, original id echoed
stdio nothing sent; frame dropped -32600 with recovered id via the session dispatcher

SSE is unchanged (its plain-text 400 is a separate redesign, noted on the issue).

How

  • New request_id_in() in mcp/shared/dispatcher.py narrows a decoded payload's top-level id (scalar string/int, bool rejected — same rules as the existing as_request_id).
  • New UnparseableMessageError in mcp/shared/jsonrpc_dispatcher.py: transports wrap undecodable frames in it, carrying the raw payload and the original exception as __cause__. Its request_id property does best-effort recovery (json.loads + request_id_in; (ValueError, RecursionError) safe).
  • JSONRPCDispatcher._dispatch, when no on_stream_exception observer is installed, now answers an UnparseableMessageError whose id is recoverable with INVALID_REQUEST through the normal _write_error path. Bare exceptions and unrecoverable frames keep the existing drop behavior, so client sessions (which install observers) are unaffected.
  • Both HTTP entries echo the recovered id directly in their validation-error responses.

Behavior change flagged for review

The 2025-era streamable HTTP entry's malformed/batched-body error code moves -32602-32600. Rationale: JSON-RPC 2.0 defines -32600 as "the JSON sent is not a valid Request object", which is precisely this case; the modern entry already uses -32600 here; and #2848 explicitly accepts either code. Existing tests only asserted status 400 + message substring, so the surface change is limited to the code value and the echoed id.

Tests

  • tests/shared/test_jsonrpc_dispatcher.py: correlated -32600 emission + loop-health after; id-recovery matrix (int/str/null/list/object-id, unparseable JSON, scalar, no payload); bare-exception drop behavior preserved.
  • tests/server/test_stdio.py: all three repro inputs from Invalid JSON-RPC envelope errors are not correlated with the original request id #2848 over real injected stdio streams.
  • tests/shared/test_streamable_http.py: id echo on all three inputs.
  • tests/server/test_streamable_http_modern.py: parametrized rejection table updated for echoed ids on posted-response/error bodies; focused test for the three issue inputs.
  • Interaction suites updated where they pinned the old wire contract.

Full gate green locally: ./scripts/test (5761 passed, 100% branch coverage, strict-no-cover clean), ruff format/check, pyright.

(AI-assisted implementation, prepared with a coding agent and reviewed by me.)

…al id

A request that is valid JSON but not a valid JSON-RPC envelope was
answered (or dropped) with an error carrying no request id, so clients
could not correlate the failure:

- streamable HTTP: the 400 validation-error body now echoes the
  original top-level id and uses INVALID_REQUEST (-32600), matching
  the JSON-RPC 2.0 meaning of an invalid Request object.
- the 2026-07-28 single-exchange entry: same id echo on its
  malformed-envelope rejection.
- stdio: a line that fails to decode now surfaces as an
  UnparseableMessageError carrying the raw payload, and the session's
  dispatcher answers it with INVALID_REQUEST naming the recovered id
  instead of dropping it silently.

Fixes modelcontextprotocol#2848
@github-actions github-actions Bot added the missing-issue-link Auto-closed: PR needs a linked issue assigned to its author (see CONTRIBUTING.md) label Aug 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for the contribution. This repository only keeps pull requests open when they're linked to an issue that a maintainer has assigned to the author — CONTRIBUTING.md explains why and how we work. This PR has been closed for now because you aren't currently assigned to #2848.

If a maintainer would like this change as a PR from you, they'll assign you to #2848 and this PR will reopen automatically — there's nothing more you need to do. (If you opened the issue, this PR already shows up on its timeline.)

There's no need to open a new PR — this one will be reopened. While it's closed, please push any updates as new commits rather than force-pushing, since GitHub can't reopen a PR whose branch has been rewritten.

Maintainers: reopening this PR, removing the missing-issue-link label, or adding bypass-issue-check bypasses the check.

@github-actions github-actions Bot closed this Aug 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

missing-issue-link Auto-closed: PR needs a linked issue assigned to its author (see CONTRIBUTING.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Invalid JSON-RPC envelope errors are not correlated with the original request id

1 participant