import { readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { Type } from "typebox"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; type McpCallResult = { content?: Array>; structuredContent?: unknown; isError?: boolean; [key: string]: unknown; }; type ExecutorLaunch = { command: string; args: string[]; configPath: string; }; function splitArgs(value: string | undefined): string[] | undefined { if (!value?.trim()) return undefined; return value.trim().split(/\s+/); } function loadExecutorLaunch(): ExecutorLaunch { const configPath = process.env.EXECUTOR_MCP_CONFIG?.trim() || join(homedir(), ".pi/agent/executor-mcp.json"); let command = "executor"; let args = ["mcp"]; try { const raw = JSON.parse(readFileSync(configPath, "utf8")) as { command?: unknown; args?: unknown; }; if (typeof raw.command === "string" && raw.command.trim()) { command = raw.command.trim(); } if (Array.isArray(raw.args) && raw.args.every((item) => typeof item === "string")) { args = raw.args; } } catch { // Config is optional; cloud `executor mcp` remains the default. } const envCommand = process.env.EXECUTOR_BIN?.trim() || process.env.EXECUTOR_COMMAND?.trim(); if (envCommand) command = envCommand; const envArgs = splitArgs(process.env.EXECUTOR_MCP_ARGS); if (envArgs) args = envArgs; return { command, args, configPath }; } const EXECUTOR_EXECUTE_DESCRIPTION = `Execute TypeScript in Executor's MCP runtime with access to configured API tools. Typical workflow inside code: 1. const matches = await tools.search({ query: "", limit: 12 }); 2. const detail = await tools.describe.tool({ path: matches[0].path }); 3. const result = await tools..(input); Useful calls: - await tools.search({ query: "github issues", limit: 5 }) - await tools.executor.sources.list() - await tools.describe.tool({ path: "namespace.resource.method" }) Rules: - Do not use fetch; call APIs through tools.*. - Always include the namespace when calling configured tools. - Return the final value from the TypeScript code.`; function renderMcpResult(result: McpCallResult): string { const parts: string[] = []; for (const block of result.content ?? []) { if (block.type === "text" && typeof block.text === "string") { parts.push(block.text); } else { parts.push(JSON.stringify(block, null, 2)); } } if (parts.length === 0 && result.structuredContent !== undefined) { parts.push(JSON.stringify(result.structuredContent, null, 2)); } if (parts.length === 0) { parts.push(JSON.stringify(result, null, 2)); } return parts.join("\n"); } export default function executorMcpExtension(pi: ExtensionAPI) { let client: Client | undefined; let transport: StdioClientTransport | undefined; let connecting: Promise | undefined; let lastStderr = ""; async function disconnect() { const current = client; client = undefined; transport = undefined; connecting = undefined; if (current) { try { await current.close(); } catch { // Ignore shutdown errors. } } } async function connect(): Promise { if (client) return client; if (connecting) return connecting; connecting = (async () => { const launch = loadExecutorLaunch(); const nextClient = new Client({ name: "pi-executor-mcp", version: "1.0.0" }); const nextTransport = new StdioClientTransport({ command: launch.command, args: launch.args, stderr: "pipe", }); nextTransport.stderr?.on("data", (chunk) => { lastStderr = `${lastStderr}${chunk.toString()}`.slice(-4000); }); nextTransport.onerror = (error) => { lastStderr = `${lastStderr}\n${error.message}`.slice(-4000); }; nextTransport.onclose = () => { client = undefined; transport = undefined; connecting = undefined; }; await nextClient.connect(nextTransport); client = nextClient; transport = nextTransport; return nextClient; })(); try { return await connecting; } catch (error) { connecting = undefined; throw error; } } async function callExecutorTool(name: string, args: Record, signal?: AbortSignal) { const c = await connect(); const result = (await c.callTool( { name, arguments: args }, undefined, { signal } as never, )) as McpCallResult; const text = renderMcpResult(result); if (result.isError) { throw new Error(text); } return { content: [{ type: "text" as const, text }], details: result, }; } function executeCode(code: string, signal?: AbortSignal) { return callExecutorTool("execute", { code }, signal); } function asJson(value: unknown) { return JSON.stringify(value).replaceAll("({ type: "string", enum: ["accept", "decline", "cancel"], description: "How to respond to the paused Executor interaction.", }), content: Type.Optional( Type.String({ default: "{}", description: "Optional JSON-encoded response content for form elicitations.", }), ), }), async execute(_toolCallId, params, signal) { return callExecutorTool( "resume", { executionId: params.executionId, action: params.action, content: params.content ?? "{}", }, signal, ); }, }); pi.registerCommand("executor-search", { description: "Search Executor tools by natural-language query", handler: async (args, ctx) => { const query = args.trim(); if (!query) { ctx.ui.notify('Usage: /executor-search e.g. /executor-search github issues', "warning"); return; } try { const result = await executeCode( `return await tools.search({ query: ${asJson(query)}, limit: 10 });`, ); ctx.ui.notify(`Executor search results for "${query}":\n${result.content[0].text}`, "info"); } catch (error) { const message = error instanceof Error ? error.message : String(error); ctx.ui.notify(`Executor search failed: ${message}`, "error"); } }, }); pi.registerCommand("executor-describe", { description: "Describe an Executor tool by path", handler: async (args, ctx) => { const path = args.trim(); if (!path) { ctx.ui.notify('Usage: /executor-describe e.g. /executor-describe github.issues.create', "warning"); return; } try { const result = await executeCode( `return await tools.describe.tool({ path: ${asJson(path)} });`, ); ctx.ui.notify(`Executor tool ${path}:\n${result.content[0].text}`, "info"); } catch (error) { const message = error instanceof Error ? error.message : String(error); ctx.ui.notify(`Executor describe failed: ${message}`, "error"); } }, }); pi.registerCommand("executor-status", { description: "Check the Executor MCP connection and configured source list", handler: async (_args, ctx) => { try { const result = await executeCode("return await tools.executor.sources.list();"); ctx.ui.notify(`Executor MCP connected:\n${result.content[0].text}`, "info"); } catch (error) { const message = error instanceof Error ? error.message : String(error); ctx.ui.notify( `Executor MCP failed: ${message}${lastStderr ? `\n\nRecent stderr:\n${lastStderr}` : ""}`, "error", ); } }, }); pi.registerCommand("executor-restart", { description: "Restart the Executor MCP child process used by pi", handler: async (_args, ctx) => { await disconnect(); lastStderr = ""; try { await connect(); ctx.ui.notify("Executor MCP restarted.", "info"); } catch (error) { const message = error instanceof Error ? error.message : String(error); ctx.ui.notify(`Executor MCP restart failed: ${message}`, "error"); } }, }); pi.on("session_start", async (_event, ctx) => { // Connect lazily in non-interactive modes; in the TUI, give immediate feedback if it is broken. if (!ctx.hasUI) return; try { await connect(); ctx.ui.setStatus("executor-mcp", "executor:mcp"); } catch { ctx.ui.setStatus("executor-mcp", "executor:mcp offline"); } }); pi.on("session_shutdown", async () => { await disconnect(); }); }