Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,23 @@ More can be added as the community desires; feel free to open an issue if you ar
Yes, versioning is supported:

- Each version gets its own immutable metadata
- Version bumps are required for updates
- Version strings must be unique for each server
- Old versions remain accessible for compatibility
- The registry tracks which version is "latest"
- The registry tracks which version is "latest" based on semantic version ordering when possible

### How do I update my server metadata?

Submit a new `server.json` with an incremented version number. Once published, version metadata is immutable (similar to npm).
Submit a new `server.json` with a unique version string. Once published, version metadata is immutable (similar to npm).

### What version format should I use?

The registry accepts any version string up to 255 characters, but we recommend:

- **SHOULD use semantic versioning** (e.g., "1.0.2", "2.1.0-alpha") for predictable ordering
- **SHOULD align with package versions** to reduce confusion
- **MAY use prerelease labels** (e.g., "1.0.0-1") for registry-specific versions

The registry attempts to parse versions as semantic versions for proper ordering. Non-semantic versions are allowed but will be ordered by publication timestamp. See the [versioning guide](./versioning.md) for detailed guidance.

### Can I delete/unpublish my server?

Expand Down
3 changes: 2 additions & 1 deletion docs/server-json/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@
"properties": {
"version": {
"type": "string",
"maxLength": 255,
"example": "1.0.2",
"description": "Equivalent of Implementation.version in MCP specification."
"description": "Version string for this server. SHOULD follow semantic versioning (e.g., '1.0.2', '2.1.0-alpha'). Equivalent of Implementation.version in MCP specification. Non-semantic versions are allowed but may not sort predictably."
}
}
},
Expand Down
135 changes: 135 additions & 0 deletions docs/versioning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Server Versioning Guide

This document describes the versioning approach for MCP servers published to the registry.

## Overview

The MCP Registry supports flexible versioning while encouraging semantic versioning best practices. The registry attempts to parse versions as semantic versions for ordering and comparison, but falls back gracefully for non-semantic versions.

## Version Requirements

1. **Version String**: `version` MUST be a string up to 255 characters
2. **Uniqueness**: Each version for a given server name must be unique
3. **Immutability**: Once published, version metadata cannot be changed

## Best Practices

### 1. Use Semantic Versioning
Server authors SHOULD use [semantic versions](https://semver.org/) following the `MAJOR.MINOR.PATCH` format:

```json
{
"version_detail": {
"version": "1.2.3"
}
}
```

### 2. Align with Package Versions
Server authors SHOULD use versions aligned with their underlying packages to reduce confusion:

```json
{
"version_detail": {
"version": "1.2.3"
},
"packages": [{
"registry_name": "npm",
"name": "@myorg/my-server",
"version": "1.2.3"
}]
}
```

### 3. Multiple Registry Versions
If server authors expect to have multiple registry versions for the same package version, they SHOULD follow the semantic version spec using the prerelease label:

```json
{
"version_detail": {
"version": "1.2.3-1"
},
"packages": [{
"registry_name": "npm",
"name": "@myorg/my-server",
"version": "1.2.3"
}]
}
```

**Note**: According to semantic versioning, `1.2.3-1` is considered lower than `1.2.3`, so if you expect to need a `1.2.3-1`, you should publish that before `1.2.3`.

## Version Ordering and "Latest" Determination

### For Semantic Versions
The registry attempts to parse versions as semantic versions. If successful, it uses semantic version comparison rules to determine:
- Version ordering in lists
- Which version is marked as `is_latest`

### For Non-Semantic Versions
If version parsing as semantic version fails:
- The registry will always mark the version as latest (overriding any previous version)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels reasonable in isolation. but actually means that version's total ordering is inconsistent with is_latest, for packages with a mix of versions.

Not sure what is best to do here. Maybe this is fine. Maybe we should only update is_latest if it's now the latest in the total ordering spec below, idk.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this behavior is necessary because the publisher does not indicate whether they are actually using semantic versioning. For example, if we said "semantic versions always take precedence", and a publisher is using non-semantic version numbers that sometimes appear to be semantic, then they are going to have a bad time. 😆

Unfortunately, as you noted, this means there is no natural sort order. Instead, we have to maintain an insertion order based on this logic.

- Clients should fall back to using publish timestamp for ordering

**Important Note**: This behavior means that for servers with mixed semantic and non-semantic versions, the `is_latest` flag may not align with the total ordering. A non-semantic version published after semantic versions will be marked as latest, even if semantic versions are considered "higher" in the ordering.

## Implementation Details

### Registry Behavior
1. **Validation**: Versions are validated for uniqueness within a server name
2. **Parsing**: The registry attempts to parse each version as semantic version
3. **Comparison**: Uses semantic version rules when possible, falls back to timestamp
4. **Latest Flag**: The `is_latest` field is set based on the comparison results

### Client Recommendations
Registry clients SHOULD:
1. Attempt to interpret versions as semantic versions when possible
2. Use the following ordering rules:
- If one version is marked as is_latest: it is later
- If both versions are valid semver: use semver comparison
- If neither are valid semver: use publish timestamp
- If one is semver and one is not: semver version is considered higher

## Examples

### Valid Semantic Versions
```javascript
"1.0.0" // Basic semantic version
"2.1.3-alpha" // Prerelease version
"1.0.0-beta.1" // Prerelease with numeric suffix
"3.0.0-rc.2" // Release candidate
```

### Non-Semantic Versions (Allowed)
```javascript
"v1.0" // Version with prefix
"2021.03.15" // Date-based versioning
"snapshot" // Development snapshots
"latest" // Custom versioning scheme
```

### Alignment Examples
```json
{
"version_detail": {
"version": "1.2.3-1"
},
"packages": [{
"registry_name": "npm",
"name": "@myorg/k8s-server",
"version": "1.2.3"
}]
}
```

## Migration Path

Existing servers with non-semantic versions will continue to work without changes. However, to benefit from proper version ordering, server maintainers are encouraged to:

1. Adopt semantic versioning for new releases
2. Consider the ordering implications when transitioning from non-semantic to semantic versions
3. Use prerelease labels for registry-specific versioning needs

## Future Considerations

This versioning approach is designed to be compatible with potential future changes to the MCP specification's `Implementation.version` field. Any SHOULD requirements introduced here may be proposed as updates to the specification through the SEP (Specification Enhancement Proposal) process.
5 changes: 4 additions & 1 deletion internal/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ type Database interface {
// GetByID retrieves a single ServerRecord by its ID
GetByID(ctx context.Context, id string) (*model.ServerRecord, error)
// Publish adds a new server to the database with separated server.json and extensions
Publish(ctx context.Context, serverDetail model.ServerDetail, publisherExtensions map[string]interface{}) (*model.ServerRecord, error)
// The registryMetadata contains metadata determined by the service layer (e.g., is_latest, timestamps)
Publish(ctx context.Context, serverDetail model.ServerDetail, publisherExtensions map[string]interface{}, registryMetadata model.RegistryMetadata) (*model.ServerRecord, error)
// UpdateLatestFlag updates the is_latest flag for a specific server record
UpdateLatestFlag(ctx context.Context, id string, isLatest bool) error
// ImportSeed imports initial data from a seed file
ImportSeed(ctx context.Context, seedFilePath string) error
// Close closes the database connection
Expand Down
102 changes: 19 additions & 83 deletions internal/database/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@ import (
"context"
"fmt"
"sort"
"strconv"
"strings"
"sync"
"time"

"github.com/google/uuid"
"github.com/modelcontextprotocol/registry/internal/model"
)

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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()
}
Expand All @@ -221,50 +171,18 @@ 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,
RegistryMetadata: registryMetadata,
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

Expand Down Expand Up @@ -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 {
Expand Down
Loading
Loading