Skip to content

Commit 78c482b

Browse files
authored
feat(validators): Validate that Server versions are not ranges (#435)
<!-- Provide a brief summary of your changes --> Validates that server versions are not ranges. **NB:** This also introduces `validateVersion` as a check on the top-level `ValidateServerJSON`, as previously we were only checking version ranges of packages via `validatePackageField` (i.e. `latest` would have still been a valid top level server version), despite #413's wording suggesting otherwise. ## Motivation and Context Solves #415 ## How Has This Been Tested? Tests added in `internal/validators/validators_test.go` `make check` is clean. ## Breaking Changes As per @domdomegg's comment, we should > check nobody has done this in the existing registry data (if they have, we should explore and understand why people have done this / if it's valid etc.). ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed ## Additional context <!-- Add any other context, implementation notes, or design decisions -->
1 parent a100367 commit 78c482b

File tree

6 files changed

+265
-26
lines changed

6 files changed

+265
-26
lines changed

docs/explanations/versioning.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,22 @@ Registry clients SHOULD:
9898
```javascript
9999
"v1.0" // Version with prefix
100100
"2021.03.15" // Date-based versioning
101-
"snapshot" // Development snapshots
102-
"latest" // Custom versioning scheme
101+
```
102+
103+
### Not Allowed: Version Ranges
104+
The registry requires specific versions for both the top-level `version` and any `packages[].version`. Version ranges or wildcard versions are rejected during publish, including but not limited to:
105+
106+
```javascript
107+
"^1.2.3"
108+
"~1.2.3"
109+
">=1.2.3"
110+
"<=1.2.3"
111+
">1.2.3"
112+
"<1.2.3"
113+
"1.x"
114+
"1.2.*"
115+
"1 - 2"
116+
"1.2 || 1.3"
103117
```
104118

105119
### Alignment Examples

docs/reference/faq.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ The registry accepts any version string up to 255 characters, but we recommend:
8080
- **SHOULD align with package versions** to reduce confusion
8181
- **MAY use prerelease labels** (e.g., "1.0.0-1") for registry-specific versions
8282

83-
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](../explanations/versioning.md) for detailed guidance.
83+
The registry attempts to parse versions as semantic versions for proper ordering. Non-semantic versions are allowed but will be ordered by publication timestamp. Version ranges (e.g., `^1.2.3`, `~1.2.3`, `>=1.2.3`, `1.x`, `1.*`) are rejected; publish a specific version instead. See the [versioning guide](../explanations/versioning.md) for detailed guidance.
8484

8585
### Can I add custom metadata when publishing?
8686

docs/reference/server-json/server.schema.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
"type": "string",
7373
"maxLength": 255,
7474
"example": "1.0.2",
75-
"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."
75+
"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. Version ranges are rejected (e.g., '^1.2.3', '~1.2.3', '>=1.2.3', '1.x', '1.*')."
7676
},
7777
"website_url": {
7878
"type": "string",
@@ -105,7 +105,8 @@
105105
"type": "string",
106106
"description": "Package version",
107107
"example": "1.0.2",
108-
"minLength": 1
108+
"minLength": 1,
109+
"remarks": "Must be a specific version. Version ranges are rejected (e.g., '^1.2.3', '~1.2.3', '>=1.2.3', '1.x', '1.*')."
109110
},
110111
"file_sha256": {
111112
"type": "string",

internal/validators/constants.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ var (
99
ErrInvalidSubfolderPath = errors.New("invalid subfolder path")
1010

1111
// Package validation errors
12-
ErrPackageNameHasSpaces = errors.New("package name cannot contain spaces")
12+
ErrPackageNameHasSpaces = errors.New("package name cannot contain spaces")
1313
ErrReservedVersionString = errors.New("version string 'latest' is reserved and cannot be used")
14+
ErrVersionLooksLikeRange = errors.New("version must be a specific version, not a range")
1415

1516
// Remote validation errors
1617
ErrInvalidRemoteURL = errors.New("invalid remote URL")

internal/validators/validators.go

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"net/url"
8+
"regexp"
89
"slices"
910
"strings"
1011

@@ -13,12 +14,43 @@ import (
1314
"github.com/modelcontextprotocol/registry/pkg/model"
1415
)
1516

17+
// Regexes to detect semver range syntaxes
18+
var (
19+
// Case 1: comparator ranges
20+
// - "^1.2.3",
21+
// - "~1.2.3",
22+
// - ">=1.0.0",
23+
// - "<=1.0.0",
24+
// - ">1.0.0",
25+
// - "<1.0.0",
26+
// - "=1.0.0",
27+
comparatorRangeRe = regexp.MustCompile(`^\s*(?:\^|~|>=|<=|>|<|=)\s*v?\d+(?:\.\d+){0,3}(?:-[0-9A-Za-z.-]+)?\s*$`)
28+
// Case 2: hyphen ranges
29+
// - "1.2.3 - 2.0.0",
30+
hyphenRangeRe = regexp.MustCompile(`^\s*v?\d+(?:\.\d+){0,3}(?:-[0-9A-Za-z.-]+)?\s-\s*v?\d+(?:\.\d+){0,3}(?:-[0-9A-Za-z.-]+)?\s*$`)
31+
// Case 3: OR ranges
32+
// - "1.2 || 1.3",
33+
orRangeRe = regexp.MustCompile(`^\s*(?:v?\d+(?:\.\d+){0,3}(?:-[0-9A-Za-z.-]+)?\s*)(?:\|\|\s*v?\d+(?:\.\d+){0,3}(?:-[0-9A-Za-z.-]+)?\s*)+$`)
34+
// Case 4: dotted version wildcards
35+
// - "1.2.*",
36+
// - "1.2.x",
37+
// - "1.2.X",
38+
// - "1.x",
39+
// etc.
40+
dottedVersionLikeRe = regexp.MustCompile(`^\s*(?:v?\d+|x|X|\*)(?:\.(?:\d+|x|X|\*)){1,2}(?:-[0-9A-Za-z.-]+)?\s*$`)
41+
)
42+
1643
func ValidateServerJSON(serverJSON *apiv0.ServerJSON) error {
1744
// Validate server name exists and format
1845
if _, err := parseServerName(*serverJSON); err != nil {
1946
return err
2047
}
2148

49+
// Validate top-level server version is a specific version (not a range) & not "latest"
50+
if err := validateVersion(serverJSON.Version); err != nil {
51+
return err
52+
}
53+
2254
// Validate repository
2355
if err := validateRepository(&serverJSON.Repository); err != nil {
2456
return err
@@ -135,16 +167,53 @@ func validatePackageField(obj *model.Package) error {
135167
return nil
136168
}
137169

138-
// validateVersion validates the version string
170+
// validateVersion validates the version string.
139171
// NB: we decided that we would not enforce strict semver for version strings
140172
func validateVersion(version string) error {
141173
if version == "latest" {
142174
return ErrReservedVersionString
143175
}
144176

177+
// Reject semver range-like inputs
178+
if looksLikeVersionRange(version) {
179+
return fmt.Errorf("%w: %q", ErrVersionLooksLikeRange, version)
180+
}
181+
145182
return nil
146183
}
147184

185+
// looksLikeVersionRange detects common semver range syntaxes and wildcard patterns.
186+
// that indicate the value is not a single, specific version.
187+
// Examples that should return true:
188+
// - "^1.2.3",
189+
// - "~1.2.3",
190+
// - ">=1.0.0",
191+
// - "1.x",
192+
// - "1.2.*",
193+
// - "1 - 2",
194+
// - "1.2 || 1.3"
195+
func looksLikeVersionRange(version string) bool {
196+
trimmed := strings.TrimSpace(version)
197+
if trimmed == "" {
198+
return false
199+
}
200+
201+
if comparatorRangeRe.MatchString(trimmed) {
202+
return true
203+
}
204+
if hyphenRangeRe.MatchString(trimmed) {
205+
return true
206+
}
207+
if orRangeRe.MatchString(trimmed) {
208+
return true
209+
}
210+
if dottedVersionLikeRe.MatchString(trimmed) {
211+
// wildcard in a dotted version (x/X/*) implies range-like intent
212+
return strings.Contains(trimmed, "x") || strings.Contains(trimmed, "X") || strings.Contains(trimmed, "*")
213+
}
214+
return false
215+
}
216+
148217
// validateArgument validates argument details
149218
func validateArgument(obj *model.Argument) error {
150219
if obj.Type == model.ArgumentTypeNamed {

0 commit comments

Comments
 (0)