X Tutup
Skip to content
Open
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
16 changes: 15 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, Field, TypeAdapter, model_validator

RequestId = Annotated[int, Field(strict=True)] | str
"""The ID of a JSON-RPC request."""
Expand All @@ -26,6 +26,20 @@ class JSONRPCNotification(BaseModel):
method: str
params: dict[str, Any] | None = None

@model_validator(mode="before")
@classmethod
def reject_id_field(cls, data: Any) -> Any:
"""Reject messages that contain an 'id' field.

Per JSON-RPC 2.0, notifications MUST NOT have an 'id' member.
Without this check, a request with an invalid id (e.g. null)
would silently fall through union validation and be misclassified
as a notification.
"""
if isinstance(data, dict) and "id" in data:
raise ValueError("Notifications must not contain an 'id' field")
return data


# TODO(Marcelo): This is actually not correct. A JSONRPCResponse is the union of a successful response and an error.
class JSONRPCResponse(BaseModel):
Expand Down
41 changes: 41 additions & 0 deletions tests/test_types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Any

import pytest
from pydantic import ValidationError

from mcp.types import (
LATEST_PROTOCOL_VERSION,
Expand All @@ -11,6 +12,7 @@
Implementation,
InitializeRequest,
InitializeRequestParams,
JSONRPCNotification,
JSONRPCRequest,
ListToolsResult,
SamplingCapability,
Expand Down Expand Up @@ -360,3 +362,42 @@ def test_list_tools_result_preserves_json_schema_2020_12_fields():
assert tool.input_schema["$schema"] == "https://json-schema.org/draft/2020-12/schema"
assert "$defs" in tool.input_schema
assert tool.input_schema["additionalProperties"] is False


def test_jsonrpc_message_rejects_null_id():
"""Requests with 'id': null must not silently become notifications.

Per JSON-RPC 2.0, request IDs must be strings or integers. A null id
should be rejected, not reclassified as a notification (issue #2057).
"""
msg = {"jsonrpc": "2.0", "method": "initialize", "id": None}
with pytest.raises(ValidationError):
jsonrpc_message_adapter.validate_python(msg)


@pytest.mark.parametrize(
"invalid_id",
[None, 1.5, True, False, [], {}],
ids=["null", "float", "true", "false", "list", "dict"],
)
def test_jsonrpc_message_rejects_invalid_id_types(invalid_id: Any):
"""Requests with non-string/non-integer id values must be rejected."""
msg = {"jsonrpc": "2.0", "method": "test", "id": invalid_id}
with pytest.raises(ValidationError):
jsonrpc_message_adapter.validate_python(msg)


def test_jsonrpc_notification_without_id_still_works():
"""Normal notifications (no id field) must still be accepted."""
msg = {"jsonrpc": "2.0", "method": "notifications/initialized"}
parsed = jsonrpc_message_adapter.validate_python(msg)
assert isinstance(parsed, JSONRPCNotification)


def test_jsonrpc_request_with_valid_id_still_works():
"""Requests with valid string or integer ids must still be accepted."""
for valid_id in [1, 0, 42, "abc", "request-1"]:
msg = {"jsonrpc": "2.0", "method": "test", "id": valid_id}
parsed = jsonrpc_message_adapter.validate_python(msg)
assert isinstance(parsed, JSONRPCRequest)
assert parsed.id == valid_id
Loading
X Tutup