import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { formatContent } from "./serenaUtils"; import type { SerenaServerManager } from "./serenaServer"; export type SerenaClientManager = { getClient: () => Promise; resetClient: () => Promise; setProjectRoot: (root: string) => Promise; callSerena: (toolName: string, args: Record, timeoutMs: number) => Promise; }; export const createSerenaClientManager = ( serverManager: SerenaServerManager, onProjectRootChange: (root: string) => Promise, ): SerenaClientManager => { let clientPromise: Promise | null = null; let activeClient: Client | null = null; let projectRoot = serverManager.getProjectRoot(); const getClient = async () => { if (clientPromise) return clientPromise; clientPromise = (async () => { await serverManager.ensureServerRunning(); const transport = new StreamableHTTPClientTransport(serverManager.getSerenaUrl(), { headers: {} }); const client = new Client( { name: "pi-serena", version: "1.0.0" }, { capabilities: { tools: {} } }, ); await client.connect(transport); activeClient = client; return client; })(); return clientPromise; }; const resetClient = async () => { if (!activeClient) return; try { await activeClient.close(); } catch { // ignore cleanup errors } activeClient = null; clientPromise = null; }; const setProjectRoot = async (root: string) => { const nextRoot = root || process.cwd(); if (nextRoot === projectRoot) return; projectRoot = nextRoot; await resetClient(); await onProjectRootChange(nextRoot); }; const callSerena = async (toolName: string, args: Record, timeoutMs: number) => { const client = await getClient(); return new Promise((resolve, reject) => { const timer = setTimeout(() => { resetClient(); reject(new Error(`Serena MCP request timed out after ${timeoutMs}ms`)); }, timeoutMs); client .callTool({ name: toolName, arguments: args }) .then((response) => { clearTimeout(timer); resolve(formatContent(response.content)); }) .catch((error) => { clearTimeout(timer); reject(error); }); }); }; return { getClient, resetClient, setProjectRoot, callSerena, }; };