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

Method handleFileView

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

3863 * through validatePathWithinRoot (#527).
3864 */
3865 private async handleFileView(
3866 cg: CodeGraph,
3867 fileArg: string,
3868 opts: { offset?: number; limit?: number; symbolsOnly?: boolean } = {},
3869 ): Promise<ToolResult> {
3870 const normalize = (p: string) => p.replace(/\\/g, '/').replace(/^(?:\.?\/+)+/, '').replace(/\/+$/, '');
3871 const wantLower = normalize(fileArg).toLowerCase();
3872 const allFiles = cg.getFiles();
3873 if (allFiles.length === 0) return this.textResult('No files indexed. Run `codegraph index` first.');
3874
3875 let resolved = allFiles.find((f) => f.path.toLowerCase() === wantLower);
3876 let candidates: typeof allFiles = [];
3877 if (!resolved) {
3878 candidates = allFiles.filter((f) => f.path.toLowerCase().endsWith('/' + wantLower));
3879 if (candidates.length === 1) resolved = candidates[0];
3880 }
3881 if (!resolved && candidates.length === 0) {
3882 candidates = allFiles.filter((f) => f.path.toLowerCase().includes(wantLower));
3883 if (candidates.length === 1) resolved = candidates[0];
3884 }
3885 if (!resolved && candidates.length > 1) {
3886 return this.textResult(
3887 [`"${fileArg}" matches ${candidates.length} indexed files — pass a longer path:`, '',
3888 ...candidates.slice(0, 25).map((f) => `- ${f.path}`)].join('\n'),
3889 );
3890 }
3891 if (!resolved) {
3892 return this.textResult(
3893 `No indexed file matches "${fileArg}". Codegraph indexes source files; configs/docs it doesn't parse won't appear — Read those directly.`,
3894 );
3895 }
3896
3897 const filePath = resolved.path;
3898 const nodes = cg.getNodesInFile(filePath)
3899 .filter((n) => n.kind !== 'file' && n.kind !== 'import' && n.kind !== 'export')
3900 .sort((a, b) => a.startLine - b.startLine);
3901 const dependents = cg.getFileDependents(filePath);
3902
3903 // Compact, one-line blast radius (codegraph's value-add over a plain Read).
3904 const depSummary = dependents.length
3905 ? `used by ${dependents.length} file${dependents.length === 1 ? '' : 's'}: ${dependents.slice(0, 8).join(', ')}${dependents.length > 8 ? `, +${dependents.length - 8} more` : ''}`
3906 : 'no other indexed file depends on it';
3907
3908 // Symbol-map renderer — for symbolsOnly, the config fallback, and read errors.
3909 const symbolMap = (heading: string, limit = 200): string[] => {
3910 const lines: string[] = [heading];
3911 for (const n of nodes.slice(0, limit)) {
3912 const sig = n.signature ? ` ${n.signature.replace(/\s+/g, ' ').trim()}` : '';
3913 lines.push(`- \`${n.name}\` (${n.kind})${sig} — :${n.startLine}`);
3914 }
3915 if (nodes.length > limit) lines.push(`- … +${nodes.length - limit} more`);
3916 return lines;
3917 };
3918
3919 // symbolsOnly → the cheap structural overview, no source.
3920 if (opts.symbolsOnly) {
3921 const out = [`**${filePath}** — ${nodes.length} symbol${nodes.length === 1 ? '' : 's'}, ${depSummary}`, ''];
3922 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