registry/tools/migrate-seed/main.go
Radoslav Dimitrov 8d7e471fd3
Allow for vendor extensions on servers (#298)
<!-- Provide a brief summary of your changes -->
This PR serves as a placeholder and brings together all of the changes
related to supporting vendor extensions passed through the API. The idea
is to have a single place to track and review the work as it comes
together (even if it’s still a bit of a work in progress).

**The changes in this PR cover:**
* Updates on the data model
* Updates on both databases (in-memory and postgresql), added a new
table to PSQL for tracking the extensions leaving the server table only
for the clean server.json properties
* Updates the seed.json file (thus the big line change)
* Adds a seed-migrate tool to help migrating an existing seed file to
the new format
* Updates the seed import mechanism to support this
* Updates the existing tests - unit, integration and validation tests
* Adds the publish handler to the OpenAPI spec (it was missing)
* Fixes linting errors
* Updates on the API side on (publish, list, get) to support the new
format of `ServerResponse` -
```
{
  "server": {},
  "x-io.modelcontextprotocol.registry": {}, /*not available for publish since this gets populated by the registry*/
  "x-publisher": {}
}
```

## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->
Motivated by the discussions in:
https://github.com/modelcontextprotocol/registry/discussions/284

## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->
Locally and by running all of the tests

## Breaking Changes
<!-- Will users need to update their code or configurations? -->
Yes, it changes both the API and the format of the seed file
 
## 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)
- [ ] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update

---

Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
Co-authored-by: adam jones <domdomegg+git@gmail.com>
2025-08-23 09:59:03 +01:00

170 lines
No EOL
4.6 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/modelcontextprotocol/registry/internal/model"
)
// OldServerFormat represents the legacy seed format
type OldServerFormat struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Status string `json:"status,omitempty"`
Repository model.Repository `json:"repository"`
VersionDetail OldVersionDetail `json:"version_detail"`
Packages []model.Package `json:"packages,omitempty"`
Remotes []model.Remote `json:"remotes,omitempty"`
Extensions map[string]interface{} `json:"extensions,omitempty"`
}
// OldVersionDetail represents the legacy version format with registry metadata
type OldVersionDetail struct {
Version string `json:"version"`
ReleaseDate string `json:"release_date,omitempty"`
IsLatest bool `json:"is_latest,omitempty"`
}
func main() {
if len(os.Args) < 3 {
log.Println("Usage: migrate-seed <input-source> <output-file>")
log.Println(" input-source: file path or HTTP URL")
log.Println(" output-file: path for the migrated seed file")
os.Exit(1)
}
inputSource := os.Args[1]
outputFile := os.Args[2]
log.Printf("Migrating seed from %s to %s", inputSource, outputFile)
// Read source data
var data []byte
var err error
if strings.HasPrefix(inputSource, "http://") || strings.HasPrefix(inputSource, "https://") {
data, err = fetchFromHTTP(inputSource)
} else {
data, err = os.ReadFile(inputSource)
}
if err != nil {
log.Fatalf("Failed to read input source: %v", err)
}
// Parse old format
var oldServers []OldServerFormat
if err := json.Unmarshal(data, &oldServers); err != nil {
log.Fatalf("Failed to parse old format: %v", err)
}
log.Printf("Found %d servers to migrate", len(oldServers))
// Convert to new format
var newServers []model.ServerResponse
for _, old := range oldServers {
converted := convertServer(old)
newServers = append(newServers, converted)
}
// Write migrated data
migratedData, err := json.MarshalIndent(newServers, "", " ")
if err != nil {
log.Fatalf("Failed to marshal migrated data: %v", err)
}
// Ensure output directory exists
if err := os.MkdirAll(filepath.Dir(outputFile), 0755); err != nil {
log.Fatalf("Failed to create output directory: %v", err)
}
if err := os.WriteFile(outputFile, migratedData, 0600); err != nil {
log.Fatalf("Failed to write output file: %v", err)
}
log.Printf("Successfully migrated %d servers to %s", len(newServers), outputFile)
}
func fetchFromHTTP(url string) ([]byte, error) {
log.Printf("Fetching data from HTTP: %s", url)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create 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 convertServer(old OldServerFormat) model.ServerResponse {
// Create pure MCP server specification
server := model.ServerDetail{
Name: old.Name,
Description: old.Description,
Repository: old.Repository,
VersionDetail: model.VersionDetail{
Version: old.VersionDetail.Version,
},
Packages: old.Packages,
Remotes: old.Remotes,
}
// Set status if provided, otherwise default to active
if old.Status != "" {
server.Status = model.ServerStatus(old.Status)
} else {
server.Status = model.ServerStatusActive
}
// Create registry metadata
registryMetadata := model.RegistryMetadata{
ID: old.ID,
IsLatest: old.VersionDetail.IsLatest,
ReleaseDate: old.VersionDetail.ReleaseDate,
}
// Publisher extensions (combine any additional fields)
publisherExtensions := make(map[string]interface{})
if old.Extensions != nil {
for k, v := range old.Extensions {
publisherExtensions[k] = v
}
}
// Create the extension wrapper response
response := model.ServerResponse{
Server: server,
}
// Add registry metadata extension
response.XIOModelContextProtocolRegistry = registryMetadata.CreateRegistryExtensions()["x-io.modelcontextprotocol.registry"]
// Add publisher extensions if any
if len(publisherExtensions) > 0 {
response.XPublisher = publisherExtensions
}
return response
}