Skip to content
Open
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/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,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 @@ -94,7 +98,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: "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
23 changes: 10 additions & 13 deletions internal/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,13 @@ const (
ServerStatusDeprecated ServerStatus = "deprecated"
)


// Repository represents a source code repository as defined in the spec
type Repository struct {
URL string `json:"url" bson:"url"`
Source string `json:"source" bson:"source"`
ID string `json:"id,omitempty" bson:"id,omitempty"`
}


// create an enum for Format
type Format string

Expand Down Expand Up @@ -99,7 +97,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 @@ -108,12 +106,11 @@ type VersionDetail struct {
Version string `json:"version" bson:"version"`
}


// ServerDetail represents complete server information as defined in the MCP spec (pure, no registry metadata)
// ServerDetail represents complete server information as defined in the MCP spec (pure, no registry metadata)
type ServerDetail struct {
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 All @@ -131,8 +128,8 @@ type RegistryMetadata struct {

// ServerRecord represents the complete storage model that separates server.json from registry metadata
type ServerRecord struct {
ServerJSON ServerDetail `json:"server" bson:"server"` // Pure MCP server.json
RegistryMetadata RegistryMetadata `json:"registry_metadata" bson:"registry_metadata"` // Registry-generated data
ServerJSON ServerDetail `json:"server" bson:"server"` // Pure MCP server.json
RegistryMetadata RegistryMetadata `json:"registry_metadata" bson:"registry_metadata"` // Registry-generated data
PublisherExtensions map[string]interface{} `json:"publisher_extensions" bson:"publisher_extensions"` // x-publisher extensions
}

Expand Down Expand Up @@ -252,14 +249,14 @@ func (sr *ServerRecord) ToServerResponse() ServerResponse {
response := ServerResponse{
Server: sr.ServerJSON,
}

// Add registry metadata extension
response.XIOModelContextProtocolRegistry = sr.RegistryMetadata.CreateRegistryExtensions()["x-io.modelcontextprotocol.registry"]

// Add publisher extensions directly
if len(sr.PublisherExtensions) > 0 {
response.XPublisher = sr.PublisherExtensions
}

return response
}
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
}
Loading
Loading