/** * MCP tool cache. * * Stores tool definitions per server in agent.db for fast startup. */ import { isRecord, logger } from "@oh-my-pi/pi-utils"; import type { AgentStorage } from "../session/agent-storage"; import type { MCPServerConfig, MCPToolDefinition } from "./types"; const CACHE_VERSION = 1; const CACHE_PREFIX = "mcp_tools:"; const CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000; type MCPToolCachePayload = { version: number; configHash: string; tools: MCPToolDefinition[]; }; function stableClone(value: unknown): unknown { if (Array.isArray(value)) { return value.map(item => stableClone(item)); } if (isRecord(value)) { const sorted: Record = {}; for (const key of Object.keys(value).sort()) { sorted[key] = stableClone(value[key]); } return sorted; } return value; } function stableStringify(value: unknown): string { return JSON.stringify(stableClone(value)); } function toHex(buffer: ArrayBuffer): string { const bytes = new Uint8Array(buffer); let output = ""; for (const byte of bytes) { output += byte.toString(16).padStart(2, "0"); } return output; } async function hashConfig(config: MCPServerConfig): Promise { const stable = stableStringify(config); const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(stable)); return toHex(digest); } function cacheKey(serverName: string): string { return `${CACHE_PREFIX}${serverName}`; } export class MCPToolCache { constructor(private storage: AgentStorage) {} async get(serverName: string, config: MCPServerConfig): Promise { const key = cacheKey(serverName); const raw = this.storage.getCache(key); if (!raw) return null; let parsed: unknown; try { parsed = JSON.parse(raw); } catch (error) { logger.warn("MCP tool cache parse failed", { serverName, error: String(error) }); return null; } if (!isRecord(parsed)) return null; if (parsed.version !== CACHE_VERSION) return null; if (typeof parsed.configHash !== "string") return null; if (!Array.isArray(parsed.tools)) return null; let currentHash: string; try { currentHash = await hashConfig(config); } catch (error) { logger.warn("MCP tool cache hash failed", { serverName, error: String(error) }); return null; } if (parsed.configHash !== currentHash) return null; return parsed.tools as MCPToolDefinition[]; } async set(serverName: string, config: MCPServerConfig, tools: MCPToolDefinition[]): Promise { let configHash: string; try { configHash = await hashConfig(config); } catch (error) { logger.warn("MCP tool cache hash failed", { serverName, error: String(error) }); return; } const payload: MCPToolCachePayload = { version: CACHE_VERSION, configHash, tools, }; let serialized: string; try { serialized = JSON.stringify(payload); } catch (error) { logger.warn("MCP tool cache serialize failed", { serverName, error: String(error) }); return; } const expiresAtSec = Math.floor((Date.now() + CACHE_TTL_MS) / 1000); this.storage.setCache(cacheKey(serverName), serialized, expiresAtSec); } }