registry/internal/service/fake_service.go
adam jones 7b211a46d6
feat: implement server versioning approach with semantic versioning support (#296)
This implements the versioning approach agreed upon in issue #158:

## Key Changes

### Documentation
- Added comprehensive versioning guide (docs/versioning.md)
- Updated FAQ with versioning guidance and best practices 
- Enhanced schema description with SHOULD recommendations

### Schema Updates
- Added 255-character limit for version strings in schema.json
- Enhanced version field description with semantic versioning guidance

### Implementation 
- New isSemanticVersion() function for proper semver detection
- Enhanced compareSemanticVersions() with prerelease support
- Implemented compareVersions() strategy:
  * Both semver: use semantic comparison
  * Neither semver: use timestamp comparison
  * Mixed: semver versions always take precedence
- Updated publish logic to determine "latest" using new strategy
- Added version length validation (255 char max)

### Versioning Strategy
1. Version MUST be string up to 255 characters
2. SHOULD use semantic versioning for predictable ordering
3. SHOULD align with package versions to reduce confusion 
4. MAY use prerelease labels for registry-specific versions
5. Registry attempts semver parsing, falls back to timestamp ordering
6. Clients SHOULD follow same comparison logic

Resolves #158

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: adam jones <domdomegg@users.noreply.github.com>
2025-08-26 14:55:38 +01:00

154 lines
No EOL
4.5 KiB
Go

package service
import (
"context"
"time"
"github.com/google/uuid"
"github.com/modelcontextprotocol/registry/internal/database"
"github.com/modelcontextprotocol/registry/internal/model"
)
// fakeRegistryService implements RegistryService interface with an in-memory database
type fakeRegistryService struct {
db *database.MemoryDB
}
// NewFakeRegistryService creates a new fake registry service with pre-populated data
//
//nolint:ireturn // Factory function intentionally returns interface for dependency injection
func NewFakeRegistryService() RegistryService {
// Sample registry entries with updated model structure using ServerDetail
serverDetails := []*model.ServerDetail{
{
Name: "bluegreen/mcp-server",
Description: "A dummy MCP registry for testing",
Repository: model.Repository{
URL: "https://github.com/example/mcp-1",
Source: "github",
ID: "example/mcp-1",
},
VersionDetail: model.VersionDetail{
Version: "1.0.0",
},
},
{
Name: "orangepurple/mcp-server",
Description: "Another dummy MCP registry for testing",
Repository: model.Repository{
URL: "https://github.com/example/mcp-2",
Source: "github",
ID: "example/mcp-2",
},
VersionDetail: model.VersionDetail{
Version: "0.9.0",
},
},
{
Name: "greenyellow/mcp-server",
Description: "Yet another dummy MCP registry for testing",
Repository: model.Repository{
URL: "https://github.com/example/mcp-3",
Source: "github",
ID: "example/mcp-3",
},
VersionDetail: model.VersionDetail{
Version: "0.9.5",
},
},
}
// Create a new in-memory database using registry metadata IDs
serverDetailMap := make(map[string]*model.ServerDetail)
for _, entry := range serverDetails {
registryID := uuid.New().String() // Generate registry metadata ID
serverDetailMap[registryID] = entry
}
memDB := database.NewMemoryDB(serverDetailMap)
return &fakeRegistryService{
db: memDB,
}
}
// List retrieves servers with extension wrapper format
func (s *fakeRegistryService) List(cursor string, limit int) ([]model.ServerResponse, string, error) {
// Create a timeout context for the database operation
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Use the database's List method to get ServerRecord entries
serverRecords, nextCursor, err := s.db.List(ctx, nil, cursor, limit)
if err != nil {
return nil, "", err
}
// Convert ServerRecord to ServerResponse format
result := make([]model.ServerResponse, len(serverRecords))
for i, record := range serverRecords {
result[i] = record.ToServerResponse()
}
return result, nextCursor, nil
}
// GetByID retrieves a specific server by its registry metadata ID in extension wrapper format
func (s *fakeRegistryService) GetByID(id string) (*model.ServerResponse, error) {
// Create a timeout context for the database operation
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Use the database's GetByID method to retrieve the server record
serverRecord, err := s.db.GetByID(ctx, id)
if err != nil {
return nil, err
}
// Convert ServerRecord to ServerResponse format
response := serverRecord.ToServerResponse()
return &response, nil
}
// Publish publishes a server with separated extensions
func (s *fakeRegistryService) Publish(req model.PublishRequest) (*model.ServerResponse, 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 := model.ValidatePublisherExtensions(req); err != nil {
return nil, err
}
// Validate server name exists
if _, err := model.ParseServerName(req.Server); err != nil {
return nil, err
}
// Extract publisher extensions from request
publisherExtensions := model.ExtractPublisherExtensions(req)
// Create registry metadata for fake service (always marks as latest)
now := time.Now()
registryMetadata := model.RegistryMetadata{
ID: uuid.New().String(),
PublishedAt: now,
UpdatedAt: now,
IsLatest: true,
ReleaseDate: now.Format(time.RFC3339),
}
// Publish to database
serverRecord, err := s.db.Publish(ctx, req.Server, publisherExtensions, registryMetadata)
if err != nil {
return nil, err
}
// Convert ServerRecord to ServerResponse format
response := serverRecord.ToServerResponse()
return &response, nil
}
// Close closes the in-memory database connection
func (s *fakeRegistryService) Close() error {
return s.db.Close()
}