Skip to main content
Glama

vulcan-file-ops

shell-tool.ts8.63 kB
import { zodToJsonSchema } from "zod-to-json-schema"; import { ToolSchema } from "@modelcontextprotocol/sdk/types.js"; import { ShellCommandArgsSchema, type ShellCommandArgs, } from "../types/index.js"; import { validateCommand, isCommandApproved, extractRootCommands, isDangerousCommand, getShellConfig, } from "../utils/command-validation.js"; import { executeShellCommand, type ExecutionResult, } from "../utils/shell-execution.js"; import { validatePath, getAllowedDirectories } from "../utils/lib.js"; import { extractPathsFromCommand } from "../utils/command-path-extraction.js"; import { isPathWithinAllowedDirectories } from "../utils/path-validation.js"; const ToolInputSchema = ToolSchema.shape.inputSchema; type ToolInput = any; // Global state for approved commands let approvedCommands: Set<string> = new Set(); let alwaysApprovedCommands: Set<string> = new Set(); /** * Initialize the shell tool with approved commands */ export function initializeShellTool(commands: string[]): void { approvedCommands = new Set(commands); } /** * Get the list of approved commands */ export function getApprovedCommands(): string[] { return Array.from(approvedCommands); } /** * Add command to always-approved list (runtime approval) */ export function addToAlwaysApproved(command: string): void { alwaysApprovedCommands.add(command); } /** * Get shell tool definition */ export function getShellTools() { const shellConfig = getShellConfig(); const currentApprovedCommands = getApprovedCommands(); // Generate dynamic description with approved commands let approvedCommandsText = ""; if (currentApprovedCommands.length > 0) { const cmdList = currentApprovedCommands .map((cmd) => ` - ${cmd}`) .join("\n"); approvedCommandsText = `\n\nPRE-APPROVED COMMANDS (no confirmation needed):\n${cmdList}\n\nOther commands may require user approval before execution.`; } else { approvedCommandsText = "\n\nNo pre-approved commands. All commands require user approval before execution."; } return [ { name: "execute_shell", description: `Execute shell commands on the host system with security controls. ` + `Commands are executed as '${shellConfig.shell} ${shellConfig.args.join( " " )} <command>' on ${shellConfig.platform}. ` + `\n\nThe tool captures stdout, stderr, exit codes, and signals. ` + `Working directory can be specified (must be within allowed directories). ` + `Commands exceeding the timeout will be automatically terminated. ` + `\n\n⚠️ SECURITY: Command substitution and certain dangerous patterns may be restricted.` + approvedCommandsText + `\n\nIMPORTANT: Always provide a clear description of what the command does and why it's needed.`, inputSchema: zodToJsonSchema(ShellCommandArgsSchema) as ToolInput, }, ]; } /** * Format execution result for AI assistant */ function formatExecutionResult( args: ShellCommandArgs, result: ExecutionResult, workdir: string ): string { const lines: string[] = []; lines.push("Shell Command Execution Result:"); lines.push("================================"); lines.push(""); lines.push(`Command: ${args.command}`); if (args.description) { lines.push(`Description: ${args.description}`); } lines.push(`Working Directory: ${workdir}`); lines.push(`Exit Code: ${result.exitCode ?? "(none)"}`); lines.push(`Signal: ${result.signal ?? "(none)"}`); if (result.timedOut) { lines.push( `⚠️ TIMEOUT: Command exceeded ${args.timeout || 30000}ms limit` ); } lines.push(""); lines.push("--- Standard Output ---"); lines.push(result.stdout || "(empty)"); lines.push(""); lines.push("--- Standard Error ---"); lines.push(result.stderr || "(empty)"); if (result.error) { lines.push(""); lines.push("--- Error ---"); lines.push(result.error.message); } return lines.join("\n"); } /** * Handle shell tool execution */ export async function handleShellTool( name: string, args: unknown ): Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean; }> { if (name !== "execute_shell") { throw new Error(`Unknown shell tool: ${name}`); } // Validate arguments const validatedArgs = ShellCommandArgsSchema.parse(args); // Validate command security const commandValidation = validateCommand(validatedArgs.command, false); if (!commandValidation.allowed) { throw new Error(`Command validation failed: ${commandValidation.reason}`); } // Extract root commands for approval checking const rootCommands = extractRootCommands(validatedArgs.command); const allApproved = rootCommands.every( (cmd) => approvedCommands.has(cmd) || alwaysApprovedCommands.has(cmd) ); // Check for dangerous patterns const isDangerous = isDangerousCommand(validatedArgs.command); if (!allApproved && (validatedArgs.requiresApproval || isDangerous)) { const unapprovedCommands = rootCommands.filter( (cmd) => !approvedCommands.has(cmd) && !alwaysApprovedCommands.has(cmd) ); throw new Error( `Command requires approval. Unapproved commands: ${unapprovedCommands.join( ", " )}\n` + `Command: ${validatedArgs.command}\n` + `${ isDangerous ? "⚠️ Warning: This command matches dangerous patterns\n" : "" }` + `To approve, add these commands to --approved-commands or .env configuration.` ); } // Validate working directory if provided let workdir = process.cwd(); if (validatedArgs.workdir) { try { workdir = await validatePath(validatedArgs.workdir); } catch (error) { throw new Error( `Invalid working directory: ${validatedArgs.workdir}\n` + `Error: ${error instanceof Error ? error.message : String(error)}\n` + `Working directory must be within allowed directories.` ); } } // Extract and validate paths from command arguments try { const extractedPaths = extractPathsFromCommand(validatedArgs.command, workdir); if (extractedPaths.length > 0) { const allowedDirs = getAllowedDirectories(); // If no allowed directories are configured, block all paths for security if (allowedDirs.length === 0) { throw new Error( `Access denied: Command contains paths but no allowed directories are configured.\n` + `Extracted paths:\n` + extractedPaths.map(p => ` - ${p}`).join('\n') + `\n\nPlease configure allowed directories using --approved-folders or register_directory tool.` ); } // Validate each extracted path const invalidPaths: string[] = []; for (const extractedPath of extractedPaths) { if (!isPathWithinAllowedDirectories(extractedPath, allowedDirs)) { invalidPaths.push(extractedPath); } } if (invalidPaths.length > 0) { throw new Error( `Access denied: Command contains paths outside allowed directories:\n` + invalidPaths.map(p => ` - ${p}`).join('\n') + `\n\nAllowed directories:\n` + allowedDirs.map(d => ` - ${d}`).join('\n') + `\n\nTo access these paths, register their parent directories using register_directory tool.` ); } } } catch (error) { // If path extraction fails, be conservative and block // (better to block than allow potentially unsafe commands) if (error instanceof Error && error.message.includes('Access denied')) { throw error; } // For extraction errors, block the command to be safe throw new Error( `Path validation failed: ${error instanceof Error ? error.message : String(error)}\n` + `Command blocked for security. Please ensure all paths in the command are within allowed directories.` ); } // Execute command try { const result = await executeShellCommand(validatedArgs.command, { workdir, timeout: validatedArgs.timeout, }); const formattedResult = formatExecutionResult( validatedArgs, result, workdir ); // Consider non-zero exit codes as errors const isError = result.exitCode !== 0 || result.timedOut || !!result.error; return { content: [{ type: "text", text: formattedResult }], isError, }; } catch (error) { throw new Error( `Command execution failed: ${ error instanceof Error ? error.message : String(error) }` ); } }

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/n0zer0d4y/vulcan-file-ops'

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