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:
parent
16790150e9
commit
8f5e17d0fe
8 changed files with 72 additions and 24 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue