diff --git a/docs/guides/publishing/publish-server.md b/docs/guides/publishing/publish-server.md index 8b58651..fa339bf 100644 --- a/docs/guides/publishing/publish-server.md +++ b/docs/guides/publishing/publish-server.md @@ -219,6 +219,15 @@ The official MCP registry currently only supports the official Docker registry ( ### Requirements **MCP reference** - MCPB package URLs must contain "mcp" somewhere within them, to ensure the correct artifact has been uploaded. This may be with the `.mcpb` extension or in the name of your repository. +**File integrity** - MCPB packages must include a SHA-256 hash for file integrity verification. This is required at publish time and MCP clients will validate this hash before installation. + +### How to Generate File Hashes +Calculate the SHA-256 hash of your MCPB file: + +```bash +openssl dgst -sha256 server.mcpb +``` + ### Example server.json ```json { @@ -226,12 +235,19 @@ The official MCP registry currently only supports the official Docker registry ( "packages": [ { "registry_type": "mcpb", - "identifier": "https://github.com/you/your-repo/releases/download/v1.0.0/server.mcpb" + "identifier": "https://github.com/you/your-repo/releases/download/v1.0.0/server.mcpb", + "file_sha256": "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce" } ] } ``` +### File Hash Validation +- **Authors** are responsible for generating correct SHA-256 hashes when creating server.json +- **MCP clients** validate the hash before installing packages to ensure file integrity +- **The official registry** stores hashes but does not validate them +- **Subregistries** may choose to implement their own validation. This enables them to perform security scanning on MCPB files, and ensure clients get the same security scanned content. + The official MCP registry currently only supports artifacts hosted on GitHub or GitLab releases. diff --git a/docs/reference/server-json/server.schema.json b/docs/reference/server-json/server.schema.json index 4caadd6..4daa5e4 100644 --- a/docs/reference/server-json/server.schema.json +++ b/docs/reference/server-json/server.schema.json @@ -115,7 +115,8 @@ }, "file_sha256": { "type": "string", - "description": "SHA-256 hash of the package file for integrity verification.", + "pattern": "^[a-f0-9]{64}$", + "description": "SHA-256 hash of the package file for integrity verification. Required for MCPB packages and optional for other package types. Authors are responsible for generating correct SHA-256 hashes when creating server.json. If present, MCP clients must validate the downloaded file matches the hash before running packages to ensure file integrity.", "example": "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce" }, "runtime_hint": { diff --git a/internal/api/handlers/v0/publish_integration_test.go b/internal/api/handlers/v0/publish_integration_test.go index 8b8ac37..2f0094e 100644 --- a/internal/api/handlers/v0/publish_integration_test.go +++ b/internal/api/handlers/v0/publish_integration_test.go @@ -234,6 +234,7 @@ func TestPublishIntegration(t *testing.T) { RegistryType: model.RegistryTypeMCPB, Identifier: "github.com/domdomegg/airtable-mcp-server/releases/download/v1.7.2/airtable-mcp-server.mcpb", Version: "1.7.2", + FileSHA256: "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce", Transport: model.Transport{ Type: model.TransportTypeStdio, }, diff --git a/internal/api/handlers/v0/publish_registry_validation_test.go b/internal/api/handlers/v0/publish_registry_validation_test.go index b1091c9..145f51f 100644 --- a/internal/api/handlers/v0/publish_registry_validation_test.go +++ b/internal/api/handlers/v0/publish_registry_validation_test.go @@ -98,6 +98,7 @@ func TestPublishRegistryValidation(t *testing.T) { RegistryType: model.RegistryTypeMCPB, Identifier: "https://github.com/microsoft/playwright-mcp/releases/download/v0.0.36/playwright-mcp-extension-v0.0.36.zip", Version: "0.0.36", + FileSHA256: "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce", Transport: model.Transport{ Type: model.TransportTypeStdio, }, @@ -149,6 +150,7 @@ func TestPublishRegistryValidation(t *testing.T) { RegistryType: model.RegistryTypeMCPB, Identifier: "https://github.com/microsoft/playwright-mcp/releases/download/v0.0.36/playwright-mcp-extension-v0.0.36.zip", Version: "1.0.0", + FileSHA256: "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce", Transport: model.Transport{ Type: model.TransportTypeStdio, }, @@ -209,6 +211,7 @@ func TestPublishRegistryValidation(t *testing.T) { RegistryType: model.RegistryTypeMCPB, Identifier: "https://github.com/microsoft/playwright-mcp/releases/download/v0.0.36/playwright-mcp-extension-v0.0.36.zip", Version: "1.0.0", + FileSHA256: "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce", Transport: model.Transport{ Type: model.TransportTypeStdio, }, diff --git a/internal/api/handlers/v0/publish_test.go b/internal/api/handlers/v0/publish_test.go index 81bf588..03e4dac 100644 --- a/internal/api/handlers/v0/publish_test.go +++ b/internal/api/handlers/v0/publish_test.go @@ -222,6 +222,7 @@ func TestPublishEndpoint(t *testing.T) { RegistryType: model.RegistryTypeMCPB, Identifier: "https://github.com/example/server/releases/download/v1.0.0/server.tar.gz", Version: "1.0.0", + FileSHA256: "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce", Transport: model.Transport{ Type: model.TransportTypeStdio, }, diff --git a/internal/validators/registries/mcpb.go b/internal/validators/registries/mcpb.go index 340a4c5..fe8d325 100644 --- a/internal/validators/registries/mcpb.go +++ b/internal/validators/registries/mcpb.go @@ -13,6 +13,11 @@ import ( ) func ValidateMCPB(ctx context.Context, pkg model.Package, _ string) error { + // MCPB packages must include a file hash for integrity verification + if pkg.FileSHA256 == "" { + return fmt.Errorf("MCPB package must include a file_sha256 hash for integrity verification") + } + err := validateMCPBUrl(pkg.Identifier) if err != nil { return err diff --git a/internal/validators/registries/mcpb_test.go b/internal/validators/registries/mcpb_test.go index e6487bf..e7a54e0 100644 --- a/internal/validators/registries/mcpb_test.go +++ b/internal/validators/registries/mcpb_test.go @@ -16,6 +16,7 @@ func TestValidateMCPB(t *testing.T) { name string packageName string serverName string + fileSHA256 string expectError bool errorMessage string }{ @@ -23,18 +24,29 @@ func TestValidateMCPB(t *testing.T) { name: "valid MCPB package should pass", packageName: "https://github.com/domdomegg/airtable-mcp-server/releases/download/v1.7.2/airtable-mcp-server.mcpb", serverName: "io.github.domdomegg/airtable-mcp-server", + fileSHA256: "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce", expectError: false, }, { name: "valid MCPB package should pass", packageName: "https://github.com/microsoft/playwright-mcp/releases/download/v0.0.36/playwright-mcp-extension-v0.0.36.zip", serverName: "com.microsoft/playwright-mcp", + fileSHA256: "abc123ef4567890abcdef1234567890abcdef1234567890abcdef1234567890", expectError: false, }, { - name: "valid MCPB package with .mcpb extension should fail accessibility check", + name: "MCPB package without file hash should fail", packageName: "https://github.com/example/server/releases/download/v1.0.0/server.mcpb", serverName: "com.example/test", + fileSHA256: "", + expectError: true, + errorMessage: "must include a file_sha256 hash for integrity verification", + }, + { + name: "non-existent .mcpb package should fail accessibility check", + packageName: "https://github.com/example/server/releases/download/v1.0.0/server.mcpb", + serverName: "com.example/test", + fileSHA256: "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce", expectError: true, errorMessage: "not publicly accessible", }, @@ -42,6 +54,7 @@ func TestValidateMCPB(t *testing.T) { name: "invalid URL without mcp anywhere should fail", packageName: "https://github.com/example/server/releases/download/v1.0.0/server.tar.gz", serverName: "com.example/test", + fileSHA256: "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce", expectError: true, errorMessage: "URL must contain 'mcp'", }, @@ -49,6 +62,7 @@ func TestValidateMCPB(t *testing.T) { name: "invalid URL format should fail", packageName: "not://a valid url for mcpb!", serverName: "com.example/test", + fileSHA256: "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce", expectError: true, errorMessage: "invalid MCPB package URL", }, @@ -56,6 +70,7 @@ func TestValidateMCPB(t *testing.T) { name: "non-existent file should fail accessibility check", packageName: "https://github.com/nonexistent/repo/releases/download/v1.0.0/mcp-server.tar.gz", serverName: "com.example/test", + fileSHA256: "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce", expectError: true, errorMessage: "not publicly accessible", }, @@ -66,6 +81,7 @@ func TestValidateMCPB(t *testing.T) { pkg := model.Package{ RegistryType: model.RegistryTypeMCPB, Identifier: tt.packageName, + FileSHA256: tt.fileSHA256, } err := registries.ValidateMCPB(ctx, pkg, tt.serverName) diff --git a/internal/validators/validators_test.go b/internal/validators/validators_test.go index 35e654e..9114059 100644 --- a/internal/validators/validators_test.go +++ b/internal/validators/validators_test.go @@ -1189,33 +1189,37 @@ func TestValidate_RegistryTypesAndUrls(t *testing.T) { baseURL string identifier string version string + fileSHA256 string expectError bool }{ // Valid registry types (should pass) - {"valid_npm", "io.github.domdomegg/airtable-mcp-server", model.RegistryTypeNPM, model.RegistryURLNPM, "airtable-mcp-server", "1.7.2", false}, - {"valid_npm", "io.github.domdomegg/airtable-mcp-server", model.RegistryTypeNPM, "", "airtable-mcp-server", "1.7.2", false}, - {"valid_pypi", "io.github.domdomegg/time-mcp-pypi", model.RegistryTypePyPI, model.RegistryURLPyPI, "time-mcp-pypi", "1.0.1", false}, - {"valid_pypi", "io.github.domdomegg/time-mcp-pypi", model.RegistryTypePyPI, "", "time-mcp-pypi", "1.0.1", false}, - {"valid_oci", "io.github.domdomegg/airtable-mcp-server", model.RegistryTypeOCI, model.RegistryURLDocker, "domdomegg/airtable-mcp-server", "1.7.2", false}, - {"valid_nuget", "io.github.domdomegg/time-mcp-server", model.RegistryTypeNuGet, model.RegistryURLNuGet, "TimeMcpServer", "1.0.2", false}, - {"valid_nuget", "io.github.domdomegg/time-mcp-server", model.RegistryTypeNuGet, "", "TimeMcpServer", "1.0.2", false}, - {"valid_mcpb_github", "io.github.domdomegg/airtable-mcp-server", model.RegistryTypeMCPB, model.RegistryURLGitHub, "https://github.com/domdomegg/airtable-mcp-server/releases/download/v1.7.2/airtable-mcp-server.mcpb", "1.7.2", false}, - {"valid_mcpb_github", "io.github.domdomegg/airtable-mcp-server", model.RegistryTypeMCPB, "", "https://github.com/domdomegg/airtable-mcp-server/releases/download/v1.7.2/airtable-mcp-server.mcpb", "1.7.2", false}, - {"valid_mcpb_gitlab", "io.gitlab.fforster/gitlab-mcp", model.RegistryTypeMCPB, model.RegistryURLGitLab, "https://gitlab.com/fforster/gitlab-mcp/-/releases/v1.31.0/downloads/gitlab-mcp_1.31.0_Linux_x86_64.tar.gz", "1.31.0", false}, // this is not actually a valid mcpb, but it's the closest I can get for testing for now - {"valid_mcpb_gitlab", "io.gitlab.fforster/gitlab-mcp", model.RegistryTypeMCPB, "", "https://gitlab.com/fforster/gitlab-mcp/-/releases/v1.31.0/downloads/gitlab-mcp_1.31.0_Linux_x86_64.tar.gz", "1.31.0", false}, // this is not actually a valid mcpb, but it's the closest I can get for testing for now + {"valid_npm", "io.github.domdomegg/airtable-mcp-server", model.RegistryTypeNPM, model.RegistryURLNPM, "airtable-mcp-server", "1.7.2", "", false}, + {"valid_npm", "io.github.domdomegg/airtable-mcp-server", model.RegistryTypeNPM, "", "airtable-mcp-server", "1.7.2", "", false}, + {"valid_pypi", "io.github.domdomegg/time-mcp-pypi", model.RegistryTypePyPI, model.RegistryURLPyPI, "time-mcp-pypi", "1.0.1", "", false}, + {"valid_pypi", "io.github.domdomegg/time-mcp-pypi", model.RegistryTypePyPI, "", "time-mcp-pypi", "1.0.1", "", false}, + {"valid_oci", "io.github.domdomegg/airtable-mcp-server", model.RegistryTypeOCI, model.RegistryURLDocker, "domdomegg/airtable-mcp-server", "1.7.2", "", false}, + {"valid_nuget", "io.github.domdomegg/time-mcp-server", model.RegistryTypeNuGet, model.RegistryURLNuGet, "TimeMcpServer", "1.0.2", "", false}, + {"valid_nuget", "io.github.domdomegg/time-mcp-server", model.RegistryTypeNuGet, "", "TimeMcpServer", "1.0.2", "", false}, + {"valid_mcpb_github", "io.github.domdomegg/airtable-mcp-server", model.RegistryTypeMCPB, model.RegistryURLGitHub, "https://github.com/domdomegg/airtable-mcp-server/releases/download/v1.7.2/airtable-mcp-server.mcpb", "1.7.2", "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce", false}, + {"valid_mcpb_github", "io.github.domdomegg/airtable-mcp-server", model.RegistryTypeMCPB, "", "https://github.com/domdomegg/airtable-mcp-server/releases/download/v1.7.2/airtable-mcp-server.mcpb", "1.7.2", "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce", false}, + {"valid_mcpb_gitlab", "io.gitlab.fforster/gitlab-mcp", model.RegistryTypeMCPB, model.RegistryURLGitLab, "https://gitlab.com/fforster/gitlab-mcp/-/releases/v1.31.0/downloads/gitlab-mcp_1.31.0_Linux_x86_64.tar.gz", "1.31.0", "abc123ef4567890abcdef1234567890abcdef1234567890abcdef1234567890", false}, // this is not actually a valid mcpb, but it's the closest I can get for testing for now + {"valid_mcpb_gitlab", "io.gitlab.fforster/gitlab-mcp", model.RegistryTypeMCPB, "", "https://gitlab.com/fforster/gitlab-mcp/-/releases/v1.31.0/downloads/gitlab-mcp_1.31.0_Linux_x86_64.tar.gz", "1.31.0", "abc123ef4567890abcdef1234567890abcdef1234567890abcdef1234567890", false}, // this is not actually a valid mcpb, but it's the closest I can get for testing for now + + // Test MCPB without file hash (should fail) + {"invalid_mcpb_no_hash", "io.github.domdomegg/airtable-mcp-server", model.RegistryTypeMCPB, model.RegistryURLGitHub, "https://github.com/domdomegg/airtable-mcp-server/releases/download/v1.7.2/airtable-mcp-server.mcpb", "1.7.2", "", true}, // Invalid registry types (should fail) - {"invalid_maven", "io.github.domdomegg/airtable-mcp-server", "maven", model.RegistryURLNPM, "airtable-mcp-server", "1.7.2", true}, - {"invalid_cargo", "io.github.domdomegg/time-mcp-pypi", "cargo", model.RegistryURLPyPI, "time-mcp-pypi", "1.0.1", true}, - {"invalid_gem", "io.github.domdomegg/airtable-mcp-server", "gem", model.RegistryURLDocker, "domdomegg/airtable-mcp-server", "1.7.2", true}, - {"invalid_unknown", "io.github.domdomegg/time-mcp-server", "unknown", model.RegistryURLNuGet, "TimeMcpServer", "1.0.2", true}, - {"invalid_blank", "io.github.domdomegg/time-mcp-server", "", model.RegistryURLNuGet, "TimeMcpServer", "1.0.2", true}, - {"invalid_docker", "io.github.domdomegg/airtable-mcp-server", "docker", model.RegistryURLDocker, "domdomegg/airtable-mcp-server", "1.7.2", true}, // should be oci - {"invalid_github", "io.github.domdomegg/airtable-mcp-server", "github", model.RegistryURLGitHub, "https://github.com/domdomegg/airtable-mcp-server/releases/download/v1.7.2/airtable-mcp-server.mcpb", "1.7.2", true}, // should be mcpb + {"invalid_maven", "io.github.domdomegg/airtable-mcp-server", "maven", model.RegistryURLNPM, "airtable-mcp-server", "1.7.2", "", true}, + {"invalid_cargo", "io.github.domdomegg/time-mcp-pypi", "cargo", model.RegistryURLPyPI, "time-mcp-pypi", "1.0.1", "", true}, + {"invalid_gem", "io.github.domdomegg/airtable-mcp-server", "gem", model.RegistryURLDocker, "domdomegg/airtable-mcp-server", "1.7.2", "", true}, + {"invalid_unknown", "io.github.domdomegg/time-mcp-server", "unknown", model.RegistryURLNuGet, "TimeMcpServer", "1.0.2", "", true}, + {"invalid_blank", "io.github.domdomegg/time-mcp-server", "", model.RegistryURLNuGet, "TimeMcpServer", "1.0.2", "", true}, + {"invalid_docker", "io.github.domdomegg/airtable-mcp-server", "docker", model.RegistryURLDocker, "domdomegg/airtable-mcp-server", "1.7.2", "", true}, // should be oci + {"invalid_github", "io.github.domdomegg/airtable-mcp-server", "github", model.RegistryURLGitHub, "https://github.com/domdomegg/airtable-mcp-server/releases/download/v1.7.2/airtable-mcp-server.mcpb", "1.7.2", "", true}, // should be mcpb - {"invalid_mix_1", "io.github.domdomegg/time-mcp-server", model.RegistryTypeNuGet, model.RegistryURLNPM, "TimeMcpServer", "1.0.2", true}, - {"invalid_mix_2", "io.github.domdomegg/airtable-mcp-server", model.RegistryTypeOCI, model.RegistryURLNPM, "domdomegg/airtable-mcp-server", "1.7.2", true}, - {"invalid_mix_3", "io.github.domdomegg/airtable-mcp-server", model.RegistryURLNPM, model.RegistryURLNPM, "airtable-mcp-server", "1.7.2", true}, + {"invalid_mix_1", "io.github.domdomegg/time-mcp-server", model.RegistryTypeNuGet, model.RegistryURLNPM, "TimeMcpServer", "1.0.2", "", true}, + {"invalid_mix_2", "io.github.domdomegg/airtable-mcp-server", model.RegistryTypeOCI, model.RegistryURLNPM, "domdomegg/airtable-mcp-server", "1.7.2", "", true}, + {"invalid_mix_3", "io.github.domdomegg/airtable-mcp-server", model.RegistryURLNPM, model.RegistryURLNPM, "airtable-mcp-server", "1.7.2", "", true}, } for _, tc := range testCases { @@ -1237,6 +1241,7 @@ func TestValidate_RegistryTypesAndUrls(t *testing.T) { RegistryType: tc.registryType, RegistryBaseURL: tc.baseURL, Version: tc.version, + FileSHA256: tc.fileSHA256, Transport: model.Transport{ Type: "stdio", },