Skip to content

Commit 7b211a4

Browse files
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 <[email protected]>
1 parent b3ae1c2 commit 7b211a4

File tree

17 files changed

+561
-164
lines changed

17 files changed

+561
-164
lines changed

docs/faq.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,23 @@ More can be added as the community desires; feel free to open an issue if you ar
7272
Yes, versioning is supported:
7373

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

7979
### How do I update my server metadata?
8080

81-
Submit a new `server.json` with an incremented version number. Once published, version metadata is immutable (similar to npm).
81+
Submit a new `server.json` with a unique version string. Once published, version metadata is immutable (similar to npm).
82+
83+
### What version format should I use?
84+
85+
The registry accepts any version string up to 255 characters, but we recommend:
86+
87+
- **SHOULD use semantic versioning** (e.g., "1.0.2", "2.1.0-alpha") for predictable ordering
88+
- **SHOULD align with package versions** to reduce confusion
89+
- **MAY use prerelease labels** (e.g., "1.0.0-1") for registry-specific versions
90+
91+
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.
8292

8393
### Can I delete/unpublish my server?
8494

docs/server-json/schema.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@
3636
"properties": {
3737
"version": {
3838
"type": "string",
39+
"maxLength": 255,
3940
"example": "1.0.2",
40-
"description": "Equivalent of Implementation.version in MCP specification."
41+
"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."
4142
}
4243
}
4344
},

docs/versioning.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Server Versioning Guide
2+
3+
This document describes the versioning approach for MCP servers published to the registry.
4+
5+
## Overview
6+
7+
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.
8+
9+
## Version Requirements
10+
11+
1. **Version String**: `version` MUST be a string up to 255 characters
12+
2. **Uniqueness**: Each version for a given server name must be unique
13+
3. **Immutability**: Once published, version metadata cannot be changed
14+
15+
## Best Practices
16+
17+
### 1. Use Semantic Versioning
18+
Server authors SHOULD use [semantic versions](https://semver.org/) following the `MAJOR.MINOR.PATCH` format:
19+
20+
```json
21+
{
22+
"version_detail": {
23+
"version": "1.2.3"
24+
}
25+
}
26+
```
27+
28+
### 2. Align with Package Versions
29+
Server authors SHOULD use versions aligned with their underlying packages to reduce confusion:
30+
31+
```json
32+
{
33+
"version_detail": {
34+
"version": "1.2.3"
35+
},
36+
"packages": [{
37+
"registry_name": "npm",
38+
"name": "@myorg/my-server",
39+
"version": "1.2.3"
40+
}]
41+
}
42+
```
43+
44+
### 3. Multiple Registry Versions
45+
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:
46+
47+
```json
48+
{
49+
"version_detail": {
50+
"version": "1.2.3-1"
51+
},
52+
"packages": [{
53+
"registry_name": "npm",
54+
"name": "@myorg/my-server",
55+
"version": "1.2.3"
56+
}]
57+
}
58+
```
59+
60+
**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`.
61+
62+
## Version Ordering and "Latest" Determination
63+
64+
### For Semantic Versions
65+
The registry attempts to parse versions as semantic versions. If successful, it uses semantic version comparison rules to determine:
66+
- Version ordering in lists
67+
- Which version is marked as `is_latest`
68+
69+
### For Non-Semantic Versions
70+
If version parsing as semantic version fails:
71+
- The registry will always mark the version as latest (overriding any previous version)
72+
- Clients should fall back to using publish timestamp for ordering
73+
74+
**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.
75+
76+
## Implementation Details
77+
78+
### Registry Behavior
79+
1. **Validation**: Versions are validated for uniqueness within a server name
80+
2. **Parsing**: The registry attempts to parse each version as semantic version
81+
3. **Comparison**: Uses semantic version rules when possible, falls back to timestamp
82+
4. **Latest Flag**: The `is_latest` field is set based on the comparison results
83+
84+
### Client Recommendations
85+
Registry clients SHOULD:
86+
1. Attempt to interpret versions as semantic versions when possible
87+
2. Use the following ordering rules:
88+
- If one version is marked as is_latest: it is later
89+
- If both versions are valid semver: use semver comparison
90+
- If neither are valid semver: use publish timestamp
91+
- If one is semver and one is not: semver version is considered higher
92+
93+
## Examples
94+
95+
### Valid Semantic Versions
96+
```javascript
97+
"1.0.0" // Basic semantic version
98+
"2.1.3-alpha" // Prerelease version
99+
"1.0.0-beta.1" // Prerelease with numeric suffix
100+
"3.0.0-rc.2" // Release candidate
101+
```
102+
103+
### Non-Semantic Versions (Allowed)
104+
```javascript
105+
"v1.0" // Version with prefix
106+
"2021.03.15" // Date-based versioning
107+
"snapshot" // Development snapshots
108+
"latest" // Custom versioning scheme
109+
```
110+
111+
### Alignment Examples
112+
```json
113+
{
114+
"version_detail": {
115+
"version": "1.2.3-1"
116+
},
117+
"packages": [{
118+
"registry_name": "npm",
119+
"name": "@myorg/k8s-server",
120+
"version": "1.2.3"
121+
}]
122+
}
123+
```
124+
125+
## Migration Path
126+
127+
Existing servers with non-semantic versions will continue to work without changes. However, to benefit from proper version ordering, server maintainers are encouraged to:
128+
129+
1. Adopt semantic versioning for new releases
130+
2. Consider the ordering implications when transitioning from non-semantic to semantic versions
131+
3. Use prerelease labels for registry-specific versioning needs
132+
133+
## Future Considerations
134+
135+
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.

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ require (
3838
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
3939
go.opentelemetry.io/otel/trace v1.37.0 // indirect
4040
golang.org/x/crypto v0.41.0 // indirect
41+
golang.org/x/mod v0.27.0 // indirect
4142
golang.org/x/sys v0.35.0 // indirect
4243
golang.org/x/text v0.28.0 // indirect
4344
google.golang.org/protobuf v1.36.6 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
8383
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
8484
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
8585
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
86+
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
87+
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
8688
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
8789
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
8890
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=

internal/database/database.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import (
99

1010
// Common database errors
1111
var (
12-
ErrNotFound = errors.New("record not found")
13-
ErrAlreadyExists = errors.New("record already exists")
14-
ErrInvalidInput = errors.New("invalid input")
15-
ErrDatabase = errors.New("database error")
16-
ErrInvalidVersion = errors.New("invalid version: cannot publish older version after newer version")
12+
ErrNotFound = errors.New("record not found")
13+
ErrAlreadyExists = errors.New("record already exists")
14+
ErrInvalidInput = errors.New("invalid input")
15+
ErrDatabase = errors.New("database error")
16+
ErrInvalidVersion = errors.New("invalid version: cannot publish older version after newer version")
17+
ErrMaxServersReached = errors.New("maximum number of versions for this server reached (10000): please reach out at https://github.com/modelcontextprotocol/registry to explain your use case")
1718
)
1819

1920
// Database defines the interface for database operations with extension wrapper architecture
@@ -23,7 +24,10 @@ type Database interface {
2324
// GetByID retrieves a single ServerRecord by its ID
2425
GetByID(ctx context.Context, id string) (*model.ServerRecord, error)
2526
// Publish adds a new server to the database with separated server.json and extensions
26-
Publish(ctx context.Context, serverDetail model.ServerDetail, publisherExtensions map[string]interface{}) (*model.ServerRecord, error)
27+
// The registryMetadata contains metadata determined by the service layer (e.g., is_latest, timestamps)
28+
Publish(ctx context.Context, serverDetail model.ServerDetail, publisherExtensions map[string]interface{}, registryMetadata model.RegistryMetadata) (*model.ServerRecord, error)
29+
// UpdateLatestFlag updates the is_latest flag for a specific server record
30+
UpdateLatestFlag(ctx context.Context, id string, isLatest bool) error
2731
// ImportSeed imports initial data from a seed file
2832
ImportSeed(ctx context.Context, seedFilePath string) error
2933
// Close closes the database connection

internal/database/memory.go

Lines changed: 19 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,9 @@ import (
44
"context"
55
"fmt"
66
"sort"
7-
"strconv"
8-
"strings"
97
"sync"
108
"time"
119

12-
"github.com/google/uuid"
1310
"github.com/modelcontextprotocol/registry/internal/model"
1411
)
1512

@@ -44,53 +41,6 @@ func NewMemoryDB(e map[string]*model.ServerDetail) *MemoryDB {
4441
}
4542
}
4643

47-
// compareSemanticVersions compares two semantic version strings
48-
// Returns:
49-
//
50-
// -1 if version1 < version2
51-
// 0 if version1 == version2
52-
// +1 if version1 > version2
53-
func compareSemanticVersions(version1, version2 string) int {
54-
// Simple semantic version comparison
55-
// Assumes format: major.minor.patch
56-
57-
parts1 := strings.Split(version1, ".")
58-
parts2 := strings.Split(version2, ".")
59-
60-
// Pad with zeros if needed
61-
maxLen := max(len(parts2), len(parts1))
62-
63-
for len(parts1) < maxLen {
64-
parts1 = append(parts1, "0")
65-
}
66-
for len(parts2) < maxLen {
67-
parts2 = append(parts2, "0")
68-
}
69-
70-
// Compare each part
71-
for i := 0; i < maxLen; i++ {
72-
num1, err1 := strconv.Atoi(parts1[i])
73-
num2, err2 := strconv.Atoi(parts2[i])
74-
75-
// If parsing fails, fall back to string comparison
76-
if err1 != nil || err2 != nil {
77-
if parts1[i] < parts2[i] {
78-
return -1
79-
} else if parts1[i] > parts2[i] {
80-
return 1
81-
}
82-
continue
83-
}
84-
85-
if num1 < num2 {
86-
return -1
87-
} else if num1 > num2 {
88-
return 1
89-
}
90-
}
91-
92-
return 0
93-
}
9444

9545
// List retrieves ServerRecord entries with optional filtering and pagination
9646
func (db *MemoryDB) List(
@@ -202,7 +152,7 @@ func (db *MemoryDB) GetByID(ctx context.Context, id string) (*model.ServerRecord
202152
}
203153

204154
// Publish adds a new server to the database with separated server.json and extensions
205-
func (db *MemoryDB) Publish(ctx context.Context, serverDetail model.ServerDetail, publisherExtensions map[string]interface{}) (*model.ServerRecord, error) {
155+
func (db *MemoryDB) Publish(ctx context.Context, serverDetail model.ServerDetail, publisherExtensions map[string]interface{}, registryMetadata model.RegistryMetadata) (*model.ServerRecord, error) {
206156
if ctx.Err() != nil {
207157
return nil, ctx.Err()
208158
}
@@ -221,50 +171,18 @@ func (db *MemoryDB) Publish(ctx context.Context, serverDetail model.ServerDetail
221171
db.mu.Lock()
222172
defer db.mu.Unlock()
223173

224-
// Check for existing entry with same name and compare versions
225-
var existingRecord *model.ServerRecord
226-
for _, entry := range db.entries {
227-
if entry.RegistryMetadata.IsLatest && entry.ServerJSON.Name == name {
228-
existingRecord = entry
229-
break
230-
}
231-
}
232-
233-
// Version comparison
234-
if existingRecord != nil {
235-
existingVersion := existingRecord.ServerJSON.VersionDetail.Version
236-
if compareSemanticVersions(version, existingVersion) <= 0 {
237-
return nil, fmt.Errorf("version must be greater than existing version %s", existingVersion)
238-
}
239-
}
240-
241174
// Validate repository URL
242175
if serverDetail.Repository.URL == "" {
243176
return nil, ErrInvalidInput
244177
}
245178

246-
// Create new registry metadata
247-
now := time.Now()
248-
registryMetadata := model.RegistryMetadata{
249-
ID: uuid.New().String(),
250-
PublishedAt: now,
251-
UpdatedAt: now,
252-
IsLatest: true,
253-
ReleaseDate: now.Format(time.RFC3339),
254-
}
255-
256179
// Create server record
257180
record := &model.ServerRecord{
258181
ServerJSON: serverDetail,
259182
RegistryMetadata: registryMetadata,
260183
PublisherExtensions: publisherExtensions,
261184
}
262185

263-
// Mark existing record as not latest
264-
if existingRecord != nil {
265-
existingRecord.RegistryMetadata.IsLatest = false
266-
}
267-
268186
// Store the record using registry metadata ID
269187
db.entries[registryMetadata.ID] = record
270188

@@ -298,6 +216,24 @@ func (db *MemoryDB) ImportSeed(ctx context.Context, seedFilePath string) error {
298216
return nil
299217
}
300218

219+
// UpdateLatestFlag updates the is_latest flag for a specific server record
220+
func (db *MemoryDB) UpdateLatestFlag(ctx context.Context, id string, isLatest bool) error {
221+
if ctx.Err() != nil {
222+
return ctx.Err()
223+
}
224+
225+
db.mu.Lock()
226+
defer db.mu.Unlock()
227+
228+
if entry, exists := db.entries[id]; exists {
229+
entry.RegistryMetadata.IsLatest = isLatest
230+
entry.RegistryMetadata.UpdatedAt = time.Now()
231+
return nil
232+
}
233+
234+
return ErrNotFound
235+
}
236+
301237
// Close closes the database connection
302238
// For an in-memory database, this is a no-op
303239
func (db *MemoryDB) Close() error {

0 commit comments

Comments
 (0)