Skip to main content
Glama
ferrislucas

iTerm MCP Server

by ferrislucas

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

TableJSON Schema
NameRequiredDescriptionDefault
commandYesThe 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"]
      }
    },
  • 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.`
        }]
      };
    }
  • 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"]
    }
  • 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}`);
        }
      }
    }
  • 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();
      }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations, and description lacks behavioral details such as whether the command waits for completion, effect on terminal state, or authentication needs. Critical for a command execution tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

One succinct sentence with no unnecessary information. Front-loaded with core action.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Adequate for a simple tool, but lacks deeper context about execution behavior (synchronous? interactive?). Without annotations, description could do more to clarify usage.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Single parameter 'command' with schema description 'The command to run or text to write to the terminal'. Description adds marginal value beyond schema, but schema coverage is 100%.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clearly states it writes text to the active iTerm terminal, often to run a command. Distinguishes from siblings (read_terminal_output and send_control_character) by its write action.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Implies use for running commands and writing text. Context with siblings suggests when not to use (reading output or sending control characters), but no explicit exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/ferrislucas/iterm-mcp'

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