callgraph-mcp 1.8.0 → 1.8.1

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 CHANGED
@@ -129,9 +129,20 @@ Optional parameters shown in `[brackets]`.
129
129
  | `FLOWMAP_GRAMMARS` | *(bundled)* | Override path to WASM grammar files |
130
130
  | `FLOWMAP_BATCH_SIZE` | `50` | Files per parallel parsing batch (must be ≥ 1) |
131
131
  | `FLOWMAP_CACHE_TTL_MS` | `30000` | Result cache time-to-live in milliseconds (0 to disable) |
132
+ | `FLOWMAP_VERBOSE` | `true` | Enable/disable diagnostic log lines on stderr (`Log: ...`) |
132
133
  | `FLOWMAP_DUP_THRESHOLD` | `0.75` | Jaccard similarity threshold for `find_duplicates` (0–1) |
133
134
  | `FLOWMAP_DUP_MIN_CALLEES` | `2` | Min callee count for `find_duplicates` |
134
135
 
136
+ ### stderr output format
137
+
138
+ FlowMap writes structured runtime messages to `stderr` with a severity prefix:
139
+
140
+ - `Log: ...` for verbose progress and diagnostic output
141
+ - `Warning: ...` for recoverable configuration/runtime issues
142
+ - `Error: ...` for startup failures and other fatal errors
143
+
144
+ Set `FLOWMAP_VERBOSE=false` to suppress `Log:` lines while keeping `Warning:` and `Error:` output.
145
+
135
146
  ---
136
147
 
137
148
  ## Example Use Cases
package/dist/index.js CHANGED
@@ -1,7 +1,3 @@
1
1
  #!/usr/bin/env node
2
- "use strict";var Ue=Object.create;var Y=Object.defineProperty;var Ie=Object.getOwnPropertyDescriptor;var ze=Object.getOwnPropertyNames;var We=Object.getPrototypeOf,Je=Object.prototype.hasOwnProperty;var $e=(t,e,r,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of ze(e))!Je.call(t,n)&&n!==r&&Y(t,n,{get:()=>e[n],enumerable:!(s=Ie(e,n))||s.enumerable});return t};var O=(t,e,r)=>(r=t!=null?Ue(We(t)):{},$e(e||!t||!t.__esModule?Y(r,"default",{value:t,enumerable:!0}):r,t));var Te=require("http"),Ne=require("@modelcontextprotocol/sdk/server/mcp.js"),Ae=require("@modelcontextprotocol/sdk/server/stdio.js"),Fe=require("@modelcontextprotocol/sdk/server/streamableHttp.js"),De=require("crypto");var k=require("zod"),oe=O(require("fs"));var F=O(require("path")),K=O(require("fs")),b=require("@codeflow-map/core");var V=new Map;function ke(){let t=process.env.FLOWMAP_CACHE_TTL_MS;if(!t)return 3e4;let e=parseInt(t,10);return!isFinite(e)||e<0?(process.stderr.write(`[flowmap] Invalid FLOWMAP_CACHE_TTL_MS: "${t}" (must be non-negative integer). Using default 30000ms.
3
- `),3e4):e}var Ge=ke();function Q(t){let e=V.get(t);return e?Date.now()-e.cachedAt>Ge?(V.delete(t),null):e.graph:null}function ee(t,e){V.set(t,{graph:e,cachedAt:Date.now(),workspacePath:t})}var J=O(require("path")),te=O(require("fast-glob")),W=require("@codeflow-map/core"),He=["**/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 re(t,e={}){let{exclude:r=[],language:s}=e,n;if(s){let o=Object.entries(W.FILE_EXTENSION_MAP).filter(([,p])=>p===s).map(([p])=>p.replace(".",""));n=o.length>0?o:[]}else n=Object.keys(W.FILE_EXTENSION_MAP).map(o=>o.replace(".",""));if(n.length===0)return[];let a=n.length===1?`**/*.${n[0]}`:`**/*.{${n.join(",")}}`,i=[...He,...r],l=t.replace(/\\/g,"/"),g=await(0,te.default)(a,{cwd:l,ignore:i,absolute:!1,dot:!1,onlyFiles:!0}),d=[];for(let o of g){let p=J.extname(o),f=W.FILE_EXTENSION_MAP[p];f&&d.push({filePath:o.replace(/\\/g,"/"),absPath:J.resolve(t,o),languageId:f})}return d}function je(){let t=process.env.FLOWMAP_BATCH_SIZE;if(!t)return 50;let e=parseInt(t,10);return!isFinite(e)||e<1?(process.stderr.write(`[flowmap] Invalid FLOWMAP_BATCH_SIZE: "${t}" (must be positive integer). Using default 50.
4
- `),50):e}var se=je(),ne=!1;function $(){if(process.env.FLOWMAP_GRAMMARS)return process.env.FLOWMAP_GRAMMARS;let t=[F.resolve(__dirname,"..","grammars"),F.resolve(__dirname,"..","..","grammars")];for(let e of t)if(K.existsSync(F.join(e,"tree-sitter.wasm")))return e;return t[0]}async function Be(){if(!ne){let t=$(),e=F.join(t,"tree-sitter.wasm"),r=K.existsSync(e);console.error(`[flowmap] Grammar directory: ${t} (tree-sitter.wasm ${r?"found":"missing"})`),await(0,b.initTreeSitter)(t),ne=!0}}async function v(t,e={}){let r=Q(t);if(r)return r;await Be();let s=$(),n=Date.now(),a=await re(t,e),i=[],l=[],g=0;for(let u=0;u<a.length;u+=se){let m=a.slice(u,u+se),y=await Promise.all(m.map(S=>(0,b.parseFile)(S.filePath,S.absPath,s,S.languageId).catch(()=>null)));for(let S of y)S&&(i.push(...S.functions),l.push(...S.calls),g++)}let d=(0,b.buildCallGraph)(i,l);(0,b.detectEntryPoints)(i,d);let{flows:o,orphans:p}=(0,b.partitionFlows)(i,d),f={nodes:i,edges:d,flows:o,orphans:p,scannedFiles:g,durationMs:Date.now()-n};return ee(t,f),f}function x(t,e,r,s,n){t.tool(e,r,s,n)}var h=class{constructor(e){this.toolName=e;this.logToStderr(`[${e}] Starting analysis...`)}steps=[];startTime=Date.now();stageStartTime=Date.now();currentStep=0;reportProgress(e){let r=Date.now(),s=r-this.stageStartTime;this.currentStep++,this.steps.push({step:this.currentStep,stage:e,timestamp:r,durationMs:s}),this.logToStderr(`[${this.toolName}] Step ${this.currentStep}: ${e} (${s}ms)`),this.stageStartTime=r}getProgress(){return this.steps}getTotalDurationMs(){return Date.now()-this.startTime}getSummary(){let e=this.getTotalDurationMs();return`${this.steps.length} steps in ${e}ms`}logToStderr(e){process.stderr.write(e+`
5
- `)}};var Ve=["typescript","javascript","python","java","go","rust","tsx","jsx"],Ke=["node_modules","dist",".git","__pycache__","*.test.*","*.spec.*"];function ie(t){x(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:k.z.string().describe("Absolute path to the repository root"),exclude:k.z.string().optional().describe("Comma-separated glob patterns to exclude. Defaults: node_modules,dist,.git,__pycache__,*.test.*,*.spec.*"),language:k.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:r,language:s})=>{let n=new h("flowmap_analyze_workspace");try{if(n.reportProgress("Validating workspace path"),!oe.existsSync(e))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${e}`,workspacePath:e})}]};let a=r?r.split(",").map(g=>g.trim()).filter(Boolean):Ke,i=s&&Ve.includes(s)?s:void 0;n.reportProgress("Starting codebase analysis");let l=await v(e,{exclude:a,language:i});return n.reportProgress("Analysis complete"),{content:[{type:"text",text:JSON.stringify({...l,progress:{steps:n.getProgress(),summary:n.getSummary()}})}]}}catch(a){let i=a instanceof Error?a.message:String(a);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:i,workspacePath:e})}]}}})}var ce=require("zod"),le=O(require("fs")),G=O(require("path")),D=require("@codeflow-map/core");var ae=!1;function pe(t){x(t,"flowmap_analyze_file","Scan a single file and return all functions defined in it, their parameters, and calls made within the file.",{filePath:ce.z.string().describe("Absolute path to the file to analyse")},async({filePath:e})=>{let r=new h("flowmap_analyze_file");try{if(r.reportProgress("Validating file path"),!le.existsSync(e))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"FILE_NOT_FOUND",message:`File does not exist: ${e}`})}]};let s=G.extname(e),n=D.FILE_EXTENSION_MAP[s];if(!n)return{content:[{type:"text",text:JSON.stringify({error:!0,code:"UNSUPPORTED_LANGUAGE",message:`Unsupported file extension: ${s}`})}]};let a=$();ae||(r.reportProgress("Initializing TreeSitter"),await(0,D.initTreeSitter)(a),ae=!0);let i=Date.now(),l=G.basename(e);r.reportProgress("Parsing file");let g=await(0,D.parseFile)(l,e,a,n);return r.reportProgress("Analysis complete"),{content:[{type:"text",text:JSON.stringify({filePath:l,functions:g.functions,calls:g.calls,durationMs:Date.now()-i,progress:{steps:r.getProgress(),summary:r.getSummary()}})}]}}catch(s){let n=s instanceof Error?s.message:String(s);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:n})}]}}})}var X=require("zod"),ge=O(require("fs"));function me(t){x(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:r})=>{let s=new h("flowmap_get_callers");try{if(s.reportProgress("Validating workspace path"),!ge.existsSync(r))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${r}`,workspacePath:r})}]};s.reportProgress("Building call graph");let n=await v(r);s.reportProgress("Searching for target function");let a=n.nodes.filter(o=>o.name===e);if(a.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:r})}]};let i=a[0],l=new Set(a.map(o=>o.id));s.reportProgress("Filtering callers");let d=n.edges.filter(o=>l.has(o.to)).map(o=>{let p=n.nodes.find(f=>f.id===o.from);return{id:o.from,name:p?.name??"unknown",filePath:p?.filePath??"unknown",startLine:p?.startLine??0,callLine:o.line}});return s.reportProgress("Analysis complete"),{content:[{type:"text",text:JSON.stringify({target:e,targetId:i.id,callers:d,count:d.length,progress:{steps:s.getProgress(),summary:s.getSummary()}})}]}}catch(n){let a=n instanceof Error?n.message:String(n);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:a,workspacePath:r})}]}}})}var Z=require("zod"),de=O(require("fs"));function ue(t){x(t,"flowmap_get_callees","Return all functions directly called by the named function. Use this to understand what a function depends on.",{functionName:Z.z.string().describe("The function name to find callees of"),workspacePath:Z.z.string().describe("Absolute path to the repository root")},async({functionName:e,workspacePath:r})=>{let s=new h("flowmap_get_callees");try{if(s.reportProgress("Validating workspace path"),!de.existsSync(r))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${r}`,workspacePath:r})}]};s.reportProgress("Building call graph");let n=await v(r);s.reportProgress("Searching for target function");let a=n.nodes.filter(o=>o.name===e);if(a.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:r})}]};let i=a[0],l=new Set(a.map(o=>o.id));s.reportProgress("Filtering callees");let d=n.edges.filter(o=>l.has(o.from)).map(o=>{let p=n.nodes.find(f=>f.id===o.to);return{id:o.to,name:p?.name??"unknown",filePath:p?.filePath??"unknown",startLine:p?.startLine??0,callLine:o.line}});return s.reportProgress("Analysis complete"),{content:[{type:"text",text:JSON.stringify({target:e,targetId:i.id,callees:d,count:d.length,progress:{steps:s.getProgress(),summary:s.getSummary()}})}]}}catch(n){let a=n instanceof Error?n.message:String(n);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:a,workspacePath:r})}]}}})}var H=require("zod"),fe=O(require("fs"));function he(t){x(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:H.z.string().describe("The starting function name"),workspacePath:H.z.string().describe("Absolute path to the repository root"),maxDepth:H.z.number().optional().describe("Maximum recursion depth. Default 10.")},async({functionName:e,workspacePath:r,maxDepth:s})=>{let n=new h("flowmap_get_flow"),a=s??10;try{if(n.reportProgress("Validating workspace path"),!fe.existsSync(r))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${r}`,workspacePath:r})}]};n.reportProgress("Building call graph");let i=await v(r);n.reportProgress("Locating start function");let l=i.nodes.filter(y=>y.name===e);if(l.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:r})}]};let g=l[0];n.reportProgress("Tracing call flow");let d=new Map;for(let y of i.edges){let S=d.get(y.from)||[],M=i.nodes.find(T=>T.id===y.to);M&&(S.push({edge:y,node:M}),d.set(y.from,S))}let o=new Set,p=[],f=[],u=0,m=[g.id];for(o.add(g.id),p.push(g);m.length>0&&u<a;){let y=[];for(let S of m){let M=d.get(S)||[];for(let{edge:T,node:_}of M)f.push(T),o.has(_.id)||(o.add(_.id),p.push(_),y.push(_.id))}m=y,u++}return n.reportProgress("Analysis complete"),{content:[{type:"text",text:JSON.stringify({entryFunction:e,nodes:p,edges:f,depth:u,totalFunctions:p.length,progress:{steps:n.getProgress(),summary:n.getSummary()}})}]}}catch(i){let l=i instanceof Error?i.message:String(i);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:l,workspacePath:r})}]}}})}var ye=require("zod"),Se=O(require("fs"));function _e(t){x(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:ye.z.string().describe("Absolute path to the repository root")},async({workspacePath:e})=>{let r=new h("flowmap_list_entry_points");try{if(r.reportProgress("Validating workspace path"),!Se.existsSync(e))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${e}`,workspacePath:e})}]};r.reportProgress("Building call graph");let s=await v(e);r.reportProgress("Filtering entry points");let a=s.nodes.filter(i=>i.isEntryPoint).map(i=>({id:i.id,name:i.name,filePath:i.filePath,startLine:i.startLine,language:i.language,isExported:i.isExported,isAsync:i.isAsync}));return r.reportProgress("Analysis complete"),{content:[{type:"text",text:JSON.stringify({entryPoints:a,count:a.length,durationMs:s.durationMs,progress:{steps:r.getProgress(),summary:r.getSummary()}})}]}}catch(s){let n=s instanceof Error?s.message:String(s);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:n,workspacePath:e})}]}}})}var xe=require("zod"),we=O(require("fs"));function ve(t){x(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:xe.z.string().describe("Absolute path to the repository root")},async({workspacePath:e})=>{let r=new h("flowmap_find_orphans");try{if(r.reportProgress("Validating workspace path"),!we.existsSync(e))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${e}`,workspacePath:e})}]};r.reportProgress("Building call graph");let s=await v(e);r.reportProgress("Identifying orphan functions");let n=s.orphans.map(a=>{let i=s.nodes.find(l=>l.id===a);return i?{id:i.id,name:i.name,filePath:i.filePath,startLine:i.startLine,language:i.language,isExported:i.isExported}:{id:a,name:"unknown",filePath:"unknown",startLine:0}});return r.reportProgress("Analysis complete"),{content:[{type:"text",text:JSON.stringify({orphans:n,count:n.length,durationMs:s.durationMs,note:"Exported functions may be used by external consumers \u2014 verify before deleting.",progress:{steps:r.getProgress(),summary:r.getSummary()}})}]}}catch(s){let n=s instanceof Error?s.message:String(s);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:n,workspacePath:e})}]}}})}var j=require("zod"),Pe=O(require("fs"));function Xe(t,e){let r=new Map,s=new Map,n=new Map,a=[],i=[],l=0,g=new Map;for(let o of t)g.set(o,[]);for(let o of e)g.has(o.from)&&g.has(o.to)&&g.get(o.from).push(o.to);function d(o){r.set(o,l),s.set(o,l),l++,a.push(o),n.set(o,!0);for(let p of g.get(o)??[])r.has(p)?n.get(p)&&s.set(o,Math.min(s.get(o),r.get(p))):(d(p),s.set(o,Math.min(s.get(o),s.get(p))));if(s.get(o)===r.get(o)){let p=[],f;do f=a.pop(),n.set(f,!1),p.push(f);while(f!==o);i.push(p)}}for(let o of t)r.has(o)||d(o);return i}function Ze(t,e){let r=new Set(t);return e.filter(s=>r.has(s.from)&&r.has(s.to)).map(s=>({from:s.from,to:s.to,line:s.line}))}function Oe(t){x(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:r=1,exclude:s})=>{let n=new h("flowmap_find_cycles");try{if(n.reportProgress("Validating workspace path"),!Pe.existsSync(e))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${e}`,workspacePath:e})}]};let i=s?s.split(",").map(m=>m.trim()).filter(Boolean):["node_modules","dist",".git","__pycache__","*.test.*","*.spec.*"];n.reportProgress("Building call graph");let l=await v(e,{exclude:i});n.reportProgress("Detecting cycle patterns");let g=l.nodes.map(m=>m.id),d=Xe(g,l.edges),o=new Set(l.edges.filter(m=>m.from===m.to).map(m=>m.from)),p=d.filter(m=>m.length>1?m.length>=r:r<=1&&o.has(m[0])),f=new Map(l.nodes.map(m=>[m.id,m]));n.reportProgress("Building cycle details");let u=p.map((m,y)=>{let S=m.map(T=>{let _=f.get(T);return _?{id:T,name:_.name,filePath:_.filePath,startLine:_.startLine,language:_.language}:{id:T,name:"unknown",filePath:"unknown",startLine:0,language:"unknown"}}),M=Ze(m,l.edges);return{cycleIndex:y+1,length:m.length,members:S,edges:M}});return n.reportProgress("Analysis complete"),{content:[{type:"text",text:JSON.stringify({cycles:u,totalCycles:u.length,durationMs:l.durationMs,scannedFiles:l.scannedFiles,progress:{steps:n.getProgress(),summary:n.getSummary()},note:u.length===0?"No cycles detected \u2014 the call graph is acyclic.":`${u.length} cycle(s) found. Cycles involving many functions or cross-module calls are the highest priority to review.`})}]}}catch(a){let i=a instanceof Error?a.message:String(a);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:i,workspacePath:e})}]}}})}var U=require("zod"),be=O(require("fs"));function Ee(t,e){if(t.size===0&&e.size===0)return 1;let r=0;for(let n of t)e.has(n)&&r++;let s=t.size+e.size-r;return s===0?0:r/s}var q=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,r){this.parent.has(e)||this.parent.set(e,e),this.parent.has(r)||this.parent.set(r,r);let s=this.find(e),n=this.find(r);s!==n&&this.parent.set(s,n)}init(e){this.parent.has(e)||this.parent.set(e,e)}clusters(){let e=new Map;for(let r of this.parent.keys()){let s=this.find(r);e.has(s)||e.set(s,[]),e.get(s).push(r)}return e}};function qe(){let t=parseFloat(process.env.FLOWMAP_DUP_THRESHOLD??"");return isFinite(t)&&t>=0&&t<=1?t:.75}function Ye(){let t=parseInt(process.env.FLOWMAP_DUP_MIN_CALLEES??"",10);return isFinite(t)&&t>=1?t:2}function Me(t){x(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:U.z.string().describe("Absolute path to the repository root"),similarityThreshold:U.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:U.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:U.z.string().optional().describe("Comma-separated glob patterns to exclude. Defaults: node_modules,dist,.git,__pycache__,*.test.*,*.spec.*")},async({workspacePath:e,similarityThreshold:r,minCallees:s,exclude:n})=>{let a=new h("flowmap_find_duplicates"),i=r??qe(),l=s??Ye();try{if(a.reportProgress("Validating workspace path"),!be.existsSync(e))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${e}`,workspacePath:e})}]};let d=n?n.split(",").map(c=>c.trim()).filter(Boolean):["node_modules","dist",".git","__pycache__","*.test.*","*.spec.*"];a.reportProgress("Building call graph");let o=await v(e,{exclude:d});a.reportProgress("Computing callee signatures");let p=i,f=l,u=new Map,m=new Map(o.nodes.map(c=>[c.id,c]));for(let c of o.nodes)u.set(c.id,new Set);for(let c of o.edges){if(c.from===c.to)continue;let A=m.get(c.to)?.name??c.to;u.get(c.from)?.add(A)}let y=o.nodes.filter(c=>(u.get(c.id)?.size??0)>=f);a.reportProgress("Comparing function signatures");let S=new q;for(let c of y)S.init(c.id);let M=new Map;for(let c=0;c<y.length;c++){let w=y[c],A=u.get(w.id);for(let C=c+1;C<y.length;C++){let N=y[C];if(w.name===N.name&&w.filePath===N.filePath)continue;let L=u.get(N.id),R=Ee(A,L);if(R>=p){S.union(w.id,N.id);let B=[w.id,N.id].sort().join("|||");M.set(B,R)}}}let T=S.clusters(),_=[],Re=1;for(let[,c]of T){if(c.length<2)continue;let w=c.map(P=>{let E=m.get(P),I=[...u.get(P)??[]].sort();return{id:P,name:E?.name??"unknown",filePath:E?.filePath??"unknown",startLine:E?.startLine??0,language:E?.language??"unknown",calleeCount:I.length,callees:I}}),A=c.map(P=>u.get(P)),C=[...A[0]].filter(P=>A.every(E=>E.has(P))).sort(),N=1,L=0;for(let P=0;P<c.length;P++)for(let E=P+1;E<c.length;E++){let I=[c[P],c[E]].sort().join("|||"),z=M.get(I)??Ee(u.get(c[P]),u.get(c[E]));z<N&&(N=z),z>L&&(L=z)}let R=new Set(w.map(P=>P.filePath)).size,B=R>1?`These ${w.length} functions across ${R} files share the same core logic. Consider extracting a shared utility that accepts parameters for any behavioural differences.`:`These ${w.length} functions in the same file appear to duplicate logic. Consider merging them or extracting a private helper.`;_.push({clusterIndex:Re++,size:w.length,members:w,sharedCallees:C,minSimilarity:Math.round(N*100)/100,maxSimilarity:Math.round(L*100)/100,suggestion:B})}return _.sort((c,w)=>w.size-c.size||w.sharedCallees.length-c.sharedCallees.length),a.reportProgress("Analysis complete"),{content:[{type:"text",text:JSON.stringify({duplicateClusters:_,totalClusters:_.length,totalFunctionsInvolved:_.reduce((c,w)=>c+w.size,0),parameters:{similarityThreshold:p,minCallees:f,envOverrides:{FLOWMAP_DUP_THRESHOLD:process.env.FLOWMAP_DUP_THRESHOLD??null,FLOWMAP_DUP_MIN_CALLEES:process.env.FLOWMAP_DUP_MIN_CALLEES??null}},durationMs:o.durationMs,scannedFiles:o.scannedFiles,progress:{steps:a.getProgress(),summary:a.getSummary()},note:_.length===0?"No functionally duplicate functions detected at the current threshold. Try lowering similarityThreshold or minCallees.":`${_.length} duplicate cluster(s) found. Each cluster is a group of functions that call the same logical dependencies and are candidates for generalisation.`})}]}}catch(g){let d=g instanceof Error?g.message:String(g);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:d,workspacePath:e})}]}}})}function Ce(){let t=new Ne.McpServer({name:"callgraph-mcp",version:"1.0.0"});return Qe(t),t}function Qe(t){ie(t),pe(t),me(t),ue(t),he(t),_e(t),ve(t),Oe(t),Me(t)}async function Le(){let t=(process.env.FLOWMAP_TRANSPORT||"stdio").toLowerCase();t==="http"||t==="sse"?await tt():await et()}async function et(){let t=Ce(),e=new Ae.StdioServerTransport;await t.connect(e)}async function tt(){let t=parseInt(process.env.FLOWMAP_PORT||"3100",10),e=Ce(),r=new Fe.StreamableHTTPServerTransport({sessionIdGenerator:()=>(0,De.randomUUID)()}),s=(0,Te.createServer)(async(n,a)=>{let i=n.url||"/";i==="/mcp"||i==="/"?await r.handleRequest(n,a):a.writeHead(404).end("Not Found")});await e.connect(r),s.listen(t,()=>{process.stderr.write(`FlowMap MCP server listening on http://localhost:${t}/mcp
6
- `)})}Le().catch(t=>{process.stderr.write(`FlowMap MCP server failed to start: ${t}
7
- `),process.exit(1)});
2
+ "use strict";var $e=Object.create;var te=Object.defineProperty;var Je=Object.getOwnPropertyDescriptor;var ke=Object.getOwnPropertyNames;var Ge=Object.getPrototypeOf,Be=Object.prototype.hasOwnProperty;var He=(t,e,r,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of ke(e))!Be.call(t,n)&&n!==r&&te(t,n,{get:()=>e[n],enumerable:!(s=Je(e,n))||s.enumerable});return t};var O=(t,e,r)=>(r=t!=null?$e(Ge(t)):{},He(e||!t||!t.__esModule?te(r,"default",{value:t,enumerable:!0}):r,t));var Le=require("http"),De=require("@modelcontextprotocol/sdk/server/mcp.js"),Ce=require("@modelcontextprotocol/sdk/server/stdio.js"),Re=require("@modelcontextprotocol/sdk/server/streamableHttp.js"),Ue=require("crypto");var B=require("zod"),le=O(require("fs"));var L=O(require("path")),q=O(require("fs")),b=require("@codeflow-map/core");function X(t,e){t.write(e+`
3
+ `)}function N(t){process.env.FLOWMAP_VERBOSE!=="false"&&X(process.stderr,`Log: ${t}`)}function $(t){X(process.stderr,`Warning: ${t}`)}function re(t){X(process.stderr,`Error: ${t}`)}var Z=new Map;function je(){let t=process.env.FLOWMAP_CACHE_TTL_MS;if(!t)return 3e4;let e=parseInt(t,10);return!isFinite(e)||e<0?($(`[flowmap] Invalid FLOWMAP_CACHE_TTL_MS: "${t}" (must be non-negative integer). Using default 30000ms.`),3e4):e}var Ve=je();function se(t){let e=Z.get(t);return e?Date.now()-e.cachedAt>Ve?(Z.delete(t),null):e.graph:null}function ne(t,e){Z.set(t,{graph:e,cachedAt:Date.now(),workspacePath:t})}var k=O(require("path")),oe=O(require("fast-glob")),J=require("@codeflow-map/core"),Ke=["**/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 ie(t,e={}){let{exclude:r=[],language:s}=e,n;if(s){let o=Object.entries(J.FILE_EXTENSION_MAP).filter(([,p])=>p===s).map(([p])=>p.replace(".",""));n=o.length>0?o:[]}else n=Object.keys(J.FILE_EXTENSION_MAP).map(o=>o.replace(".",""));if(n.length===0)return[];let a=n.length===1?`**/*.${n[0]}`:`**/*.{${n.join(",")}}`,i=[...Ke,...r],l=t.replace(/\\/g,"/"),g=await(0,oe.default)(a,{cwd:l,ignore:i,absolute:!1,dot:!1,onlyFiles:!0}),f=[];for(let o of g){let p=k.extname(o),u=J.FILE_EXTENSION_MAP[p];u&&f.push({filePath:o.replace(/\\/g,"/"),absPath:k.resolve(t,o),languageId:u})}return f}function Xe(){let t=process.env.FLOWMAP_BATCH_SIZE;if(!t)return 50;let e=parseInt(t,10);return!isFinite(e)||e<1?($(`[flowmap] Invalid FLOWMAP_BATCH_SIZE: "${t}" (must be positive integer). Using default 50.`),50):e}var ae=Xe(),ce=!1;function G(){if(process.env.FLOWMAP_GRAMMARS)return process.env.FLOWMAP_GRAMMARS;let t=[L.resolve(__dirname,"..","grammars"),L.resolve(__dirname,"..","..","grammars")];for(let e of t)if(q.existsSync(L.join(e,"tree-sitter.wasm")))return e;return t[0]}async function Ze(){if(!ce){let t=G();if(process.env.FLOWMAP_VERBOSE!=="false"){let e=L.join(t,"tree-sitter.wasm"),r=q.existsSync(e);N(`[flowmap] Grammar directory: ${t} (tree-sitter.wasm ${r?"found":"missing"})`)}await(0,b.initTreeSitter)(t),ce=!0}}async function w(t,e={}){let r=se(t);if(r)return r;await Ze();let s=G(),n=Date.now(),a=await ie(t,e),i=[],l=[],g=0;for(let d=0;d<a.length;d+=ae){let m=a.slice(d,d+ae),y=await Promise.all(m.map(S=>(0,b.parseFile)(S.filePath,S.absPath,s,S.languageId).catch(()=>null)));for(let S of y)S&&(i.push(...S.functions),l.push(...S.calls),g++)}let f=(0,b.buildCallGraph)(i,l);(0,b.detectEntryPoints)(i,f);let{flows:o,orphans:p}=(0,b.partitionFlows)(i,f),u={nodes:i,edges:f,flows:o,orphans:p,scannedFiles:g,durationMs:Date.now()-n};return ne(t,u),u}function x(t,e,r,s,n){t.tool(e,r,s,n)}var h=class{constructor(e){this.toolName=e;this.verbose&&N(`[${e}] Starting analysis...`)}steps=[];startTime=Date.now();stageStartTime=Date.now();currentStep=0;verbose=process.env.FLOWMAP_VERBOSE!=="false";reportProgress(e){let r=Date.now(),s=r-this.stageStartTime;this.currentStep++,this.steps.push({step:this.currentStep,stage:e,timestamp:r,durationMs:s}),this.verbose&&N(`[${this.toolName}] Step ${this.currentStep}: ${e} (${s}ms)`),this.stageStartTime=r}getProgress(){return this.steps}getTotalDurationMs(){return Date.now()-this.startTime}getSummary(){let e=this.getTotalDurationMs();return`${this.steps.length} steps in ${e}ms`}};var qe=["typescript","javascript","python","java","go","rust","tsx","jsx"],Ye=["node_modules","dist",".git","__pycache__","*.test.*","*.spec.*"];function pe(t){x(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:B.z.string().describe("Absolute path to the repository root"),exclude:B.z.string().optional().describe("Comma-separated glob patterns to exclude. Defaults: node_modules,dist,.git,__pycache__,*.test.*,*.spec.*"),language:B.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:r,language:s})=>{let n=new h("flowmap_analyze_workspace");try{if(n.reportProgress("Validating workspace path"),!le.existsSync(e))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${e}`,workspacePath:e})}]};let a=r?r.split(",").map(g=>g.trim()).filter(Boolean):Ye,i=s&&qe.includes(s)?s:void 0;n.reportProgress("Starting codebase analysis");let l=await w(e,{exclude:a,language:i});return n.reportProgress("Analysis complete"),{content:[{type:"text",text:JSON.stringify({...l,progress:{steps:n.getProgress(),summary:n.getSummary()}})}]}}catch(a){let i=a instanceof Error?a.message:String(a);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:i,workspacePath:e})}]}}})}var me=require("zod"),fe=O(require("fs")),H=O(require("path")),D=require("@codeflow-map/core");var ge=!1;function de(t){x(t,"flowmap_analyze_file","Scan a single file and return all functions defined in it, their parameters, and calls made within the file.",{filePath:me.z.string().describe("Absolute path to the file to analyse")},async({filePath:e})=>{let r=new h("flowmap_analyze_file");try{if(r.reportProgress("Validating file path"),!fe.existsSync(e))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"FILE_NOT_FOUND",message:`File does not exist: ${e}`})}]};let s=H.extname(e),n=D.FILE_EXTENSION_MAP[s];if(!n)return{content:[{type:"text",text:JSON.stringify({error:!0,code:"UNSUPPORTED_LANGUAGE",message:`Unsupported file extension: ${s}`})}]};let a=G();ge||(r.reportProgress("Initializing TreeSitter"),await(0,D.initTreeSitter)(a),ge=!0);let i=Date.now(),l=H.basename(e);r.reportProgress("Parsing file");let g=await(0,D.parseFile)(l,e,a,n);return r.reportProgress("Analysis complete"),{content:[{type:"text",text:JSON.stringify({filePath:l,functions:g.functions,calls:g.calls,durationMs:Date.now()-i,progress:{steps:r.getProgress(),summary:r.getSummary()}})}]}}catch(s){let n=s instanceof Error?s.message:String(s);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:n})}]}}})}var Y=require("zod"),ue=O(require("fs"));function he(t){x(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:Y.z.string().describe("The function name to find callers of"),workspacePath:Y.z.string().describe("Absolute path to the repository root")},async({functionName:e,workspacePath:r})=>{let s=new h("flowmap_get_callers");try{if(s.reportProgress("Validating workspace path"),!ue.existsSync(r))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${r}`,workspacePath:r})}]};s.reportProgress("Building call graph");let n=await w(r);s.reportProgress("Searching for target function");let a=n.nodes.filter(o=>o.name===e);if(a.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:r})}]};let i=a[0],l=new Set(a.map(o=>o.id));s.reportProgress("Filtering callers");let f=n.edges.filter(o=>l.has(o.to)).map(o=>{let p=n.nodes.find(u=>u.id===o.from);return{id:o.from,name:p?.name??"unknown",filePath:p?.filePath??"unknown",startLine:p?.startLine??0,callLine:o.line}});return s.reportProgress("Analysis complete"),{content:[{type:"text",text:JSON.stringify({target:e,targetId:i.id,callers:f,count:f.length,progress:{steps:s.getProgress(),summary:s.getSummary()}})}]}}catch(n){let a=n instanceof Error?n.message:String(n);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:a,workspacePath:r})}]}}})}var Q=require("zod"),ye=O(require("fs"));function Se(t){x(t,"flowmap_get_callees","Return all functions directly called by the named function. Use this to understand what a function depends on.",{functionName:Q.z.string().describe("The function name to find callees of"),workspacePath:Q.z.string().describe("Absolute path to the repository root")},async({functionName:e,workspacePath:r})=>{let s=new h("flowmap_get_callees");try{if(s.reportProgress("Validating workspace path"),!ye.existsSync(r))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${r}`,workspacePath:r})}]};s.reportProgress("Building call graph");let n=await w(r);s.reportProgress("Searching for target function");let a=n.nodes.filter(o=>o.name===e);if(a.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:r})}]};let i=a[0],l=new Set(a.map(o=>o.id));s.reportProgress("Filtering callees");let f=n.edges.filter(o=>l.has(o.from)).map(o=>{let p=n.nodes.find(u=>u.id===o.to);return{id:o.to,name:p?.name??"unknown",filePath:p?.filePath??"unknown",startLine:p?.startLine??0,callLine:o.line}});return s.reportProgress("Analysis complete"),{content:[{type:"text",text:JSON.stringify({target:e,targetId:i.id,callees:f,count:f.length,progress:{steps:s.getProgress(),summary:s.getSummary()}})}]}}catch(n){let a=n instanceof Error?n.message:String(n);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:a,workspacePath:r})}]}}})}var j=require("zod"),_e=O(require("fs"));function xe(t){x(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:j.z.string().describe("The starting function name"),workspacePath:j.z.string().describe("Absolute path to the repository root"),maxDepth:j.z.number().optional().describe("Maximum recursion depth. Default 10.")},async({functionName:e,workspacePath:r,maxDepth:s})=>{let n=new h("flowmap_get_flow"),a=s??10;try{if(n.reportProgress("Validating workspace path"),!_e.existsSync(r))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${r}`,workspacePath:r})}]};n.reportProgress("Building call graph");let i=await w(r);n.reportProgress("Locating start function");let l=i.nodes.filter(y=>y.name===e);if(l.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:r})}]};let g=l[0];n.reportProgress("Tracing call flow");let f=new Map;for(let y of i.edges){let S=f.get(y.from)||[],M=i.nodes.find(T=>T.id===y.to);M&&(S.push({edge:y,node:M}),f.set(y.from,S))}let o=new Set,p=[],u=[],d=0,m=[g.id];for(o.add(g.id),p.push(g);m.length>0&&d<a;){let y=[];for(let S of m){let M=f.get(S)||[];for(let{edge:T,node:_}of M)u.push(T),o.has(_.id)||(o.add(_.id),p.push(_),y.push(_.id))}m=y,d++}return n.reportProgress("Analysis complete"),{content:[{type:"text",text:JSON.stringify({entryFunction:e,nodes:p,edges:u,depth:d,totalFunctions:p.length,progress:{steps:n.getProgress(),summary:n.getSummary()}})}]}}catch(i){let l=i instanceof Error?i.message:String(i);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:l,workspacePath:r})}]}}})}var ve=require("zod"),we=O(require("fs"));function Pe(t){x(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:ve.z.string().describe("Absolute path to the repository root")},async({workspacePath:e})=>{let r=new h("flowmap_list_entry_points");try{if(r.reportProgress("Validating workspace path"),!we.existsSync(e))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${e}`,workspacePath:e})}]};r.reportProgress("Building call graph");let s=await w(e);r.reportProgress("Filtering entry points");let a=s.nodes.filter(i=>i.isEntryPoint).map(i=>({id:i.id,name:i.name,filePath:i.filePath,startLine:i.startLine,language:i.language,isExported:i.isExported,isAsync:i.isAsync}));return r.reportProgress("Analysis complete"),{content:[{type:"text",text:JSON.stringify({entryPoints:a,count:a.length,durationMs:s.durationMs,progress:{steps:r.getProgress(),summary:r.getSummary()}})}]}}catch(s){let n=s instanceof Error?s.message:String(s);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:n,workspacePath:e})}]}}})}var Oe=require("zod"),Ee=O(require("fs"));function be(t){x(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:Oe.z.string().describe("Absolute path to the repository root")},async({workspacePath:e})=>{let r=new h("flowmap_find_orphans");try{if(r.reportProgress("Validating workspace path"),!Ee.existsSync(e))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${e}`,workspacePath:e})}]};r.reportProgress("Building call graph");let s=await w(e);r.reportProgress("Identifying orphan functions");let n=s.orphans.map(a=>{let i=s.nodes.find(l=>l.id===a);return i?{id:i.id,name:i.name,filePath:i.filePath,startLine:i.startLine,language:i.language,isExported:i.isExported}:{id:a,name:"unknown",filePath:"unknown",startLine:0}});return r.reportProgress("Analysis complete"),{content:[{type:"text",text:JSON.stringify({orphans:n,count:n.length,durationMs:s.durationMs,note:"Exported functions may be used by external consumers \u2014 verify before deleting.",progress:{steps:r.getProgress(),summary:r.getSummary()}})}]}}catch(s){let n=s instanceof Error?s.message:String(s);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:n,workspacePath:e})}]}}})}var V=require("zod"),Me=O(require("fs"));function Qe(t,e){let r=new Map,s=new Map,n=new Map,a=[],i=[],l=0,g=new Map;for(let o of t)g.set(o,[]);for(let o of e)g.has(o.from)&&g.has(o.to)&&g.get(o.from).push(o.to);function f(o){r.set(o,l),s.set(o,l),l++,a.push(o),n.set(o,!0);for(let p of g.get(o)??[])r.has(p)?n.get(p)&&s.set(o,Math.min(s.get(o),r.get(p))):(f(p),s.set(o,Math.min(s.get(o),s.get(p))));if(s.get(o)===r.get(o)){let p=[],u;do u=a.pop(),n.set(u,!1),p.push(u);while(u!==o);i.push(p)}}for(let o of t)r.has(o)||f(o);return i}function et(t,e){let r=new Set(t);return e.filter(s=>r.has(s.from)&&r.has(s.to)).map(s=>({from:s.from,to:s.to,line:s.line}))}function Te(t){x(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:V.z.string().describe("Absolute path to the repository root"),minCycleLength:V.z.number().int().min(1).optional().describe("Minimum number of functions in a cycle to report (default: 1, includes self-recursion)"),exclude:V.z.string().optional().describe("Comma-separated glob patterns to exclude. Defaults: node_modules,dist,.git,__pycache__,*.test.*,*.spec.*")},async({workspacePath:e,minCycleLength:r=1,exclude:s})=>{let n=new h("flowmap_find_cycles");try{if(n.reportProgress("Validating workspace path"),!Me.existsSync(e))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${e}`,workspacePath:e})}]};let i=s?s.split(",").map(m=>m.trim()).filter(Boolean):["node_modules","dist",".git","__pycache__","*.test.*","*.spec.*"];n.reportProgress("Building call graph");let l=await w(e,{exclude:i});n.reportProgress("Detecting cycle patterns");let g=l.nodes.map(m=>m.id),f=Qe(g,l.edges),o=new Set(l.edges.filter(m=>m.from===m.to).map(m=>m.from)),p=f.filter(m=>m.length>1?m.length>=r:r<=1&&o.has(m[0])),u=new Map(l.nodes.map(m=>[m.id,m]));n.reportProgress("Building cycle details");let d=p.map((m,y)=>{let S=m.map(T=>{let _=u.get(T);return _?{id:T,name:_.name,filePath:_.filePath,startLine:_.startLine,language:_.language}:{id:T,name:"unknown",filePath:"unknown",startLine:0,language:"unknown"}}),M=et(m,l.edges);return{cycleIndex:y+1,length:m.length,members:S,edges:M}});return n.reportProgress("Analysis complete"),{content:[{type:"text",text:JSON.stringify({cycles:d,totalCycles:d.length,durationMs:l.durationMs,scannedFiles:l.scannedFiles,progress:{steps:n.getProgress(),summary:n.getSummary()},note:d.length===0?"No cycles detected \u2014 the call graph is acyclic.":`${d.length} cycle(s) found. Cycles involving many functions or cross-module calls are the highest priority to review.`})}]}}catch(a){let i=a instanceof Error?a.message:String(a);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:i,workspacePath:e})}]}}})}var I=require("zod"),Ne=O(require("fs"));function Ae(t,e){if(t.size===0&&e.size===0)return 1;let r=0;for(let n of t)e.has(n)&&r++;let s=t.size+e.size-r;return s===0?0:r/s}var ee=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,r){this.parent.has(e)||this.parent.set(e,e),this.parent.has(r)||this.parent.set(r,r);let s=this.find(e),n=this.find(r);s!==n&&this.parent.set(s,n)}init(e){this.parent.has(e)||this.parent.set(e,e)}clusters(){let e=new Map;for(let r of this.parent.keys()){let s=this.find(r);e.has(s)||e.set(s,[]),e.get(s).push(r)}return e}};function tt(){let t=parseFloat(process.env.FLOWMAP_DUP_THRESHOLD??"");return isFinite(t)&&t>=0&&t<=1?t:.75}function rt(){let t=parseInt(process.env.FLOWMAP_DUP_MIN_CALLEES??"",10);return isFinite(t)&&t>=1?t:2}function Fe(t){x(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:I.z.string().describe("Absolute path to the repository root"),similarityThreshold:I.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:I.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:I.z.string().optional().describe("Comma-separated glob patterns to exclude. Defaults: node_modules,dist,.git,__pycache__,*.test.*,*.spec.*")},async({workspacePath:e,similarityThreshold:r,minCallees:s,exclude:n})=>{let a=new h("flowmap_find_duplicates"),i=r??tt(),l=s??rt();try{if(a.reportProgress("Validating workspace path"),!Ne.existsSync(e))return{content:[{type:"text",text:JSON.stringify({error:!0,code:"WORKSPACE_NOT_FOUND",message:`Directory does not exist: ${e}`,workspacePath:e})}]};let f=n?n.split(",").map(c=>c.trim()).filter(Boolean):["node_modules","dist",".git","__pycache__","*.test.*","*.spec.*"];a.reportProgress("Building call graph");let o=await w(e,{exclude:f});a.reportProgress("Computing callee signatures");let p=i,u=l,d=new Map,m=new Map(o.nodes.map(c=>[c.id,c]));for(let c of o.nodes)d.set(c.id,new Set);for(let c of o.edges){if(c.from===c.to)continue;let F=m.get(c.to)?.name??c.to;d.get(c.from)?.add(F)}let y=o.nodes.filter(c=>(d.get(c.id)?.size??0)>=u);a.reportProgress("Comparing function signatures");let S=new ee;for(let c of y)S.init(c.id);let M=new Map;for(let c=0;c<y.length;c++){let v=y[c],F=d.get(v.id);for(let C=c+1;C<y.length;C++){let A=y[C];if(v.name===A.name&&v.filePath===A.filePath)continue;let R=d.get(A.id),U=Ae(F,R);if(U>=p){S.union(v.id,A.id);let K=[v.id,A.id].sort().join("|||");M.set(K,U)}}}let T=S.clusters(),_=[],ze=1;for(let[,c]of T){if(c.length<2)continue;let v=c.map(P=>{let E=m.get(P),W=[...d.get(P)??[]].sort();return{id:P,name:E?.name??"unknown",filePath:E?.filePath??"unknown",startLine:E?.startLine??0,language:E?.language??"unknown",calleeCount:W.length,callees:W}}),F=c.map(P=>d.get(P)),C=[...F[0]].filter(P=>F.every(E=>E.has(P))).sort(),A=1,R=0;for(let P=0;P<c.length;P++)for(let E=P+1;E<c.length;E++){let W=[c[P],c[E]].sort().join("|||"),z=M.get(W)??Ae(d.get(c[P]),d.get(c[E]));z<A&&(A=z),z>R&&(R=z)}let U=new Set(v.map(P=>P.filePath)).size,K=U>1?`These ${v.length} functions across ${U} files share the same core logic. Consider extracting a shared utility that accepts parameters for any behavioural differences.`:`These ${v.length} functions in the same file appear to duplicate logic. Consider merging them or extracting a private helper.`;_.push({clusterIndex:ze++,size:v.length,members:v,sharedCallees:C,minSimilarity:Math.round(A*100)/100,maxSimilarity:Math.round(R*100)/100,suggestion:K})}return _.sort((c,v)=>v.size-c.size||v.sharedCallees.length-c.sharedCallees.length),a.reportProgress("Analysis complete"),{content:[{type:"text",text:JSON.stringify({duplicateClusters:_,totalClusters:_.length,totalFunctionsInvolved:_.reduce((c,v)=>c+v.size,0),parameters:{similarityThreshold:p,minCallees:u,envOverrides:{FLOWMAP_DUP_THRESHOLD:process.env.FLOWMAP_DUP_THRESHOLD??null,FLOWMAP_DUP_MIN_CALLEES:process.env.FLOWMAP_DUP_MIN_CALLEES??null}},durationMs:o.durationMs,scannedFiles:o.scannedFiles,progress:{steps:a.getProgress(),summary:a.getSummary()},note:_.length===0?"No functionally duplicate functions detected at the current threshold. Try lowering similarityThreshold or minCallees.":`${_.length} duplicate cluster(s) found. Each cluster is a group of functions that call the same logical dependencies and are candidates for generalisation.`})}]}}catch(g){let f=g instanceof Error?g.message:String(g);return{content:[{type:"text",text:JSON.stringify({error:!0,code:"PARSE_ERROR",message:f,workspacePath:e})}]}}})}function Ie(){let t=new De.McpServer({name:"callgraph-mcp",version:"1.0.0"});return st(t),t}function st(t){pe(t),de(t),he(t),Se(t),xe(t),Pe(t),be(t),Te(t),Fe(t)}async function We(){let t=(process.env.FLOWMAP_TRANSPORT||"stdio").toLowerCase();t==="http"||t==="sse"?await ot():await nt()}async function nt(){let t=Ie(),e=new Ce.StdioServerTransport;await t.connect(e)}async function ot(){let t=parseInt(process.env.FLOWMAP_PORT||"3100",10),e=Ie(),r=new Re.StreamableHTTPServerTransport({sessionIdGenerator:()=>(0,Ue.randomUUID)()}),s=(0,Le.createServer)(async(n,a)=>{let i=n.url||"/";i==="/mcp"||i==="/"?await r.handleRequest(n,a):a.writeHead(404).end("Not Found")});await e.connect(r),s.listen(t,()=>{N(`FlowMap MCP server listening on http://localhost:${t}/mcp`)})}We().catch(t=>{re(`FlowMap MCP server failed to start: ${t}`),process.exit(1)});
@@ -0,0 +1,3 @@
1
+ export declare function logVerbose(message: string): void;
2
+ export declare function logWarning(message: string): void;
3
+ export declare function logError(message: string): void;
@@ -1,6 +1,3 @@
1
- /**
2
- * Progress tracker for tools to report operation stages and timing
3
- */
4
1
  export interface ProgressStep {
5
2
  step: number;
6
3
  stage: string;
@@ -13,10 +10,10 @@ export declare class ProgressTracker {
13
10
  private startTime;
14
11
  private stageStartTime;
15
12
  private currentStep;
13
+ private verbose;
16
14
  constructor(toolName: string);
17
15
  reportProgress(stage: string): void;
18
16
  getProgress(): ProgressStep[];
19
17
  getTotalDurationMs(): number;
20
18
  getSummary(): string;
21
- private logToStderr;
22
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "callgraph-mcp",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
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",