Skip to main content
Glama

Codex MCP Server

by cexll
commandExecutor.ts5.85 kB
import { spawn } from 'child_process'; import { Logger } from './logger.js'; export interface CommandResult { ok: boolean; code: number | null; signal?: NodeJS.Signals; stdout: string; stderr: string; timedOut: boolean; partialStdout?: string; } export interface RetryOptions { attempts: number; backoffMs: number; retryOn: ('timeout' | 'exit_nonzero' | 'spawn_error')[]; } export interface ExecuteOptions { onProgress?: (newOutput: string) => void; timeoutMs?: number; maxOutputBytes?: number; retry?: RetryOptions; } /** * Execute a command with streaming output and structured error handling */ export async function executeCommandDetailed( command: string, args: string[], options: ExecuteOptions = {} ): Promise<CommandResult> { const { onProgress, timeoutMs = 600000, maxOutputBytes = 50 * 1024 * 1024, // 50MB default retry, } = options; let attempt = 0; const maxAttempts = retry?.attempts || 1; while (attempt < maxAttempts) { attempt++; const result = await executeOnce(command, args, { onProgress, timeoutMs, maxOutputBytes, }); if (result.ok) { return result; } const shouldRetry = retry && ((result.timedOut && retry.retryOn.includes('timeout')) || (result.code !== 0 && result.code !== null && retry.retryOn.includes('exit_nonzero')) || (result.code === null && !result.signal && retry.retryOn.includes('spawn_error'))); if (!shouldRetry || attempt >= maxAttempts) { return result; } // Exponential backoff const delay = retry.backoffMs * Math.pow(2, attempt - 1); Logger.warn(`Retrying command after ${delay}ms (attempt ${attempt + 1}/${maxAttempts})`); await new Promise(resolve => setTimeout(resolve, delay)); } // This should never be reached throw new Error('Unexpected retry loop exit'); } async function executeOnce( command: string, args: string[], { onProgress, timeoutMs, maxOutputBytes }: Omit<ExecuteOptions, 'retry'> ): Promise<CommandResult> { return new Promise(resolve => { const startTime = Date.now(); Logger.commandExecution(command, args, startTime); const childProcess = spawn(command, args, { env: process.env, shell: false, stdio: ['ignore', 'pipe', 'pipe'], }); const stdoutChunks: Buffer[] = []; const stderrChunks: Buffer[] = []; let totalStdoutBytes = 0; let isResolved = false; let outputExceeded = false; // Set up timeout with SIGKILL fallback const timeoutId = setTimeout(() => { if (!isResolved) { childProcess.kill('SIGTERM'); Logger.warn(`Process timeout after ${timeoutMs}ms, sending SIGTERM`); // Give process 5 seconds to terminate gracefully setTimeout(() => { if (!isResolved) { childProcess.kill('SIGKILL'); Logger.error(`Process did not terminate, sending SIGKILL`); } }, 5000); } }, timeoutMs || 600000); childProcess.stdout.on('data', (data: Buffer) => { // Check output size limit if (maxOutputBytes && totalStdoutBytes + data.length > maxOutputBytes) { if (!outputExceeded) { outputExceeded = true; Logger.warn(`Output exceeded ${maxOutputBytes} bytes, stopping collection`); childProcess.kill('SIGTERM'); } return; } stdoutChunks.push(data); totalStdoutBytes += data.length; // Stream progress without buffering if (onProgress) { onProgress(data.toString('utf8')); } }); // Capture stderr for error reporting childProcess.stderr.on('data', (data: Buffer) => { stderrChunks.push(data); }); childProcess.on('error', error => { if (!isResolved) { isResolved = true; clearTimeout(timeoutId); Logger.error(`Process error:`, error); // Check for common errors const errorMessage = error.message; if ((error as any).code === 'ENOENT') { resolve({ ok: false, code: null, stdout: '', stderr: `Command '${command}' not found. Is it installed and in PATH?`, timedOut: false, }); } else { resolve({ ok: false, code: null, stdout: Buffer.concat(stdoutChunks).toString('utf8'), stderr: errorMessage, timedOut: false, }); } } }); childProcess.on('close', (code, signal) => { if (!isResolved) { isResolved = true; clearTimeout(timeoutId); const stdout = Buffer.concat(stdoutChunks).toString('utf8'); const stderr = Buffer.concat(stderrChunks).toString('utf8'); const timedOut = signal === 'SIGTERM' || signal === 'SIGKILL'; Logger.commandComplete(startTime, code, stdout.length); resolve({ ok: code === 0 && !outputExceeded, code, signal: signal || undefined, stdout: stdout.trim(), stderr: stderr.trim(), timedOut, partialStdout: outputExceeded ? stdout : undefined, }); } }); }); } /** * Backward compatible wrapper that returns stdout string */ export async function executeCommand( command: string, args: string[], onProgress?: (newOutput: string) => void, timeoutMs: number = 600000 ): Promise<string> { const result = await executeCommandDetailed(command, args, { onProgress, timeoutMs, }); if (!result.ok) { const errorMessage = result.stderr || 'Unknown error'; throw new Error( result.timedOut ? `Command timed out after ${timeoutMs}ms` : `Command failed with exit code ${result.code}: ${errorMessage}` ); } return result.stdout; }

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/cexll/codex-mcp-server'

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