Files
SuperCharged-Claude-Code-Up…/dexto/packages/webui/lib/serverRegistry.ts
admin b52318eeae feat: Add intelligent auto-router and enhanced integrations
- Add intelligent-router.sh hook for automatic agent routing
- Add AUTO-TRIGGER-SUMMARY.md documentation
- Add FINAL-INTEGRATION-SUMMARY.md documentation
- Complete Prometheus integration (6 commands + 4 tools)
- Complete Dexto integration (12 commands + 5 tools)
- Enhanced Ralph with access to all agents
- Fix /clawd command (removed disable-model-invocation)
- Update hooks.json to v5 with intelligent routing
- 291 total skills now available
- All 21 commands with automatic routing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-28 00:27:56 +04:00

195 lines
6.8 KiB
TypeScript

import type { ServerRegistryEntry, ServerRegistryFilter } from '@dexto/registry';
import { serverRegistry as sharedRegistry } from '@dexto/registry';
import { client } from './client';
/**
* MCP Server Registry Service
* Manages a registry of available MCP servers that can be quickly added to agents
*
* The built-in registry data is loaded from an external JSON file (server-registry-data.json)
* to make it easy to add new servers without rebuilding the codebase.
*/
export class ServerRegistryService {
private static instance: ServerRegistryService;
private registryEntries: ServerRegistryEntry[] = [];
private isInitialized = false;
private constructor() {
// Private constructor for singleton
}
private static normalizeId(s: string): string {
return s
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
static getInstance(): ServerRegistryService {
if (!ServerRegistryService.instance) {
ServerRegistryService.instance = new ServerRegistryService();
}
return ServerRegistryService.instance;
}
/**
* Initialize the registry with default entries and load from external sources
*/
async initialize(): Promise<void> {
if (this.isInitialized) return;
// Load built-in registry entries from shared @dexto/registry package
this.registryEntries = await this.getBuiltinEntries();
// Load custom entries from localStorage
// TODO: Load from external registry sources (GitHub, npm, etc.)
// await this.loadExternalRegistries();
this.isInitialized = true;
}
/**
* Get all registry entries with optional filtering
*/
async getEntries(filter?: ServerRegistryFilter): Promise<ServerRegistryEntry[]> {
await this.initialize();
let filtered = [...this.registryEntries];
if (filter?.category) {
filtered = filtered.filter((entry) => entry.category === filter.category);
}
if (filter?.tags?.length) {
filtered = filtered.filter((entry) =>
filter.tags!.some((tag) => entry.tags.includes(tag))
);
}
if (filter?.search) {
const searchLower = filter.search.toLowerCase();
filtered = filtered.filter(
(entry) =>
entry.name.toLowerCase().includes(searchLower) ||
entry.description.toLowerCase().includes(searchLower) ||
entry.tags.some((tag) => tag.toLowerCase().includes(searchLower))
);
}
if (filter?.installed !== undefined) {
filtered = filtered.filter((entry) => entry.isInstalled === filter.installed);
}
if (filter?.official !== undefined) {
filtered = filtered.filter((entry) => entry.isOfficial === filter.official);
}
return filtered.sort((a, b) => {
// Sort by: installed first, then official, then name
if (a.isInstalled !== b.isInstalled) {
return a.isInstalled ? -1 : 1;
}
if (a.isOfficial !== b.isOfficial) {
return a.isOfficial ? -1 : 1;
}
return a.name.localeCompare(b.name);
});
}
/**
* Update an existing registry entry
*/
async updateEntry(id: string, updates: Partial<ServerRegistryEntry>): Promise<boolean> {
const index = this.registryEntries.findIndex((entry) => entry.id === id);
if (index === -1) return false;
this.registryEntries[index] = {
...this.registryEntries[index],
...updates,
};
return true;
}
/**
* Mark a server as installed/uninstalled
*/
async setInstalled(id: string, installed: boolean): Promise<boolean> {
return this.updateEntry(id, { isInstalled: installed });
}
/**
* Sync registry installed status with current server states
* This handles both disconnected and deleted servers
*/
async syncWithServerStatus(): Promise<void> {
try {
// Ensure registry is initialized before syncing
await this.initialize();
if (this.registryEntries.length === 0) {
console.warn('Registry entries not available for status sync');
return;
}
// Fetch current server states
const response = await client.api.mcp.servers.$get();
if (!response.ok) {
throw new Error(`Failed to fetch servers: ${response.status}`);
}
const data = await response.json();
const servers = data.servers || [];
// Create set for all server IDs
const allServerIds = new Set<string>();
servers.forEach((server: { id?: string; status?: string }) => {
if (!server?.id || typeof server.id !== 'string') return;
const normalizedId = ServerRegistryService.normalizeId(server.id);
allServerIds.add(normalizedId);
});
// Update registry entries based on server status
for (const entry of this.registryEntries) {
const aliases = [entry.id, entry.name, ...(entry.matchIds || [])]
.filter(Boolean)
.map((x) => ServerRegistryService.normalizeId(String(x)));
const hasMatchingServer = aliases.some((alias) => allServerIds.has(alias));
// Note: We could also track connection status separately in the future (e.g., maintain a connectedIds set)
// Update installed status:
// - If no matching server exists, mark as uninstalled
// - If server exists but is not connected, still consider as installed (just disconnected)
// - If server is connected, mark as installed
const shouldBeInstalled = hasMatchingServer;
if (entry.isInstalled !== shouldBeInstalled) {
entry.isInstalled = shouldBeInstalled;
}
}
} catch (error) {
// Non-fatal error, log and continue
console.warn('Failed to sync registry with server status:', error);
}
}
/**
* Get server configuration for connecting
*/
async getServerConfig(id: string): Promise<ServerRegistryEntry | null> {
await this.initialize();
return this.registryEntries.find((entry) => entry.id === id) || null;
}
/**
* Built-in registry entries for popular MCP servers
* Loaded from shared @dexto/registry package
*/
private async getBuiltinEntries(): Promise<ServerRegistryEntry[]> {
return sharedRegistry.getEntries();
}
}
// Export singleton instance
export const serverRegistry = ServerRegistryService.getInstance();