Skip to main content
Glama
command-executor.ts6.77 kB
/** * Command execution module for tool documentation * * Handles secure execution of tool commands for version and help text retrieval. */ import { spawn } from 'child_process'; import type { ToolInfo } from '@/types/index'; import { logger } from './logger'; import { existsSync } from 'fs'; import { getWorkspacePath } from '@/utils/workspace'; interface CommandOptions { cwd?: string; currentDir?: string; timeout?: number; maxOutputSize?: number; } interface CommandResult { output: string; code: number; } interface CommandError extends Error { code?: string; } /** * Executes a tool with proper working directory handling * @param tool The tool info * @param args Command arguments * @param options Additional execution options * @returns Promise<CommandResult> */ export async function executeTool( tool: ToolInfo, args: string[], options: Partial<CommandOptions> = {} ): Promise<CommandResult> { let command = tool.name; // Initialize with tool name as default let finalArgs: string[] = [...args]; const finalOptions: CommandOptions = { cwd: tool.workingDirectory || getWorkspacePath(), timeout: options.timeout || 5000, maxOutputSize: options.maxOutputSize || 50000 }; // logger.debug('Executing tool:', { // tool, // args, // options: finalOptions, // currentDir: getWorkspacePath() // }); try { switch (tool.type) { case 'npm-script': command = 'npm'; finalArgs = ['run', tool.name.replace('npm:', ''), ...args]; break; case 'package-bin': case 'workspace-bin': command = tool.location || tool.name; logger.debug('Resolved command path:', { command, location: tool.location, name: tool.name, exists: existsSync(command) }); break; case 'global-bin': // For global tools, we don't need to check location since they should be in PATH // logger.debug('Using global tool:', { // command: tool.name, // args // }); break; default: throw new Error(`Unknown tool type: ${(tool as ToolInfo).type}`); } return executeCommand(command, finalArgs, finalOptions); } catch (error) { logger.error('Failed to execute tool:', { tool, args, error, command, finalArgs, options: finalOptions }); // Enhance error message based on error type if (error instanceof Error) { if (error.message.includes('ENOENT')) { throw new Error(`Tool not found: ${tool.name}. Please ensure it is installed and available in your PATH.`); } if (error.message.includes('EACCES')) { throw new Error(`Permission denied when executing ${tool.name}. Please check file permissions.`); } if (error.message.includes('ETIMEDOUT')) { throw new Error(`Execution of ${tool.name} timed out. Try increasing the timeout value.`); } throw error; } throw new Error(`Failed to execute ${tool.name}: ${error}`); } } /** * Executes a command with security measures * @param command The command to execute * @param args Command arguments * @param options Execution options * @returns Promise<CommandResult> */ export async function executeCommand( command: string, args: string[], options: CommandOptions ): Promise<CommandResult> { const workspacePath = getWorkspacePath(); const cwd = options.cwd || workspacePath; // const currentDir = options.currentDir || workspacePath; const timeout = options.timeout || 5000; const maxOutputSize = options.maxOutputSize || 50000; return new Promise<CommandResult>((resolve, reject) => { try { // logger.debug('Executing command:', { // command, // args, // cwd, // timeout, // maxOutputSize, // currentDir, // commandExists: existsSync(command) // }); const child = spawn(command, args, { cwd, shell: false, // Security: Disable shell execution timeout // Security: Timeout after specified duration }); let output = ''; let error = ''; child.stdout.on('data', (data) => { const chunk = data.toString(); // logger.debug('Command stdout:', chunk); if (output.length + chunk.length <= maxOutputSize) { output += chunk; } }); child.stderr.on('data', (data) => { const chunk = data.toString(); logger.debug('Command stderr:', chunk); error += chunk; }); child.on('error', (err: CommandError) => { logger.error('Command error:', { error: err, code: err.code, command, args, cwd }); if (err.code === 'ENOENT') { reject(new Error(`Command not found: ${command}`)); } else { reject(err); } }); child.on('close', (code) => { // logger.debug('Command completed:', { // code, // output, // error, // command, // args, // cwd // }); resolve({ output: output || error, code: code || 0 }); }); } catch (error) { logger.error('Failed to execute command:', { error, command, args, cwd }); reject(error); } }); } /** * Checks if a tool is available and executable * @param tool The tool info to check * @returns Promise<boolean> */ export async function isToolExecutable(tool: ToolInfo): Promise<boolean> { if (!tool.name) { logger.debug('Tool name is undefined'); return false; } try { // For scripts, we don't need to check executability if (tool.type === 'script' || tool.type === 'npm-script') { return true; // Scripts are always "executable" since they're defined in package.json } // For binaries and tools, check if they're actually executable if (tool.type === 'package-bin' || tool.type === 'workspace-bin' || tool.type === 'global-bin') { // Try running with --version first (less output than --help) const result = await executeTool(tool, ['--version'], { timeout: 2000, maxOutputSize: 1000 }); if (result.code === 0) return true; // If --version fails, try -v as fallback const fallbackResult = await executeTool(tool, ['-v'], { timeout: 2000, maxOutputSize: 1000 }); return fallbackResult.code === 0; } // For unknown types, assume not executable return false; } catch (error) { logger.debug(`Tool not executable: ${tool.name}`, error); return 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/patelnav/my-tools-mcp'

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