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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down
8 changes: 5 additions & 3 deletions internal/api/handlers/v0/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this PR is accepted I could change the custom templating here in a follow-up to html/template or so to also accept things like custom titles and so on.

}
9 changes: 5 additions & 4 deletions internal/api/handlers/v0/ui_index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ <h1 class="text-4xl font-bold text-gray-900 mb-2">Official MCP Registry</h1>
<div class="flex gap-6 text-sm">
<a href="https://github.com/modelcontextprotocol/registry" target="_blank" class="text-blue-600 hover:text-blue-700 font-medium">GitHub</a>
<a href="https://github.com/modelcontextprotocol/registry/tree/main/docs" target="_blank" class="text-blue-600 hover:text-blue-700 font-medium">Docs</a>
<a href="/docs" class="text-blue-600 hover:text-blue-700 font-medium">API Reference</a>
<a href="{{UI_BASE_PATH}}/docs" class="text-blue-600 hover:text-blue-700 font-medium">API Reference</a>
</div>
</header>

Expand Down Expand Up @@ -156,11 +156,11 @@ <h3 class="text-lg font-semibold mb-4">API Base URL</h3>
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}}';
}
}

Expand Down Expand Up @@ -197,7 +197,8 @@ <h3 class="text-lg font-semibold mb-4">API Base URL</h3>
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);
}

Expand Down
12 changes: 7 additions & 5 deletions internal/api/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
}

Expand Down Expand Up @@ -236,15 +238,15 @@ 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)
}
return
}

// Handle 404 for all non-matched routes
handle404(w, r)
handle404(w, r, cfg.UIBasePath)
})

return api
Expand Down
5 changes: 5 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading