Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
11 changes: 8 additions & 3 deletions docs/server-json/server.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,16 @@
"name": {
"type": "string",
"description": "Server name/identifier",
"example": "io.modelcontextprotocol/filesystem"
"example": "io.modelcontextprotocol/filesystem",
"minLength": 1,
"maxLength": 200
},
"description": {
"type": "string",
"description": "Human-readable description of the server's functionality",
"example": "Node.js server implementing Model Context Protocol (MCP) for filesystem operations."
"example": "Node.js server implementing Model Context Protocol (MCP) for filesystem operations.",
"minLength": 1,
"maxLength": 100
},
"status": {
"type": "string",
Expand Down Expand Up @@ -95,7 +99,8 @@
"version": {
"type": "string",
"description": "Package version",
"example": "1.0.2"
"example": "1.0.2",
"minLength": 1
},
"runtime_hint": {
"type": "string",
Expand Down
7 changes: 7 additions & 0 deletions internal/api/handlers/v0/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/modelcontextprotocol/registry/internal/config"
"github.com/modelcontextprotocol/registry/internal/model"
"github.com/modelcontextprotocol/registry/internal/service"
"github.com/modelcontextprotocol/registry/internal/validators"
)

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

// Validate the server detail
validator := validators.NewObjectValidator()
if err := validator.Validate(&serverDetail); err != nil {
return nil, huma.Error400BadRequest(err.Error())
}

// Verify that the token's repository matches the server being published
if !jwtManager.HasPermission(serverDetail.Name, auth.PermissionActionPublish, claims.Permissions) {
return nil, huma.Error403Forbidden("You do not have permission to publish this server")
Expand Down
11 changes: 8 additions & 3 deletions internal/api/handlers/v0/publish_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ func TestPublishIntegration(t *testing.T) {
Name: "com.example/test-mcp-server-no-auth",
Description: "A test MCP server without authentication",
Repository: model.Repository{
URL: "https://example.com/test-mcp-server",
Source: "example",
ID: "test-mcp-server",
URL: "https://github.com/example/test-server",
Source: "github",
ID: "example/test-server",
},
VersionDetail: model.VersionDetail{
Version: "1.0.0",
Expand Down Expand Up @@ -194,6 +194,11 @@ func TestPublishIntegration(t *testing.T) {
VersionDetail: model.VersionDetail{
Version: "1.0.0",
},
Repository: model.Repository{
URL: "https://github.com/example/test-server",
Source: "github",
ID: "example/test-server",
},
},
}

Expand Down
14 changes: 12 additions & 2 deletions internal/api/handlers/v0/publish_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ func TestPublishEndpoint(t *testing.T) {
Name: "example/test-server",
Description: "A test server without auth",
Repository: model.Repository{
URL: "https://example.com/test-server",
Source: "example",
URL: "https://github.com/example/test-server",
Source: "github",
ID: "example/test-server",
},
VersionDetail: model.VersionDetail{
Expand Down Expand Up @@ -173,6 +173,11 @@ func TestPublishEndpoint(t *testing.T) {
VersionDetail: model.VersionDetail{
Version: "1.0.0",
},
Repository: model.Repository{
URL: "https://github.com/example/test-server",
Source: "github",
ID: "example/test-server",
},
},
},
tokenClaims: &auth.JWTClaims{
Expand All @@ -194,6 +199,11 @@ func TestPublishEndpoint(t *testing.T) {
VersionDetail: model.VersionDetail{
Version: "1.0.0",
},
Repository: model.Repository{
URL: "https://github.com/example/test-server",
Source: "github",
ID: "example/test-server",
},
},
},
tokenClaims: &auth.JWTClaims{
Expand Down
8 changes: 4 additions & 4 deletions internal/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ type Package struct {
// Remote represents a remote connection endpoint
type Remote struct {
TransportType string `json:"transport_type" bson:"transport_type"`
URL string `json:"url" bson:"url"`
URL string `json:"url" format:"uri" bson:"url"`
Headers []KeyValueInput `json:"headers,omitempty" bson:"headers,omitempty"`
}

Expand All @@ -112,9 +112,9 @@ type VersionDetail struct {
// ServerDetail represents complete server information as defined in the MCP spec (pure, no registry metadata)
type ServerDetail struct {
Schema string `json:"$schema,omitempty" bson:"$schema,omitempty"`
Name string `json:"name" bson:"name"`
Description string `json:"description" bson:"description"`
Status ServerStatus `json:"status,omitempty" bson:"status,omitempty"`
Name string `json:"name" minLength:"1" maxLength:"200" bson:"name"`
Description string `json:"description" minLength:"1" maxLength:"200" bson:"description"`
Status ServerStatus `json:"status,omitempty" minLength:"1" bson:"status,omitempty"`
Repository Repository `json:"repository,omitempty" bson:"repository"`
VersionDetail VersionDetail `json:"version_detail" bson:"version_detail"`
Packages []Package `json:"packages,omitempty" bson:"packages,omitempty"`
Expand Down
38 changes: 38 additions & 0 deletions internal/validators/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package validators

import "errors"

// Error messages for validation
var (
// Server validation errors
ErrNameRequired = errors.New("name is required")
ErrServerNameTooLong = errors.New("server name is too long")
ErrVersionRequired = errors.New("version is required")
ErrDescriptionTooLong = errors.New("description is too long")

// Repository validation errors
ErrInvalidRepositorySource = errors.New("invalid repository source")
ErrInvalidRepositoryURL = errors.New("invalid repository URL")

// Package validation errors
ErrPackageNameTooLong = errors.New("package name is too long")
ErrPackageNameHasSpaces = errors.New("package name cannot contain spaces")

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

// Constants for validation limits
const (
MaxLengthForServerName = 255
MaxLengthForDescription = 1000
MaxLengthForPackageName = 255
)

// RepositorySource represents valid repository sources
type RepositorySource string

const (
SourceGitHub RepositorySource = "github"
SourceGitLab RepositorySource = "gitlab"
)
50 changes: 50 additions & 0 deletions internal/validators/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package validators

import (
"net/url"
"regexp"
"strings"
)

var (
// Regular expressions for validating repository URLs
// These regex patterns ensure the URL is in the format of a valid GitHub or GitLab repository
// For example: // - GitHub: https://github.com/user/repo
githubURLRegex = regexp.MustCompile(`^https?://(www\.)?github\.com/[\w.-]+/[\w.-]+/?$`)
gitlabURLRegex = regexp.MustCompile(`^https?://(www\.)?gitlab\.com/[\w.-]+/[\w.-]+/?$`)
)

// IsValidRepositoryURL checks if the given URL is valid for the specified repository source
func IsValidRepositoryURL(source RepositorySource, url string) bool {
switch source {
case SourceGitHub:
return githubURLRegex.MatchString(url)
case SourceGitLab:
return gitlabURLRegex.MatchString(url)
}
return false
}

// HasNoSpaces checks if a string contains no spaces
func HasNoSpaces(s string) bool {
return !strings.Contains(s, " ")
}

// IsValidURL checks if a URL is in valid format
func IsValidURL(rawURL string) bool {
// Parse the URL
u, err := url.Parse(rawURL)
if err != nil {
return false
}

// Check if scheme is present (http or https)
if u.Scheme != "http" && u.Scheme != "https" {
return false
}

if u.Host == "" {
return false
}
return true
}
149 changes: 149 additions & 0 deletions internal/validators/validators.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package validators

import (
"fmt"

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

// ServerValidator validates server details
type ServerValidator struct {
*RespositoryValidator // Embedded RespositoryValidator for repository validation
}

// Validate checks if the server details are valid
func (v *ServerValidator) Validate(obj *model.ServerDetail) error {
if obj.Name == "" {
return ErrNameRequired
}

// Add format validation according to https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1086 in the next PR
if len(obj.Name) > MaxLengthForServerName {
return ErrServerNameTooLong
}

// Version is required
if obj.VersionDetail.Version == "" {
return ErrVersionRequired
}

if len(obj.Description) > MaxLengthForDescription {
return ErrDescriptionTooLong
}

if err := v.RespositoryValidator.Validate(&obj.Repository); err != nil {
return err
}
return nil
}

// NewServerValidator creates a new ServerValidator instance
func NewServerValidator() *ServerValidator {
return &ServerValidator{
RespositoryValidator: NewRepositoryValidator(),
}
}

// RepositoryValidator validates repository details
type RespositoryValidator struct {
validSources map[RepositorySource]bool
}

// Validate checks if the repository details are valid
func (rv *RespositoryValidator) Validate(obj *model.Repository) error {
// empty repository object, this check is needed because we get empty repo object (if not present in request) everytime we unmarshal the request json
if len(obj.URL) == 0 && len(obj.Source) == 0 && len(obj.ID) == 0 {
return nil
}
// validate the repository URL
repoSource := RepositorySource(obj.Source)
if _, ok := rv.validSources[repoSource]; !ok {
return fmt.Errorf("%w: %s", ErrInvalidRepositorySource, obj.Source)
}
if !IsValidRepositoryURL(repoSource, obj.URL) {
return fmt.Errorf("%w: %s", ErrInvalidRepositoryURL, obj.URL)
}

// Add validator for repo ID after confirming ID type
return nil
}

// NewRepositoryValidator creates a new RespositoryValidator instance
func NewRepositoryValidator() *RespositoryValidator {
return &RespositoryValidator{
validSources: map[RepositorySource]bool{SourceGitHub: true, SourceGitLab: true},
}
}

// PackageValidator validates package details
type PackageValidator struct{}

// Validate checks if the package details are valid
func (pv *PackageValidator) Validate(obj *model.Package) error {
if len(obj.Name) > MaxLengthForPackageName {
return ErrPackageNameTooLong
}

if !HasNoSpaces(obj.Name) {
return ErrPackageNameHasSpaces
}

return nil
}

// NewPackageValidator creates a new PackageValidator instance
func NewPackageValidator() *PackageValidator {
return &PackageValidator{}
}

// RemoteValidator validates remote connection details
type RemoteValidator struct{}

// Validate checks if the remote connection details are valid
func (rv *RemoteValidator) Validate(obj *model.Remote) error {
if !IsValidURL(obj.URL) {
return fmt.Errorf("%w: %s", ErrInvalidRemoteURL, obj.URL)
}
return nil
}

// NewRemoteValidator creates a new RemoteValidator instance
func NewRemoteValidator() *RemoteValidator {
return &RemoteValidator{}
}

// ObjectValidator aggregates multiple validators for different object types
// This allows for a single entry point to validate complex objects that may contain multiple fields
// that need validation.
type ObjectValidator struct {
ServerValidator *ServerValidator
PackageValidator *PackageValidator
RemoteValidator *RemoteValidator
}

func NewObjectValidator() *ObjectValidator {
return &ObjectValidator{
ServerValidator: NewServerValidator(),
PackageValidator: NewPackageValidator(),
RemoteValidator: NewRemoteValidator(),
}
}

func (ov *ObjectValidator) Validate(obj *model.ServerDetail) error {
if err := ov.ServerValidator.Validate(obj); err != nil {
return err
}

for _, pkg := range obj.Packages {
if err := ov.PackageValidator.Validate(&pkg); err != nil {
return err
}
}

for _, remote := range obj.Remotes {
if err := ov.RemoteValidator.Validate(&remote); err != nil {
return err
}
}
return nil
}
Loading
Loading