Enforce and document file hash requirement for MCPB packages (#368)

## Summary
Implements mandatory file hash validation for MCPB (MCP Bundle) packages
to ensure file integrity and security.

## Changes
- **Validator enforcement**: MCPB packages now require `file_sha256`
field at publish time
- **Test coverage**: Updated all MCPB test cases to include valid
SHA-256 hashes
- **Documentation**: Updated publisher guide and schema to reflect hash
requirement
- **Error handling**: Clear error message when hash is missing

## Security Impact
This ensures the complete security workflow for MCPB packages:
1. Authors generate and provide SHA-256 hashes when publishing
2. Registry validates hash is present (but doesn't verify content)
3. MCP clients validate hash matches downloaded file before installation

Fixes #351
This commit is contained in:
adam jones 2025-09-08 11:38:25 -07:00 • committed by GitHub
commit 8f5e17d0fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 72 additions and 24 deletions

View file

@ -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.
</details>

View file

@ -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": {

View file

@ -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,
},

View file

@ -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,
},

View file

@ -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,
},

View file

@ -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

View file

@ -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)

View file

@ -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",
},