Skip to content

Commit c3b72ef

Browse files
Avish34Avish Porwaldomdomegg
authored
Add more validation for free-form server fields (#272)
<!-- Provide a brief summary of your changes --> The changes are intended to create validators for all the free-form fields. Please find the details of the validation of different field below. - Repository.Source: only github and gitlab is accepted. - Repository.Url - It should be a valid github or gitlab URL. Created regex for both. - Server.Name - Max 200 char can be there. Reverse domain name verification is part of another PR (not owned by me). - Package.Name - Max 200 char and no spaces. - Server.Description - Max 100 char. - Remote.Url - It should be valid URL. ## Motivation and Context <!-- Why is this change needed? What problem does it solve? --> The motivation was to make the code clean and add base code for more validator for different objects like Repository etc. #97 ## How Has This Been Tested? It's tested via UTs. Added UT's for all the validations. ## Breaking Changes Yes ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [x] Breaking change (fix or feature that would cause existing functionality to change) - [ ] 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) - [ ] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [ ] I have added or updated documentation as needed Co-authored-by: Avish Porwal <[email protected]> Co-authored-by: Adam Jones <[email protected]>
1 parent 5fb6ee2 commit c3b72ef

File tree

9 files changed

+484
-12
lines changed

9 files changed

+484
-12
lines changed

docs/server-json/server.schema.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,16 @@
5353
"name": {
5454
"type": "string",
5555
"description": "Server name/identifier",
56-
"example": "io.modelcontextprotocol/filesystem"
56+
"example": "io.modelcontextprotocol/filesystem",
57+
"minLength": 1,
58+
"maxLength": 200
5759
},
5860
"description": {
5961
"type": "string",
6062
"description": "Human-readable description of the server's functionality",
61-
"example": "Node.js server implementing Model Context Protocol (MCP) for filesystem operations."
63+
"example": "Node.js server implementing Model Context Protocol (MCP) for filesystem operations.",
64+
"minLength": 1,
65+
"maxLength": 100
6266
},
6367
"status": {
6468
"type": "string",
@@ -95,7 +99,8 @@
9599
"version": {
96100
"type": "string",
97101
"description": "Package version",
98-
"example": "1.0.2"
102+
"example": "1.0.2",
103+
"minLength": 1
99104
},
100105
"runtime_hint": {
101106
"type": "string",

internal/api/handlers/v0/publish.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/modelcontextprotocol/registry/internal/config"
1212
"github.com/modelcontextprotocol/registry/internal/model"
1313
"github.com/modelcontextprotocol/registry/internal/service"
14+
"github.com/modelcontextprotocol/registry/internal/validators"
1415
)
1516

1617
// PublishServerInput represents the input for publishing a server
@@ -63,6 +64,12 @@ func RegisterPublishEndpoint(api huma.API, registry service.RegistryService, cfg
6364
// Get server details from request body
6465
serverDetail := publishRequest.Server
6566

67+
// Validate the server detail
68+
validator := validators.NewObjectValidator()
69+
if err := validator.Validate(&serverDetail); err != nil {
70+
return nil, huma.Error400BadRequest(err.Error())
71+
}
72+
6673
// Verify that the token's repository matches the server being published
6774
if !jwtManager.HasPermission(serverDetail.Name, auth.PermissionActionPublish, claims.Permissions) {
6875
return nil, huma.Error403Forbidden("You do not have permission to publish this server")

internal/api/handlers/v0/publish_integration_test.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,9 @@ func TestPublishIntegration(t *testing.T) {
105105
Name: "com.example/test-mcp-server-no-auth",
106106
Description: "A test MCP server without authentication",
107107
Repository: model.Repository{
108-
URL: "https://example.com/test-mcp-server",
109-
Source: "example",
110-
ID: "test-mcp-server",
108+
URL: "https://github.com/example/test-server",
109+
Source: "github",
110+
ID: "example/test-server",
111111
},
112112
VersionDetail: model.VersionDetail{
113113
Version: "1.0.0",
@@ -194,6 +194,11 @@ func TestPublishIntegration(t *testing.T) {
194194
VersionDetail: model.VersionDetail{
195195
Version: "1.0.0",
196196
},
197+
Repository: model.Repository{
198+
URL: "https://github.com/example/test-server",
199+
Source: "github",
200+
ID: "example/test-server",
201+
},
197202
},
198203
}
199204

internal/api/handlers/v0/publish_test.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ func TestPublishEndpoint(t *testing.T) {
112112
Name: "example/test-server",
113113
Description: "A test server without auth",
114114
Repository: model.Repository{
115-
URL: "https://example.com/test-server",
116-
Source: "example",
115+
URL: "https://github.com/example/test-server",
116+
Source: "github",
117117
ID: "example/test-server",
118118
},
119119
VersionDetail: model.VersionDetail{
@@ -173,6 +173,11 @@ func TestPublishEndpoint(t *testing.T) {
173173
VersionDetail: model.VersionDetail{
174174
Version: "1.0.0",
175175
},
176+
Repository: model.Repository{
177+
URL: "https://github.com/example/test-server",
178+
Source: "github",
179+
ID: "example/test-server",
180+
},
176181
},
177182
},
178183
tokenClaims: &auth.JWTClaims{
@@ -194,6 +199,11 @@ func TestPublishEndpoint(t *testing.T) {
194199
VersionDetail: model.VersionDetail{
195200
Version: "1.0.0",
196201
},
202+
Repository: model.Repository{
203+
URL: "https://github.com/example/test-server",
204+
Source: "github",
205+
ID: "example/test-server",
206+
},
197207
},
198208
},
199209
tokenClaims: &auth.JWTClaims{

internal/model/model.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ type Package struct {
100100
// Remote represents a remote connection endpoint
101101
type Remote struct {
102102
TransportType string `json:"transport_type" bson:"transport_type"`
103-
URL string `json:"url" bson:"url"`
103+
URL string `json:"url" format:"uri" bson:"url"`
104104
Headers []KeyValueInput `json:"headers,omitempty" bson:"headers,omitempty"`
105105
}
106106

@@ -112,9 +112,9 @@ type VersionDetail struct {
112112
// ServerDetail represents complete server information as defined in the MCP spec (pure, no registry metadata)
113113
type ServerDetail struct {
114114
Schema string `json:"$schema,omitempty" bson:"$schema,omitempty"`
115-
Name string `json:"name" bson:"name"`
116-
Description string `json:"description" bson:"description"`
117-
Status ServerStatus `json:"status,omitempty" bson:"status,omitempty"`
115+
Name string `json:"name" minLength:"1" maxLength:"200" bson:"name"`
116+
Description string `json:"description" minLength:"1" maxLength:"100" bson:"description"`
117+
Status ServerStatus `json:"status,omitempty" minLength:"1" bson:"status,omitempty"`
118118
Repository Repository `json:"repository,omitempty" bson:"repository"`
119119
VersionDetail VersionDetail `json:"version_detail" bson:"version_detail"`
120120
Packages []Package `json:"packages,omitempty" bson:"packages,omitempty"`

internal/validators/constants.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package validators
2+
3+
import "errors"
4+
5+
// Error messages for validation
6+
var (
7+
// Repository validation errors
8+
ErrInvalidRepositoryURL = errors.New("invalid repository URL")
9+
10+
// Package validation errors
11+
ErrPackageNameHasSpaces = errors.New("package name cannot contain spaces")
12+
13+
// Remote validation errors
14+
ErrInvalidRemoteURL = errors.New("invalid remote URL")
15+
)
16+
17+
// RepositorySource represents valid repository sources
18+
type RepositorySource string
19+
20+
const (
21+
SourceGitHub RepositorySource = "github"
22+
SourceGitLab RepositorySource = "gitlab"
23+
)

internal/validators/utils.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package validators
2+
3+
import (
4+
"net/url"
5+
"regexp"
6+
"strings"
7+
)
8+
9+
var (
10+
// Regular expressions for validating repository URLs
11+
// These regex patterns ensure the URL is in the format of a valid GitHub or GitLab repository
12+
// For example: // - GitHub: https://github.com/user/repo
13+
githubURLRegex = regexp.MustCompile(`^https?://(www\.)?github\.com/[\w.-]+/[\w.-]+/?$`)
14+
gitlabURLRegex = regexp.MustCompile(`^https?://(www\.)?gitlab\.com/[\w.-]+/[\w.-]+/?$`)
15+
)
16+
17+
// IsValidRepositoryURL checks if the given URL is valid for the specified repository source
18+
func IsValidRepositoryURL(source RepositorySource, url string) bool {
19+
switch source {
20+
case SourceGitHub:
21+
return githubURLRegex.MatchString(url)
22+
case SourceGitLab:
23+
return gitlabURLRegex.MatchString(url)
24+
}
25+
return false
26+
}
27+
28+
// HasNoSpaces checks if a string contains no spaces
29+
func HasNoSpaces(s string) bool {
30+
return !strings.Contains(s, " ")
31+
}
32+
33+
// IsValidURL checks if a URL is in valid format
34+
func IsValidURL(rawURL string) bool {
35+
// Parse the URL
36+
u, err := url.Parse(rawURL)
37+
if err != nil {
38+
return false
39+
}
40+
41+
// Check if scheme is present (http or https)
42+
if u.Scheme != "http" && u.Scheme != "https" {
43+
return false
44+
}
45+
46+
if u.Host == "" {
47+
return false
48+
}
49+
return true
50+
}

internal/validators/validators.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package validators
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/modelcontextprotocol/registry/internal/model"
7+
)
8+
9+
// ServerValidator validates server details
10+
type ServerValidator struct {
11+
*RepositoryValidator // Embedded RepositoryValidator for repository validation
12+
}
13+
14+
// Validate checks if the server details are valid
15+
func (v *ServerValidator) Validate(obj *model.ServerDetail) error {
16+
if err := v.RepositoryValidator.Validate(&obj.Repository); err != nil {
17+
return err
18+
}
19+
return nil
20+
}
21+
22+
// NewServerValidator creates a new ServerValidator instance
23+
func NewServerValidator() *ServerValidator {
24+
return &ServerValidator{
25+
RepositoryValidator: NewRepositoryValidator(),
26+
}
27+
}
28+
29+
// RepositoryValidator validates repository details
30+
type RepositoryValidator struct {
31+
validSources map[RepositorySource]bool
32+
}
33+
34+
// Validate checks if the repository details are valid
35+
func (rv *RepositoryValidator) Validate(obj *model.Repository) error {
36+
// Skip validation for empty repository (optional field)
37+
if obj.URL == "" && obj.Source == "" {
38+
return nil
39+
}
40+
41+
// validate the repository source
42+
repoSource := RepositorySource(obj.Source)
43+
if !IsValidRepositoryURL(repoSource, obj.URL) {
44+
return fmt.Errorf("%w: %s", ErrInvalidRepositoryURL, obj.URL)
45+
}
46+
47+
return nil
48+
}
49+
50+
// NewRepositoryValidator creates a new RepositoryValidator instance
51+
func NewRepositoryValidator() *RepositoryValidator {
52+
return &RepositoryValidator{
53+
validSources: map[RepositorySource]bool{SourceGitHub: true, SourceGitLab: true},
54+
}
55+
}
56+
57+
// PackageValidator validates package details
58+
type PackageValidator struct{}
59+
60+
// Validate checks if the package details are valid
61+
func (pv *PackageValidator) Validate(obj *model.Package) error {
62+
if !HasNoSpaces(obj.Name) {
63+
return ErrPackageNameHasSpaces
64+
}
65+
66+
return nil
67+
}
68+
69+
// NewPackageValidator creates a new PackageValidator instance
70+
func NewPackageValidator() *PackageValidator {
71+
return &PackageValidator{}
72+
}
73+
74+
// RemoteValidator validates remote connection details
75+
type RemoteValidator struct{}
76+
77+
// Validate checks if the remote connection details are valid
78+
func (rv *RemoteValidator) Validate(obj *model.Remote) error {
79+
if !IsValidURL(obj.URL) {
80+
return fmt.Errorf("%w: %s", ErrInvalidRemoteURL, obj.URL)
81+
}
82+
return nil
83+
}
84+
85+
// NewRemoteValidator creates a new RemoteValidator instance
86+
func NewRemoteValidator() *RemoteValidator {
87+
return &RemoteValidator{}
88+
}
89+
90+
// ObjectValidator aggregates multiple validators for different object types
91+
// This allows for a single entry point to validate complex objects that may contain multiple fields
92+
// that need validation.
93+
type ObjectValidator struct {
94+
ServerValidator *ServerValidator
95+
PackageValidator *PackageValidator
96+
RemoteValidator *RemoteValidator
97+
}
98+
99+
func NewObjectValidator() *ObjectValidator {
100+
return &ObjectValidator{
101+
ServerValidator: NewServerValidator(),
102+
PackageValidator: NewPackageValidator(),
103+
RemoteValidator: NewRemoteValidator(),
104+
}
105+
}
106+
107+
func (ov *ObjectValidator) Validate(obj *model.ServerDetail) error {
108+
if err := ov.ServerValidator.Validate(obj); err != nil {
109+
return err
110+
}
111+
112+
for _, pkg := range obj.Packages {
113+
if err := ov.PackageValidator.Validate(&pkg); err != nil {
114+
return err
115+
}
116+
}
117+
118+
for _, remote := range obj.Remotes {
119+
if err := ov.RemoteValidator.Validate(&remote); err != nil {
120+
return err
121+
}
122+
}
123+
return nil
124+
}

0 commit comments

Comments
 (0)