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:
parent
9d04671ee3
commit
099a2d702e
1 changed files with 1280 additions and 1408 deletions
|
|
@ -8,17 +8,16 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/internal/model"
|
||||
apiv1 "github.com/modelcontextprotocol/registry/pkg/api/v1"
|
||||
)
|
||||
|
||||
// ReadSeedFile reads seed data from various sources:
|
||||
// 1. Local file paths (*.json files) - expects extension wrapper format
|
||||
// 2. Direct HTTP URLs to seed.json files - expects extension wrapper format
|
||||
// 2. Direct HTTP URLs to seed.json files - expects extension wrapper format
|
||||
// 3. Registry root URLs (automatically appends /v0/servers and paginates)
|
||||
// Only the extension wrapper format is supported (array of ServerResponse objects)
|
||||
func ReadSeedFile(ctx context.Context, path string) ([]*model.ServerRecord, error) {
|
||||
func ReadSeedFile(ctx context.Context, path string) ([]*apiv1.ServerRecord, error) {
|
||||
var data []byte
|
||||
var err error
|
||||
|
||||
|
|
@ -40,17 +39,17 @@ func ReadSeedFile(ctx context.Context, path string) ([]*model.ServerRecord, erro
|
|||
}
|
||||
|
||||
// Parse extension wrapper format (only supported format)
|
||||
var serverResponses []model.ServerResponse
|
||||
var serverResponses []apiv1.ServerRecord
|
||||
if err := json.Unmarshal(data, &serverResponses); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse seed data as extension wrapper format: %w", err)
|
||||
}
|
||||
|
||||
if len(serverResponses) == 0 {
|
||||
return []*model.ServerRecord{}, nil
|
||||
return []*apiv1.ServerRecord{}, nil
|
||||
}
|
||||
|
||||
// Convert ServerResponse to ServerRecord
|
||||
var records []*model.ServerRecord
|
||||
var records []*apiv1.ServerRecord
|
||||
for _, response := range serverResponses {
|
||||
record := convertServerResponseToRecord(response)
|
||||
records = append(records, record)
|
||||
|
|
@ -78,8 +77,8 @@ func fetchFromHTTP(ctx context.Context, url string) ([]byte, error) {
|
|||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func fetchFromRegistryAPI(ctx context.Context, baseURL string) ([]*model.ServerRecord, error) {
|
||||
var allRecords []*model.ServerRecord
|
||||
func fetchFromRegistryAPI(ctx context.Context, baseURL string) ([]*apiv1.ServerRecord, error) {
|
||||
var allRecords []*apiv1.ServerRecord
|
||||
cursor := ""
|
||||
|
||||
for {
|
||||
|
|
@ -98,7 +97,7 @@ func fetchFromRegistryAPI(ctx context.Context, baseURL string) ([]*model.ServerR
|
|||
}
|
||||
|
||||
var response struct {
|
||||
Servers []model.ServerResponse `json:"servers"`
|
||||
Servers []apiv1.ServerRecord `json:"servers"`
|
||||
Metadata *struct {
|
||||
NextCursor string `json:"next_cursor,omitempty"`
|
||||
} `json:"metadata,omitempty"`
|
||||
|
|
@ -124,55 +123,20 @@ func fetchFromRegistryAPI(ctx context.Context, baseURL string) ([]*model.ServerR
|
|||
return allRecords, nil
|
||||
}
|
||||
|
||||
func convertServerResponseToRecord(response model.ServerResponse) *model.ServerRecord {
|
||||
// Extract registry metadata from the extension
|
||||
registryExt := response.XIOModelContextProtocolRegistry
|
||||
|
||||
// Parse timestamps
|
||||
publishedAt, _ := time.Parse(time.RFC3339, getStringFromInterface(registryExt, "published_at"))
|
||||
updatedAt, _ := time.Parse(time.RFC3339, getStringFromInterface(registryExt, "updated_at"))
|
||||
|
||||
registryMetadata := model.RegistryMetadata{
|
||||
ID: getStringFromInterface(registryExt, "id"),
|
||||
IsLatest: getBoolFromInterface(registryExt, "is_latest"),
|
||||
PublishedAt: publishedAt,
|
||||
UpdatedAt: updatedAt,
|
||||
ReleaseDate: getStringFromInterface(registryExt, "release_date"),
|
||||
}
|
||||
func convertServerResponseToRecord(response apiv1.ServerRecord) *apiv1.ServerRecord {
|
||||
// The registry extensions are already properly typed, so we can use them directly
|
||||
registryMetadata := response.XIOModelContextProtocolRegistry
|
||||
|
||||
// Publisher extensions
|
||||
publisherExtensions := make(map[string]interface{})
|
||||
if response.XPublisher != nil {
|
||||
if publisherMap, ok := response.XPublisher.(map[string]interface{}); ok {
|
||||
publisherExtensions = publisherMap
|
||||
}
|
||||
publisherExtensions := response.XPublisher
|
||||
if publisherExtensions == nil {
|
||||
publisherExtensions = make(map[string]interface{})
|
||||
}
|
||||
|
||||
return &model.ServerRecord{
|
||||
ServerJSON: response.Server,
|
||||
RegistryMetadata: registryMetadata,
|
||||
PublisherExtensions: publisherExtensions,
|
||||
return &apiv1.ServerRecord{
|
||||
Server: response.Server,
|
||||
XIOModelContextProtocolRegistry: registryMetadata,
|
||||
XPublisher: publisherExtensions,
|
||||
}
|
||||
}
|
||||
|
||||
func getStringFromInterface(data interface{}, key string) string {
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
if value, exists := dataMap[key]; exists {
|
||||
if strValue, ok := value.(string); ok {
|
||||
return strValue
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getBoolFromInterface(data interface{}, key string) bool {
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
if value, exists := dataMap[key]; exists {
|
||||
if boolValue, ok := value.(bool); ok {
|
||||
return boolValue
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Loading…
Reference in a new issue