write_to_terminal
Sends a command or text to the active iTerm terminal for execution, enabling direct terminal control through the MCP server.
Instructions
Writes text to the active iTerm terminal - often used to run a command in the terminal
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| command | Yes | The command to run or text to write to the terminal |
Implementation Reference
- src/index.ts:29-41 (registration)Tool registration: defines the 'write_to_terminal' tool with name, description, and inputSchema (requires 'command' string).
name: "write_to_terminal", description: "Writes text to the active iTerm terminal - often used to run a command in the terminal", inputSchema: { type: "object", properties: { command: { type: "string", description: "The command to run or text to write to the terminal" }, }, required: ["command"] } }, - src/index.ts:76-94 (handler)Handler for 'write_to_terminal': instantiates CommandExecutor, captures buffer before/after command, executes the command via AppleScript on iTerm2, and returns the number of output lines.
case "write_to_terminal": { let executor = new CommandExecutor(); const command = String(request.params.arguments?.command); const beforeCommandBuffer = await TtyOutputReader.retrieveBuffer(); const beforeCommandBufferLines = beforeCommandBuffer.split("\n").length; await executor.executeCommand(command); const afterCommandBuffer = await TtyOutputReader.retrieveBuffer(); const afterCommandBufferLines = afterCommandBuffer.split("\n").length; const outputLines = afterCommandBufferLines - beforeCommandBufferLines return { content: [{ type: "text", text: `${outputLines} lines were output after sending the command to the terminal. Read the last ${outputLines} lines of terminal contents to orient yourself. Never assume that the command was executed or that it was successful.` }] }; } - src/index.ts:32-40 (schema)Input schema for write_to_terminal: accepts a required 'command' string property.
type: "object", properties: { command: { type: "string", description: "The command to run or text to write to the terminal" }, }, required: ["command"] } - src/CommandExecutor.ts:19-212 (helper)CommandExecutor class: handles sending commands to iTerm2 via AppleScript, with special handling for multiline commands. Uses AppleScript to write text to the current session, waits for processing to complete and for the terminal to be ready for user input.
class CommandExecutor { private _execPromise: typeof execPromise; constructor(execPromiseOverride?: typeof execPromise) { this._execPromise = execPromiseOverride || execPromise; } /** * Executes a command in the iTerm2 terminal. * * This method handles both single-line and multiline commands by: * 1. Properly escaping the command string for AppleScript * 2. Using different AppleScript approaches based on whether the command contains newlines * 3. Waiting for the command to complete execution * 4. Retrieving the terminal output after command execution * * @param command The command to execute (can contain newlines) * @returns A promise that resolves to the terminal output after command execution */ async executeCommand(command: string): Promise<string> { const escapedCommand = this.escapeForAppleScript(command); try { // Check if this is a multiline command (which would have been processed differently) if (command.includes('\n')) { // For multiline text, we use parentheses around our prepared string expression // This allows AppleScript to evaluate the string concatenation expression await this._execPromise(`/usr/bin/osascript -e 'tell application "iTerm2" to tell current session of current window to write text (${escapedCommand})'`); } else { // For single line commands, we can use the standard approach with quoted strings await this._execPromise(`/usr/bin/osascript -e 'tell application "iTerm2" to tell current session of current window to write text "${escapedCommand}"'`); } // Wait until iTerm2 reports that command processing is complete while (await this.isProcessing()) { await sleep(100); } // Get the TTY path and check if it's waiting for user input const ttyPath = await this.retrieveTtyPath(); while (await this.isWaitingForUserInput(ttyPath) === false) { await sleep(100); } // Give a small delay for output to settle await sleep(200); // Retrieve the terminal output after command execution const afterCommandBuffer = await TtyOutputReader.retrieveBuffer() return afterCommandBuffer } catch (error: unknown) { throw new Error(`Failed to execute command: ${(error as Error).message}`); } } async isWaitingForUserInput(ttyPath: string): Promise<boolean> { let fd; try { // Open the TTY file descriptor in non-blocking mode fd = openSync(ttyPath, 'r'); const tracker = new ProcessTracker(); let belowThresholdTime = 0; while (true) { try { const activeProcess = await tracker.getActiveProcess(ttyPath); if (!activeProcess) return true; if (activeProcess.metrics.totalCPUPercent < 1) { belowThresholdTime += 350; if (belowThresholdTime >= 1000) return true; } else { belowThresholdTime = 0; } } catch { return true; } await sleep(350); } } catch (error: unknown) { return true; } finally { if (fd !== undefined) { closeSync(fd); } return true; } } /** * Escapes a string for use in an AppleScript command. * * This method handles two scenarios: * 1. For multiline text (containing newlines), it uses a special AppleScript * string concatenation approach to properly handle line breaks * 2. For single-line text, it escapes special characters for AppleScript compatibility * * @param str The string to escape * @returns A properly escaped string ready for AppleScript execution */ escapeForAppleScript(str: string): string { // Check if the string contains newlines if (str.includes('\n')) { // For multiline text, we need to use a different AppleScript approach // that properly handles newlines in AppleScript return this.prepareMultilineCommand(str); } // First, escape any backslashes str = str.replace(/\\/g, '\\\\'); // Escape double quotes str = str.replace(/"/g, '\\"'); // Handle single quotes by breaking out of the quote, escaping the quote, and going back in str = str.replace(/'/g, "'\\''"); // Handle control characters only - AppleScript handles Unicode characters natively str = str.replace(/[\x00-\x1F\x7F]/g, (char) => { return '\\u' + char.charCodeAt(0).toString(16).padStart(4, '0'); }); return str; } /** * Prepares a multiline string for use in AppleScript. * * This method handles multiline text by splitting it into separate lines * and creating an AppleScript expression that concatenates these lines * with explicit 'return' statements between them. This approach avoids * syntax errors that occur when trying to directly include newlines in * AppleScript strings. * * @param str The multiline string to prepare * @returns An AppleScript-compatible string expression that preserves line breaks */ private prepareMultilineCommand(str: string): string { // Split the input by newlines and prepare each line separately const lines = str.split('\n'); // Create an AppleScript string that concatenates all lines with proper line breaks let applescriptString = '"' + this.escapeAppleScriptString(lines[0]) + '"'; for (let i = 1; i < lines.length; i++) { // For each subsequent line, use AppleScript's string concatenation with line feed // The 'return' keyword in AppleScript adds a newline character applescriptString += ' & return & "' + this.escapeAppleScriptString(lines[i]) + '"'; } return applescriptString; } /** * Escapes a single line of text for use in an AppleScript string. * * Handles special characters that would otherwise cause syntax errors * in AppleScript strings: * - Backslashes are doubled to avoid escape sequence interpretation * - Double quotes are escaped to avoid prematurely terminating the string * - Tabs are replaced with their escape sequence * * @param str The string to escape (should not contain newlines) * @returns The escaped string */ private escapeAppleScriptString(str: string): string { // Escape quotes and backslashes for AppleScript string return str .replace(/\\/g, '\\\\') // Double backslashes .replace(/"/g, '\\"') // Escape double quotes .replace(/\t/g, '\\t'); // Handle tabs } private async retrieveTtyPath(): Promise<string> { try { const { stdout } = await this._execPromise(`/usr/bin/osascript -e 'tell application "iTerm2" to tell current session of current window to get tty'`); return stdout.trim(); } catch (error: unknown) { throw new Error(`Failed to retrieve TTY path: ${(error as Error).message}`); } } private async isProcessing(): Promise<boolean> { try { const { stdout } = await this._execPromise(`/usr/bin/osascript -e 'tell application "iTerm2" to tell current session of current window to get is processing'`); return stdout.trim() === 'true'; } catch (error: unknown) { throw new Error(`Failed to check processing status: ${(error as Error).message}`); } } } - src/TtyOutputReader.ts:6-31 (helper)TtyOutputReader helper: retrieves terminal buffer contents via AppleScript (used before and after command execution to count output lines).
export default class TtyOutputReader { static async call(linesOfOutput?: number) { const buffer = await this.retrieveBuffer(); if (!linesOfOutput) { return buffer; } const lines = buffer.split('\n'); return lines.slice(-linesOfOutput - 1).join('\n'); } static async retrieveBuffer(): Promise<string> { const ascript = ` tell application "iTerm2" tell front window tell current session of current tab set numRows to number of rows set allContent to contents return allContent end tell end tell end tell `; const { stdout: finalContent } = await execPromise(`osascript -e '${ascript}'`); return finalContent.trim(); }