llama.cpp/tools/server/webui/src/lib/stores/mcp.svelte.ts

268 lines
7.2 KiB
TypeScript

/**
* mcpStore - Reactive State Store for MCP (Model Context Protocol)
*
* This store contains ONLY reactive state ($state, $derived).
* All business logic is delegated to MCPClient.
*
* **Architecture & Relationships:**
* - **MCPClient**: Business logic facade (lifecycle, tool execution, health checks)
* - **MCPService**: Stateless protocol layer (transport, connect, callTool)
* - **mcpStore** (this): Reactive state for UI components
*
* **Responsibilities:**
* - Hold reactive state for UI binding
* - Provide getters for computed values
* - Expose setters for MCPClient to update state
* - Forward method calls to MCPClient
*
* @see MCPClient in clients/mcp/ for business logic
* @see MCPService in services/mcp.ts for protocol operations
*/
import { browser } from '$app/environment';
import { mcpClient, type HealthCheckState } from '$lib/clients';
import type { MCPServerSettingsEntry, McpServerUsageStats } from '$lib/types/mcp';
import type { McpServerOverride } from '$lib/types/database';
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 };
class MCPStore {
private _isInitializing = $state(false);
private _error = $state<string | null>(null);
private _toolCount = $state(0);
private _connectedServers = $state<string[]>([]);
private _healthChecks = $state<Record<string, HealthCheckState>>({});
constructor() {
if (browser) {
mcpClient.setStateChangeCallback((state) => {
if (state.isInitializing !== undefined) {
this._isInitializing = state.isInitializing;
}
if (state.error !== undefined) {
this._error = state.error;
}
if (state.toolCount !== undefined) {
this._toolCount = state.toolCount;
}
if (state.connectedServers !== undefined) {
this._connectedServers = state.connectedServers;
}
});
mcpClient.setHealthCheckCallback((serverId, state) => {
this._healthChecks = { ...this._healthChecks, [serverId]: state };
});
mcpClient.setServerUsageCallback((serverId) => {
this.incrementServerUsage(serverId);
});
}
}
get isInitializing(): boolean {
return this._isInitializing;
}
get isInitialized(): boolean {
return mcpClient.isInitialized;
}
get error(): string | null {
return this._error;
}
get toolCount(): number {
return this._toolCount;
}
get connectedServerCount(): number {
return this._connectedServers.length;
}
get connectedServerNames(): string[] {
return this._connectedServers;
}
/**
* Check if MCP is enabled (has configured servers)
*/
get isEnabled(): boolean {
const mcpConfig = buildMcpClientConfig(config());
return (
mcpConfig !== null && mcpConfig !== undefined && Object.keys(mcpConfig.servers).length > 0
);
}
/**
* Get list of available tool names
*/
get availableTools(): string[] {
return mcpClient.getToolNames();
}
/**
* Get health check state for a specific server
*/
getHealthCheckState(serverId: string): HealthCheckState {
return this._healthChecks[serverId] ?? { status: 'idle' };
}
/**
* Check if health check has been performed for a server
*/
hasHealthCheck(serverId: string): boolean {
return serverId in this._healthChecks && this._healthChecks[serverId].status !== 'idle';
}
/**
* Clear health check state for a specific server
*/
clearHealthCheck(serverId: string): void {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [serverId]: _removed, ...rest } = this._healthChecks;
this._healthChecks = rest;
}
/**
* Clear all health check states
*/
clearAllHealthChecks(): void {
this._healthChecks = {};
}
/**
* Clear error state
*/
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 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();
export const mcpIsInitializing = () => mcpStore.isInitializing;
export const mcpIsInitialized = () => mcpStore.isInitialized;
export const mcpError = () => mcpStore.error;
export const mcpIsEnabled = () => mcpStore.isEnabled;
export const mcpAvailableTools = () => mcpStore.availableTools;
export const mcpConnectedServerCount = () => mcpStore.connectedServerCount;
export const mcpConnectedServerNames = () => mcpStore.connectedServerNames;
export const mcpToolCount = () => mcpStore.toolCount;