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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ MCP_REGISTRY_DATABASE_URL=postgres://username:password@localhost:5432/mcp-regist
# For offline development, use: data/seed.json
MCP_REGISTRY_SEED_FROM=https://registry.modelcontextprotocol.io/v0/servers

# Custom MCP Registry URL for self-hosted instances (optional)
MCP_REGISTRY_CUSTOM_MCP_REGISTRY_URL=https://your-registry-url.com

# GitHub OAuth configuration
# These creds are for local development with the 'MCP Registry Login (Local)' GitHub App
# They don't provide any real privileged access, hence why it's okay that they're here
Expand Down
33 changes: 30 additions & 3 deletions internal/api/handlers/v0/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,39 @@ package v0

import (
_ "embed"
"encoding/json"
"strings"
)

//go:embed ui_index.html
var embedUI string

// GetUIHTML returns the embedded HTML for the UI
func GetUIHTML() string {
return embedUI
const (
defaultAPIBasePlaceholder = "PLACEHOLDER"
defaultAPIBaseValueKey = "__DEFAULT_API_BASE_VALUE__"
defaultAPIBasePlaceholderKey = "__DEFAULT_API_BASE_PLACEHOLDER__"
)

// GetUIHTML returns the embedded HTML for the UI with injected API base URL configuration.
// The defaultBaseURL is injected into the JavaScript context to allow the frontend to
// access a custom registry URL set via environment variable. If no custom URL is provided,
// the frontend will fall back to the default production registry.
func GetUIHTML(defaultBaseURL string) string {
// Marshal the URL to get a JSON-encoded string for safe JavaScript injection
jsonEncoded, err := json.Marshal(defaultBaseURL)
if err != nil {
// Fallback to empty string if marshaling fails (should be very rare for strings)
jsonEncoded = []byte(`""`)
}

// Trim the surrounding quotes from JSON encoding to get the raw URL value
value := strings.Trim(string(jsonEncoded), `"`)

// Inject the actual URL value into the UI
html := strings.ReplaceAll(embedUI, defaultAPIBaseValueKey, value)

// Inject the placeholder sentinel value for frontend comparison logic
html = strings.ReplaceAll(html, defaultAPIBasePlaceholderKey, defaultAPIBasePlaceholder)

return html
}
39 changes: 33 additions & 6 deletions internal/api/handlers/v0/ui_index.html
Original file line number Diff line number Diff line change
Expand Up @@ -138,16 +138,31 @@ <h3 class="text-lg font-semibold mb-4">API Base URL</h3>
</div>

<script>
window.__DEFAULT_API_BASE__ = "__DEFAULT_API_BASE_VALUE__";
window.__DEFAULT_API_BASE_PLACEHOLDER__ = "__DEFAULT_API_BASE_PLACEHOLDER__";

let servers = [];
let cursorHistory = [];
let currentCursor = null;
let nextCursor = null;
let searchTimeout = null;
let baseUrl = '';

function getInjectedDefaultBase() {
return (window.__DEFAULT_API_BASE__ && window.__DEFAULT_API_BASE__ !== window.__DEFAULT_API_BASE_PLACEHOLDER__)
? window.__DEFAULT_API_BASE__
: '';
}

function normalizeBaseUrl(url) {
const trimmed = (url || '').trim();
return trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed;
}

// Get base URL based on selection
function getBaseUrl() {
const selection = localStorage.getItem('baseUrl') || 'local';
const defaultApiBase = getInjectedDefaultBase();
const selection = (defaultApiBase && !localStorage.getItem('customUrl')) ? 'custom' : (localStorage.getItem('baseUrl') || 'prod');
const customUrl = localStorage.getItem('customUrl') || '';

switch (selection) {
Expand All @@ -158,15 +173,16 @@ <h3 class="text-lg font-semibold mb-4">API Base URL</h3>
case 'local':
return '';
case 'custom':
return customUrl;
return customUrl || defaultApiBase;
default:
return '';
}
}

// Get server display name
function getServerName() {
const selection = localStorage.getItem('baseUrl') || 'local';
const defaultApiBase = getInjectedDefaultBase();
const selection = (defaultApiBase && !localStorage.getItem('customUrl')) ? 'custom' : (localStorage.getItem('baseUrl') || 'prod');
const customUrl = localStorage.getItem('customUrl') || '';

switch (selection) {
Expand All @@ -184,6 +200,15 @@ <h3 class="text-lg font-semibold mb-4">API Base URL</h3>
}

// Initialize base URL and server name
if (!localStorage.getItem('baseUrl')) {
const defaultApiBase = getInjectedDefaultBase();
if (defaultApiBase) {
localStorage.setItem('baseUrl', 'custom');
localStorage.setItem('customUrl', normalizeBaseUrl(defaultApiBase));
} else {
localStorage.setItem('baseUrl', 'prod');
}
}
baseUrl = getBaseUrl();
document.getElementById('server-name').textContent = getServerName();

Expand Down Expand Up @@ -220,9 +245,11 @@ <h3 class="text-lg font-semibold mb-4">API Base URL</h3>

// Debug modal handlers
document.getElementById('debug-btn').addEventListener('click', () => {
const selection = localStorage.getItem('baseUrl') || 'prod';
const defaultApiBase = getInjectedDefaultBase();
const selection = (defaultApiBase && !localStorage.getItem('customUrl')) ? 'custom' : (localStorage.getItem('baseUrl') || 'prod');
document.querySelector(`input[name="baseUrl"][value="${selection}"]`).checked = true;
document.getElementById('custom-url').value = localStorage.getItem('customUrl') || '';
const customUrlValue = localStorage.getItem('customUrl') || (selection === 'custom' ? defaultApiBase : '');
document.getElementById('custom-url').value = customUrlValue;
document.getElementById('custom-url').disabled = selection !== 'custom';
document.getElementById('debug-modal').classList.remove('hidden');
});
Expand All @@ -233,7 +260,7 @@ <h3 class="text-lg font-semibold mb-4">API Base URL</h3>

document.getElementById('debug-apply').addEventListener('click', () => {
const selection = document.querySelector('input[name="baseUrl"]:checked').value;
const customUrl = document.getElementById('custom-url').value;
const customUrl = normalizeBaseUrl(document.getElementById('custom-url').value);
localStorage.setItem('baseUrl', selection);
localStorage.setItem('customUrl', customUrl);
location.reload();
Expand Down
2 changes: 1 addition & 1 deletion internal/api/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ func NewHumaAPI(cfg *config.Config, registry service.RegistryService, mux *http.
if r.URL.Path == "/" {
// Serve UI at root
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, err := w.Write([]byte(v0.GetUIHTML()))
_, err := w.Write([]byte(v0.GetUIHTML(cfg.CustomMCPRegistryURL)))
if err != nil {
http.Error(w, "Failed to write response", http.StatusInternalServerError)
}
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Config struct {
DatabaseURL string `env:"DATABASE_URL" envDefault:"postgres://localhost:5432/mcp-registry?sslmode=disable"`
SeedFrom string `env:"SEED_FROM" envDefault:""`
Version string `env:"VERSION" envDefault:"dev"`
CustomMCPRegistryURL string `env:"CUSTOM_MCP_REGISTRY_URL" envDefault:""`
GithubClientID string `env:"GITHUB_CLIENT_ID" envDefault:""`
GithubClientSecret string `env:"GITHUB_CLIENT_SECRET" envDefault:""`
JWTPrivateKey string `env:"JWT_PRIVATE_KEY" envDefault:""`
Expand Down
Loading