Skip to main content
Glama
bhouston

mcp-server-text-editor

by bhouston

text_editor

Manage and modify files with persistent state using commands like view, create, str_replace, insert, and undo_edit. Access or edit specific file lines and content across sessions with precise control.

Instructions

View, create, and edit files with persistent state across command calls. This tool is identical with Claude's built in text editor tool called text_editor_20241022

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
commandYesThe commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`.
descriptionYesThe reason you are using the text editor (max 80 chars)
file_textNoRequired parameter of `create` command, with the content of the file to be created.
insert_lineNoRequired parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`.
new_strNoOptional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert.
old_strNoRequired parameter of `str_replace` command containing the string in `path` to replace.
pathYesAbsolute path to file or directory, e.g. `/repo/file.py` or `/repo`.
view_rangeNoOptional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.

Implementation Reference

  • src/index.ts:14-19 (registration)
    Registration of the 'text_editor' tool in the MCP server using server.tool(), providing name, description, schema (toolParameters), and handler (textEditorExecute).
    server.tool(
      'text_editor',
      "View, create, and edit files with persistent state across command calls.  This tool is identical with Claude's built in text editor tool called text_editor_20241022",
      toolParameters,
      textEditorExecute,
    );
  • Zod schema definition for the text_editor tool input parameters, including commands like view, create, str_replace, insert, undo_edit.
    export const toolParameters = {
      command: z
        .enum(['view', 'create', 'str_replace', 'insert', 'undo_edit'])
        .describe(
          'The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`.',
        ),
      path: z
        .string()
        .describe(
          'Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.',
        ),
      file_text: z
        .string()
        .optional()
        .describe(
          'Required parameter of `create` command, with the content of the file to be created.',
        ),
      insert_line: z
        .number()
        .optional()
        .describe(
          'Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`.',
        ),
      new_str: z
        .string()
        .optional()
        .describe(
          'Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert.',
        ),
      old_str: z
        .string()
        .optional()
        .describe(
          'Required parameter of `str_replace` command containing the string in `path` to replace.',
        ),
      view_range: z
        .array(z.number())
        .optional()
        .describe(
          'Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.',
        ),
      description: z
        .string()
        .describe('The reason you are using the text editor (max 80 chars)'),
    };
  • Exported handler function textEditorExecute that wraps textEditorInternal and formats the response as ContentResponse for the MCP server.
    export const textEditorExecute = async (
      parameters: Parameters,
    ): Promise<ContentResponse> => {
      try {
        const result = await textEditorInternal(parameters);
        return buildContentResponse(result);
      } catch (error) {
        return buildContentResponse({
          success: false,
          message: error instanceof Error ? error.message : 'Unknown error',
          content: 'undefined',
        });
      }
    };
  • Core implementation of the text_editor tool logic in textEditorInternal, handling all commands: view (file/dir), create, str_replace, insert, undo_edit with persistent history.
    const textEditorInternal = async ({
      command,
      path: filePath,
      file_text,
      insert_line,
      new_str,
      old_str,
      view_range,
    }: Parameters): Promise<ReturnType> => {
      if (!path.isAbsolute(filePath)) {
        throw new Error('Path must be absolute');
      }
    
      switch (command) {
        case 'view': {
          // Check if path is a directory
          const stats = await fs.stat(filePath).catch(() => null);
          if (!stats) {
            throw new Error(`File or directory not found: ${filePath}`);
          }
    
          if (stats.isDirectory()) {
            // List directory contents up to 2 levels deep
            try {
              const output = execSync(
                `find "${filePath}" -type f -not -path "*/\\.*" -maxdepth 2 | sort`,
                { encoding: 'utf8' },
              );
              return {
                success: true,
                message: `Directory listing for ${filePath}:`,
                content: output,
              };
            } catch (error) {
              throw new Error(`Error listing directory: ${error}`);
            }
          } else {
            // Read file content
            const content = await fs.readFile(filePath, 'utf8');
            const lines = content.split('\n');
    
            // Apply view range if specified
            let displayContent = content;
            if (view_range && view_range.length === 2) {
              const [start, end] = view_range;
              const startLine = Math.max(1, start || 1) - 1; // Convert to 0-indexed
              const endLine = end === -1 ? lines.length : end;
              displayContent = lines.slice(startLine, endLine).join('\n');
            }
    
            // Add line numbers
            const startLineNum =
              view_range && view_range.length === 2 ? view_range[0] : 1;
            const numberedContent = displayContent
              .split('\n')
              .map((line, i) => `${(startLineNum || 1) + i}: ${line}`)
              .join('\n');
    
            // Truncate if too large
            if (numberedContent.length > OUTPUT_LIMIT) {
              const truncatedContent = numberedContent.substring(0, OUTPUT_LIMIT);
              return {
                success: true,
                message: `File content (truncated):`,
                content: `${truncatedContent}\n<response clipped>`,
              };
            }
    
            return {
              success: true,
              message: `File content:`,
              content: numberedContent,
            };
          }
        }
    
        case 'create': {
          if (!file_text) {
            throw new Error('file_text parameter is required for create command');
          }
    
          // Create parent directories if they don't exist
          await fs.mkdir(path.dirname(filePath), { recursive: true });
    
          // Check if file already exists
          const fileExists = fsSync.existsSync(filePath);
    
          if (fileExists) {
            // Save current state for undo if file exists
            const currentContent = await fs.readFile(filePath, 'utf8');
            if (!fileStateHistory[filePath]) {
              fileStateHistory[filePath] = [];
            }
            fileStateHistory[filePath].push(currentContent);
          } else {
            // Initialize history for new files
            fileStateHistory[filePath] = [];
          }
    
          // Create or overwrite the file
          await fs.writeFile(filePath, file_text, 'utf8');
    
          return {
            success: true,
            message: fileExists
              ? `File overwritten: ${filePath}`
              : `File created: ${filePath}`,
          };
        }
    
        case 'str_replace': {
          if (!old_str) {
            throw new Error(
              'old_str parameter is required for str_replace command',
            );
          }
    
          // Ensure the file exists
          if (!fsSync.existsSync(filePath)) {
            throw new Error(`File not found: ${filePath}`);
          }
    
          // Read the current content
          const content = await fs.readFile(filePath, 'utf8');
    
          // Check if old_str exists uniquely in the file
          const occurrences = content.split(old_str).length - 1;
          if (occurrences === 0) {
            throw new Error(`The specified old_str was not found in the file`);
          }
          if (occurrences > 1) {
            throw new Error(
              `Found ${occurrences} occurrences of old_str, expected exactly 1`,
            );
          }
    
          // Save current state for undo
          if (!fileStateHistory[filePath]) {
            fileStateHistory[filePath] = [];
          }
          fileStateHistory[filePath].push(content);
    
          // Replace the content
          const updatedContent = content.replace(old_str, new_str || '');
          await fs.writeFile(filePath, updatedContent, 'utf8');
    
          return {
            success: true,
            message: `Successfully replaced text in ${filePath}`,
          };
        }
    
        case 'insert': {
          if (insert_line === undefined) {
            throw new Error('insert_line parameter is required for insert command');
          }
    
          if (!new_str) {
            throw new Error('new_str parameter is required for insert command');
          }
    
          // Ensure the file exists
          if (!fsSync.existsSync(filePath)) {
            throw new Error(`File not found: ${filePath}`);
          }
    
          // Read the current content
          const content = await fs.readFile(filePath, 'utf8');
          const lines = content.split('\n');
    
          // Validate line number
          if (insert_line < 0 || insert_line > lines.length) {
            throw new Error(
              `Invalid line number: ${insert_line}. File has ${lines.length} lines.`,
            );
          }
    
          // Save current state for undo
          if (!fileStateHistory[filePath]) {
            fileStateHistory[filePath] = [];
          }
          fileStateHistory[filePath].push(content);
    
          // Insert the new content after the specified line
          lines.splice(insert_line, 0, new_str);
          const updatedContent = lines.join('\n');
          await fs.writeFile(filePath, updatedContent, 'utf8');
    
          return {
            success: true,
            message: `Successfully inserted text after line ${insert_line} in ${filePath}`,
          };
        }
    
        case 'undo_edit': {
          // Check if we have history for this file
          if (
            !fileStateHistory[filePath] ||
            fileStateHistory[filePath].length === 0
          ) {
            throw new Error(`No edit history found for ${filePath}`);
          }
    
          // Get the previous state
          const previousState = fileStateHistory[filePath].pop();
          await fs.writeFile(filePath, previousState as string, 'utf8');
    
          return {
            success: true,
            message: `Successfully reverted last edit to ${filePath}`,
          };
        }
    
        default:
          throw new Error(`Unknown command: ${command}`);
      }
    };
  • Global state for maintaining file edit history to enable undo functionality across tool calls.
    // Store file states for undo functionality
    const fileStateHistory: Record<string, string[]> = {};
Behavior3/5

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

With no annotations provided, the description carries the full burden. It mentions 'persistent state across command calls,' which is a useful behavioral trait not evident from the schema. However, it lacks details on permissions, error handling, file system interactions, or what 'persistent state' entails operationally. The description adds some value but leaves significant behavioral aspects unexplained.

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

Conciseness3/5

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

The description is two sentences: the first is front-loaded with core functionality, but the second sentence about being identical to another tool adds no value for tool selection and wastes space. It could be more concise by omitting the redundant second sentence.

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?

Given the complexity (8 parameters, no annotations, no output schema), the description is somewhat incomplete. It covers basic purpose and a behavioral trait (persistent state), but lacks details on usage context, error cases, or output expectations. For a multi-command tool with no output schema, more guidance would be helpful.

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?

Schema description coverage is 100%, so the schema already documents all parameters thoroughly. The description adds no parameter-specific information beyond what's in the schema. According to the rules, with high schema coverage, the baseline is 3 even with no param info in the description.

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

Purpose4/5

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

The description clearly states the tool's purpose: 'View, create, and edit files with persistent state across command calls.' It specifies the actions (view, create, edit) and resource (files), and mentions the persistent state feature. However, it doesn't distinguish from siblings since there are none, and the second sentence about being identical to another tool adds no functional clarity.

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

Usage Guidelines2/5

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

The description provides no guidance on when to use this tool versus alternatives. It mentions it's 'identical with Claude's built in text editor tool,' but this doesn't help an agent decide between this and other file manipulation tools. There are no explicit when/when-not scenarios or prerequisites described.

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

Related 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/bhouston/mcp-server-text-editor'

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