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
@@ -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