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
1 change: 0 additions & 1 deletion src/mcp/client/streamable_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,6 @@ async def _send_session_terminated_error(
) -> None:
"""Send a session terminated error response."""
jsonrpc_error = JSONRPCError(
jsonrpc="2.0",
id=request_id,
error=ErrorData(code=32600, message="Session terminated"),
)
Expand Down
3 changes: 1 addition & 2 deletions src/mcp/server/streamable_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,6 @@ def _create_error_response(

# Return a properly formatted JSON error response
error_response = JSONRPCError(
jsonrpc="2.0",
id="server-error", # We don't have a request ID for general errors
error=ErrorData(
code=error_code,
Expand Down Expand Up @@ -872,7 +871,7 @@ async def message_router():
self._request_streams.pop(request_stream_id, None)
else:
logging.debug(
f"""Request stream {request_stream_id} not found
f"""Request stream {request_stream_id} not found
for message. Still processing message as the client
might reconnect and replay."""
)
Expand Down
8 changes: 2 additions & 6 deletions src/mcp/shared/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,6 @@ async def send_request(

try:
jsonrpc_request = JSONRPCRequest(
jsonrpc="2.0",
id=request_id,
**request_data,
)
Expand Down Expand Up @@ -305,7 +304,6 @@ async def send_notification(
# Some transport implementations may need to set the related_request_id
# to attribute to the notifications to the request that triggered them.
jsonrpc_notification = JSONRPCNotification(
jsonrpc="2.0",
**notification.model_dump(by_alias=True, mode="json", exclude_none=True),
)
session_message = SessionMessage(
Expand All @@ -316,12 +314,11 @@ async def send_notification(

async def _send_response(self, request_id: RequestId, response: SendResultT | ErrorData) -> None:
if isinstance(response, ErrorData):
jsonrpc_error = JSONRPCError(jsonrpc="2.0", id=request_id, error=response)
jsonrpc_error = JSONRPCError(id=request_id, error=response)
session_message = SessionMessage(message=JSONRPCMessage(jsonrpc_error))
await self._write_stream.send(session_message)
else:
jsonrpc_response = JSONRPCResponse(
jsonrpc="2.0",
id=request_id,
result=response.model_dump(by_alias=True, mode="json", exclude_none=True),
)
Expand Down Expand Up @@ -363,7 +360,6 @@ async def _receive_loop(self) -> None:
logging.warning(f"Failed to validate request: {e}")
logging.debug(f"Message that failed validation: {message.message.root}")
error_response = JSONRPCError(
jsonrpc="2.0",
id=message.message.root.id,
error=ErrorData(
code=INVALID_PARAMS,
Expand Down Expand Up @@ -428,7 +424,7 @@ async def _receive_loop(self) -> None:
for id, stream in self._response_streams.items():
error = ErrorData(code=CONNECTION_CLOSED, message="Connection closed")
try:
await stream.send(JSONRPCError(jsonrpc="2.0", id=id, error=error))
await stream.send(JSONRPCError(id=id, error=error))
await stream.aclose()
except Exception:
# Stream might already be closed
Expand Down
14 changes: 10 additions & 4 deletions src/mcp/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
"""
DEFAULT_NEGOTIATED_VERSION = "2025-03-26"

"""
The JSON-RPC version (fixed at "2.0") to use for MCP messages.
"""
JSONRPCVersionType: TypeAlias = Literal["2.0"]
JSON_RPC_VERSION: JSONRPCVersionType = "2.0"

ProgressToken = str | int
Cursor = str
Role = Literal["user", "assistant"]
Expand Down Expand Up @@ -124,7 +130,7 @@ class PaginatedResult(Result):
class JSONRPCRequest(Request[dict[str, Any] | None, str]):
"""A request that expects a response."""

jsonrpc: Literal["2.0"]
jsonrpc: JSONRPCVersionType = JSON_RPC_VERSION
id: RequestId
method: str
params: dict[str, Any] | None = None
Expand All @@ -133,14 +139,14 @@ class JSONRPCRequest(Request[dict[str, Any] | None, str]):
class JSONRPCNotification(Notification[dict[str, Any] | None, str]):
"""A notification which does not expect a response."""

jsonrpc: Literal["2.0"]
jsonrpc: JSONRPCVersionType = JSON_RPC_VERSION
params: dict[str, Any] | None = None


class JSONRPCResponse(BaseModel):
"""A successful (non-error) response to a request."""

jsonrpc: Literal["2.0"]
jsonrpc: JSONRPCVersionType = JSON_RPC_VERSION
id: RequestId
result: dict[str, Any]
model_config = ConfigDict(extra="allow")
Expand Down Expand Up @@ -182,7 +188,7 @@ class ErrorData(BaseModel):
class JSONRPCError(BaseModel):
"""A response to a request that indicates an error occurred."""

jsonrpc: Literal["2.0"]
jsonrpc: JSONRPCVersionType = JSON_RPC_VERSION
id: str | int
error: ErrorData
model_config = ConfigDict(extra="allow")
Expand Down
7 changes: 0 additions & 7 deletions tests/client/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ async def mock_server():
SessionMessage(
JSONRPCMessage(
JSONRPCResponse(
jsonrpc="2.0",
id=jsonrpc_request.root.id,
result=result.model_dump(by_alias=True, mode="json", exclude_none=True),
)
Expand Down Expand Up @@ -146,7 +145,6 @@ async def mock_server():
SessionMessage(
JSONRPCMessage(
JSONRPCResponse(
jsonrpc="2.0",
id=jsonrpc_request.root.id,
result=result.model_dump(by_alias=True, mode="json", exclude_none=True),
)
Expand Down Expand Up @@ -207,7 +205,6 @@ async def mock_server():
SessionMessage(
JSONRPCMessage(
JSONRPCResponse(
jsonrpc="2.0",
id=jsonrpc_request.root.id,
result=result.model_dump(by_alias=True, mode="json", exclude_none=True),
)
Expand Down Expand Up @@ -268,7 +265,6 @@ async def mock_server():
SessionMessage(
JSONRPCMessage(
JSONRPCResponse(
jsonrpc="2.0",
id=jsonrpc_request.root.id,
result=result.model_dump(by_alias=True, mode="json", exclude_none=True),
)
Expand Down Expand Up @@ -327,7 +323,6 @@ async def mock_server():
SessionMessage(
JSONRPCMessage(
JSONRPCResponse(
jsonrpc="2.0",
id=jsonrpc_request.root.id,
result=result.model_dump(by_alias=True, mode="json", exclude_none=True),
)
Expand Down Expand Up @@ -386,7 +381,6 @@ async def mock_server():
SessionMessage(
JSONRPCMessage(
JSONRPCResponse(
jsonrpc="2.0",
id=jsonrpc_request.root.id,
result=result.model_dump(by_alias=True, mode="json", exclude_none=True),
)
Expand Down Expand Up @@ -464,7 +458,6 @@ async def mock_server():
SessionMessage(
JSONRPCMessage(
JSONRPCResponse(
jsonrpc="2.0",
id=jsonrpc_request.root.id,
result=result.model_dump(by_alias=True, mode="json", exclude_none=True),
)
Expand Down
8 changes: 4 additions & 4 deletions tests/client/test_stdio.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ async def test_stdio_client():
async with stdio_client(server_parameters) as (read_stream, write_stream):
# Test sending and receiving messages
messages = [
JSONRPCMessage(root=JSONRPCRequest(jsonrpc="2.0", id=1, method="ping")),
JSONRPCMessage(root=JSONRPCResponse(jsonrpc="2.0", id=2, result={})),
JSONRPCMessage(root=JSONRPCRequest(id=1, method="ping")),
JSONRPCMessage(root=JSONRPCResponse(id=2, result={})),
]

async with write_stream:
Expand All @@ -61,8 +61,8 @@ async def test_stdio_client():
break

assert len(read_messages) == 2
assert read_messages[0] == JSONRPCMessage(root=JSONRPCRequest(jsonrpc="2.0", id=1, method="ping"))
assert read_messages[1] == JSONRPCMessage(root=JSONRPCResponse(jsonrpc="2.0", id=2, result={}))
assert read_messages[0] == JSONRPCMessage(root=JSONRPCRequest(id=1, method="ping"))
assert read_messages[1] == JSONRPCMessage(root=JSONRPCResponse(id=2, result={}))


@pytest.mark.anyio
Expand Down
4 changes: 1 addition & 3 deletions tests/issues/test_192_request_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ async def run_server():
capabilities=ClientCapabilities(),
clientInfo=Implementation(name="test-client", version="1.0.0"),
).model_dump(by_alias=True, exclude_none=True),
jsonrpc="2.0",
)

await client_writer.send(SessionMessage(JSONRPCMessage(root=init_req)))
Expand All @@ -73,12 +72,11 @@ async def run_server():
initialized_notification = JSONRPCNotification(
method="notifications/initialized",
params=NotificationParams().model_dump(by_alias=True, exclude_none=True),
jsonrpc="2.0",
)
await client_writer.send(SessionMessage(JSONRPCMessage(root=initialized_notification)))

# Send ping request with custom ID
ping_request = JSONRPCRequest(id=custom_request_id, method="ping", params={}, jsonrpc="2.0")
ping_request = JSONRPCRequest(id=custom_request_id, method="ping", params={})

await client_writer.send(SessionMessage(JSONRPCMessage(root=ping_request)))

Expand Down
6 changes: 2 additions & 4 deletions tests/issues/test_malformed_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from mcp.shared.message import SessionMessage
from mcp.types import (
INVALID_PARAMS,
JSON_RPC_VERSION,
JSONRPCError,
JSONRPCMessage,
JSONRPCRequest,
Expand All @@ -31,7 +32,6 @@ async def test_malformed_initialize_request_does_not_crash_server():
try:
# Create a malformed initialize request (missing required params field)
malformed_request = JSONRPCRequest(
jsonrpc="2.0",
id="f20fe86132ed4cd197f89a7134de5685",
method="initialize",
# params=None # Missing required params field
Expand Down Expand Up @@ -63,15 +63,14 @@ async def test_malformed_initialize_request_does_not_crash_server():

# Verify it's a proper JSON-RPC error response
assert isinstance(response, JSONRPCError)
assert response.jsonrpc == "2.0"
assert response.jsonrpc == JSON_RPC_VERSION, f"jsonrpc should be set to '{JSON_RPC_VERSION}'"
assert response.id == "f20fe86132ed4cd197f89a7134de5685"
assert response.error.code == INVALID_PARAMS
assert "Invalid request parameters" in response.error.message

# Verify the session is still alive and can handle more requests
# Send another malformed request to confirm server stability
another_malformed_request = JSONRPCRequest(
jsonrpc="2.0",
id="test_id_2",
method="tools/call",
# params=None # Missing required params
Expand Down Expand Up @@ -123,7 +122,6 @@ async def test_multiple_concurrent_malformed_requests():
malformed_requests: list[SessionMessage] = []
for i in range(10):
malformed_request = JSONRPCRequest(
jsonrpc="2.0",
id=f"malformed_{i}",
method="initialize",
# params=None # Missing required params
Expand Down
6 changes: 0 additions & 6 deletions tests/server/test_lifespan.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ async def run_server():
SessionMessage(
JSONRPCMessage(
root=JSONRPCRequest(
jsonrpc="2.0",
id=1,
method="initialize",
params=TypeAdapter(InitializeRequestParams).dump_python(params),
Expand All @@ -100,7 +99,6 @@ async def run_server():
SessionMessage(
JSONRPCMessage(
root=JSONRPCNotification(
jsonrpc="2.0",
method="notifications/initialized",
)
)
Expand All @@ -112,7 +110,6 @@ async def run_server():
SessionMessage(
JSONRPCMessage(
root=JSONRPCRequest(
jsonrpc="2.0",
id=2,
method="tools/call",
params={"name": "check_lifespan", "arguments": {}},
Expand Down Expand Up @@ -190,7 +187,6 @@ async def run_server():
SessionMessage(
JSONRPCMessage(
root=JSONRPCRequest(
jsonrpc="2.0",
id=1,
method="initialize",
params=TypeAdapter(InitializeRequestParams).dump_python(params),
Expand All @@ -206,7 +202,6 @@ async def run_server():
SessionMessage(
JSONRPCMessage(
root=JSONRPCNotification(
jsonrpc="2.0",
method="notifications/initialized",
)
)
Expand All @@ -218,7 +213,6 @@ async def run_server():
SessionMessage(
JSONRPCMessage(
root=JSONRPCRequest(
jsonrpc="2.0",
id=2,
method="tools/call",
params={"name": "check_lifespan", "arguments": {}},
Expand Down
2 changes: 0 additions & 2 deletions tests/server/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@ async def mock_client():
SessionMessage(
types.JSONRPCMessage(
types.JSONRPCRequest(
jsonrpc="2.0",
id=1,
method="initialize",
params=types.InitializeRequestParams(
Expand Down Expand Up @@ -194,7 +193,6 @@ async def mock_client():
SessionMessage(
types.JSONRPCMessage(
types.JSONRPCNotification(
jsonrpc="2.0",
method="notifications/initialized",
)
)
Expand Down
16 changes: 8 additions & 8 deletions tests/server/test_stdio.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ async def test_stdio_server():
stdout = io.StringIO()

messages = [
JSONRPCMessage(root=JSONRPCRequest(jsonrpc="2.0", id=1, method="ping")),
JSONRPCMessage(root=JSONRPCResponse(jsonrpc="2.0", id=2, result={})),
JSONRPCMessage(root=JSONRPCRequest(id=1, method="ping")),
JSONRPCMessage(root=JSONRPCResponse(id=2, result={})),
]

for message in messages:
Expand All @@ -37,13 +37,13 @@ async def test_stdio_server():

# Verify received messages
assert len(received_messages) == 2
assert received_messages[0] == JSONRPCMessage(root=JSONRPCRequest(jsonrpc="2.0", id=1, method="ping"))
assert received_messages[1] == JSONRPCMessage(root=JSONRPCResponse(jsonrpc="2.0", id=2, result={}))
assert received_messages[0] == JSONRPCMessage(root=JSONRPCRequest(id=1, method="ping"))
assert received_messages[1] == JSONRPCMessage(root=JSONRPCResponse(id=2, result={}))

# Test sending responses from the server
responses = [
JSONRPCMessage(root=JSONRPCRequest(jsonrpc="2.0", id=3, method="ping")),
JSONRPCMessage(root=JSONRPCResponse(jsonrpc="2.0", id=4, result={})),
JSONRPCMessage(root=JSONRPCRequest(id=3, method="ping")),
JSONRPCMessage(root=JSONRPCResponse(id=4, result={})),
]

async with write_stream:
Expand All @@ -57,5 +57,5 @@ async def test_stdio_server():

received_responses = [JSONRPCMessage.model_validate_json(line.strip()) for line in output_lines]
assert len(received_responses) == 2
assert received_responses[0] == JSONRPCMessage(root=JSONRPCRequest(jsonrpc="2.0", id=3, method="ping"))
assert received_responses[1] == JSONRPCMessage(root=JSONRPCResponse(jsonrpc="2.0", id=4, result={}))
assert received_responses[0] == JSONRPCMessage(root=JSONRPCRequest(id=3, method="ping"))
assert received_responses[1] == JSONRPCMessage(root=JSONRPCResponse(id=4, result={}))
4 changes: 2 additions & 2 deletions tests/shared/test_sse.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,11 +478,11 @@ def test_sse_message_id_coercion():
"""
json_message = '{"jsonrpc": "2.0", "id": "123", "method": "ping", "params": null}'
msg = types.JSONRPCMessage.model_validate_json(json_message)
assert msg == snapshot(types.JSONRPCMessage(root=types.JSONRPCRequest(method="ping", jsonrpc="2.0", id="123")))
assert msg == snapshot(types.JSONRPCMessage(root=types.JSONRPCRequest(method="ping", id="123")))

json_message = '{"jsonrpc": "2.0", "id": 123, "method": "ping", "params": null}'
msg = types.JSONRPCMessage.model_validate_json(json_message)
assert msg == snapshot(types.JSONRPCMessage(root=types.JSONRPCRequest(method="ping", jsonrpc="2.0", id=123)))
assert msg == snapshot(types.JSONRPCMessage(root=types.JSONRPCRequest(method="ping", id=123)))


@pytest.mark.parametrize(
Expand Down
Loading
Loading