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>
This commit is contained in:
Radoslav Dimitrov 2025-08-23 11:59:03 +03:00 • committed by GitHub
commit 8d7e471fd3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 19980 additions and 15589 deletions

170
tools/migrate-seed/main.go Normal file
View file

@ -0,0 +1,170 @@
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
}

View file

@ -0,0 +1,355 @@
package main
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/modelcontextprotocol/registry/internal/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// createTestLegacySeed creates a small test file with legacy format data
func createTestLegacySeed(t *testing.T) string {
t.Helper()
testData := []OldServerFormat{
{
ID: "4e9cf4cf-71f6-4aca-bae8-2d10a29ca2e0",
Name: "io.github.21st-dev/magic-mcp",
Description: "It's like v0 but in your Cursor/WindSurf/Cline. 21st dev Magic MCP server for working with your frontend like Magic",
Repository: model.Repository{
URL: "https://github.com/21st-dev/magic-mcp",
Source: "github",
ID: "935450522",
},
VersionDetail: OldVersionDetail{
Version: "0.0.1-seed",
ReleaseDate: "2025-05-16T18:56:49Z",
IsLatest: true,
},
Packages: []model.Package{
{
RegistryName: "npm",
Name: "@21st-dev/magic",
Version: "0.0.46",
EnvironmentVariables: []model.KeyValueInput{
{
Name: "API_KEY",
InputWithVariables: model.InputWithVariables{
Input: model.Input{
Description: "${input:apiKey}",
},
},
},
},
},
},
},
{
ID: "d3669201-252f-403c-944b-c3ec0845782b",
Name: "io.github.adfin-engineering/mcp-server-adfin",
Description: "A Model Context Protocol Server for connecting with Adfin APIs",
Repository: model.Repository{
URL: "https://github.com/Adfin-Engineering/mcp-server-adfin",
Source: "github",
ID: "951338147",
},
VersionDetail: OldVersionDetail{
Version: "0.0.1-seed",
ReleaseDate: "2025-05-16T18:56:52Z",
IsLatest: true,
},
Packages: []model.Package{
{
RegistryName: "pypi",
Name: "adfinmcp",
Version: "0.1.0",
},
},
},
}
tempFile, err := os.CreateTemp("", "test-legacy-seed-*.json")
require.NoError(t, err)
defer tempFile.Close()
jsonData, err := json.MarshalIndent(testData, "", " ")
require.NoError(t, err)
_, err = tempFile.Write(jsonData)
require.NoError(t, err)
return tempFile.Name()
}
func TestMigrationCLI(t *testing.T) {
// Create test legacy seed file
legacyFile := createTestLegacySeed(t)
defer os.Remove(legacyFile)
// Create output file path
outputFile := filepath.Join(t.TempDir(), "migrated-output.json")
// Run migration (simulate command line execution)
err := migrateSeed(legacyFile, outputFile)
require.NoError(t, err, "Migration should succeed")
// Read the output file
outputData, err := os.ReadFile(outputFile)
require.NoError(t, err, "Should be able to read output file")
// Parse as ServerResponse array (new format)
var migratedServers []model.ServerResponse
err = json.Unmarshal(outputData, &migratedServers)
require.NoError(t, err, "Output should be valid JSON in ServerResponse format")
// Verify we have the expected number of servers
assert.Len(t, migratedServers, 2, "Should have migrated 2 servers")
// Verify first server structure
server1 := migratedServers[0]
assert.Equal(t, "io.github.21st-dev/magic-mcp", server1.Server.Name)
assert.Equal(t, "It's like v0 but in your Cursor/WindSurf/Cline. 21st dev Magic MCP server for working with your frontend like Magic", server1.Server.Description)
assert.Equal(t, "https://github.com/21st-dev/magic-mcp", server1.Server.Repository.URL)
assert.Equal(t, "0.0.1-seed", server1.Server.VersionDetail.Version)
assert.Len(t, server1.Server.Packages, 1)
assert.Equal(t, "npm", server1.Server.Packages[0].RegistryName)
assert.Equal(t, "@21st-dev/magic", server1.Server.Packages[0].Name)
// Verify registry metadata extension
assert.NotNil(t, server1.XIOModelContextProtocolRegistry, "Should have registry metadata extension")
registryMeta1, ok := server1.XIOModelContextProtocolRegistry.(map[string]interface{})
require.True(t, ok, "Registry metadata should be a map")
assert.Equal(t, "4e9cf4cf-71f6-4aca-bae8-2d10a29ca2e0", registryMeta1["id"])
assert.Equal(t, true, registryMeta1["is_latest"])
assert.Equal(t, "2025-05-16T18:56:49Z", registryMeta1["release_date"])
// Verify second server structure
server2 := migratedServers[1]
assert.Equal(t, "io.github.adfin-engineering/mcp-server-adfin", server2.Server.Name)
assert.Equal(t, "A Model Context Protocol Server for connecting with Adfin APIs", server2.Server.Description)
assert.Equal(t, "pypi", server2.Server.Packages[0].RegistryName)
assert.Equal(t, "adfinmcp", server2.Server.Packages[0].Name)
// Verify registry metadata for second server
registryMeta2, ok := server2.XIOModelContextProtocolRegistry.(map[string]interface{})
require.True(t, ok, "Registry metadata should be a map")
assert.Equal(t, "d3669201-252f-403c-944b-c3ec0845782b", registryMeta2["id"])
assert.Equal(t, true, registryMeta2["is_latest"])
assert.Equal(t, "2025-05-16T18:56:52Z", registryMeta2["release_date"])
// Verify no x-publisher extensions (should be empty/nil for seed data)
assert.Nil(t, server1.XPublisher, "Seed data should not have x-publisher extensions")
assert.Nil(t, server2.XPublisher, "Seed data should not have x-publisher extensions")
}
// migrateSeed is a testable version of the main migration logic
func migrateSeed(inputFile, outputFile string) error {
// Read source data
data, err := os.ReadFile(inputFile)
if err != nil {
return err
}
// Parse old format
var oldServers []OldServerFormat
if err := json.Unmarshal(data, &oldServers); err != nil {
return err
}
// 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 {
return err
}
// Ensure output directory exists
if err := os.MkdirAll(filepath.Dir(outputFile), 0755); err != nil {
return err
}
return os.WriteFile(outputFile, migratedData, 0600)
}
func TestMigrationCLI_CommandLine(t *testing.T) {
// Create test legacy seed file
legacyFile := createTestLegacySeed(t)
defer os.Remove(legacyFile)
// Create output file path
outputFile := filepath.Join(t.TempDir(), "cli-migrated-output.json")
// Simulate command line arguments
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
os.Args = []string{"migrate-seed", legacyFile, outputFile}
// Capture any panics or exits from main()
defer func() {
if r := recover(); r != nil {
t.Errorf("main() panicked: %v", r)
}
}()
// Test the main function (Note: this would normally call os.Exit, but in tests it should return)
main()
// Verify the output file was created and has correct content
require.FileExists(t, outputFile, "Output file should be created")
outputData, err := os.ReadFile(outputFile)
require.NoError(t, err, "Should be able to read output file")
var migratedServers []model.ServerResponse
err = json.Unmarshal(outputData, &migratedServers)
require.NoError(t, err, "Output should be valid JSON in ServerResponse format")
// Basic verification
assert.Len(t, migratedServers, 2, "Should have migrated 2 servers")
assert.Equal(t, "io.github.21st-dev/magic-mcp", migratedServers[0].Server.Name)
assert.Equal(t, "io.github.adfin-engineering/mcp-server-adfin", migratedServers[1].Server.Name)
}
func TestMigrationCLI_ErrorHandling(t *testing.T) {
t.Run("Invalid JSON input", func(t *testing.T) {
// Create invalid JSON file
invalidFile, err := os.CreateTemp("", "invalid-*.json")
require.NoError(t, err)
defer os.Remove(invalidFile.Name())
_, err = invalidFile.WriteString(`{"invalid": json}`)
require.NoError(t, err)
invalidFile.Close()
outputFile := filepath.Join(t.TempDir(), "output.json")
err = migrateSeed(invalidFile.Name(), outputFile)
assert.Error(t, err, "Should error on invalid JSON")
})
t.Run("Missing input file", func(t *testing.T) {
outputFile := filepath.Join(t.TempDir(), "output.json")
err := migrateSeed("/nonexistent/file.json", outputFile)
assert.Error(t, err, "Should error on missing input file")
})
t.Run("Empty input file", func(t *testing.T) {
// Create empty JSON array
emptyFile, err := os.CreateTemp("", "empty-*.json")
require.NoError(t, err)
defer os.Remove(emptyFile.Name())
_, err = emptyFile.WriteString("[]")
require.NoError(t, err)
emptyFile.Close()
outputFile := filepath.Join(t.TempDir(), "output.json")
err = migrateSeed(emptyFile.Name(), outputFile)
assert.NoError(t, err, "Empty array should be valid")
// Verify empty output
outputData, err := os.ReadFile(outputFile)
require.NoError(t, err)
var result []model.ServerResponse
err = json.Unmarshal(outputData, &result)
require.NoError(t, err, "Should be valid JSON")
assert.Len(t, result, 0, "Should have no servers")
})
}
func TestMigrationCLI_GoldenFile(t *testing.T) {
// Test migration with predefined input and expected output files
inputFile := "testdata/legacy-input.json"
expectedFile := "testdata/expected-output.json"
// Ensure test files exist
require.FileExists(t, inputFile, "Input test file should exist")
require.FileExists(t, expectedFile, "Expected output test file should exist")
// Run migration on test input
outputFile := filepath.Join(t.TempDir(), "golden-output.json")
err := migrateSeed(inputFile, outputFile)
require.NoError(t, err, "Migration should succeed")
// Read expected and actual output
expectedData, err := os.ReadFile(expectedFile)
require.NoError(t, err, "Should be able to read expected output")
actualData, err := os.ReadFile(outputFile)
require.NoError(t, err, "Should be able to read actual output")
// Parse both as JSON to normalize formatting and compare semantically
var expectedJSON, actualJSON []model.ServerResponse
err = json.Unmarshal(expectedData, &expectedJSON)
require.NoError(t, err, "Expected output should be valid JSON")
err = json.Unmarshal(actualData, &actualJSON)
require.NoError(t, err, "Actual output should be valid JSON")
// Compare the parsed structures
assert.Equal(t, expectedJSON, actualJSON, "Migration output should match expected golden file")
// Also compare server count
assert.Len(t, actualJSON, len(expectedJSON), "Should have same number of servers")
// Verify specific server names to ensure ordering is preserved
if len(expectedJSON) >= 2 && len(actualJSON) >= 2 {
assert.Equal(t, expectedJSON[0].Server.Name, actualJSON[0].Server.Name, "First server name should match")
assert.Equal(t, expectedJSON[1].Server.Name, actualJSON[1].Server.Name, "Second server name should match")
}
}
func TestGoldenFiles_Validation(t *testing.T) {
// Validate that the golden files themselves are valid
t.Run("Input file is valid legacy format", func(t *testing.T) {
inputData, err := os.ReadFile("testdata/legacy-input.json")
require.NoError(t, err, "Should be able to read input file")
var legacyServers []OldServerFormat
err = json.Unmarshal(inputData, &legacyServers)
require.NoError(t, err, "Input file should be valid legacy format JSON")
assert.Len(t, legacyServers, 2, "Input should contain exactly 2 servers")
// Verify it contains expected server names
names := []string{legacyServers[0].Name, legacyServers[1].Name}
assert.Contains(t, names, "io.github.21st-dev/magic-mcp")
assert.Contains(t, names, "io.github.adfin-engineering/mcp-server-adfin")
})
t.Run("Expected output is valid extension wrapper format", func(t *testing.T) {
expectedData, err := os.ReadFile("testdata/expected-output.json")
require.NoError(t, err, "Should be able to read expected output file")
var expectedServers []model.ServerResponse
err = json.Unmarshal(expectedData, &expectedServers)
require.NoError(t, err, "Expected output should be valid ServerResponse format JSON")
assert.Len(t, expectedServers, 2, "Expected output should contain exactly 2 servers")
// Verify structure of first server
server1 := expectedServers[0]
assert.NotEmpty(t, server1.Server.Name, "Server should have a name")
assert.NotEmpty(t, server1.Server.Description, "Server should have a description")
assert.NotNil(t, server1.XIOModelContextProtocolRegistry, "Should have registry metadata")
assert.Nil(t, server1.XPublisher, "Seed data should not have publisher extensions")
// Verify registry metadata contains required fields
registryMeta, ok := server1.XIOModelContextProtocolRegistry.(map[string]interface{})
require.True(t, ok, "Registry metadata should be a map")
assert.Contains(t, registryMeta, "id", "Registry metadata should have id")
assert.Contains(t, registryMeta, "is_latest", "Registry metadata should have is_latest")
assert.Contains(t, registryMeta, "release_date", "Registry metadata should have release_date")
})
}

38
tools/migrate-seed/testdata/README.md vendored Normal file
View file

@ -0,0 +1,38 @@
# Migration Test Data
This directory contains golden files for testing the seed migration CLI tool.
## Files
- **`legacy-input.json`** - Sample input file in the legacy seed format (flat structure with registry metadata mixed in)
- **`expected-output.json`** - Expected output after migration to extension wrapper format
## Usage
The golden file test (`TestMigrationCLI_GoldenFile`) runs the migration tool on `legacy-input.json` and compares the output against `expected-output.json` to ensure the migration produces the correct result.
## Updating Golden Files
If you make changes to the migration logic, you may need to update the expected output:
1. Review the changes to ensure they're correct
2. Regenerate the expected output:
```bash
cd cmd/migrate-seed
../../bin/migrate-seed testdata/legacy-input.json testdata/expected-output.json
```
3. Verify the new output is correct by reviewing the diff
4. Run the tests to ensure they pass
## Format Details
### Legacy Input Format
- Flat JSON array of server objects
- Registry metadata fields (`id`, `is_latest`, `release_date`) mixed directly in server objects
- Version detail contains registry metadata
### Expected Output Format
- Extension wrapper format
- Each server wrapped in `"server"` field
- Registry metadata in `"x-io.modelcontextprotocol.registry"` extension
- No `"x-publisher"` extensions for seed data

View file

@ -0,0 +1,77 @@
[
{
"server": {
"name": "io.github.21st-dev/magic-mcp",
"description": "It's like v0 but in your Cursor/WindSurf/Cline. 21st dev Magic MCP server for working with your frontend like Magic",
"status": "active",
"repository": {
"url": "https://github.com/21st-dev/magic-mcp",
"source": "github",
"id": "935450522"
},
"version_detail": {
"version": "0.0.1-seed"
},
"packages": [
{
"registry_name": "npm",
"name": "@21st-dev/magic",
"version": "0.0.46",
"environment_variables": [
{
"description": "${input:apiKey}",
"name": "API_KEY"
}
]
}
]
},
"x-io.modelcontextprotocol.registry": {
"id": "4e9cf4cf-71f6-4aca-bae8-2d10a29ca2e0",
"is_latest": true,
"published_at": "0001-01-01T00:00:00Z",
"release_date": "2025-05-16T18:56:49Z",
"updated_at": "0001-01-01T00:00:00Z"
}
},
{
"server": {
"name": "io.github.adfin-engineering/mcp-server-adfin",
"description": "A Model Context Protocol Server for connecting with Adfin APIs",
"status": "active",
"repository": {
"url": "https://github.com/Adfin-Engineering/mcp-server-adfin",
"source": "github",
"id": "951338147"
},
"version_detail": {
"version": "0.0.1-seed"
},
"packages": [
{
"registry_name": "pypi",
"name": "adfinmcp",
"version": "0.1.0",
"package_arguments": [
{
"description": "Path to the configuration file",
"is_required": true,
"format": "string",
"value": "/config/adfin.json",
"default": "/config/adfin.json",
"type": "positional",
"value_hint": "/path/to/config.json"
}
]
}
]
},
"x-io.modelcontextprotocol.registry": {
"id": "d3669201-252f-403c-944b-c3ec0845782b",
"is_latest": true,
"published_at": "0001-01-01T00:00:00Z",
"release_date": "2025-05-16T18:56:52Z",
"updated_at": "0001-01-01T00:00:00Z"
}
}
]

View file

@ -0,0 +1,63 @@
[
{
"id": "4e9cf4cf-71f6-4aca-bae8-2d10a29ca2e0",
"name": "io.github.21st-dev/magic-mcp",
"description": "It's like v0 but in your Cursor/WindSurf/Cline. 21st dev Magic MCP server for working with your frontend like Magic",
"repository": {
"url": "https://github.com/21st-dev/magic-mcp",
"source": "github",
"id": "935450522"
},
"version_detail": {
"version": "0.0.1-seed",
"release_date": "2025-05-16T18:56:49Z",
"is_latest": true
},
"packages": [
{
"registry_name": "npm",
"name": "@21st-dev/magic",
"version": "0.0.46",
"environment_variables": [
{
"description": "${input:apiKey}",
"name": "API_KEY"
}
]
}
]
},
{
"id": "d3669201-252f-403c-944b-c3ec0845782b",
"name": "io.github.adfin-engineering/mcp-server-adfin",
"description": "A Model Context Protocol Server for connecting with Adfin APIs",
"repository": {
"url": "https://github.com/Adfin-Engineering/mcp-server-adfin",
"source": "github",
"id": "951338147"
},
"version_detail": {
"version": "0.0.1-seed",
"release_date": "2025-05-16T18:56:52Z",
"is_latest": true
},
"packages": [
{
"registry_name": "pypi",
"name": "adfinmcp",
"version": "0.1.0",
"package_arguments": [
{
"description": "Path to the configuration file",
"is_required": true,
"format": "string",
"value": "/config/adfin.json",
"default": "/config/adfin.json",
"type": "positional",
"value_hint": "/path/to/config.json"
}
]
}
]
}
]

View file

@ -5,8 +5,7 @@ type DNSProvider struct {
}
// NewDNSProvider creates a new DNS-based auth provider
//
//nolint:ireturn
//nolint:ireturn // Factory function returns interface by design
func NewDNSProvider(registryURL, domain, hexSeed string) Provider {
return &DNSProvider{
CryptoProvider: &CryptoProvider{

View file

@ -63,7 +63,8 @@ type ServerHealthResponse struct {
}
// NewGitHubATProvider creates a new GitHub OAuth provider
func NewGitHubATProvider(forceLogin bool, registryURL string) Provider { //nolint:ireturn
//nolint:ireturn // Factory function returns interface by design
func NewGitHubATProvider(forceLogin bool, registryURL string) Provider {
return &GitHubATProvider{
forceLogin: forceLogin,
registryURL: registryURL,

View file

@ -14,7 +14,8 @@ type GitHubOIDCProvider struct {
registryURL string
}
func NewGitHubOIDCProvider(registryURL string) Provider { //nolint:ireturn
//nolint:ireturn // Factory function returns interface by design
func NewGitHubOIDCProvider(registryURL string) Provider {
return &GitHubOIDCProvider{
registryURL: registryURL,
}

View file

@ -5,8 +5,7 @@ type HTTPProvider struct {
}
// NewHTTPProvider creates a new HTTP-based auth provider
//
//nolint:ireturn
//nolint:ireturn // Factory function returns interface by design
func NewHTTPProvider(registryURL, domain, hexSeed string) Provider {
return &HTTPProvider{
CryptoProvider: &CryptoProvider{

View file

@ -19,7 +19,8 @@ type TokenResponse struct {
ExpiresAt int64 `json:"expires_at"`
}
func NewNoneProvider(registryURL string) Provider { //nolint:ireturn
//nolint:ireturn // Factory function returns interface by design
func NewNoneProvider(registryURL string) Provider {
return &NoneProvider{
registryURL: registryURL,
}

View file

@ -344,8 +344,17 @@ func publishToRegistry(registryURL string, mcpData []byte, token string) error {
return fmt.Errorf("error parsing server.json file: %w", err)
}
// Create the publish request payload (without authentication)
publishReq := mcpDetails
// Create the publish request payload
var publishReq map[string]any
if _, hasServerField := mcpDetails["server"]; hasServerField {
// Already in PublishRequest format with server field (and possibly x-publisher)
publishReq = mcpDetails
} else {
// Legacy ServerDetail format - wrap it in extension wrapper format
publishReq = map[string]any{
"server": mcpDetails,
}
}
// Convert the request to JSON
jsonData, err := json.Marshal(publishReq)

View file

@ -74,10 +74,26 @@ func runValidation() error {
continue
}
// Extract server portion if this is a PublishRequest format
serverData := data
publishRequestValid := true
if dataMap, ok := data.(map[string]any); ok {
if server, exists := dataMap["server"]; exists {
// This is a PublishRequest format - validate only expected properties exist
for key := range dataMap {
if key != "server" && key != "x-publisher" {
log.Printf(" Invalid PublishRequest property: ❌ %s (only 'server' and optional 'x-publisher' are allowed)", key)
publishRequestValid = false
}
}
serverData = server
}
}
baseValid := false
registryValid := false
if err := baseSchema.Validate(data); err != nil {
if err := baseSchema.Validate(serverData); err != nil {
log.Printf(" Validating against schema.json: ❌")
log.Printf(" Error: %v", err)
} else {
@ -85,7 +101,7 @@ func runValidation() error {
baseValid = true
}
if err := registrySchema.Validate(data); err != nil {
if err := registrySchema.Validate(serverData); err != nil {
log.Printf(" Validating against registry-schema.json: ❌")
log.Printf(" Error: %v", err)
} else {
@ -93,8 +109,8 @@ func runValidation() error {
registryValid = true
}
// Only count as validated if both schemas passed
if baseValid && registryValid {
// Only count as validated if all validations passed
if publishRequestValid && baseValid && registryValid {
validatedCount++
}