feat: Add support for MCP Bundles (MCPB) in registry (#295)

## Motivation and Context
- Add MCPB as a new registry_name option
- Add file_hashes field to package model for integrity verification
- Implement URL validation restricting MCPB packages to GitHub/GitLab
hosts
- Require SHA-256 hash for all MCPB packages
- Add example MCPB package in documentation
- Update schemas and OpenAPI specification

See issue #260

## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->

## Breaking Changes
N/A, registry has not yet been launched

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [x] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [ ] My code follows the repository's style guidelines
- [ ] New and existing tests pass locally
- [ ] I have added appropriate error handling
- [x] I have added or updated documentation as needed

---------

Co-authored-by: Adam Jones <adamj+git@anthropic.com>
This commit is contained in:
joan-anthropic 2025-08-26 20:44:53 -07:00 • committed by GitHub
commit 57efb58143
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 2014 additions and 2040 deletions

View file

@ -16,6 +16,16 @@ import (
"github.com/modelcontextprotocol/registry/tools/publisher/auth"
)
// Package type constants
const (
packageTypeDocker = "docker"
registryNPM = "npm"
registryPyPI = "pypi"
registryDocker = "docker"
registryNuGet = "nuget"
registryURL = "url"
)
// Server structure types for JSON generation
type Repository struct {
URL string `json:"url"`
@ -42,9 +52,10 @@ type RuntimeArgument struct {
}
type Package struct {
RegistryName string `json:"registry_name"`
Name string `json:"name"`
Version string `json:"version"`
RegistryType string `json:"registry_type,omitempty"`
RegistryBaseURL string `json:"registry_base_url,omitempty"`
Identifier string `json:"identifier,omitempty"`
Version string `json:"version,omitempty"`
RuntimeHint string `json:"runtime_hint,omitempty"`
RuntimeArguments []RuntimeArgument `json:"runtime_arguments,omitempty"`
PackageArguments []RuntimeArgument `json:"package_arguments,omitempty"`
@ -226,7 +237,7 @@ func createCommand() error {
createFlags.StringVar(&output, "o", "server.json", "Output file path (shorthand)")
createFlags.StringVar(&status, "status", "active", "Server status (active or deprecated)")
createFlags.StringVar(&registryName, "registry", "npm", "Package registry name")
createFlags.StringVar(&registryName, "registry", registryNPM, "Package registry name")
createFlags.StringVar(&packageName, "package-name", "", "Package name (defaults to server name)")
createFlags.StringVar(&packageVersion, "package-version", "", "Package version (defaults to server version)")
createFlags.StringVar(&runtimeHint, "runtime-hint", "", "Runtime hint (e.g., docker)")
@ -304,9 +315,9 @@ func createCommand() error {
// Set runtime hint based on registry name if not explicitly provided
if runtimeHint == "" {
switch registryName {
case "docker":
runtimeHint = "docker"
case "npm":
case packageTypeDocker:
runtimeHint = packageTypeDocker
case registryNPM:
runtimeHint = "npx"
}
}
@ -398,12 +409,8 @@ func publishToRegistry(registryURL string, mcpData []byte, token string) error {
return nil
}
func createServerStructure(
name, description, version, repoURL, repoSource, registryName,
packageName, packageVersion, runtimeHint, execute string,
envVars []string, packageArgs []string, status string,
) ServerJSON {
// Parse environment variables
// parseEnvironmentVariables parses environment variable specifications
func parseEnvironmentVariables(envVars []string) []EnvironmentVariable {
var environmentVariables []EnvironmentVariable
for _, envVar := range envVars {
parts := strings.SplitN(envVar, ":", 2)
@ -420,8 +427,11 @@ func createServerStructure(
})
}
}
return environmentVariables
}
// Parse package arguments
// parsePackageArguments parses package argument specifications
func parsePackageArguments(packageArgs []string) []RuntimeArgument {
var packageArguments []RuntimeArgument
for i, pkgArg := range packageArgs {
parts := strings.SplitN(pkgArg, ":", 2)
@ -441,46 +451,121 @@ func createServerStructure(
ValueHint: value,
})
}
return packageArguments
}
// Parse execute command to create runtime arguments
// parseRuntimeArguments parses the execute command to create runtime arguments
func parseRuntimeArguments(execute string) []RuntimeArgument {
var runtimeArguments []RuntimeArgument
if execute != "" {
// Split the execute command into parts, handling quoted strings
parts := smartSplit(execute)
if len(parts) > 1 {
// Skip the first part (command) and add each argument as a runtime argument
for i, arg := range parts[1:] {
description := fmt.Sprintf("Runtime argument %d", i+1)
// Try to provide better descriptions based on common patterns
switch {
case strings.HasPrefix(arg, "--"):
description = fmt.Sprintf("Command line flag: %s", arg)
case strings.HasPrefix(arg, "-") && len(arg) == 2:
description = fmt.Sprintf("Command line option: %s", arg)
case strings.Contains(arg, "="):
description = fmt.Sprintf("Configuration parameter: %s", arg)
case i > 0 && strings.HasPrefix(parts[i], "-"):
description = fmt.Sprintf("Value for %s", parts[i])
}
runtimeArguments = append(runtimeArguments, RuntimeArgument{
Description: description,
IsRequired: false,
Format: "string",
Value: arg,
Default: arg,
Type: "positional",
ValueHint: arg,
})
}
}
if execute == "" {
return runtimeArguments
}
// Create package
// Split the execute command into parts, handling quoted strings
parts := smartSplit(execute)
if len(parts) <= 1 {
return runtimeArguments
}
// Skip the first part (command) and add each argument as a runtime argument
for i, arg := range parts[1:] {
description := getArgumentDescription(arg, i, parts)
runtimeArguments = append(runtimeArguments, RuntimeArgument{
Description: description,
IsRequired: false,
Format: "string",
Value: arg,
Default: arg,
Type: "positional",
ValueHint: arg,
})
}
return runtimeArguments
}
// getArgumentDescription provides a description for a runtime argument
func getArgumentDescription(arg string, index int, parts []string) string {
description := fmt.Sprintf("Runtime argument %d", index+1)
// Try to provide better descriptions based on common patterns
switch {
case strings.HasPrefix(arg, "--"):
description = fmt.Sprintf("Command line flag: %s", arg)
case strings.HasPrefix(arg, "-") && len(arg) == 2:
description = fmt.Sprintf("Command line option: %s", arg)
case strings.Contains(arg, "="):
description = fmt.Sprintf("Configuration parameter: %s", arg)
case index > 0 && strings.HasPrefix(parts[index], "-"):
description = fmt.Sprintf("Value for %s", parts[index])
}
return description
}
func createServerStructure(
name, description, version, repoURL, repoSource, registryName,
packageName, packageVersion, runtimeHint, execute string,
envVars []string, packageArgs []string, status string,
) ServerJSON {
// Parse environment variables
environmentVariables := parseEnvironmentVariables(envVars)
// Parse package arguments
packageArguments := parsePackageArguments(packageArgs)
// Parse execute command to create runtime arguments
runtimeArguments := parseRuntimeArguments(execute)
// Determine registry_type and registry_base_url from registryName
var registryTypeValue, registryBaseURLValue, identifier string
switch registryName {
case registryNPM:
registryTypeValue = "npm"
registryBaseURLValue = "https://registry.npmjs.org"
identifier = packageName
case registryPyPI:
registryTypeValue = "pypi"
registryBaseURLValue = "https://pypi.org"
identifier = packageName
case registryDocker:
registryTypeValue = "docker-hub"
registryBaseURLValue = "https://docker.io"
identifier = packageName
case registryNuGet:
registryTypeValue = "nuget"
registryBaseURLValue = "https://api.nuget.org"
identifier = packageName
case registryURL:
// For URL-based packages, determine registry from URL and file type
if strings.HasSuffix(strings.ToLower(packageName), ".mcpb") {
registryTypeValue = "mcpb"
// Determine base URL from the download URL
switch {
case strings.Contains(packageName, "github.com"):
registryBaseURLValue = "https://github.com"
case strings.Contains(packageName, "gitlab.com"):
registryBaseURLValue = "https://gitlab.com"
default:
registryBaseURLValue = ""
}
} else {
registryTypeValue = "url"
registryBaseURLValue = ""
}
identifier = packageName
default:
// Unknown or custom registry
registryTypeValue = registryName
registryBaseURLValue = ""
identifier = packageName
}
// Create package with new structured fields
pkg := Package{
RegistryName: registryName,
Name: packageName,
RegistryType: registryTypeValue,
RegistryBaseURL: registryBaseURLValue,
Identifier: identifier,
Version: packageVersion,
RuntimeHint: runtimeHint,
RuntimeArguments: runtimeArguments,

View file

@ -5,8 +5,9 @@
"status": "active",
"packages": [
{
"package_type": "javascript",
"registry_name": "npm",
"name": "io.github.<owner>/<server-name>",
"identifier": "io.github.<owner>/<server-name>",
"version": "0.2.23",
"package_arguments": [
{
@ -26,8 +27,9 @@
}
]
},{
"registry_name": "docker",
"name": "io.github.<owner>/<server-name>-cli",
"package_type": "docker",
"registry_name": "docker-hub",
"identifier": "io.github.<owner>/<server-name>-cli:0.123.223",
"version": "0.123.223",
"runtime_hint": "docker",
"runtime_arguments": [