Skip to main content
Glama

Claude Desktop Commander MCP

terminal-manager.ts8.2 kB
import { spawn } from 'child_process'; import { TerminalSession, CommandExecutionResult, ActiveSession } from './types.js'; import { DEFAULT_COMMAND_TIMEOUT } from './config.js'; import { configManager } from './config-manager.js'; import {capture} from "./utils/capture.js"; import { analyzeProcessState } from './utils/process-detection.js'; interface CompletedSession { pid: number; output: string; exitCode: number | null; startTime: Date; endTime: Date; } export class TerminalManager { private sessions: Map<number, TerminalSession> = new Map(); private completedSessions: Map<number, CompletedSession> = new Map(); /** * Send input to a running process * @param pid Process ID * @param input Text to send to the process * @returns Whether input was successfully sent */ sendInputToProcess(pid: number, input: string): boolean { const session = this.sessions.get(pid); if (!session) { return false; } try { if (session.process.stdin && !session.process.stdin.destroyed) { // Ensure input ends with a newline for most REPLs const inputWithNewline = input.endsWith('\n') ? input : input + '\n'; session.process.stdin.write(inputWithNewline); return true; } return false; } catch (error) { console.error(`Error sending input to process ${pid}:`, error); return false; } } async executeCommand(command: string, timeoutMs: number = DEFAULT_COMMAND_TIMEOUT, shell?: string): Promise<CommandExecutionResult> { // Get the shell from config if not specified let shellToUse: string | boolean | undefined = shell; if (!shellToUse) { try { const config = await configManager.getConfig(); shellToUse = config.defaultShell || true; } catch (error) { // If there's an error getting the config, fall back to default shellToUse = true; } } // For REPL interactions, we need to ensure stdin, stdout, and stderr are properly configured // Note: No special stdio options needed here, Node.js handles pipes by default // Enhance SSH commands automatically let enhancedCommand = command; if (command.trim().startsWith('ssh ') && !command.includes(' -t')) { enhancedCommand = command.replace(/^ssh /, 'ssh -t '); console.log(`Enhanced SSH command: ${enhancedCommand}`); } const spawnOptions: any = { shell: shellToUse, env: { ...process.env, TERM: 'xterm-256color' // Better terminal compatibility } }; // Spawn the process with an empty array of arguments and our options const childProcess = spawn(enhancedCommand, [], spawnOptions); let output = ''; // Ensure childProcess.pid is defined before proceeding if (!childProcess.pid) { // Return a consistent error object instead of throwing return { pid: -1, // Use -1 to indicate an error state output: 'Error: Failed to get process ID. The command could not be executed.', isBlocked: false }; } const session: TerminalSession = { pid: childProcess.pid, process: childProcess, lastOutput: '', isBlocked: false, startTime: new Date() }; this.sessions.set(childProcess.pid, session); return new Promise((resolve) => { let resolved = false; let periodicCheck: NodeJS.Timeout | null = null; // Quick prompt patterns for immediate detection const quickPromptPatterns = />>>\s*$|>\s*$|\$\s*$|#\s*$/; const resolveOnce = (result: CommandExecutionResult) => { if (resolved) return; resolved = true; if (periodicCheck) clearInterval(periodicCheck); resolve(result); }; childProcess.stdout.on('data', (data: any) => { const text = data.toString(); output += text; session.lastOutput += text; // Immediate check for obvious prompts if (quickPromptPatterns.test(text)) { session.isBlocked = true; resolveOnce({ pid: childProcess.pid!, output, isBlocked: true }); } }); childProcess.stderr.on('data', (data: any) => { const text = data.toString(); output += text; session.lastOutput += text; }); // Periodic comprehensive check every 100ms periodicCheck = setInterval(() => { if (output.trim()) { const processState = analyzeProcessState(output, childProcess.pid); if (processState.isWaitingForInput) { session.isBlocked = true; resolveOnce({ pid: childProcess.pid!, output, isBlocked: true }); } } }, 100); // Timeout fallback setTimeout(() => { session.isBlocked = true; resolveOnce({ pid: childProcess.pid!, output, isBlocked: true }); }, timeoutMs); childProcess.on('exit', (code: any) => { if (childProcess.pid) { // Store completed session before removing active session this.completedSessions.set(childProcess.pid, { pid: childProcess.pid, output: output, // Use only the main output variable exitCode: code, startTime: session.startTime, endTime: new Date() }); // Keep only last 100 completed sessions if (this.completedSessions.size > 100) { const oldestKey = Array.from(this.completedSessions.keys())[0]; this.completedSessions.delete(oldestKey); } this.sessions.delete(childProcess.pid); } resolveOnce({ pid: childProcess.pid!, output, isBlocked: false }); }); }); } getNewOutput(pid: number): string | null { // First check active sessions const session = this.sessions.get(pid); if (session) { const output = session.lastOutput; session.lastOutput = ''; return output; } // Then check completed sessions const completedSession = this.completedSessions.get(pid); if (completedSession) { // Format with output first, then completion info const runtime = (completedSession.endTime.getTime() - completedSession.startTime.getTime()) / 1000; const output = completedSession.output.trim(); if (output) { return `${output}\n\nProcess completed with exit code ${completedSession.exitCode}\nRuntime: ${runtime}s`; } else { return `Process completed with exit code ${completedSession.exitCode}\nRuntime: ${runtime}s\n(No output produced)`; } } return null; } /** * Get a session by PID * @param pid Process ID * @returns The session or undefined if not found */ getSession(pid: number): TerminalSession | undefined { return this.sessions.get(pid); } forceTerminate(pid: number): boolean { const session = this.sessions.get(pid); if (!session) { return false; } try { session.process.kill('SIGINT'); setTimeout(() => { if (this.sessions.has(pid)) { session.process.kill('SIGKILL'); } }, 1000); return true; } catch (error) { // Convert error to string, handling both Error objects and other types const errorMessage = error instanceof Error ? error.message : String(error); capture('server_request_error', {error: errorMessage, message: `Failed to terminate process ${pid}:`}); return false; } } listActiveSessions(): ActiveSession[] { const now = new Date(); return Array.from(this.sessions.values()).map(session => ({ pid: session.pid, isBlocked: session.isBlocked, runtime: now.getTime() - session.startTime.getTime() })); } listCompletedSessions(): CompletedSession[] { return Array.from(this.completedSessions.values()); } } export const terminalManager = new TerminalManager();

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wonderwhy-er/DesktopCommanderMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server