<!-- htmlhint doctype-first:false -->
<script>
// Set global ROOT_PATH for consistency across all JavaScript
if (typeof window.ROOT_PATH === 'undefined') {
window.ROOT_PATH = '{{ root_path }}';
}
// Filter badge click handlers - these trigger the HTMX selects in the partial
function filterByCategory(category) {
const select = document.getElementById('category-filter');
if (select) {
select.value = category;
// Trigger HTMX change event
if (window.htmx) {
window.htmx.trigger(select, 'change');
}
}
}
function filterByAuthType(authType) {
const select = document.getElementById('auth-filter');
if (select) {
select.value = authType;
// Trigger HTMX change event
if (window.htmx) {
window.htmx.trigger(select, 'change');
}
}
}
function filterByProvider(provider) {
// Provider filter not implemented yet
console.log('Provider filter clicked:', provider);
}
</script>
<div class="space-y-6">
<!-- MCP Registry Overview Card -->
<div
class="bg-gradient-to-r from-blue-500 to-cyan-600 rounded-lg shadow-lg p-6 text-white"
>
<div class="flex justify-between items-start mb-4">
<h2 class="text-2xl font-bold flex items-center">
<svg
class="h-8 w-8 mr-3"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg>
MCP Registry
</h2>
<button
onclick="refreshCatalog()"
class="px-4 py-2 bg-white/20 hover:bg-white/30 rounded-lg transition-colors flex items-center gap-2"
title="Refresh catalog"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Refresh
</button>
</div>
<div class="grid grid-cols-3 md:grid-cols-3 gap-4">
<div class="text-center">
<div class="text-3xl font-bold">{{ stats.total_servers }}</div>
<div class="text-blue-100 text-sm">Total Servers</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold">{{ stats.registered_servers }}</div>
<div class="text-blue-100 text-sm">Registered</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold">{{ stats.categories|length }}</div>
<div class="text-blue-100 text-sm">Categories</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
>Category</label
>
<select
id="category-filter"
name="category"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
hx-get="{{ root_path }}/admin/mcp-registry/partial"
hx-trigger="change"
hx-target="#mcp-registry-servers"
hx-include="[name='auth_type'],[name='search']"
>
<option value="" {% if not filter_params.category %}selected{% endif %}>All Categories</option>
{% for category in stats.categories %}
{% set is_selected = (filter_params.category == category) %}
<option value="{{ category }}" {% if is_selected %}selected{% endif %}>{{ category }}</option>
{% endfor %}
</select>
</div>
<div>
<label
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
>Auth Type</label
>
<select
id="auth-filter"
name="auth_type"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
hx-get="{{ root_path }}/admin/mcp-registry/partial"
hx-trigger="change"
hx-target="#mcp-registry-servers"
hx-include="[name='category'],[name='search']"
>
<option value="" {% if not filter_params.auth_type %}selected{% endif %}>All Auth Types</option>
{% for auth_type in stats.auth_types %}
{% set is_selected = (filter_params.auth_type == auth_type) %}
<option value="{{ auth_type }}" {% if is_selected %}selected{% endif %}>{{ auth_type }}</option>
{% endfor %}
</select>
</div>
<div class="md:col-span-2">
<label
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
>Search</label
>
<input
type="text"
id="search-input"
name="search"
placeholder="Search servers via tags..."
value="{{ filter_params.search or '' }}"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
hx-get="{{ root_path }}/admin/mcp-registry/partial"
hx-trigger="keyup changed delay:500ms"
hx-target="#mcp-registry-servers"
hx-include="[name='category'],[name='auth_type']"
/>
</div>
</div>
</div>
<!-- Server Statistics Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Categories -->
<div class="bg-white rounded-lg shadow p-6 dark:bg-gray-800">
<h3 class="text-lg font-medium mb-4 dark:text-gray-200 flex items-center">
<svg
class="h-6 w-6 mr-2 text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
/>
</svg>
Categories
</h3>
<div class="flex flex-wrap gap-2">
{% set is_all_selected = not filter_params.category %}
<span
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium {% if is_all_selected %}bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 border border-blue-300 dark:border-blue-700{% else %}bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200{% endif %} cursor-pointer hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors"
data-filter-type="category"
data-filter-value=""
onclick="filterByCategory('')"
>
All Categories
<span class="ml-1 text-xs {% if is_all_selected %}text-blue-600 dark:text-blue-300{% else %}text-gray-500 dark:text-gray-400{% endif %}"
>({{ stats.total_servers }})</span
>
</span>
{% for category, count in stats.servers_by_category.items() %}
{% if count > 0 %}
{% set is_selected = (filter_params.category == category) %}
<span
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium {% if is_selected %}bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 border border-blue-300 dark:border-blue-700{% else %}bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200{% endif %} cursor-pointer hover:bg-blue-200 dark:hover:bg-gray-600 transition-colors"
data-filter-type="category"
data-filter-value="{{ category }}"
onclick="filterByCategory('{{ category }}')"
>
{{ category }}
<span class="ml-1 text-xs {% if is_selected %}text-blue-600 dark:text-blue-300{% else %}text-gray-500 dark:text-gray-400{% endif %}"
>({{ count }})</span
>
</span>
{% endif %}
{% endfor %}
</div>
</div>
<!-- Auth Types -->
<div class="bg-white rounded-lg shadow p-6 dark:bg-gray-800">
<h3 class="text-lg font-medium mb-4 dark:text-gray-200 flex items-center">
<svg
class="h-6 w-6 mr-2 text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
Authentication Types
</h3>
<div class="space-y-2">
{% set is_all_auth_selected = not filter_params.auth_type %}
<div
class="flex justify-between items-center p-2 {% if is_all_auth_selected %}bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800{% else %}bg-gray-50 dark:bg-gray-700{% endif %} rounded cursor-pointer hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors"
data-filter-type="auth"
data-filter-value=""
onclick="filterByAuthType('')"
>
<span
class="text-sm font-medium {% if is_all_auth_selected %}text-blue-700 dark:text-blue-300{% else %}text-gray-700 dark:text-gray-300{% endif %}"
>All Auth Types</span
>
<span
class="{% if is_all_auth_selected %}bg-blue-200 text-blue-900 dark:bg-blue-800 dark:text-blue-100{% else %}bg-gray-200 text-gray-900 dark:bg-gray-800 dark:text-gray-100{% endif %} text-xs font-medium px-2.5 py-0.5 rounded"
>
{{ stats.total_servers }}
</span>
</div>
{% for auth_type, count in stats.servers_by_auth_type.items() %}
{% if count > 0 %}
{% set is_auth_selected = (filter_params.auth_type == auth_type) %}
<div
class="flex justify-between items-center p-2 {% if is_auth_selected %}bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800{% else %}bg-gray-50 dark:bg-gray-700{% endif %} rounded cursor-pointer hover:bg-blue-100 dark:hover:bg-gray-600 transition-colors"
data-filter-type="auth"
data-filter-value="{{ auth_type }}"
onclick="filterByAuthType('{{ auth_type }}')"
>
<span class="text-sm font-medium {% if is_auth_selected %}text-blue-700 dark:text-blue-300{% else %}text-gray-700 dark:text-gray-300{% endif %}"
>{{ auth_type }}</span
>
<span
class="{% if is_auth_selected %}bg-blue-200 text-blue-900 dark:bg-blue-800 dark:text-blue-100{% else %}bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200{% endif %} text-xs font-medium px-2.5 py-0.5 rounded"
>
{{ count }}
</span>
</div>
{% endif %}
{% endfor %}
</div>
</div>
<!-- Providers -->
<div class="bg-white rounded-lg shadow p-6 dark:bg-gray-800">
<h3 class="text-lg font-medium mb-4 dark:text-gray-200 flex items-center">
<svg
class="h-6 w-6 mr-2 text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
/>
</svg>
Providers
</h3>
<div class="flex flex-wrap gap-2">
<span
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 cursor-pointer hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors border border-blue-300 dark:border-blue-700"
data-filter-type="provider"
data-filter-value=""
onclick="filterByProvider('')"
>
All Providers
<span class="ml-1 text-xs text-blue-600 dark:text-blue-300"
>({{ stats.total_servers }})</span
>
</span>
{% for provider, count in stats.servers_by_provider.items() %}
{% if count > 0 %}
<span
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
data-filter-type="provider"
data-filter-value="{{ provider | lower }}"
onclick="filterByProvider('{{ provider | lower }}')"
>
{{ provider }}
<span class="ml-1 text-xs text-gray-500 dark:text-gray-400"
>({{ count }})</span
>
</span>
{% endif %}
{% endfor %}
</div>
</div>
</div>
<!-- Note: Filter controls are in the main admin.html template and use HTMX for server-side filtering -->
<!-- Pagination Controls -->
{% if total_pages > 1 %}
<div class="flex justify-center items-center gap-2">
<!-- First Page -->
{% if page > 1 %}
<a
href="{{ root_path }}/admin/mcp-registry/partial?page=1{% if filter_params.category %}&category={{ filter_params.category }}{% endif %}{% if filter_params.auth_type %}&auth_type={{ filter_params.auth_type }}{% endif %}{% if filter_params.search %}&search={{ filter_params.search }}{% endif %}"
hx-get="{{ root_path }}/admin/mcp-registry/partial?page=1{% if filter_params.category %}&category={{ filter_params.category }}{% endif %}{% if filter_params.auth_type %}&auth_type={{ filter_params.auth_type }}{% endif %}{% if filter_params.search %}&search={{ filter_params.search }}{% endif %}"
hx-target="#mcp-registry-content"
hx-swap="innerHTML"
class="px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 transition-colors"
>
First
</a>
<!-- Previous Page -->
<a
href="{{ root_path }}/admin/mcp-registry/partial?page={{ page - 1 }}{% if filter_params.category %}&category={{ filter_params.category }}{% endif %}{% if filter_params.auth_type %}&auth_type={{ filter_params.auth_type }}{% endif %}{% if filter_params.search %}&search={{ filter_params.search }}{% endif %}"
hx-get="{{ root_path }}/admin/mcp-registry/partial?page={{ page - 1 }}{% if filter_params.category %}&category={{ filter_params.category }}{% endif %}{% if filter_params.auth_type %}&auth_type={{ filter_params.auth_type }}{% endif %}{% if filter_params.search %}&search={{ filter_params.search }}{% endif %}"
hx-target="#mcp-registry-content"
hx-swap="innerHTML"
class="px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 transition-colors"
>
Previous
</a>
{% else %}
<button
disabled
class="px-3 py-2 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-400 dark:text-gray-500 cursor-not-allowed"
>
First
</button>
<button
disabled
class="px-3 py-2 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-400 dark:text-gray-500 cursor-not-allowed"
>
Previous
</button>
{% endif %}
<!-- Page Info -->
<span class="px-4 py-2 text-gray-700 dark:text-gray-300">
Page {{ page }} of {{ total_pages }}
</span>
<!-- Next Page -->
{% if page < total_pages %}
<a
href="{{ root_path }}/admin/mcp-registry/partial?page={{ page + 1 }}{% if filter_params.category %}&category={{ filter_params.category }}{% endif %}{% if filter_params.auth_type %}&auth_type={{ filter_params.auth_type }}{% endif %}{% if filter_params.search %}&search={{ filter_params.search }}{% endif %}"
hx-get="{{ root_path }}/admin/mcp-registry/partial?page={{ page + 1 }}{% if filter_params.category %}&category={{ filter_params.category }}{% endif %}{% if filter_params.auth_type %}&auth_type={{ filter_params.auth_type }}{% endif %}{% if filter_params.search %}&search={{ filter_params.search }}{% endif %}"
hx-target="#mcp-registry-content"
hx-swap="innerHTML"
class="px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 transition-colors"
>
Next
</a>
<!-- Last Page -->
<a
href="{{ root_path }}/admin/mcp-registry/partial?page={{ total_pages }}{% if filter_params.category %}&category={{ filter_params.category }}{% endif %}{% if filter_params.auth_type %}&auth_type={{ filter_params.auth_type }}{% endif %}{% if filter_params.search %}&search={{ filter_params.search }}{% endif %}"
hx-get="{{ root_path }}/admin/mcp-registry/partial?page={{ total_pages }}{% if filter_params.category %}&category={{ filter_params.category }}{% endif %}{% if filter_params.auth_type %}&auth_type={{ filter_params.auth_type }}{% endif %}{% if filter_params.search %}&search={{ filter_params.search }}{% endif %}"
hx-target="#mcp-registry-content"
hx-swap="innerHTML"
class="px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 transition-colors"
>
Last
</a>
{% else %}
<button
disabled
class="px-3 py-2 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-400 dark:text-gray-500 cursor-not-allowed"
>
Next
</button>
<button
disabled
class="px-3 py-2 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-400 dark:text-gray-500 cursor-not-allowed"
>
Last
</button>
{% endif %}
</div>
{% endif %}
<!-- Server Grid -->
<div
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
id="server-grid"
>
{% for server in servers %}
<div
class="server-card bg-white rounded-lg shadow-lg p-6 hover:shadow-xl transition-shadow dark:bg-gray-800"
data-name="{{ server.name | lower }}"
data-description="{{ server.description | lower }}"
data-provider="{{ server.provider | lower }}"
data-category="{{ server.category }}"
data-auth-type="{{ server.auth_type }}"
data-registered="{{ server.is_registered }}"
>
<!-- Server Header -->
<div class="flex justify-between items-start mb-3">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
{{ server.name }}
</h3>
{% if server.is_registered %}
<span class="w-2 h-2 bg-green-500 rounded-full"></span>
{% endif %}
</div>
<!-- Badges -->
<div class="flex gap-2 mb-3">
{% if server.auth_type == "OAuth2.1" %}
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
>
{{ server.auth_type }}
</span>
{% elif server.auth_type == "API Key" %}
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200"
>
{{ server.auth_type }}
</span>
{% elif server.auth_type == "Open" %}
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
>
{{ server.auth_type }}
</span>
{% else %}
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200"
>
{{ server.auth_type }}
</span>
{% endif %}
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200"
>
{{ server.category }}
</span>
</div>
<!-- Description -->
<p class="text-sm text-gray-600 dark:text-gray-300 mb-4 line-clamp-2">
{{ server.description[:100] }}{% if server.description|length > 100
%}...{% endif %}
</p>
<!-- Provider and URL -->
<div class="text-xs text-gray-500 dark:text-gray-400 mb-3">
<div class="truncate">
<span class="font-semibold">Provider:</span> {{ server.provider }}
</div>
<div class="truncate">
<span class="font-semibold">URL:</span> {{ server.url }}
</div>
</div>
<!-- Tags -->
{% if server.tags %}
<div class="flex flex-wrap gap-1 mb-4">
{% for tag in server.tags[:3] %}
<span class="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded dark:bg-gray-700 dark:text-gray-300">
{% if tag is mapping %}
{{ tag.id }}
{% else %}
{{ tag }}
{% endif %}
</span>
{% endfor %}
{% if server.tags|length > 3 %}
<span class="px-2 py-1 text-gray-500 text-xs">+{{ server.tags|length - 3 }} more</span>
{% endif %}
</div>
{% endif %}
<!-- Action Button -->
{% if server.is_registered %}
{% if server.requires_oauth_config %}
<button
class="w-full px-4 py-2 bg-yellow-600 text-white rounded-md cursor-default"
disabled
title="Server is registered but OAuth configuration is required before activation"
>
<svg class="inline-block h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
OAuth Config Required
</button>
{% else %}
<button
class="w-full px-4 py-2 bg-gray-300 text-gray-600 rounded-md cursor-not-allowed"
disabled
>
Already Registered
</button>
{% endif %}
{% else %} {% if server.requires_api_key or server.auth_type in ["API
Key", "OAuth2.1 & API Key"] %}
<button
class="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
onclick="showApiKeyModal('{{ server.id }}', '{{ server.name }}', '{{ server.url }}')"
>
Add Server
</button>
{% else %}
<div id="{{ server.id }}-button-container">
<button
id="{{ server.id }}-register-btn"
class="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed"
hx-post="{{ root_path }}/admin/mcp-registry/{{ server.id }}/register"
hx-target="#{{ server.id }}-button-container"
hx-swap="innerHTML"
hx-disabled-elt="this"
hx-on::before-request="this.innerHTML = '<span class=\'inline-flex items-center\'><span class=\'inline-block animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2\'></span>Registering...</span>'"
hx-on::response-error="this.className = 'w-full px-4 py-2 bg-orange-600 text-white rounded-md hover:bg-orange-700 transition-colors'; this.innerHTML = '<svg class=\'inline-block h-4 w-4 mr-2\' fill=\'none\' stroke=\'currentColor\' viewBox=\'0 0 24 24\'><path stroke-linecap=\'round\' stroke-linejoin=\'round\' stroke-width=\'2\' d=\'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\'></path></svg>Request Failed - Click to Retry'; this.disabled = false;"
>
Add Server
</button>
</div>
{% endif %} {% endif %}
</div>
{% endfor %}
</div>
{% if not servers %}
<div class="bg-gray-50 rounded-lg p-8 text-center dark:bg-gray-800">
<svg
class="mx-auto h-12 w-12 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">
No servers found
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
No servers match your current filters.
</p>
</div>
{% endif %}
</div>
<!-- API Key Modal (for servers requiring API key) -->
<div
id="api-key-modal"
class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50"
>
<div
class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 lg:w-1/3 shadow-lg rounded-md bg-white dark:bg-gray-800"
>
<div class="mt-3">
<div class="flex justify-between items-start mb-4">
<h3
class="text-lg font-medium text-gray-900 dark:text-gray-100"
id="modal-server-name"
>
Add Server
</h3>
<button
onclick="closeApiKeyModal()"
class="text-gray-400 hover:text-gray-500"
>
<svg
class="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div class="mt-2">
<input
type="hidden"
id="modal-server-id"
/>
<input
type="hidden"
id="modal-server-url"
/>
<label
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>Custom Name (Optional)</label
>
<input
type="text"
id="modal-custom-name"
class="w-full px-4 py-2 mb-4 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
placeholder="Leave blank to use default name"
/>
<label
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>API Key</label
>
<input
type="password"
id="modal-api-key"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
placeholder="Enter your API key"
/>
<button
onclick="registerServerWithApiKey()"
class="w-full mt-4 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors"
>
Register Server
</button>
</div>
</div>
</div>
</div>
<script>
// API Key modal functions
function showApiKeyModal(serverId, serverName, serverUrl) {
document.getElementById('modal-server-id').value = serverId;
document.getElementById('modal-server-name').textContent = 'Add ' + serverName;
document.getElementById('modal-server-url').value = serverUrl;
document.getElementById('modal-api-key').value = '';
document.getElementById('api-key-modal').classList.remove('hidden');
}
function closeApiKeyModal() {
document.getElementById('api-key-modal').classList.add('hidden');
}
function registerServerWithApiKey() {
const serverId = document.getElementById('modal-server-id').value;
const apiKey = document.getElementById('modal-api-key').value;
const customName = document.getElementById('modal-custom-name').value;
if (!apiKey) {
alert('Please enter an API key');
return;
}
// Build request body
const requestBody = {
server_id: serverId,
api_key: apiKey
};
// Add custom name if provided
if (customName && customName.trim()) {
requestBody.name = customName.trim();
}
// Use HTMX to post the registration
const rootPath = document.querySelector('[data-root-path]')?.dataset.rootPath || window.ROOT_PATH || '';
fetch(`${rootPath}/admin/mcp-registry/${serverId}/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + (getCookie('jwt_token') || '')
},
body: JSON.stringify(requestBody)
})
.then(response => response.json())
.then(data => {
if (data.success) {
closeApiKeyModal();
// Refresh the server list
if (window.htmx && window.htmx.ajax) {
window.htmx.ajax('GET', `${rootPath}/admin/mcp-registry/partial`, {target: '#mcp-registry-servers', swap: 'innerHTML'});
} else {
location.reload();
}
} else {
alert('Registration failed: ' + (data.error || data.message));
}
})
.catch(error => {
alert('Registration failed: ' + error.message);
});
}
// Helper function to get cookie
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return '';
}
// Refresh catalog by reloading the partial
function refreshCatalog() {
const rootPath = window.ROOT_PATH || '';
if (window.htmx && window.htmx.ajax) {
window.htmx.ajax('GET', `${rootPath}/admin/mcp-registry/partial`, {target: '#mcp-registry-servers', swap: 'innerHTML'});
} else {
location.reload();
}
}
// Listen for successful catalog registration to trigger delayed refresh
// This is triggered by the HX-Trigger-After-Swap header from the server
// Use a guard to prevent re-attaching on partial refresh
if (!window._catalogRegistrationListenerAttached) {
window._catalogRegistrationListenerAttached = true;
// Store timeout ID for debouncing - prevents multiple refreshes when registering multiple servers
window._catalogRefreshTimeout = null;
document.body.addEventListener('catalogRegistrationSuccess', function(evt) {
const delayMs = evt.detail && evt.detail.delayMs ? evt.detail.delayMs : 1500;
// Clear any pending refresh to debounce rapid registrations
if (window._catalogRefreshTimeout) {
clearTimeout(window._catalogRefreshTimeout);
}
window._catalogRefreshTimeout = setTimeout(function() {
window._catalogRefreshTimeout = null;
refreshCatalog();
}, delayMs);
});
}
</script>