refactor: MCP state management + stores/clients relationship
This commit is contained in:
parent
9c53bd4486
commit
58ab834b18
|
|
@ -36,10 +36,9 @@ import type {
|
|||
} from '$lib/types/mcp';
|
||||
import type { McpServerOverride } from '$lib/types/database';
|
||||
import { MCPError } from '$lib/errors';
|
||||
import { buildMcpClientConfig, incrementMcpServerUsage } from '$lib/utils/mcp';
|
||||
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { buildMcpClientConfig, detectMcpTransportFromUrl } from '$lib/utils/mcp';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp';
|
||||
import { detectMcpTransportFromUrl } from '$lib/utils/mcp';
|
||||
|
||||
export type HealthCheckState =
|
||||
| { status: 'idle' }
|
||||
|
|
@ -68,6 +67,7 @@ export class MCPClient {
|
|||
}) => void;
|
||||
|
||||
private onHealthCheckChange?: (serverId: string, state: HealthCheckState) => void;
|
||||
private onServerUsage?: (serverId: string) => void;
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -99,6 +99,13 @@ export class MCPClient {
|
|||
this.onHealthCheckChange = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set callback for server usage tracking
|
||||
*/
|
||||
setServerUsageCallback(callback: (serverId: string) => void): void {
|
||||
this.onServerUsage = callback;
|
||||
}
|
||||
|
||||
private notifyStateChange(state: Parameters<NonNullable<typeof this.onStateChange>>[0]): void {
|
||||
this.onStateChange?.(state);
|
||||
}
|
||||
|
|
@ -435,8 +442,7 @@ export class MCPClient {
|
|||
throw new MCPError(`Server "${serverName}" is not connected`, -32000);
|
||||
}
|
||||
|
||||
const updatedStats = incrementMcpServerUsage(config(), serverName);
|
||||
settingsStore.updateConfig('mcpServerUsageStats', updatedStats);
|
||||
this.onServerUsage?.(serverName);
|
||||
|
||||
const args = this.parseToolArguments(toolCall.function.arguments);
|
||||
return MCPService.callTool(connection, { name: toolName, arguments: args }, signal);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import {
|
||||
chatStore,
|
||||
pendingEditMessageId,
|
||||
clearPendingEditMessageId,
|
||||
removeSystemPromptPlaceholder
|
||||
} from '$lib/stores/chat.svelte';
|
||||
import { chatStore, pendingEditMessageId } from '$lib/stores/chat.svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { DatabaseService } from '$lib/services';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
|
|
@ -86,7 +81,7 @@
|
|||
|
||||
if (pendingId && pendingId === message.id && !isEditing) {
|
||||
handleEdit();
|
||||
clearPendingEditMessageId();
|
||||
chatStore.clearPendingEditMessageId();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -95,7 +90,7 @@
|
|||
|
||||
// If canceling a new system message with placeholder content, remove it without deleting children
|
||||
if (message.role === MessageRole.SYSTEM) {
|
||||
const conversationDeleted = await removeSystemPromptPlaceholder(message.id);
|
||||
const conversationDeleted = await chatStore.removeSystemPromptPlaceholder(message.id);
|
||||
|
||||
if (conversationDeleted) {
|
||||
goto(`${base}/`);
|
||||
|
|
@ -187,7 +182,7 @@
|
|||
|
||||
// If content is empty, remove without deleting children
|
||||
if (!newContent) {
|
||||
const conversationDeleted = await removeSystemPromptPlaceholder(message.id);
|
||||
const conversationDeleted = await chatStore.removeSystemPromptPlaceholder(message.id);
|
||||
isEditing = false;
|
||||
if (conversationDeleted) {
|
||||
goto(`${base}/`);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
import { AttachmentType, FileTypeCategory, MimeTypeText } from '$lib/enums';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { useModelChangeValidation } from '$lib/hooks/use-model-change-validation.svelte';
|
||||
import { setEditModeActive, clearEditMode } from '$lib/stores/chat.svelte';
|
||||
import { chatStore } from '$lib/stores/chat.svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { modelsStore } from '$lib/stores/models.svelte';
|
||||
import { isRouterMode } from '$lib/stores/server.svelte';
|
||||
|
|
@ -266,10 +266,10 @@
|
|||
});
|
||||
|
||||
$effect(() => {
|
||||
setEditModeActive(processNewFiles);
|
||||
chatStore.setEditModeActive(processNewFiles);
|
||||
|
||||
return () => {
|
||||
clearEditMode();
|
||||
chatStore.clearEditMode();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { McpLogo, McpSettingsSection } from '$lib/components/app';
|
||||
|
||||
|
|
@ -11,38 +10,9 @@
|
|||
|
||||
let { onOpenChange, open = $bindable(false) }: Props = $props();
|
||||
|
||||
let localConfig = $state(config());
|
||||
|
||||
function handleClose() {
|
||||
onOpenChange?.(false);
|
||||
}
|
||||
|
||||
function handleConfigChange(key: string, value: string | boolean) {
|
||||
localConfig = { ...localConfig, [key]: value };
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
// Save all changes to settingsStore
|
||||
Object.entries(localConfig).forEach(([key, value]) => {
|
||||
if (config()[key as keyof typeof localConfig] !== value) {
|
||||
settingsStore.updateConfig(key as keyof typeof localConfig, value);
|
||||
}
|
||||
});
|
||||
onOpenChange?.(false);
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
// Reset to current config
|
||||
localConfig = config();
|
||||
onOpenChange?.(false);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
// Reset local config when dialog opens
|
||||
localConfig = config();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Dialog.Root {open} onOpenChange={handleClose}>
|
||||
|
|
@ -63,12 +33,11 @@
|
|||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-4 md:p-6">
|
||||
<McpSettingsSection {localConfig} onConfigChange={handleConfigChange} />
|
||||
<McpSettingsSection />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 border-t p-4 md:p-6">
|
||||
<Button variant="outline" onclick={handleCancel}>Cancel</Button>
|
||||
<Button onclick={handleSave}>Save Changes</Button>
|
||||
<Button onclick={handleClose}>Close</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@
|
|||
import McpLogo from '$lib/components/app/misc/McpLogo.svelte';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { parseMcpServerSettings, parseMcpServerUsageStats } from '$lib/utils/mcp';
|
||||
import { parseMcpServerSettings, getServerDisplayName, getFaviconUrl } from '$lib/utils/mcp';
|
||||
import type { MCPServerSettingsEntry } from '$lib/types/mcp';
|
||||
import {
|
||||
mcpGetHealthCheckState,
|
||||
mcpHasHealthCheck,
|
||||
mcpRunHealthCheck
|
||||
mcpGetUsageStats
|
||||
} from '$lib/stores/mcp.svelte';
|
||||
import { extractServerNameFromUrl, getFaviconUrl } from '$lib/utils/mcp';
|
||||
import { mcpClient } from '$lib/clients/mcp.client';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
|
||||
let hasMcpServers = $derived(mcpServers.length > 0);
|
||||
|
||||
let mcpUsageStats = $derived(parseMcpServerUsageStats(settingsStore.config.mcpServerUsageStats));
|
||||
let mcpUsageStats = $derived(mcpGetUsageStats());
|
||||
|
||||
function getServerUsageCount(serverId: string): number {
|
||||
return mcpUsageStats[serverId] || 0;
|
||||
|
|
@ -94,11 +94,6 @@
|
|||
await conversationsStore.toggleMcpServerForChat(serverId, globalEnabled);
|
||||
}
|
||||
|
||||
function getServerDisplayName(server: MCPServerSettingsEntry): string {
|
||||
if (server.name) return server.name;
|
||||
return extractServerNameFromUrl(server.url);
|
||||
}
|
||||
|
||||
let mcpFavicons = $derived(
|
||||
healthyEnabledMcpServers
|
||||
.slice(0, 3)
|
||||
|
|
@ -111,7 +106,7 @@
|
|||
onMount(() => {
|
||||
for (const server of serversWithUrls) {
|
||||
if (!mcpHasHealthCheck(server.id)) {
|
||||
mcpRunHealthCheck(server);
|
||||
mcpClient.runHealthCheck(server);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
import {
|
||||
mcpGetHealthCheckState,
|
||||
mcpHasHealthCheck,
|
||||
mcpRunHealthCheck,
|
||||
type HealthCheckState
|
||||
} from '$lib/stores/mcp.svelte';
|
||||
import { mcpClient } from '$lib/clients/mcp.client';
|
||||
import McpServerCardHeader from './McpServerCardHeader.svelte';
|
||||
import McpServerCardActions from './McpServerCardActions.svelte';
|
||||
import McpServerCardToolsList from './McpServerCardToolsList.svelte';
|
||||
|
|
@ -38,12 +38,12 @@
|
|||
|
||||
onMount(() => {
|
||||
if (!mcpHasHealthCheck(server.id) && server.enabled && server.url.trim()) {
|
||||
mcpRunHealthCheck(server);
|
||||
mcpClient.runHealthCheck(server);
|
||||
}
|
||||
});
|
||||
|
||||
function handleHealthCheck() {
|
||||
mcpRunHealthCheck(server);
|
||||
mcpClient.runHealthCheck(server);
|
||||
}
|
||||
|
||||
function startEditing() {
|
||||
|
|
@ -67,7 +67,7 @@
|
|||
isEditing = false;
|
||||
|
||||
if (server.enabled && url) {
|
||||
setTimeout(() => mcpRunHealthCheck({ ...server, url }), 100);
|
||||
setTimeout(() => mcpClient.runHealthCheck({ ...server, url }), 100);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,23 +2,14 @@
|
|||
import { Plus, X } from '@lucide/svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { parseMcpServerSettings } from '$lib/utils/mcp';
|
||||
import { getServerDisplayName, getFaviconUrl } from '$lib/utils/mcp';
|
||||
import type { MCPServerSettingsEntry } from '$lib/types/mcp';
|
||||
import type { SettingsConfigType } from '$lib/types/settings';
|
||||
import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp';
|
||||
import { extractServerNameFromUrl, getFaviconUrl } from '$lib/utils/mcp';
|
||||
import { mcpStore, mcpGetServers } from '$lib/stores/mcp.svelte';
|
||||
import { McpServerCard } from '$lib/components/app/mcp/McpServerCard';
|
||||
import McpServerForm from './McpServerForm.svelte';
|
||||
|
||||
interface Props {
|
||||
localConfig: SettingsConfigType;
|
||||
onConfigChange: (key: string, value: string | boolean) => void;
|
||||
}
|
||||
|
||||
let { localConfig, onConfigChange }: Props = $props();
|
||||
|
||||
// Get servers from localConfig
|
||||
let servers = $derived<MCPServerSettingsEntry[]>(parseMcpServerSettings(localConfig.mcpServers));
|
||||
// Get servers from store
|
||||
let servers = $derived<MCPServerSettingsEntry[]>(mcpGetServers());
|
||||
|
||||
// New server form state
|
||||
let isAddingServer = $state(false);
|
||||
|
|
@ -36,10 +27,6 @@
|
|||
}
|
||||
});
|
||||
|
||||
function serializeServers(updatedServers: MCPServerSettingsEntry[]) {
|
||||
onConfigChange('mcpServers', JSON.stringify(updatedServers));
|
||||
}
|
||||
|
||||
function showAddServerForm() {
|
||||
isAddingServer = true;
|
||||
newServerUrl = '';
|
||||
|
|
@ -54,35 +41,15 @@
|
|||
|
||||
function saveNewServer() {
|
||||
if (newServerUrlError) return;
|
||||
const newServer: MCPServerSettingsEntry = {
|
||||
id: crypto.randomUUID ? crypto.randomUUID() : `server-${Date.now()}`,
|
||||
mcpStore.addServer({
|
||||
enabled: true,
|
||||
url: newServerUrl.trim(),
|
||||
headers: newServerHeaders.trim() || undefined,
|
||||
requestTimeoutSeconds: DEFAULT_MCP_CONFIG.requestTimeoutSeconds
|
||||
};
|
||||
serializeServers([...servers, newServer]);
|
||||
headers: newServerHeaders.trim() || undefined
|
||||
});
|
||||
isAddingServer = false;
|
||||
newServerUrl = '';
|
||||
newServerHeaders = '';
|
||||
}
|
||||
|
||||
function updateServer(id: string, updates: Partial<MCPServerSettingsEntry>) {
|
||||
const nextServers = servers.map((server) =>
|
||||
server.id === id ? { ...server, ...updates } : server
|
||||
);
|
||||
serializeServers(nextServers);
|
||||
}
|
||||
|
||||
function removeServer(id: string) {
|
||||
serializeServers(servers.filter((server) => server.id !== id));
|
||||
}
|
||||
|
||||
// Get display name for server
|
||||
function getServerDisplayName(server: MCPServerSettingsEntry): string {
|
||||
if (server.name) return server.name;
|
||||
return extractServerNameFromUrl(server.url);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
|
|
@ -154,9 +121,9 @@
|
|||
{server}
|
||||
displayName={getServerDisplayName(server)}
|
||||
faviconUrl={getFaviconUrl(server.url)}
|
||||
onToggle={(enabled) => updateServer(server.id, { enabled })}
|
||||
onUpdate={(updates) => updateServer(server.id, updates)}
|
||||
onDelete={() => removeServer(server.id)}
|
||||
onToggle={(enabled) => mcpStore.updateServer(server.id, { enabled })}
|
||||
onUpdate={(updates) => mcpStore.updateServer(server.id, updates)}
|
||||
onDelete={() => mcpStore.removeServer(server.id)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -204,6 +204,10 @@ class ChatStore {
|
|||
return this.addFilesHandler;
|
||||
}
|
||||
|
||||
clearPendingEditMessageId(): void {
|
||||
this.pendingEditMessageId = null;
|
||||
}
|
||||
|
||||
getAllLoadingChats(): string[] {
|
||||
return Array.from(this.chatLoadingStates.keys());
|
||||
}
|
||||
|
|
@ -333,8 +337,8 @@ class ChatStore {
|
|||
|
||||
export const chatStore = new ChatStore();
|
||||
|
||||
// State access functions (getters only - use chatStore.method() for actions)
|
||||
export const activeProcessingState = () => chatStore.activeProcessingState;
|
||||
export const clearEditMode = () => chatStore.clearEditMode();
|
||||
export const currentResponse = () => chatStore.currentResponse;
|
||||
export const errorDialog = () => chatStore.errorDialogState;
|
||||
export const getAddFilesHandler = () => chatStore.getAddFilesHandler();
|
||||
|
|
@ -345,9 +349,4 @@ export const isChatLoading = (convId: string) => chatStore.isChatLoadingPublic(c
|
|||
export const isChatStreaming = () => chatStore.isStreaming();
|
||||
export const isEditing = () => chatStore.isEditing();
|
||||
export const isLoading = () => chatStore.isLoading;
|
||||
export const setEditModeActive = (handler: (files: File[]) => void) =>
|
||||
chatStore.setEditModeActive(handler);
|
||||
export const pendingEditMessageId = () => chatStore.pendingEditMessageId;
|
||||
export const clearPendingEditMessageId = () => (chatStore.pendingEditMessageId = null);
|
||||
export const removeSystemPromptPlaceholder = (messageId: string) =>
|
||||
chatStore.removeSystemPromptPlaceholder(messageId);
|
||||
|
|
|
|||
|
|
@ -20,16 +20,37 @@
|
|||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { mcpClient, type HealthCheckState, type HealthCheckParams } from '$lib/clients';
|
||||
import type {
|
||||
OpenAIToolDefinition,
|
||||
ServerStatus,
|
||||
ToolExecutionResult,
|
||||
MCPToolCall
|
||||
} from '$lib/types/mcp';
|
||||
import { mcpClient, type HealthCheckState } from '$lib/clients';
|
||||
import type { MCPServerSettingsEntry, McpServerUsageStats } from '$lib/types/mcp';
|
||||
import type { McpServerOverride } from '$lib/types/database';
|
||||
import { buildMcpClientConfig } from '$lib/utils/mcp';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { buildMcpClientConfig, parseMcpServerSettings } from '$lib/utils/mcp';
|
||||
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp';
|
||||
|
||||
/**
|
||||
* Parses MCP server usage stats from settings.
|
||||
* @param rawStats - The raw stats to parse
|
||||
* @returns MCP server usage stats or empty object if invalid
|
||||
*/
|
||||
function parseMcpServerUsageStats(rawStats: unknown): McpServerUsageStats {
|
||||
if (!rawStats) return {};
|
||||
|
||||
if (typeof rawStats === 'string') {
|
||||
const trimmed = rawStats.trim();
|
||||
if (!trimmed) return {};
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||
return parsed as McpServerUsageStats;
|
||||
}
|
||||
} catch {
|
||||
console.warn('[MCP] Failed to parse mcpServerUsageStats JSON, ignoring value');
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export type { HealthCheckState };
|
||||
|
||||
|
|
@ -60,6 +81,10 @@ class MCPStore {
|
|||
mcpClient.setHealthCheckCallback((serverId, state) => {
|
||||
this._healthChecks = { ...this._healthChecks, [serverId]: state };
|
||||
});
|
||||
|
||||
mcpClient.setServerUsageCallback((serverId) => {
|
||||
this.incrementServerUsage(serverId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -104,67 +129,6 @@ class MCPStore {
|
|||
return mcpClient.getToolNames();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure MCP is initialized with current config.
|
||||
* @param perChatOverrides - Optional per-chat MCP server overrides
|
||||
*/
|
||||
async ensureInitialized(perChatOverrides?: McpServerOverride[]): Promise<boolean> {
|
||||
return mcpClient.ensureInitialized(perChatOverrides);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown MCP connections and clear state
|
||||
*/
|
||||
async shutdown(): Promise<void> {
|
||||
return mcpClient.shutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool definitions for LLM (OpenAI function calling format)
|
||||
*/
|
||||
getToolDefinitions(): OpenAIToolDefinition[] {
|
||||
return mcpClient.getToolDefinitionsForLLM();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status of all servers
|
||||
*/
|
||||
getServersStatus(): ServerStatus[] {
|
||||
return mcpClient.getServersStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a tool call via MCP.
|
||||
*/
|
||||
async executeTool(toolCall: MCPToolCall, signal?: AbortSignal): Promise<ToolExecutionResult> {
|
||||
return mcpClient.executeTool(toolCall, signal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a tool by name with arguments.
|
||||
*/
|
||||
async executeToolByName(
|
||||
toolName: string,
|
||||
args: Record<string, unknown>,
|
||||
signal?: AbortSignal
|
||||
): Promise<ToolExecutionResult> {
|
||||
return mcpClient.executeToolByName(toolName, args, signal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool exists
|
||||
*/
|
||||
hasTool(toolName: string): boolean {
|
||||
return mcpClient.hasTool(toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get which server provides a specific tool
|
||||
*/
|
||||
getToolServer(toolName: string): string | undefined {
|
||||
return mcpClient.getToolServer(toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get health check state for a specific server
|
||||
*/
|
||||
|
|
@ -179,13 +143,6 @@ class MCPStore {
|
|||
return serverId in this._healthChecks && this._healthChecks[serverId].status !== 'idle';
|
||||
}
|
||||
|
||||
/**
|
||||
* Run health check for a specific server
|
||||
*/
|
||||
async runHealthCheck(server: HealthCheckParams): Promise<void> {
|
||||
return mcpClient.runHealthCheck(server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear health check state for a specific server
|
||||
*/
|
||||
|
|
@ -208,6 +165,107 @@ class MCPStore {
|
|||
clearError(): void {
|
||||
this._error = null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Server Management (CRUD)
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get all configured MCP servers from settings
|
||||
*/
|
||||
getServers(): MCPServerSettingsEntry[] {
|
||||
return parseMcpServerSettings(config().mcpServers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new MCP server
|
||||
*/
|
||||
addServer(
|
||||
serverData: Omit<MCPServerSettingsEntry, 'id' | 'requestTimeoutSeconds'> & { id?: string }
|
||||
): void {
|
||||
const servers = this.getServers();
|
||||
const newServer: MCPServerSettingsEntry = {
|
||||
id: serverData.id || (crypto.randomUUID ? crypto.randomUUID() : `server-${Date.now()}`),
|
||||
enabled: serverData.enabled,
|
||||
url: serverData.url.trim(),
|
||||
name: serverData.name,
|
||||
headers: serverData.headers?.trim() || undefined,
|
||||
requestTimeoutSeconds: DEFAULT_MCP_CONFIG.requestTimeoutSeconds
|
||||
};
|
||||
settingsStore.updateConfig('mcpServers', JSON.stringify([...servers, newServer]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing MCP server
|
||||
*/
|
||||
updateServer(id: string, updates: Partial<MCPServerSettingsEntry>): void {
|
||||
const servers = this.getServers();
|
||||
const nextServers = servers.map((server) =>
|
||||
server.id === id ? { ...server, ...updates } : server
|
||||
);
|
||||
settingsStore.updateConfig('mcpServers', JSON.stringify(nextServers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an MCP server by ID
|
||||
*/
|
||||
removeServer(id: string): void {
|
||||
const servers = this.getServers();
|
||||
settingsStore.updateConfig('mcpServers', JSON.stringify(servers.filter((s) => s.id !== id)));
|
||||
this.clearHealthCheck(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a server is enabled considering per-chat overrides
|
||||
*/
|
||||
isServerEnabled(server: MCPServerSettingsEntry, perChatOverrides?: McpServerOverride[]): boolean {
|
||||
if (perChatOverrides) {
|
||||
const override = perChatOverrides.find((o) => o.serverId === server.id);
|
||||
if (override !== undefined) {
|
||||
return override.enabled;
|
||||
}
|
||||
}
|
||||
return server.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any enabled MCP servers
|
||||
*/
|
||||
hasEnabledServers(perChatOverrides?: McpServerOverride[]): boolean {
|
||||
return Boolean(buildMcpClientConfig(config(), perChatOverrides));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Server Usage Stats
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get parsed usage stats for all servers
|
||||
*/
|
||||
getUsageStats(): McpServerUsageStats {
|
||||
return parseMcpServerUsageStats(config().mcpServerUsageStats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get usage count for a specific server
|
||||
*/
|
||||
getServerUsageCount(serverId: string): number {
|
||||
const stats = this.getUsageStats();
|
||||
return stats[serverId] || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment usage count for a server
|
||||
*/
|
||||
incrementServerUsage(serverId: string): void {
|
||||
const stats = this.getUsageStats();
|
||||
stats[serverId] = (stats[serverId] || 0) + 1;
|
||||
settingsStore.updateConfig('mcpServerUsageStats', JSON.stringify(stats));
|
||||
}
|
||||
}
|
||||
|
||||
export const mcpStore = new MCPStore();
|
||||
|
|
@ -244,6 +302,7 @@ export function mcpToolCount() {
|
|||
return mcpStore.toolCount;
|
||||
}
|
||||
|
||||
// State access functions (getters only - use mcpStore.method() for actions)
|
||||
export function mcpGetHealthCheckState(serverId: string) {
|
||||
return mcpStore.getHealthCheckState(serverId);
|
||||
}
|
||||
|
|
@ -252,10 +311,25 @@ export function mcpHasHealthCheck(serverId: string) {
|
|||
return mcpStore.hasHealthCheck(serverId);
|
||||
}
|
||||
|
||||
export async function mcpRunHealthCheck(server: HealthCheckParams) {
|
||||
return mcpStore.runHealthCheck(server);
|
||||
export function mcpGetServers() {
|
||||
return mcpStore.getServers();
|
||||
}
|
||||
|
||||
export function mcpClearHealthCheck(serverId: string) {
|
||||
return mcpStore.clearHealthCheck(serverId);
|
||||
export function mcpIsServerEnabled(
|
||||
server: MCPServerSettingsEntry,
|
||||
perChatOverrides?: McpServerOverride[]
|
||||
) {
|
||||
return mcpStore.isServerEnabled(server, perChatOverrides);
|
||||
}
|
||||
|
||||
export function mcpHasEnabledServers(perChatOverrides?: McpServerOverride[]) {
|
||||
return mcpStore.hasEnabledServers(perChatOverrides);
|
||||
}
|
||||
|
||||
export function mcpGetUsageStats() {
|
||||
return mcpStore.getUsageStats();
|
||||
}
|
||||
|
||||
export function mcpGetServerUsageCount(serverId: string) {
|
||||
return mcpStore.getServerUsageCount(serverId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ import type {
|
|||
MCPTransportType,
|
||||
MCPClientConfig,
|
||||
MCPServerConfig,
|
||||
MCPServerSettingsEntry,
|
||||
McpServerUsageStats
|
||||
MCPServerSettingsEntry
|
||||
} from '$lib/types/mcp';
|
||||
import type { SettingsConfigType } from '$lib/types/settings';
|
||||
import type { McpServerOverride } from '$lib/types/database';
|
||||
|
|
@ -21,6 +20,7 @@ export type HeaderPair = { key: string; value: string };
|
|||
*/
|
||||
export function detectMcpTransportFromUrl(url: string): MCPTransportType {
|
||||
const normalized = url.trim().toLowerCase();
|
||||
|
||||
return normalized.startsWith('ws://') || normalized.startsWith('wss://')
|
||||
? 'websocket'
|
||||
: 'streamable_http';
|
||||
|
|
@ -34,6 +34,7 @@ export function generateMcpServerId(id: unknown, index: number): string {
|
|||
if (typeof id === 'string' && id.trim()) {
|
||||
return id.trim();
|
||||
}
|
||||
|
||||
return `server-${index + 1}`;
|
||||
}
|
||||
|
||||
|
|
@ -46,12 +47,23 @@ export function extractServerNameFromUrl(url: string): string {
|
|||
const parsedUrl = new URL(url);
|
||||
const host = parsedUrl.hostname.replace(/^(www\.|mcp\.)/, '');
|
||||
const name = host.split('.')[0] || 'Unknown';
|
||||
|
||||
return name.charAt(0).toUpperCase() + name.slice(1);
|
||||
} catch {
|
||||
return 'New Server';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a display name for an MCP server.
|
||||
* Returns server.name if set, otherwise extracts name from URL.
|
||||
*/
|
||||
export function getServerDisplayName(server: MCPServerSettingsEntry): string {
|
||||
if (server.name) return server.name;
|
||||
|
||||
return extractServerNameFromUrl(server.url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a favicon URL for an MCP server using Google's favicon service.
|
||||
* Returns null if the URL is invalid.
|
||||
|
|
@ -74,6 +86,7 @@ export function getFaviconUrl(serverUrl: string): string | null {
|
|||
*/
|
||||
export function parseHeadersToArray(headersJson: string): HeaderPair[] {
|
||||
if (!headersJson?.trim()) return [];
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(headersJson);
|
||||
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||
|
|
@ -85,6 +98,7 @@ export function parseHeadersToArray(headersJson: string): HeaderPair[] {
|
|||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -94,11 +108,15 @@ export function parseHeadersToArray(headersJson: string): HeaderPair[] {
|
|||
*/
|
||||
export function serializeHeaders(pairs: HeaderPair[]): string {
|
||||
const validPairs = pairs.filter((p) => p.key.trim());
|
||||
|
||||
if (validPairs.length === 0) return '';
|
||||
|
||||
const obj: Record<string, string> = {};
|
||||
|
||||
for (const pair of validPairs) {
|
||||
obj[pair.key.trim()] = pair.value;
|
||||
}
|
||||
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
|
||||
|
|
@ -186,7 +204,8 @@ function buildServerConfig(
|
|||
}
|
||||
|
||||
/**
|
||||
* TODO - move stateful logic to store
|
||||
* Checks if a server is enabled considering per-chat overrides.
|
||||
* Per-chat override takes precedence over global setting.
|
||||
*/
|
||||
function isServerEnabled(
|
||||
server: MCPServerSettingsEntry,
|
||||
|
|
@ -198,6 +217,7 @@ function isServerEnabled(
|
|||
return override.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
return server.enabled;
|
||||
}
|
||||
|
||||
|
|
@ -241,7 +261,9 @@ export function buildMcpClientConfig(
|
|||
}
|
||||
|
||||
/**
|
||||
* TODO - move stateful logic to store
|
||||
* Checks if there are any enabled MCP servers in the configuration.
|
||||
* @param config - Global settings configuration
|
||||
* @param perChatOverrides - Optional per-chat server overrides
|
||||
*/
|
||||
export function hasEnabledMcpServers(
|
||||
config: SettingsConfigType,
|
||||
|
|
@ -249,51 +271,3 @@ export function hasEnabledMcpServers(
|
|||
): boolean {
|
||||
return Boolean(buildMcpClientConfig(config, perChatOverrides));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses MCP server usage stats from settings.
|
||||
* @param rawStats - The raw stats to parse
|
||||
* @returns MCP server usage stats or empty object if invalid
|
||||
*/
|
||||
export function parseMcpServerUsageStats(rawStats: unknown): McpServerUsageStats {
|
||||
if (!rawStats) return {};
|
||||
|
||||
if (typeof rawStats === 'string') {
|
||||
const trimmed = rawStats.trim();
|
||||
if (!trimmed) return {};
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||
return parsed as McpServerUsageStats;
|
||||
}
|
||||
} catch {
|
||||
console.warn('[MCP] Failed to parse mcpServerUsageStats JSON, ignoring value');
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets usage count for a specific server.
|
||||
* @param config - Global settings configuration
|
||||
* @param serverId - The server ID to get the usage count for
|
||||
* @returns The usage count for the server
|
||||
*/
|
||||
export function getMcpServerUsageCount(config: SettingsConfigType, serverId: string): number {
|
||||
const stats = parseMcpServerUsageStats(config.mcpServerUsageStats);
|
||||
return stats[serverId] || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments usage count for a server and returns updated stats JSON.
|
||||
* @param config - Global settings configuration
|
||||
* @param serverId - The server ID to increment the usage count for
|
||||
* @returns The updated stats JSON
|
||||
*/
|
||||
export function incrementMcpServerUsage(config: SettingsConfigType, serverId: string): string {
|
||||
const stats = parseMcpServerUsageStats(config.mcpServerUsageStats);
|
||||
stats[serverId] = (stats[serverId] || 0) + 1;
|
||||
return JSON.stringify(stats);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue