From 84e43f656ce662624be835dd65776ae26a353075 Mon Sep 17 00:00:00 2001 From: Amrit Subramanian Date: Tue, 7 Oct 2025 19:37:08 -0400 Subject: [PATCH] Run MCP add/remove/get commands in poller PTY instead of execAsync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixes issue where execAsync spawns non-login shell without claude in PATH - All MCP operations now run in the session's MCP poller PTY - Adds executeInMcpPoller helper to run commands and wait for completion - Uses prompt detection to know when command finishes - 10 second timeout for safety 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- main.ts | 60 ++++++++++++++++++++++++++++++++++++++++------------- renderer.ts | 8 +++---- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/main.ts b/main.ts index 84405a8..03a4fce 100644 --- a/main.ts +++ b/main.ts @@ -631,23 +631,55 @@ async function listMcpServers() { } } -async function addMcpServer(name: string, config: any) { +// Helper to execute command in MCP poller PTY and wait for completion +function executeInMcpPoller(sessionId: string, command: string): Promise { + return new Promise((resolve, reject) => { + const mcpPoller = mcpPollerPtyProcesses.get(sessionId); + if (!mcpPoller) { + reject(new Error("No active MCP poller for this session")); + return; + } + + let output = ""; + const timeout = setTimeout(() => { + dataHandler && mcpPoller.off("data", dataHandler); + reject(new Error("Command timeout")); + }, 10000); + + const dataHandler = (data: string) => { + output += data; + // Check if command completed (prompt returned) + if (data.includes("% ") || data.includes("$ ") || data.includes("➜ ")) { + clearTimeout(timeout); + mcpPoller.off("data", dataHandler); + resolve(output); + } + }; + + mcpPoller.on("data", dataHandler); + mcpPoller.write(command + "\r"); + }); +} + +async function addMcpServer(sessionId: string, name: string, config: any) { // Use add-json to support full configuration including env vars, headers, etc. - const jsonConfig = JSON.stringify(config); - await execAsync(`claude mcp add-json --scope user "${name}" '${jsonConfig}'`); + const jsonConfig = JSON.stringify(config).replace(/'/g, "'\\''"); // Escape single quotes for shell + const command = `claude mcp add-json --scope user "${name}" '${jsonConfig}'`; + await executeInMcpPoller(sessionId, command); } -async function removeMcpServer(name: string) { - await execAsync(`claude mcp remove "${name}"`); +async function removeMcpServer(sessionId: string, name: string) { + const command = `claude mcp remove "${name}"`; + await executeInMcpPoller(sessionId, command); } -async function getMcpServerDetails(name: string) { +async function getMcpServerDetails(sessionId: string, name: string) { try { - const { stdout } = await execAsync(`claude mcp get "${name}"`); + const output = await executeInMcpPoller(sessionId, `claude mcp get "${name}"`); // Parse the output to extract details const details: any = { name }; - const lines = stdout.split("\n"); + const lines = output.split("\n"); for (const line of lines) { const trimmed = line.trim(); @@ -688,27 +720,27 @@ ipcMain.handle("list-mcp-servers", async (_event, sessionId: string) => { } }); -ipcMain.handle("add-mcp-server", async (_event, name: string, config: any) => { +ipcMain.handle("add-mcp-server", async (_event, sessionId: string, name: string, config: any) => { try { - await addMcpServer(name, config); + await addMcpServer(sessionId, name, config); } catch (error) { console.error("Error adding MCP server:", error); throw error; } }); -ipcMain.handle("remove-mcp-server", async (_event, name: string) => { +ipcMain.handle("remove-mcp-server", async (_event, sessionId: string, name: string) => { try { - await removeMcpServer(name); + await removeMcpServer(sessionId, name); } catch (error) { console.error("Error removing MCP server:", error); throw error; } }); -ipcMain.handle("get-mcp-server-details", async (_event, name: string) => { +ipcMain.handle("get-mcp-server-details", async (_event, sessionId: string, name: string) => { try { - return await getMcpServerDetails(name); + return await getMcpServerDetails(sessionId, name); } catch (error) { console.error("Error getting MCP server details:", error); throw error; diff --git a/renderer.ts b/renderer.ts index c593187..195b14d 100644 --- a/renderer.ts +++ b/renderer.ts @@ -949,7 +949,7 @@ function renderMcpServers() { e.stopPropagation(); if (confirm(`Remove MCP server "${server.name}"?`)) { try { - await ipcRenderer.invoke("remove-mcp-server", server.name); + await ipcRenderer.invoke("remove-mcp-server", activeSessionId, server.name); await loadMcpServers(); } catch (error) { alert(`Failed to remove server: ${error}`); @@ -978,7 +978,7 @@ async function showMcpServerDetails(name: string) { detailsModal?.classList.remove("hidden"); try { - const details = await ipcRenderer.invoke("get-mcp-server-details", name); + const details = await ipcRenderer.invoke("get-mcp-server-details", activeSessionId, name); if (detailsContent) { let html = ""; @@ -1139,7 +1139,7 @@ addMcpBtn?.addEventListener("click", async () => { addMcpBtn.classList.add("opacity-50", "cursor-not-allowed"); try { - await ipcRenderer.invoke("add-mcp-server", name, config); + await ipcRenderer.invoke("add-mcp-server", activeSessionId, name, config); await loadMcpServers(); mcpModal?.classList.add("hidden"); } catch (error) { @@ -1168,7 +1168,7 @@ removeMcpDetailsBtn?.addEventListener("click", async () => { if (confirm(`Remove MCP server "${serverName}"?`)) { try { - await ipcRenderer.invoke("remove-mcp-server", serverName); + await ipcRenderer.invoke("remove-mcp-server", activeSessionId, serverName); mcpDetailsModal?.classList.add("hidden"); await loadMcpServers(); } catch (error) {