codebase_flow
Trace forward execution flow from any entry point in your codebase. Auto-detect entry points or explore call trees with depth control.
Instructions
Trace the EXECUTION FLOW forward from an entry point — what does this code call into? With NO args, returns a ranked list of auto-detected entry points (orphans with outgoing calls, conventional names like main(), framework routes, tests). With an entrypoint argument, returns the call tree.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| projectPath | No | Absolute path to the project directory. | |
| entrypoint | No | Symbol name to trace from. Omit to list auto-detected entry points. | |
| file | No | Optional file hint to disambiguate the symbol. | |
| depth | No | Maximum DFS depth (default 5, max 10). |
Implementation Reference
- src/index.ts:299-311 (registration)The tool 'codebase_flow' is registered with the MCP server, defining its schema (projectPath, entrypoint, file, depth) and delegating to handleGraphTool.
server.tool( "codebase_flow", "Trace the EXECUTION FLOW forward from an entry point — what does this code call into? With NO args, returns a ranked list of auto-detected entry points (orphans with outgoing calls, conventional names like main(), framework routes, tests). With an entrypoint argument, returns the call tree.", { projectPath: z.string().describe("Absolute path to the project directory.").optional(), entrypoint: z.string().describe("Symbol name to trace from. Omit to list auto-detected entry points.").optional(), file: z.string().describe("Optional file hint to disambiguate the symbol.").optional(), depth: z.number().describe("Maximum DFS depth (default 5, max 10).").optional(), }, async (args) => ({ content: [{ type: "text", text: await handleGraphTool("codebase_flow", args) }], }), ); - src/tools/graph-tools.ts:356-413 (handler)The handler case for 'codebase_flow' inside handleGraphTool. Supports two modes: (1) no args -> auto-detect entry points, (2) with entrypoint -> resolve symbol and DFS trace forward call flow.
case "codebase_flow": { const projectId = projectIdFromPath(projectPath); const cache = await getSymbolGraphCache(projectId); if (!cache) { return "No symbol graph found. Run codebase_graph_build (or codebase_index) first."; } const entrypoint = (args.entrypoint as string | undefined)?.trim(); // Zero-arg mode → ranked entry-point list if (!entrypoint) { // Build a fresh detection using the file graph + per-file payloads from the cache. // For efficiency we only list entry points by walking known symbols via the name index. const fileGraph = await getOrBuildGraph(projectPath); const nameIndex = await cache.getNameIndex(); const seenFiles = new Set<string>(); const payloads = []; for (const refs of nameIndex.values()) { for (const ref of refs) { if (seenFiles.has(ref.file)) continue; seenFiles.add(ref.file); const p = await cache.getFilePayload(ref.file); if (p) payloads.push(p); } } const entries = detectEntryPoints(fileGraph, payloads); if (entries.length === 0) { return "No entry points detected. The codebase may not have orphan files, conventional main() functions, or framework routes."; } const lines = [`Detected ${entries.length} entry point(s):`, ""]; for (const e of entries.slice(0, 50)) { lines.push(` ${e.name} (${e.file}${e.line ? `:${e.line}` : ""}) — ${e.reason}`); } if (entries.length > 50) lines.push(` ... and ${entries.length - 50} more`); lines.push("", "Pass `entrypoint` to trace forward call flow from any of these."); return lines.join("\n"); } // Resolve symbol name → id via name index (file hint disambiguates) const nameIndex = await cache.getNameIndex(); let refs = nameIndex.get(entrypoint) ?? []; const fileHint = (args.file as string | undefined)?.trim(); if (fileHint) refs = refs.filter((r) => r.file === fileHint); if (refs.length === 0) { return `No symbol named "${entrypoint}" found${fileHint ? ` in ${fileHint}` : ""}.`; } if (refs.length > 1) { const lines = [`Symbol "${entrypoint}" is ambiguous (${refs.length} matches). Pass \`file\` to disambiguate:`, ""]; for (const r of refs) lines.push(` - ${r.file}`); return lines.join("\n"); } const depth = typeof args.depth === "number" ? args.depth : 5; const tree = await getCallFlow(cache, refs[0].id, depth); if (!tree) return `Could not load symbol "${entrypoint}".`; const lines = [`Call flow from ${tree.symbolName} (${tree.file}:${tree.line})`, ""]; renderFlowTree(tree, "", true, lines); return lines.join("\n"); } - src/services/graph-impact.ts:110-124 (helper)Main helper function that performs DFS forward call-flow traversal from a given symbol, using lazy-loaded per-file payloads and cycle detection.
export async function getCallFlow( cache: SymbolGraphCache, entrypointId: string, depth: number = 5, ): Promise<FlowNode | null> { const safeDepth = Math.max(1, Math.min(depth, MAX_FLOW_DEPTH)); const file = symbolIdToFile(entrypointId); if (!file) return null; const payload = await cache.getFilePayload(file); if (!payload) return null; const sym = payload.symbols.find((s) => s.id === entrypointId); if (!sym) return null; const visited = new Set<string>(); return await walk(cache, sym, 0, safeDepth, visited); - src/services/graph-impact.ts:127-170 (helper)Recursive walker for getCallFlow that follows outgoingCalls edges, building a tree of FlowNode objects with cycle and depth truncation.
async function walk( cache: SymbolGraphCache, sym: SymbolNode, hop: number, maxDepth: number, visited: Set<string>, ): Promise<FlowNode> { const node: FlowNode = { symbolId: sym.id, symbolName: sym.qualifiedName, file: sym.file, line: sym.line, children: [], }; if (visited.has(sym.id)) { node.truncatedReason = "cycle"; return node; } visited.add(sym.id); if (hop >= maxDepth) { node.truncatedReason = "depth"; return node; } const payload = await cache.getFilePayload(sym.file); if (!payload) return node; const calls = payload.outgoingCalls.filter( (e) => e.callerId === sym.id && e.calleeCandidates.length > 0, ); for (const e of calls) { for (const calleeId of e.calleeCandidates) { const calleeFile = symbolIdToFile(calleeId); if (!calleeFile) continue; const calleePayload = await cache.getFilePayload(calleeFile); if (!calleePayload) continue; const calleeSym = calleePayload.symbols.find((s) => s.id === calleeId); if (!calleeSym) continue; node.children.push(await walk(cache, calleeSym, hop + 1, maxDepth, visited)); } } return node; } - src/tools/graph-tools.ts:482-497 (helper)Renders the FlowNode tree into human-readable ASCII lines with branch characters and indentation.
function renderFlowTree( node: FlowNode, prefix: string, isLast: boolean, out: string[], ): void { const branch = isLast ? "└── " : "├── "; const suffix = node.truncatedReason ? ` [truncated: ${node.truncatedReason}]` : ""; out.push(`${prefix}${branch}${node.symbolName} (${node.file}:${node.line})${suffix}`); const childPrefix = prefix + (isLast ? " " : "│ "); for (let i = 0; i < node.children.length; i++) { renderFlowTree(node.children[i], childPrefix, i === node.children.length - 1, out); } }