analyze_file
Send a file path and a question to analyze a file server-side. The file never enters your context window; returns analysis with metadata and token usage.
Instructions
Offload file analysis to a worker model. The file is read server-side — it never enters your context window. You send a file path and a question, and get back only the analysis.
OUTPUT: Markdown with the model's analysis of the file, including file metadata (path, lines, chars), latency, and token usage. If max_response_tokens is set and compression occurred, includes distillation metadata (original tokens, compressed tokens, compressor model, compressor latency).
WHEN TO USE: When you need to analyze, review, or search a file but want to avoid reading it yourself. Especially valuable for large files (1000+ lines) where reading would consume significant context. The file is sent to a large-context model (Gemini 1M) that can process the entire file at once.
FAILURE MODES:
"File not found" → The path is wrong. Retry with the correct absolute path.
"Binary file detected" → Only text files are supported. Do not retry with this file.
"File too large" → The file exceeds 800K chars. Try analyzing a specific section or ask the user to split the file.
"No models available" → CLIProxyAPI or Ollama is not running. Tell the user to start their model provider.
"Model query failed" → Try a different model or check provider status with list_models.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| file_path | Yes | Absolute path to the file to analyze. The file is read server-side — it never enters your context window. | |
| prompt | Yes | What to analyze, find, or review in the file. Be specific for better results. | |
| model | No | Model to use for analysis. Auto-picks a large-context model (Gemini 1M) if omitted. | |
| max_response_tokens | No | Maximum tokens in the response returned to you. If the model's response exceeds this, it will be distilled by a fast model to fit — preserving code, file paths, errors, and actionable details while stripping filler. Omit for no compression. | |
| max_tokens | No | Maximum tokens the analysis model generates (default: 1024) | |
| format | No | Response format — 'brief' for token-efficient summary, 'detailed' for full response | detailed |
| include_raw | No | When true and compression is active, include the original uncompressed response for quality comparison. Use this to verify distillation preserved critical details. |
Implementation Reference
- src/tools/analyze-file.ts:116-241 (handler)The main handler function `analyzeFile` that executes the tool logic. Reads the file server-side, validates it (exists, not binary, not too large), picks a large-context model, builds an analysis prompt with the full file content, queries the model, optionally compresses the response, and formats the result.
export async function analyzeFile( provider: Provider, input: AnalyzeFileInput ): Promise<string> { const startTime = Date.now(); // Step 1: Validate file exists if (!existsSync(input.file_path)) { return ( `## Analysis Failed\n\n` + `File not found: \`${input.file_path}\`\n\n` + `**Recovery:** Check the file path. Use an absolute path.` ); } // Step 2: Check for binary files if (isBinaryFile(input.file_path)) { return ( `## Analysis Failed\n\n` + `Binary file detected: \`${input.file_path}\`\n\n` + `**Recovery:** This appears to be a binary file. Only text files are supported.` ); } // Step 3: Read the file server-side let fileContent: string; try { fileContent = readFileSync(input.file_path, "utf-8"); } catch (err) { return ( `## Analysis Failed\n\n` + `Could not read file: \`${input.file_path}\`\n` + `Error: ${err instanceof Error ? err.message : String(err)}\n\n` + `**Recovery:** Check file permissions and ensure the path is correct.` ); } // Step 4: Check file size if (fileContent.length > MAX_FILE_CHARS) { const sizeMB = (fileContent.length / 1_000_000).toFixed(1); return ( `## Analysis Failed\n\n` + `File too large: \`${input.file_path}\` (${sizeMB}M chars, limit: ${MAX_FILE_CHARS / 1_000_000}M)\n\n` + `**Recovery:** The file exceeds the 800K character limit. ` + `Try analyzing a specific section, or split the file and analyze parts individually.` ); } const fileName = basename(input.file_path); const fileChars = fileContent.length; const fileLines = fileContent.split("\n").length; logger.info( `analyze_file: ${fileName} (${fileLines} lines, ${fileChars} chars)` ); // Step 5: Pick a large-context model const model = await pickLargeContextModel(provider, input.model); if (!model) { return ( `## Analysis Failed\n\n` + `No models available for file analysis.\n\n` + `**Recovery:** The user needs to start a model provider. ` + `Tell them to start CLIProxyAPI or Ollama, then retry. ` + `You can verify provider status by calling list_models first.` ); } logger.info(`analyze_file: using model ${model}`); // Step 6: Build prompt with file content const analysisPrompt = `You are analyzing a file. Answer the user's question about this file. Be specific — include line numbers, function names, variable names, and exact code when relevant. File: ${input.file_path} Lines: ${fileLines} Characters: ${fileChars} \`\`\` ${fileContent} \`\`\` Question: ${input.prompt}`; // Step 7: Query the model let response: QueryResponse; try { response = await provider.query(model, analysisPrompt, { temperature: 0.2, max_tokens: input.max_tokens, }); } catch (err) { return ( `## Analysis Failed\n\n` + `Model query failed: ${err instanceof Error ? err.message : String(err)}\n\n` + `**Recovery:** Try a different model or check provider status with list_models.` ); } // Step 8: Compress if requested let compression: CompressionResult | undefined; if (input.max_response_tokens) { compression = await compressResponse( provider, response, input.max_response_tokens ); } const totalMs = Date.now() - startTime; // Step 9: Format response return formatResponse( response, input.format ?? "detailed", compression, { fileName, filePath: input.file_path, fileLines, fileChars, totalMs, }, input.include_raw ?? false ); } - src/tools/analyze-file.ts:28-77 (schema)Zod schema `analyzeFileSchema` defining the tool's input parameters: file_path (string), prompt (string), model (optional string), max_response_tokens (optional number), max_tokens (optional number, default 1024), format (optional 'brief' | 'detailed', default 'detailed'), and include_raw (optional boolean, default false).
export const analyzeFileSchema = z.object({ file_path: z .string() .describe( "Absolute path to the file to analyze. The file is read server-side — it never enters your context window." ), prompt: z .string() .describe( "What to analyze, find, or review in the file. Be specific for better results." ), model: z .string() .optional() .describe( "Model to use for analysis. Auto-picks a large-context model (Gemini 1M) if omitted." ), max_response_tokens: z .number() .int() .positive() .optional() .describe( "Maximum tokens in the response returned to you. If the model's response exceeds this, " + "it will be distilled by a fast model to fit — preserving code, file paths, errors, " + "and actionable details while stripping filler. Omit for no compression." ), max_tokens: z .number() .int() .positive() .optional() .default(1024) .describe("Maximum tokens the analysis model generates (default: 1024)"), format: z .enum(["brief", "detailed"]) .optional() .default("detailed") .describe( "Response format — 'brief' for token-efficient summary, 'detailed' for full response" ), include_raw: z .boolean() .optional() .default(false) .describe( "When true and compression is active, include the original uncompressed response " + "for quality comparison. Use this to verify distillation preserved critical details." ), }); - src/server.ts:223-253 (registration)Registration of the `analyze_file` tool via `server.tool()` on the MCP server. Registers the tool name 'analyze_file', its description/handler, attaches the schema's shape, and wires the handler to call `analyzeFile(provider, input)`.
// --- analyze_file --- server.tool( "analyze_file", `Offload file analysis to a worker model. The file is read server-side — it never enters your context window. You send a file path and a question, and get back only the analysis. OUTPUT: Markdown with the model's analysis of the file, including file metadata (path, lines, chars), latency, and token usage. If max_response_tokens is set and compression occurred, includes distillation metadata (original tokens, compressed tokens, compressor model, compressor latency). WHEN TO USE: When you need to analyze, review, or search a file but want to avoid reading it yourself. Especially valuable for large files (1000+ lines) where reading would consume significant context. The file is sent to a large-context model (Gemini 1M) that can process the entire file at once. FAILURE MODES: - "File not found" → The path is wrong. Retry with the correct absolute path. - "Binary file detected" → Only text files are supported. Do not retry with this file. - "File too large" → The file exceeds 800K chars. Try analyzing a specific section or ask the user to split the file. - "No models available" → CLIProxyAPI or Ollama is not running. Tell the user to start their model provider. - "Model query failed" → Try a different model or check provider status with list_models.`, analyzeFileSchema.shape, async (input) => { logger.info(`analyze_file: ${input.file_path}`); try { const result = await analyzeFile(provider, input); return { content: [{ type: "text" as const, text: result }] }; } catch (err) { const message = err instanceof Error ? err.message : String(err); logger.error(`analyze_file failed: ${message}`); return { content: [{ type: "text" as const, text: `Error: ${message}` }], isError: true, }; } } ); - src/tools/analyze-file.ts:255-328 (helper)The `formatResponse` helper function that formats the model's analysis response in 'brief' or 'detailed' markdown, including file metadata, model info, latency, token usage, context savings metrics, and optional compression/distillation details.
function formatResponse( response: QueryResponse, format: "brief" | "detailed", compression: CompressionResult | undefined, meta: FileMetadata, includeRaw: boolean ): string { const content = compression?.content ?? response.content; // Calculate context savings: tokens Claude would have burned reading the file const fileTokensEstimate = Math.ceil(meta.fileChars / 4); const responseTokens = compression?.compressedTokens ?? response.usage?.completion_tokens ?? Math.ceil(content.length / 4); const contextSaved = fileTokensEstimate - responseTokens; if (format === "brief") { const lines = [ `**${meta.fileName}** → ${response.model} (${meta.totalMs}ms)`, "", content, "", `*Context saved: ~${contextSaved.toLocaleString()} tokens*`, ]; if (compression?.compressed) { const saved = (compression.originalTokens ?? 0) - (compression.compressedTokens ?? 0); lines.push( `*Distilled by ${compression.compressorModel} — saved additional ${saved} tokens*` ); } return lines.join("\n"); } // Detailed format const lines = [ `## File Analysis: ${meta.fileName}`, "", content, "", "---", `**File:** \`${meta.filePath}\` (${meta.fileLines} lines, ${meta.fileChars} chars)`, `**Model:** ${response.model} | **Latency:** ${response.latency_ms}ms | **Total:** ${meta.totalMs}ms`, `**Context saved:** ~${contextSaved.toLocaleString()} tokens (Claude didn't read ${meta.fileChars.toLocaleString()} chars)`, ]; if (response.usage) { lines.push( `**Tokens:** ${response.usage.prompt_tokens} in → ${response.usage.completion_tokens} out (${response.usage.total_tokens} total)` ); } if (compression?.compressed) { const orig = compression.originalTokens ?? 0; const comp = compression.compressedTokens ?? 0; const saved = orig - comp; const pct = orig > 0 ? Math.round((saved / orig) * 100) : 0; lines.push( `**Distilled:** ${orig} → ${comp} tokens by ${compression.compressorModel} (${compression.compressorLatency}ms)` ); lines.push(`**Saved:** ${saved} tokens (${pct}% smaller)`); } // Escape hatch: include raw uncompressed response if (includeRaw && compression?.compressed && compression.rawContent) { lines.push(""); lines.push( `<details>\n<summary>Raw response (${compression.originalTokens ?? "?"} tokens, before distillation)</summary>\n\n${compression.rawContent}\n\n</details>` ); } return lines.join("\n"); } - src/tools/analyze-file.ts:95-110 (helper)The `isBinaryFile` helper function that checks for null bytes in the first 8KB of a file to detect binary files, returning true if binary content is detected.
function isBinaryFile(filePath: string): boolean { try { const fd = require("node:fs").openSync(filePath, "r"); const buffer = Buffer.alloc(BINARY_CHECK_BYTES); const bytesRead = require("node:fs").readSync(fd, buffer, 0, BINARY_CHECK_BYTES, 0); require("node:fs").closeSync(fd); // Check for null bytes — strong indicator of binary content for (let i = 0; i < bytesRead; i++) { if (buffer[i] === 0) return true; } return false; } catch { return false; } }