Skip to content

[Schema][Server] Preserve request id in JSON-RPC error responses instead of fabricating id:""#379

Open
valeriudev wants to merge 1 commit into
modelcontextprotocol:mainfrom
valeriudev:fix/issue-333-parse-error-id
Open

[Schema][Server] Preserve request id in JSON-RPC error responses instead of fabricating id:""#379
valeriudev wants to merge 1 commit into
modelcontextprotocol:mainfrom
valeriudev:fix/issue-333-parse-error-id

Conversation

@valeriudev
Copy link
Copy Markdown

The bug

When the server receives input it cannot turn into a valid message, the JSON-RPC error response carries a fabricated empty-string id that was never in the request:

{"jsonrpc":"2.0","id":"","error":{"code":-32700,...}}

The reporter sent an initialize request with a real numeric id (900512) nested past PHP's json_decode() depth limit; the server replied with id:"". An empty string is not a valid JSON-RPC / MCP RequestId, so this breaks client correlation.

Root cause

Two paths both collapsed to "":

  • Error::forParseError() and Error::forInvalidRequest() defaulted $id to '', and Error::$id was typed string|int (no null). On an unrecoverable parse failure (\JsonException in Protocol::processInput()) the factory was called with no id, so "" reached the wire.
  • On an invalid-but-parseable message, MessageFactory threw InvalidInputMessageException without carrying the decoded id, so Protocol::handleInvalidMessage() also fell back to "" — even though the real id was recoverable from the decoded payload.

The fix

  • Unrecoverable parse errors (-32700) now return id: null, per JSON-RPC 2.0 (id is null when it cannot be determined).
  • Invalid-but-parseable requests (-32600) now preserve the original request id.
  • Error::$id / Error::getId() and the $id parameter of Error::forParseError() / Error::forInvalidRequest() are widened to string|int|null. jsonSerialize() is unchanged — it now emits "id":null instead of "id":"".
  • The recoverable id is threaded from MessageFactory through InvalidInputMessageException via new getRequestId()/setRequestId(). The id is only attached when the decoded value is a valid string|int (mirroring Request/Error::fromArray validation); id:0 is preserved (type-checked, not truthiness), while null/true/array/missing stay null.

No [BC Break]: the type is widened, not narrowed, and the only behavioral change is replacing an invalid fabricated id with a spec-compliant one.

Tests

Two regression tests in tests/Unit/Server/ProtocolTest.php:

  • testParseErrorDoesNotFabricateEmptyStringId — feeds JSON nested 600 deep so json_decode() throws, asserts error.code === -32700, id !== '', and that id is null or omitted.
  • testInvalidMessagePreservesRecoverableId — feeds valid JSON with id 42 but no method/result/error, asserts error.code === -32600 and id === 42.

Both were red for the right reason (got "") before the fix and are green after. Full unit suite (766 tests), phpstan (level 6), and php-cs-fixer all pass.

Fixes #333

…g id:""

When the server received input it could not turn into a valid message, the
error response carried a fabricated empty-string id that was never in the
request, breaking JSON-RPC client correlation.

The id default for the error factories was '' and that default reached the
wire unchanged: unrecoverable parse errors emitted id:"", and for invalid-
but-parseable messages MessageFactory discarded the decoded id so it also
fell back to "".

Now unrecoverable parse errors (-32700) return id: null per JSON-RPC 2.0,
and invalid-but-parseable requests (-32600) preserve the original request
id. Error::$id / getId() and the $id parameter of forParseError() /
forInvalidRequest() are widened to string|int|null, and the recoverable id
is threaded through InvalidInputMessageException.

Fixes modelcontextprotocol#333
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Server] stdio parse errors use id:"" instead of preserving the request id or returning null

1 participant