Run MCP add/remove/get commands in poller PTY instead of execAsync

- 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 <noreply@anthropic.com>
This commit is contained in:
Amrit Subramanian 2025-10-07 19:37:08 -04:00
parent 7c344ff0f3
commit 84e43f656c
2 changed files with 50 additions and 18 deletions

60
main.ts
View file

@ -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<string> {
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. // Use add-json to support full configuration including env vars, headers, etc.
const jsonConfig = JSON.stringify(config); const jsonConfig = JSON.stringify(config).replace(/'/g, "'\\''"); // Escape single quotes for shell
await execAsync(`claude mcp add-json --scope user "${name}" '${jsonConfig}'`); const command = `claude mcp add-json --scope user "${name}" '${jsonConfig}'`;
await executeInMcpPoller(sessionId, command);
} }
async function removeMcpServer(name: string) { async function removeMcpServer(sessionId: string, name: string) {
await execAsync(`claude mcp remove "${name}"`); const command = `claude mcp remove "${name}"`;
await executeInMcpPoller(sessionId, command);
} }
async function getMcpServerDetails(name: string) { async function getMcpServerDetails(sessionId: string, name: string) {
try { try {
const { stdout } = await execAsync(`claude mcp get "${name}"`); const output = await executeInMcpPoller(sessionId, `claude mcp get "${name}"`);
// Parse the output to extract details // Parse the output to extract details
const details: any = { name }; const details: any = { name };
const lines = stdout.split("\n"); const lines = output.split("\n");
for (const line of lines) { for (const line of lines) {
const trimmed = line.trim(); 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 { try {
await addMcpServer(name, config); await addMcpServer(sessionId, name, config);
} catch (error) { } catch (error) {
console.error("Error adding MCP server:", error); console.error("Error adding MCP server:", error);
throw error; throw error;
} }
}); });
ipcMain.handle("remove-mcp-server", async (_event, name: string) => { ipcMain.handle("remove-mcp-server", async (_event, sessionId: string, name: string) => {
try { try {
await removeMcpServer(name); await removeMcpServer(sessionId, name);
} catch (error) { } catch (error) {
console.error("Error removing MCP server:", error); console.error("Error removing MCP server:", error);
throw 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 { try {
return await getMcpServerDetails(name); return await getMcpServerDetails(sessionId, name);
} catch (error) { } catch (error) {
console.error("Error getting MCP server details:", error); console.error("Error getting MCP server details:", error);
throw error; throw error;

View file

@ -949,7 +949,7 @@ function renderMcpServers() {
e.stopPropagation(); e.stopPropagation();
if (confirm(`Remove MCP server "${server.name}"?`)) { if (confirm(`Remove MCP server "${server.name}"?`)) {
try { try {
await ipcRenderer.invoke("remove-mcp-server", server.name); await ipcRenderer.invoke("remove-mcp-server", activeSessionId, server.name);
await loadMcpServers(); await loadMcpServers();
} catch (error) { } catch (error) {
alert(`Failed to remove server: ${error}`); alert(`Failed to remove server: ${error}`);
@ -978,7 +978,7 @@ async function showMcpServerDetails(name: string) {
detailsModal?.classList.remove("hidden"); detailsModal?.classList.remove("hidden");
try { try {
const details = await ipcRenderer.invoke("get-mcp-server-details", name); const details = await ipcRenderer.invoke("get-mcp-server-details", activeSessionId, name);
if (detailsContent) { if (detailsContent) {
let html = ""; let html = "";
@ -1139,7 +1139,7 @@ addMcpBtn?.addEventListener("click", async () => {
addMcpBtn.classList.add("opacity-50", "cursor-not-allowed"); addMcpBtn.classList.add("opacity-50", "cursor-not-allowed");
try { try {
await ipcRenderer.invoke("add-mcp-server", name, config); await ipcRenderer.invoke("add-mcp-server", activeSessionId, name, config);
await loadMcpServers(); await loadMcpServers();
mcpModal?.classList.add("hidden"); mcpModal?.classList.add("hidden");
} catch (error) { } catch (error) {
@ -1168,7 +1168,7 @@ removeMcpDetailsBtn?.addEventListener("click", async () => {
if (confirm(`Remove MCP server "${serverName}"?`)) { if (confirm(`Remove MCP server "${serverName}"?`)) {
try { try {
await ipcRenderer.invoke("remove-mcp-server", serverName); await ipcRenderer.invoke("remove-mcp-server", activeSessionId, serverName);
mcpDetailsModal?.classList.add("hidden"); mcpDetailsModal?.classList.add("hidden");
await loadMcpServers(); await loadMcpServers();
} catch (error) { } catch (error) {