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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Run lint
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a
with:
version: v2.11.4
version: v2.13.1

- name: Validate schemas and examples
run: make validate
Expand Down
6 changes: 5 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ linters:
- gocyclo
- godox
- gomoddirectives
- gomodguard
- gomodguard_v2
- goprintffuncname
- gosec
- grouper
Expand Down Expand Up @@ -55,6 +55,10 @@ linters:
max-complexity: 20
dupl:
threshold: 300
goconst:
# Repeated literals in tests are fixture data ("1.0.0", "Test server", ...);
# hoisting them into constants hurts readability more than it helps.
ignore-tests: true
gocognit:
min-complexity: 51
nestif:
Expand Down
4 changes: 2 additions & 2 deletions cmd/publisher/auth/azurekeyvault/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ func (d Signer) GetSignedTimestamp(ctx context.Context) (*string, []byte, error)
fmt.Fprintln(os.Stdout, "Successfully read the public key from Key Vault.")
auth.PrintEcdsaP384KeyInfo(ecdsa.PublicKey{
Curve: elliptic.P384(),
X: new(big.Int).SetBytes(keyResp.Key.X),
Y: new(big.Int).SetBytes(keyResp.Key.Y),
X: new(big.Int).SetBytes(keyResp.Key.X), //nolint:staticcheck // SA1019: needs crypto/ecdh refactor
Y: new(big.Int).SetBytes(keyResp.Key.Y), //nolint:staticcheck // SA1019: needs crypto/ecdh refactor
})

timestamp := auth.GetTimestamp()
Expand Down
6 changes: 3 additions & 3 deletions cmd/publisher/auth/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,10 @@ func parseRawPrivateKey(curve elliptic.Curve, privateKeyBytes []byte) (*ecdsa.Pr
return &ecdsa.PrivateKey{
PublicKey: ecdsa.PublicKey{
Curve: curve,
X: x,
Y: y,
X: x, //nolint:staticcheck // SA1019: needs crypto/ecdh refactor
Y: y, //nolint:staticcheck // SA1019: needs crypto/ecdh refactor
},
D: d,
D: d, //nolint:staticcheck // SA1019: needs crypto/ecdh refactor
}, nil
}

Expand Down
2 changes: 1 addition & 1 deletion internal/api/handlers/v0/auth/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ func ParsePublicKey(algorithm, publicKey string) (*PublicKeyInfo, error) {
}
return &PublicKeyInfo{
Algorithm: AlgorithmECDSAP384,
Key: ecdsa.PublicKey{Curve: curve, X: x, Y: y},
Key: ecdsa.PublicKey{Curve: curve, X: x, Y: y}, //nolint:staticcheck // SA1019: needs crypto/ecdh refactor
}, nil
}

Expand Down
2 changes: 1 addition & 1 deletion internal/api/handlers/v0/auth/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func RegisterDNSEndpoint(api huma.API, pathPrefix string, cfg *config.Config) {
Path: pathPrefix + "/auth/dns",
Summary: "Exchange DNS signature for Registry JWT",
Description: "Authenticate using DNS TXT record public key and signed timestamp",
Tags: []string{"auth"},
Tags: []string{tagAuth},
}, func(ctx context.Context, input *DNSTokenExchangeInput) (*v0.Response[auth.TokenResponse], error) {
response, err := handler.ExchangeToken(ctx, input.Body.Domain, input.Body.Timestamp, input.Body.SignedTimestamp)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/api/handlers/v0/auth/github_at.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func RegisterGitHubATEndpoint(api huma.API, pathPrefix string, cfg *config.Confi
Path: pathPrefix + "/auth/github-at",
Summary: "Exchange GitHub OAuth access token for Registry JWT",
Description: "Exchange a GitHub OAuth access token for a short-lived Registry JWT token",
Tags: []string{"auth"},
Tags: []string{tagAuth},
}, func(ctx context.Context, input *GitHubTokenExchangeInput) (*v0.Response[auth.TokenResponse], error) {
response, err := handler.ExchangeToken(ctx, input.Body.GitHubToken)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/api/handlers/v0/auth/github_oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ func RegisterGitHubOIDCEndpoint(api huma.API, pathPrefix string, cfg *config.Con
Path: pathPrefix + "/auth/github-oidc",
Summary: "Exchange GitHub OIDC token for Registry JWT",
Description: "Exchange a GitHub Actions OIDC token for a short-lived Registry JWT token",
Tags: []string{"auth"},
Tags: []string{tagAuth},
}, func(ctx context.Context, input *GitHubOIDCTokenExchangeInput) (*v0.Response[auth.TokenResponse], error) {
response, err := handler.ExchangeToken(ctx, input.Body.OIDCToken)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/api/handlers/v0/auth/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ func RegisterHTTPEndpoint(api huma.API, pathPrefix string, cfg *config.Config) {
Path: pathPrefix + "/auth/http",
Summary: "Exchange HTTP signature for Registry JWT",
Description: "Authenticate using HTTP-hosted public key and signed timestamp",
Tags: []string{"auth"},
Tags: []string{tagAuth},
}, func(ctx context.Context, input *HTTPTokenExchangeInput) (*v0.Response[auth.TokenResponse], error) {
response, err := handler.ExchangeToken(ctx, input.Body.Domain, input.Body.Timestamp, input.Body.SignedTimestamp)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions internal/api/handlers/v0/auth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"github.com/modelcontextprotocol/registry/internal/config"
)

// tagAuth is the OpenAPI tag grouping all token exchange operations.
const tagAuth = "auth"

// RegisterAuthEndpoints registers all authentication endpoints with a custom path prefix
func RegisterAuthEndpoints(api huma.API, pathPrefix string, cfg *config.Config) {
// Register GitHub access token authentication endpoint
Expand Down
2 changes: 1 addition & 1 deletion internal/api/handlers/v0/auth/none.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func RegisterNoneEndpoint(api huma.API, pathPrefix string, cfg *config.Config) {
Path: pathPrefix + "/auth/none",
Summary: "Get anonymous Registry JWT (Development/Testing Only)",
Description: "Get a short-lived Registry JWT token for publishing and editing servers in the io.modelcontextprotocol.anonymous/* namespace. This endpoint is intended for local development and automated testing only.",
Tags: []string{"auth"},
Tags: []string{tagAuth},
}, func(ctx context.Context, _ *struct{}) (*v0.Response[auth.TokenResponse], error) {
response, err := handler.GetAnonymousToken(ctx)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/api/handlers/v0/auth/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ func RegisterOIDCEndpoints(api huma.API, pathPrefix string, cfg *config.Config)
Path: pathPrefix + "/auth/oidc",
Summary: "Exchange OIDC ID token for Registry JWT",
Description: "Exchange an OIDC ID token from any configured provider for a short-lived Registry JWT token",
Tags: []string{"auth"},
Tags: []string{tagAuth},
}, func(ctx context.Context, input *OIDCTokenExchangeInput) (*v0.Response[auth.TokenResponse], error) {
response, err := handler.ExchangeToken(ctx, input.Body.OIDCToken)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions internal/api/handlers/v0/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ func RegisterEditEndpoints(api huma.API, pathPrefix string, registry service.Reg
Path: pathPrefix + "/servers/{serverName}/versions/{version}",
Summary: "Edit MCP server",
Description: "Update the configuration of a specific version of an existing MCP server. Requires edit permission for the server. Use PATCH /servers/{serverName}/versions/{version}/status to update status metadata.",
Tags: []string{"servers"},
Tags: []string{tagServers},
Security: []map[string][]string{
{"bearer": {}},
{securitySchemeBearer: {}},
},
}, func(ctx context.Context, input *EditServerInput) (*Response[apiv0.ServerResponse], error) {
// Extract bearer token
Expand Down
12 changes: 12 additions & 0 deletions internal/api/handlers/v0/openapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package v0

// Shared OpenAPI operation metadata, kept in one place so the generated spec
// groups operations consistently.
const (
// tagServers groups the server CRUD and status operations.
tagServers = "servers"

// securitySchemeBearer is the name of the Registry JWT bearer security
// scheme declared in the OpenAPI spec.
securitySchemeBearer = "bearer"
)
2 changes: 1 addition & 1 deletion internal/api/handlers/v0/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func RegisterPublishEndpoint(api huma.API, pathPrefix string, registry service.R
Description: "Publish a new MCP server to the registry or update an existing one",
Tags: []string{"publish"},
Security: []map[string][]string{
{"bearer": {}},
{securitySchemeBearer: {}},
},
}, func(ctx context.Context, input *PublishServerInput) (*Response[apiv0.ServerResponse], error) {
// Extract bearer token
Expand Down
6 changes: 3 additions & 3 deletions internal/api/handlers/v0/servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func RegisterServersEndpoints(api huma.API, pathPrefix string, registry service.
Path: pathPrefix + "/servers",
Summary: "List MCP servers",
Description: "Get a paginated list of MCP servers from the registry",
Tags: []string{"servers"},
Tags: []string{tagServers},
}, func(ctx context.Context, input *ListServersInput) (*Response[apiv0.ServerListResponse], error) {
// Build filter from input parameters
filter := &database.ServerFilter{}
Expand Down Expand Up @@ -159,7 +159,7 @@ func RegisterServersEndpoints(api huma.API, pathPrefix string, registry service.
Path: pathPrefix + "/servers/{serverName}/versions/{version}",
Summary: "Get specific MCP server version",
Description: "Get detailed information about a specific version of an MCP server. Use the special version 'latest' to get the latest version.",
Tags: []string{"servers"},
Tags: []string{tagServers},
}, func(ctx context.Context, input *ServerVersionDetailInput) (*Response[apiv0.ServerResponse], error) {
// URL-decode the server name
serverName, err := url.PathUnescape(input.ServerName)
Expand Down Expand Up @@ -201,7 +201,7 @@ func RegisterServersEndpoints(api huma.API, pathPrefix string, registry service.
Path: pathPrefix + "/servers/{serverName}/versions",
Summary: "Get all versions of an MCP server",
Description: "Get all available versions for a specific MCP server",
Tags: []string{"servers"},
Tags: []string{tagServers},
}, func(ctx context.Context, input *ServerVersionsInput) (*Response[apiv0.ServerListResponse], error) {
// URL-decode the server name
serverName, err := url.PathUnescape(input.ServerName)
Expand Down
8 changes: 4 additions & 4 deletions internal/api/handlers/v0/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ func RegisterStatusEndpoints(api huma.API, pathPrefix string, registry service.R
Path: pathPrefix + "/servers/{serverName}/versions/{version}/status",
Summary: "Update MCP server status",
Description: "Update the status metadata of a specific version of an MCP server. Requires publish or edit permission for the server. This endpoint allows changing status and status message without requiring the full server configuration.",
Tags: []string{"servers"},
Tags: []string{tagServers},
Security: []map[string][]string{
{"bearer": {}},
{securitySchemeBearer: {}},
},
}, func(ctx context.Context, input *UpdateServerStatusInput) (*Response[apiv0.ServerResponse], error) {
// Extract bearer token
Expand Down Expand Up @@ -198,9 +198,9 @@ func RegisterAllVersionsStatusEndpoints(api huma.API, pathPrefix string, registr
Path: pathPrefix + "/servers/{serverName}/status",
Summary: "Update status for all versions of an MCP server",
Description: "Update the status metadata of all versions of an MCP server in a single transaction. Requires publish or edit permission for the server. Either all versions are updated or none on failure.",
Tags: []string{"servers"},
Tags: []string{tagServers},
Security: []map[string][]string{
{"bearer": {}},
{securitySchemeBearer: {}},
},
}, func(ctx context.Context, input *UpdateAllVersionsStatusInput) (*Response[UpdateAllVersionsStatusResponse], error) {
// Extract bearer token
Expand Down
12 changes: 10 additions & 2 deletions internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"log"
"net/http"
"net/url"
"path"
"strings"
"time"
Expand Down Expand Up @@ -72,11 +73,18 @@ func TrailingSlashMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Only redirect if the path is not "/" and ends with a "/"
if r.URL.Path != "/" && strings.HasSuffix(r.URL.Path, "/") {
// Build the target from the path and query alone, dropping any
// scheme/host the client may have supplied in an absolute-form
// request URI ("GET http://evil.com/foo/ HTTP/1.1"), which would
// otherwise be echoed back as an off-site redirect.
//
// path.Clean both removes the trailing slash and collapses any
// leading "//" to "/", which prevents an open-redirect via a
// protocol-relative path like "//evil.com/" (GHSA-v8vw-gw5j-w7m6).
newURL := *r.URL
newURL.Path = path.Clean(r.URL.Path)
newURL := url.URL{
Path: path.Clean(r.URL.Path),
RawQuery: r.URL.RawQuery,
}

// Use 308 Permanent Redirect to preserve the request method
http.Redirect(w, r, newURL.String(), http.StatusPermanentRedirect)
Expand Down
13 changes: 9 additions & 4 deletions internal/validators/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import (
"strings"
)

const (
schemeHTTP = "http"
schemeHTTPS = "https"
)

var (
// Regular expressions for validating repository URLs
// These regex patterns ensure the URL is in the format of a valid GitHub or GitLab repository
Expand Down Expand Up @@ -52,8 +57,8 @@ func replaceTemplateVariables(rawURL string) string {
"{host}": "example.com",
"{port}": "8080",
"{path}": "api",
"{protocol}": "http",
"{scheme}": "http",
"{protocol}": schemeHTTP,
"{scheme}": schemeHTTP,
}

result := rawURL
Expand Down Expand Up @@ -86,7 +91,7 @@ func IsValidURL(rawURL string) bool {
}

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

Expand Down Expand Up @@ -153,7 +158,7 @@ func IsValidRemoteURL(rawURL string) bool {
return false
}

if u.Scheme != "https" {
if u.Scheme != schemeHTTPS {
return false
}

Expand Down
9 changes: 6 additions & 3 deletions tools/extract-server-schema/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import (
const (
openAPIPath = "docs/reference/api/openapi.yaml"
schemaOutputDir = "docs/reference/server-json/draft"

// refKey is the JSON Schema reference key.
refKey = "$ref"
)

func main() {
Expand Down Expand Up @@ -99,7 +102,7 @@ func main() {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": schemaID,
"title": "server.json defining a Model Context Protocol (MCP) server",
"$ref": "#/definitions/ServerDetail",
refKey: "#/definitions/ServerDetail",
"definitions": definitions,
}

Expand Down Expand Up @@ -148,7 +151,7 @@ func findReferencedSchemas(obj interface{}, found map[string]bool) {
switch v := obj.(type) {
case map[string]interface{}:
for key, value := range v {
if key == "$ref" {
if key == refKey {
if ref, ok := value.(string); ok {
// Extract schema name from #/components/schemas/SchemaName
if strings.HasPrefix(ref, "#/components/schemas/") {
Expand All @@ -173,7 +176,7 @@ func replaceComponentRefs(obj interface{}) interface{} {
case map[string]interface{}:
result := make(map[string]interface{})
for key, value := range v {
if key == "$ref" {
if key == refKey {
if ref, ok := value.(string); ok {
// Replace the reference path
result[key] = strings.ReplaceAll(ref, "#/components/schemas/", "#/definitions/")
Expand Down
Loading