7b5af2dd9f
- converted relative imports to path aliases ($c/*, $ai/*, $tui/*, etc.) across all packages - added per-package tsconfig.json with complete path mappings for runtime resolution - set importModuleSpecifier to non-relative for IDE auto-import preferences - updated dev script to run from monorepo root for consistent path resolution
123 lines
3.2 KiB
TypeScript
123 lines
3.2 KiB
TypeScript
/**
|
|
* MCP tool cache.
|
|
*
|
|
* Stores tool definitions per server in agent.db for fast startup.
|
|
*/
|
|
|
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
import type { AgentStorage } from "$c/core/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 isRecord(value: unknown): value is Record<string, unknown> {
|
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
}
|
|
|
|
function stableClone(value: unknown): unknown {
|
|
if (Array.isArray(value)) {
|
|
return value.map((item) => stableClone(item));
|
|
}
|
|
if (isRecord(value)) {
|
|
const sorted: Record<string, unknown> = {};
|
|
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<string> {
|
|
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<MCPToolDefinition[] | null> {
|
|
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<void> {
|
|
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);
|
|
}
|
|
}
|