registry/internal/service/registry_service.go
adam jones f975e68cf2
Clean up schema by simplifying version structure (#379)
## 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.
2025-09-08 14:32:59 -07:00

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 &registryServiceImpl{
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
}