Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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:"100" 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
23 changes: 23 additions & 0 deletions internal/validators/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package validators

import "errors"

// Error messages for validation
var (
// Repository validation errors
ErrInvalidRepositoryURL = errors.New("invalid repository URL")

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

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

// 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
}
124 changes: 124 additions & 0 deletions internal/validators/validators.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package validators

import (
"fmt"

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

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

// Validate checks if the server details are valid
func (v *ServerValidator) Validate(obj *model.ServerDetail) error {
if err := v.RepositoryValidator.Validate(&obj.Repository); err != nil {
return err
}
return nil
}

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

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

// Validate checks if the repository details are valid
func (rv *RepositoryValidator) Validate(obj *model.Repository) error {
// Skip validation for empty repository (optional field)
if obj.URL == "" && obj.Source == "" {
return nil
}

// validate the repository source
repoSource := RepositorySource(obj.Source)
if !IsValidRepositoryURL(repoSource, obj.URL) {
return fmt.Errorf("%w: %s", ErrInvalidRepositoryURL, obj.URL)
}

return nil
}

// NewRepositoryValidator creates a new RepositoryValidator instance
func NewRepositoryValidator() *RepositoryValidator {
return &RepositoryValidator{
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 !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