/** * Serena MCP Extension * * Exposes Serena MCP tools as native Pi tools via Streamable HTTP. */ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { createSerenaClientManager } from "./serenaClient"; import { createSerenaServerManager } from "./serenaServer"; import { createSerenaSettingsStore } from "./serenaSettings"; import { createWithCommonHandling, wrapResult } from "./serenaResponses"; import { registerSerenaTools } from "./serenaTools"; const DEFAULT_TIMEOUT_MS = 20_000; const DEFAULT_SERENA_PORT = 9121; const DEFAULT_SERENA_HOST = "localhost"; const SERENA_CONTEXT = "agent"; const SERENA_STARTUP_TIMEOUT_MS = 15_000; const SERENA_POLL_INTERVAL_MS = 500; export default function (pi: ExtensionAPI) { let projectRoot = process.cwd(); const defaultBlockedToolNames = ["read", "write", "edit", "ls", "find", "grep"]; let blockedToolNames = [...defaultBlockedToolNames]; const serverManager = createSerenaServerManager({ defaultHost: DEFAULT_SERENA_HOST, defaultPort: DEFAULT_SERENA_PORT, context: SERENA_CONTEXT, startupTimeoutMs: SERENA_STARTUP_TIMEOUT_MS, pollIntervalMs: SERENA_POLL_INTERVAL_MS, projectRoot: () => projectRoot, getPortOverride: () => Number(process.env.SERENA_MCP_PORT ?? "0"), }); const settingsStore = createSerenaSettingsStore( () => projectRoot, defaultBlockedToolNames, ); const clientManager = createSerenaClientManager(serverManager, async (root) => { projectRoot = root; await serverManager.stopServer(); }); const withCommonHandling = createWithCommonHandling({ callSerena: clientManager.callSerena, resetClient: clientManager.resetClient, defaultTimeoutMs: DEFAULT_TIMEOUT_MS, }); registerSerenaTools({ pi, withCommonHandling, getClient: clientManager.getClient, resetClient: clientManager.resetClient, wrapResult, defaultTimeoutMs: DEFAULT_TIMEOUT_MS, }); const handleToolCall = async (event: { toolName: string }) => { if (blockedToolNames.includes(event.toolName)) { return { block: true, reason: `Tool '${event.toolName}' is disabled. Use Serena tools instead.`, }; } }; const handleToolBlockerCommand = async (_args: unknown, ctx: any) => { await clientManager.setProjectRoot(ctx.cwd ?? process.cwd()); blockedToolNames = await settingsStore.loadBlockedTools(); const allTools = pi .getAllTools() .map((tool) => tool.name) .sort((a, b) => a.localeCompare(b)); const blocked = new Set(blockedToolNames); while (ctx.hasUI) { const options = allTools.map((name) => `${blocked.has(name) ? "✓" : " "} ${name}`); options.push("Done"); const choice = await ctx.ui.select("Toggle blocked tools", options); if (!choice || choice === "Done") break; const name = choice.slice(2); if (blocked.has(name)) { blocked.delete(name); } else { blocked.add(name); } } blockedToolNames = Array.from(blocked).sort((a, b) => a.localeCompare(b)); await settingsStore.saveBlockedTools(blockedToolNames); if (ctx.hasUI) { ctx.ui.notify("Serena tool blacklist updated.", "info"); } }; const handleSessionStart = async (_event: unknown, ctx: any) => { await clientManager.setProjectRoot(ctx.cwd ?? process.cwd()); blockedToolNames = await settingsStore.loadBlockedTools(); try { const client = await clientManager.getClient(); const tools = await client.listTools(); const toolNames = new Set(tools.tools.map((tool) => tool.name)); if (toolNames.has("activate_project")) { await clientManager.callSerena("activate_project", { project: projectRoot }, DEFAULT_TIMEOUT_MS); } if (toolNames.has("check_onboarding_performed")) { await clientManager.callSerena("check_onboarding_performed", {}, DEFAULT_TIMEOUT_MS); } } catch { await clientManager.resetClient(); } }; const handleSessionShutdown = async () => { await clientManager.resetClient(); await serverManager.stopServer(); }; pi.on("tool_call", handleToolCall); pi.registerCommand("serena-tool-blocker", { description: "Configure the Serena tool blacklist", handler: handleToolBlockerCommand, }); pi.on("session_start", handleSessionStart); pi.on("session_shutdown", handleSessionShutdown); }