Skip to content

Commit 705497a

Browse files
authored
fix: allow null id in JSONRPCError per JSON-RPC 2.0 spec (#2056)
1 parent 3b53fb9 commit 705497a

File tree

15 files changed

+151
-24
lines changed

15 files changed

+151
-24
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
@@ -260,7 +260,7 @@ async def _handle_post_request(self, ctx: RequestContext) -> None:
260260
async with ctx.client.stream(
261261
"POST",
262262
self.url,
263-
json=message.model_dump(by_alias=True, mode="json", exclude_none=True),
263+
json=message.model_dump(by_alias=True, mode="json", exclude_unset=True),
264264
headers=headers,
265265
) as response:
266266
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: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -298,12 +298,12 @@ def _create_error_response(
298298
# Return a properly formatted JSON error response
299299
error_response = JSONRPCError(
300300
jsonrpc="2.0",
301-
id="server-error", # We don't have a request ID for general errors
301+
id=None,
302302
error=ErrorData(code=error_code, message=error_message),
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
@@ -975,12 +975,11 @@ async def message_router():
975975
# Determine which request stream(s) should receive this message
976976
message = session_message.message
977977
target_request_id = None
978-
# Check if this is a response
979-
if isinstance(message, JSONRPCResponse | JSONRPCError):
980-
response_id = str(message.id)
981-
# If this response is for an existing request stream,
982-
# send it there
983-
target_request_id = response_id
978+
# Check if this is a response with a known request id.
979+
# Null-id errors (e.g., parse errors) fall through to
980+
# the GET stream since they can't be correlated.
981+
if isinstance(message, JSONRPCResponse | JSONRPCError) and message.id is not None:
982+
target_request_id = str(message.id)
984983
# Extract related_request_id from meta if it exists
985984
elif ( # pragma: no cover
986985
session_message.metadata is not None

src/mcp/server/streamable_http_manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,11 +244,11 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE
244244
# See: https://github.com/modelcontextprotocol/python-sdk/issues/1821
245245
error_response = JSONRPCError(
246246
jsonrpc="2.0",
247-
id="server-error",
247+
id=None,
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)