chat-ui / src /lib /stores /mcpServers.ts
victor's picture
victor HF Staff
localstorage: add :mcp:
2daa11b
raw
history blame
7.2 kB
/**
* MCP Servers Store
* Manages base (env-configured) and custom (user-added) MCP servers
* Stores custom servers and selection state in browser localStorage
*/
import { writable, derived } 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";
// Namespace storage by app identity to avoid collisions across apps
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 : "");
// Final prefix format requested: "huggingchat:key" (no mcp:/chat)
const KEY_PREFIX = appLabel || baseLabel || "app";
const STORAGE_KEYS = {
CUSTOM_SERVERS: `${KEY_PREFIX}:mcp:custom-servers`,
SELECTED_IDS: `${KEY_PREFIX}:mcp:selected-ids`,
} as const;
// No migration needed per request — read/write only namespaced keys
// Load custom servers from localStorage
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 [];
}
}
// Load selected server IDs from localStorage
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();
}
}
// Save custom servers to localStorage
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);
}
}
// Save selected IDs to localStorage
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);
}
}
// Store for all servers (base + custom)
export const allMcpServers = writable<MCPServer[]>([]);
// Store for selected server IDs
export const selectedServerIds = writable<Set<string>>(loadSelectedIds());
// Auto-persist selected IDs when they change
if (browser) {
selectedServerIds.subscribe((ids) => {
saveSelectedIds(ids);
});
}
// Derived store: only enabled servers
export const enabledServers = derived([allMcpServers, selectedServerIds], ([$all, $selected]) =>
$all.filter((s) => $selected.has(s.id))
);
// Derived store: count of enabled servers
export const enabledServersCount = derived(enabledServers, ($enabled) => $enabled.length);
// Note: Authorization overlay (with user's HF token) for the Hugging Face MCP host
// is applied server-side when enabled via MCP_FORWARD_HF_USER_TOKEN.
/**
* Refresh base servers from API and merge with custom servers
*/
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();
// Merge base and custom servers
const merged = [...baseServers, ...customServers];
allMcpServers.set(merged);
// Prune selected IDs that no longer correspond to existing servers
const validIds = new Set(merged.map((s) => s.id));
selectedServerIds.update(($ids) => {
const filtered = new Set([...$ids].filter((id) => validIds.has(id)));
return filtered;
});
} catch (error) {
console.error("Failed to refresh MCP servers:", error);
// On error, just use custom servers
allMcpServers.set(loadCustomServers());
}
}
/**
* Toggle a server on/off
*/
export function toggleServer(id: string) {
selectedServerIds.update(($ids) => {
const newSet = new Set($ids);
if (newSet.has(id)) {
newSet.delete(id);
} else {
newSet.add(id);
}
return newSet;
});
}
/**
* Add a custom MCP server
*/
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);
// Refresh all servers to include the new one
refreshMcpServers();
return newServer.id;
}
/**
* Update an existing custom server
*/
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();
}
}
/**
* Delete a custom server
*/
export function deleteCustomServer(id: string) {
const customServers = loadCustomServers();
const filtered = customServers.filter((s) => s.id !== id);
saveCustomServers(filtered);
// Also remove from selected IDs
selectedServerIds.update(($ids) => {
const newSet = new Set($ids);
newSet.delete(id);
return newSet;
});
refreshMcpServers();
}
/**
* Update server status (from health check)
*/
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
)
);
}
/**
* Run health check on a server
*/
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 };
}
}
// Initialize on module load
if (browser) {
refreshMcpServers();
}