using ModelContextProtocol.Protocol; using System.Text.Json; using System.Text.Json.Nodes; namespace ModelContextProtocol.Tests.Protocol; /// /// Tests for the optimized JsonRpcMessage.Converter implementation. /// public static class JsonRpcMessageConverterTests { [Fact] public static void Deserialize_JsonRpcRequest_WithAllProperties() { // Arrange string json = """{"jsonrpc":"2.0","id":123,"method":"test/method","params":{"key":"value"}}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var request = (JsonRpcRequest)message; Assert.Equal("2.0", request.JsonRpc); Assert.Equal(new RequestId(123), request.Id); Assert.Equal("test/method", request.Method); Assert.NotNull(request.Params); Assert.Equal("value", request.Params["key"]?.GetValue()); } [Fact] public static void Deserialize_JsonRpcRequest_WithStringId() { // Arrange string json = """{"jsonrpc":"2.0","id":"abc-123","method":"test/method"}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var request = (JsonRpcRequest)message; Assert.Equal("2.0", request.JsonRpc); Assert.Equal(new RequestId("abc-123"), request.Id); Assert.Equal("test/method", request.Method); } [Fact] public static void Deserialize_JsonRpcNotification_WithParams() { // Arrange string json = """{"jsonrpc":"2.0","method":"notifications/progress","params":{"progress":50}}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var notification = (JsonRpcNotification)message; Assert.Equal("2.0", notification.JsonRpc); Assert.Equal("notifications/progress", notification.Method); Assert.NotNull(notification.Params); Assert.Equal(50, notification.Params["progress"]?.GetValue()); } [Fact] public static void Deserialize_JsonRpcResponse_WithResult() { // Arrange string json = """{"jsonrpc":"2.0","id":42,"result":{"status":"success","data":[1,2,3]}}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var response = (JsonRpcResponse)message; Assert.Equal("2.0", response.JsonRpc); Assert.Equal(new RequestId(42), response.Id); Assert.NotNull(response.Result); Assert.Equal("success", response.Result["status"]?.GetValue()); } [Fact] public static void Deserialize_JsonRpcResponse_WithNullResult() { // Arrange string json = """{"jsonrpc":"2.0","id":1,"result":null}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var response = (JsonRpcResponse)message; Assert.Equal("2.0", response.JsonRpc); Assert.Equal(new RequestId(1), response.Id); Assert.Null(response.Result); } [Fact] public static void Deserialize_JsonRpcError_WithErrorDetails() { // Arrange string json = """{"jsonrpc":"2.0","id":"req-1","error":{"code":-32600,"message":"Invalid Request","data":"Additional error info"}}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var error = (JsonRpcError)message; Assert.Equal("2.0", error.JsonRpc); Assert.Equal(new RequestId("req-1"), error.Id); Assert.NotNull(error.Error); Assert.Equal(-32600, error.Error.Code); Assert.Equal("Invalid Request", error.Error.Message); Assert.Equal("Additional error info", error.Error.Data?.ToString()); } [Fact] public static void Deserialize_JsonRpcMessage_IgnoresUnknownProperties() { // Arrange - JSON with unknown properties string json = """{"jsonrpc":"2.0","id":1,"method":"test","params":{},"extra":"ignored","another":123}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert - should successfully deserialize, ignoring unknown properties Assert.NotNull(message); Assert.IsType(message); var request = (JsonRpcRequest)message; Assert.Equal("test", request.Method); } [Fact] public static void Deserialize_InvalidJsonRpcVersion_ThrowsException() { // Arrange string json = """{"jsonrpc":"1.0","id":1,"method":"test"}"""; // Act & Assert var exception = Assert.Throws(() => JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions)); Assert.Contains("jsonrpc version", exception.Message); } [Fact] public static void Deserialize_MissingJsonRpcVersion_ThrowsException() { // Arrange string json = """{"id":1,"method":"test"}"""; // Act & Assert var exception = Assert.Throws(() => JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions)); Assert.Contains("jsonrpc version", exception.Message); } [Fact] public static void Deserialize_ResponseWithoutResultOrError_ThrowsException() { // Arrange string json = """{"jsonrpc":"2.0","id":1}"""; // Act & Assert var exception = Assert.Throws(() => JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions)); Assert.Contains("result or error", exception.Message); } [Fact] public static void Deserialize_InvalidMessageFormat_ThrowsException() { // Arrange - neither request nor response nor notification string json = """{"jsonrpc":"2.0"}"""; // Act & Assert var exception = Assert.Throws(() => JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions)); Assert.Contains("Invalid JSON-RPC message format", exception.Message); } [Fact] public static void Serialize_JsonRpcRequest_ProducesCorrectJson() { // Arrange var request = new JsonRpcRequest { JsonRpc = "2.0", Id = new RequestId(123), Method = "test/method", Params = JsonNode.Parse("""{"key":"value"}""") }; // Act string json = JsonSerializer.Serialize(request, McpJsonUtilities.DefaultOptions); // Assert Assert.Contains("\"jsonrpc\":\"2.0\"", json); Assert.Contains("\"id\":123", json); Assert.Contains("\"method\":\"test/method\"", json); Assert.Contains("\"key\":\"value\"", json); } [Fact] public static void Serialize_JsonRpcNotification_ProducesCorrectJson() { // Arrange var notification = new JsonRpcNotification { JsonRpc = "2.0", Method = "notifications/test" }; // Act string json = JsonSerializer.Serialize(notification, McpJsonUtilities.DefaultOptions); // Assert Assert.Contains("\"jsonrpc\":\"2.0\"", json); Assert.Contains("\"method\":\"notifications/test\"", json); Assert.DoesNotContain("\"id\"", json); } [Fact] public static void RoundTrip_Request_PreservesData() { // Arrange var original = new JsonRpcRequest { JsonRpc = "2.0", Id = new RequestId("test-id"), Method = "some/method", Params = JsonNode.Parse("""{"nested":{"value":42}}""") }; // Act string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions); var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions) as JsonRpcRequest; // Assert Assert.NotNull(deserialized); Assert.Equal(original.JsonRpc, deserialized.JsonRpc); Assert.Equal(original.Id, deserialized.Id); Assert.Equal(original.Method, deserialized.Method); Assert.Equal(42, deserialized.Params?["nested"]?["value"]?.GetValue()); } [Fact] public static void RoundTrip_Response_PreservesData() { // Arrange var original = new JsonRpcResponse { JsonRpc = "2.0", Id = new RequestId(999), Result = JsonNode.Parse("""{"success":true,"items":[1,2,3]}""") }; // Act string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions); var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions) as JsonRpcResponse; // Assert Assert.NotNull(deserialized); Assert.Equal(original.JsonRpc, deserialized.JsonRpc); Assert.Equal(original.Id, deserialized.Id); Assert.True(deserialized.Result?["success"]?.GetValue()); } [Fact] public static void RoundTrip_Error_PreservesData() { // Arrange var original = new JsonRpcError { JsonRpc = "2.0", Id = new RequestId(100), Error = new JsonRpcErrorDetail { Code = -32601, Message = "Method not found", Data = "test/unknown" } }; // Act string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions); var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions) as JsonRpcError; // Assert Assert.NotNull(deserialized); Assert.Equal(original.JsonRpc, deserialized.JsonRpc); Assert.Equal(original.Id, deserialized.Id); Assert.Equal(original.Error.Code, deserialized.Error.Code); Assert.Equal(original.Error.Message, deserialized.Error.Message); } [Fact] public static void Deserialize_ResponseWithExplicitNullError_TreatedAsSuccessResponse() { // Arrange - Some implementations may include "error": null in success responses. // While JSON-RPC 2.0 spec says responses have either result OR error (not both), // this tests that we handle the lenient case gracefully. string json = """{"jsonrpc":"2.0","id":1,"result":{"data":"value"},"error":null}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert - Should be a success response since error is null Assert.NotNull(message); Assert.IsType(message); var response = (JsonRpcResponse)message; Assert.Equal(new RequestId(1), response.Id); Assert.NotNull(response.Result); Assert.Equal("value", response.Result["data"]?.GetValue()); } [Fact] public static void Deserialize_ResponseWithNullResultAndNullError_TreatedAsSuccessWithNullResult() { // Arrange - Both result and error are explicitly null. // Per JSON-RPC 2.0, result: null is a valid success response value. string json = """{"jsonrpc":"2.0","id":1,"result":null,"error":null}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert - result: null is valid, error: null is ignored Assert.NotNull(message); Assert.IsType(message); var response = (JsonRpcResponse)message; Assert.Equal(new RequestId(1), response.Id); Assert.Null(response.Result); } [Fact] public static void Deserialize_ResponseWithBothErrorAndResult_ErrorTakesPrecedence() { // Arrange - JSON-RPC 2.0 spec says a response should have either result OR error, not both. // However, if a non-compliant implementation sends both, we verify consistent behavior: // error takes precedence regardless of property order. string json = """{"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid"},"result":{"data":"ignored"}}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert - Error takes precedence Assert.NotNull(message); Assert.IsType(message); var error = (JsonRpcError)message; Assert.Equal(new RequestId(1), error.Id); Assert.Equal(-32600, error.Error.Code); } [Fact] public static void Deserialize_ResponseWithBothResultAndError_ErrorTakesPrecedenceRegardlessOfOrder() { // Arrange - Same as above but with result appearing before error in the JSON. // Validates that property order doesn't affect the precedence logic. string json = """{"jsonrpc":"2.0","id":1,"result":{"data":"ignored"},"error":{"code":-32600,"message":"Invalid"}}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert - Error still takes precedence Assert.NotNull(message); Assert.IsType(message); var error = (JsonRpcError)message; Assert.Equal(new RequestId(1), error.Id); Assert.Equal(-32600, error.Error.Code); } [Fact] public static void Deserialize_RequestWithEmptyStringId_IsValidRequest() { // Arrange - Empty string is a valid ID per JSON-RPC 2.0 string json = """{"jsonrpc":"2.0","id":"","method":"test"}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var request = (JsonRpcRequest)message; Assert.Equal(new RequestId(""), request.Id); Assert.Equal("test", request.Method); } [Fact] public static void Deserialize_RequestWithZeroId_IsValidRequest() { // Arrange - Zero is a valid numeric ID string json = """{"jsonrpc":"2.0","id":0,"method":"test"}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var request = (JsonRpcRequest)message; Assert.Equal(new RequestId(0), request.Id); } [Fact] public static void Deserialize_RequestWithNegativeId_IsValidRequest() { // Arrange - Negative numbers are valid IDs string json = """{"jsonrpc":"2.0","id":-42,"method":"test"}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var request = (JsonRpcRequest)message; Assert.Equal(new RequestId(-42), request.Id); } [Fact] public static void Deserialize_RequestWithLargeNumericId_IsValidRequest() { // Arrange - Large number ID string json = """{"jsonrpc":"2.0","id":9223372036854775807,"method":"test"}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var request = (JsonRpcRequest)message; Assert.Equal(new RequestId(long.MaxValue), request.Id); } [Fact] public static void Deserialize_NotificationWithExplicitNullParams_IsValidNotification() { // Arrange - params: null is valid string json = """{"jsonrpc":"2.0","method":"notify","params":null}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var notification = (JsonRpcNotification)message; Assert.Equal("notify", notification.Method); Assert.Null(notification.Params); } [Fact] public static void Deserialize_RequestWithEmptyObjectParams_IsValidRequest() { // Arrange - Empty object params string json = """{"jsonrpc":"2.0","id":1,"method":"test","params":{}}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var request = (JsonRpcRequest)message; Assert.NotNull(request.Params); Assert.IsType(request.Params); } [Fact] public static void Deserialize_RequestWithArrayParams_IsValidRequest() { // Arrange - Array params (positional arguments per JSON-RPC 2.0) string json = """{"jsonrpc":"2.0","id":1,"method":"test","params":["arg1",42,true]}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var request = (JsonRpcRequest)message; Assert.NotNull(request.Params); Assert.IsType(request.Params); var array = (JsonArray)request.Params; Assert.Equal(3, array.Count); Assert.Equal("arg1", array[0]?.GetValue()); Assert.Equal(42, array[1]?.GetValue()); Assert.True(array[2]?.GetValue()); } [Fact] public static void Deserialize_ErrorWithNullData_IsValidError() { // Arrange - Error with explicit null data string json = """{"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid","data":null}}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var error = (JsonRpcError)message; Assert.Equal(-32600, error.Error.Code); Assert.Equal("Invalid", error.Error.Message); Assert.Null(error.Error.Data); } [Fact] public static void Deserialize_ErrorWithComplexData_IsValidError() { // Arrange - Error with complex object data string json = """{"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid","data":{"details":["error1","error2"],"field":"name"}}}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var error = (JsonRpcError)message; Assert.NotNull(error.Error.Data); } [Fact] public static void Deserialize_RequestWithPropertiesInUnusualOrder_IsValidRequest() { // Arrange - Properties in unusual order (params, method, id, jsonrpc) string json = """{"params":{"key":"value"},"method":"test","id":123,"jsonrpc":"2.0"}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var request = (JsonRpcRequest)message; Assert.Equal("2.0", request.JsonRpc); Assert.Equal(new RequestId(123), request.Id); Assert.Equal("test", request.Method); Assert.Equal("value", request.Params?["key"]?.GetValue()); } [Fact] public static void Deserialize_ResponseWithPropertiesInUnusualOrder_IsValidResponse() { // Arrange - Properties in unusual order (result, id, jsonrpc) string json = """{"result":{"status":"ok"},"id":"abc","jsonrpc":"2.0"}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var response = (JsonRpcResponse)message; Assert.Equal("2.0", response.JsonRpc); Assert.Equal(new RequestId("abc"), response.Id); Assert.Equal("ok", response.Result?["status"]?.GetValue()); } [Fact] public static void Deserialize_MessageWithUnicodeInStringValues_PreservesUnicode() { // Arrange - Unicode characters in method name, ID, and params string json = """{"jsonrpc":"2.0","id":"请求-123","method":"日本語/メソッド","params":{"emoji":"🚀","text":"Ελληνικά"}}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var request = (JsonRpcRequest)message; Assert.Equal(new RequestId("请求-123"), request.Id); Assert.Equal("日本語/メソッド", request.Method); Assert.Equal("🚀", request.Params?["emoji"]?.GetValue()); Assert.Equal("Ελληνικά", request.Params?["text"]?.GetValue()); } [Fact] public static void Deserialize_MessageWithEscapedCharacters_HandlesEscaping() { // Arrange - JSON with escaped characters string json = """{"jsonrpc":"2.0","id":1,"method":"test","params":{"path":"C:\\Users\\test","quote":"He said \"hello\""}}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var request = (JsonRpcRequest)message; Assert.Equal("C:\\Users\\test", request.Params?["path"]?.GetValue()); Assert.Equal("He said \"hello\"", request.Params?["quote"]?.GetValue()); } [Fact] public static void Deserialize_ResponseWithPrimitiveResult_IsValid() { // Arrange - Result is a primitive string, not an object string json = """{"jsonrpc":"2.0","id":1,"result":"simple string result"}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var response = (JsonRpcResponse)message; Assert.NotNull(response.Result); Assert.Equal("simple string result", response.Result.GetValue()); } [Fact] public static void Deserialize_ResponseWithNumericResult_IsValid() { // Arrange - Result is a number string json = """{"jsonrpc":"2.0","id":1,"result":42}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var response = (JsonRpcResponse)message; Assert.NotNull(response.Result); Assert.Equal(42, response.Result.GetValue()); } [Fact] public static void Deserialize_ResponseWithBooleanResult_IsValid() { // Arrange - Result is a boolean string json = """{"jsonrpc":"2.0","id":1,"result":true}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var response = (JsonRpcResponse)message; Assert.NotNull(response.Result); Assert.True(response.Result.GetValue()); } [Fact] public static void Deserialize_ResponseWithArrayResult_IsValid() { // Arrange - Result is an array string json = """{"jsonrpc":"2.0","id":1,"result":[1,2,3,"four"]}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var response = (JsonRpcResponse)message; Assert.NotNull(response.Result); Assert.IsType(response.Result); var array = (JsonArray)response.Result; Assert.Equal(4, array.Count); } [Fact] public static void Deserialize_MessageWithMultipleUnknownPropertiesInterspersed_IgnoresUnknown() { // Arrange - Unknown properties interspersed with known ones string json = """{"unknown1":"x","jsonrpc":"2.0","unknown2":123,"id":1,"unknown3":true,"method":"test","unknown4":null}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var request = (JsonRpcRequest)message; Assert.Equal("2.0", request.JsonRpc); Assert.Equal(new RequestId(1), request.Id); Assert.Equal("test", request.Method); } [Fact] public static void Deserialize_NotificationWithMethodOnly_NoParams_IsValid() { // Arrange - Minimal notification with no params string json = """{"jsonrpc":"2.0","method":"ping"}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var notification = (JsonRpcNotification)message; Assert.Equal("ping", notification.Method); Assert.Null(notification.Params); } [Fact] public static void Deserialize_RequestWithNestedComplexParams_IsValid() { // Arrange - Deeply nested params structure string json = """{"jsonrpc":"2.0","id":1,"method":"test","params":{"level1":{"level2":{"level3":{"value":"deep"}}}}}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var request = (JsonRpcRequest)message; Assert.NotNull(request.Params); var deepValue = request.Params["level1"]?["level2"]?["level3"]?["value"]?.GetValue(); Assert.Equal("deep", deepValue); } [Fact] public static void Deserialize_ErrorWithNumericData_IsValid() { // Arrange - Error with numeric data (not object or string) string json = """{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"Error","data":42}}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var error = (JsonRpcError)message; Assert.NotNull(error.Error.Data); } [Fact] public static void Deserialize_ErrorWithArrayData_IsValid() { // Arrange - Error with array data string json = """{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"Multiple errors","data":["error1","error2","error3"]}}"""; // Act var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); // Assert Assert.NotNull(message); Assert.IsType(message); var error = (JsonRpcError)message; Assert.NotNull(error.Error.Data); } [Fact] public static void Deserialize_ErrorWithNullId_IsValid() { // Per JSON-RPC 2.0 §5.1, when an error occurs before the request id can be determined // (parse error or invalid request), the server MUST respond with id=null. This shape is // produced by some peers (e.g. Python's simple-streamablehttp-stateless on a 2026-07-28 probe) // and must be accepted so the HTTP-fallback path can recognize the structured signal. string json = """{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Bad Request"}}"""; var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); Assert.NotNull(message); var error = Assert.IsType(message); Assert.Equal(default(RequestId), error.Id); Assert.Equal(-32600, error.Error.Code); Assert.Equal("Bad Request", error.Error.Message); } [Fact] public static void Deserialize_ErrorWithMissingId_IsValid() { // Some peers omit `id` entirely on pre-routing errors; treat as null per JSON-RPC 2.0 §5.1. string json = """{"jsonrpc":"2.0","error":{"code":-32700,"message":"Parse error"}}"""; var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); Assert.NotNull(message); var error = Assert.IsType(message); Assert.Equal(default(RequestId), error.Id); Assert.Equal(-32700, error.Error.Code); } [Fact] public static void Deserialize_RequestWithExplicitNullId_Throws() { // A message carrying a `method` and an explicit `id:null` is a malformed request. Per the MCP // base protocol the request id "MUST NOT be null", and a null id does NOT denote a notification // (JSON-RPC 2.0 notifications omit the id member entirely). The converter must reject it rather // than silently downgrading to a notification (which would swallow the id and skip the response). string json = """{"jsonrpc":"2.0","id":null,"method":"tools/list"}"""; Assert.Throws(() => JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions)); } [Fact] public static void Deserialize_RequestWithExplicitNullIdAndParams_Throws() { string json = """{"jsonrpc":"2.0","id":null,"method":"tools/call","params":{"name":"echo"}}"""; Assert.Throws(() => JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions)); } [Fact] public static void Deserialize_NotificationWithoutIdMember_IsNotConfusedWithNullIdRequest() { // Contrast with the explicit-null-id case above: omitting the id member entirely is a valid // notification and must continue to deserialize as one. string json = """{"jsonrpc":"2.0","method":"notifications/cancelled","params":{"requestId":1}}"""; var message = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); var notification = Assert.IsType(message); Assert.Equal("notifications/cancelled", notification.Method); } }