Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 94 additions & 10 deletions dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <file> [outputDir]` | Extract archive |
| `mcpb sign <file> [--cert cert.pem --key key.pem --self-signed]` | Sign bundle |
| `mcpb verify <file>` | Verify signature |
| `mcpb info <file>` | Show bundle info (and signature) |
| `mcpb unsign <file>` | 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 <type>` | Server type: `node`, `python`, `binary`, or `auto` (default: `auto`) |
| `--entry-point <path>` | 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 <dir>` | 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: `<name>.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 <mcpb-file> [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 <mcpb-file>`

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 <mcpb-file>`

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 <mcpb-file>`

Display file size and signature information for an `.mcpb` file.

| Option / Argument | Description |
| --- | --- |
| `mcpb-file` | Path to the `.mcpb` file (required) |

---

### `mcpb unsign <mcpb-file>`

Remove the signature block from an `.mcpb` file.

| Option / Argument | Description |
| --- | --- |
| `mcpb-file` | Path to the `.mcpb` file (required) |

## License Compliance

Expand Down
270 changes: 270 additions & 0 deletions dotnet/mcpb.Tests/CliPackToolDiscoveryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Xunit;
using System.IO;
using System.Linq;
using Mcpb.Core;

namespace Mcpb.Tests;

Expand Down Expand Up @@ -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<Mcpb.Core.McpbManifest>(
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<Mcpb.Core.McpbManifest>(
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);
}
}
}
Loading
Loading