diff --git a/.env.example b/.env.example index 786fc1bf..9fed2310 100644 --- a/.env.example +++ b/.env.example @@ -27,6 +27,10 @@ MCP_REGISTRY_JWT_PRIVATE_KEY=bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb # This should be disabled in prod MCP_REGISTRY_ENABLE_ANONYMOUS_AUTH=false +# Base path for the UI when served behind a reverse proxy under a subpath (e.g. /mcp/registry). +# Only affects browser-side links and navigation, not API routing. +MCP_REGISTRY_UI_BASE_PATH= + # GitHub OIDC token exchange (for `mcp-publisher login github-oidc`) # Expected `aud` claim on incoming GitHub Actions OIDC tokens. Must equal the # scheme + host that publishers pass via `--registry` (e.g. `https://registry.example.com`). diff --git a/internal/api/handlers/v0/ui.go b/internal/api/handlers/v0/ui.go index 32aadd4e..2be85ac3 100644 --- a/internal/api/handlers/v0/ui.go +++ b/internal/api/handlers/v0/ui.go @@ -2,12 +2,14 @@ package v0 import ( _ "embed" + "strings" ) //go:embed ui_index.html var embedUI string -// GetUIHTML returns the embedded HTML for the UI -func GetUIHTML() string { - return embedUI +// GetUIHTML returns the embedded HTML for the UI with the given base path +// substituted into browser-side links and navigation. +func GetUIHTML(basePath string) string { + return strings.ReplaceAll(embedUI, "{{UI_BASE_PATH}}", basePath) } diff --git a/internal/api/handlers/v0/ui_index.html b/internal/api/handlers/v0/ui_index.html index dc437ff7..29f09af6 100644 --- a/internal/api/handlers/v0/ui_index.html +++ b/internal/api/handlers/v0/ui_index.html @@ -20,7 +20,7 @@

Official MCP Registry

GitHub Docs - API Reference + API Reference
@@ -156,11 +156,11 @@

API Base URL

case 'staging': return 'https://staging.registry.modelcontextprotocol.io'; case 'local': - return ''; + return '{{UI_BASE_PATH}}'; case 'custom': return customUrl; default: - return ''; + return '{{UI_BASE_PATH}}'; } } @@ -197,7 +197,8 @@

API Base URL

if (!latestOnly) params.set('all', '1'); if (currentCursor) params.set('cursor', currentCursor); - const newUrl = params.toString() ? `?${params.toString()}` : '/'; + const base = '{{UI_BASE_PATH}}' || '/'; + const newUrl = params.toString() ? `${base}?${params.toString()}` : base; history.pushState({cursor: currentCursor, search, latestOnly}, '', newUrl); } diff --git a/internal/api/router/router.go b/internal/api/router/router.go index 7d642582..f89f8918 100644 --- a/internal/api/router/router.go +++ b/internal/api/router/router.go @@ -121,20 +121,22 @@ func WithSkipPaths(paths ...string) MiddlewareOption { } // handle404 returns a helpful 404 error with suggestions for common mistakes -func handle404(w http.ResponseWriter, r *http.Request) { +func handle404(w http.ResponseWriter, r *http.Request, uiBasePath string) { w.Header().Set("Content-Type", "application/problem+json") w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(http.StatusNotFound) path := r.URL.Path - detail := "Endpoint not found. See /docs for the API documentation." + docsPath := uiBasePath + "/docs" + detail := fmt.Sprintf("Endpoint not found. See %s for the API documentation.", docsPath) // Provide suggestions for common API endpoint mistakes if !strings.HasPrefix(path, "/v0/") && !strings.HasPrefix(path, "/v0.1/") { detail = fmt.Sprintf( - "Endpoint not found. Did you mean '%s' or '%s'? See /docs for the API documentation.", + "Endpoint not found. Did you mean '%s' or '%s'? See %s for the API documentation.", "/v0.1"+path, "/v0"+path, + docsPath, ) } @@ -236,7 +238,7 @@ func NewHumaAPI(cfg *config.Config, registry service.RegistryService, mux *http. "frame-ancestors 'none'; "+ "base-uri 'self'; "+ "form-action 'self'") - _, err := w.Write([]byte(v0.GetUIHTML())) + _, err := w.Write([]byte(v0.GetUIHTML(cfg.UIBasePath))) if err != nil { http.Error(w, "Failed to write response", http.StatusInternalServerError) } @@ -244,7 +246,7 @@ func NewHumaAPI(cfg *config.Config, registry service.RegistryService, mux *http. } // Handle 404 for all non-matched routes - handle404(w, r) + handle404(w, r, cfg.UIBasePath) }) return api diff --git a/internal/config/config.go b/internal/config/config.go index a15a67a1..8f38e822 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -17,6 +17,11 @@ type Config struct { EnableAnonymousAuth bool `env:"ENABLE_ANONYMOUS_AUTH" envDefault:"false"` EnableRegistryValidation bool `env:"ENABLE_REGISTRY_VALIDATION" envDefault:"true"` + // UIBasePath is a path prefix for browser-side links when the UI is served + // behind a reverse proxy under a subpath (e.g. "/mcp/registry"). It does + // not affect API routing. + UIBasePath string `env:"UI_BASE_PATH" envDefault:""` + GitHubOIDCAudience string `env:"GITHUB_OIDC_AUDIENCE" envDefault:""` // OIDC Configuration