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.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/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..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"); @@ -253,17 +254,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) @@ -481,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 25aba29..451a3d0 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; @@ -139,37 +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) - { - // 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) { @@ -186,6 +165,7 @@ public static Command Create() .ToList(); manifest.ToolsGenerated ??= false; } + if (discoveredPrompts != null) { manifest.Prompts = ManifestCommandHelpers.MergePromptMetadata(manifest.Prompts, discoveredPrompts); @@ -342,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); - 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); - 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) 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