1818import java .util .Deque ;
1919import java .util .HashSet ;
2020import java .util .Set ;
21- import java .util .concurrent .atomic .AtomicInteger ;
2221
2322public class CallChainTool implements MCPTool {
2423 @ Override
@@ -60,6 +59,8 @@ public ToolResult execute(JsonObject arguments) throws Exception {
6059 String direction = normalizeDirection (JsonUtils .getString (arguments , "direction" , "both" ));
6160 int depth = JsonUtils .getInt (arguments , "depth" , 3 );
6261 int maxNodes = JsonUtils .getInt (arguments , "maxNodes" , 128 );
62+ if (depth < 1 ) depth = 1 ;
63+ if (maxNodes < 1 ) maxNodes = 1 ;
6364
6465 var candidates = index .resolveMethodCandidates (owner , methodName , descriptor );
6566 var broadCandidates = (descriptor != null && !descriptor .isBlank ()) ? index .resolveMethodCandidates (owner , methodName , null ) : candidates ;
@@ -74,7 +75,6 @@ public ToolResult execute(JsonObject arguments) throws Exception {
7475 }
7576
7677 ScopedMethodRef root = new ScopedMethodRef (candidates .get (0 ).sourcePath (), candidates .get (0 ).indexedMethod ().ref ());
77- AtomicInteger remaining = new AtomicInteger (Math .max (1 , maxNodes ));
7878 JsonObject structured = GraphSupport .graphMeta (direction , depth , maxNodes );
7979 structured .addProperty ("resolved" , true );
8080 structured .addProperty ("indexBackend" , "sqlite" );
@@ -84,16 +84,18 @@ public ToolResult execute(JsonObject arguments) throws Exception {
8484 structured .add ("root" , methodJson (root ));
8585
8686 StringBuilder text = new StringBuilder ();
87- text .append ("Call chain for " ).append (root .methodRef (). displayName ( )).append (" @ " ).append (root .sourcePath ()).append ('\n' );
87+ text .append ("Call chain for " ).append (humanSignature ( root .methodRef ())).append (" @ " ).append (root .sourcePath ()).append ('\n' );
8888
8989 if ("callers" .equals (direction ) || "both" .equals (direction )) {
9090 text .append ("\n Callers:\n " );
91- JsonObject callersTree = buildTreeBfs (index , root , true , depth , remaining , new HashSet <>(), structured , text );
91+ int [] callerBudget = {maxNodes };
92+ JsonObject callersTree = buildTreeBfs (index , root , true , depth , callerBudget , new HashSet <>(), structured , text );
9293 structured .add ("callers" , callersTree );
9394 }
9495 if ("callees" .equals (direction ) || "both" .equals (direction )) {
9596 text .append ("\n Callees:\n " );
96- JsonObject calleesTree = buildTreeBfs (index , root , false , depth , remaining , new HashSet <>(), structured , text );
97+ int [] calleeBudget = {maxNodes };
98+ JsonObject calleesTree = buildTreeBfs (index , root , false , depth , calleeBudget , new HashSet <>(), structured , text );
9799 structured .add ("callees" , calleesTree );
98100 }
99101 structured .addProperty ("queryMillis" , (System .nanoTime () - startedAt ) / 1_000_000L );
@@ -105,7 +107,7 @@ private static JsonObject buildTreeBfs(PersistentScopeIndex index,
105107 ScopedMethodRef root ,
106108 boolean reverse ,
107109 int depth ,
108- AtomicInteger remaining ,
110+ int [] remaining ,
109111 Set <ScopedMethodRef > visited ,
110112 JsonObject graphMeta ,
111113 StringBuilder text ) throws Exception {
@@ -114,14 +116,14 @@ private static JsonObject buildTreeBfs(PersistentScopeIndex index,
114116 return rootNode ;
115117 }
116118 rootNode .add ("children" , new JsonArray ());
117- text .append ("- " ). append (root .methodRef (). displayName ( )).append (" @ " ).append (root .sourcePath ()).append ('\n' );
119+ text .append (humanSignature (root .methodRef ())).append (" @ " ).append (root .sourcePath ()).append ('\n' );
118120
119- record BfsFrame (ScopedMethodRef method , JsonObject parentNode , int currentDepth ) {}
121+ record BfsFrame (ScopedMethodRef method , JsonObject parentNode , int currentDepth , String prefix ) {}
120122 Deque <BfsFrame > queue = new ArrayDeque <>();
121- queue .addLast (new BfsFrame (root , rootNode , 0 ));
123+ queue .addLast (new BfsFrame (root , rootNode , 0 , "" ));
122124 visited .add (root );
123125
124- while (!queue .isEmpty () && remaining . get () > 0 && depth > 0 ) {
126+ while (!queue .isEmpty () && remaining [ 0 ] > 0 ) {
125127 BfsFrame frame = queue .removeFirst ();
126128 if (frame .currentDepth () >= depth ) {
127129 continue ;
@@ -131,14 +133,19 @@ record BfsFrame(ScopedMethodRef method, JsonObject parentNode, int currentDepth)
131133 ? index .incomingScoped (frame .method ().methodRef ())
132134 : index .outgoingScoped (frame .method ().methodRef ());
133135
134- for (ScopedMethodRef neighbor : neighbors ) {
135- if (remaining .get () <= 0 ) {
136+ var eligible = new java .util .ArrayList <ScopedMethodRef >();
137+ for (ScopedMethodRef n : neighbors ) {
138+ if (remaining [0 ] <= 0 ) break ;
139+ if (!visited .contains (n )) eligible .add (n );
140+ }
141+
142+ for (int idx = 0 ; idx < eligible .size (); idx ++) {
143+ if (remaining [0 ] <= 0 ) {
136144 graphMeta .addProperty ("truncated" , true );
137145 break ;
138146 }
139- if (visited .contains (neighbor )) {
140- continue ;
141- }
147+ ScopedMethodRef neighbor = eligible .get (idx );
148+ boolean last = (idx == eligible .size () - 1 );
142149 visited .add (neighbor );
143150
144151 JsonObject childNode = methodJson (neighbor );
@@ -148,17 +155,20 @@ record BfsFrame(ScopedMethodRef method, JsonObject parentNode, int currentDepth)
148155 }
149156 childNode .add ("children" , new JsonArray ());
150157 frame .parentNode ().getAsJsonArray ("children" ).add (childNode );
151- text .append (" " .repeat (frame .currentDepth () + 1 ))
152- .append ("- " ).append (neighbor .methodRef ().displayName ())
158+
159+ String branch = last ? "└── " : "├── " ;
160+ text .append (frame .prefix ()).append (branch )
161+ .append (humanSignature (neighbor .methodRef ()))
153162 .append (" @ " ).append (neighbor .sourcePath ()).append ('\n' );
154163
155164 if (frame .currentDepth () + 1 < depth ) {
156- queue .addLast (new BfsFrame (neighbor , childNode , frame .currentDepth () + 1 ));
165+ String childPrefix = frame .prefix () + (last ? " " : "│ " );
166+ queue .addLast (new BfsFrame (neighbor , childNode , frame .currentDepth () + 1 , childPrefix ));
157167 }
158168 }
159169 }
160170
161- if (remaining . get () <= 0 ) {
171+ if (remaining [ 0 ] <= 0 ) {
162172 graphMeta .addProperty ("truncated" , true );
163173 }
164174 return rootNode ;
@@ -172,10 +182,67 @@ private static JsonObject methodJson(ScopedMethodRef scopedMethod) {
172182 json .addProperty ("displayOwner" , method .owner ().replace ('/' , '.' ));
173183 json .addProperty ("name" , method .name ());
174184 json .addProperty ("descriptor" , method .descriptor ());
185+ var sig = formatSignature (method .descriptor ());
186+ json .addProperty ("params" , sig .params ());
187+ json .addProperty ("returnType" , sig .returnType ());
188+ json .addProperty ("signature" , sig .params () + ": " + sig .returnType ());
175189 json .addProperty ("displayName" , method .displayName ());
176190 return json ;
177191 }
178192
193+ private static String humanSignature (MethodRef method ) {
194+ var sig = formatSignature (method .descriptor ());
195+ return method .owner ().replace ('/' , '.' ) + "." + method .name () + sig .params () + ": " + sig .returnType ();
196+ }
197+
198+ private record MethodSignature (String params , String returnType ) {}
199+
200+ private static MethodSignature formatSignature (String descriptor ) {
201+ int end = descriptor .indexOf (')' );
202+ if (end < 0 ) return new MethodSignature ("()" , "void" );
203+ StringBuilder params = new StringBuilder ("(" );
204+ int i = 1 ;
205+ while (i < end ) {
206+ if (params .length () > 1 ) params .append (", " );
207+ i = appendTypeName (descriptor , i , params );
208+ }
209+ params .append (')' );
210+ StringBuilder ret = new StringBuilder ();
211+ if (end + 1 < descriptor .length ()) {
212+ appendTypeName (descriptor , end + 1 , ret );
213+ } else {
214+ ret .append ("void" );
215+ }
216+ return new MethodSignature (params .toString (), ret .toString ());
217+ }
218+
219+ private static int appendTypeName (String descriptor , int pos , StringBuilder sb ) {
220+ char ch = descriptor .charAt (pos );
221+ return switch (ch ) {
222+ case 'B' -> { sb .append ("byte" ); yield pos + 1 ; }
223+ case 'C' -> { sb .append ("char" ); yield pos + 1 ; }
224+ case 'D' -> { sb .append ("double" ); yield pos + 1 ; }
225+ case 'F' -> { sb .append ("float" ); yield pos + 1 ; }
226+ case 'I' -> { sb .append ("int" ); yield pos + 1 ; }
227+ case 'J' -> { sb .append ("long" ); yield pos + 1 ; }
228+ case 'S' -> { sb .append ("short" ); yield pos + 1 ; }
229+ case 'Z' -> { sb .append ("boolean" ); yield pos + 1 ; }
230+ case 'V' -> { sb .append ("void" ); yield pos + 1 ; }
231+ case 'L' -> {
232+ int end = descriptor .indexOf (';' , pos );
233+ String full = descriptor .substring (pos + 1 , end );
234+ sb .append (full .substring (full .lastIndexOf ('/' ) + 1 ));
235+ yield end + 1 ;
236+ }
237+ case '[' -> {
238+ int nextPos = appendTypeName (descriptor , pos + 1 , sb );
239+ sb .append ("[]" );
240+ yield nextPos ;
241+ }
242+ default -> pos + 1 ;
243+ };
244+ }
245+
179246 private static String normalizeDirection (String direction ) {
180247 String normalized = direction == null ? "both" : direction .trim ().toLowerCase ();
181248 return switch (normalized ) {
0 commit comments