MCPcopy Create free account
hub / github.com/colbymchenry/codegraph / handleFileView

Method handleFileView

src/mcp/tools.ts:4034–4162  ·  view source on GitHub ↗

* FILE READ MODE: resolve `fileArg` (path or basename) to an indexed file and * read it like the Read tool — its current on-disk source with line numbers, * narrowable with `offset`/`limit` exactly as Read's are — preceded by a * one-line blast-radius header (which files depend on it). `sym

(
    cg: CodeGraph,
    fileArg: string,
    opts: { offset?: number; limit?: number; symbolsOnly?: boolean } = {},
  )

Source from the content-addressed store, hash-verified

4032 * through validatePathWithinRoot (#527).
4033 */
4034 private async handleFileView(
4035 cg: CodeGraph,
4036 fileArg: string,
4037 opts: { offset?: number; limit?: number; symbolsOnly?: boolean } = {},
4038 ): Promise<ToolResult> {
4039 const normalize = (p: string) => p.replace(/\\/g, '/').replace(/^(?:\.?\/+)+/, '').replace(/\/+$/, '');
4040 const wantLower = normalize(fileArg).toLowerCase();
4041 const allFiles = cg.getFiles();
4042 if (allFiles.length === 0) return this.textResult('No files indexed. Run `codegraph index` first.');
4043
4044 let resolved = allFiles.find((f) => f.path.toLowerCase() === wantLower);
4045 let candidates: typeof allFiles = [];
4046 if (!resolved) {
4047 candidates = allFiles.filter((f) => f.path.toLowerCase().endsWith('/' + wantLower));
4048 if (candidates.length === 1) resolved = candidates[0];
4049 }
4050 if (!resolved && candidates.length === 0) {
4051 candidates = allFiles.filter((f) => f.path.toLowerCase().includes(wantLower));
4052 if (candidates.length === 1) resolved = candidates[0];
4053 }
4054 if (!resolved && candidates.length > 1) {
4055 return this.textResult(
4056 [`"${fileArg}" matches ${candidates.length} indexed files — pass a longer path:`, '',
4057 ...candidates.slice(0, 25).map((f) => `- ${f.path}`)].join('\n'),
4058 );
4059 }
4060 if (!resolved) {
4061 return this.textResult(
4062 `No indexed file matches "${fileArg}". Codegraph indexes source files; configs/docs it doesn't parse won't appear — Read those directly.`,
4063 );
4064 }
4065
4066 const filePath = resolved.path;
4067 const nodes = cg.getNodesInFile(filePath)
4068 .filter((n) => n.kind !== 'file' && n.kind !== 'import' && n.kind !== 'export')
4069 .sort((a, b) => a.startLine - b.startLine);
4070 const dependents = cg.getFileDependents(filePath);
4071
4072 // Compact, one-line blast radius (codegraph's value-add over a plain Read).
4073 const depSummary = dependents.length
4074 ? `used by ${dependents.length} file${dependents.length === 1 ? '' : 's'}: ${dependents.slice(0, 8).join(', ')}${dependents.length > 8 ? `, +${dependents.length - 8} more` : ''}`
4075 : 'no other indexed file depends on it';
4076
4077 // Symbol-map renderer — for symbolsOnly, the config fallback, and read errors.
4078 const symbolMap = (heading: string, limit = 200): string[] => {
4079 const lines: string[] = [heading];
4080 for (const n of nodes.slice(0, limit)) {
4081 const sig = n.signature ? ` ${n.signature.replace(/\s+/g, ' ').trim()}` : '';
4082 lines.push(`- \`${n.name}\` (${n.kind})${sig} — :${n.startLine}`);
4083 }
4084 if (nodes.length > limit) lines.push(`- … +${nodes.length - limit} more`);
4085 return lines;
4086 };
4087
4088 // symbolsOnly → the cheap structural overview, no source.
4089 if (opts.symbolsOnly) {
4090 const out = [`**${filePath}** — ${nodes.length} symbol${nodes.length === 1 ? '' : 's'}, ${depSummary}`, ''];
4091 if (nodes.length) out.push(...symbolMap('**Symbols**'));

Callers 1

handleNodeMethod · 0.95

Calls 10

textResultMethod · 0.95
truncateOutputMethod · 0.95
validatePathWithinRootFunction · 0.90
normalizeFunction · 0.85
getFilesMethod · 0.80
hasMethod · 0.80
getNodesInFileMethod · 0.65
getProjectRootMethod · 0.65
joinMethod · 0.45
getFileDependentsMethod · 0.45

Tested by

no test coverage detected