Skip to content

Commit 43ee2d9

Browse files
committed
refactor: use exclude_unset for JSONRPC wire serialization
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'.
1 parent c583fb6 commit 43ee2d9

File tree

13 files changed

+17
-27
lines changed

13 files changed

+17
-27
lines changed

src/mcp/client/sse.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ async def post_writer(endpoint_url: str):
138138
json=session_message.message.model_dump(
139139
by_alias=True,
140140
mode="json",
141-
exclude_none=True,
141+
exclude_unset=True,
142142
),
143143
)
144144
response.raise_for_status()

src/mcp/client/stdio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ async def stdin_writer():
167167
try:
168168
async with write_stream_reader:
169169
async for session_message in write_stream_reader:
170-
json = session_message.message.model_dump_json(by_alias=True, exclude_none=True)
170+
json = session_message.message.model_dump_json(by_alias=True, exclude_unset=True)
171171
await process.stdin.send(
172172
(json + "\n").encode(
173173
encoding=server.encoding,

src/mcp/client/streamable_http.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ async def _handle_post_request(self, ctx: RequestContext) -> None:
259259
async with ctx.client.stream(
260260
"POST",
261261
self.url,
262-
json=message.model_dump(by_alias=True, mode="json", exclude_none=True),
262+
json=message.model_dump(by_alias=True, mode="json", exclude_unset=True),
263263
headers=headers,
264264
) as response:
265265
if response.status_code == 202:

src/mcp/client/websocket.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ async def ws_writer():
6565
async with write_stream_reader:
6666
async for session_message in write_stream_reader:
6767
# Convert to a dict, then to JSON
68-
msg_dict = session_message.message.model_dump(by_alias=True, mode="json", exclude_none=True)
68+
msg_dict = session_message.message.model_dump(by_alias=True, mode="json", exclude_unset=True)
6969
await ws.send(json.dumps(msg_dict))
7070

7171
async with anyio.create_task_group() as tg:

src/mcp/server/lowlevel/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ async def _handle_request(
490490
except Exception as err:
491491
if raise_exceptions: # pragma: no cover
492492
raise err
493-
response = types.ErrorData(code=0, message=str(err), data=None)
493+
response = types.ErrorData(code=0, message=str(err))
494494

495495
await message.respond(response)
496496
else: # pragma: no cover

src/mcp/server/sse.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ async def sse_writer():
170170
await sse_stream_writer.send(
171171
{
172172
"event": "message",
173-
"data": session_message.message.model_dump_json(by_alias=True, exclude_none=True),
173+
"data": session_message.message.model_dump_json(by_alias=True, exclude_unset=True),
174174
}
175175
)
176176

src/mcp/server/stdio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ async def stdout_writer():
7171
try:
7272
async with write_stream_reader:
7373
async for session_message in write_stream_reader:
74-
json = session_message.message.model_dump_json(by_alias=True, exclude_none=True)
74+
json = session_message.message.model_dump_json(by_alias=True, exclude_unset=True)
7575
await stdout.write(json + "\n")
7676
await stdout.flush()
7777
except anyio.ClosedResourceError: # pragma: no cover

src/mcp/server/streamable_http.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ def _create_error_response(
303303
)
304304

305305
return Response(
306-
error_response.model_dump_json(by_alias=True, exclude_none=True),
306+
error_response.model_dump_json(by_alias=True, exclude_unset=True),
307307
status_code=status_code,
308308
headers=response_headers,
309309
)
@@ -323,7 +323,7 @@ def _create_json_response(
323323
response_headers[MCP_SESSION_ID_HEADER] = self.mcp_session_id
324324

325325
return Response(
326-
response_message.model_dump_json(by_alias=True, exclude_none=True) if response_message else None,
326+
response_message.model_dump_json(by_alias=True, exclude_unset=True) if response_message else None,
327327
status_code=status_code,
328328
headers=response_headers,
329329
)
@@ -336,7 +336,7 @@ def _create_event_data(self, event_message: EventMessage) -> dict[str, str]:
336336
"""Create event data dictionary from an EventMessage."""
337337
event_data = {
338338
"event": "message",
339-
"data": event_message.message.model_dump_json(by_alias=True, exclude_none=True),
339+
"data": event_message.message.model_dump_json(by_alias=True, exclude_unset=True),
340340
}
341341

342342
# If an event ID was provided, include it

src/mcp/server/streamable_http_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE
248248
error=ErrorData(code=INVALID_REQUEST, message="Session not found"),
249249
)
250250
response = Response(
251-
content=error_response.model_dump_json(by_alias=True, exclude_none=True),
251+
content=error_response.model_dump_json(by_alias=True, exclude_unset=True),
252252
status_code=HTTPStatus.NOT_FOUND,
253253
media_type="application/json",
254254
)

src/mcp/server/websocket.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ async def ws_writer():
4747
try:
4848
async with write_stream_reader:
4949
async for session_message in write_stream_reader:
50-
obj = session_message.message.model_dump_json(by_alias=True, exclude_none=True)
50+
obj = session_message.message.model_dump_json(by_alias=True, exclude_unset=True)
5151
await websocket.send_text(obj)
5252
except anyio.ClosedResourceError:
5353
await websocket.close()

0 commit comments

Comments
 (0)