Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/mcp/types/jsonrpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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
Expand Down
40 changes: 40 additions & 0 deletions tests/issues/test_2057_null_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Test for issue #2057: Requests with "id": null silently misclassified as notifications."""

import pytest
from pydantic import ValidationError

from mcp.types import 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)