registry/internal/database/import.go
adam jones c0a94cca8c
Rename api/v1 -> api/v0 to match API paths (#320)
This PR renames the API paths from v1 to v0 to align with the actual API
versioning scheme.
2025-08-28 15:06:00 +01:00

141 lines
3.9 KiB
Go

package database
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
)
// 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) ([]*apiv0.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 []apiv0.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 []*apiv0.ServerRecord{}, nil
}
// Convert ServerResponse to ServerRecord
var records []*apiv0.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) ([]*apiv0.ServerRecord, error) {
var allRecords []*apiv0.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 []apiv0.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 apiv0.ServerRecord) *apiv0.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 &apiv0.ServerRecord{
Server: response.Server,
XIOModelContextProtocolRegistry: registryMetadata,
XPublisher: publisherExtensions,
}
}