diff_files
Compare two files to generate a unified diff showing differences. Use the output to apply patches or verify file changes.
Instructions
Generate a unified diff between two files. Output feeds directly into apply_patch. isIdentical=true means files match — no patch needed.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| original | Yes | Path to original file | |
| modified | Yes | Path to modified file | |
| context | No | Lines of context to include in the diff | |
| ignoreWhitespace | No | Ignore leading/trailing whitespace when comparing lines | |
| stripTrailingCr | No | Strip trailing carriage returns before diffing |
Implementation Reference
- src/tools/diff-files.ts:77-168 (handler)The 'handleDiffFiles' function executes the core logic for the 'diff_files' tool, including reading files and computing the diff using the 'diff' library.
async function handleDiffFiles( args: z.infer<typeof DiffFilesInputSchema>, signal?: AbortSignal, resourceStore?: ToolRegistrationOptions['resourceStore'] ): Promise<ToolResponse<z.infer<typeof DiffFilesOutputSchema>>> { const maxFileSize = MAX_TEXT_FILE_SIZE; const [originalPath, modifiedPath] = await Promise.all([ validateExistingPath(args.original, signal), validateExistingPath(args.modified, signal), ]); const [originalStats, modifiedStats] = await Promise.all([ withAbort(fs.stat(originalPath), signal), withAbort(fs.stat(modifiedPath), signal), ]); assertDiffFileSizeWithinLimit(originalPath, originalStats.size, maxFileSize); assertDiffFileSizeWithinLimit(modifiedPath, modifiedStats.size, maxFileSize); const [originalContent, modifiedContent] = await Promise.all([ fs.readFile(originalPath, { encoding: 'utf-8', signal }), fs.readFile(modifiedPath, { encoding: 'utf-8', signal }), ]); const patchObj = await new Promise<StructuredPatch | undefined>((resolve) => { structuredPatch( path.basename(originalPath), path.basename(modifiedPath), originalContent, modifiedContent, undefined, undefined, { ...(args.context !== undefined ? { context: args.context } : {}), ignoreWhitespace: args.ignoreWhitespace, stripTrailingCr: args.stripTrailingCr, timeout: 10000, callback: (res) => { resolve(res); }, } ); }); if (!patchObj) { throw new McpError( ErrorCode.E_TIMEOUT, `Diff computation timed out or failed due to complexity.`, originalPath ); } const isIdentical = patchObj.hunks.length === 0; const diffText = isIdentical ? '' : formatPatch(patchObj); const stats = isIdentical ? undefined : computeDiffStats(patchObj.hunks); const externalized = maybeExternalizeTextContent(resourceStore, diffText, { name: 'diff:patch', mimeType: 'text/x-diff', }); if (!externalized) { return buildToolResponse(isIdentical ? 'No differences' : diffText, { ok: true, diff: diffText, isIdentical, ...(stats ?? {}), }); } const { preview, entry } = externalized; return buildToolResponse( preview, { ok: true, diff: preview, isIdentical, ...(stats ?? {}), truncated: true, resourceUri: entry.uri, }, [ buildResourceLink({ uri: entry.uri, name: entry.name, mimeType: entry.mimeType, description: 'Full diff content', expiresAt: entry.expiresAt, }), ] ); } - src/tools/diff-files.ts:170-231 (registration)The 'registerDiffFilesTool' function registers the 'diff_files' tool with the McpServer, wrapping the handler with validation and telemetry/progress reporting.
export function registerDiffFilesTool( server: McpServer, options: ToolRegistrationOptions = {} ): void { const handler = ( args: z.infer<typeof DiffFilesInputSchema>, extra: ToolExtra ): Promise<ToolResult<z.infer<typeof DiffFilesOutputSchema>>> => executeToolWithDiagnostics({ toolName: 'diff_files', extra, outputSchema: DiffFilesOutputSchema, timedSignal: {}, context: { path: args.original }, run: (signal) => handleDiffFiles(args, signal, options.resourceStore), onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.original), }); const wrappedHandler = wrapToolHandler(handler, { guard: options.isInitialized, progressMessage: (args) => { const n1 = path.basename(args.original); const n2 = path.basename(args.modified); return `🕮 diff: ${n1} ⟷ ${n2}`; }, completionMessage: (args, result) => { const n1 = path.basename(args.original); const n2 = path.basename(args.modified); if (result.isError) return `🕮 diff: ${n1} ⟷ ${n2} • failed`; const sc = result.structuredContent; if (sc.isIdentical) return `🕮 diff: ${n1} ⟷ ${n2} • identical`; const added = sc.linesAdded ?? 0; const removed = sc.linesRemoved ?? 0; if (added > 0 || removed > 0) return `🕮 diff: ${n1} ⟷ ${n2} • +${added} -${removed}`; return `🕮 diff: ${n1} ⟷ ${n2}`; }, }); const validatedHandler = withValidatedArgs( DiffFilesInputSchema, wrappedHandler ); if ( registerToolTaskIfAvailable( server, 'diff_files', DIFF_FILES_TOOL, validatedHandler, options.iconInfo, options.isInitialized ) ) return; server.registerTool( 'diff_files', withDefaultIcons({ ...DIFF_FILES_TOOL }, options.iconInfo), validatedHandler ); } - src/schemas.ts:757-776 (schema)The 'DiffFilesInputSchema' defines the expected input parameters for the 'diff_files' tool, requiring 'original' and 'modified' file paths.
export const DiffFilesInputSchema = z.strictObject({ original: RequiredPathSchema.describe('Path to original file'), modified: RequiredPathSchema.describe('Path to modified file'), context: z .int({ error: 'Must be integer' }) .min(0, 'Min: 0') .max(10000, 'Max: 10,000') .optional() .describe('Lines of context to include in the diff'), ignoreWhitespace: z .boolean() .optional() .default(false) .describe('Ignore leading/trailing whitespace when comparing lines'), stripTrailingCr: z .boolean() .optional() .default(false) .describe('Strip trailing carriage returns before diffing'), });