From 019075c1fd9573e34351f8f33e6519975ae15943 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Sat, 8 Nov 2025 20:03:33 -0800 Subject: [PATCH 1/3] Ignore unexpected properties when deserializing a ContentBlock --- .../Protocol/ContentBlock.cs | 2 + .../Protocol/ContentBlockTests.cs | 47 ++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs b/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs index 407441483..7b85d412c 100644 --- a/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs +++ b/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs @@ -141,6 +141,8 @@ public class Converter : JsonConverter break; default: + // Skip unknown properties to handle unexpected data or future protocol extensions gracefully + reader.Skip(); break; } } diff --git a/tests/ModelContextProtocol.Tests/Protocol/ContentBlockTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ContentBlockTests.cs index c5ab88b3a..3d8d8ff18 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/ContentBlockTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/ContentBlockTests.cs @@ -1,5 +1,6 @@ -using System.Text.Json; +using Microsoft.Extensions.AI; using ModelContextProtocol.Protocol; +using System.Text.Json; namespace ModelContextProtocol.Tests.Protocol; @@ -80,4 +81,48 @@ public void ResourceLinkBlock_DeserializationWithoutName_ThrowsJsonException() Assert.Contains("Name must be provided for 'resource_link' type", exception.Message); } + + [Fact] + public void Deserialize_IgnoresUnknownArrayProperty() + { + // This is a regression test where a server returned an unexpected response with + // `structuredContent` as an array nested inside a content block. This should be + // permitted with the `structuredContent` gracefully ignored in that location. + string responseJson = @"{ + ""type"": ""text"", + ""text"": ""[\n {\n \""Data\"": \""1234567890\""\n }\n]"", + ""structuredContent"": [ + { + ""Data"": ""1234567890"" + } + ] + }"; + + var contentBlock = JsonSerializer.Deserialize(responseJson, McpJsonUtilities.DefaultOptions); + Assert.NotNull(contentBlock); + + var textBlock = Assert.IsType(contentBlock); + Assert.Contains("1234567890", textBlock.Text); + } + + [Fact] + public void Deserialize_IgnoresUnknownObjectProperties() + { + string responseJson = @"{ + ""type"": ""text"", + ""text"": ""Sample text"", + ""unknownObject"": { + ""nestedProp1"": ""value1"", + ""nestedProp2"": { + ""deeplyNested"": true + } + } + }"; + + var contentBlock = JsonSerializer.Deserialize(responseJson, McpJsonUtilities.DefaultOptions); + Assert.NotNull(contentBlock); + + var textBlock = Assert.IsType(contentBlock); + Assert.Contains("Sample text", textBlock.Text); + } } \ No newline at end of file From ef029b407b5f70e1179b4e3a667c3fba62516827 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Sat, 8 Nov 2025 22:27:08 -0800 Subject: [PATCH 2/3] Fix deserialization of Reference.Title and more bugs for unknown props --- .../Protocol/ElicitRequestParams.cs | 2 + .../Protocol/ProgressNotificationParams.cs | 5 + .../Protocol/Reference.cs | 6 + .../Protocol/ResourceContents.cs | 2 + .../PrimitiveSchemaDefinitionTests.cs | 372 ++++++++++++++ .../ProgressNotificationParamsTests.cs | 458 +++++++++++++++++ .../Protocol/ReferenceTests.cs | 412 ++++++++++++++++ .../Protocol/ResourceContentsTests.cs | 462 ++++++++++++++++++ 8 files changed, 1719 insertions(+) create mode 100644 tests/ModelContextProtocol.Tests/Protocol/PrimitiveSchemaDefinitionTests.cs create mode 100644 tests/ModelContextProtocol.Tests/Protocol/ProgressNotificationParamsTests.cs create mode 100644 tests/ModelContextProtocol.Tests/Protocol/ReferenceTests.cs create mode 100644 tests/ModelContextProtocol.Tests/Protocol/ResourceContentsTests.cs diff --git a/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs index 93bc77185..a5e207c62 100644 --- a/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs @@ -188,6 +188,8 @@ public class Converter : JsonConverter break; default: + // Skip unknown properties to handle unexpected data or future protocol extensions gracefully + reader.Skip(); break; } } diff --git a/src/ModelContextProtocol.Core/Protocol/ProgressNotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/ProgressNotificationParams.cs index 294b12f1e..7d1daebba 100644 --- a/src/ModelContextProtocol.Core/Protocol/ProgressNotificationParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ProgressNotificationParams.cs @@ -81,6 +81,11 @@ public sealed class Converter : JsonConverter case "_meta": meta = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.JsonObject); break; + + default: + // Skip unknown properties to handle unexpected data or future protocol extensions gracefully + reader.Skip(); + break; } } } diff --git a/src/ModelContextProtocol.Core/Protocol/Reference.cs b/src/ModelContextProtocol.Core/Protocol/Reference.cs index 9f57d9483..0cb342a5d 100644 --- a/src/ModelContextProtocol.Core/Protocol/Reference.cs +++ b/src/ModelContextProtocol.Core/Protocol/Reference.cs @@ -81,11 +81,17 @@ public sealed class Converter : JsonConverter name = reader.GetString(); break; + case "title": + title = reader.GetString(); + break; + case "uri": uri = reader.GetString(); break; default: + // Skip unknown properties to handle unexpected data or future protocol extensions gracefully + reader.Skip(); break; } } diff --git a/src/ModelContextProtocol.Core/Protocol/ResourceContents.cs b/src/ModelContextProtocol.Core/Protocol/ResourceContents.cs index 90a37af1c..b4214b058 100644 --- a/src/ModelContextProtocol.Core/Protocol/ResourceContents.cs +++ b/src/ModelContextProtocol.Core/Protocol/ResourceContents.cs @@ -116,6 +116,8 @@ public class Converter : JsonConverter break; default: + // Skip unknown properties to handle unexpected data or future protocol extensions gracefully + reader.Skip(); break; } } diff --git a/tests/ModelContextProtocol.Tests/Protocol/PrimitiveSchemaDefinitionTests.cs b/tests/ModelContextProtocol.Tests/Protocol/PrimitiveSchemaDefinitionTests.cs new file mode 100644 index 000000000..bb5ab1c67 --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Protocol/PrimitiveSchemaDefinitionTests.cs @@ -0,0 +1,372 @@ +using ModelContextProtocol.Protocol; +using System.Text.Json; + +namespace ModelContextProtocol.Tests.Protocol; + +public static class PrimitiveSchemaDefinitionTests +{ + [Fact] + public static void StringSchema_UnknownArrayProperty_IsIgnored() + { + // This test verifies that the PrimitiveSchemaDefinition.Converter properly skips unknown properties + // even when they contain complex structures like arrays or objects. + // + // In this unexpected JSON, "unknownArray" appears inside a string schema (where it doesn't belong). + // The converter should gracefully ignore this unknown property and successfully deserialize + // the rest of the schema. + + const string jsonWithUnknownArray = """ + { + "type": "string", + "title": "Test String", + "minLength": 1, + "maxLength": 100, + "unknownArray": [ + { + "nested": "value" + }, + { + "another": "object" + } + ] + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithUnknownArray, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var stringSchema = Assert.IsType(result); + Assert.Equal("string", stringSchema.Type); + Assert.Equal("Test String", stringSchema.Title); + Assert.Equal(1, stringSchema.MinLength); + Assert.Equal(100, stringSchema.MaxLength); + } + + [Fact] + public static void NumberSchema_UnknownObjectProperty_IsIgnored() + { + // Test that unknown properties with nested objects are properly skipped + + const string jsonWithUnknownObject = """ + { + "type": "number", + "description": "Test Number", + "minimum": 0, + "maximum": 1000, + "unknownObject": { + "deeply": { + "nested": { + "value": "should be ignored" + } + } + } + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithUnknownObject, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var numberSchema = Assert.IsType(result); + Assert.Equal("number", numberSchema.Type); + Assert.Equal("Test Number", numberSchema.Description); + Assert.Equal(0, numberSchema.Minimum); + Assert.Equal(1000, numberSchema.Maximum); + } + + [Fact] + public static void BooleanSchema_UnknownMixedProperties_AreIgnored() + { + // Test multiple unknown properties with different types + + const string jsonWithMixedUnknown = """ + { + "type": "boolean", + "title": "Test Boolean", + "unknownString": "value", + "unknownNumber": 42, + "unknownArray": [1, 2, 3], + "unknownObject": {"key": "value"}, + "unknownBool": true, + "unknownNull": null, + "default": false + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithMixedUnknown, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var boolSchema = Assert.IsType(result); + Assert.Equal("boolean", boolSchema.Type); + Assert.Equal("Test Boolean", boolSchema.Title); + Assert.False(boolSchema.Default); + } + + [Fact] + public static void EnumSchema_UnknownNestedArrays_AreIgnored() + { + // Test complex unknown properties with arrays of objects + + const string jsonWithNestedArrays = """ + { + "type": "string", + "enum": ["option1", "option2", "option3"], + "enumNames": ["Name1", "Name2", "Name3"], + "unknownComplex": [ + { + "nested": [ + {"deep": "value1"}, + {"deep": "value2"} + ] + }, + { + "nested": [ + {"deep": "value3"} + ] + } + ], + "default": "option1" + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithNestedArrays, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var enumSchema = Assert.IsType(result); + Assert.Equal("string", enumSchema.Type); + Assert.Equal(3, enumSchema.Enum.Count); + Assert.Contains("option1", enumSchema.Enum); + Assert.Contains("option2", enumSchema.Enum); + Assert.Contains("option3", enumSchema.Enum); + Assert.Equal(3, enumSchema.EnumNames!.Count); + Assert.Contains("Name1", enumSchema.EnumNames); + Assert.Contains("Name2", enumSchema.EnumNames); + Assert.Contains("Name3", enumSchema.EnumNames); + Assert.Equal("option1", enumSchema.Default); + } + + [Fact] + public static void StringSchema_MultipleUnknownProperties_AllIgnored() + { + // Test that multiple unknown properties are all properly skipped + + const string jsonWithMultipleUnknown = """ + { + "type": "string", + "title": "Test", + "unknownOne": {"a": 1}, + "minLength": 5, + "unknownTwo": [1, 2, 3], + "maxLength": 50, + "unknownThree": {"b": {"c": "d"}}, + "format": "email" + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithMultipleUnknown, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var stringSchema = Assert.IsType(result); + Assert.Equal("string", stringSchema.Type); + Assert.Equal("Test", stringSchema.Title); + Assert.Equal(5, stringSchema.MinLength); + Assert.Equal(50, stringSchema.MaxLength); + Assert.Equal("email", stringSchema.Format); + } + + [Fact] + public static void IntegerSchema_UnknownArrayOfArrays_IsIgnored() + { + // Test deeply nested array structures in unknown properties + + const string jsonWithArrayOfArrays = """ + { + "type": "integer", + "minimum": 1, + "maximum": 100, + "unknownNested": [ + [ + [1, 2, 3], + [4, 5, 6] + ], + [ + [7, 8, 9] + ] + ] + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithArrayOfArrays, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var numberSchema = Assert.IsType(result); + Assert.Equal("integer", numberSchema.Type); + Assert.Equal(1, numberSchema.Minimum); + Assert.Equal(100, numberSchema.Maximum); + } + + [Fact] + public static void StringSchema_EmptyUnknownArray_IsIgnored() + { + // Test empty arrays in unknown properties + + const string jsonWithEmptyArray = """ + { + "type": "string", + "description": "Test", + "unknownEmpty": [], + "minLength": 0 + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithEmptyArray, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var stringSchema = Assert.IsType(result); + Assert.Equal("string", stringSchema.Type); + Assert.Equal("Test", stringSchema.Description); + Assert.Equal(0, stringSchema.MinLength); + } + + [Fact] + public static void NumberSchema_EmptyUnknownObject_IsIgnored() + { + // Test empty objects in unknown properties + + const string jsonWithEmptyObject = """ + { + "type": "number", + "title": "Test Number", + "unknownEmpty": {}, + "minimum": 0.0, + "maximum": 100.0 + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithEmptyObject, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var numberSchema = Assert.IsType(result); + Assert.Equal("number", numberSchema.Type); + Assert.Equal("Test Number", numberSchema.Title); + Assert.Equal(0.0, numberSchema.Minimum); + Assert.Equal(100.0, numberSchema.Maximum); + } + + [Fact] + public static void EnumSchema_UnknownPropertiesBetweenRequired_AreIgnored() + { + // Test unknown properties interspersed with required ones + + const string jsonWithInterspersedUnknown = """ + { + "unknownFirst": {"x": 1}, + "type": "string", + "unknownSecond": [1, 2], + "enum": ["a", "b"], + "unknownThird": {"nested": {"value": true}}, + "enumNames": ["Alpha", "Beta"] + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithInterspersedUnknown, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var enumSchema = Assert.IsType(result); + Assert.Equal("string", enumSchema.Type); + Assert.Equal(2, enumSchema.Enum.Count); + Assert.Contains("a", enumSchema.Enum); + Assert.Contains("b", enumSchema.Enum); + Assert.Equal(2, enumSchema.EnumNames!.Count); + Assert.Contains("Alpha", enumSchema.EnumNames); + Assert.Contains("Beta", enumSchema.EnumNames); + } + + [Fact] + public static void BooleanSchema_VeryDeeplyNestedUnknown_IsIgnored() + { + // Test very deeply nested structures in unknown properties + + const string jsonWithVeryDeepNesting = """ + { + "type": "boolean", + "unknownDeep": { + "level1": { + "level2": { + "level3": { + "level4": { + "level5": { + "value": "deep" + } + } + } + } + } + }, + "default": true + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithVeryDeepNesting, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var boolSchema = Assert.IsType(result); + Assert.Equal("boolean", boolSchema.Type); + Assert.True(boolSchema.Default); + } + + [Fact] + public static void EnumSchema_Deserialization_PreservesKnownProperties() + { + // Test deserialization of enum schema with all properties + + const string enumSchemaJson = """ + { + "type": "string", + "title": "Test Enum", + "description": "A test enum schema", + "enum": ["option1", "option2", "option3"], + "enumNames": ["Name1", "Name2", "Name3"], + "default": "option2" + } + """; + + var deserialized = JsonSerializer.Deserialize( + enumSchemaJson, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + var enumSchema = Assert.IsType(deserialized); + Assert.Equal("string", enumSchema.Type); + Assert.Equal("Test Enum", enumSchema.Title); + Assert.Equal("A test enum schema", enumSchema.Description); + Assert.Equal(3, enumSchema.Enum.Count); + Assert.Contains("option1", enumSchema.Enum); + Assert.Contains("option2", enumSchema.Enum); + Assert.Contains("option3", enumSchema.Enum); + Assert.Equal(3, enumSchema.EnumNames!.Count); + Assert.Contains("Name1", enumSchema.EnumNames); + Assert.Contains("Name2", enumSchema.EnumNames); + Assert.Contains("Name3", enumSchema.EnumNames); + Assert.Equal("option2", enumSchema.Default); + } +} diff --git a/tests/ModelContextProtocol.Tests/Protocol/ProgressNotificationParamsTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ProgressNotificationParamsTests.cs new file mode 100644 index 000000000..427961ccb --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Protocol/ProgressNotificationParamsTests.cs @@ -0,0 +1,458 @@ +using ModelContextProtocol.Protocol; +using System.Text.Json; + +namespace ModelContextProtocol.Tests.Protocol; + +public static class ProgressNotificationParamsTests +{ + [Fact] + public static void ProgressNotificationParams_UnknownArrayProperty_IsIgnored() + { + // This test verifies that the ProgressNotificationParams.Converter properly skips unknown properties + // even when they contain complex structures like arrays or objects. + // + // In this unexpected JSON, "unknownArray" appears inside progress notification params (where it doesn't belong). + // The converter should gracefully ignore this unknown property and successfully deserialize + // the rest of the notification parameters. + + const string jsonWithUnknownArray = """ + { + "progressToken": "test-token", + "progress": 50.0, + "total": 100.0, + "message": "Processing items", + "unknownArray": [ + { + "nested": "value" + }, + { + "another": "object" + } + ] + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithUnknownArray, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + Assert.Equal("test-token", result.ProgressToken.ToString()); + Assert.Equal(50.0f, result.Progress.Progress); + Assert.Equal(100.0f, result.Progress.Total); + Assert.Equal("Processing items", result.Progress.Message); + } + + [Fact] + public static void ProgressNotificationParams_UnknownObjectProperty_IsIgnored() + { + // Test that unknown properties with nested objects are properly skipped + + const string jsonWithUnknownObject = """ + { + "progressToken": 12345, + "progress": 75.5, + "unknownObject": { + "deeply": { + "nested": { + "value": "should be ignored" + } + } + } + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithUnknownObject, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + Assert.Equal("12345", result.ProgressToken.ToString()); + Assert.Equal(75.5f, result.Progress.Progress); + Assert.Null(result.Progress.Total); + } + + [Fact] + public static void ProgressNotificationParams_UnknownMixedProperties_AreIgnored() + { + // Test multiple unknown properties with different types + + const string jsonWithMixedUnknown = """ + { + "progressToken": "abc-123", + "unknownString": "value", + "progress": 25.0, + "unknownNumber": 42, + "total": 200.0, + "unknownArray": [1, 2, 3], + "message": "Working on it", + "unknownObject": {"key": "value"}, + "unknownBool": true, + "unknownNull": null + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithMixedUnknown, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + Assert.Equal("abc-123", result.ProgressToken.ToString()); + Assert.Equal(25.0f, result.Progress.Progress); + Assert.Equal(200.0f, result.Progress.Total); + Assert.Equal("Working on it", result.Progress.Message); + } + + [Fact] + public static void ProgressNotificationParams_UnknownNestedArrays_AreIgnored() + { + // Test complex unknown properties with arrays of objects + + const string jsonWithNestedArrays = """ + { + "progressToken": "nested-test", + "progress": 33.3, + "total": 99.9, + "unknownComplex": [ + { + "nested": [ + {"deep": "value1"}, + {"deep": "value2"} + ] + }, + { + "nested": [ + {"deep": "value3"} + ] + } + ] + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithNestedArrays, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + Assert.Equal("nested-test", result.ProgressToken.ToString()); + Assert.Equal(33.3f, result.Progress.Progress); + Assert.Equal(99.9f, result.Progress.Total); + } + + [Fact] + public static void ProgressNotificationParams_MultipleUnknownProperties_AllIgnored() + { + // Test that multiple unknown properties are all properly skipped + + const string jsonWithMultipleUnknown = """ + { + "unknownOne": {"a": 1}, + "progressToken": "multi-test", + "unknownTwo": [1, 2, 3], + "progress": 10.0, + "unknownThree": {"b": {"c": "d"}}, + "message": "Multiple unknowns" + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithMultipleUnknown, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + Assert.Equal("multi-test", result.ProgressToken.ToString()); + Assert.Equal(10.0f, result.Progress.Progress); + Assert.Equal("Multiple unknowns", result.Progress.Message); + } + + [Fact] + public static void ProgressNotificationParams_UnknownArrayOfArrays_IsIgnored() + { + // Test deeply nested array structures in unknown properties + + const string jsonWithArrayOfArrays = """ + { + "progressToken": 999, + "progress": 88.0, + "unknownNested": [ + [ + [1, 2, 3], + [4, 5, 6] + ], + [ + [7, 8, 9] + ] + ] + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithArrayOfArrays, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + Assert.Equal("999", result.ProgressToken.ToString()); + Assert.Equal(88.0f, result.Progress.Progress); + } + + [Fact] + public static void ProgressNotificationParams_EmptyUnknownArray_IsIgnored() + { + // Test empty arrays in unknown properties + + const string jsonWithEmptyArray = """ + { + "progressToken": "empty-array", + "progress": 0.0, + "unknownEmpty": [] + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithEmptyArray, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + Assert.Equal("empty-array", result.ProgressToken.ToString()); + Assert.Equal(0.0f, result.Progress.Progress); + } + + [Fact] + public static void ProgressNotificationParams_EmptyUnknownObject_IsIgnored() + { + // Test empty objects in unknown properties + + const string jsonWithEmptyObject = """ + { + "progressToken": "empty-obj", + "progress": 100.0, + "total": 100.0, + "unknownEmpty": {} + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithEmptyObject, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + Assert.Equal("empty-obj", result.ProgressToken.ToString()); + Assert.Equal(100.0f, result.Progress.Progress); + Assert.Equal(100.0f, result.Progress.Total); + } + + [Fact] + public static void ProgressNotificationParams_UnknownPropertiesBetweenRequired_AreIgnored() + { + // Test unknown properties interspersed with required ones + + const string jsonWithInterspersedUnknown = """ + { + "unknownFirst": {"x": 1}, + "progressToken": "interspersed", + "unknownSecond": [1, 2], + "progress": 42.0, + "unknownThird": {"nested": {"value": true}}, + "total": 84.0, + "unknownFourth": [], + "message": "Interspersed test" + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithInterspersedUnknown, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + Assert.Equal("interspersed", result.ProgressToken.ToString()); + Assert.Equal(42.0f, result.Progress.Progress); + Assert.Equal(84.0f, result.Progress.Total); + Assert.Equal("Interspersed test", result.Progress.Message); + } + + [Fact] + public static void ProgressNotificationParams_VeryDeeplyNestedUnknown_IsIgnored() + { + // Test very deeply nested structures in unknown properties + + const string jsonWithVeryDeepNesting = """ + { + "progressToken": 777, + "progress": 50.0, + "unknownDeep": { + "level1": { + "level2": { + "level3": { + "level4": { + "level5": { + "value": "deep" + } + } + } + } + } + } + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithVeryDeepNesting, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + Assert.Equal("777", result.ProgressToken.ToString()); + Assert.Equal(50.0f, result.Progress.Progress); + } + + [Fact] + public static void ProgressNotificationParams_WithMeta_UnknownPropertiesIgnored() + { + // Test that _meta property works correctly alongside unknown properties + + const string jsonWithMetaAndUnknown = """ + { + "progressToken": "meta-test", + "progress": 65.0, + "unknownProp": {"data": "ignored"}, + "_meta": { + "customField": "metaValue" + } + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithMetaAndUnknown, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + Assert.Equal("meta-test", result.ProgressToken.ToString()); + Assert.Equal(65.0f, result.Progress.Progress); + Assert.NotNull(result.Meta); + Assert.True(result.Meta.ContainsKey("customField")); + } + + [Fact] + public static void ProgressNotificationParams_SerializationRoundTrip_PreservesKnownProperties() + { + // Test that serialization/deserialization preserves known properties + + var original = new ProgressNotificationParams + { + ProgressToken = new ProgressToken("roundtrip-test"), + Progress = new ProgressNotificationValue + { + Progress = 45.5f, + Total = 91.0f, + Message = "Roundtrip test" + } + }; + + var json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + Assert.Equal(original.ProgressToken.ToString(), deserialized.ProgressToken.ToString()); + Assert.Equal(original.Progress.Progress, deserialized.Progress.Progress); + Assert.Equal(original.Progress.Total, deserialized.Progress.Total); + Assert.Equal(original.Progress.Message, deserialized.Progress.Message); + } + + [Fact] + public static void ProgressNotificationParams_MinimalProperties_Deserializes() + { + // Test with only required properties + + const string jsonMinimal = """ + { + "progressToken": "minimal", + "progress": 50.0 + } + """; + + var result = JsonSerializer.Deserialize( + jsonMinimal, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + Assert.Equal("minimal", result.ProgressToken.ToString()); + Assert.Equal(50.0f, result.Progress.Progress); + Assert.Null(result.Progress.Total); + Assert.Null(result.Progress.Message); + } + + [Fact] + public static void ProgressNotificationParams_MissingProgress_ThrowsException() + { + // Test that missing required progress property throws an exception + + const string jsonMissingProgress = """ + { + "progressToken": "test", + "total": 100.0 + } + """; + + Assert.Throws(() => + JsonSerializer.Deserialize(jsonMissingProgress, McpJsonUtilities.DefaultOptions)); + } + + [Fact] + public static void ProgressNotificationParams_MissingProgressToken_ThrowsException() + { + // Test that missing required progressToken property throws an exception + + const string jsonMissingToken = """ + { + "progress": 50.0, + "total": 100.0 + } + """; + + Assert.Throws(() => + JsonSerializer.Deserialize(jsonMissingToken, McpJsonUtilities.DefaultOptions)); + } + + [Fact] + public static void ProgressNotificationParams_StringProgressToken_Deserializes() + { + // Test with string progress token + + const string jsonStringToken = """ + { + "progressToken": "string-token-123", + "progress": 25.0, + "total": 100.0 + } + """; + + var result = JsonSerializer.Deserialize( + jsonStringToken, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + Assert.Equal("string-token-123", result.ProgressToken.ToString()); + Assert.Equal(25.0f, result.Progress.Progress); + } + + [Fact] + public static void ProgressNotificationParams_IntegerProgressToken_Deserializes() + { + // Test with integer progress token + + const string jsonIntToken = """ + { + "progressToken": 42, + "progress": 75.0, + "total": 100.0 + } + """; + + var result = JsonSerializer.Deserialize( + jsonIntToken, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + Assert.Equal("42", result.ProgressToken.ToString()); + Assert.Equal(75.0f, result.Progress.Progress); + } +} diff --git a/tests/ModelContextProtocol.Tests/Protocol/ReferenceTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ReferenceTests.cs new file mode 100644 index 000000000..d9d4c199f --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Protocol/ReferenceTests.cs @@ -0,0 +1,412 @@ +using ModelContextProtocol.Protocol; +using System.Text.Json; + +namespace ModelContextProtocol.Tests.Protocol; + +public static class ReferenceTests +{ + [Fact] + public static void PromptReference_UnknownArrayProperty_IsIgnored() + { + // This test verifies that the Reference.Converter properly skips unknown properties + // even when they contain complex structures like arrays or objects. + // + // In this unexpected JSON, "unknownArray" appears inside a prompt reference (where it doesn't belong). + // The converter should gracefully ignore this unknown property and successfully deserialize + // the rest of the reference. + + const string jsonWithUnknownArray = """ + { + "type": "ref/prompt", + "name": "test_prompt", + "title": "Test Prompt", + "unknownArray": [ + { + "nested": "value" + }, + { + "another": "object" + } + ] + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithUnknownArray, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var promptRef = Assert.IsType(result); + Assert.Equal("ref/prompt", promptRef.Type); + Assert.Equal("test_prompt", promptRef.Name); + Assert.Equal("Test Prompt", promptRef.Title); + } + + [Fact] + public static void ResourceReference_UnknownObjectProperty_IsIgnored() + { + // Test that unknown properties with nested objects are properly skipped + + const string jsonWithUnknownObject = """ + { + "type": "ref/resource", + "uri": "file:///test/resource", + "unknownObject": { + "deeply": { + "nested": { + "value": "should be ignored" + } + } + } + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithUnknownObject, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var resourceRef = Assert.IsType(result); + Assert.Equal("ref/resource", resourceRef.Type); + Assert.Equal("file:///test/resource", resourceRef.Uri); + } + + [Fact] + public static void PromptReference_UnknownMixedProperties_AreIgnored() + { + // Test multiple unknown properties with different types + + const string jsonWithMixedUnknown = """ + { + "type": "ref/prompt", + "name": "my_prompt", + "unknownString": "value", + "unknownNumber": 42, + "unknownArray": [1, 2, 3], + "unknownObject": {"key": "value"}, + "unknownBool": true, + "unknownNull": null + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithMixedUnknown, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var promptRef = Assert.IsType(result); + Assert.Equal("ref/prompt", promptRef.Type); + Assert.Equal("my_prompt", promptRef.Name); + } + + [Fact] + public static void ResourceReference_UnknownNestedArrays_AreIgnored() + { + // Test complex unknown properties with arrays of objects + + const string jsonWithNestedArrays = """ + { + "type": "ref/resource", + "uri": "resource://test/{id}", + "unknownComplex": [ + { + "nested": [ + {"deep": "value1"}, + {"deep": "value2"} + ] + }, + { + "nested": [ + {"deep": "value3"} + ] + } + ] + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithNestedArrays, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var resourceRef = Assert.IsType(result); + Assert.Equal("ref/resource", resourceRef.Type); + Assert.Equal("resource://test/{id}", resourceRef.Uri); + } + + [Fact] + public static void PromptReference_MultipleUnknownProperties_AllIgnored() + { + // Test that multiple unknown properties are all properly skipped + + const string jsonWithMultipleUnknown = """ + { + "type": "ref/prompt", + "unknownOne": {"a": 1}, + "name": "test", + "unknownTwo": [1, 2, 3], + "title": "Test Title", + "unknownThree": {"b": {"c": "d"}} + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithMultipleUnknown, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var promptRef = Assert.IsType(result); + Assert.Equal("ref/prompt", promptRef.Type); + Assert.Equal("test", promptRef.Name); + Assert.Equal("Test Title", promptRef.Title); + } + + [Fact] + public static void ResourceReference_UnknownArrayOfArrays_IsIgnored() + { + // Test deeply nested array structures in unknown properties + + const string jsonWithArrayOfArrays = """ + { + "type": "ref/resource", + "uri": "http://example.com/resource", + "unknownNested": [ + [ + [1, 2, 3], + [4, 5, 6] + ], + [ + [7, 8, 9] + ] + ] + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithArrayOfArrays, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var resourceRef = Assert.IsType(result); + Assert.Equal("ref/resource", resourceRef.Type); + Assert.Equal("http://example.com/resource", resourceRef.Uri); + } + + [Fact] + public static void PromptReference_EmptyUnknownArray_IsIgnored() + { + // Test empty arrays in unknown properties + + const string jsonWithEmptyArray = """ + { + "type": "ref/prompt", + "name": "prompt", + "unknownEmpty": [] + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithEmptyArray, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var promptRef = Assert.IsType(result); + Assert.Equal("ref/prompt", promptRef.Type); + Assert.Equal("prompt", promptRef.Name); + } + + [Fact] + public static void ResourceReference_EmptyUnknownObject_IsIgnored() + { + // Test empty objects in unknown properties + + const string jsonWithEmptyObject = """ + { + "type": "ref/resource", + "uri": "test://resource", + "unknownEmpty": {} + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithEmptyObject, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var resourceRef = Assert.IsType(result); + Assert.Equal("ref/resource", resourceRef.Type); + Assert.Equal("test://resource", resourceRef.Uri); + } + + [Fact] + public static void PromptReference_UnknownPropertiesBetweenRequired_AreIgnored() + { + // Test unknown properties interspersed with required ones + + const string jsonWithInterspersedUnknown = """ + { + "unknownFirst": {"x": 1}, + "type": "ref/prompt", + "unknownSecond": [1, 2], + "name": "my_prompt", + "unknownThird": {"nested": {"value": true}}, + "title": "My Prompt" + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithInterspersedUnknown, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var promptRef = Assert.IsType(result); + Assert.Equal("ref/prompt", promptRef.Type); + Assert.Equal("my_prompt", promptRef.Name); + Assert.Equal("My Prompt", promptRef.Title); + } + + [Fact] + public static void ResourceReference_VeryDeeplyNestedUnknown_IsIgnored() + { + // Test very deeply nested structures in unknown properties + + const string jsonWithVeryDeepNesting = """ + { + "type": "ref/resource", + "uri": "deep://resource", + "unknownDeep": { + "level1": { + "level2": { + "level3": { + "level4": { + "level5": { + "value": "deep" + } + } + } + } + } + } + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithVeryDeepNesting, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var resourceRef = Assert.IsType(result); + Assert.Equal("ref/resource", resourceRef.Type); + Assert.Equal("deep://resource", resourceRef.Uri); + } + + [Fact] + public static void PromptReference_SerializationRoundTrip_PreservesKnownProperties() + { + // Test that serialization/deserialization preserves known properties + + var original = new PromptReference + { + Name = "test_prompt", + Title = "Test Prompt Title" + }; + + var json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + var promptRef = Assert.IsType(deserialized); + Assert.Equal(original.Type, promptRef.Type); + Assert.Equal(original.Name, promptRef.Name); + Assert.Equal(original.Title, promptRef.Title); + } + + [Fact] + public static void ResourceReference_SerializationRoundTrip_PreservesKnownProperties() + { + // Test that serialization/deserialization preserves known properties + + var original = new ResourceTemplateReference + { + Uri = "file:///path/to/resource" + }; + + var json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + var resourceRef = Assert.IsType(deserialized); + Assert.Equal(original.Type, resourceRef.Type); + Assert.Equal(original.Uri, resourceRef.Uri); + } + + [Fact] + public static void PromptReference_WithoutTitle_Deserializes() + { + // Test that title is optional + + const string jsonWithoutTitle = """ + { + "type": "ref/prompt", + "name": "minimal_prompt" + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithoutTitle, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var promptRef = Assert.IsType(result); + Assert.Equal("ref/prompt", promptRef.Type); + Assert.Equal("minimal_prompt", promptRef.Name); + Assert.Null(promptRef.Title); + } + + [Fact] + public static void Reference_UnknownType_ThrowsException() + { + // Test that unknown reference types throw an exception + + const string jsonWithUnknownType = """ + { + "type": "ref/unknown", + "name": "test" + } + """; + + Assert.Throws(() => + JsonSerializer.Deserialize(jsonWithUnknownType, McpJsonUtilities.DefaultOptions)); + } + + [Fact] + public static void PromptReference_MissingName_ThrowsException() + { + // Test that missing required name property throws an exception + + const string jsonMissingName = """ + { + "type": "ref/prompt", + "title": "Test" + } + """; + + Assert.Throws(() => + JsonSerializer.Deserialize(jsonMissingName, McpJsonUtilities.DefaultOptions)); + } + + [Fact] + public static void ResourceReference_MissingUri_ThrowsException() + { + // Test that missing required uri property throws an exception + + const string jsonMissingUri = """ + { + "type": "ref/resource" + } + """; + + Assert.Throws(() => + JsonSerializer.Deserialize(jsonMissingUri, McpJsonUtilities.DefaultOptions)); + } +} diff --git a/tests/ModelContextProtocol.Tests/Protocol/ResourceContentsTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ResourceContentsTests.cs new file mode 100644 index 000000000..4f9890f7b --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Protocol/ResourceContentsTests.cs @@ -0,0 +1,462 @@ +using ModelContextProtocol.Protocol; +using System.Text.Json; + +namespace ModelContextProtocol.Tests.Protocol; + +public static class ResourceContentsTests +{ + [Fact] + public static void TextResourceContents_UnknownArrayProperty_IsIgnored() + { + // This test verifies that the ResourceContents.Converter properly skips unknown properties + // even when they contain complex structures like arrays or objects. + // + // In this unexpected JSON, "unknownArray" appears inside text resource contents (where it doesn't belong). + // The converter should gracefully ignore this unknown property and successfully deserialize + // the rest of the resource contents. + + const string jsonWithUnknownArray = """ + { + "uri": "file:///test.txt", + "mimeType": "text/plain", + "text": "Test content", + "unknownArray": [ + { + "nested": "value" + }, + { + "another": "object" + } + ] + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithUnknownArray, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var textResource = Assert.IsType(result); + Assert.Equal("file:///test.txt", textResource.Uri); + Assert.Equal("text/plain", textResource.MimeType); + Assert.Equal("Test content", textResource.Text); + } + + [Fact] + public static void BlobResourceContents_UnknownObjectProperty_IsIgnored() + { + // Test that unknown properties with nested objects are properly skipped + + const string jsonWithUnknownObject = """ + { + "uri": "file:///test.bin", + "mimeType": "application/octet-stream", + "blob": "AQIDBA==", + "unknownObject": { + "deeply": { + "nested": { + "value": "should be ignored" + } + } + } + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithUnknownObject, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var blobResource = Assert.IsType(result); + Assert.Equal("file:///test.bin", blobResource.Uri); + Assert.Equal("application/octet-stream", blobResource.MimeType); + Assert.Equal("AQIDBA==", blobResource.Blob); + } + + [Fact] + public static void TextResourceContents_UnknownMixedProperties_AreIgnored() + { + // Test multiple unknown properties with different types + + const string jsonWithMixedUnknown = """ + { + "uri": "test://resource", + "text": "content", + "unknownString": "value", + "unknownNumber": 42, + "unknownArray": [1, 2, 3], + "unknownObject": {"key": "value"}, + "unknownBool": true, + "unknownNull": null + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithMixedUnknown, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var textResource = Assert.IsType(result); + Assert.Equal("test://resource", textResource.Uri); + Assert.Equal("content", textResource.Text); + } + + [Fact] + public static void BlobResourceContents_UnknownNestedArrays_AreIgnored() + { + // Test complex unknown properties with arrays of objects + + const string jsonWithNestedArrays = """ + { + "uri": "blob://test", + "blob": "SGVsbG8=", + "mimeType": "application/custom", + "unknownComplex": [ + { + "nested": [ + {"deep": "value1"}, + {"deep": "value2"} + ] + }, + { + "nested": [ + {"deep": "value3"} + ] + } + ] + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithNestedArrays, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var blobResource = Assert.IsType(result); + Assert.Equal("blob://test", blobResource.Uri); + Assert.Equal("SGVsbG8=", blobResource.Blob); + Assert.Equal("application/custom", blobResource.MimeType); + } + + [Fact] + public static void TextResourceContents_MultipleUnknownProperties_AllIgnored() + { + // Test that multiple unknown properties are all properly skipped + + const string jsonWithMultipleUnknown = """ + { + "uri": "file:///test", + "unknownOne": {"a": 1}, + "text": "Test text", + "unknownTwo": [1, 2, 3], + "mimeType": "text/plain", + "unknownThree": {"b": {"c": "d"}} + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithMultipleUnknown, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var textResource = Assert.IsType(result); + Assert.Equal("file:///test", textResource.Uri); + Assert.Equal("Test text", textResource.Text); + Assert.Equal("text/plain", textResource.MimeType); + } + + [Fact] + public static void BlobResourceContents_UnknownArrayOfArrays_IsIgnored() + { + // Test deeply nested array structures in unknown properties + + const string jsonWithArrayOfArrays = """ + { + "uri": "http://example.com/blob", + "blob": "Zm9v", + "unknownNested": [ + [ + [1, 2, 3], + [4, 5, 6] + ], + [ + [7, 8, 9] + ] + ] + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithArrayOfArrays, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var blobResource = Assert.IsType(result); + Assert.Equal("http://example.com/blob", blobResource.Uri); + Assert.Equal("Zm9v", blobResource.Blob); + } + + [Fact] + public static void TextResourceContents_EmptyUnknownArray_IsIgnored() + { + // Test empty arrays in unknown properties + + const string jsonWithEmptyArray = """ + { + "uri": "test://text", + "text": "content", + "unknownEmpty": [] + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithEmptyArray, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var textResource = Assert.IsType(result); + Assert.Equal("test://text", textResource.Uri); + Assert.Equal("content", textResource.Text); + } + + [Fact] + public static void BlobResourceContents_EmptyUnknownObject_IsIgnored() + { + // Test empty objects in unknown properties + + const string jsonWithEmptyObject = """ + { + "uri": "test://blob", + "blob": "YmFy", + "unknownEmpty": {} + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithEmptyObject, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var blobResource = Assert.IsType(result); + Assert.Equal("test://blob", blobResource.Uri); + Assert.Equal("YmFy", blobResource.Blob); + } + + [Fact] + public static void TextResourceContents_UnknownPropertiesBetweenRequired_AreIgnored() + { + // Test unknown properties interspersed with required ones + + const string jsonWithInterspersedUnknown = """ + { + "unknownFirst": {"x": 1}, + "uri": "file:///document.txt", + "unknownSecond": [1, 2], + "text": "Document content", + "unknownThird": {"nested": {"value": true}}, + "mimeType": "text/plain" + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithInterspersedUnknown, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var textResource = Assert.IsType(result); + Assert.Equal("file:///document.txt", textResource.Uri); + Assert.Equal("Document content", textResource.Text); + Assert.Equal("text/plain", textResource.MimeType); + } + + [Fact] + public static void BlobResourceContents_VeryDeeplyNestedUnknown_IsIgnored() + { + // Test very deeply nested structures in unknown properties + + const string jsonWithVeryDeepNesting = """ + { + "uri": "deep://blob", + "blob": "ZGVlcA==", + "unknownDeep": { + "level1": { + "level2": { + "level3": { + "level4": { + "level5": { + "value": "deep" + } + } + } + } + } + } + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithVeryDeepNesting, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var blobResource = Assert.IsType(result); + Assert.Equal("deep://blob", blobResource.Uri); + Assert.Equal("ZGVlcA==", blobResource.Blob); + } + + [Fact] + public static void TextResourceContents_WithMeta_UnknownPropertiesIgnored() + { + // Test that _meta property works correctly alongside unknown properties + + const string jsonWithMetaAndUnknown = """ + { + "uri": "test://meta", + "text": "content with meta", + "unknownProp": {"data": "ignored"}, + "_meta": { + "customField": "metaValue" + } + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithMetaAndUnknown, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var textResource = Assert.IsType(result); + Assert.Equal("test://meta", textResource.Uri); + Assert.Equal("content with meta", textResource.Text); + Assert.NotNull(textResource.Meta); + Assert.True(textResource.Meta.ContainsKey("customField")); + } + + [Fact] + public static void TextResourceContents_SerializationRoundTrip_PreservesKnownProperties() + { + // Test that serialization/deserialization preserves known properties + + var original = new TextResourceContents + { + Uri = "file:///test.txt", + MimeType = "text/plain", + Text = "Test content" + }; + + var json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + var textResource = Assert.IsType(deserialized); + Assert.Equal(original.Uri, textResource.Uri); + Assert.Equal(original.MimeType, textResource.MimeType); + Assert.Equal(original.Text, textResource.Text); + } + + [Fact] + public static void BlobResourceContents_SerializationRoundTrip_PreservesKnownProperties() + { + // Test that serialization/deserialization preserves known properties + + var original = new BlobResourceContents + { + Uri = "file:///test.bin", + MimeType = "application/octet-stream", + Blob = "AQIDBA==" + }; + + var json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + var blobResource = Assert.IsType(deserialized); + Assert.Equal(original.Uri, blobResource.Uri); + Assert.Equal(original.MimeType, blobResource.MimeType); + Assert.Equal(original.Blob, blobResource.Blob); + } + + [Fact] + public static void ResourceContents_MissingBothTextAndBlob_ReturnsNull() + { + // Test that missing both text and blob properties returns null + + const string jsonWithoutContent = """ + { + "uri": "test://empty", + "mimeType": "application/unknown" + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithoutContent, + McpJsonUtilities.DefaultOptions); + + Assert.Null(result); + } + + [Fact] + public static void ResourceContents_WithBothTextAndBlob_PrefersBlob() + { + // Test that when both text and blob are present, blob takes precedence + + const string jsonWithBoth = """ + { + "uri": "test://both", + "text": "text content", + "blob": "YmxvYg==" + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithBoth, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var blobResource = Assert.IsType(result); + Assert.Equal("test://both", blobResource.Uri); + Assert.Equal("YmxvYg==", blobResource.Blob); + } + + [Fact] + public static void TextResourceContents_MissingUri_UsesEmptyString() + { + // Test that missing uri defaults to empty string + + const string jsonWithoutUri = """ + { + "text": "content without uri" + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithoutUri, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var textResource = Assert.IsType(result); + Assert.Equal(string.Empty, textResource.Uri); + Assert.Equal("content without uri", textResource.Text); + } + + [Fact] + public static void BlobResourceContents_MissingUri_UsesEmptyString() + { + // Test that missing uri defaults to empty string + + const string jsonWithoutUri = """ + { + "blob": "YmxvYg==" + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithoutUri, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var blobResource = Assert.IsType(result); + Assert.Equal(string.Empty, blobResource.Uri); + Assert.Equal("YmxvYg==", blobResource.Blob); + } +} From 2bd12cdb416aed6b9a3bc232b1ac94cadd8140e9 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Sun, 9 Nov 2025 00:02:10 -0800 Subject: [PATCH 3/3] Remove comments about skipping additional properties --- src/ModelContextProtocol.Core/Protocol/ContentBlock.cs | 1 - src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs | 1 - .../Protocol/ProgressNotificationParams.cs | 1 - src/ModelContextProtocol.Core/Protocol/Reference.cs | 1 - src/ModelContextProtocol.Core/Protocol/ResourceContents.cs | 1 - 5 files changed, 5 deletions(-) diff --git a/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs b/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs index 7b85d412c..ccc5e9623 100644 --- a/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs +++ b/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs @@ -141,7 +141,6 @@ public class Converter : JsonConverter break; default: - // Skip unknown properties to handle unexpected data or future protocol extensions gracefully reader.Skip(); break; } diff --git a/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs index a5e207c62..2cd94a5de 100644 --- a/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs @@ -188,7 +188,6 @@ public class Converter : JsonConverter break; default: - // Skip unknown properties to handle unexpected data or future protocol extensions gracefully reader.Skip(); break; } diff --git a/src/ModelContextProtocol.Core/Protocol/ProgressNotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/ProgressNotificationParams.cs index 7d1daebba..cc309ed04 100644 --- a/src/ModelContextProtocol.Core/Protocol/ProgressNotificationParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ProgressNotificationParams.cs @@ -83,7 +83,6 @@ public sealed class Converter : JsonConverter break; default: - // Skip unknown properties to handle unexpected data or future protocol extensions gracefully reader.Skip(); break; } diff --git a/src/ModelContextProtocol.Core/Protocol/Reference.cs b/src/ModelContextProtocol.Core/Protocol/Reference.cs index 0cb342a5d..876ce4017 100644 --- a/src/ModelContextProtocol.Core/Protocol/Reference.cs +++ b/src/ModelContextProtocol.Core/Protocol/Reference.cs @@ -90,7 +90,6 @@ public sealed class Converter : JsonConverter break; default: - // Skip unknown properties to handle unexpected data or future protocol extensions gracefully reader.Skip(); break; } diff --git a/src/ModelContextProtocol.Core/Protocol/ResourceContents.cs b/src/ModelContextProtocol.Core/Protocol/ResourceContents.cs index b4214b058..9c295a1f8 100644 --- a/src/ModelContextProtocol.Core/Protocol/ResourceContents.cs +++ b/src/ModelContextProtocol.Core/Protocol/ResourceContents.cs @@ -116,7 +116,6 @@ public class Converter : JsonConverter break; default: - // Skip unknown properties to handle unexpected data or future protocol extensions gracefully reader.Skip(); break; }