callgraph-mcp 1.4.7 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +3 -7
- package/package.json +3 -3
- package/dist/server.js +0 -71
- package/dist/tools/analyzeFile.js +0 -109
- package/dist/tools/analyzeWorkspace.js +0 -95
- package/dist/tools/findCycles.js +0 -177
- package/dist/tools/findDuplicates.js +0 -254
- package/dist/tools/findOrphans.js +0 -100
- package/dist/tools/getCallees.js +0 -116
- package/dist/tools/getCallers.js +0 -117
- package/dist/tools/getFlow.js +0 -140
- package/dist/tools/listEntryPoints.js +0 -96
- package/dist/utils/analysis.js +0 -113
- package/dist/utils/cache.js +0 -28
- package/dist/utils/fileDiscovery.js +0 -94
- package/dist/utils/formatGraph.js +0 -38
- package/dist/utils/toolHelper.js +0 -11
package/README.md
CHANGED
|
@@ -24,6 +24,9 @@ Most AI coding tools answer structural questions about your codebase by reading
|
|
|
24
24
|
|
|
25
25
|
**callgraph-mcp eliminates all three.** It never reads your code as prose. It parses every file into an AST using Tree-sitter, builds an exact directed call graph, and answers structural queries against that graph. Every caller, every callee, every reachable function, every cycle - returned as a precise index. The answer is always the same regardless of how large your codebase is, which files happen to be in context, or how deeply buried a function is. **There is no probability involved. There is no attention to dilute.**
|
|
26
26
|
|
|
27
|
+

|
|
28
|
+
*The image shows how callgraph explains the POST method flow in python-fastAPI codebase irrespective of how large or small the codebase is.*
|
|
29
|
+
|
|
27
30
|
---
|
|
28
31
|
|
|
29
32
|
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
(0, server_1.startServer)().catch((err) => {
|
|
6
|
-
process.stderr.write(`FlowMap MCP server failed to start: ${err}\n`);
|
|
7
|
-
process.exit(1);
|
|
8
|
-
});
|
|
2
|
+
"use strict";var Te=Object.create;var q=Object.defineProperty;var Pe=Object.getOwnPropertyDescriptor;var Ue=Object.getOwnPropertyNames;var Ie=Object.getPrototypeOf,ze=Object.prototype.hasOwnProperty;var We=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Ue(e))!ze.call(t,s)&&s!==n&&q(t,s,{get:()=>e[s],enumerable:!(r=Pe(e,s))||r.enumerable});return t};var O=(t,e,n)=>(n=t!=null?Te(Ie(t)):{},We(e||!t||!t.__esModule?q(n,"default",{value:t,enumerable:!0}):n,t));var be=require("http"),Me=require("@modelcontextprotocol/sdk/server/mcp.js"),Fe=require("@modelcontextprotocol/sdk/server/stdio.js"),De=require("@modelcontextprotocol/sdk/server/streamableHttp.js"),Le=require("crypto");var J=require("zod"),re=O(require("fs"));var D=O(require("path")),K=O(require("fs")),N=require("@codeflow-map/core");var k=new Map,Je=3e4;function V(t){let e=k.get(t);return e?Date.now()-e.cachedAt>Je?(k.delete(t),null):e.graph:null}function Y(t,e){k.set(t,{graph:e,cachedAt:Date.now(),workspacePath:t})}var z=O(require("path")),Q=O(require("fast-glob")),I=require("@codeflow-map/core"),$e=["**/node_modules/**","**/venv/**","**/.venv/**","**/__pycache__/**","**/vendor/**","**/target/**","**/.git/**","**/dist/**","**/build/**","**/.next/**","**/.turbo/**","**/coverage/**","**/.gradle/**","**/.cache/**","**/site-packages/**","**/.mypy_cache/**","**/.pytest_cache/**","**/out/**","**/bin/**","**/obj/**","**/tests/**","**/__tests__/**","**/spec/**","**/__specs__/**","**/test/**"];async function ee(t,e={}){let{exclude:n=[],language:r}=e,s;if(r){let i=Object.entries(I.FILE_EXTENSION_MAP).filter(([,p])=>p===r).map(([p])=>p.replace(".",""));s=i.length>0?i:[]}else s=Object.keys(I.FILE_EXTENSION_MAP).map(i=>i.replace(".",""));if(s.length===0)return[];let o=s.length===1?`**/*.${s[0]}`:`**/*.{${s.join(",")}}`,c=[...$e,...n],m=t.replace(/\\/g,"/"),g=await(0,Q.default)(o,{cwd:m,ignore:c,absolute:!1,dot:!1,onlyFiles:!0}),a=[];for(let i of g){let p=z.extname(i),f=I.FILE_EXTENSION_MAP[p];f&&a.push({filePath:i.replace(/\\/g,"/"),absPath:z.resolve(t,i),languageId:f})}return a}var te=50,ne=!1;function W(){if(process.env.FLOWMAP_GRAMMARS)return process.env.FLOWMAP_GRAMMARS;let t=[D.resolve(__dirname,"..","grammars"),D.resolve(__dirname,"..","..","grammars")];for(let e of t)if(K.existsSync(D.join(e,"tree-sitter.wasm")))return e;return t[0]}async function Ge(){if(!ne){let t=W(),e=D.join(t,"tree-sitter.wasm"),n=K.existsSync(e);console.error(`[flowmap] Grammar directory: ${t} (tree-sitter.wasm ${n?"found":"missing"})`),await(0,N.initTreeSitter)(t),ne=!0}}async function _(t,e={}){let n=V(t);if(n)return n;await Ge();let r=W(),s=Date.now(),o=await ee(t,e),c=[],m=[],g=0;for(let d=0;d<o.length;d+=te){let u=o.slice(d,d+te),E=await Promise.all(u.map(S=>(0,N.parseFile)(S.filePath,S.absPath,r,S.languageId).catch(()=>null)));for(let S of E)S&&(c.push(...S.functions),m.push(...S.calls),g++)}let a=(0,N.buildCallGraph)(c,m);(0,N.detectEntryPoints)(c,a);let{flows:i,orphans:p}=(0,N.partitionFlows)(c,a),f={nodes:c,edges:a,flows:i,orphans:p,scannedFiles:g,durationMs:Date.now()-s};return Y(t,f),f}function y(t,e,n,r,s){t.tool(e,n,r,s)}var je=["typescript","javascript","python","java","go","rust","tsx","jsx"],He=["node_modules","dist",".git","__pycache__","*.test.*","*.spec.*"];function se(t){y(t,"flowmap_analyze_workspace","Scan an entire codebase and return a full call graph \u2014 all functions, their parameters, and all call relationships between them. Use this first when exploring an unfamiliar codebase.",{workspacePath:J.z.string().describe("Absolute path to the repository root"),exclude:J.z.string().optional().describe("Comma-separated glob patterns to exclude. Defaults: node_modules,dist,.git,__pycache__,*.test.*,*.spec.*"),language:J.z.string().optional().describe("Filter to a single language: typescript, javascript, python, java, go, rust, tsx, jsx. Omit to scan all.")},async({workspacePath:e,exclude:n,language:r})=>{try{if(!re.existsSync(e))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${e}`,workspacePath:e})}]};let s=n?n.split(",").map(m=>m.trim()).filter(Boolean):He,o=r&&je.includes(r)?r:void 0,c=await _(e,{exclude:s,language:o});return{content:[{type:"text",text:JSON.stringify(c)}]}}catch(s){let o=s instanceof Error?s.message:String(s);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:o,workspacePath:e})}]}}})}var ie=require("zod"),ae=O(require("fs")),$=O(require("path")),L=require("@codeflow-map/core");var oe=!1;function ce(t){y(t,"flowmap_analyze_file","Scan a single file and return all functions defined in it, their parameters, and calls made within the file.",{filePath:ie.z.string().describe("Absolute path to the file to analyse")},async({filePath:e})=>{try{if(!ae.existsSync(e))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"FILE_NOT_FOUND",message:`File does not exist: ${e}`})}]};let n=$.extname(e),r=L.FILE_EXTENSION_MAP[n];if(!r)return{content:[{type:"text",text:JSON.stringify({error:!0,code:"UNSUPPORTED_LANGUAGE",message:`Unsupported file extension: ${n}`})}]};let s=W();oe||(await(0,L.initTreeSitter)(s),oe=!0);let o=Date.now(),c=$.basename(e),m=await(0,L.parseFile)(c,e,s,r);return{content:[{type:"text",text:JSON.stringify({filePath:c,functions:m.functions,calls:m.calls,durationMs:Date.now()-o})}]}}catch(n){let r=n instanceof Error?n.message:String(n);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:r})}]}}})}var X=require("zod"),le=O(require("fs"));function de(t){y(t,"flowmap_get_callers","Return all functions that directly call the named function. Use this for impact analysis \u2014 to understand what breaks if you change a function's signature.",{functionName:X.z.string().describe("The function name to find callers of"),workspacePath:X.z.string().describe("Absolute path to the repository root")},async({functionName:e,workspacePath:n})=>{try{if(!le.existsSync(n))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${n}`,workspacePath:n})}]};let r=await _(n),s=r.nodes.filter(a=>a.name===e);if(s.length===0)return{content:[{type:"text",text:JSON.stringify({error:!0,code:"FUNCTION_NOT_FOUND",message:`No function named "${e}" found in the codebase.`,workspacePath:n})}]};let o=s[0],c=new Set(s.map(a=>a.id)),g=r.edges.filter(a=>c.has(a.to)).map(a=>{let i=r.nodes.find(p=>p.id===a.from);return{id:a.from,name:i?.name??"unknown",filePath:i?.filePath??"unknown",startLine:i?.startLine??0,callLine:a.line}});return{content:[{type:"text",text:JSON.stringify({target:e,targetId:o.id,callers:g,count:g.length})}]}}catch(r){let s=r instanceof Error?r.message:String(r);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:s,workspacePath:n})}]}}})}var B=require("zod"),fe=O(require("fs"));function pe(t){y(t,"flowmap_get_callees","Return all functions directly called by the named function. Use this to understand what a function depends on.",{functionName:B.z.string().describe("The function name to find callees of"),workspacePath:B.z.string().describe("Absolute path to the repository root")},async({functionName:e,workspacePath:n})=>{try{if(!fe.existsSync(n))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${n}`,workspacePath:n})}]};let r=await _(n),s=r.nodes.filter(a=>a.name===e);if(s.length===0)return{content:[{type:"text",text:JSON.stringify({error:!0,code:"FUNCTION_NOT_FOUND",message:`No function named "${e}" found in the codebase.`,workspacePath:n})}]};let o=s[0],c=new Set(s.map(a=>a.id)),g=r.edges.filter(a=>c.has(a.from)).map(a=>{let i=r.nodes.find(p=>p.id===a.to);return{id:a.to,name:i?.name??"unknown",filePath:i?.filePath??"unknown",startLine:i?.startLine??0,callLine:a.line}});return{content:[{type:"text",text:JSON.stringify({target:e,targetId:o.id,callees:g,count:g.length})}]}}catch(r){let s=r instanceof Error?r.message:String(r);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:s,workspacePath:n})}]}}})}var G=require("zod"),me=O(require("fs"));function ge(t){y(t,"flowmap_get_flow","Return the complete sub-graph reachable from a given function \u2014 every function it calls, every function those call, and so on recursively. Use this to understand the full execution path of a feature or entry point.",{functionName:G.z.string().describe("The starting function name"),workspacePath:G.z.string().describe("Absolute path to the repository root"),maxDepth:G.z.number().optional().describe("Maximum recursion depth. Default 10.")},async({functionName:e,workspacePath:n,maxDepth:r})=>{let s=r??10;try{if(!me.existsSync(n))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${n}`,workspacePath:n})}]};let o=await _(n),c=o.nodes.filter(u=>u.name===e);if(c.length===0)return{content:[{type:"text",text:JSON.stringify({error:!0,code:"FUNCTION_NOT_FOUND",message:`No function named "${e}" found in the codebase.`,workspacePath:n})}]};let m=c[0],g=new Map;for(let u of o.edges){let E=g.get(u.from)||[],S=o.nodes.find(b=>b.id===u.to);S&&(E.push({edge:u,node:S}),g.set(u.from,E))}let a=new Set,i=[],p=[],f=0,d=[m.id];for(a.add(m.id),i.push(m);d.length>0&&f<s;){let u=[];for(let E of d){let S=g.get(E)||[];for(let{edge:b,node:h}of S)p.push(b),a.has(h.id)||(a.add(h.id),i.push(h),u.push(h.id))}d=u,f++}return{content:[{type:"text",text:JSON.stringify({entryFunction:e,nodes:i,edges:p,depth:f,totalFunctions:i.length})}]}}catch(o){let c=o instanceof Error?o.message:String(o);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:c,workspacePath:n})}]}}})}var ue=require("zod"),he=O(require("fs"));function ye(t){y(t,"flowmap_list_entry_points","Return all detected entry points in the codebase \u2014 main functions, HTTP route handlers, React root renders, CLI commands, etc. Always call this first when exploring a new codebase to understand where execution begins.",{workspacePath:ue.z.string().describe("Absolute path to the repository root")},async({workspacePath:e})=>{try{if(!he.existsSync(e))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${e}`,workspacePath:e})}]};let n=await _(e),s=n.nodes.filter(o=>o.isEntryPoint).map(o=>({id:o.id,name:o.name,filePath:o.filePath,startLine:o.startLine,language:o.language,isExported:o.isExported,isAsync:o.isAsync}));return{content:[{type:"text",text:JSON.stringify({entryPoints:s,count:s.length,durationMs:n.durationMs})}]}}catch(n){let r=n instanceof Error?n.message:String(n);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:r,workspacePath:e})}]}}})}var Se=require("zod"),xe=O(require("fs"));function _e(t){y(t,"flowmap_find_orphans","Return all functions that are never called from any entry point \u2014 potential dead code. Use this during refactoring to identify code that can safely be removed.",{workspacePath:Se.z.string().describe("Absolute path to the repository root")},async({workspacePath:e})=>{try{if(!xe.existsSync(e))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${e}`,workspacePath:e})}]};let n=await _(e),r=n.orphans.map(s=>{let o=n.nodes.find(c=>c.id===s);return o?{id:o.id,name:o.name,filePath:o.filePath,startLine:o.startLine,language:o.language,isExported:o.isExported}:{id:s,name:"unknown",filePath:"unknown",startLine:0}});return{content:[{type:"text",text:JSON.stringify({orphans:r,count:r.length,durationMs:n.durationMs,note:"Exported functions may be used by external consumers \u2014 verify before deleting."})}]}}catch(n){let r=n instanceof Error?n.message:String(n);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:r,workspacePath:e})}]}}})}var j=require("zod"),ve=O(require("fs"));function ke(t,e){let n=new Map,r=new Map,s=new Map,o=[],c=[],m=0,g=new Map;for(let i of t)g.set(i,[]);for(let i of e)g.has(i.from)&&g.has(i.to)&&g.get(i.from).push(i.to);function a(i){n.set(i,m),r.set(i,m),m++,o.push(i),s.set(i,!0);for(let p of g.get(i)??[])n.has(p)?s.get(p)&&r.set(i,Math.min(r.get(i),n.get(p))):(a(p),r.set(i,Math.min(r.get(i),r.get(p))));if(r.get(i)===n.get(i)){let p=[],f;do f=o.pop(),s.set(f,!1),p.push(f);while(f!==i);c.push(p)}}for(let i of t)n.has(i)||a(i);return c}function Ke(t,e){let n=new Set(t);return e.filter(r=>n.has(r.from)&&n.has(r.to)).map(r=>({from:r.from,to:r.to,line:r.line}))}function Oe(t){y(t,"flowmap_find_cycles","Detect all call cycles (circular dependencies / mutual recursion) in the codebase. Returns each cycle as an ordered list of functions that call each other in a loop, along with the exact call edges forming the cycle. Use this to identify architectural problems, infinite-recursion risks, or tightly coupled modules.",{workspacePath:j.z.string().describe("Absolute path to the repository root"),minCycleLength:j.z.number().int().min(1).optional().describe("Minimum number of functions in a cycle to report (default: 1, includes self-recursion)"),exclude:j.z.string().optional().describe("Comma-separated glob patterns to exclude. Defaults: node_modules,dist,.git,__pycache__,*.test.*,*.spec.*")},async({workspacePath:e,minCycleLength:n=1,exclude:r})=>{try{if(!ve.existsSync(e))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${e}`,workspacePath:e})}]};let o=r?r.split(",").map(d=>d.trim()).filter(Boolean):["node_modules","dist",".git","__pycache__","*.test.*","*.spec.*"],c=await _(e,{exclude:o}),m=c.nodes.map(d=>d.id),g=ke(m,c.edges),a=new Set(c.edges.filter(d=>d.from===d.to).map(d=>d.from)),i=g.filter(d=>d.length>1?d.length>=n:n<=1&&a.has(d[0])),p=new Map(c.nodes.map(d=>[d.id,d])),f=i.map((d,u)=>{let E=d.map(b=>{let h=p.get(b);return h?{id:b,name:h.name,filePath:h.filePath,startLine:h.startLine,language:h.language}:{id:b,name:"unknown",filePath:"unknown",startLine:0,language:"unknown"}}),S=Ke(d,c.edges);return{cycleIndex:u+1,length:d.length,members:E,edges:S}});return{content:[{type:"text",text:JSON.stringify({cycles:f,totalCycles:f.length,durationMs:c.durationMs,scannedFiles:c.scannedFiles,note:f.length===0?"No cycles detected \u2014 the call graph is acyclic.":`${f.length} cycle(s) found. Cycles involving many functions or cross-module calls are the highest priority to review.`})}]}}catch(s){let o=s instanceof Error?s.message:String(s);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:o,workspacePath:e})}]}}})}var T=require("zod"),we=O(require("fs"));function Ee(t,e){if(t.size===0&&e.size===0)return 1;let n=0;for(let s of t)e.has(s)&&n++;let r=t.size+e.size-n;return r===0?0:n/r}var Z=class{parent=new Map;find(e){return this.parent.get(e)!==e&&this.parent.set(e,this.find(this.parent.get(e))),this.parent.get(e)}union(e,n){this.parent.has(e)||this.parent.set(e,e),this.parent.has(n)||this.parent.set(n,n);let r=this.find(e),s=this.find(n);r!==s&&this.parent.set(r,s)}init(e){this.parent.has(e)||this.parent.set(e,e)}clusters(){let e=new Map;for(let n of this.parent.keys()){let r=this.find(n);e.has(r)||e.set(r,[]),e.get(r).push(n)}return e}};function Xe(){let t=parseFloat(process.env.FLOWMAP_DUP_THRESHOLD??"");return isFinite(t)&&t>=0&&t<=1?t:.75}function Be(){let t=parseInt(process.env.FLOWMAP_DUP_MIN_CALLEES??"",10);return isFinite(t)&&t>=1?t:2}function Ne(t){y(t,"flowmap_find_duplicates","Identify functionally duplicate functions \u2014 different names, potentially in different files or components, but calling the same set of dependencies (same business logic). Uses callee-set Jaccard similarity: two functions are flagged as duplicates when the overlap of what they call exceeds the similarity threshold. Results are grouped into clusters so you can see when 3+ functions are all doing the same thing. Use this to find refactoring opportunities and candidates for a shared utility. Default thresholds can be tuned via FLOWMAP_DUP_THRESHOLD and FLOWMAP_DUP_MIN_CALLEES environment variables.",{workspacePath:T.z.string().describe("Absolute path to the repository root"),similarityThreshold:T.z.number().min(0).max(1).optional().describe("Jaccard similarity threshold (0\u20131). Default: 0.75 (or FLOWMAP_DUP_THRESHOLD env var). Lower = more matches, higher = stricter. 1.0 = identical callee sets."),minCallees:T.z.number().int().min(1).optional().describe("Minimum number of distinct callees a function must have to be considered. Default: 2 (or FLOWMAP_DUP_MIN_CALLEES env var). Raising this avoids matching trivial one-liner wrappers."),exclude:T.z.string().optional().describe("Comma-separated glob patterns to exclude. Defaults: node_modules,dist,.git,__pycache__,*.test.*,*.spec.*")},async({workspacePath:e,similarityThreshold:n,minCallees:r,exclude:s})=>{let o=n??Xe(),c=r??Be();try{if(!we.existsSync(e))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${e}`,workspacePath:e})}]};let g=s?s.split(",").map(l=>l.trim()).filter(Boolean):["node_modules","dist",".git","__pycache__","*.test.*","*.spec.*"],a=await _(e,{exclude:g}),i=o,p=c,f=new Map,d=new Map(a.nodes.map(l=>[l.id,l]));for(let l of a.nodes)f.set(l.id,new Set);for(let l of a.edges){if(l.from===l.to)continue;let F=d.get(l.to)?.name??l.to;f.get(l.from)?.add(F)}let u=a.nodes.filter(l=>(f.get(l.id)?.size??0)>=p),E=new Z;for(let l of u)E.init(l.id);let S=new Map;for(let l=0;l<u.length;l++){let x=u[l],F=f.get(x.id);for(let A=l+1;A<u.length;A++){let M=u[A];if(x.name===M.name&&x.filePath===M.filePath)continue;let C=f.get(M.id),R=Ee(F,C);if(R>=i){E.union(x.id,M.id);let H=[x.id,M.id].sort().join("|||");S.set(H,R)}}}let b=E.clusters(),h=[],Re=1;for(let[,l]of b){if(l.length<2)continue;let x=l.map(v=>{let w=d.get(v),P=[...f.get(v)??[]].sort();return{id:v,name:w?.name??"unknown",filePath:w?.filePath??"unknown",startLine:w?.startLine??0,language:w?.language??"unknown",calleeCount:P.length,callees:P}}),F=l.map(v=>f.get(v)),A=[...F[0]].filter(v=>F.every(w=>w.has(v))).sort(),M=1,C=0;for(let v=0;v<l.length;v++)for(let w=v+1;w<l.length;w++){let P=[l[v],l[w]].sort().join("|||"),U=S.get(P)??Ee(f.get(l[v]),f.get(l[w]));U<M&&(M=U),U>C&&(C=U)}let R=new Set(x.map(v=>v.filePath)).size,H=R>1?`These ${x.length} functions across ${R} files share the same core logic. Consider extracting a shared utility that accepts parameters for any behavioural differences.`:`These ${x.length} functions in the same file appear to duplicate logic. Consider merging them or extracting a private helper.`;h.push({clusterIndex:Re++,size:x.length,members:x,sharedCallees:A,minSimilarity:Math.round(M*100)/100,maxSimilarity:Math.round(C*100)/100,suggestion:H})}return h.sort((l,x)=>x.size-l.size||x.sharedCallees.length-l.sharedCallees.length),{content:[{type:"text",text:JSON.stringify({duplicateClusters:h,totalClusters:h.length,totalFunctionsInvolved:h.reduce((l,x)=>l+x.size,0),parameters:{similarityThreshold:i,minCallees:p,envOverrides:{FLOWMAP_DUP_THRESHOLD:process.env.FLOWMAP_DUP_THRESHOLD??null,FLOWMAP_DUP_MIN_CALLEES:process.env.FLOWMAP_DUP_MIN_CALLEES??null}},durationMs:a.durationMs,scannedFiles:a.scannedFiles,note:h.length===0?"No functionally duplicate functions detected at the current threshold. Try lowering similarityThreshold or minCallees.":`${h.length} duplicate cluster(s) found. Each cluster is a group of functions that call the same logical dependencies and are candidates for generalisation.`})}]}}catch(m){let g=m instanceof Error?m.message:String(m);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:g,workspacePath:e})}]}}})}function Ae(){let t=new Me.McpServer({name:"callgraph-mcp",version:"1.0.0"});return Ze(t),t}function Ze(t){se(t),ce(t),de(t),pe(t),ge(t),ye(t),_e(t),Oe(t),Ne(t)}async function Ce(){let t=(process.env.FLOWMAP_TRANSPORT||"stdio").toLowerCase();t==="http"||t==="sse"?await Ve():await qe()}async function qe(){let t=Ae(),e=new Fe.StdioServerTransport;await t.connect(e)}async function Ve(){let t=parseInt(process.env.FLOWMAP_PORT||"3100",10),e=Ae(),n=new De.StreamableHTTPServerTransport({sessionIdGenerator:()=>(0,Le.randomUUID)()}),r=(0,be.createServer)(async(s,o)=>{let c=s.url||"/";c==="/mcp"||c==="/"?await n.handleRequest(s,o):o.writeHead(404).end("Not Found")});await e.connect(n),r.listen(t,()=>{process.stderr.write(`FlowMap MCP server listening on http://localhost:${t}/mcp
|
|
3
|
+
`)})}Ce().catch(t=>{process.stderr.write(`FlowMap MCP server failed to start: ${t}
|
|
4
|
+
`),process.exit(1)});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "callgraph-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "MCP server for codebase call-flow analysis. Local, deterministic, language-agnostic. Powered by @codeflow-map/core.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -37,13 +37,13 @@
|
|
|
37
37
|
"scripts": {
|
|
38
38
|
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
39
39
|
"build:types": "tsc --emitDeclarationOnly",
|
|
40
|
-
"build:bundle": "
|
|
40
|
+
"build:bundle": "node scripts/build.cjs",
|
|
41
41
|
"build": "pnpm run clean && pnpm run build:types && pnpm run build:bundle && node scripts/copy-grammars.cjs",
|
|
42
42
|
"start": "node dist/index.js",
|
|
43
43
|
"dev": "tsx src/index.ts"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@codeflow-map/core": "^0.
|
|
46
|
+
"@codeflow-map/core": "^1.0.1",
|
|
47
47
|
"@modelcontextprotocol/sdk": "^1.27.0",
|
|
48
48
|
"fast-glob": "^3.3.0",
|
|
49
49
|
"zod": "^3.25.0"
|
package/dist/server.js
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createMcpServer = createMcpServer;
|
|
4
|
-
exports.startServer = startServer;
|
|
5
|
-
const http_1 = require("http");
|
|
6
|
-
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
7
|
-
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
8
|
-
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
9
|
-
const crypto_1 = require("crypto");
|
|
10
|
-
const analyzeWorkspace_1 = require("./tools/analyzeWorkspace");
|
|
11
|
-
const analyzeFile_1 = require("./tools/analyzeFile");
|
|
12
|
-
const getCallers_1 = require("./tools/getCallers");
|
|
13
|
-
const getCallees_1 = require("./tools/getCallees");
|
|
14
|
-
const getFlow_1 = require("./tools/getFlow");
|
|
15
|
-
const listEntryPoints_1 = require("./tools/listEntryPoints");
|
|
16
|
-
const findOrphans_1 = require("./tools/findOrphans");
|
|
17
|
-
const findCycles_1 = require("./tools/findCycles");
|
|
18
|
-
const findDuplicates_1 = require("./tools/findDuplicates");
|
|
19
|
-
function createMcpServer() {
|
|
20
|
-
const server = new mcp_js_1.McpServer({
|
|
21
|
-
name: 'callgraph-mcp',
|
|
22
|
-
version: '1.0.0',
|
|
23
|
-
});
|
|
24
|
-
registerTools(server);
|
|
25
|
-
return server;
|
|
26
|
-
}
|
|
27
|
-
function registerTools(server) {
|
|
28
|
-
(0, analyzeWorkspace_1.registerAnalyzeWorkspace)(server);
|
|
29
|
-
(0, analyzeFile_1.registerAnalyzeFile)(server);
|
|
30
|
-
(0, getCallers_1.registerGetCallers)(server);
|
|
31
|
-
(0, getCallees_1.registerGetCallees)(server);
|
|
32
|
-
(0, getFlow_1.registerGetFlow)(server);
|
|
33
|
-
(0, listEntryPoints_1.registerListEntryPoints)(server);
|
|
34
|
-
(0, findOrphans_1.registerFindOrphans)(server);
|
|
35
|
-
(0, findCycles_1.registerFindCycles)(server);
|
|
36
|
-
(0, findDuplicates_1.registerFindDuplicates)(server);
|
|
37
|
-
}
|
|
38
|
-
async function startServer() {
|
|
39
|
-
const mode = (process.env.FLOWMAP_TRANSPORT || 'stdio').toLowerCase();
|
|
40
|
-
if (mode === 'http' || mode === 'sse') {
|
|
41
|
-
await startHttpServer();
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
await startStdioServer();
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
async function startStdioServer() {
|
|
48
|
-
const server = createMcpServer();
|
|
49
|
-
const transport = new stdio_js_1.StdioServerTransport();
|
|
50
|
-
await server.connect(transport);
|
|
51
|
-
}
|
|
52
|
-
async function startHttpServer() {
|
|
53
|
-
const port = parseInt(process.env.FLOWMAP_PORT || '3100', 10);
|
|
54
|
-
const server = createMcpServer();
|
|
55
|
-
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
56
|
-
sessionIdGenerator: () => (0, crypto_1.randomUUID)(),
|
|
57
|
-
});
|
|
58
|
-
const httpServer = (0, http_1.createServer)(async (req, res) => {
|
|
59
|
-
const url = req.url || '/';
|
|
60
|
-
if (url === '/mcp' || url === '/') {
|
|
61
|
-
await transport.handleRequest(req, res);
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
res.writeHead(404).end('Not Found');
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
await server.connect(transport);
|
|
68
|
-
httpServer.listen(port, () => {
|
|
69
|
-
process.stderr.write(`FlowMap MCP server listening on http://localhost:${port}/mcp\n`);
|
|
70
|
-
});
|
|
71
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.registerAnalyzeFile = registerAnalyzeFile;
|
|
37
|
-
const zod_1 = require("zod");
|
|
38
|
-
const fs = __importStar(require("fs"));
|
|
39
|
-
const path = __importStar(require("path"));
|
|
40
|
-
const core_1 = require("@codeflow-map/core");
|
|
41
|
-
const analysis_1 = require("../utils/analysis");
|
|
42
|
-
const toolHelper_1 = require("../utils/toolHelper");
|
|
43
|
-
let treeSitterInitialized = false;
|
|
44
|
-
function registerAnalyzeFile(server) {
|
|
45
|
-
(0, toolHelper_1.registerTool)(server, 'flowmap_analyze_file', 'Scan a single file and return all functions defined in it, their parameters, and calls made within the file.', {
|
|
46
|
-
filePath: zod_1.z.string().describe('Absolute path to the file to analyse'),
|
|
47
|
-
}, async ({ filePath: absolutePath }) => {
|
|
48
|
-
try {
|
|
49
|
-
if (!fs.existsSync(absolutePath)) {
|
|
50
|
-
return {
|
|
51
|
-
content: [{
|
|
52
|
-
type: 'text',
|
|
53
|
-
text: JSON.stringify({
|
|
54
|
-
error: true,
|
|
55
|
-
code: 'FILE_NOT_FOUND',
|
|
56
|
-
message: `File does not exist: ${absolutePath}`,
|
|
57
|
-
}),
|
|
58
|
-
}],
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
const ext = path.extname(absolutePath);
|
|
62
|
-
const languageId = core_1.FILE_EXTENSION_MAP[ext];
|
|
63
|
-
if (!languageId) {
|
|
64
|
-
return {
|
|
65
|
-
content: [{
|
|
66
|
-
type: 'text',
|
|
67
|
-
text: JSON.stringify({
|
|
68
|
-
error: true,
|
|
69
|
-
code: 'UNSUPPORTED_LANGUAGE',
|
|
70
|
-
message: `Unsupported file extension: ${ext}`,
|
|
71
|
-
}),
|
|
72
|
-
}],
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
const wasmDir = (0, analysis_1.resolveWasmDir)();
|
|
76
|
-
if (!treeSitterInitialized) {
|
|
77
|
-
await (0, core_1.initTreeSitter)(wasmDir);
|
|
78
|
-
treeSitterInitialized = true;
|
|
79
|
-
}
|
|
80
|
-
const startTime = Date.now();
|
|
81
|
-
const relativePath = path.basename(absolutePath);
|
|
82
|
-
const result = await (0, core_1.parseFile)(relativePath, absolutePath, wasmDir, languageId);
|
|
83
|
-
return {
|
|
84
|
-
content: [{
|
|
85
|
-
type: 'text',
|
|
86
|
-
text: JSON.stringify({
|
|
87
|
-
filePath: relativePath,
|
|
88
|
-
functions: result.functions,
|
|
89
|
-
calls: result.calls,
|
|
90
|
-
durationMs: Date.now() - startTime,
|
|
91
|
-
}),
|
|
92
|
-
}],
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
catch (err) {
|
|
96
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
97
|
-
return {
|
|
98
|
-
content: [{
|
|
99
|
-
type: 'text',
|
|
100
|
-
text: JSON.stringify({
|
|
101
|
-
error: true,
|
|
102
|
-
code: 'PARSE_ERROR',
|
|
103
|
-
message,
|
|
104
|
-
}),
|
|
105
|
-
}],
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.registerAnalyzeWorkspace = registerAnalyzeWorkspace;
|
|
37
|
-
const zod_1 = require("zod");
|
|
38
|
-
const fs = __importStar(require("fs"));
|
|
39
|
-
const analysis_1 = require("../utils/analysis");
|
|
40
|
-
const toolHelper_1 = require("../utils/toolHelper");
|
|
41
|
-
const SUPPORTED_LANGUAGES = ['typescript', 'javascript', 'python', 'java', 'go', 'rust', 'tsx', 'jsx'];
|
|
42
|
-
const DEFAULT_EXCLUDES = ['node_modules', 'dist', '.git', '__pycache__', '*.test.*', '*.spec.*'];
|
|
43
|
-
function registerAnalyzeWorkspace(server) {
|
|
44
|
-
(0, toolHelper_1.registerTool)(server, 'flowmap_analyze_workspace', 'Scan an entire codebase and return a full call graph — all functions, their parameters, and all call relationships between them. Use this first when exploring an unfamiliar codebase.', {
|
|
45
|
-
workspacePath: zod_1.z.string().describe('Absolute path to the repository root'),
|
|
46
|
-
exclude: zod_1.z.string().optional().describe('Comma-separated glob patterns to exclude. Defaults: node_modules,dist,.git,__pycache__,*.test.*,*.spec.*'),
|
|
47
|
-
language: zod_1.z.string().optional().describe('Filter to a single language: typescript, javascript, python, java, go, rust, tsx, jsx. Omit to scan all.'),
|
|
48
|
-
}, async ({ workspacePath, exclude, language }) => {
|
|
49
|
-
try {
|
|
50
|
-
if (!fs.existsSync(workspacePath)) {
|
|
51
|
-
return {
|
|
52
|
-
content: [{
|
|
53
|
-
type: 'text',
|
|
54
|
-
text: JSON.stringify({
|
|
55
|
-
error: true,
|
|
56
|
-
code: 'WORKSPACE_NOT_FOUND',
|
|
57
|
-
message: `Directory does not exist: ${workspacePath}`,
|
|
58
|
-
workspacePath,
|
|
59
|
-
}),
|
|
60
|
-
}],
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
const excludeList = exclude
|
|
64
|
-
? exclude.split(',').map(s => s.trim()).filter(Boolean)
|
|
65
|
-
: DEFAULT_EXCLUDES;
|
|
66
|
-
const lang = language && SUPPORTED_LANGUAGES.includes(language)
|
|
67
|
-
? language
|
|
68
|
-
: undefined;
|
|
69
|
-
const graph = await (0, analysis_1.analyzeWorkspace)(workspacePath, {
|
|
70
|
-
exclude: excludeList,
|
|
71
|
-
language: lang,
|
|
72
|
-
});
|
|
73
|
-
return {
|
|
74
|
-
content: [{
|
|
75
|
-
type: 'text',
|
|
76
|
-
text: JSON.stringify(graph),
|
|
77
|
-
}],
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
catch (err) {
|
|
81
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
82
|
-
return {
|
|
83
|
-
content: [{
|
|
84
|
-
type: 'text',
|
|
85
|
-
text: JSON.stringify({
|
|
86
|
-
error: true,
|
|
87
|
-
code: 'PARSE_ERROR',
|
|
88
|
-
message,
|
|
89
|
-
workspacePath,
|
|
90
|
-
}),
|
|
91
|
-
}],
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
}
|
package/dist/tools/findCycles.js
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.registerFindCycles = registerFindCycles;
|
|
37
|
-
const zod_1 = require("zod");
|
|
38
|
-
const fs = __importStar(require("fs"));
|
|
39
|
-
const analysis_1 = require("../utils/analysis");
|
|
40
|
-
const toolHelper_1 = require("../utils/toolHelper");
|
|
41
|
-
/** Tarjan's SCC — returns groups of node IDs that form cycles. */
|
|
42
|
-
function findStronglyConnectedComponents(nodeIds, edges) {
|
|
43
|
-
const index = new Map();
|
|
44
|
-
const lowlink = new Map();
|
|
45
|
-
const onStack = new Map();
|
|
46
|
-
const stack = [];
|
|
47
|
-
const sccs = [];
|
|
48
|
-
let counter = 0;
|
|
49
|
-
// Build adjacency list
|
|
50
|
-
const adj = new Map();
|
|
51
|
-
for (const id of nodeIds)
|
|
52
|
-
adj.set(id, []);
|
|
53
|
-
for (const e of edges) {
|
|
54
|
-
if (adj.has(e.from) && adj.has(e.to)) {
|
|
55
|
-
adj.get(e.from).push(e.to);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
function strongConnect(v) {
|
|
59
|
-
index.set(v, counter);
|
|
60
|
-
lowlink.set(v, counter);
|
|
61
|
-
counter++;
|
|
62
|
-
stack.push(v);
|
|
63
|
-
onStack.set(v, true);
|
|
64
|
-
for (const w of (adj.get(v) ?? [])) {
|
|
65
|
-
if (!index.has(w)) {
|
|
66
|
-
strongConnect(w);
|
|
67
|
-
lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
|
|
68
|
-
}
|
|
69
|
-
else if (onStack.get(w)) {
|
|
70
|
-
lowlink.set(v, Math.min(lowlink.get(v), index.get(w)));
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
if (lowlink.get(v) === index.get(v)) {
|
|
74
|
-
const scc = [];
|
|
75
|
-
let w;
|
|
76
|
-
do {
|
|
77
|
-
w = stack.pop();
|
|
78
|
-
onStack.set(w, false);
|
|
79
|
-
scc.push(w);
|
|
80
|
-
} while (w !== v);
|
|
81
|
-
sccs.push(scc);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
for (const id of nodeIds) {
|
|
85
|
-
if (!index.has(id))
|
|
86
|
-
strongConnect(id);
|
|
87
|
-
}
|
|
88
|
-
return sccs;
|
|
89
|
-
}
|
|
90
|
-
function describeCycleEdges(cycle, edges) {
|
|
91
|
-
const memberSet = new Set(cycle);
|
|
92
|
-
return edges
|
|
93
|
-
.filter(e => memberSet.has(e.from) && memberSet.has(e.to))
|
|
94
|
-
.map(e => ({ from: e.from, to: e.to, line: e.line }));
|
|
95
|
-
}
|
|
96
|
-
function registerFindCycles(server) {
|
|
97
|
-
(0, toolHelper_1.registerTool)(server, 'flowmap_find_cycles', 'Detect all call cycles (circular dependencies / mutual recursion) in the codebase. Returns each cycle as an ordered list of functions that call each other in a loop, along with the exact call edges forming the cycle. Use this to identify architectural problems, infinite-recursion risks, or tightly coupled modules.', {
|
|
98
|
-
workspacePath: zod_1.z.string().describe('Absolute path to the repository root'),
|
|
99
|
-
minCycleLength: zod_1.z.number().int().min(1).optional().describe('Minimum number of functions in a cycle to report (default: 1, includes self-recursion)'),
|
|
100
|
-
exclude: zod_1.z.string().optional().describe('Comma-separated glob patterns to exclude. Defaults: node_modules,dist,.git,__pycache__,*.test.*,*.spec.*'),
|
|
101
|
-
}, async ({ workspacePath, minCycleLength = 1, exclude }) => {
|
|
102
|
-
try {
|
|
103
|
-
if (!fs.existsSync(workspacePath)) {
|
|
104
|
-
return {
|
|
105
|
-
content: [{
|
|
106
|
-
type: 'text',
|
|
107
|
-
text: JSON.stringify({
|
|
108
|
-
error: true,
|
|
109
|
-
code: 'WORKSPACE_NOT_FOUND',
|
|
110
|
-
message: `Directory does not exist: ${workspacePath}`,
|
|
111
|
-
workspacePath,
|
|
112
|
-
}),
|
|
113
|
-
}],
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
const DEFAULT_EXCLUDES = ['node_modules', 'dist', '.git', '__pycache__', '*.test.*', '*.spec.*'];
|
|
117
|
-
const excludeList = exclude
|
|
118
|
-
? exclude.split(',').map(s => s.trim()).filter(Boolean)
|
|
119
|
-
: DEFAULT_EXCLUDES;
|
|
120
|
-
const graph = await (0, analysis_1.analyzeWorkspace)(workspacePath, { exclude: excludeList });
|
|
121
|
-
const nodeIds = graph.nodes.map(n => n.id);
|
|
122
|
-
const sccs = findStronglyConnectedComponents(nodeIds, graph.edges);
|
|
123
|
-
// A self-loop counts as a cycle of length 1
|
|
124
|
-
const selfLoopIds = new Set(graph.edges.filter(e => e.from === e.to).map(e => e.from));
|
|
125
|
-
const cyclesRaw = sccs.filter(scc => {
|
|
126
|
-
if (scc.length > 1)
|
|
127
|
-
return scc.length >= minCycleLength;
|
|
128
|
-
// single-node SCC — only a cycle if there's a self-edge
|
|
129
|
-
return minCycleLength <= 1 && selfLoopIds.has(scc[0]);
|
|
130
|
-
});
|
|
131
|
-
const nodeById = new Map(graph.nodes.map(n => [n.id, n]));
|
|
132
|
-
const cycles = cyclesRaw.map((scc, i) => {
|
|
133
|
-
const members = scc.map(id => {
|
|
134
|
-
const n = nodeById.get(id);
|
|
135
|
-
return n
|
|
136
|
-
? { id, name: n.name, filePath: n.filePath, startLine: n.startLine, language: n.language }
|
|
137
|
-
: { id, name: 'unknown', filePath: 'unknown', startLine: 0, language: 'unknown' };
|
|
138
|
-
});
|
|
139
|
-
const cycleEdges = describeCycleEdges(scc, graph.edges);
|
|
140
|
-
return {
|
|
141
|
-
cycleIndex: i + 1,
|
|
142
|
-
length: scc.length,
|
|
143
|
-
members,
|
|
144
|
-
edges: cycleEdges,
|
|
145
|
-
};
|
|
146
|
-
});
|
|
147
|
-
return {
|
|
148
|
-
content: [{
|
|
149
|
-
type: 'text',
|
|
150
|
-
text: JSON.stringify({
|
|
151
|
-
cycles,
|
|
152
|
-
totalCycles: cycles.length,
|
|
153
|
-
durationMs: graph.durationMs,
|
|
154
|
-
scannedFiles: graph.scannedFiles,
|
|
155
|
-
note: cycles.length === 0
|
|
156
|
-
? 'No cycles detected — the call graph is acyclic.'
|
|
157
|
-
: `${cycles.length} cycle(s) found. Cycles involving many functions or cross-module calls are the highest priority to review.`,
|
|
158
|
-
}),
|
|
159
|
-
}],
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
catch (err) {
|
|
163
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
164
|
-
return {
|
|
165
|
-
content: [{
|
|
166
|
-
type: 'text',
|
|
167
|
-
text: JSON.stringify({
|
|
168
|
-
error: true,
|
|
169
|
-
code: 'PARSE_ERROR',
|
|
170
|
-
message,
|
|
171
|
-
workspacePath,
|
|
172
|
-
}),
|
|
173
|
-
}],
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
}
|