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>
This commit is contained in:
parent
b3ae1c2ac3
commit
7b211a46d6
1 changed files with 561 additions and 164 deletions
|
|
@ -4,12 +4,9 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/modelcontextprotocol/registry/internal/model"
|
||||
)
|
||||
|
||||
|
|
@ -44,53 +41,6 @@ func NewMemoryDB(e map[string]*model.ServerDetail) *MemoryDB {
|
|||
}
|
||||
}
|
||||
|
||||
// compareSemanticVersions compares two semantic version strings
|
||||
// Returns:
|
||||
//
|
||||
// -1 if version1 < version2
|
||||
// 0 if version1 == version2
|
||||
// +1 if version1 > version2
|
||||
func compareSemanticVersions(version1, version2 string) int {
|
||||
// Simple semantic version comparison
|
||||
// Assumes format: major.minor.patch
|
||||
|
||||
parts1 := strings.Split(version1, ".")
|
||||
parts2 := strings.Split(version2, ".")
|
||||
|
||||
// Pad with zeros if needed
|
||||
maxLen := max(len(parts2), len(parts1))
|
||||
|
||||
for len(parts1) < maxLen {
|
||||
parts1 = append(parts1, "0")
|
||||
}
|
||||
for len(parts2) < maxLen {
|
||||
parts2 = append(parts2, "0")
|
||||
}
|
||||
|
||||
// Compare each part
|
||||
for i := 0; i < maxLen; i++ {
|
||||
num1, err1 := strconv.Atoi(parts1[i])
|
||||
num2, err2 := strconv.Atoi(parts2[i])
|
||||
|
||||
// If parsing fails, fall back to string comparison
|
||||
if err1 != nil || err2 != nil {
|
||||
if parts1[i] < parts2[i] {
|
||||
return -1
|
||||
} else if parts1[i] > parts2[i] {
|
||||
return 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if num1 < num2 {
|
||||
return -1
|
||||
} else if num1 > num2 {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// List retrieves ServerRecord entries with optional filtering and pagination
|
||||
func (db *MemoryDB) List(
|
||||
|
|
@ -202,7 +152,7 @@ func (db *MemoryDB) GetByID(ctx context.Context, id string) (*model.ServerRecord
|
|||
}
|
||||
|
||||
// Publish adds a new server to the database with separated server.json and extensions
|
||||
func (db *MemoryDB) Publish(ctx context.Context, serverDetail model.ServerDetail, publisherExtensions map[string]interface{}) (*model.ServerRecord, error) {
|
||||
func (db *MemoryDB) Publish(ctx context.Context, serverDetail model.ServerDetail, publisherExtensions map[string]interface{}, registryMetadata model.RegistryMetadata) (*model.ServerRecord, error) {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
|
@ -221,38 +171,11 @@ func (db *MemoryDB) Publish(ctx context.Context, serverDetail model.ServerDetail
|
|||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
|
||||
// Check for existing entry with same name and compare versions
|
||||
var existingRecord *model.ServerRecord
|
||||
for _, entry := range db.entries {
|
||||
if entry.RegistryMetadata.IsLatest && entry.ServerJSON.Name == name {
|
||||
existingRecord = entry
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Version comparison
|
||||
if existingRecord != nil {
|
||||
existingVersion := existingRecord.ServerJSON.VersionDetail.Version
|
||||
if compareSemanticVersions(version, existingVersion) <= 0 {
|
||||
return nil, fmt.Errorf("version must be greater than existing version %s", existingVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate repository URL
|
||||
if serverDetail.Repository.URL == "" {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
|
||||
// Create new registry metadata
|
||||
now := time.Now()
|
||||
registryMetadata := model.RegistryMetadata{
|
||||
ID: uuid.New().String(),
|
||||
PublishedAt: now,
|
||||
UpdatedAt: now,
|
||||
IsLatest: true,
|
||||
ReleaseDate: now.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
// Create server record
|
||||
record := &model.ServerRecord{
|
||||
ServerJSON: serverDetail,
|
||||
|
|
@ -260,11 +183,6 @@ func (db *MemoryDB) Publish(ctx context.Context, serverDetail model.ServerDetail
|
|||
PublisherExtensions: publisherExtensions,
|
||||
}
|
||||
|
||||
// Mark existing record as not latest
|
||||
if existingRecord != nil {
|
||||
existingRecord.RegistryMetadata.IsLatest = false
|
||||
}
|
||||
|
||||
// Store the record using registry metadata ID
|
||||
db.entries[registryMetadata.ID] = record
|
||||
|
||||
|
|
@ -298,6 +216,24 @@ func (db *MemoryDB) ImportSeed(ctx context.Context, seedFilePath string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// UpdateLatestFlag updates the is_latest flag for a specific server record
|
||||
func (db *MemoryDB) UpdateLatestFlag(ctx context.Context, id string, isLatest bool) error {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
|
||||
if entry, exists := db.entries[id]; exists {
|
||||
entry.RegistryMetadata.IsLatest = isLatest
|
||||
entry.RegistryMetadata.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
// Close closes the database connection
|
||||
// For an in-memory database, this is a no-op
|
||||
func (db *MemoryDB) Close() error {
|
||||
|
|
|
|||
Loading…
Reference in a new issue