execute_command
Run commands locally or on remote hosts using SSH with session persistence and environment variable support. Maintain terminal state for efficient workflows in specific environments like conda.
Instructions
Execute commands on remote hosts or locally (This tool can be used for both remote hosts and the current machine)
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| command | Yes | Command to execute. Before running commands, it's best to determine the system type (Mac, Linux, etc.) | |
| env | No | Environment variables | |
| host | No | Host to connect to (optional, if not provided the command will be executed locally) | |
| session | No | Session name, defaults to 'default'. The same session name will reuse the same terminal environment for 20 minutes, which is useful for operations requiring specific environments like conda. | default |
| username | No | Username for SSH connection (required when host is specified) |
Implementation Reference
- src/executor.ts:128-320 (handler)Core handler function in CommandExecutor class that implements the logic to execute commands either locally or on remote hosts via SSH, supporting sessions and environment variables.async executeCommand( command: string, options: { host?: string; username?: string; session?: string; env?: Record<string, string>; } = {} ): Promise<{stdout: string; stderr: string}> { const { host, username, session = 'default', env = {} } = options; const sessionKey = this.getSessionKey(host, session); // 如果指定了host,则使用SSH执行命令 if (host) { if (!username) { throw new Error('Username is required when using SSH'); } let sessionData = this.sessions.get(sessionKey); let needNewConnection = false; if (!sessionData || sessionData.host !== host) { needNewConnection = true; } else if (sessionData.client) { if (sessionData.client.listenerCount('ready') === 0 && sessionData.client.listenerCount('data') === 0) { log.info(`Session ${sessionKey} disconnected, reconnecting`); needNewConnection = true; } } else { needNewConnection = true; } if (needNewConnection) { log.info(`Creating new connection for command execution: ${sessionKey}`); await this.connect(host, username, session); sessionData = this.sessions.get(sessionKey); } else { log.info(`Reusing existing session for command execution: ${sessionKey}`); } if (!sessionData || !sessionData.client) { throw new Error(`host ${host} err`); } this.resetTimeout(sessionKey); if (sessionData.shellReady && sessionData.shell) { log.info(`Executing command using interactive shell: ${command}`); return new Promise((resolve, reject) => { let stdout = ""; let stderr = ""; let commandFinished = false; const uniqueMarker = `CMD_END_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; const envSetup = Object.entries(env) .map(([key, value]) => `export ${key}="${String(value).replace(/"/g, '\\"')}"`) .join(' && '); const fullCommand = envSetup ? `${envSetup} && ${command}` : command; const dataHandler = (data: Buffer) => { const str = data.toString(); log.debug(`Shell数据: ${str}`); if (str.includes(uniqueMarker)) { commandFinished = true; const lines = stdout.split('\n'); let commandOutput = ''; let foundCommand = false; for (const line of lines) { if (foundCommand) { if (line.includes(uniqueMarker)) { break; } commandOutput += line + '\n'; } else if (line.includes(fullCommand)) { foundCommand = true; } } resolve({ stdout: commandOutput.trim(), stderr }); sessionData.shell.removeListener('data', dataHandler); clearTimeout(timeout); } else if (!commandFinished) { stdout += str; } }; const errorHandler = (err: Error) => { stderr += err.message; reject(err); sessionData.shell.removeListener('data', dataHandler); sessionData.shell.removeListener('error', errorHandler); }; sessionData.shell.on('data', dataHandler); sessionData.shell.on('error', errorHandler); sessionData.shell.write(`echo "Starting command execution: ${fullCommand}"\n`); sessionData.shell.write(`${fullCommand}\n`); sessionData.shell.write(`echo "${uniqueMarker}"\n`); const timeout = setTimeout(() => { if (!commandFinished) { stderr += "Command execution timed out"; resolve({ stdout, stderr }); sessionData.shell.removeListener('data', dataHandler); sessionData.shell.removeListener('error', errorHandler); } }, 30000); // 30 seconds }); } else { log.info(`Executing command using exec: ${command}`); return new Promise((resolve, reject) => { const envSetup = Object.entries(env) .map(([key, value]) => `export ${key}="${String(value).replace(/"/g, '\\"')}"`) .join(' && '); const fullCommand = envSetup ? `${envSetup} && ${command}` : command; sessionData?.client?.exec(`/bin/bash --login -c "${fullCommand.replace(/"/g, '\\"')}"`, (err, stream) => { if (err) { reject(err); return; } let stdout = ""; let stderr = ''; stream .on("data", (data: Buffer) => { this.resetTimeout(sessionKey); stdout += data.toString(); }) .stderr.on('data', (data: Buffer) => { stderr += data.toString(); }) .on('close', () => { resolve({ stdout, stderr }); }) .on('error', (err) => { reject(err); }); }); }); } } else { log.info(`Executing command using local session: ${sessionKey}`); let sessionData = this.sessions.get(sessionKey); let sessionEnv = {}; if (!sessionData) { sessionData = { client: null, connection: null, timeout: null, host: undefined, env: { ...env } }; this.sessions.set(sessionKey, sessionData); log.info(`Creating new local session: ${sessionKey}`); sessionEnv = env; } else { log.info(`Reusing existing local session: ${sessionKey}`); if (!sessionData.env) { sessionData.env = {}; } sessionData.env = { ...sessionData.env, ...env }; sessionEnv = sessionData.env; this.sessions.set(sessionKey, sessionData); } this.resetTimeout(sessionKey); return new Promise((resolve, reject) => { const envVars = { ...process.env, ...sessionEnv }; log.info(`Executing local command: ${command}`); exec(command, { env: envVars }, (error, stdout, stderr) => { if (error && error.code !== 0) { resolve({ stdout, stderr: stderr || error.message }); } else { resolve({ stdout, stderr }); } }); }); } }
- src/index.ts:49-76 (schema)Input schema defining parameters for the execute_command tool: host, username, session, command, env.inputSchema: { type: "object", properties: { host: { type: "string", description: "Host to connect to (optional, if not provided the command will be executed locally)" }, username: { type: "string", description: "Username for SSH connection (required when host is specified)" }, session: { type: "string", description: "Session name, defaults to 'default'. The same session name will reuse the same terminal environment for 20 minutes, which is useful for operations requiring specific environments like conda.", default: "default" }, command: { type: "string", description: "Command to execute. Before running commands, it's best to determine the system type (Mac, Linux, etc.)" }, env: { type: "object", description: "Environment variables", default: {} } }, required: ["command"] }
- src/index.ts:46-79 (registration)Tool registration in the MCP ListToolsRequestSchema handler, including name, description, and schema.{ name: "execute_command", description: "Execute commands on remote hosts or locally (This tool can be used for both remote hosts and the current machine)", inputSchema: { type: "object", properties: { host: { type: "string", description: "Host to connect to (optional, if not provided the command will be executed locally)" }, username: { type: "string", description: "Username for SSH connection (required when host is specified)" }, session: { type: "string", description: "Session name, defaults to 'default'. The same session name will reuse the same terminal environment for 20 minutes, which is useful for operations requiring specific environments like conda.", default: "default" }, command: { type: "string", description: "Command to execute. Before running commands, it's best to determine the system type (Mac, Linux, etc.)" }, env: { type: "object", description: "Environment variables", default: {} } }, required: ["command"] } } ] };
- src/index.ts:82-133 (handler)MCP server handler for CallToolRequestSchema specific to execute_command, validates params and delegates to CommandExecutor.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { if (request.params.name !== "execute_command") { throw new McpError(ErrorCode.MethodNotFound, "Unknown tool"); } const host = request.params.arguments?.host ? String(request.params.arguments.host) : undefined; const username = request.params.arguments?.username ? String(request.params.arguments.username) : undefined; const session = String(request.params.arguments?.session || "default"); const command = String(request.params.arguments?.command); if (!command) { throw new McpError(ErrorCode.InvalidParams, "Command is required"); } const env = request.params.arguments?.env || {}; if (host && !username) { throw new McpError(ErrorCode.InvalidParams, "Username is required when host is specified"); } try { const result = await commandExecutor.executeCommand(command, { host, username, session, env: env as Record<string, string> }); return { content: [{ type: "text", text: `Command Output:\nstdout: ${result.stdout}\nstderr: ${result.stderr}` }] }; } catch (error) { if (error instanceof Error && error.message.includes('SSH')) { throw new McpError( ErrorCode.InternalError, `SSH connection error: ${error.message}. Please ensure SSH key-based authentication is set up.` ); } throw error; } } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, error instanceof Error ? error.message : String(error) ); } });