From 0996c24bce9cf586d06df98f4695609945d58246 Mon Sep 17 00:00:00 2001 From: Alberto Medina Gutierrez Date: Tue, 31 Mar 2026 14:39:28 -0700 Subject: [PATCH 1/7] Fix validation for mismatches on tools/list --- dotnet/mcpb.Tests/MetaFieldTests.cs | 47 ++-- .../mcpb/Commands/ManifestCommandHelpers.cs | 11 +- dotnet/mcpb/Commands/PackCommand.cs | 11 +- dotnet/mcpb/Core/ManifestModels.cs | 3 +- dotnet/mcpb/Core/ToolsListComparer.cs | 243 ++++++++++++++++++ 5 files changed, 281 insertions(+), 34 deletions(-) create mode 100644 dotnet/mcpb/Core/ToolsListComparer.cs diff --git a/dotnet/mcpb.Tests/MetaFieldTests.cs b/dotnet/mcpb.Tests/MetaFieldTests.cs index ccfd062..65afe2e 100644 --- a/dotnet/mcpb.Tests/MetaFieldTests.cs +++ b/dotnet/mcpb.Tests/MetaFieldTests.cs @@ -2,6 +2,7 @@ using System.Text.Json; using Mcpb.Core; using Mcpb.Json; +using ModelContextProtocol.Protocol; using Xunit; namespace Mcpb.Tests; @@ -168,31 +169,31 @@ public void StaticResponses_ContainInputAndOutputSchemas() var toolsListResult = new McpbToolsListResult { - Tools = new List + Tools = new List { - new + new Tool { - name = "search_tool", - description = "A search tool", - inputSchema = new - { - type = "object", - properties = new - { - query = new { type = "string", description = "Search query" }, - maxResults = new { type = "number", description = "Max results" } - }, - required = new[] { "query" } - }, - outputSchema = new - { - type = "object", - properties = new - { - results = new { type = "array" }, - count = new { type = "number" } - } - } + Name = "search_tool", + Description = "A search tool", + InputSchema = JsonDocument.Parse(""" + { + "type": "object", + "properties": { + "query": { "type": "string", "description": "Search query" }, + "maxResults": { "type": "number", "description": "Max results" } + }, + "required": ["query"] + } + """).RootElement, +OutputSchema = JsonDocument.Parse(""" + { + "type": "object", + "properties": { + "results": { "type": "array" }, + "count": { "type": "number" } + } + } + """).RootElement } } }; diff --git a/dotnet/mcpb/Commands/ManifestCommandHelpers.cs b/dotnet/mcpb/Commands/ManifestCommandHelpers.cs index 36c1c3c..cee238e 100644 --- a/dotnet/mcpb/Commands/ManifestCommandHelpers.cs +++ b/dotnet/mcpb/Commands/ManifestCommandHelpers.cs @@ -253,17 +253,12 @@ internal static async Task DiscoverCapabilitiesAsync( // Filter out null properties to match JsonIgnoreCondition.WhenWritingNull behavior try { - var toolsList = new List(); + var toolsList = new List(); foreach (var tool in tools) { - // Serialize the tool and parse to JsonElement - var json = JsonSerializer.Serialize(tool.ProtocolTool); - var element = JsonSerializer.Deserialize(json); - - // Filter out null properties recursively - var filtered = FilterNullProperties(element); - toolsList.Add(filtered); + toolsList.Add(tool.ProtocolTool); } + toolsListResponse = new McpbToolsListResult { Tools = toolsList }; } catch (Exception ex) diff --git a/dotnet/mcpb/Commands/PackCommand.cs b/dotnet/mcpb/Commands/PackCommand.cs index 25aba29..7c7d118 100644 --- a/dotnet/mcpb/Commands/PackCommand.cs +++ b/dotnet/mcpb/Commands/PackCommand.cs @@ -161,11 +161,18 @@ public static Command Create() staticResponses.Initialize = initDict; } + if (discoveredToolsListResponse != null) { + if (!ToolsListComparer.AreEqual(staticResponses.ToolsList, discoveredToolsListResponse)) + { + mismatchOccurred = true; + } + // Store the entire tools/list response object as-is staticResponses.ToolsList = discoveredToolsListResponse; } + windowsMeta.StaticResponses = staticResponses; SetWindowsMeta(manifest, windowsMeta); Console.WriteLine("Updated _meta static_responses to match discovered results."); @@ -354,7 +361,7 @@ private static McpbWindowsMeta GetOrCreateWindowsMeta(McpbManifest manifest) // Try to deserialize the dictionary to McpbWindowsMeta try { - var json = JsonSerializer.Serialize(windowsMetaDict); + var json = JsonSerializer.Serialize(windowsMetaDict, McpbJsonContext.WriteOptions); return JsonSerializer.Deserialize(json) ?? new McpbWindowsMeta(); } catch @@ -368,7 +375,7 @@ private static void SetWindowsMeta(McpbManifest manifest, McpbWindowsMeta window manifest.Meta ??= new Dictionary>(); // Serialize to dictionary - var json = JsonSerializer.Serialize(windowsMeta); + var json = JsonSerializer.Serialize(windowsMeta, McpbJsonContext.WriteOptions); var dict = JsonSerializer.Deserialize>(json) ?? new Dictionary(); manifest.Meta["com.microsoft.windows"] = dict; diff --git a/dotnet/mcpb/Core/ManifestModels.cs b/dotnet/mcpb/Core/ManifestModels.cs index e4e8aa9..57b4bf1 100644 --- a/dotnet/mcpb/Core/ManifestModels.cs +++ b/dotnet/mcpb/Core/ManifestModels.cs @@ -1,3 +1,4 @@ +using ModelContextProtocol.Protocol; using System.Text.Json.Serialization; namespace Mcpb.Core; @@ -84,7 +85,7 @@ public class McpbInitializeResult public class McpbToolsListResult { - [JsonPropertyName("tools")] public List? Tools { get; set; } + [JsonPropertyName("tools")] public List? Tools { get; set; } } public class McpbStaticResponses diff --git a/dotnet/mcpb/Core/ToolsListComparer.cs b/dotnet/mcpb/Core/ToolsListComparer.cs new file mode 100644 index 0000000..486cbec --- /dev/null +++ b/dotnet/mcpb/Core/ToolsListComparer.cs @@ -0,0 +1,243 @@ +using ModelContextProtocol.Protocol; + +namespace Mcpb.Core; + +/// +/// Compares static_response tools/list (from the manifest) against the runtime tools/list +/// response from the server, ensuring the server's actual tools match what was declared. +/// +public static class ToolsListComparer +{ + /// + /// Validates that the server's tools/list response matches the static_response tools declared in the manifest. + /// Returns false on the first mismatch found. + /// + /// The tools/list declared in the manifest's static_response. + /// The result retrieved from the server's tools/list response. + /// true if the server tools/list response matches the static_response; otherwise, false. + public static bool AreEqual(McpbToolsListResult? manifestToolsListResult, McpbToolsListResult? serverToolsListResult) + { + if ((manifestToolsListResult is null && serverToolsListResult is not null) || (manifestToolsListResult is not null && serverToolsListResult is null)) + { + return false; + } + + if (manifestToolsListResult is not null) + { + List manifestTools = manifestToolsListResult.Tools ?? new List(); + List serverTools = serverToolsListResult?.Tools ?? new List(); + + int manifestCount = manifestTools.Count; + int serverCount = serverTools.Count; + + if (manifestCount != serverCount) + { + return false; + } + + foreach (Tool manifestTool in manifestTools) + { + bool foundMatch = false; + + foreach (Tool serverTool in serverTools!) + { + if (string.Equals(manifestTool.Name, serverTool.Name, StringComparison.OrdinalIgnoreCase)) + { + if (CompareToolProperties(manifestTool, serverTool)) + { + foundMatch = true; + break; + } + } + } + + if (!foundMatch) + { + return false; + } + } + } + + return true; + } + + private static bool CompareToolProperties( + Tool manifestTool, + Tool serverTool) + { + string toolName = manifestTool.Name; + + // Compare description (optional — only check if static_response has one). + if (manifestTool.Description is not null) + { + string? serverToolDescription = serverTool.Description; + if (!string.Equals(manifestTool.Description, serverToolDescription, StringComparison.Ordinal)) + { + return false; + } + } + + // Compare inputSchema. + if (!CompareSchema(manifestTool.InputSchema, serverTool.InputSchema)) + { + return false; + } + + // Compare outputSchema. + if (!CompareSchema(manifestTool.OutputSchema, serverTool.OutputSchema)) + { + return false; + } + + return true; + } + + private static bool CompareSchema(JsonElement? manifestSchema, JsonElement? serverSchema) + { + bool manifestDefinesSchema = manifestSchema.HasValue && manifestSchema.Value.ValueKind != JsonValueKind.Undefined; + bool serverDefinesSchema = serverSchema.HasValue && serverSchema.Value.ValueKind != JsonValueKind.Undefined; + + if (manifestDefinesSchema != serverDefinesSchema) + { + return false; + } + + if (manifestDefinesSchema && + !JsonElementDeepEquals(manifestSchema!.Value, serverSchema!.Value, out string? mismatchPath)) + { + return false; + } + + return true; + } + + /// + /// Performs a deep comparison of two values. + /// + /// The first element to compare. + /// The second element to compare. + /// When the elements differ, the JSON path where the mismatch was found. + /// true if the elements are deeply equal; otherwise, false. + internal static bool JsonElementDeepEquals(JsonElement element1, JsonElement element2, out string? mismatchPath) + { + return JsonElementDeepEqualsCore(element1, element2, string.Empty, out mismatchPath); + } + + private static bool JsonElementDeepEqualsCore(JsonElement element1, JsonElement element2, string currentPath, out string? mismatchPath) + { + if (element1.ValueKind != element2.ValueKind) + { + mismatchPath = currentPath; + return false; + } + + switch (element1.ValueKind) + { + case JsonValueKind.Object: + int count1 = 0; + foreach (JsonProperty _ in element1.EnumerateObject()) + { + count1++; + } + + int count2 = 0; + foreach (JsonProperty _ in element2.EnumerateObject()) + { + count2++; + } + + if (count1 != count2) + { + mismatchPath = currentPath; + return false; + } + + foreach (JsonProperty property1 in element1.EnumerateObject()) + { + string propertyPath = string.IsNullOrEmpty(currentPath) ? property1.Name : $"{currentPath}.{property1.Name}"; + + if (!element2.TryGetProperty(property1.Name, out JsonElement property2Value)) + { + mismatchPath = propertyPath; + return false; + } + + if (!JsonElementDeepEqualsCore(property1.Value, property2Value, propertyPath, out mismatchPath)) + { + return false; + } + } + + mismatchPath = null; + return true; + + case JsonValueKind.Array: + int arrayLength1 = 0; + foreach (JsonElement _ in element1.EnumerateArray()) + { + arrayLength1++; + } + + int arrayLength2 = 0; + foreach (JsonElement _ in element2.EnumerateArray()) + { + arrayLength2++; + } + + if (arrayLength1 != arrayLength2) + { + mismatchPath = currentPath; + return false; + } + + int index = 0; + using (JsonElement.ArrayEnumerator enumerator1 = element1.EnumerateArray()) + using (JsonElement.ArrayEnumerator enumerator2 = element2.EnumerateArray()) + { + while (enumerator1.MoveNext() && enumerator2.MoveNext()) + { + string arrayPath = $"{currentPath}[{index}]"; + if (!JsonElementDeepEqualsCore(enumerator1.Current, enumerator2.Current, arrayPath, out mismatchPath)) + { + return false; + } + + index++; + } + } + + mismatchPath = null; + return true; + + case JsonValueKind.String: + if (element1.GetString() != element2.GetString()) + { + mismatchPath = currentPath; + return false; + } + + mismatchPath = null; + return true; + + case JsonValueKind.Number: + if (element1.GetRawText() != element2.GetRawText()) + { + mismatchPath = currentPath; + return false; + } + + mismatchPath = null; + return true; + + case JsonValueKind.True: + case JsonValueKind.False: + case JsonValueKind.Null: + mismatchPath = null; + return true; + + default: + mismatchPath = currentPath; + return false; + } + } +} From 72b2670aca076fcaff81638017aa8ee0943d0551 Mon Sep 17 00:00:00 2001 From: Alberto Medina Gutierrez Date: Tue, 31 Mar 2026 15:01:49 -0700 Subject: [PATCH 2/7] Add unit tests --- dotnet/mcpb.Tests/ToolsListComparerTests.cs | 636 ++++++++++++++++++++ 1 file changed, 636 insertions(+) create mode 100644 dotnet/mcpb.Tests/ToolsListComparerTests.cs diff --git a/dotnet/mcpb.Tests/ToolsListComparerTests.cs b/dotnet/mcpb.Tests/ToolsListComparerTests.cs new file mode 100644 index 0000000..3c46c3b --- /dev/null +++ b/dotnet/mcpb.Tests/ToolsListComparerTests.cs @@ -0,0 +1,636 @@ +using Mcpb.Core; +using ModelContextProtocol.Protocol; +using System.Text.Json; +using Xunit; + +namespace Mcpb.Tests; + +public class ToolsListComparerTests +{ + [Fact] + public void AreEqual_BothNull_ReturnsTrue() + { + var result = ToolsListComparer.AreEqual(null, null); + Assert.True(result); + } + + [Fact] + public void AreEqual_ManifestNullServerNotNull_ReturnsFalse() + { + var serverToolsList = new McpbToolsListResult { Tools = new List() }; + var result = ToolsListComparer.AreEqual(null, serverToolsList); + Assert.False(result); + } + + [Fact] + public void AreEqual_ManifestNotNullServerNull_ReturnsFalse() + { + var manifestToolsList = new McpbToolsListResult { Tools = new List() }; + var result = ToolsListComparer.AreEqual(manifestToolsList, null); + Assert.False(result); + } + + [Fact] + public void AreEqual_EmptyToolsLists_ReturnsTrue() + { + var manifest = new McpbToolsListResult { Tools = new List() }; + var server = new McpbToolsListResult { Tools = new List() }; + var result = ToolsListComparer.AreEqual(manifest, server); + Assert.True(result); + } + + [Fact] + public void AreEqual_MultipleTools_ReturnsTrue() + { + var manifest = new McpbToolsListResult + { + Tools = new List + { + new Tool { Name = "tool1", Description = "First tool" }, + new Tool { Name = "tool2", Description = "Second tool" }, + new Tool { Name = "tool3", Description = "Third tool" } + } + }; + var server = new McpbToolsListResult + { + Tools = new List + { + new Tool { Name = "tool1", Description = "First tool" }, + new Tool { Name = "tool2", Description = "Second tool" }, + new Tool { Name = "tool3", Description = "Third tool" } + } + }; + var result = ToolsListComparer.AreEqual(manifest, server); + Assert.True(result); + } + + [Fact] + public void AreEqual_MultipleToolsDifferentOrder_ReturnsTrue() + { + var manifest = new McpbToolsListResult + { + Tools = new List + { + new Tool { Name = "tool1", Description = "First tool" }, + new Tool { Name = "tool2", Description = "Second tool" } + } + }; + var server = new McpbToolsListResult + { + Tools = new List + { + new Tool { Name = "tool2", Description = "Second tool" }, + new Tool { Name = "tool1", Description = "First tool" } + } + }; + var result = ToolsListComparer.AreEqual(manifest, server); + Assert.True(result); + } + + [Fact] + public void AreEqual_DifferentToolCount_ReturnsFalse() + { + var manifest = new McpbToolsListResult + { + Tools = new List + { + new Tool { Name = "tool1" } + } + }; + var server = new McpbToolsListResult + { + Tools = new List + { + new Tool { Name = "tool1" }, + new Tool { Name = "tool2" } + } + }; + var result = ToolsListComparer.AreEqual(manifest, server); + Assert.False(result); + } + + [Fact] + public void AreEqual_SameToolsWithoutSchemas_ReturnsTrue() + { + var manifest = new McpbToolsListResult + { + Tools = new List + { + new Tool { Name = "tool1", Description = "Test tool" } + } + }; + var server = new McpbToolsListResult + { + Tools = new List + { + new Tool { Name = "tool1", Description = "Test tool" } + } + }; + var result = ToolsListComparer.AreEqual(manifest, server); + Assert.True(result); + } + + [Fact] + public void AreEqual_DifferentToolDescription_ReturnsFalse() + { + var manifest = new McpbToolsListResult + { + Tools = new List + { + new Tool { Name = "tool1", Description = "Description 1" } + } + }; + var server = new McpbToolsListResult + { + Tools = new List + { + new Tool { Name = "tool1", Description = "Description 2" } + } + }; + var result = ToolsListComparer.AreEqual(manifest, server); + Assert.False(result); + } + + [Fact] + public void AreEqual_MatchingInputSchema_ReturnsTrue() + { + var schemaJson = @"{ + ""type"": ""object"", + ""properties"": { + ""param1"": { ""type"": ""string"" }, + ""param2"": { ""type"": ""number"" } + }, + ""required"": [""param1""] + }"; + var schema = JsonDocument.Parse(schemaJson).RootElement; + + var manifest = new McpbToolsListResult + { + Tools = new List + { + new Tool { Name = "tool1", InputSchema = schema } + } + }; + var server = new McpbToolsListResult + { + Tools = new List + { + new Tool { Name = "tool1", InputSchema = schema.Clone() } + } + }; + var result = ToolsListComparer.AreEqual(manifest, server); + Assert.True(result); + } + + [Fact] + public void AreEqual_DifferentInputSchemaPropertyTypes_ReturnsFalse() + { + var manifestSchemaJson = @"{ + ""type"": ""object"", + ""properties"": { + ""param1"": { ""type"": ""string"" } + } + }"; + var serverSchemaJson = @"{ + ""type"": ""object"", + ""properties"": { + ""param1"": { ""type"": ""number"" } + } + }"; + + var manifest = new McpbToolsListResult + { + Tools = new List + { + new Tool + { + Name = "tool1", + InputSchema = JsonDocument.Parse(manifestSchemaJson).RootElement + } + } + }; + var server = new McpbToolsListResult + { + Tools = new List + { + new Tool + { + Name = "tool1", + InputSchema = JsonDocument.Parse(serverSchemaJson).RootElement + } + } + }; + var result = ToolsListComparer.AreEqual(manifest, server); + Assert.False(result); + } + + [Fact] + public void AreEqual_DifferentInputSchemaPropertyCount_ReturnsFalse() + { + var manifestSchemaJson = @"{ + ""type"": ""object"", + ""properties"": { + ""param1"": { ""type"": ""string"" } + } + }"; + var serverSchemaJson = @"{ + ""type"": ""object"", + ""properties"": { + ""param1"": { ""type"": ""string"" }, + ""param2"": { ""type"": ""number"" } + } + }"; + + var manifest = new McpbToolsListResult + { + Tools = new List + { + new Tool + { + Name = "tool1", + InputSchema = JsonDocument.Parse(manifestSchemaJson).RootElement + } + } + }; + var server = new McpbToolsListResult + { + Tools = new List + { + new Tool + { + Name = "tool1", + InputSchema = JsonDocument.Parse(serverSchemaJson).RootElement + } + } + }; + var result = ToolsListComparer.AreEqual(manifest, server); + Assert.False(result); + } + + [Fact] + public void AreEqual_MatchingOutputSchema_ReturnsTrue() + { + var schemaJson = @"{ + ""type"": ""object"", + ""properties"": { + ""result"": { ""type"": ""string"" } + } + }"; + var schema = JsonDocument.Parse(schemaJson).RootElement; + + var manifest = new McpbToolsListResult + { + Tools = new List + { + new Tool { Name = "tool1", OutputSchema = schema } + } + }; + var server = new McpbToolsListResult + { + Tools = new List + { + new Tool { Name = "tool1", OutputSchema = schema.Clone() } + } + }; + var result = ToolsListComparer.AreEqual(manifest, server); + Assert.True(result); + } + + [Fact] + public void AreEqual_DifferentOutputSchema_ReturnsFalse() + { + var manifestSchemaJson = @"{ + ""type"": ""object"", + ""properties"": { + ""result"": { ""type"": ""string"" } + } + }"; + var serverSchemaJson = @"{ + ""type"": ""object"", + ""properties"": { + ""result"": { ""type"": ""number"" } + } + }"; + + var manifest = new McpbToolsListResult + { + Tools = new List + { + new Tool + { + Name = "tool1", + OutputSchema = JsonDocument.Parse(manifestSchemaJson).RootElement + } + } + }; + var server = new McpbToolsListResult + { + Tools = new List + { + new Tool + { + Name = "tool1", + OutputSchema = JsonDocument.Parse(serverSchemaJson).RootElement + } + } + }; + var result = ToolsListComparer.AreEqual(manifest, server); + Assert.False(result); + } + + [Fact] + public void AreEqual_BothSchemasNotSet_ReturnsTrue() + { + var manifest = new McpbToolsListResult + { + Tools = new List + { + new Tool { Name = "tool1" } + } + }; + var server = new McpbToolsListResult + { + Tools = new List + { + new Tool { Name = "tool1" } + } + }; + var result = ToolsListComparer.AreEqual(manifest, server); + Assert.True(result); + } + + [Fact] + public void AreEqual_ComplexNestedSchema_ReturnsTrue() + { + var schemaJson = @"{ + ""type"": ""object"", + ""properties"": { + ""nested"": { + ""type"": ""object"", + ""properties"": { + ""value"": { ""type"": ""string"" }, + ""count"": { ""type"": ""number"" } + } + }, + ""array"": { + ""type"": ""array"", + ""items"": { ""type"": ""string"" } + } + }, + ""required"": [""nested""] + }"; + var schema = JsonDocument.Parse(schemaJson).RootElement; + + var manifest = new McpbToolsListResult + { + Tools = new List + { + new Tool { Name = "tool1", InputSchema = schema } + } + }; + var server = new McpbToolsListResult + { + Tools = new List + { + new Tool { Name = "tool1", InputSchema = schema.Clone() } + } + }; + var result = ToolsListComparer.AreEqual(manifest, server); + Assert.True(result); + } + + [Fact] + public void AreEqual_CaseInsensitiveToolName_ReturnsTrue() + { + var manifest = new McpbToolsListResult + { + Tools = new List + { + new Tool { Name = "ToolOne" } + } + }; + var server = new McpbToolsListResult + { + Tools = new List + { + new Tool { Name = "toolone" } + } + }; + var result = ToolsListComparer.AreEqual(manifest, server); + Assert.True(result); + } + + [Fact] + public void JsonElementDeepEquals_SameStringValues_ReturnsTrue() + { + var json1 = JsonDocument.Parse(@"""test""").RootElement; + var json2 = JsonDocument.Parse(@"""test""").RootElement; + + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + + Assert.True(result); + Assert.Null(mismatchPath); + } + + [Fact] + public void JsonElementDeepEquals_DifferentStringValues_ReturnsFalse() + { + var json1 = JsonDocument.Parse(@"""test1""").RootElement; + var json2 = JsonDocument.Parse(@"""test2""").RootElement; + + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + + Assert.False(result); + Assert.Equal(string.Empty, mismatchPath); + } + + [Fact] + public void JsonElementDeepEquals_SameNumberValues_ReturnsTrue() + { + var json1 = JsonDocument.Parse(@"42").RootElement; + var json2 = JsonDocument.Parse(@"42").RootElement; + + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + + Assert.True(result); + Assert.Null(mismatchPath); + } + + [Fact] + public void JsonElementDeepEquals_DifferentNumberValues_ReturnsFalse() + { + var json1 = JsonDocument.Parse(@"42").RootElement; + var json2 = JsonDocument.Parse(@"43").RootElement; + + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + + Assert.False(result); + Assert.Equal(string.Empty, mismatchPath); + } + + [Fact] + public void JsonElementDeepEquals_SameBooleanValues_ReturnsTrue() + { + var json1 = JsonDocument.Parse(@"true").RootElement; + var json2 = JsonDocument.Parse(@"true").RootElement; + + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + + Assert.True(result); + Assert.Null(mismatchPath); + } + + [Fact] + public void JsonElementDeepEquals_NullValues_ReturnsTrue() + { + var json1 = JsonDocument.Parse(@"null").RootElement; + var json2 = JsonDocument.Parse(@"null").RootElement; + + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + + Assert.True(result); + Assert.Null(mismatchPath); + } + + [Fact] + public void JsonElementDeepEquals_DifferentValueKinds_ReturnsFalse() + { + var json1 = JsonDocument.Parse(@"""test""").RootElement; + var json2 = JsonDocument.Parse(@"42").RootElement; + + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + + Assert.False(result); + Assert.Equal(string.Empty, mismatchPath); + } + + [Fact] + public void JsonElementDeepEquals_SameObjects_ReturnsTrue() + { + var json1 = JsonDocument.Parse(@"{""key"": ""value"", ""num"": 42}").RootElement; + var json2 = JsonDocument.Parse(@"{""key"": ""value"", ""num"": 42}").RootElement; + + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + + Assert.True(result); + Assert.Null(mismatchPath); + } + + [Fact] + public void JsonElementDeepEquals_ObjectsWithDifferentPropertyCount_ReturnsFalse() + { + var json1 = JsonDocument.Parse(@"{""key"": ""value""}").RootElement; + var json2 = JsonDocument.Parse(@"{""key"": ""value"", ""extra"": ""prop""}").RootElement; + + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + + Assert.False(result); + Assert.Equal(string.Empty, mismatchPath); + } + + [Fact] + public void JsonElementDeepEquals_ObjectsMissingProperty_ReturnsFalse() + { + var json1 = JsonDocument.Parse(@"{""key1"": ""value""}").RootElement; + var json2 = JsonDocument.Parse(@"{""key2"": ""value""}").RootElement; + + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + + Assert.False(result); + Assert.Equal("key1", mismatchPath); + } + + [Fact] + public void JsonElementDeepEquals_NestedObjectsDifferent_ReturnsFalseWithPath() + { + var json1 = JsonDocument.Parse(@"{""outer"": {""inner"": ""value1""}}").RootElement; + var json2 = JsonDocument.Parse(@"{""outer"": {""inner"": ""value2""}}").RootElement; + + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + + Assert.False(result); + Assert.Equal("outer.inner", mismatchPath); + } + + [Fact] + public void JsonElementDeepEquals_SameArrays_ReturnsTrue() + { + var json1 = JsonDocument.Parse(@"[1, 2, 3]").RootElement; + var json2 = JsonDocument.Parse(@"[1, 2, 3]").RootElement; + + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + + Assert.True(result); + Assert.Null(mismatchPath); + } + + [Fact] + public void JsonElementDeepEquals_DifferentArrayLengths_ReturnsFalse() + { + var json1 = JsonDocument.Parse(@"[1, 2]").RootElement; + var json2 = JsonDocument.Parse(@"[1, 2, 3]").RootElement; + + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + + Assert.False(result); + Assert.Equal(string.Empty, mismatchPath); + } + + [Fact] + public void JsonElementDeepEquals_ArrayElementsDifferent_ReturnsFalseWithPath() + { + var json1 = JsonDocument.Parse(@"[1, 2, 3]").RootElement; + var json2 = JsonDocument.Parse(@"[1, 5, 3]").RootElement; + + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + + Assert.False(result); + Assert.Equal("[1]", mismatchPath); + } + + [Fact] + public void AreEqual_BothInputAndOutputSchemas_ReturnsTrue() + { + var inputSchemaJson = @"{ + ""type"": ""object"", + ""properties"": { + ""input"": { ""type"": ""string"" } + } + }"; + var outputSchemaJson = @"{ + ""type"": ""object"", + ""properties"": { + ""output"": { ""type"": ""string"" } + } + }"; + + var manifest = new McpbToolsListResult + { + Tools = new List + { + new Tool + { + Name = "tool1", + Description = "A test tool", + InputSchema = JsonDocument.Parse(inputSchemaJson).RootElement, + OutputSchema = JsonDocument.Parse(outputSchemaJson).RootElement + } + } + }; + var server = new McpbToolsListResult + { + Tools = new List + { + new Tool + { + Name = "tool1", + Description = "A test tool", + InputSchema = JsonDocument.Parse(inputSchemaJson).RootElement, + OutputSchema = JsonDocument.Parse(outputSchemaJson).RootElement + } + } + }; + var result = ToolsListComparer.AreEqual(manifest, server); + Assert.True(result); + } +} From 17dc072b7a2ccdc7ede1dd12d57648b73e34d751 Mon Sep 17 00:00:00 2001 From: Alberto Medina Gutierrez Date: Tue, 31 Mar 2026 17:01:23 -0700 Subject: [PATCH 3/7] Remove extra content --- dotnet/mcpb/Core/ToolsListComparer.cs | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/dotnet/mcpb/Core/ToolsListComparer.cs b/dotnet/mcpb/Core/ToolsListComparer.cs index 486cbec..b21d73e 100644 --- a/dotnet/mcpb/Core/ToolsListComparer.cs +++ b/dotnet/mcpb/Core/ToolsListComparer.cs @@ -65,8 +65,6 @@ private static bool CompareToolProperties( Tool manifestTool, Tool serverTool) { - string toolName = manifestTool.Name; - // Compare description (optional — only check if static_response has one). if (manifestTool.Description is not null) { @@ -103,7 +101,7 @@ private static bool CompareSchema(JsonElement? manifestSchema, JsonElement? serv } if (manifestDefinesSchema && - !JsonElementDeepEquals(manifestSchema!.Value, serverSchema!.Value, out string? mismatchPath)) + !JsonElementDeepEquals(manifestSchema!.Value, serverSchema!.Value)) { return false; } @@ -118,16 +116,15 @@ private static bool CompareSchema(JsonElement? manifestSchema, JsonElement? serv /// The second element to compare. /// When the elements differ, the JSON path where the mismatch was found. /// true if the elements are deeply equal; otherwise, false. - internal static bool JsonElementDeepEquals(JsonElement element1, JsonElement element2, out string? mismatchPath) + internal static bool JsonElementDeepEquals(JsonElement element1, JsonElement element2) { - return JsonElementDeepEqualsCore(element1, element2, string.Empty, out mismatchPath); + return JsonElementDeepEqualsCore(element1, element2, string.Empty); } - private static bool JsonElementDeepEqualsCore(JsonElement element1, JsonElement element2, string currentPath, out string? mismatchPath) + private static bool JsonElementDeepEqualsCore(JsonElement element1, JsonElement element2, string currentPath) { if (element1.ValueKind != element2.ValueKind) { - mismatchPath = currentPath; return false; } @@ -148,7 +145,6 @@ private static bool JsonElementDeepEqualsCore(JsonElement element1, JsonElement if (count1 != count2) { - mismatchPath = currentPath; return false; } @@ -158,17 +154,15 @@ private static bool JsonElementDeepEqualsCore(JsonElement element1, JsonElement if (!element2.TryGetProperty(property1.Name, out JsonElement property2Value)) { - mismatchPath = propertyPath; return false; } - if (!JsonElementDeepEqualsCore(property1.Value, property2Value, propertyPath, out mismatchPath)) + if (!JsonElementDeepEqualsCore(property1.Value, property2Value, propertyPath)) { return false; } } - mismatchPath = null; return true; case JsonValueKind.Array: @@ -186,7 +180,6 @@ private static bool JsonElementDeepEqualsCore(JsonElement element1, JsonElement if (arrayLength1 != arrayLength2) { - mismatchPath = currentPath; return false; } @@ -197,7 +190,7 @@ private static bool JsonElementDeepEqualsCore(JsonElement element1, JsonElement while (enumerator1.MoveNext() && enumerator2.MoveNext()) { string arrayPath = $"{currentPath}[{index}]"; - if (!JsonElementDeepEqualsCore(enumerator1.Current, enumerator2.Current, arrayPath, out mismatchPath)) + if (!JsonElementDeepEqualsCore(enumerator1.Current, enumerator2.Current, arrayPath)) { return false; } @@ -206,37 +199,30 @@ private static bool JsonElementDeepEqualsCore(JsonElement element1, JsonElement } } - mismatchPath = null; return true; case JsonValueKind.String: if (element1.GetString() != element2.GetString()) { - mismatchPath = currentPath; return false; } - mismatchPath = null; return true; case JsonValueKind.Number: if (element1.GetRawText() != element2.GetRawText()) { - mismatchPath = currentPath; return false; } - mismatchPath = null; return true; case JsonValueKind.True: case JsonValueKind.False: case JsonValueKind.Null: - mismatchPath = null; return true; default: - mismatchPath = currentPath; return false; } } From 18cbe11c0d2e1f973c1aa7b624d28190582d2ec6 Mon Sep 17 00:00:00 2001 From: almedina-ms <35784165+almedina-ms@users.noreply.github.com> Date: Tue, 31 Mar 2026 17:02:02 -0700 Subject: [PATCH 4/7] Update dotnet/mcpb/Core/ToolsListComparer.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- dotnet/mcpb/Core/ToolsListComparer.cs | 28 ++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/dotnet/mcpb/Core/ToolsListComparer.cs b/dotnet/mcpb/Core/ToolsListComparer.cs index b21d73e..d1a896b 100644 --- a/dotnet/mcpb/Core/ToolsListComparer.cs +++ b/dotnet/mcpb/Core/ToolsListComparer.cs @@ -209,10 +209,32 @@ private static bool JsonElementDeepEqualsCore(JsonElement element1, JsonElement return true; - case JsonValueKind.Number: - if (element1.GetRawText() != element2.GetRawText()) + // Compare numbers by value rather than by their raw JSON text to avoid + // treating numerically equivalent values with different formatting as mismatches. + if (element1.TryGetDecimal(out decimal decimal1) && element2.TryGetDecimal(out decimal decimal2)) { - return false; + if (decimal1 != decimal2) + { + mismatchPath = currentPath; + return false; + } + } + else if (element1.TryGetDouble(out double double1) && element2.TryGetDouble(out double double2)) + { + if (double1 != double2) + { + mismatchPath = currentPath; + return false; + } + } + else + { + // Fallback to raw text comparison if numeric parsing is not possible. + if (element1.GetRawText() != element2.GetRawText()) + { + mismatchPath = currentPath; + return false; + } } return true; From 93001e5aefd1accc330266128f85bb73f32af326 Mon Sep 17 00:00:00 2001 From: Alberto Medina Gutierrez Date: Wed, 1 Apr 2026 12:14:00 -0700 Subject: [PATCH 5/7] Fix look up and fix bad tool name assumption --- dotnet/mcpb.Tests/ToolsListComparerTests.cs | 42 +++++--------- dotnet/mcpb/Core/ToolsListComparer.cs | 64 +++++++++------------ 2 files changed, 42 insertions(+), 64 deletions(-) diff --git a/dotnet/mcpb.Tests/ToolsListComparerTests.cs b/dotnet/mcpb.Tests/ToolsListComparerTests.cs index 3c46c3b..95878f7 100644 --- a/dotnet/mcpb.Tests/ToolsListComparerTests.cs +++ b/dotnet/mcpb.Tests/ToolsListComparerTests.cs @@ -426,10 +426,9 @@ public void JsonElementDeepEquals_SameStringValues_ReturnsTrue() var json1 = JsonDocument.Parse(@"""test""").RootElement; var json2 = JsonDocument.Parse(@"""test""").RootElement; - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); Assert.True(result); - Assert.Null(mismatchPath); } [Fact] @@ -438,10 +437,9 @@ public void JsonElementDeepEquals_DifferentStringValues_ReturnsFalse() var json1 = JsonDocument.Parse(@"""test1""").RootElement; var json2 = JsonDocument.Parse(@"""test2""").RootElement; - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); Assert.False(result); - Assert.Equal(string.Empty, mismatchPath); } [Fact] @@ -450,10 +448,9 @@ public void JsonElementDeepEquals_SameNumberValues_ReturnsTrue() var json1 = JsonDocument.Parse(@"42").RootElement; var json2 = JsonDocument.Parse(@"42").RootElement; - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); Assert.True(result); - Assert.Null(mismatchPath); } [Fact] @@ -462,10 +459,9 @@ public void JsonElementDeepEquals_DifferentNumberValues_ReturnsFalse() var json1 = JsonDocument.Parse(@"42").RootElement; var json2 = JsonDocument.Parse(@"43").RootElement; - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); Assert.False(result); - Assert.Equal(string.Empty, mismatchPath); } [Fact] @@ -474,10 +470,9 @@ public void JsonElementDeepEquals_SameBooleanValues_ReturnsTrue() var json1 = JsonDocument.Parse(@"true").RootElement; var json2 = JsonDocument.Parse(@"true").RootElement; - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); Assert.True(result); - Assert.Null(mismatchPath); } [Fact] @@ -486,10 +481,9 @@ public void JsonElementDeepEquals_NullValues_ReturnsTrue() var json1 = JsonDocument.Parse(@"null").RootElement; var json2 = JsonDocument.Parse(@"null").RootElement; - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); Assert.True(result); - Assert.Null(mismatchPath); } [Fact] @@ -498,10 +492,9 @@ public void JsonElementDeepEquals_DifferentValueKinds_ReturnsFalse() var json1 = JsonDocument.Parse(@"""test""").RootElement; var json2 = JsonDocument.Parse(@"42").RootElement; - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); Assert.False(result); - Assert.Equal(string.Empty, mismatchPath); } [Fact] @@ -510,10 +503,9 @@ public void JsonElementDeepEquals_SameObjects_ReturnsTrue() var json1 = JsonDocument.Parse(@"{""key"": ""value"", ""num"": 42}").RootElement; var json2 = JsonDocument.Parse(@"{""key"": ""value"", ""num"": 42}").RootElement; - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); Assert.True(result); - Assert.Null(mismatchPath); } [Fact] @@ -522,10 +514,9 @@ public void JsonElementDeepEquals_ObjectsWithDifferentPropertyCount_ReturnsFalse var json1 = JsonDocument.Parse(@"{""key"": ""value""}").RootElement; var json2 = JsonDocument.Parse(@"{""key"": ""value"", ""extra"": ""prop""}").RootElement; - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); Assert.False(result); - Assert.Equal(string.Empty, mismatchPath); } [Fact] @@ -534,10 +525,9 @@ public void JsonElementDeepEquals_ObjectsMissingProperty_ReturnsFalse() var json1 = JsonDocument.Parse(@"{""key1"": ""value""}").RootElement; var json2 = JsonDocument.Parse(@"{""key2"": ""value""}").RootElement; - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); Assert.False(result); - Assert.Equal("key1", mismatchPath); } [Fact] @@ -546,10 +536,9 @@ public void JsonElementDeepEquals_NestedObjectsDifferent_ReturnsFalseWithPath() var json1 = JsonDocument.Parse(@"{""outer"": {""inner"": ""value1""}}").RootElement; var json2 = JsonDocument.Parse(@"{""outer"": {""inner"": ""value2""}}").RootElement; - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); Assert.False(result); - Assert.Equal("outer.inner", mismatchPath); } [Fact] @@ -558,10 +547,9 @@ public void JsonElementDeepEquals_SameArrays_ReturnsTrue() var json1 = JsonDocument.Parse(@"[1, 2, 3]").RootElement; var json2 = JsonDocument.Parse(@"[1, 2, 3]").RootElement; - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); Assert.True(result); - Assert.Null(mismatchPath); } [Fact] @@ -570,10 +558,9 @@ public void JsonElementDeepEquals_DifferentArrayLengths_ReturnsFalse() var json1 = JsonDocument.Parse(@"[1, 2]").RootElement; var json2 = JsonDocument.Parse(@"[1, 2, 3]").RootElement; - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); Assert.False(result); - Assert.Equal(string.Empty, mismatchPath); } [Fact] @@ -582,10 +569,9 @@ public void JsonElementDeepEquals_ArrayElementsDifferent_ReturnsFalseWithPath() var json1 = JsonDocument.Parse(@"[1, 2, 3]").RootElement; var json2 = JsonDocument.Parse(@"[1, 5, 3]").RootElement; - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2, out string? mismatchPath); + var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); Assert.False(result); - Assert.Equal("[1]", mismatchPath); } [Fact] diff --git a/dotnet/mcpb/Core/ToolsListComparer.cs b/dotnet/mcpb/Core/ToolsListComparer.cs index d1a896b..6eac81c 100644 --- a/dotnet/mcpb/Core/ToolsListComparer.cs +++ b/dotnet/mcpb/Core/ToolsListComparer.cs @@ -35,23 +35,37 @@ public static bool AreEqual(McpbToolsListResult? manifestToolsListResult, McpbTo return false; } - foreach (Tool manifestTool in manifestTools) + // Key names must be unique, so we can build a dictionary for the server tools to speed up lookups. + // The dictionary contains a tuple of the Tool and a bool indicating whether it has been matched to + // a manifest tool, to detect duplicates. + Dictionary serverToolsByName = new(StringComparer.OrdinalIgnoreCase); + + foreach (Tool serverTool in serverTools!) { - bool foundMatch = false; + if (serverToolsByName.ContainsKey(serverTool.Name)) + { + return false; + } + serverToolsByName[serverTool.Name] = (serverTool, false); + } - foreach (Tool serverTool in serverTools!) + foreach (Tool manifestTool in manifestTools) + { + if (serverToolsByName.TryGetValue(manifestTool.Name, out var serverToolEntry)) { - if (string.Equals(manifestTool.Name, serverTool.Name, StringComparison.OrdinalIgnoreCase)) + if (serverToolEntry.IsMatched) // Much clearer than Item2 { - if (CompareToolProperties(manifestTool, serverTool)) - { - foundMatch = true; - break; - } + return false; } - } - if (!foundMatch) + if (!CompareToolProperties(manifestTool, serverToolEntry.Tool)) // Clearer than Item1 + { + return false; + } + + serverToolsByName[manifestTool.Name] = (serverToolEntry.Tool, true); + } + else { return false; } @@ -209,32 +223,10 @@ private static bool JsonElementDeepEqualsCore(JsonElement element1, JsonElement return true; - // Compare numbers by value rather than by their raw JSON text to avoid - // treating numerically equivalent values with different formatting as mismatches. - if (element1.TryGetDecimal(out decimal decimal1) && element2.TryGetDecimal(out decimal decimal2)) - { - if (decimal1 != decimal2) - { - mismatchPath = currentPath; - return false; - } - } - else if (element1.TryGetDouble(out double double1) && element2.TryGetDouble(out double double2)) - { - if (double1 != double2) - { - mismatchPath = currentPath; - return false; - } - } - else + case JsonValueKind.Number: + if (element1.GetRawText() != element2.GetRawText()) { - // Fallback to raw text comparison if numeric parsing is not possible. - if (element1.GetRawText() != element2.GetRawText()) - { - mismatchPath = currentPath; - return false; - } + return false; } return true; From f28d6ef9fbdcae3441fe8f62949976d4d7bcde45 Mon Sep 17 00:00:00 2001 From: Alberto Medina Gutierrez Date: Wed, 1 Apr 2026 18:21:05 -0700 Subject: [PATCH 6/7] Add more unit tests and improve comparison --- .../mcpb.Tests/CliPackToolDiscoveryTests.cs | 270 ++++++++ dotnet/mcpb.Tests/ToolsListComparerTests.cs | 622 ------------------ .../mcpb/Commands/ManifestCommandHelpers.cs | 23 +- dotnet/mcpb/Commands/PackCommand.cs | 12 +- dotnet/mcpb/Core/ToolsListComparer.cs | 243 ------- 5 files changed, 298 insertions(+), 872 deletions(-) delete mode 100644 dotnet/mcpb.Tests/ToolsListComparerTests.cs delete mode 100644 dotnet/mcpb/Core/ToolsListComparer.cs diff --git a/dotnet/mcpb.Tests/CliPackToolDiscoveryTests.cs b/dotnet/mcpb.Tests/CliPackToolDiscoveryTests.cs index 7904200..879d78f 100644 --- a/dotnet/mcpb.Tests/CliPackToolDiscoveryTests.cs +++ b/dotnet/mcpb.Tests/CliPackToolDiscoveryTests.cs @@ -3,6 +3,7 @@ using Xunit; using System.IO; using System.Linq; +using Mcpb.Core; namespace Mcpb.Tests; @@ -220,4 +221,273 @@ public void Pack_Update_DoesNotEscapeApostrophes() Environment.SetEnvironmentVariable("MCPB_TOOL_DISCOVERY_JSON", null); } } + + [Fact] + public void Pack_ToolInputSchemaMismatch_OutputMentionsMismatch() + { + var dir = CreateTempDir(); + var manifestPath = Path.Combine(dir, "manifest.json"); + Directory.CreateDirectory(Path.Combine(dir, "server")); + File.WriteAllText(Path.Combine(dir, "server", "demo"), "binary"); + + var manifest = MakeManifest(new[] { "search" }); + manifest.Tools![0].Description = "Search tool"; + File.WriteAllText(manifestPath, JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)); + + // Manifest has no schema, but discovered tools/list has InputSchema + Environment.SetEnvironmentVariable("MCPB_TOOLS_LIST_DISCOVERY_JSON", + "{\"tools\": [{\"name\":\"search\",\"description\":\"Search tool\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"}}}}]}"); + try + { + var (code, stdout, stderr) = InvokeCli(dir, "pack", dir, "--update"); + Assert.Equal(0, code); + Assert.Contains("Tool list mismatch", stdout + stderr); + } + finally + { + Environment.SetEnvironmentVariable("MCPB_TOOLS_LIST_DISCOVERY_JSON", null); + } + } + + [Fact] + public void Pack_ToolInputSchemaMismatch_UpdateAddsSchema() + { + var dir = CreateTempDir(); + var manifestPath = Path.Combine(dir, "manifest.json"); + Directory.CreateDirectory(Path.Combine(dir, "server")); + File.WriteAllText(Path.Combine(dir, "server", "demo"), "binary"); + + var manifest = MakeManifest(new[] { "search" }); + File.WriteAllText(manifestPath, JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)); + + Environment.SetEnvironmentVariable("MCPB_TOOLS_LIST_DISCOVERY_JSON", + "{\"tools\": [ {\"name\":\"search\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"}},\"required\":[\"query\"]}}]}"); + try + { + var (code, stdout, stderr) = InvokeCli(dir, "pack", dir, "--update"); + Assert.Equal(0, code); + Assert.Contains("Updated manifest.json capabilities", stdout + stderr); + + var jsonText = File.ReadAllText(manifestPath); + Assert.Contains("\"inputSchema\"", jsonText); + Assert.Contains("\"query\"", jsonText); + } + finally + { + Environment.SetEnvironmentVariable("MCPB_TOOLS_LIST_DISCOVERY_JSON", null); + } + } + + [Fact] + public void Pack_ToolOutputSchemaMismatch_OutputMentionsMismatch() + { + var dir = CreateTempDir(); + var manifestPath = Path.Combine(dir, "manifest.json"); + Directory.CreateDirectory(Path.Combine(dir, "server")); + File.WriteAllText(Path.Combine(dir, "server", "demo"), "binary"); + + var manifest = MakeManifest(new[] { "search" }); + manifest.Tools![0].Description = "Search tool"; + File.WriteAllText(manifestPath, JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)); + + // Manifest has no outputSchema, but discovered tools/list has OutputSchema + Environment.SetEnvironmentVariable("MCPB_TOOLS_LIST_DISCOVERY_JSON", + "{\"tools\": [{\"name\":\"search\",\"description\":\"Search tool\",\"outputSchema\":{\"type\":\"object\",\"properties\":{\"results\":{\"type\":\"array\"}}}}]}"); + try + { + var (code, stdout, stderr) = InvokeCli(dir, "pack", dir, "--update"); + Assert.Equal(0, code); + Assert.Contains("Tool list mismatch", stdout + stderr); + } + finally + { + Environment.SetEnvironmentVariable("MCPB_TOOLS_LIST_DISCOVERY_JSON", null); + } + } + + [Fact] + public void Pack_ToolOutputSchemaMismatch_UpdateAddsSchema() + { + var dir = CreateTempDir(); + var manifestPath = Path.Combine(dir, "manifest.json"); + Directory.CreateDirectory(Path.Combine(dir, "server")); + File.WriteAllText(Path.Combine(dir, "server", "demo"), "binary"); + + var manifest = MakeManifest(new[] { "search" }); + File.WriteAllText(manifestPath, JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)); + + Environment.SetEnvironmentVariable("MCPB_TOOLS_LIST_DISCOVERY_JSON", + "{\"tools\": [{\"name\":\"search\",\"outputSchema\":{\"type\":\"object\",\"properties\":{\"results\":{\"type\":\"array\"}},\"required\":[\"results\"]}}]}"); + try + { + var (code, stdout, stderr) = InvokeCli(dir, "pack", dir, "--update"); + Assert.Equal(0, code); + Assert.Contains("Updated manifest.json capabilities", stdout + stderr); + + var jsonText = File.ReadAllText(manifestPath); + Assert.Contains("\"outputSchema\"", jsonText); + Assert.Contains("\"results\"", jsonText); + } + finally + { + Environment.SetEnvironmentVariable("MCPB_TOOLS_LIST_DISCOVERY_JSON", null); + } + } + + [Fact] + public void Pack_ToolInputAndOutputSchemaMismatch_UpdateAddsBothSchemas() + { + var dir = CreateTempDir(); + var manifestPath = Path.Combine(dir, "manifest.json"); + Directory.CreateDirectory(Path.Combine(dir, "server")); + File.WriteAllText(Path.Combine(dir, "server", "demo"), "binary"); + + var manifest = MakeManifest(new[] { "search" }); + File.WriteAllText(manifestPath, JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)); + + Environment.SetEnvironmentVariable("MCPB_TOOLS_LIST_DISCOVERY_JSON", + "{\"tools\": [{\"name\":\"search\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"}}},\"outputSchema\":{\"type\":\"object\",\"properties\":{\"results\":{\"type\":\"array\"}}}}]}"); + try + { + var (code, stdout, stderr) = InvokeCli(dir, "pack", dir, "--update"); + Assert.Equal(0, code); + Assert.Contains("Updated manifest.json capabilities", stdout + stderr); + + var jsonText = File.ReadAllText(manifestPath); + Assert.Contains("\"inputSchema\"", jsonText); + Assert.Contains("\"query\"", jsonText); + Assert.Contains("\"outputSchema\"", jsonText); + Assert.Contains("\"results\"", jsonText); + } + finally + { + Environment.SetEnvironmentVariable("MCPB_TOOLS_LIST_DISCOVERY_JSON", null); + } + } + + [Fact] + public void Pack_ToolDescriptionMismatch_UpdateRewritesDescription() + { + var dir = CreateTempDir(); + var manifestPath = Path.Combine(dir, "manifest.json"); + Directory.CreateDirectory(Path.Combine(dir, "server")); + File.WriteAllText(Path.Combine(dir, "server", "demo"), "binary"); + + var manifest = MakeManifest(new[] { "search" }); + manifest.Tools![0].Description = "Old description"; + File.WriteAllText(manifestPath, JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)); + + Environment.SetEnvironmentVariable("MCPB_TOOLS_LIST_DISCOVERY_JSON", + "{\"tools\": [{\"name\":\"search\",\"description\":\"New description\"}]}"); + try + { + var (code, stdout, stderr) = InvokeCli(dir, "pack", dir, "--update"); + Assert.Equal(0, code); + Assert.Contains("Updated manifest.json capabilities", stdout + stderr); + + var jsonText = File.ReadAllText(manifestPath); + var updated = JsonSerializer.Deserialize( + jsonText, + McpbJsonContext.Default.McpbManifest)!; + + Assert.NotNull(updated.Meta); + Assert.True(updated.Meta.TryGetValue("com.microsoft.windows", out var windowsMeta)); + Assert.True(windowsMeta.TryGetValue("static_responses", out object? staticResponsesValue)); + + JsonElement staticResponseElement = (JsonElement)staticResponsesValue; + var staticResponsesData = staticResponseElement.Deserialize(McpbJsonContext.Default.McpbStaticResponses); + + Assert.NotNull(staticResponsesData); + var toolsList = staticResponsesData.ToolsList; + Assert.NotNull(toolsList); + Assert.NotNull(toolsList.Tools); + var searchTool = toolsList.Tools.Single(t => t.Name == "search"); + Assert.Equal("New description", searchTool.Description); + } + finally + { + Environment.SetEnvironmentVariable("MCPB_TOOLS_LIST_DISCOVERY_JSON", null); + } + } + + [Fact] + public void Pack_ToolCompleteMetadataMismatch_UpdateSyncsAllProperties() + { + var dir = CreateTempDir(); + var manifestPath = Path.Combine(dir, "manifest.json"); + Directory.CreateDirectory(Path.Combine(dir, "server")); + File.WriteAllText(Path.Combine(dir, "server", "demo"), "binary"); + + var manifest = MakeManifest(new[] { "search" }); + manifest.Tools![0].Description = "Old description"; + File.WriteAllText(manifestPath, JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions)); + + Environment.SetEnvironmentVariable("MCPB_TOOLS_LIST_DISCOVERY_JSON", + "{\"tools\": [{\"name\":\"search\",\"description\":\"New description\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"}}},\"outputSchema\":{\"type\":\"object\",\"properties\":{\"results\":{\"type\":\"array\"}}}}]}"); + try + { + var (code, stdout, stderr) = InvokeCli(dir, "pack", dir, "--update"); + Assert.Equal(0, code); + Assert.Contains("Updated manifest.json capabilities", stdout + stderr); + + var jsonText = File.ReadAllText(manifestPath); + var updated = JsonSerializer.Deserialize( + jsonText, + McpbJsonContext.Default.McpbManifest)!; + + Assert.NotNull(updated.Meta); + Assert.True(updated.Meta.TryGetValue("com.microsoft.windows", out var windowsMeta)); + Assert.True(windowsMeta.TryGetValue("static_responses", out object? staticResponsesValue)); + + JsonElement staticResponseElement = (JsonElement)staticResponsesValue; + + var staticResponsesData = staticResponseElement.Deserialize(McpbJsonContext.Default.McpbStaticResponses); + + Assert.NotNull(staticResponsesData); + var toolsList = staticResponsesData.ToolsList; + + Assert.NotNull(toolsList); + Assert.NotNull(toolsList.Tools); + var searchTool = toolsList.Tools.Single(t => t.Name == "search"); + Assert.Equal("New description", searchTool.Description); + } + finally + { + Environment.SetEnvironmentVariable("MCPB_TOOLS_LIST_DISCOVERY_JSON", null); + } + } + + [Fact] + public void Pack_ExistingSchemaMismatch_UpdateReplacesSchema() + { + var dir = CreateTempDir(); + var manifestPath = Path.Combine(dir, "manifest.json"); + Directory.CreateDirectory(Path.Combine(dir, "server")); + File.WriteAllText(Path.Combine(dir, "server", "demo"), "binary"); + + var manifest = MakeManifest(new[] { "search" }); + // Manually add an old schema (simulating what would be in manifest) + var manifestJson = JsonSerializer.Serialize(manifest, McpbJsonContext.WriteOptions); + var manifestWithSchema = manifestJson.Replace( + "\"tools\": [{\"name\":\"search\"}]", + "\"tools\": [{\"name\":\"search\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"oldProp\":{\"type\":\"string\"}}}}]"); + File.WriteAllText(manifestPath, manifestWithSchema); + + Environment.SetEnvironmentVariable("MCPB_TOOLS_LIST_DISCOVERY_JSON", + "{\"tools\": [{\"name\":\"search\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"newProp\":{\"type\":\"number\"}}}}]}"); + try + { + var (code, stdout, stderr) = InvokeCli(dir, "pack", dir, "--update"); + Assert.Equal(0, code); + Assert.Contains("Updated manifest.json capabilities", stdout + stderr); + + var jsonText = File.ReadAllText(manifestPath); + Assert.Contains("\"newProp\"", jsonText); + Assert.DoesNotContain("\"oldProp\"", jsonText); + } + finally + { + Environment.SetEnvironmentVariable("MCPB_TOOLS_LIST_DISCOVERY_JSON", null); + } + } } diff --git a/dotnet/mcpb.Tests/ToolsListComparerTests.cs b/dotnet/mcpb.Tests/ToolsListComparerTests.cs deleted file mode 100644 index 95878f7..0000000 --- a/dotnet/mcpb.Tests/ToolsListComparerTests.cs +++ /dev/null @@ -1,622 +0,0 @@ -using Mcpb.Core; -using ModelContextProtocol.Protocol; -using System.Text.Json; -using Xunit; - -namespace Mcpb.Tests; - -public class ToolsListComparerTests -{ - [Fact] - public void AreEqual_BothNull_ReturnsTrue() - { - var result = ToolsListComparer.AreEqual(null, null); - Assert.True(result); - } - - [Fact] - public void AreEqual_ManifestNullServerNotNull_ReturnsFalse() - { - var serverToolsList = new McpbToolsListResult { Tools = new List() }; - var result = ToolsListComparer.AreEqual(null, serverToolsList); - Assert.False(result); - } - - [Fact] - public void AreEqual_ManifestNotNullServerNull_ReturnsFalse() - { - var manifestToolsList = new McpbToolsListResult { Tools = new List() }; - var result = ToolsListComparer.AreEqual(manifestToolsList, null); - Assert.False(result); - } - - [Fact] - public void AreEqual_EmptyToolsLists_ReturnsTrue() - { - var manifest = new McpbToolsListResult { Tools = new List() }; - var server = new McpbToolsListResult { Tools = new List() }; - var result = ToolsListComparer.AreEqual(manifest, server); - Assert.True(result); - } - - [Fact] - public void AreEqual_MultipleTools_ReturnsTrue() - { - var manifest = new McpbToolsListResult - { - Tools = new List - { - new Tool { Name = "tool1", Description = "First tool" }, - new Tool { Name = "tool2", Description = "Second tool" }, - new Tool { Name = "tool3", Description = "Third tool" } - } - }; - var server = new McpbToolsListResult - { - Tools = new List - { - new Tool { Name = "tool1", Description = "First tool" }, - new Tool { Name = "tool2", Description = "Second tool" }, - new Tool { Name = "tool3", Description = "Third tool" } - } - }; - var result = ToolsListComparer.AreEqual(manifest, server); - Assert.True(result); - } - - [Fact] - public void AreEqual_MultipleToolsDifferentOrder_ReturnsTrue() - { - var manifest = new McpbToolsListResult - { - Tools = new List - { - new Tool { Name = "tool1", Description = "First tool" }, - new Tool { Name = "tool2", Description = "Second tool" } - } - }; - var server = new McpbToolsListResult - { - Tools = new List - { - new Tool { Name = "tool2", Description = "Second tool" }, - new Tool { Name = "tool1", Description = "First tool" } - } - }; - var result = ToolsListComparer.AreEqual(manifest, server); - Assert.True(result); - } - - [Fact] - public void AreEqual_DifferentToolCount_ReturnsFalse() - { - var manifest = new McpbToolsListResult - { - Tools = new List - { - new Tool { Name = "tool1" } - } - }; - var server = new McpbToolsListResult - { - Tools = new List - { - new Tool { Name = "tool1" }, - new Tool { Name = "tool2" } - } - }; - var result = ToolsListComparer.AreEqual(manifest, server); - Assert.False(result); - } - - [Fact] - public void AreEqual_SameToolsWithoutSchemas_ReturnsTrue() - { - var manifest = new McpbToolsListResult - { - Tools = new List - { - new Tool { Name = "tool1", Description = "Test tool" } - } - }; - var server = new McpbToolsListResult - { - Tools = new List - { - new Tool { Name = "tool1", Description = "Test tool" } - } - }; - var result = ToolsListComparer.AreEqual(manifest, server); - Assert.True(result); - } - - [Fact] - public void AreEqual_DifferentToolDescription_ReturnsFalse() - { - var manifest = new McpbToolsListResult - { - Tools = new List - { - new Tool { Name = "tool1", Description = "Description 1" } - } - }; - var server = new McpbToolsListResult - { - Tools = new List - { - new Tool { Name = "tool1", Description = "Description 2" } - } - }; - var result = ToolsListComparer.AreEqual(manifest, server); - Assert.False(result); - } - - [Fact] - public void AreEqual_MatchingInputSchema_ReturnsTrue() - { - var schemaJson = @"{ - ""type"": ""object"", - ""properties"": { - ""param1"": { ""type"": ""string"" }, - ""param2"": { ""type"": ""number"" } - }, - ""required"": [""param1""] - }"; - var schema = JsonDocument.Parse(schemaJson).RootElement; - - var manifest = new McpbToolsListResult - { - Tools = new List - { - new Tool { Name = "tool1", InputSchema = schema } - } - }; - var server = new McpbToolsListResult - { - Tools = new List - { - new Tool { Name = "tool1", InputSchema = schema.Clone() } - } - }; - var result = ToolsListComparer.AreEqual(manifest, server); - Assert.True(result); - } - - [Fact] - public void AreEqual_DifferentInputSchemaPropertyTypes_ReturnsFalse() - { - var manifestSchemaJson = @"{ - ""type"": ""object"", - ""properties"": { - ""param1"": { ""type"": ""string"" } - } - }"; - var serverSchemaJson = @"{ - ""type"": ""object"", - ""properties"": { - ""param1"": { ""type"": ""number"" } - } - }"; - - var manifest = new McpbToolsListResult - { - Tools = new List - { - new Tool - { - Name = "tool1", - InputSchema = JsonDocument.Parse(manifestSchemaJson).RootElement - } - } - }; - var server = new McpbToolsListResult - { - Tools = new List - { - new Tool - { - Name = "tool1", - InputSchema = JsonDocument.Parse(serverSchemaJson).RootElement - } - } - }; - var result = ToolsListComparer.AreEqual(manifest, server); - Assert.False(result); - } - - [Fact] - public void AreEqual_DifferentInputSchemaPropertyCount_ReturnsFalse() - { - var manifestSchemaJson = @"{ - ""type"": ""object"", - ""properties"": { - ""param1"": { ""type"": ""string"" } - } - }"; - var serverSchemaJson = @"{ - ""type"": ""object"", - ""properties"": { - ""param1"": { ""type"": ""string"" }, - ""param2"": { ""type"": ""number"" } - } - }"; - - var manifest = new McpbToolsListResult - { - Tools = new List - { - new Tool - { - Name = "tool1", - InputSchema = JsonDocument.Parse(manifestSchemaJson).RootElement - } - } - }; - var server = new McpbToolsListResult - { - Tools = new List - { - new Tool - { - Name = "tool1", - InputSchema = JsonDocument.Parse(serverSchemaJson).RootElement - } - } - }; - var result = ToolsListComparer.AreEqual(manifest, server); - Assert.False(result); - } - - [Fact] - public void AreEqual_MatchingOutputSchema_ReturnsTrue() - { - var schemaJson = @"{ - ""type"": ""object"", - ""properties"": { - ""result"": { ""type"": ""string"" } - } - }"; - var schema = JsonDocument.Parse(schemaJson).RootElement; - - var manifest = new McpbToolsListResult - { - Tools = new List - { - new Tool { Name = "tool1", OutputSchema = schema } - } - }; - var server = new McpbToolsListResult - { - Tools = new List - { - new Tool { Name = "tool1", OutputSchema = schema.Clone() } - } - }; - var result = ToolsListComparer.AreEqual(manifest, server); - Assert.True(result); - } - - [Fact] - public void AreEqual_DifferentOutputSchema_ReturnsFalse() - { - var manifestSchemaJson = @"{ - ""type"": ""object"", - ""properties"": { - ""result"": { ""type"": ""string"" } - } - }"; - var serverSchemaJson = @"{ - ""type"": ""object"", - ""properties"": { - ""result"": { ""type"": ""number"" } - } - }"; - - var manifest = new McpbToolsListResult - { - Tools = new List - { - new Tool - { - Name = "tool1", - OutputSchema = JsonDocument.Parse(manifestSchemaJson).RootElement - } - } - }; - var server = new McpbToolsListResult - { - Tools = new List - { - new Tool - { - Name = "tool1", - OutputSchema = JsonDocument.Parse(serverSchemaJson).RootElement - } - } - }; - var result = ToolsListComparer.AreEqual(manifest, server); - Assert.False(result); - } - - [Fact] - public void AreEqual_BothSchemasNotSet_ReturnsTrue() - { - var manifest = new McpbToolsListResult - { - Tools = new List - { - new Tool { Name = "tool1" } - } - }; - var server = new McpbToolsListResult - { - Tools = new List - { - new Tool { Name = "tool1" } - } - }; - var result = ToolsListComparer.AreEqual(manifest, server); - Assert.True(result); - } - - [Fact] - public void AreEqual_ComplexNestedSchema_ReturnsTrue() - { - var schemaJson = @"{ - ""type"": ""object"", - ""properties"": { - ""nested"": { - ""type"": ""object"", - ""properties"": { - ""value"": { ""type"": ""string"" }, - ""count"": { ""type"": ""number"" } - } - }, - ""array"": { - ""type"": ""array"", - ""items"": { ""type"": ""string"" } - } - }, - ""required"": [""nested""] - }"; - var schema = JsonDocument.Parse(schemaJson).RootElement; - - var manifest = new McpbToolsListResult - { - Tools = new List - { - new Tool { Name = "tool1", InputSchema = schema } - } - }; - var server = new McpbToolsListResult - { - Tools = new List - { - new Tool { Name = "tool1", InputSchema = schema.Clone() } - } - }; - var result = ToolsListComparer.AreEqual(manifest, server); - Assert.True(result); - } - - [Fact] - public void AreEqual_CaseInsensitiveToolName_ReturnsTrue() - { - var manifest = new McpbToolsListResult - { - Tools = new List - { - new Tool { Name = "ToolOne" } - } - }; - var server = new McpbToolsListResult - { - Tools = new List - { - new Tool { Name = "toolone" } - } - }; - var result = ToolsListComparer.AreEqual(manifest, server); - Assert.True(result); - } - - [Fact] - public void JsonElementDeepEquals_SameStringValues_ReturnsTrue() - { - var json1 = JsonDocument.Parse(@"""test""").RootElement; - var json2 = JsonDocument.Parse(@"""test""").RootElement; - - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); - - Assert.True(result); - } - - [Fact] - public void JsonElementDeepEquals_DifferentStringValues_ReturnsFalse() - { - var json1 = JsonDocument.Parse(@"""test1""").RootElement; - var json2 = JsonDocument.Parse(@"""test2""").RootElement; - - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); - - Assert.False(result); - } - - [Fact] - public void JsonElementDeepEquals_SameNumberValues_ReturnsTrue() - { - var json1 = JsonDocument.Parse(@"42").RootElement; - var json2 = JsonDocument.Parse(@"42").RootElement; - - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); - - Assert.True(result); - } - - [Fact] - public void JsonElementDeepEquals_DifferentNumberValues_ReturnsFalse() - { - var json1 = JsonDocument.Parse(@"42").RootElement; - var json2 = JsonDocument.Parse(@"43").RootElement; - - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); - - Assert.False(result); - } - - [Fact] - public void JsonElementDeepEquals_SameBooleanValues_ReturnsTrue() - { - var json1 = JsonDocument.Parse(@"true").RootElement; - var json2 = JsonDocument.Parse(@"true").RootElement; - - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); - - Assert.True(result); - } - - [Fact] - public void JsonElementDeepEquals_NullValues_ReturnsTrue() - { - var json1 = JsonDocument.Parse(@"null").RootElement; - var json2 = JsonDocument.Parse(@"null").RootElement; - - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); - - Assert.True(result); - } - - [Fact] - public void JsonElementDeepEquals_DifferentValueKinds_ReturnsFalse() - { - var json1 = JsonDocument.Parse(@"""test""").RootElement; - var json2 = JsonDocument.Parse(@"42").RootElement; - - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); - - Assert.False(result); - } - - [Fact] - public void JsonElementDeepEquals_SameObjects_ReturnsTrue() - { - var json1 = JsonDocument.Parse(@"{""key"": ""value"", ""num"": 42}").RootElement; - var json2 = JsonDocument.Parse(@"{""key"": ""value"", ""num"": 42}").RootElement; - - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); - - Assert.True(result); - } - - [Fact] - public void JsonElementDeepEquals_ObjectsWithDifferentPropertyCount_ReturnsFalse() - { - var json1 = JsonDocument.Parse(@"{""key"": ""value""}").RootElement; - var json2 = JsonDocument.Parse(@"{""key"": ""value"", ""extra"": ""prop""}").RootElement; - - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); - - Assert.False(result); - } - - [Fact] - public void JsonElementDeepEquals_ObjectsMissingProperty_ReturnsFalse() - { - var json1 = JsonDocument.Parse(@"{""key1"": ""value""}").RootElement; - var json2 = JsonDocument.Parse(@"{""key2"": ""value""}").RootElement; - - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); - - Assert.False(result); - } - - [Fact] - public void JsonElementDeepEquals_NestedObjectsDifferent_ReturnsFalseWithPath() - { - var json1 = JsonDocument.Parse(@"{""outer"": {""inner"": ""value1""}}").RootElement; - var json2 = JsonDocument.Parse(@"{""outer"": {""inner"": ""value2""}}").RootElement; - - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); - - Assert.False(result); - } - - [Fact] - public void JsonElementDeepEquals_SameArrays_ReturnsTrue() - { - var json1 = JsonDocument.Parse(@"[1, 2, 3]").RootElement; - var json2 = JsonDocument.Parse(@"[1, 2, 3]").RootElement; - - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); - - Assert.True(result); - } - - [Fact] - public void JsonElementDeepEquals_DifferentArrayLengths_ReturnsFalse() - { - var json1 = JsonDocument.Parse(@"[1, 2]").RootElement; - var json2 = JsonDocument.Parse(@"[1, 2, 3]").RootElement; - - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); - - Assert.False(result); - } - - [Fact] - public void JsonElementDeepEquals_ArrayElementsDifferent_ReturnsFalseWithPath() - { - var json1 = JsonDocument.Parse(@"[1, 2, 3]").RootElement; - var json2 = JsonDocument.Parse(@"[1, 5, 3]").RootElement; - - var result = ToolsListComparer.JsonElementDeepEquals(json1, json2); - - Assert.False(result); - } - - [Fact] - public void AreEqual_BothInputAndOutputSchemas_ReturnsTrue() - { - var inputSchemaJson = @"{ - ""type"": ""object"", - ""properties"": { - ""input"": { ""type"": ""string"" } - } - }"; - var outputSchemaJson = @"{ - ""type"": ""object"", - ""properties"": { - ""output"": { ""type"": ""string"" } - } - }"; - - var manifest = new McpbToolsListResult - { - Tools = new List - { - new Tool - { - Name = "tool1", - Description = "A test tool", - InputSchema = JsonDocument.Parse(inputSchemaJson).RootElement, - OutputSchema = JsonDocument.Parse(outputSchemaJson).RootElement - } - } - }; - var server = new McpbToolsListResult - { - Tools = new List - { - new Tool - { - Name = "tool1", - Description = "A test tool", - InputSchema = JsonDocument.Parse(inputSchemaJson).RootElement, - OutputSchema = JsonDocument.Parse(outputSchemaJson).RootElement - } - } - }; - var result = ToolsListComparer.AreEqual(manifest, server); - Assert.True(result); - } -} diff --git a/dotnet/mcpb/Commands/ManifestCommandHelpers.cs b/dotnet/mcpb/Commands/ManifestCommandHelpers.cs index cee238e..809ff24 100644 --- a/dotnet/mcpb/Commands/ManifestCommandHelpers.cs +++ b/dotnet/mcpb/Commands/ManifestCommandHelpers.cs @@ -158,13 +158,14 @@ internal static async Task DiscoverCapabilitiesAsync( { var overrideTools = TryParseToolOverride("MCPB_TOOL_DISCOVERY_JSON"); var overridePrompts = TryParsePromptOverride("MCPB_PROMPT_DISCOVERY_JSON"); - if (overrideTools != null || overridePrompts != null) + var overrideToolsList = TryParseToolsListOverride("MCPB_TOOLS_LIST_DISCOVERY_JSON"); + if (overrideTools != null || overridePrompts != null || overrideToolsList != null) { return new CapabilityDiscoveryResult( overrideTools ?? new List(), overridePrompts ?? new List(), null, - null); + overrideToolsList ?? new McpbToolsListResult()); } var cfg = manifest.Server?.McpConfig ?? throw new InvalidOperationException("Manifest server.mcp_config missing"); @@ -476,6 +477,24 @@ private static string SafeGetSpecial(Environment.SpecialFolder folder, string fa } } + private static McpbToolsListResult? TryParseToolsListOverride(string envVar) + { + var json = Environment.GetEnvironmentVariable(envVar); + if (string.IsNullOrWhiteSpace(json)) return null; + try + { + McpbToolsListResult? toolsListResult = JsonSerializer.Deserialize( + json, + McpbJsonContext.Default.McpbToolsListResult); + + return toolsListResult; + } + catch + { + return null; + } + } + private static List DeduplicateTools(IEnumerable tools) { return tools diff --git a/dotnet/mcpb/Commands/PackCommand.cs b/dotnet/mcpb/Commands/PackCommand.cs index 7c7d118..265e88c 100644 --- a/dotnet/mcpb/Commands/PackCommand.cs +++ b/dotnet/mcpb/Commands/PackCommand.cs @@ -1,11 +1,9 @@ +using Mcpb.Core; +using Mcpb.Json; using System.CommandLine; using System.IO.Compression; using System.Security.Cryptography; using System.Text; -using Mcpb.Core; -using System.Text.Json; -using Mcpb.Json; -using System.Text.RegularExpressions; namespace Mcpb.Commands; @@ -164,9 +162,13 @@ public static Command Create() if (discoveredToolsListResponse != null) { - if (!ToolsListComparer.AreEqual(staticResponses.ToolsList, discoveredToolsListResponse)) + string staticResponsesToolsListJson = JsonSerializer.Serialize(staticResponses.ToolsList, McpbJsonContext.WriteOptions); + string discoveredToolsListJson = JsonSerializer.Serialize(discoveredToolsListResponse, McpbJsonContext.WriteOptions); + + if (!string.Equals(staticResponsesToolsListJson, discoveredToolsListJson, StringComparison.Ordinal)) { mismatchOccurred = true; + Console.WriteLine("Tol schema mismatch in _meta static_responses:"); } // Store the entire tools/list response object as-is diff --git a/dotnet/mcpb/Core/ToolsListComparer.cs b/dotnet/mcpb/Core/ToolsListComparer.cs deleted file mode 100644 index 6eac81c..0000000 --- a/dotnet/mcpb/Core/ToolsListComparer.cs +++ /dev/null @@ -1,243 +0,0 @@ -using ModelContextProtocol.Protocol; - -namespace Mcpb.Core; - -/// -/// Compares static_response tools/list (from the manifest) against the runtime tools/list -/// response from the server, ensuring the server's actual tools match what was declared. -/// -public static class ToolsListComparer -{ - /// - /// Validates that the server's tools/list response matches the static_response tools declared in the manifest. - /// Returns false on the first mismatch found. - /// - /// The tools/list declared in the manifest's static_response. - /// The result retrieved from the server's tools/list response. - /// true if the server tools/list response matches the static_response; otherwise, false. - public static bool AreEqual(McpbToolsListResult? manifestToolsListResult, McpbToolsListResult? serverToolsListResult) - { - if ((manifestToolsListResult is null && serverToolsListResult is not null) || (manifestToolsListResult is not null && serverToolsListResult is null)) - { - return false; - } - - if (manifestToolsListResult is not null) - { - List manifestTools = manifestToolsListResult.Tools ?? new List(); - List serverTools = serverToolsListResult?.Tools ?? new List(); - - int manifestCount = manifestTools.Count; - int serverCount = serverTools.Count; - - if (manifestCount != serverCount) - { - return false; - } - - // Key names must be unique, so we can build a dictionary for the server tools to speed up lookups. - // The dictionary contains a tuple of the Tool and a bool indicating whether it has been matched to - // a manifest tool, to detect duplicates. - Dictionary serverToolsByName = new(StringComparer.OrdinalIgnoreCase); - - foreach (Tool serverTool in serverTools!) - { - if (serverToolsByName.ContainsKey(serverTool.Name)) - { - return false; - } - serverToolsByName[serverTool.Name] = (serverTool, false); - } - - foreach (Tool manifestTool in manifestTools) - { - if (serverToolsByName.TryGetValue(manifestTool.Name, out var serverToolEntry)) - { - if (serverToolEntry.IsMatched) // Much clearer than Item2 - { - return false; - } - - if (!CompareToolProperties(manifestTool, serverToolEntry.Tool)) // Clearer than Item1 - { - return false; - } - - serverToolsByName[manifestTool.Name] = (serverToolEntry.Tool, true); - } - else - { - return false; - } - } - } - - return true; - } - - private static bool CompareToolProperties( - Tool manifestTool, - Tool serverTool) - { - // Compare description (optional — only check if static_response has one). - if (manifestTool.Description is not null) - { - string? serverToolDescription = serverTool.Description; - if (!string.Equals(manifestTool.Description, serverToolDescription, StringComparison.Ordinal)) - { - return false; - } - } - - // Compare inputSchema. - if (!CompareSchema(manifestTool.InputSchema, serverTool.InputSchema)) - { - return false; - } - - // Compare outputSchema. - if (!CompareSchema(manifestTool.OutputSchema, serverTool.OutputSchema)) - { - return false; - } - - return true; - } - - private static bool CompareSchema(JsonElement? manifestSchema, JsonElement? serverSchema) - { - bool manifestDefinesSchema = manifestSchema.HasValue && manifestSchema.Value.ValueKind != JsonValueKind.Undefined; - bool serverDefinesSchema = serverSchema.HasValue && serverSchema.Value.ValueKind != JsonValueKind.Undefined; - - if (manifestDefinesSchema != serverDefinesSchema) - { - return false; - } - - if (manifestDefinesSchema && - !JsonElementDeepEquals(manifestSchema!.Value, serverSchema!.Value)) - { - return false; - } - - return true; - } - - /// - /// Performs a deep comparison of two values. - /// - /// The first element to compare. - /// The second element to compare. - /// When the elements differ, the JSON path where the mismatch was found. - /// true if the elements are deeply equal; otherwise, false. - internal static bool JsonElementDeepEquals(JsonElement element1, JsonElement element2) - { - return JsonElementDeepEqualsCore(element1, element2, string.Empty); - } - - private static bool JsonElementDeepEqualsCore(JsonElement element1, JsonElement element2, string currentPath) - { - if (element1.ValueKind != element2.ValueKind) - { - return false; - } - - switch (element1.ValueKind) - { - case JsonValueKind.Object: - int count1 = 0; - foreach (JsonProperty _ in element1.EnumerateObject()) - { - count1++; - } - - int count2 = 0; - foreach (JsonProperty _ in element2.EnumerateObject()) - { - count2++; - } - - if (count1 != count2) - { - return false; - } - - foreach (JsonProperty property1 in element1.EnumerateObject()) - { - string propertyPath = string.IsNullOrEmpty(currentPath) ? property1.Name : $"{currentPath}.{property1.Name}"; - - if (!element2.TryGetProperty(property1.Name, out JsonElement property2Value)) - { - return false; - } - - if (!JsonElementDeepEqualsCore(property1.Value, property2Value, propertyPath)) - { - return false; - } - } - - return true; - - case JsonValueKind.Array: - int arrayLength1 = 0; - foreach (JsonElement _ in element1.EnumerateArray()) - { - arrayLength1++; - } - - int arrayLength2 = 0; - foreach (JsonElement _ in element2.EnumerateArray()) - { - arrayLength2++; - } - - if (arrayLength1 != arrayLength2) - { - return false; - } - - int index = 0; - using (JsonElement.ArrayEnumerator enumerator1 = element1.EnumerateArray()) - using (JsonElement.ArrayEnumerator enumerator2 = element2.EnumerateArray()) - { - while (enumerator1.MoveNext() && enumerator2.MoveNext()) - { - string arrayPath = $"{currentPath}[{index}]"; - if (!JsonElementDeepEqualsCore(enumerator1.Current, enumerator2.Current, arrayPath)) - { - return false; - } - - index++; - } - } - - return true; - - case JsonValueKind.String: - if (element1.GetString() != element2.GetString()) - { - return false; - } - - return true; - - case JsonValueKind.Number: - if (element1.GetRawText() != element2.GetRawText()) - { - return false; - } - - return true; - - case JsonValueKind.True: - case JsonValueKind.False: - case JsonValueKind.Null: - return true; - - default: - return false; - } - } -} From ca4c04b717aefe7501e621ecc4f94bf82dc1d033 Mon Sep 17 00:00:00 2001 From: Alberto Medina Gutierrez Date: Thu, 2 Apr 2026 16:23:34 -0700 Subject: [PATCH 7/7] Fix Pack command too --- dotnet/README.md | 104 ++++++++++++++++-- dotnet/mcpb/Commands/PackCommand.cs | 100 ++--------------- dotnet/mcpb/Commands/StaticResponsesHelper.cs | 97 ++++++++++++++++ dotnet/mcpb/Commands/ValidateCommand.cs | 20 +++- 4 files changed, 217 insertions(+), 104 deletions(-) create mode 100644 dotnet/mcpb/Commands/StaticResponsesHelper.cs diff --git a/dotnet/README.md b/dotnet/README.md index a8a0e50..a27d70d 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -20,16 +20,100 @@ dotnet tool install --global Mcpb.Cli --add-source ./bin/Release ## Commands -| Command | Description | -| --------------------------------------------------------------------------------------- | -------------------------------- | -| `mcpb init [directory] [--server-type node\|python\|binary\|auto] [--entry-point path]` | Create manifest.json | -| `mcpb validate [manifest\|directory]` | Validate manifest | -| `mcpb pack [directory] [output]` | Create .mcpb archive | -| `mcpb unpack [outputDir]` | Extract archive | -| `mcpb sign [--cert cert.pem --key key.pem --self-signed]` | Sign bundle | -| `mcpb verify ` | Verify signature | -| `mcpb info ` | Show bundle info (and signature) | -| `mcpb unsign ` | Remove signature | +### `mcpb init [directory]` + +Create a new MCPB extension manifest (`manifest.json`). Launches an interactive +wizard unless `--yes` is provided. + +| Option / Argument | Description | +| --- | --- | +| `directory` | Target directory (default: current directory) | +| `--yes`, `-y` | Accept defaults, skip interactive prompts | +| `--server-type ` | Server type: `node`, `python`, `binary`, or `auto` (default: `auto`) | +| `--entry-point ` | Override entry point path (relative to manifest) | + +--- + +### `mcpb validate [manifest]` + +Validate an MCPB manifest file. Optionally runs dynamic tool/prompt discovery +and can auto-update the manifest to match discovered results. + +| Option / Argument | Description | +| --- | --- | +| `manifest` | Path to `manifest.json` or its containing directory | +| `--dirname ` | Directory containing referenced files and server entry point | +| `--update` | Update manifest tools/prompts and `_meta` static responses to match discovery results (requires `--dirname`) | + +--- + +### `mcpb pack [directory] [output]` + +Pack a directory into an `.mcpb` extension archive. Performs manifest validation, +file collection (respecting `.mcpbignore`), and optional dynamic tool/prompt +discovery before bundling. + +| Option / Argument | Description | +| --- | --- | +| `directory` | Extension directory (default: current directory) | +| `output` | Output `.mcpb` file path (default: `.mcpb` in current directory) | +| `--force` | Proceed even if discovered tools/prompts differ from the manifest | +| `--update` | Update manifest tools/prompts and `_meta` static responses to match discovery | +| `--no-discover` | Skip dynamic tool/prompt discovery (for offline or testing use) | + +--- + +### `mcpb unpack [output]` + +Extract the contents of an `.mcpb` archive. + +| Option / Argument | Description | +| --- | --- | +| `mcpb-file` | Path to the `.mcpb` file (required) | +| `output` | Output directory (default: current directory) | + +--- + +### `mcpb sign ` + +Sign an `.mcpb` extension file with a PKCS#7 detached signature. + +| Option / Argument | Description | +| --- | --- | +| `mcpb-file` | Path to the `.mcpb` file (required) | +| `--cert`, `-c` | Path to certificate PEM file (default: `cert.pem`) | +| `--key`, `-k` | Path to private key PEM file (default: `key.pem`) | +| `--self-signed` | Create a self-signed certificate if the cert/key files are missing | + +--- + +### `mcpb verify ` + +Verify the signature of an `.mcpb` file. Prints signer details when valid. + +| Option / Argument | Description | +| --- | --- | +| `mcpb-file` | Path to the `.mcpb` file (required) | + +--- + +### `mcpb info ` + +Display file size and signature information for an `.mcpb` file. + +| Option / Argument | Description | +| --- | --- | +| `mcpb-file` | Path to the `.mcpb` file (required) | + +--- + +### `mcpb unsign ` + +Remove the signature block from an `.mcpb` file. + +| Option / Argument | Description | +| --- | --- | +| `mcpb-file` | Path to the `.mcpb` file (required) | ## License Compliance diff --git a/dotnet/mcpb/Commands/PackCommand.cs b/dotnet/mcpb/Commands/PackCommand.cs index 265e88c..451a3d0 100644 --- a/dotnet/mcpb/Commands/PackCommand.cs +++ b/dotnet/mcpb/Commands/PackCommand.cs @@ -137,48 +137,18 @@ public static Command Create() } // Check static responses in _meta (always update when --update is used) - if (update && (discoveredInitResponse != null || discoveredToolsListResponse != null)) + if (StaticResponsesHelper.CheckAndUpdate( + manifest, + discoveredInitResponse, + discoveredToolsListResponse, + update, + out var checkMessage)) { - // Get or create _meta["com.microsoft.windows"] - var windowsMeta = GetOrCreateWindowsMeta(manifest); - var staticResponses = windowsMeta.StaticResponses ?? new McpbStaticResponses(); - - // Update static responses in _meta when --update flag is used - if (discoveredInitResponse != null) - { - // Serialize to dictionary to have full control over what's included - var initDict = new Dictionary(); - if (discoveredInitResponse.ProtocolVersion != null) - initDict["protocolVersion"] = discoveredInitResponse.ProtocolVersion; - if (discoveredInitResponse.Capabilities != null) - initDict["capabilities"] = discoveredInitResponse.Capabilities; - if (discoveredInitResponse.ServerInfo != null) - initDict["serverInfo"] = discoveredInitResponse.ServerInfo; - if (!string.IsNullOrWhiteSpace(discoveredInitResponse.Instructions)) - initDict["instructions"] = discoveredInitResponse.Instructions; - - staticResponses.Initialize = initDict; - } - - if (discoveredToolsListResponse != null) - { - string staticResponsesToolsListJson = JsonSerializer.Serialize(staticResponses.ToolsList, McpbJsonContext.WriteOptions); - string discoveredToolsListJson = JsonSerializer.Serialize(discoveredToolsListResponse, McpbJsonContext.WriteOptions); - - if (!string.Equals(staticResponsesToolsListJson, discoveredToolsListJson, StringComparison.Ordinal)) - { - mismatchOccurred = true; - Console.WriteLine("Tol schema mismatch in _meta static_responses:"); - } - - // Store the entire tools/list response object as-is - staticResponses.ToolsList = discoveredToolsListResponse; - } - - windowsMeta.StaticResponses = staticResponses; - SetWindowsMeta(manifest, windowsMeta); - Console.WriteLine("Updated _meta static_responses to match discovered results."); + mismatchOccurred = true; + Console.WriteLine(checkMessage); } + if (update && (discoveredInitResponse != null || discoveredToolsListResponse != null)) + Console.WriteLine("Updated _meta static_responses to match discovered results."); if (mismatchOccurred) { @@ -195,6 +165,7 @@ public static Command Create() .ToList(); manifest.ToolsGenerated ??= false; } + if (discoveredPrompts != null) { manifest.Prompts = ManifestCommandHelpers.MergePromptMetadata(manifest.Prompts, discoveredPrompts); @@ -351,53 +322,4 @@ private static string SanitizeFileName(string name) } private static string RegexReplace(string input, string pattern, string replacement) => System.Text.RegularExpressions.Regex.Replace(input, pattern, replacement); - private static McpbWindowsMeta GetOrCreateWindowsMeta(McpbManifest manifest) - { - manifest.Meta ??= new Dictionary>(); - - if (!manifest.Meta.TryGetValue("com.microsoft.windows", out var windowsMetaDict)) - { - return new McpbWindowsMeta(); - } - - // Try to deserialize the dictionary to McpbWindowsMeta - try - { - var json = JsonSerializer.Serialize(windowsMetaDict, McpbJsonContext.WriteOptions); - return JsonSerializer.Deserialize(json) ?? new McpbWindowsMeta(); - } - catch - { - return new McpbWindowsMeta(); - } - } - - private static void SetWindowsMeta(McpbManifest manifest, McpbWindowsMeta windowsMeta) - { - manifest.Meta ??= new Dictionary>(); - - // Serialize to dictionary - var json = JsonSerializer.Serialize(windowsMeta, McpbJsonContext.WriteOptions); - var dict = JsonSerializer.Deserialize>(json) ?? new Dictionary(); - - manifest.Meta["com.microsoft.windows"] = dict; - } - - private static bool AreStaticResponsesEqual(object? a, object? b) - { - if (a == null && b == null) return true; - if (a == null || b == null) return false; - - try - { - var jsonA = JsonSerializer.Serialize(a); - var jsonB = JsonSerializer.Serialize(b); - return jsonA == jsonB; - } - catch - { - return false; - } - } - } diff --git a/dotnet/mcpb/Commands/StaticResponsesHelper.cs b/dotnet/mcpb/Commands/StaticResponsesHelper.cs new file mode 100644 index 0000000..d2e6b5f --- /dev/null +++ b/dotnet/mcpb/Commands/StaticResponsesHelper.cs @@ -0,0 +1,97 @@ +using System.Text.Json; +using Mcpb.Core; +using Mcpb.Json; + +namespace Mcpb.Commands; + +/// +/// Shared logic for checking and updating _meta static_responses +/// used by both pack and validate commands. +/// +internal static class StaticResponsesHelper +{ + /// + /// Checks discovered static responses against the manifest and optionally + /// updates the manifest in place when is true. + /// + /// Whether a tools/list mismatch was detected. + internal static bool CheckAndUpdate( + McpbManifest manifest, + McpbInitializeResult? discoveredInitResponse, + McpbToolsListResult? discoveredToolsListResponse, + bool update, + out string? message) + { + message = null; + if (!update || (discoveredInitResponse == null && discoveredToolsListResponse == null)) + return false; + + var windowsMeta = GetOrCreateWindowsMeta(manifest); + var staticResponses = windowsMeta.StaticResponses ?? new McpbStaticResponses(); + bool mismatch = false; + + if (discoveredInitResponse != null) + { + var initDict = new Dictionary(); + if (discoveredInitResponse.ProtocolVersion != null) + initDict["protocolVersion"] = discoveredInitResponse.ProtocolVersion; + if (discoveredInitResponse.Capabilities != null) + initDict["capabilities"] = discoveredInitResponse.Capabilities; + if (discoveredInitResponse.ServerInfo != null) + initDict["serverInfo"] = discoveredInitResponse.ServerInfo; + if (!string.IsNullOrWhiteSpace(discoveredInitResponse.Instructions)) + initDict["instructions"] = discoveredInitResponse.Instructions; + + staticResponses.Initialize = initDict; + } + + if (discoveredToolsListResponse != null) + { + string staticResponsesToolsListJson = JsonSerializer.Serialize(staticResponses.ToolsList, McpbJsonContext.WriteOptions); + string discoveredToolsListJson = JsonSerializer.Serialize(discoveredToolsListResponse, McpbJsonContext.WriteOptions); + + if (!string.Equals(staticResponsesToolsListJson, discoveredToolsListJson, StringComparison.Ordinal)) + { + mismatch = true; + message = "Tool schema mismatch in _meta static_responses:"; + } + + staticResponses.ToolsList = discoveredToolsListResponse; + } + + windowsMeta.StaticResponses = staticResponses; + SetWindowsMeta(manifest, windowsMeta); + + return mismatch; + } + + internal static McpbWindowsMeta GetOrCreateWindowsMeta(McpbManifest manifest) + { + manifest.Meta ??= new Dictionary>(); + + if (!manifest.Meta.TryGetValue("com.microsoft.windows", out var windowsMetaDict)) + { + return new McpbWindowsMeta(); + } + + try + { + var json = JsonSerializer.Serialize(windowsMetaDict, McpbJsonContext.WriteOptions); + return JsonSerializer.Deserialize(json) ?? new McpbWindowsMeta(); + } + catch + { + return new McpbWindowsMeta(); + } + } + + internal static void SetWindowsMeta(McpbManifest manifest, McpbWindowsMeta windowsMeta) + { + manifest.Meta ??= new Dictionary>(); + + var json = JsonSerializer.Serialize(windowsMeta, McpbJsonContext.WriteOptions); + var dict = JsonSerializer.Deserialize>(json) ?? new Dictionary(); + + manifest.Meta["com.microsoft.windows"] = dict; + } +} diff --git a/dotnet/mcpb/Commands/ValidateCommand.cs b/dotnet/mcpb/Commands/ValidateCommand.cs index 5814ef3..61377b8 100644 --- a/dotnet/mcpb/Commands/ValidateCommand.cs +++ b/dotnet/mcpb/Commands/ValidateCommand.cs @@ -1,10 +1,6 @@ -using System.CommandLine; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json; using Mcpb.Core; using Mcpb.Json; +using System.CommandLine; namespace Mcpb.Commands; @@ -174,6 +170,20 @@ static void PrintWarnings(IEnumerable warnings, bool toError) Console.Error.WriteLine($"WARNING: {warning}"); } + // Check and optionally update _meta static_responses + if (StaticResponsesHelper.CheckAndUpdate( + manifest, + discovery.InitializeResponse, + discovery.ToolsListResponse, + update, + out var checkMessage)) + { + mismatchOccurred = true; + Console.WriteLine(checkMessage); + } + if (update && (discovery.InitializeResponse != null || discovery.ToolsListResponse != null)) + Console.WriteLine("Updated _meta static_responses to match discovered results."); + if (mismatchOccurred) { if (update)