| |
| |
| |
| |
| |
|
|
| import { writable, derived, get } from "svelte/store"; |
| import { base } from "$app/paths"; |
| import { env as publicEnv } from "$env/dynamic/public"; |
| import { browser } from "$app/environment"; |
| import type { MCPServer, ServerStatus, MCPTool } from "$lib/types/Tool"; |
|
|
| |
| function toKeyPart(s: string | undefined): string { |
| return (s || "").toLowerCase().replace(/[^a-z0-9_-]+/g, "-"); |
| } |
|
|
| const appLabel = toKeyPart(publicEnv.PUBLIC_APP_ASSETS || publicEnv.PUBLIC_APP_NAME); |
| const baseLabel = toKeyPart(typeof base === "string" ? base : ""); |
| |
| const KEY_PREFIX = appLabel || baseLabel || "app"; |
|
|
| const STORAGE_KEYS = { |
| CUSTOM_SERVERS: `${KEY_PREFIX}:mcp:custom-servers`, |
| SELECTED_IDS: `${KEY_PREFIX}:mcp:selected-ids`, |
| DISABLED_BASE_IDS: `${KEY_PREFIX}:mcp:disabled-base-ids`, |
| } as const; |
|
|
| |
|
|
| |
| function loadCustomServers(): MCPServer[] { |
| if (!browser) return []; |
|
|
| try { |
| const json = localStorage.getItem(STORAGE_KEYS.CUSTOM_SERVERS); |
| return json ? JSON.parse(json) : []; |
| } catch (error) { |
| console.error("Failed to load custom MCP servers from localStorage:", error); |
| return []; |
| } |
| } |
|
|
| |
| function loadSelectedIds(): Set<string> { |
| if (!browser) return new Set(); |
|
|
| try { |
| const json = localStorage.getItem(STORAGE_KEYS.SELECTED_IDS); |
| const ids: string[] = json ? JSON.parse(json) : []; |
| return new Set(ids); |
| } catch (error) { |
| console.error("Failed to load selected MCP server IDs from localStorage:", error); |
| return new Set(); |
| } |
| } |
|
|
| |
| function saveCustomServers(servers: MCPServer[]) { |
| if (!browser) return; |
|
|
| try { |
| localStorage.setItem(STORAGE_KEYS.CUSTOM_SERVERS, JSON.stringify(servers)); |
| } catch (error) { |
| console.error("Failed to save custom MCP servers to localStorage:", error); |
| } |
| } |
|
|
| |
| function saveSelectedIds(ids: Set<string>) { |
| if (!browser) return; |
|
|
| try { |
| localStorage.setItem(STORAGE_KEYS.SELECTED_IDS, JSON.stringify([...ids])); |
| } catch (error) { |
| console.error("Failed to save selected MCP server IDs to localStorage:", error); |
| } |
| } |
|
|
| |
| function loadDisabledBaseIds(): Set<string> { |
| if (!browser) return new Set(); |
|
|
| try { |
| const json = localStorage.getItem(STORAGE_KEYS.DISABLED_BASE_IDS); |
| return new Set(json ? JSON.parse(json) : []); |
| } catch (error) { |
| console.error("Failed to load disabled base MCP server IDs from localStorage:", error); |
| return new Set(); |
| } |
| } |
|
|
| |
| function saveDisabledBaseIds(ids: Set<string>) { |
| if (!browser) return; |
|
|
| try { |
| localStorage.setItem(STORAGE_KEYS.DISABLED_BASE_IDS, JSON.stringify([...ids])); |
| } catch (error) { |
| console.error("Failed to save disabled base MCP server IDs to localStorage:", error); |
| } |
| } |
|
|
| |
| export const allMcpServers = writable<MCPServer[]>([]); |
|
|
| |
| export const mcpServersLoaded = writable<boolean>(false); |
|
|
| |
| export const selectedServerIds = writable<Set<string>>(loadSelectedIds()); |
|
|
| |
| if (browser) { |
| selectedServerIds.subscribe((ids) => { |
| saveSelectedIds(ids); |
| }); |
| } |
|
|
| |
| export const enabledServers = derived([allMcpServers, selectedServerIds], ([$all, $selected]) => |
| $all.filter((s) => $selected.has(s.id)) |
| ); |
|
|
| |
| export const enabledServersCount = derived(enabledServers, ($enabled) => $enabled.length); |
|
|
| |
| export const allBaseServersEnabled = derived( |
| [allMcpServers, selectedServerIds], |
| ([$all, $selected]) => { |
| const baseServers = $all.filter((s) => s.type === "base"); |
| return baseServers.length > 0 && baseServers.every((s) => $selected.has(s.id)); |
| } |
| ); |
|
|
| |
| |
|
|
| |
| |
| |
| export async function refreshMcpServers() { |
| try { |
| const response = await fetch(`${base}/api/mcp/servers`); |
| if (!response.ok) { |
| throw new Error(`Failed to fetch base servers: ${response.statusText}`); |
| } |
|
|
| const baseServers: MCPServer[] = await response.json(); |
| const customServers = loadCustomServers(); |
|
|
| |
| const merged = [...baseServers, ...customServers]; |
| allMcpServers.set(merged); |
|
|
| |
| const disabledBaseIds = loadDisabledBaseIds(); |
|
|
| |
| |
| const validIds = new Set(merged.map((s) => s.id)); |
| selectedServerIds.update(($currentIds) => { |
| const newSelection = new Set<string>(); |
|
|
| |
| for (const server of baseServers) { |
| if (!disabledBaseIds.has(server.id)) { |
| newSelection.add(server.id); |
| } |
| } |
|
|
| |
| for (const id of $currentIds) { |
| if (validIds.has(id) && !id.startsWith("base-")) { |
| newSelection.add(id); |
| } |
| } |
|
|
| return newSelection; |
| }); |
| mcpServersLoaded.set(true); |
| } catch (error) { |
| console.error("Failed to refresh MCP servers:", error); |
| |
| allMcpServers.set(loadCustomServers()); |
| mcpServersLoaded.set(true); |
| } |
| } |
|
|
| |
| |
| |
| export function toggleServer(id: string) { |
| selectedServerIds.update(($ids) => { |
| const newSet = new Set($ids); |
| if (newSet.has(id)) { |
| newSet.delete(id); |
| |
| if (id.startsWith("base-")) { |
| const disabled = loadDisabledBaseIds(); |
| disabled.add(id); |
| saveDisabledBaseIds(disabled); |
| } |
| } else { |
| newSet.add(id); |
| |
| if (id.startsWith("base-")) { |
| const disabled = loadDisabledBaseIds(); |
| disabled.delete(id); |
| saveDisabledBaseIds(disabled); |
| } |
| } |
| return newSet; |
| }); |
| } |
|
|
| |
| |
| |
| export function disableAllServers() { |
| |
| const servers = get(allMcpServers); |
| const baseServerIds = servers.filter((s) => s.type === "base").map((s) => s.id); |
|
|
| |
| saveDisabledBaseIds(new Set(baseServerIds)); |
|
|
| |
| selectedServerIds.set(new Set()); |
| } |
|
|
| |
| |
| |
| export function addCustomServer(server: Omit<MCPServer, "id" | "type" | "status">): string { |
| const newServer: MCPServer = { |
| ...server, |
| id: crypto.randomUUID(), |
| type: "custom", |
| status: "disconnected", |
| }; |
|
|
| const customServers = loadCustomServers(); |
| customServers.push(newServer); |
| saveCustomServers(customServers); |
|
|
| |
| refreshMcpServers(); |
|
|
| return newServer.id; |
| } |
|
|
| |
| |
| |
| export function updateCustomServer(id: string, updates: Partial<MCPServer>) { |
| const customServers = loadCustomServers(); |
| const index = customServers.findIndex((s) => s.id === id); |
|
|
| if (index !== -1) { |
| customServers[index] = { ...customServers[index], ...updates }; |
| saveCustomServers(customServers); |
| refreshMcpServers(); |
| } |
| } |
|
|
| |
| |
| |
| export function deleteCustomServer(id: string) { |
| const customServers = loadCustomServers(); |
| const filtered = customServers.filter((s) => s.id !== id); |
| saveCustomServers(filtered); |
|
|
| |
| selectedServerIds.update(($ids) => { |
| const newSet = new Set($ids); |
| newSet.delete(id); |
| return newSet; |
| }); |
|
|
| refreshMcpServers(); |
| } |
|
|
| |
| |
| |
| export function updateServerStatus( |
| id: string, |
| status: ServerStatus, |
| errorMessage?: string, |
| tools?: MCPTool[], |
| authRequired?: boolean |
| ) { |
| allMcpServers.update(($servers) => |
| $servers.map((s) => |
| s.id === id |
| ? { |
| ...s, |
| status, |
| errorMessage, |
| tools, |
| authRequired, |
| } |
| : s |
| ) |
| ); |
| } |
|
|
| |
| |
| |
| export async function healthCheckServer( |
| server: MCPServer |
| ): Promise<{ ready: boolean; tools?: MCPTool[]; error?: string }> { |
| try { |
| updateServerStatus(server.id, "connecting"); |
|
|
| const response = await fetch(`${base}/api/mcp/health`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ url: server.url, headers: server.headers }), |
| }); |
|
|
| const result = await response.json(); |
|
|
| if (result.ready && result.tools) { |
| updateServerStatus(server.id, "connected", undefined, result.tools, false); |
| return { ready: true, tools: result.tools }; |
| } else { |
| updateServerStatus(server.id, "error", result.error, undefined, Boolean(result.authRequired)); |
| return { ready: false, error: result.error }; |
| } |
| } catch (error) { |
| const errorMessage = error instanceof Error ? error.message : "Unknown error"; |
| updateServerStatus(server.id, "error", errorMessage); |
| return { ready: false, error: errorMessage }; |
| } |
| } |
|
|
| |
| if (browser) { |
| refreshMcpServers(); |
| } |
|
|