Skip to content

Commit 7e9f2be

Browse files
author
Avish Porwal
committed
Refactor validation code
1 parent c733ced commit 7e9f2be

File tree

6 files changed

+629
-2
lines changed

6 files changed

+629
-2
lines changed

internal/api/handlers/v0/publish.go

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

1516
// PublishServerInput represents the input for publishing a server
@@ -51,6 +52,12 @@ func RegisterPublishEndpoint(api huma.API, registry service.RegistryService, cfg
5152
// Convert PublishRequest body to ServerDetail
5253
serverDetail := input.Body.ServerDetail
5354

55+
// Validate the server detail
56+
validator := validators.NewObjectValidator()
57+
if err := validator.Validate(&serverDetail); err != nil {
58+
return nil, huma.Error400BadRequest(err.Error())
59+
}
60+
5461
// Verify that the token's repository matches the server being published
5562
if !jwtManager.HasPermission(serverDetail.Name, auth.PermissionActionPublish, claims.Permissions) {
5663
return nil, huma.Error403Forbidden("You do not have permission to publish this server")

internal/api/handlers/v0/publish_integration_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ func TestPublishIntegration(t *testing.T) {
109109
Name: "test-mcp-server-no-auth",
110110
Description: "A test MCP server without authentication",
111111
Repository: model.Repository{
112-
URL: "https://example.com/test-mcp-server",
113-
Source: "example",
112+
URL: "https://github.com/mcp/test-mcp-server",
113+
Source: "github",
114114
ID: "test-mcp-server",
115115
},
116116
VersionDetail: model.VersionDetail{
@@ -201,6 +201,11 @@ func TestPublishIntegration(t *testing.T) {
201201
Server: model.Server{
202202
Name: "io.github.other/test-server",
203203
Description: "A test server",
204+
Repository: model.Repository{
205+
URL: "https://github.com/mcp/test-mcp-server",
206+
Source: "github",
207+
ID: "test-mcp-server",
208+
},
204209
VersionDetail: model.VersionDetail{
205210
Version: "1.0.0",
206211
},

internal/validators/constants.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package validators
2+
3+
import "errors"
4+
5+
// Error messages for validation
6+
var (
7+
// Server validation errors
8+
ErrNameRequired = errors.New("name is required")
9+
ErrServerNameTooLong = errors.New("server name is too long")
10+
ErrVersionRequired = errors.New("version is required")
11+
ErrDescriptionTooLong = errors.New("description is too long")
12+
13+
// Repository validation errors
14+
ErrInvalidRepositorySource = errors.New("invalid repository source")
15+
ErrInvalidRepositoryURL = errors.New("invalid repository URL")
16+
17+
// Package validation errors
18+
ErrPackageNameTooLong = errors.New("package name is too long")
19+
ErrPackageNameHasSpaces = errors.New("package name cannot contain spaces")
20+
21+
// Remote validation errors
22+
ErrInvalidRemoteURL = errors.New("invalid remote URL")
23+
)
24+
25+
// Constants for validation limits
26+
const (
27+
MaxLengthForServerName = 255
28+
MaxLengthForDescription = 1000
29+
MaxLengthForPackageName = 255
30+
)
31+
32+
// RepositorySource represents valid repository sources
33+
type RepositorySource string
34+
35+
const (
36+
SourceGitHub RepositorySource = "github"
37+
SourceGitLab RepositorySource = "gitlab"
38+
SourceBitbucket RepositorySource = "bitbucket"
39+
)

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: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package validators
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/modelcontextprotocol/registry/internal/model"
7+
)
8+
9+
type Validator interface {
10+
// Validate checks if the input is valid according to the validator's rules
11+
Validate(obj *model.ServerDetail) error
12+
}
13+
14+
// ServerValidator validates server details
15+
type ServerValidator struct {
16+
*RespositoryValidator // Embedded RespositoryValidator for repository validation
17+
}
18+
19+
// Validate checks if the server details are valid
20+
func (v *ServerValidator) Validate(obj *model.Server) error {
21+
if obj.Name == "" {
22+
return ErrNameRequired
23+
}
24+
25+
// TODO: Add format validation according to https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1086
26+
if len(obj.Name) > MaxLengthForServerName {
27+
return ErrServerNameTooLong
28+
}
29+
30+
// Version is required
31+
if obj.VersionDetail.Version == "" {
32+
return ErrVersionRequired
33+
}
34+
35+
if len(obj.Description) > MaxLengthForDescription {
36+
return ErrDescriptionTooLong
37+
}
38+
39+
if err := v.RespositoryValidator.Validate(&obj.Repository); err != nil {
40+
return err
41+
}
42+
return nil
43+
}
44+
45+
// NewServerValidator creates a new ServerValidator instance
46+
func NewServerValidator() *ServerValidator {
47+
return &ServerValidator{
48+
RespositoryValidator: NewRepositoryValidator(),
49+
}
50+
}
51+
52+
// RepositoryValidator validates repository details
53+
type RespositoryValidator struct {
54+
validSources map[RepositorySource]bool
55+
}
56+
57+
// Validate checks if the repository details are valid
58+
func (rv *RespositoryValidator) Validate(obj *model.Repository) error {
59+
// validate the repository URL
60+
repoSource := RepositorySource(obj.Source)
61+
if _, ok := rv.validSources[repoSource]; !ok {
62+
return fmt.Errorf("%w: %s", ErrInvalidRepositorySource, obj.Source)
63+
}
64+
if !IsValidRepositoryURL(repoSource, obj.URL) {
65+
return fmt.Errorf("%w: %s", ErrInvalidRepositoryURL, obj.URL)
66+
}
67+
68+
// TODO: Add validator for repo ID
69+
return nil
70+
}
71+
72+
// NewRepositoryValidator creates a new RespositoryValidator instance
73+
func NewRepositoryValidator() *RespositoryValidator {
74+
return &RespositoryValidator{
75+
validSources: map[RepositorySource]bool{SourceGitHub: true, SourceGitLab: true},
76+
}
77+
}
78+
79+
// PackageValidator validates package details
80+
type PackageValidator struct{}
81+
82+
// Validate checks if the package details are valid
83+
func (pv *PackageValidator) Validate(obj *model.Package) error {
84+
if len(obj.Name) > MaxLengthForPackageName {
85+
return ErrPackageNameTooLong
86+
}
87+
88+
if !HasNoSpaces(obj.Name) {
89+
return ErrPackageNameHasSpaces
90+
}
91+
92+
return nil
93+
}
94+
95+
// NewPackageValidator creates a new PackageValidator instance
96+
func NewPackageValidator() *PackageValidator {
97+
return &PackageValidator{}
98+
}
99+
100+
// RemoteValidator validates remote connection details
101+
type RemoteValidator struct{}
102+
103+
// Validate checks if the remote connection details are valid
104+
func (rv *RemoteValidator) Validate(obj *model.Remote) error {
105+
if !IsValidURL(obj.URL) {
106+
return fmt.Errorf("%w: %s", ErrInvalidRemoteURL, obj.URL)
107+
}
108+
return nil
109+
}
110+
111+
// NewRemoteValidator creates a new RemoteValidator instance
112+
func NewRemoteValidator() *RemoteValidator {
113+
return &RemoteValidator{}
114+
}
115+
116+
// ObjectValidator aggregates multiple validators for different object types
117+
// This allows for a single entry point to validate complex objects that may contain multiple fields
118+
// that need validation.
119+
type ObjectValidator struct {
120+
ServerValidator *ServerValidator
121+
PackageValidator *PackageValidator
122+
RemoteValidator *RemoteValidator
123+
}
124+
125+
func NewObjectValidator() Validator {
126+
return &ObjectValidator{
127+
ServerValidator: NewServerValidator(),
128+
PackageValidator: NewPackageValidator(),
129+
RemoteValidator: NewRemoteValidator(),
130+
}
131+
}
132+
133+
func (ov *ObjectValidator) Validate(obj *model.ServerDetail) error {
134+
if err := ov.ServerValidator.Validate(&obj.Server); err != nil {
135+
return err
136+
}
137+
138+
for _, pkg := range obj.Packages {
139+
if err := ov.PackageValidator.Validate(&pkg); err != nil {
140+
return err
141+
}
142+
}
143+
144+
for _, remote := range obj.Remotes {
145+
if err := ov.RemoteValidator.Validate(&remote); err != nil {
146+
return err
147+
}
148+
}
149+
return nil
150+
}

0 commit comments

Comments
 (0)