feat: Enhance MCP server dropdown with search, popularity sorting, and per-chat overrides
This commit is contained in:
parent
81ad2d5569
commit
f755673c6f
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Square, Settings, ChevronDown } from '@lucide/svelte';
|
||||
import { Square, Settings, ChevronDown, Search } from '@lucide/svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
|
|
@ -19,9 +19,13 @@
|
|||
import { modelsStore, modelOptions, selectedModelId } from '$lib/stores/models.svelte';
|
||||
import { isRouterMode } from '$lib/stores/server.svelte';
|
||||
import { chatStore } from '$lib/stores/chat.svelte';
|
||||
import { activeMessages, usedModalities } from '$lib/stores/conversations.svelte';
|
||||
import {
|
||||
activeMessages,
|
||||
usedModalities,
|
||||
conversationsStore
|
||||
} from '$lib/stores/conversations.svelte';
|
||||
import { useModelChangeValidation } from '$lib/hooks/use-model-change-validation.svelte';
|
||||
import { parseMcpServerSettings } from '$lib/config/mcp';
|
||||
import { parseMcpServerSettings, parseMcpServerUsageStats } from '$lib/config/mcp';
|
||||
import type { MCPServerSettingsEntry } from '$lib/types/mcp';
|
||||
import {
|
||||
mcpGetHealthCheckState,
|
||||
|
|
@ -176,29 +180,81 @@
|
|||
});
|
||||
|
||||
let showMcpDialog = $state(false);
|
||||
let mcpSearchQuery = $state('');
|
||||
|
||||
// MCP servers state
|
||||
let mcpServers = $derived<MCPServerSettingsEntry[]>(
|
||||
parseMcpServerSettings(currentConfig.mcpServers)
|
||||
);
|
||||
let enabledMcpServers = $derived(mcpServers.filter((s) => s.enabled && s.url.trim()));
|
||||
|
||||
// Usage stats for sorting by popularity
|
||||
let mcpUsageStats = $derived(parseMcpServerUsageStats(currentConfig.mcpServerUsageStats));
|
||||
|
||||
// Get usage count for a server
|
||||
function getServerUsageCount(serverId: string): number {
|
||||
return mcpUsageStats[serverId] || 0;
|
||||
}
|
||||
|
||||
// Helper to check if server is enabled for current chat (per-chat override or global)
|
||||
function isServerEnabledForChat(server: MCPServerSettingsEntry): boolean {
|
||||
return conversationsStore.isMcpServerEnabledForChat(server.id, server.enabled);
|
||||
}
|
||||
|
||||
// Helper to check if server has per-chat override
|
||||
function hasPerChatOverride(serverId: string): boolean {
|
||||
return conversationsStore.getMcpServerOverride(serverId) !== undefined;
|
||||
}
|
||||
|
||||
// Servers enabled for current chat (considering per-chat overrides)
|
||||
let enabledMcpServersForChat = $derived(
|
||||
mcpServers.filter((s) => isServerEnabledForChat(s) && s.url.trim())
|
||||
);
|
||||
// Filter out servers with health check errors
|
||||
let healthyEnabledMcpServers = $derived(
|
||||
enabledMcpServers.filter((s) => {
|
||||
enabledMcpServersForChat.filter((s) => {
|
||||
const healthState = mcpGetHealthCheckState(s.id);
|
||||
return healthState.status !== 'error';
|
||||
})
|
||||
);
|
||||
let hasEnabledMcpServers = $derived(enabledMcpServers.length > 0);
|
||||
let hasEnabledMcpServers = $derived(enabledMcpServersForChat.length > 0);
|
||||
let hasMcpServers = $derived(mcpServers.length > 0);
|
||||
|
||||
// Sort servers: globally enabled first (by popularity), then rest (by popularity)
|
||||
let sortedMcpServers = $derived(
|
||||
[...mcpServers].sort((a, b) => {
|
||||
// First: globally enabled servers come first
|
||||
if (a.enabled !== b.enabled) return a.enabled ? -1 : 1;
|
||||
// Then sort by usage count (descending)
|
||||
const usageA = getServerUsageCount(a.id);
|
||||
const usageB = getServerUsageCount(b.id);
|
||||
if (usageB !== usageA) return usageB - usageA;
|
||||
// Then alphabetically by name
|
||||
return getServerDisplayName(a).localeCompare(getServerDisplayName(b));
|
||||
})
|
||||
);
|
||||
|
||||
// Filtered and limited servers for dropdown display
|
||||
let displayedMcpServers = $derived(() => {
|
||||
const query = mcpSearchQuery.toLowerCase().trim();
|
||||
if (query) {
|
||||
// When searching, show all matching results
|
||||
return sortedMcpServers.filter((s) => {
|
||||
const name = getServerDisplayName(s).toLowerCase();
|
||||
const url = s.url.toLowerCase();
|
||||
return name.includes(query) || url.includes(query);
|
||||
});
|
||||
}
|
||||
// When not searching, show max 4 items
|
||||
return sortedMcpServers.slice(0, 4);
|
||||
});
|
||||
|
||||
// Count of extra servers beyond the 3 shown as favicons (excluding error servers)
|
||||
let extraServersCount = $derived(Math.max(0, healthyEnabledMcpServers.length - 3));
|
||||
|
||||
// Toggle server enabled state
|
||||
function toggleServer(serverId: string, enabled: boolean) {
|
||||
const servers = mcpServers.map((s) => (s.id === serverId ? { ...s, enabled } : s));
|
||||
settingsStore.updateConfig('mcpServers', JSON.stringify(servers));
|
||||
// Toggle server enabled state for current chat (per-chat override only)
|
||||
// Global state is only changed from MCP Settings Dialog
|
||||
async function toggleServerForChat(serverId: string, globalEnabled: boolean) {
|
||||
await conversationsStore.toggleMcpServerForChat(serverId, globalEnabled);
|
||||
}
|
||||
|
||||
// Get display name for server
|
||||
|
|
@ -234,13 +290,14 @@
|
|||
.filter((f) => f.url !== null)
|
||||
);
|
||||
|
||||
// Run health checks on mount if there are enabled servers
|
||||
// All servers with valid URLs (for health checks - check all regardless of enabled state)
|
||||
let serversWithUrls = $derived(mcpServers.filter((s) => s.url.trim()));
|
||||
|
||||
// Run health checks on mount for ALL servers with URLs (to show metadata in dropdown)
|
||||
onMount(() => {
|
||||
if (hasEnabledMcpServers) {
|
||||
for (const server of enabledMcpServers) {
|
||||
if (!mcpHasHealthCheck(server.id)) {
|
||||
mcpRunHealthCheck(server);
|
||||
}
|
||||
for (const server of serversWithUrls) {
|
||||
if (!mcpHasHealthCheck(server.id)) {
|
||||
mcpRunHealthCheck(server);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -296,11 +353,29 @@
|
|||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content align="start" class="w-64">
|
||||
<div class="max-h-64 overflow-y-auto">
|
||||
{#each mcpServers as server (server.id)}
|
||||
<DropdownMenu.Content align="start" class="w-72">
|
||||
<!-- Search Input -->
|
||||
<div class="px-2 pb-2">
|
||||
<div class="relative">
|
||||
<Search
|
||||
class="absolute top-1/2 left-2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search servers..."
|
||||
bind:value={mcpSearchQuery}
|
||||
class="h-8 w-full rounded-md border border-input bg-background pr-3 pl-8 text-sm placeholder:text-muted-foreground focus:ring-1 focus:ring-ring focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Servers (sorted by popularity then alphabetically, max 4 when no search) -->
|
||||
<div class="max-h-48 overflow-y-auto">
|
||||
{#each displayedMcpServers() as server (server.id)}
|
||||
{@const healthState = mcpGetHealthCheckState(server.id)}
|
||||
{@const hasError = healthState.status === 'error'}
|
||||
{@const isEnabledForChat = isServerEnabledForChat(server)}
|
||||
{@const hasOverride = hasPerChatOverride(server.id)}
|
||||
<div class="flex items-center justify-between gap-2 px-2 py-1.5">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2">
|
||||
{#if getFaviconUrl(server)}
|
||||
|
|
@ -319,15 +394,25 @@
|
|||
class="shrink-0 rounded bg-destructive/15 px-1.5 py-0.5 text-xs text-destructive"
|
||||
>Error</span
|
||||
>
|
||||
{:else if server.enabled}
|
||||
<span class="shrink-0 rounded bg-primary/15 px-1.5 py-0.5 text-xs text-primary"
|
||||
>Global</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<Switch
|
||||
checked={server.enabled}
|
||||
onCheckedChange={(checked) => toggleServer(server.id, checked)}
|
||||
checked={isEnabledForChat}
|
||||
onCheckedChange={() => toggleServerForChat(server.id, server.enabled)}
|
||||
disabled={hasError}
|
||||
class={hasOverride ? 'ring-2 ring-primary/50 ring-offset-1' : ''}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{#if displayedMcpServers().length === 0}
|
||||
<div class="px-2 py-3 text-center text-sm text-muted-foreground">
|
||||
No servers found
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Card.Root class="bg-muted/30 p-4">
|
||||
<Card.Root class="!gap-1.5 bg-muted/30 p-4">
|
||||
{#if isEditing}
|
||||
<!-- Edit Mode -->
|
||||
<div class="space-y-4">
|
||||
|
|
|
|||
|
|
@ -269,11 +269,20 @@ class MCPStore {
|
|||
/**
|
||||
* Execute a tool call via MCP host manager.
|
||||
* Automatically routes to the appropriate server.
|
||||
* Also tracks usage statistics for the server.
|
||||
*/
|
||||
async executeTool(toolCall: MCPToolCall, signal?: AbortSignal): Promise<ToolExecutionResult> {
|
||||
if (!this._hostManager) {
|
||||
throw new Error('MCP host manager not initialized');
|
||||
}
|
||||
|
||||
// Track usage for the server that provides this tool
|
||||
const serverId = this.getToolServer(toolCall.function.name);
|
||||
if (serverId) {
|
||||
const updatedStats = incrementMcpServerUsage(config(), serverId);
|
||||
settingsStore.updateConfig('mcpServerUsageStats', updatedStats);
|
||||
}
|
||||
|
||||
return this._hostManager.executeTool(toolCall, signal);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue