## Summary Simplified the schema structure by: - Replacing `version_detail.version` with direct `version` field - Removing unused `release_date` field from seed data - Updating all API types, models, and tests to match new structure - Maintaining backwards compatibility in API responses This change makes the schema more intuitive and reduces nesting depth.
228 lines
6.8 KiB
Go
228 lines
6.8 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/modelcontextprotocol/registry/internal/config"
|
|
"github.com/modelcontextprotocol/registry/internal/database"
|
|
"github.com/modelcontextprotocol/registry/internal/validators"
|
|
apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
|
|
)
|
|
|
|
const maxServerVersionsPerServer = 10000
|
|
|
|
// registryServiceImpl implements the RegistryService interface using our Database
|
|
type registryServiceImpl struct {
|
|
db database.Database
|
|
cfg *config.Config
|
|
}
|
|
|
|
// NewRegistryService creates a new registry service with the provided database
|
|
func NewRegistryService(db database.Database, cfg *config.Config) RegistryService {
|
|
return ®istryServiceImpl{
|
|
db: db,
|
|
cfg: cfg,
|
|
}
|
|
}
|
|
|
|
// List returns registry entries with cursor-based pagination and optional filtering
|
|
func (s *registryServiceImpl) List(filter *database.ServerFilter, cursor string, limit int) ([]apiv0.ServerJSON, string, error) {
|
|
// Create a timeout context for the database operation
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
// If limit is not set or negative, use a default limit
|
|
if limit <= 0 {
|
|
limit = 30
|
|
}
|
|
|
|
// Use the database's ListServers method with pagination and filtering
|
|
serverRecords, nextCursor, err := s.db.List(ctx, filter, cursor, limit)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
// Return ServerJSONs directly
|
|
result := make([]apiv0.ServerJSON, len(serverRecords))
|
|
for i, record := range serverRecords {
|
|
result[i] = *record
|
|
}
|
|
|
|
return result, nextCursor, nil
|
|
}
|
|
|
|
// GetByID retrieves a specific server by its registry metadata ID in flattened format
|
|
func (s *registryServiceImpl) GetByID(id string) (*apiv0.ServerJSON, error) {
|
|
// Create a timeout context for the database operation
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
serverRecord, err := s.db.GetByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Return the server record directly
|
|
return serverRecord, nil
|
|
}
|
|
|
|
// Publish publishes a server with flattened _meta extensions
|
|
func (s *registryServiceImpl) Publish(req apiv0.ServerJSON) (*apiv0.ServerJSON, error) {
|
|
// Create a timeout context for the database operation
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
// Validate the request
|
|
if err := validators.ValidatePublishRequest(req, s.cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
publishTime := time.Now()
|
|
serverJSON := req
|
|
|
|
// Check for duplicate remote URLs
|
|
if err := s.validateNoDuplicateRemoteURLs(ctx, serverJSON); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filter := &database.ServerFilter{Name: &serverJSON.Name}
|
|
existingServerVersions, _, err := s.db.List(ctx, filter, "", maxServerVersionsPerServer)
|
|
if err != nil && !errors.Is(err, database.ErrNotFound) {
|
|
return nil, err
|
|
}
|
|
|
|
// Check we haven't exceeded the maximum versions allowed for a server
|
|
if len(existingServerVersions) >= maxServerVersionsPerServer {
|
|
return nil, database.ErrMaxServersReached
|
|
}
|
|
|
|
// Check this isn't a duplicate version
|
|
for _, server := range existingServerVersions {
|
|
existingVersion := server.Version
|
|
if existingVersion == serverJSON.Version {
|
|
return nil, database.ErrInvalidVersion
|
|
}
|
|
}
|
|
|
|
// Determine if this version should be marked as latest
|
|
existingLatest := s.getCurrentLatestVersion(existingServerVersions)
|
|
isNewLatest := true
|
|
if existingLatest != nil {
|
|
var existingPublishedAt time.Time
|
|
if existingLatest.Meta != nil && existingLatest.Meta.Official != nil {
|
|
existingPublishedAt = existingLatest.Meta.Official.PublishedAt
|
|
}
|
|
isNewLatest = CompareVersions(
|
|
serverJSON.Version,
|
|
existingLatest.Version,
|
|
publishTime,
|
|
existingPublishedAt,
|
|
) > 0
|
|
}
|
|
|
|
// Create complete server with metadata
|
|
server := serverJSON // Copy the input
|
|
|
|
// Initialize meta if not present
|
|
if server.Meta == nil {
|
|
server.Meta = &apiv0.ServerMeta{}
|
|
}
|
|
|
|
// Set registry metadata
|
|
server.Meta.Official = &apiv0.RegistryExtensions{
|
|
ID: uuid.New().String(),
|
|
PublishedAt: publishTime,
|
|
UpdatedAt: publishTime,
|
|
IsLatest: isNewLatest,
|
|
}
|
|
|
|
// Create server in database
|
|
serverRecord, err := s.db.CreateServer(ctx, &server)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Mark previous latest as no longer latest
|
|
if isNewLatest && existingLatest != nil {
|
|
var existingLatestID string
|
|
if existingLatest.Meta != nil && existingLatest.Meta.Official != nil {
|
|
existingLatestID = existingLatest.Meta.Official.ID
|
|
}
|
|
if existingLatestID != "" {
|
|
// Update the existing server to set is_latest = false
|
|
existingLatest.Meta.Official.IsLatest = false
|
|
existingLatest.Meta.Official.UpdatedAt = time.Now()
|
|
if _, err := s.db.UpdateServer(ctx, existingLatestID, existingLatest); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return the server record directly
|
|
return serverRecord, nil
|
|
}
|
|
|
|
// validateNoDuplicateRemoteURLs checks that no other server is using the same remote URLs
|
|
func (s *registryServiceImpl) validateNoDuplicateRemoteURLs(ctx context.Context, serverDetail apiv0.ServerJSON) error {
|
|
// Check each remote URL in the new server for conflicts
|
|
for _, remote := range serverDetail.Remotes {
|
|
// Use filter to find servers with this remote URL
|
|
filter := &database.ServerFilter{RemoteURL: &remote.URL}
|
|
|
|
conflictingServers, _, err := s.db.List(ctx, filter, "", 1000)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check remote URL conflict: %w", err)
|
|
}
|
|
|
|
// Check if any conflicting server has a different name
|
|
for _, conflictingServer := range conflictingServers {
|
|
if conflictingServer.Name != serverDetail.Name {
|
|
return fmt.Errorf("remote URL %s is already used by server %s", remote.URL, conflictingServer.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getCurrentLatestVersion finds the current latest version from existing server versions
|
|
func (s *registryServiceImpl) getCurrentLatestVersion(existingServerVersions []*apiv0.ServerJSON) *apiv0.ServerJSON {
|
|
for _, server := range existingServerVersions {
|
|
if server.Meta != nil && server.Meta.Official != nil &&
|
|
server.Meta.Official.IsLatest {
|
|
return server
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EditServer updates an existing server with new details (admin operation)
|
|
func (s *registryServiceImpl) EditServer(id string, req apiv0.ServerJSON) (*apiv0.ServerJSON, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
// Validate the request
|
|
if err := validators.ValidatePublishRequest(req, s.cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
serverJSON := req
|
|
|
|
// Check for duplicate remote URLs
|
|
if err := s.validateNoDuplicateRemoteURLs(ctx, serverJSON); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Update server in database
|
|
serverRecord, err := s.db.UpdateServer(ctx, id, &serverJSON)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Return the server record directly
|
|
return serverRecord, nil
|
|
}
|