Deduplicate server JSON structs between publisher and registry (#318)

<!-- Provide a brief summary of your changes -->

## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->

The following PR eliminates duplicate struct definitions by migrating
the publisher tool to use canonical types from pkg/model, creating a
single source of truth across the codebase. The publisher tool
previously had its own copies of structs that were duplicated, ie.
ServerJSON, Package, Repository, VersionDetail, EnvironmentVariable,
RuntimeArgument and a few others which was not optimal


**Changes in the PR:**
- Refactored the structs to single canonical types located at pkg/model
- Refactor the implementation to use/build canonical structures directly
  - Remove all duplicate definitions (5 structs eliminated)
- Publisher now uses model.ServerJSON, model.Package, model.Repository,
etc.
- Renamed some of the structs to better reflect their use case, i.e.
ServerJSON instead of ServerDetail
- Fixes an issue where the Remote property was missing from the previous
publisher copy of the Server

**Benefits:**
  - Single source of truth - Schema changes in one place
  - Type safety - Compiler catches mismatches
  - Enhanced validation - Field constraints and bson tags
  - Zero breaking changes - Same CLI interface and JSON output
  - Future-proof - New canonical fields automatically available

Supersedes: #217 and #240

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

Locally

## Breaking Changes
<!-- Will users need to update their code or configurations? -->
No

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] 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. -->
- [ ] 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
- [ ] I have added or updated documentation as needed

## Additional context
<!-- Add any other context, implementation notes, or design decisions
-->

---------

Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
This commit is contained in:
Radoslav Dimitrov 2025-08-28 16:58:15 +03:00 • committed by GitHub
commit 099a2d702e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -13,6 +13,7 @@ import (
"os"
"strings"
"github.com/modelcontextprotocol/registry/pkg/model"
"github.com/modelcontextprotocol/registry/tools/publisher/auth"
)
@ -27,50 +28,6 @@ const (
)
// Server structure types for JSON generation
type Repository struct {
URL string `json:"url"`
Source string `json:"source"`
}
type VersionDetail struct {
Version string `json:"version"`
}
type EnvironmentVariable struct {
Name string `json:"name"`
Description string `json:"description"`
}
type RuntimeArgument struct {
Description string `json:"description"`
IsRequired bool `json:"is_required"`
Format string `json:"format"`
Value string `json:"value"`
Default string `json:"default"`
Type string `json:"type"`
ValueHint string `json:"value_hint"`
}
type Package struct {
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"`
EnvironmentVariables []EnvironmentVariable `json:"environment_variables,omitempty"`
}
type ServerJSON struct {
Schema string `json:"$schema"`
Name string `json:"name"`
Description string `json:"description"`
Status string `json:"status,omitempty"`
Repository Repository `json:"repository"`
VersionDetail VersionDetail `json:"version_detail"`
Packages []Package `json:"packages"`
}
func main() {
if len(os.Args) < 2 {
@ -410,29 +367,31 @@ func publishToRegistry(registryURL string, mcpData []byte, token string) error {
}
// parseEnvironmentVariables parses environment variable specifications
func parseEnvironmentVariables(envVars []string) []EnvironmentVariable {
var environmentVariables []EnvironmentVariable
func parseEnvironmentVariables(envVars []string) []model.KeyValueInput {
var environmentVariables []model.KeyValueInput
for _, envVar := range envVars {
parts := strings.SplitN(envVar, ":", 2)
description := fmt.Sprintf("Environment variable for %s", parts[0])
if len(parts) == 2 {
environmentVariables = append(environmentVariables, EnvironmentVariable{
Name: parts[0],
Description: parts[1],
})
} else {
// If no description provided, use a default
environmentVariables = append(environmentVariables, EnvironmentVariable{
Name: parts[0],
Description: fmt.Sprintf("Environment variable for %s", parts[0]),
})
description = parts[1]
}
environmentVariables = append(environmentVariables, model.KeyValueInput{
Name: parts[0],
InputWithVariables: model.InputWithVariables{
Input: model.Input{
Description: description,
Format: model.FormatString,
},
},
})
}
return environmentVariables
}
// parsePackageArguments parses package argument specifications
func parsePackageArguments(packageArgs []string) []RuntimeArgument {
var packageArguments []RuntimeArgument
func parsePackageArguments(packageArgs []string) []model.Argument {
var packageArguments []model.Argument
for i, pkgArg := range packageArgs {
parts := strings.SplitN(pkgArg, ":", 2)
value := parts[0]
@ -441,22 +400,26 @@ func parsePackageArguments(packageArgs []string) []RuntimeArgument {
description = parts[1]
}
packageArguments = append(packageArguments, RuntimeArgument{
Description: description,
IsRequired: true, // Package arguments are typically required
Format: "string",
Value: value,
Default: value,
Type: "positional",
ValueHint: value,
packageArguments = append(packageArguments, model.Argument{
Type: model.ArgumentTypePositional,
ValueHint: value,
InputWithVariables: model.InputWithVariables{
Input: model.Input{
Description: description,
IsRequired: true, // Package arguments are typically required
Format: model.FormatString,
Value: value,
Default: value,
},
},
})
}
return packageArguments
}
// parseRuntimeArguments parses the execute command to create runtime arguments
func parseRuntimeArguments(execute string) []RuntimeArgument {
var runtimeArguments []RuntimeArgument
func parseRuntimeArguments(execute string) []model.Argument {
var runtimeArguments []model.Argument
if execute == "" {
return runtimeArguments
}
@ -470,14 +433,18 @@ func parseRuntimeArguments(execute string) []RuntimeArgument {
// 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,
runtimeArguments = append(runtimeArguments, model.Argument{
Type: model.ArgumentTypePositional,
ValueHint: arg,
InputWithVariables: model.InputWithVariables{
Input: model.Input{
Description: description,
IsRequired: false,
Format: model.FormatString,
Value: arg,
Default: arg,
},
},
})
}
@ -507,7 +474,7 @@ func createServerStructure(
name, description, version, repoURL, repoSource, registryName,
packageName, packageVersion, runtimeHint, execute string,
envVars []string, packageArgs []string, status string,
) ServerJSON {
) model.ServerJSON {
// Parse environment variables
environmentVariables := parseEnvironmentVariables(envVars)
@ -562,31 +529,42 @@ func createServerStructure(
}
// Create package with new structured fields
pkg := Package{
pkg := model.Package{
RegistryType: registryTypeValue,
RegistryBaseURL: registryBaseURLValue,
Identifier: identifier,
Version: packageVersion,
RuntimeHint: runtimeHint,
RunTimeHint: runtimeHint,
RuntimeArguments: runtimeArguments,
PackageArguments: packageArguments,
EnvironmentVariables: environmentVariables,
}
// Convert string status to model.Status type
var statusValue model.Status
switch status {
case "active":
statusValue = model.StatusActive
case "deprecated":
statusValue = model.StatusDeprecated
default:
statusValue = model.StatusActive // fallback to active
}
// Create server structure
return ServerJSON{
return model.ServerJSON{
Schema: "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
Name: name,
Description: description,
Status: status,
Repository: Repository{
Status: statusValue,
Repository: model.Repository{
URL: repoURL,
Source: repoSource,
},
VersionDetail: VersionDetail{
VersionDetail: model.VersionDetail{
Version: version,
},
Packages: []Package{pkg},
Packages: []model.Package{pkg},
}
}