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

Method handleFileView

src/mcp/tools.ts:6125–6253  ·  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

6123 * through validatePathWithinRoot (#527).
6124 */
6125 private async handleFileView(
6126 cg: CodeGraph,
6127 fileArg: string,
6128 opts: { offset?: number; limit?: number; symbolsOnly?: boolean } = {},
6129 ): Promise<ToolResult> {
6130 const normalize = (p: string) => p.replace(/\\/g, '/').replace(/^(?:\.?\/+)+/, '').replace(/\/+$/, '');
6131 const wantLower = normalize(fileArg).toLowerCase();
6132 const allFiles = cg.getFiles();
6133 if (allFiles.length === 0) return this.textResult('No files indexed. Run `codegraph index` first.');
6134
6135 let resolved = allFiles.find((f) => f.path.toLowerCase() === wantLower);
6136 let candidates: typeof allFiles = [];
6137 if (!resolved) {
6138 candidates = allFiles.filter((f) => f.path.toLowerCase().endsWith('/' + wantLower));
6139 if (candidates.length === 1) resolved = candidates[0];
6140 }
6141 if (!resolved && candidates.length === 0) {
6142 candidates = allFiles.filter((f) => f.path.toLowerCase().includes(wantLower));
6143 if (candidates.length === 1) resolved = candidates[0];
6144 }
6145 if (!resolved && candidates.length > 1) {
6146 return this.textResult(
6147 [`"${fileArg}" matches ${candidates.length} indexed files — pass a longer path:`, '',
6148 ...candidates.slice(0, 25).map((f) => `- ${f.path}`)].join('\n'),
6149 );
6150 }
6151 if (!resolved) {
6152 return this.textResult(
6153 `No indexed file matches "${fileArg}". Codegraph indexes source files; configs/docs it doesn't parse won't appear — Read those directly.`,
6154 );
6155 }
6156
6157 const filePath = resolved.path;
6158 const nodes = cg.getNodesInFile(filePath)
6159 .filter((n) => n.kind !== 'file' && n.kind !== 'import' && n.kind !== 'export')
6160 .sort((a, b) => a.startLine - b.startLine);
6161 const dependents = cg.getFileDependents(filePath);
6162
6163 // Compact, one-line blast radius (codegraph's value-add over a plain Read).
6164 const depSummary = dependents.length
6165 ? `used by ${dependents.length} file${dependents.length === 1 ? '' : 's'}: ${dependents.slice(0, 8).join(', ')}${dependents.length > 8 ? `, +${dependents.length - 8} more` : ''}`
6166 : 'no other indexed file depends on it';
6167
6168 // Symbol-map renderer — for symbolsOnly, the config fallback, and read errors.
6169 const symbolMap = (heading: string, limit = 200): string[] => {
6170 const lines: string[] = [heading];
6171 for (const n of nodes.slice(0, limit)) {
6172 const sig = n.signature ? ` ${n.signature.replace(/\s+/g, ' ').trim()}` : '';
6173 lines.push(`- \`${n.name}\` (${n.kind})${sig} — :${n.startLine}`);
6174 }
6175 if (nodes.length > limit) lines.push(`- … +${nodes.length - limit} more`);
6176 return lines;
6177 };
6178
6179 // symbolsOnly → the cheap structural overview, no source.
6180 if (opts.symbolsOnly) {
6181 const out = [`**${filePath}** — ${nodes.length} symbol${nodes.length === 1 ? '' : 's'}, ${depSummary}`, ''];
6182 if (nodes.length) out.push(...symbolMap('**Symbols**'));

Callers 1

handleNodeMethod · 0.95

Calls 10

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

Tested by

no test coverage detected