From 8055ec7a17cf1d3c7740f276417d3c903c4e9b09 Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Sat, 28 Feb 2026 07:00:37 +0100 Subject: [PATCH 1/4] fix: reject JSON-RPC requests with id: null instead of misclassifying as notification When a JSON-RPC message arrives with "id": null, it should be rejected. Both JSON-RPC 2.0 and the MCP spec restrict request IDs to strings or integers. Previously, JSONRPCRequest correctly rejected null IDs, but Pydantic fell through to JSONRPCNotification which silently absorbed the extra "id" field via implicit extra='allow'. The caller got a 202 with no response and no error. Set extra='forbid' on JSONRPCNotification so that any unrecognised field (including "id": null) causes a validation error. Fixes #2057 --- src/mcp/types/jsonrpc.py | 4 ++- tests/issues/test_2057_null_id.py | 52 +++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 tests/issues/test_2057_null_id.py diff --git a/src/mcp/types/jsonrpc.py b/src/mcp/types/jsonrpc.py index 84304a37c..b1acbe3e1 100644 --- a/src/mcp/types/jsonrpc.py +++ b/src/mcp/types/jsonrpc.py @@ -4,7 +4,7 @@ from typing import Annotated, Any, Literal -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, ConfigDict, Field, TypeAdapter RequestId = Annotated[int, Field(strict=True)] | str """The ID of a JSON-RPC request.""" @@ -22,6 +22,8 @@ class JSONRPCRequest(BaseModel): class JSONRPCNotification(BaseModel): """A JSON-RPC notification which does not expect a response.""" + model_config = ConfigDict(extra="forbid") + jsonrpc: Literal["2.0"] method: str params: dict[str, Any] | None = None diff --git a/tests/issues/test_2057_null_id.py b/tests/issues/test_2057_null_id.py new file mode 100644 index 000000000..d3372df2a --- /dev/null +++ b/tests/issues/test_2057_null_id.py @@ -0,0 +1,52 @@ +"""Test for issue #2057: Requests with "id": null silently misclassified as notifications.""" + +import pytest +from pydantic import ValidationError + +from mcp.types import JSONRPCMessage, JSONRPCNotification, JSONRPCRequest, jsonrpc_message_adapter + + +class TestNullIdRejection: + """Verify that JSON-RPC messages with id: null are rejected.""" + + def test_request_rejects_null_id(self): + """JSONRPCRequest should reject id: null.""" + with pytest.raises(ValidationError): + JSONRPCRequest.model_validate( + {"jsonrpc": "2.0", "method": "initialize", "id": None} + ) + + def test_notification_rejects_extra_id_field(self): + """JSONRPCNotification should not absorb an extra 'id' field.""" + with pytest.raises(ValidationError): + JSONRPCNotification.model_validate( + {"jsonrpc": "2.0", "method": "initialize", "id": None} + ) + + def test_message_adapter_rejects_null_id(self): + """The union adapter should reject messages with id: null entirely.""" + with pytest.raises(ValidationError): + jsonrpc_message_adapter.validate_python( + {"jsonrpc": "2.0", "method": "initialize", "id": None} + ) + + def test_valid_notification_without_id(self): + """A proper notification (no id field) should still validate.""" + msg = jsonrpc_message_adapter.validate_python( + {"jsonrpc": "2.0", "method": "notifications/initialized"} + ) + assert isinstance(msg, JSONRPCNotification) + + def test_valid_request_with_int_id(self): + """A proper request with an integer id should still validate.""" + msg = jsonrpc_message_adapter.validate_python( + {"jsonrpc": "2.0", "method": "initialize", "id": 1} + ) + assert isinstance(msg, JSONRPCRequest) + + def test_valid_request_with_string_id(self): + """A proper request with a string id should still validate.""" + msg = jsonrpc_message_adapter.validate_python( + {"jsonrpc": "2.0", "method": "initialize", "id": "abc-123"} + ) + assert isinstance(msg, JSONRPCRequest) From 0035dfc406c78056724ec7953572815ecbef926b Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Sat, 28 Feb 2026 14:05:52 +0100 Subject: [PATCH 2/4] style: apply ruff format --- tests/issues/test_2057_null_id.py | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/tests/issues/test_2057_null_id.py b/tests/issues/test_2057_null_id.py index d3372df2a..2bfeb819e 100644 --- a/tests/issues/test_2057_null_id.py +++ b/tests/issues/test_2057_null_id.py @@ -12,41 +12,29 @@ class TestNullIdRejection: def test_request_rejects_null_id(self): """JSONRPCRequest should reject id: null.""" with pytest.raises(ValidationError): - JSONRPCRequest.model_validate( - {"jsonrpc": "2.0", "method": "initialize", "id": None} - ) + JSONRPCRequest.model_validate({"jsonrpc": "2.0", "method": "initialize", "id": None}) def test_notification_rejects_extra_id_field(self): """JSONRPCNotification should not absorb an extra 'id' field.""" with pytest.raises(ValidationError): - JSONRPCNotification.model_validate( - {"jsonrpc": "2.0", "method": "initialize", "id": None} - ) + JSONRPCNotification.model_validate({"jsonrpc": "2.0", "method": "initialize", "id": None}) def test_message_adapter_rejects_null_id(self): """The union adapter should reject messages with id: null entirely.""" with pytest.raises(ValidationError): - jsonrpc_message_adapter.validate_python( - {"jsonrpc": "2.0", "method": "initialize", "id": None} - ) + jsonrpc_message_adapter.validate_python({"jsonrpc": "2.0", "method": "initialize", "id": None}) def test_valid_notification_without_id(self): """A proper notification (no id field) should still validate.""" - msg = jsonrpc_message_adapter.validate_python( - {"jsonrpc": "2.0", "method": "notifications/initialized"} - ) + msg = jsonrpc_message_adapter.validate_python({"jsonrpc": "2.0", "method": "notifications/initialized"}) assert isinstance(msg, JSONRPCNotification) def test_valid_request_with_int_id(self): """A proper request with an integer id should still validate.""" - msg = jsonrpc_message_adapter.validate_python( - {"jsonrpc": "2.0", "method": "initialize", "id": 1} - ) + msg = jsonrpc_message_adapter.validate_python({"jsonrpc": "2.0", "method": "initialize", "id": 1}) assert isinstance(msg, JSONRPCRequest) def test_valid_request_with_string_id(self): """A proper request with a string id should still validate.""" - msg = jsonrpc_message_adapter.validate_python( - {"jsonrpc": "2.0", "method": "initialize", "id": "abc-123"} - ) + msg = jsonrpc_message_adapter.validate_python({"jsonrpc": "2.0", "method": "initialize", "id": "abc-123"}) assert isinstance(msg, JSONRPCRequest) From f433a4b29f1aa11de5f3e9bdc96c7f999f06284e Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Sat, 28 Feb 2026 16:27:30 +0100 Subject: [PATCH 3/4] fix: remove unused JSONRPCMessage import --- tests/issues/test_2057_null_id.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/issues/test_2057_null_id.py b/tests/issues/test_2057_null_id.py index 2bfeb819e..39adb8636 100644 --- a/tests/issues/test_2057_null_id.py +++ b/tests/issues/test_2057_null_id.py @@ -3,7 +3,7 @@ import pytest from pydantic import ValidationError -from mcp.types import JSONRPCMessage, JSONRPCNotification, JSONRPCRequest, jsonrpc_message_adapter +from mcp.types import JSONRPCNotification, JSONRPCRequest, jsonrpc_message_adapter class TestNullIdRejection: From 2f74380d12ece4fbb7da055daa1b7c4708e4f51f Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Sat, 28 Feb 2026 16:42:16 +0100 Subject: [PATCH 4/4] chore: retrigger CI (previous run had cancelled job) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>