Skip to main content
Glama
runCommand.ts4.08 kB
/** * run_command tool implementation * Provides safe command execution capabilities within the workspace */ import { spawn } from 'child_process'; import { ServerConfig } from '../config.js'; import { resolveSafePath } from '../utils/pathUtils.js'; import { createCommandNotAllowedError, classifyError } from '../utils/errors.js'; /** * Input parameters for run_command tool */ export interface RunCommandInput { command: string; args?: string[]; cwd?: string; timeoutMs?: number; } /** * Output from run_command tool */ export interface RunCommandOutput { exitCode: number; stdout: string; stderr: string; timedOut: boolean; } /** * Tool metadata for MCP registration */ export const runCommandTool = { name: 'run_command', description: 'Execute an allowed command in the workspace. Only commands in the allowlist can be executed.', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'Command to execute (must be in allowed commands list)', }, args: { type: 'array', items: { type: 'string' }, description: 'Command arguments as an array', default: [], }, cwd: { type: 'string', description: 'Working directory relative to workspace root', }, timeoutMs: { type: 'number', description: 'Timeout in milliseconds (defaults to server configured timeout)', }, }, required: ['command'], }, }; /** * Executes the run_command tool * @param input - Tool input parameters * @param config - Server configuration * @returns Command execution results */ export async function executeRunCommand( input: RunCommandInput, config: ServerConfig ): Promise<RunCommandOutput> { const command = input.command; const args = input.args ?? []; const timeoutMs = input.timeoutMs ?? config.commandTimeout; // Validate command is in allowlist if (!config.allowedCommands.includes(command)) { throw createCommandNotAllowedError(command, config.allowedCommands); } // Resolve working directory if specified let workingDirectory = config.workspaceRoot; if (input.cwd) { try { workingDirectory = await resolveSafePath(config.workspaceRoot, input.cwd); } catch (error: unknown) { throw classifyError(error, 'run_command'); } } return new Promise((resolve) => { let stdout = ''; let stderr = ''; let timedOut = false; let exitCode: number | null = null; // Spawn the process with array arguments to prevent injection const childProcess = spawn(command, args, { cwd: workingDirectory, shell: false, // Prevent shell interpretation windowsHide: true, }); // Set up timeout const timeoutHandle = setTimeout(() => { timedOut = true; childProcess.kill('SIGTERM'); // Force kill after 1 second if still running setTimeout(() => { if (childProcess.exitCode === null) { childProcess.kill('SIGKILL'); } }, 1000); }, timeoutMs); // Collect stdout childProcess.stdout?.on('data', (data: Buffer) => { stdout += data.toString(); }); // Collect stderr childProcess.stderr?.on('data', (data: Buffer) => { stderr += data.toString(); }); // Handle process exit childProcess.on('close', (code: number | null) => { clearTimeout(timeoutHandle); // If killed by signal and we timed out, use special exit code if (timedOut) { exitCode = 124; // Standard timeout exit code } else { exitCode = code ?? 1; // Default to 1 if code is null } resolve({ exitCode, stdout, stderr, timedOut, }); }); // Handle spawn errors childProcess.on('error', (error: Error) => { clearTimeout(timeoutHandle); resolve({ exitCode: 1, stdout, stderr: stderr + `\nError spawning process: ${error.message}`, timedOut: false, }); }); }); }

Latest Blog Posts

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/ShayYeffet/mcp_server'

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