diff --git a/src/mcp/client/streamable_http.py b/src/mcp/client/streamable_http.py index 57df64705..7b35679c6 100644 --- a/src/mcp/client/streamable_http.py +++ b/src/mcp/client/streamable_http.py @@ -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"), ) diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index 802cb8680..7c480ed76 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -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, @@ -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.""" ) diff --git a/src/mcp/shared/session.py b/src/mcp/shared/session.py index b2f49fc8b..52cc4885a 100644 --- a/src/mcp/shared/session.py +++ b/src/mcp/shared/session.py @@ -253,7 +253,6 @@ async def send_request( try: jsonrpc_request = JSONRPCRequest( - jsonrpc="2.0", id=request_id, **request_data, ) @@ -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( @@ -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), ) @@ -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, @@ -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 diff --git a/src/mcp/types.py b/src/mcp/types.py index 62feda87a..974fa4c05 100644 --- a/src/mcp/types.py +++ b/src/mcp/types.py @@ -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"] @@ -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 @@ -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") @@ -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") diff --git a/tests/client/test_session.py b/tests/client/test_session.py index 53b60fce6..13307260d 100644 --- a/tests/client/test_session.py +++ b/tests/client/test_session.py @@ -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), ) @@ -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), ) @@ -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), ) @@ -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), ) @@ -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), ) @@ -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), ) @@ -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), ) diff --git a/tests/client/test_stdio.py b/tests/client/test_stdio.py index 69dad4846..20bd1625f 100644 --- a/tests/client/test_stdio.py +++ b/tests/client/test_stdio.py @@ -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: @@ -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 diff --git a/tests/issues/test_192_request_id.py b/tests/issues/test_192_request_id.py index 3762b092b..958c56fdb 100644 --- a/tests/issues/test_192_request_id.py +++ b/tests/issues/test_192_request_id.py @@ -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))) @@ -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))) diff --git a/tests/issues/test_malformed_input.py b/tests/issues/test_malformed_input.py index 065bc7841..5a4f13353 100644 --- a/tests/issues/test_malformed_input.py +++ b/tests/issues/test_malformed_input.py @@ -11,6 +11,7 @@ from mcp.shared.message import SessionMessage from mcp.types import ( INVALID_PARAMS, + JSON_RPC_VERSION, JSONRPCError, JSONRPCMessage, JSONRPCRequest, @@ -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 @@ -63,7 +63,7 @@ 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 @@ -71,7 +71,6 @@ async def test_malformed_initialize_request_does_not_crash_server(): # 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 @@ -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 diff --git a/tests/server/test_lifespan.py b/tests/server/test_lifespan.py index 9d73fd47a..e47ed8592 100644 --- a/tests/server/test_lifespan.py +++ b/tests/server/test_lifespan.py @@ -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), @@ -100,7 +99,6 @@ async def run_server(): SessionMessage( JSONRPCMessage( root=JSONRPCNotification( - jsonrpc="2.0", method="notifications/initialized", ) ) @@ -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": {}}, @@ -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), @@ -206,7 +202,6 @@ async def run_server(): SessionMessage( JSONRPCMessage( root=JSONRPCNotification( - jsonrpc="2.0", method="notifications/initialized", ) ) @@ -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": {}}, diff --git a/tests/server/test_session.py b/tests/server/test_session.py index 89e807b29..33d4261da 100644 --- a/tests/server/test_session.py +++ b/tests/server/test_session.py @@ -166,7 +166,6 @@ async def mock_client(): SessionMessage( types.JSONRPCMessage( types.JSONRPCRequest( - jsonrpc="2.0", id=1, method="initialize", params=types.InitializeRequestParams( @@ -194,7 +193,6 @@ async def mock_client(): SessionMessage( types.JSONRPCMessage( types.JSONRPCNotification( - jsonrpc="2.0", method="notifications/initialized", ) ) diff --git a/tests/server/test_stdio.py b/tests/server/test_stdio.py index a1d1792f8..eddfb1ebf 100644 --- a/tests/server/test_stdio.py +++ b/tests/server/test_stdio.py @@ -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: @@ -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: @@ -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={})) diff --git a/tests/shared/test_sse.py b/tests/shared/test_sse.py index 7b0d89cb4..219d3dff5 100644 --- a/tests/shared/test_sse.py +++ b/tests/shared/test_sse.py @@ -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( diff --git a/tests/test_types.py b/tests/test_types.py index 415eba66a..53e4f9053 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,6 +1,7 @@ import pytest from mcp.types import ( + JSON_RPC_VERSION, LATEST_PROTOCOL_VERSION, ClientCapabilities, ClientRequest, @@ -15,7 +16,6 @@ @pytest.mark.anyio async def test_jsonrpc_request(): json_data = { - "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { @@ -29,7 +29,7 @@ async def test_jsonrpc_request(): assert isinstance(request.root, JSONRPCRequest) ClientRequest.model_validate(request.model_dump(by_alias=True, exclude_none=True)) - assert request.root.jsonrpc == "2.0" + assert request.root.jsonrpc == JSON_RPC_VERSION, f"jsonrpc should be set to '{JSON_RPC_VERSION}'" assert request.root.id == 1 assert request.root.method == "initialize" assert request.root.params is not None