fix: allow null id in JSONRPCError per JSON-RPC 2.0 spec by maxisbey · Pull Request #2056 · modelcontextprotocol/python-sdk
The JSON-RPC 2.0 specification requires error responses to use id: null when the request id could not be determined (e.g., parse errors, invalid requests). The SDK rejected null ids, forcing a non-compliant id="server-error" sentinel workaround. Changes: - JSONRPCError.id now accepts None (JSONRPCResponse.id unchanged) - Add model_serializer to preserve id: null under exclude_none=True - Replace id="server-error" sentinel with id=None in server transports - Add null-id guard in session layer to surface errors via message handler - Guard server-side message router against str(None) misrouting Github-Issue: #1821
Merge the nested if conditions in the server message router into a single condition so the false branch is naturally exercised by non-response messages. Remove isinstance guards in test callbacks since we control the input.
Restructure the null-id guard so that `id is None` is checked first, allowing Pyright to naturally narrow the type to JSONRPCError (since JSONRPCResponse.id is always RequestId). This eliminates the need for the assert that was previously required to work around Pyright's inability to narrow through negated compound conditions.
Switch all JSONRPC wire-level serialization from exclude_none=True to exclude_unset=True. This correctly preserves the null-vs-absent distinction required by JSON-RPC 2.0 (e.g., id must be null in parse error responses, not absent entirely). exclude_unset only omits fields not passed to the constructor, so explicitly set id=None is preserved while optional params/data fields that were never set are still omitted. This eliminates the need for the model_serializer hack on JSONRPCError that was re-inserting id after exclude_none stripped it. Inner MCP model serialization (capabilities, tools, resources, etc.) retains exclude_none=True since those types have many optional fields where None genuinely means 'omit'.
MCPError.__init__ always passed data=data to ErrorData(), even when data was None (the default). This marked data as 'set' in Pydantic's model_fields_set, causing exclude_unset=True to emit 'data': null on the wire. Fix by only passing data when non-None.
maxisbey
deleted the
fix/jsonrpc-error-null-id
branch
maxisbey added a commit that referenced this pull request
Collapse 12 dense bullets to 6: keep BaseSession removal, the new concurrency model, pre-enter/post-close error contracts, the raising- callback code change, the removed metadata kwargs, and the ClientRequestContext rename. Drop ids-from-1, the courtesy-cancel exemption taxonomy, the (incorrect) resumption-hints-from-callbacks bullet, the shutdown-CONNECTION_CLOSED detail, and the stray-response handling - these are feature/internals trivia, not migration steps. Also correct the protocol:error:null-id divergence note in the requirements manifest: v1.x had a non-nullable JSONRPCError.id, so a null-id error response failed transport validation and the ValidationError reached message_handler as an exception. The note previously described the v2-prerelease BaseSession's MCPError path (PR #2056) as v1 behavior.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters