<!-- 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>
142 lines
3.9 KiB
Go
142 lines
3.9 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
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
|
|
// 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) ([]*apiv1.ServerRecord, error) {
|
|
var data []byte
|
|
var err error
|
|
|
|
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
|
|
// Handle HTTP URLs
|
|
if strings.HasSuffix(path, "/v0/servers") || strings.Contains(path, "/v0/servers") {
|
|
// This is a registry API endpoint - fetch paginated data
|
|
return fetchFromRegistryAPI(ctx, path)
|
|
}
|
|
// This is a direct file URL
|
|
data, err = fetchFromHTTP(ctx, path)
|
|
} else {
|
|
// Handle local file paths
|
|
data, err = os.ReadFile(path)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read seed data from %s: %w", path, err)
|
|
}
|
|
|
|
// Parse extension wrapper format (only supported format)
|
|
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 []*apiv1.ServerRecord{}, nil
|
|
}
|
|
|
|
// Convert ServerResponse to ServerRecord
|
|
var records []*apiv1.ServerRecord
|
|
for _, response := range serverResponses {
|
|
record := convertServerResponseToRecord(response)
|
|
records = append(records, record)
|
|
}
|
|
|
|
return records, nil
|
|
}
|
|
|
|
func fetchFromHTTP(ctx context.Context, url string) ([]byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create HTTP request: %w", err)
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch from HTTP: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("HTTP request failed with status: %d", resp.StatusCode)
|
|
}
|
|
|
|
return io.ReadAll(resp.Body)
|
|
}
|
|
|
|
func fetchFromRegistryAPI(ctx context.Context, baseURL string) ([]*apiv1.ServerRecord, error) {
|
|
var allRecords []*apiv1.ServerRecord
|
|
cursor := ""
|
|
|
|
for {
|
|
url := baseURL
|
|
if cursor != "" {
|
|
if strings.Contains(url, "?") {
|
|
url += "&cursor=" + cursor
|
|
} else {
|
|
url += "?cursor=" + cursor
|
|
}
|
|
}
|
|
|
|
data, err := fetchFromHTTP(ctx, url)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch page from registry API: %w", err)
|
|
}
|
|
|
|
var response struct {
|
|
Servers []apiv1.ServerRecord `json:"servers"`
|
|
Metadata *struct {
|
|
NextCursor string `json:"next_cursor,omitempty"`
|
|
} `json:"metadata,omitempty"`
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &response); err != nil {
|
|
return nil, fmt.Errorf("failed to parse registry API response: %w", err)
|
|
}
|
|
|
|
// Convert and add servers
|
|
for _, serverResponse := range response.Servers {
|
|
record := convertServerResponseToRecord(serverResponse)
|
|
allRecords = append(allRecords, record)
|
|
}
|
|
|
|
// Check if there's a next page
|
|
if response.Metadata == nil || response.Metadata.NextCursor == "" {
|
|
break
|
|
}
|
|
cursor = response.Metadata.NextCursor
|
|
}
|
|
|
|
return allRecords, nil
|
|
}
|
|
|
|
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 := response.XPublisher
|
|
if publisherExtensions == nil {
|
|
publisherExtensions = make(map[string]interface{})
|
|
}
|
|
|
|
return &apiv1.ServerRecord{
|
|
Server: response.Server,
|
|
XIOModelContextProtocolRegistry: registryMetadata,
|
|
XPublisher: publisherExtensions,
|
|
}
|
|
}
|
|
|