Ignore unexpected properties when deserializing a ContentBlock (#956)
* Ignore unexpected properties when deserializing a ContentBlock * Fix deserialization of Reference.Title and more bugs for unknown props * Remove comments about skipping additional properties
This commit is contained in:
parent
98a0261585
commit
d4ee78797b
10 changed files with 1762 additions and 1 deletions
|
|
@ -141,6 +141,7 @@ public abstract class ContentBlock
|
|||
break;
|
||||
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ public sealed class ElicitRequestParams
|
|||
break;
|
||||
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,10 @@ public sealed class ProgressNotificationParams : NotificationParams
|
|||
case "_meta":
|
||||
meta = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.JsonObject);
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,11 +81,16 @@ public abstract class Reference
|
|||
name = reader.GetString();
|
||||
break;
|
||||
|
||||
case "title":
|
||||
title = reader.GetString();
|
||||
break;
|
||||
|
||||
case "uri":
|
||||
uri = reader.GetString();
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ public abstract class ResourceContents
|
|||
break;
|
||||
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 class ContentBlockTests
|
|||
|
||||
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<ContentBlock>(responseJson, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(contentBlock);
|
||||
|
||||
var textBlock = Assert.IsType<TextContentBlock>(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<ContentBlock>(responseJson, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(contentBlock);
|
||||
|
||||
var textBlock = Assert.IsType<TextContentBlock>(contentBlock);
|
||||
Assert.Contains("Sample text", textBlock.Text);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ElicitRequestParams.PrimitiveSchemaDefinition>(
|
||||
jsonWithUnknownArray,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var stringSchema = Assert.IsType<ElicitRequestParams.StringSchema>(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<ElicitRequestParams.PrimitiveSchemaDefinition>(
|
||||
jsonWithUnknownObject,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var numberSchema = Assert.IsType<ElicitRequestParams.NumberSchema>(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<ElicitRequestParams.PrimitiveSchemaDefinition>(
|
||||
jsonWithMixedUnknown,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var boolSchema = Assert.IsType<ElicitRequestParams.BooleanSchema>(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<ElicitRequestParams.PrimitiveSchemaDefinition>(
|
||||
jsonWithNestedArrays,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var enumSchema = Assert.IsType<ElicitRequestParams.EnumSchema>(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<ElicitRequestParams.PrimitiveSchemaDefinition>(
|
||||
jsonWithMultipleUnknown,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var stringSchema = Assert.IsType<ElicitRequestParams.StringSchema>(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<ElicitRequestParams.PrimitiveSchemaDefinition>(
|
||||
jsonWithArrayOfArrays,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var numberSchema = Assert.IsType<ElicitRequestParams.NumberSchema>(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<ElicitRequestParams.PrimitiveSchemaDefinition>(
|
||||
jsonWithEmptyArray,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var stringSchema = Assert.IsType<ElicitRequestParams.StringSchema>(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<ElicitRequestParams.PrimitiveSchemaDefinition>(
|
||||
jsonWithEmptyObject,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var numberSchema = Assert.IsType<ElicitRequestParams.NumberSchema>(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<ElicitRequestParams.PrimitiveSchemaDefinition>(
|
||||
jsonWithInterspersedUnknown,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var enumSchema = Assert.IsType<ElicitRequestParams.EnumSchema>(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<ElicitRequestParams.PrimitiveSchemaDefinition>(
|
||||
jsonWithVeryDeepNesting,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var boolSchema = Assert.IsType<ElicitRequestParams.BooleanSchema>(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<ElicitRequestParams.PrimitiveSchemaDefinition>(
|
||||
enumSchemaJson,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(deserialized);
|
||||
var enumSchema = Assert.IsType<ElicitRequestParams.EnumSchema>(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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ProgressNotificationParams>(
|
||||
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<ProgressNotificationParams>(
|
||||
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<ProgressNotificationParams>(
|
||||
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<ProgressNotificationParams>(
|
||||
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<ProgressNotificationParams>(
|
||||
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<ProgressNotificationParams>(
|
||||
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<ProgressNotificationParams>(
|
||||
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<ProgressNotificationParams>(
|
||||
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<ProgressNotificationParams>(
|
||||
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<ProgressNotificationParams>(
|
||||
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<ProgressNotificationParams>(
|
||||
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<ProgressNotificationParams>(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<ProgressNotificationParams>(
|
||||
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<JsonException>(() =>
|
||||
JsonSerializer.Deserialize<ProgressNotificationParams>(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<JsonException>(() =>
|
||||
JsonSerializer.Deserialize<ProgressNotificationParams>(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<ProgressNotificationParams>(
|
||||
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<ProgressNotificationParams>(
|
||||
jsonIntToken,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("42", result.ProgressToken.ToString());
|
||||
Assert.Equal(75.0f, result.Progress.Progress);
|
||||
}
|
||||
}
|
||||
412
tests/ModelContextProtocol.Tests/Protocol/ReferenceTests.cs
Normal file
412
tests/ModelContextProtocol.Tests/Protocol/ReferenceTests.cs
Normal file
|
|
@ -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<Reference>(
|
||||
jsonWithUnknownArray,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var promptRef = Assert.IsType<PromptReference>(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<Reference>(
|
||||
jsonWithUnknownObject,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var resourceRef = Assert.IsType<ResourceTemplateReference>(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<Reference>(
|
||||
jsonWithMixedUnknown,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var promptRef = Assert.IsType<PromptReference>(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<Reference>(
|
||||
jsonWithNestedArrays,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var resourceRef = Assert.IsType<ResourceTemplateReference>(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<Reference>(
|
||||
jsonWithMultipleUnknown,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var promptRef = Assert.IsType<PromptReference>(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<Reference>(
|
||||
jsonWithArrayOfArrays,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var resourceRef = Assert.IsType<ResourceTemplateReference>(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<Reference>(
|
||||
jsonWithEmptyArray,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var promptRef = Assert.IsType<PromptReference>(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<Reference>(
|
||||
jsonWithEmptyObject,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var resourceRef = Assert.IsType<ResourceTemplateReference>(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<Reference>(
|
||||
jsonWithInterspersedUnknown,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var promptRef = Assert.IsType<PromptReference>(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<Reference>(
|
||||
jsonWithVeryDeepNesting,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var resourceRef = Assert.IsType<ResourceTemplateReference>(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<Reference>(original, McpJsonUtilities.DefaultOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<Reference>(json, McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(deserialized);
|
||||
var promptRef = Assert.IsType<PromptReference>(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<Reference>(original, McpJsonUtilities.DefaultOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<Reference>(json, McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(deserialized);
|
||||
var resourceRef = Assert.IsType<ResourceTemplateReference>(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<Reference>(
|
||||
jsonWithoutTitle,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var promptRef = Assert.IsType<PromptReference>(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<JsonException>(() =>
|
||||
JsonSerializer.Deserialize<Reference>(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<JsonException>(() =>
|
||||
JsonSerializer.Deserialize<Reference>(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<JsonException>(() =>
|
||||
JsonSerializer.Deserialize<Reference>(jsonMissingUri, McpJsonUtilities.DefaultOptions));
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ResourceContents>(
|
||||
jsonWithUnknownArray,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var textResource = Assert.IsType<TextResourceContents>(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<ResourceContents>(
|
||||
jsonWithUnknownObject,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var blobResource = Assert.IsType<BlobResourceContents>(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<ResourceContents>(
|
||||
jsonWithMixedUnknown,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var textResource = Assert.IsType<TextResourceContents>(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<ResourceContents>(
|
||||
jsonWithNestedArrays,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var blobResource = Assert.IsType<BlobResourceContents>(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<ResourceContents>(
|
||||
jsonWithMultipleUnknown,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var textResource = Assert.IsType<TextResourceContents>(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<ResourceContents>(
|
||||
jsonWithArrayOfArrays,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var blobResource = Assert.IsType<BlobResourceContents>(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<ResourceContents>(
|
||||
jsonWithEmptyArray,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var textResource = Assert.IsType<TextResourceContents>(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<ResourceContents>(
|
||||
jsonWithEmptyObject,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var blobResource = Assert.IsType<BlobResourceContents>(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<ResourceContents>(
|
||||
jsonWithInterspersedUnknown,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var textResource = Assert.IsType<TextResourceContents>(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<ResourceContents>(
|
||||
jsonWithVeryDeepNesting,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var blobResource = Assert.IsType<BlobResourceContents>(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<ResourceContents>(
|
||||
jsonWithMetaAndUnknown,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var textResource = Assert.IsType<TextResourceContents>(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<ResourceContents>(original, McpJsonUtilities.DefaultOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<ResourceContents>(json, McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(deserialized);
|
||||
var textResource = Assert.IsType<TextResourceContents>(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<ResourceContents>(original, McpJsonUtilities.DefaultOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<ResourceContents>(json, McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(deserialized);
|
||||
var blobResource = Assert.IsType<BlobResourceContents>(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<ResourceContents>(
|
||||
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<ResourceContents>(
|
||||
jsonWithBoth,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var blobResource = Assert.IsType<BlobResourceContents>(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<ResourceContents>(
|
||||
jsonWithoutUri,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var textResource = Assert.IsType<TextResourceContents>(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<ResourceContents>(
|
||||
jsonWithoutUri,
|
||||
McpJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
var blobResource = Assert.IsType<BlobResourceContents>(result);
|
||||
Assert.Equal(string.Empty, blobResource.Uri);
|
||||
Assert.Equal("YmxvYg==", blobResource.Blob);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue