Skip to main content
Glama

diff_files

Compare two files to identify differences. Supports multiple output formats (unified, context, side-by-side, JSON) and optional whitespace ignoring and context adjustment.

Instructions

Compare two files and show differences

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
file1YesPath to the first file
file2YesPath to the second file
formatNoOutput format for the diffunified
contextNoNumber of context lines to show
ignoreWhitespaceNoIgnore whitespace differences

Implementation Reference

  • Primary handler implementation of the diff_files tool. Extends BaseCommand with name='diff_files', defines inputSchema, validates args, and executes via IDiffService.compareFiles(). Returns a JSON result with additions, deletions, changes, and diff content.
    import { BaseCommand } from '../../base/BaseCommand.js';
    import { CommandResult, CommandContext } from '../../../core/interfaces/ICommand.js';
    import { IDiffService } from '../../../core/interfaces/IDiffService.js';
    
    export class DiffFilesCommand extends BaseCommand {
      readonly name = 'diff_files';
      readonly description = 'Compare two files and show differences';
      readonly inputSchema = {
        type: 'object',
        properties: {
          file1: { 
            type: 'string', 
            description: 'Path to the first file' 
          },
          file2: { 
            type: 'string', 
            description: 'Path to the second file' 
          },
          format: { 
            type: 'string',
            enum: ['unified', 'context', 'side-by-side', 'json'],
            description: 'Output format for the diff',
            default: 'unified'
          },
          context: { 
            type: 'number', 
            description: 'Number of context lines to show',
            minimum: 0,
            default: 3 
          },
          ignoreWhitespace: { 
            type: 'boolean', 
            description: 'Ignore whitespace differences',
            default: false 
          }
        },
        required: ['file1', 'file2'],
        additionalProperties: false
      };
    
      protected validateArgs(args: Record<string, any>): void {
        this.assertString(args.file1, 'file1');
        this.assertString(args.file2, 'file2');
        if (args.format !== undefined) {
          this.assertString(args.format, 'format');
          const validFormats = ['unified', 'context', 'side-by-side', 'json'];
          if (!validFormats.includes(args.format)) {
            throw new Error(`Format must be one of: ${validFormats.join(', ')}`);
          }
        }
        if (args.context !== undefined) {
          this.assertNumber(args.context, 'context');
          if (args.context < 0) {
            throw new Error('Context must be a non-negative number');
          }
        }
        if (args.ignoreWhitespace !== undefined) {
          this.assertBoolean(args.ignoreWhitespace, 'ignoreWhitespace');
        }
      }
    
    
      protected async executeCommand(context: CommandContext): Promise<CommandResult> {
        try {
          const diffService = context.container.getService<IDiffService>('diffService');
          const diff = await diffService.compareFiles(
            context.args.file1,
            context.args.file2,
            {
              format: context.args.format,
              context: context.args.context,
              ignoreWhitespace: context.args.ignoreWhitespace
            }
          );
    
          return {
            content: [{
              type: 'text',
              text: JSON.stringify({
                file1: context.args.file1,
                file2: context.args.file2,
                format: context.args.format,
                additions: diff.additions,
                deletions: diff.deletions,
                changes: diff.changes,
                diff: diff.content
              }, null, 2)
            }]
          };
        } catch (error) {
          return {
            content: [{
              type: 'text',
              text: `Failed to compare files: ${error instanceof Error ? error.message : String(error)}`
            }]
          };
        }
      }
    }
  • Alternative/legacy handler for diff_files in the old Command pattern. Delegates to FileSystemManager.diffFiles() which calls DiffManager.
    export class DiffFilesCommand extends Command {
      readonly name = 'diff_files';
      readonly description = 'Compare two files and show differences';
      readonly inputSchema = {
        type: 'object' as const,
        properties: {
          file1: { type: 'string' as const, description: 'First file path' },
          file2: { type: 'string' as const, description: 'Second file path' },
          format: { 
            type: 'string' as const, 
            enum: ['unified', 'side-by-side', 'inline'],
            description: 'Diff output format' 
          }
        },
        required: ['file1', 'file2']
      };
    
      protected validateArgs(args: Record<string, any>): void {
        this.assertString(args.file1, 'file1');
        this.assertString(args.file2, 'file2');
        if (args.format !== undefined) {
          this.assertString(args.format, 'format');
          if (!['unified', 'side-by-side', 'inline'].includes(args.format)) {
            throw new Error(`Invalid format: ${args.format}. Must be one of: unified, side-by-side, inline`);
          }
        }
      }
    
      protected async executeCommand(context: CommandContext): Promise<CommandResult> {
        const fsManager = context.fsManager as FileSystemManager;
        return await fsManager.diffFiles(context.args.file1, context.args.file2, context.args.format);
      }
    }
  • Core diff engine. DiffManager.diffFiles() reads two files and computes line-by-line diff. Returns additions, deletions, formatted diff output, and patches array. Also supports patchFile() for applying patches.
    import { promises as fs } from 'fs';
    import * as path from 'path';
    
    interface DiffResult {
      additions: number;
      deletions: number;
      formatted: string;
      patches: DiffPatch[];
    }
    
    interface DiffPatch {
      start: number;
      end: number;
      type: 'addition' | 'deletion' | 'modification';
      content: string;
    }
    
    interface DiffOptions {
      format?: 'unified' | 'side-by-side' | 'inline';
      contextLines?: number;
      ignoreWhitespace?: boolean;
    }
    
    export class DiffManager {
      async diffFiles(file1: string, file2: string, options: DiffOptions = {}): Promise<DiffResult> {
        const content1 = await fs.readFile(file1, 'utf-8');
        const content2 = await fs.readFile(file2, 'utf-8');
        
        return this.diff(content1, content2, options);
      }
    
      diff(text1: string, text2: string, options: DiffOptions = {}): DiffResult {
        const lines1 = text1.split('\n');
        const lines2 = text2.split('\n');
        
        const patches: DiffPatch[] = [];
        let additions = 0;
        let deletions = 0;
        
        // Simple line-by-line diff algorithm
        const maxLines = Math.max(lines1.length, lines2.length);
        let formatted = '';
        
        for (let i = 0; i < maxLines; i++) {
          const line1 = lines1[i];
          const line2 = lines2[i];
          
          if (line1 === undefined && line2 !== undefined) {
            // Addition
            additions++;
            formatted += `+ ${line2}\n`;
            patches.push({
              start: i,
              end: i,
              type: 'addition',
              content: line2
            });
          } else if (line1 !== undefined && line2 === undefined) {
            // Deletion
            deletions++;
            formatted += `- ${line1}\n`;
            patches.push({
              start: i,
              end: i,
              type: 'deletion',
              content: line1
            });
          } else if (line1 !== line2) {
            // Modification
            deletions++;
            additions++;
            formatted += `- ${line1}\n`;
            formatted += `+ ${line2}\n`;
            patches.push({
              start: i,
              end: i,
              type: 'modification',
              content: `${line1} -> ${line2}`
            });
          } else {
            // No change
            if (options.format === 'unified') {
              formatted += `  ${line1}\n`;
            }
          }
        }
        
        return {
          additions,
          deletions,
          formatted,
          patches
        };
      }
    
      async patchFile(filePath: string, patches: DiffPatch[]): Promise<void> {
        const content = await fs.readFile(filePath, 'utf-8');
        const lines = content.split('\n');
        
        // Apply patches in reverse order to maintain line numbers
        patches.sort((a, b) => b.start - a.start);
        
        for (const patch of patches) {
          switch (patch.type) {
            case 'addition':
              lines.splice(patch.start, 0, patch.content);
              break;
            case 'deletion':
              lines.splice(patch.start, 1);
              break;
            case 'modification':
              lines[patch.start] = patch.content.split(' -> ')[1];
              break;
          }
        }
        
        await fs.writeFile(filePath, lines.join('\n'), 'utf-8');
      }
    }
  • FileSystemManager.diffFiles() bridge method used by the legacy MetadataCommands handler. Delegates to DiffManager and formats the result.
    async diffFiles(file1: string, file2: string, format: 'unified' | 'side-by-side' | 'inline' = 'unified'): Promise<{ content: [{ type: string; text: string }] }> {
      const result = await this.diffManager.diffFiles(file1, file2, { format });
      
      return {
        content: [{
          type: 'text',
          text: `Diff between ${file1} and ${file2}:\n\nAdditions: ${result.additions}\nDeletions: ${result.deletions}\n\n${result.formatted}`
        }]
      };
    }
  • IDiffService interface and DiffResult type used by the primary DiffFilesCommand handler (in the commands/ directory). Defines compareFiles() signature and options.
    export interface DiffResult {
      additions: number;
      deletions: number;
      changes: number;
      content: string;
    }
    
    export interface IDiffService {
      compareFiles(
        file1: string,
        file2: string,
        options?: {
          format?: 'unified' | 'context' | 'side-by-side' | 'json';
          context?: number;
          ignoreWhitespace?: boolean;
        }
      ): Promise<DiffResult>;
    }
Behavior2/5

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

With no annotations, the description must carry the burden of disclosing behavior. It only states that files are compared and differences shown, but does not explain error handling (e.g., missing files), the output format beyond schema parameters, or any side effects (none likely). The minimal description lacks important behavioral context.

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

Conciseness4/5

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

The description is a single sentence, very concise. It avoids unnecessary words or repetition. However, it is so brief that it may omit useful details, but for the purpose of conciseness, it earns a 4.

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

Completeness2/5

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

Given no output schema, the description should at least hint at what the tool returns (e.g., diff text). It does not. Also, with 5 parameters, the description is too sparse; it doesn't describe the quality of the diff (e.g., line-by-line, character-level) or any limitations. The tool is simple, but more context would help.

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?

The input schema has 100% description coverage, so the baseline is 3. The tool description does not add any extra meaning beyond what the schema already provides. For example, it does not explain that 'format' determines output style or that 'context' affects nearby lines. No additional value is added.

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?

The description 'Compare two files and show differences' clearly identifies the action and resource. It distinguishes this tool from siblings like read_file or execute_shell, as it specifically targets file comparison.

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?

No guidance is provided on when to use this tool versus alternatives. For instance, it does not mention that fuzzy_search could be used for non-exact matches or that git_diff might be more appropriate for version-controlled files. No exclusions or context are given.

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/proofmath-owner/ai-filesystem-mcp'

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