Skip to content

Commit b9f4c82

Browse files
author
Your Name
committed
fix(mcp): resolve trace_call_path through Class DEFINES_METHOD edges
When trace_call_path targets a Class or Interface node, the BFS now resolves through DEFINES_METHOD edges to find the actual callable methods, then runs BFS from each method and merges results. Previously, tracing a class name returned 0 results because Class nodes have no direct CALLS edges — only their Method children do. Also expands edge types to include HTTP_CALLS and ASYNC_CALLS alongside CALLS for broader cross-service coverage. Node selection improved: when multiple nodes share the same name (e.g. a Class and its constructor Method), prefer the Class for resolution since constructors rarely have interesting outbound CALLS. Tested: DataBuilder in cube (C# monolith) went from 0 to 87 callees and 8 callers. TS repos (simcapture-cloud) unchanged at 50 callers.
1 parent a7d6403 commit b9f4c82

File tree

1 file changed

+130
-30
lines changed

1 file changed

+130
-30
lines changed

src/mcp/mcp.c

Lines changed: 130 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,15 +1265,94 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
12651265
return cbm_mcp_text_result("{\"error\":\"function not found\"}", true);
12661266
}
12671267

1268+
/* Pick the best node for tracing. Strategy:
1269+
* 1. Prefer Function/Method nodes that are NOT constructors (same name as a
1270+
* Class in the result set — constructors rarely have interesting CALLS).
1271+
* 2. If only Class/Interface nodes match, resolve through DEFINES_METHOD. */
1272+
int best_idx = 0;
1273+
bool has_class = false;
1274+
int class_idx = -1;
1275+
for (int i = 0; i < node_count; i++) {
1276+
const char *lbl = nodes[i].label;
1277+
if (lbl && (strcmp(lbl, "Class") == 0 || strcmp(lbl, "Interface") == 0)) {
1278+
has_class = true;
1279+
if (class_idx < 0) class_idx = i;
1280+
}
1281+
}
1282+
/* Look for a non-constructor Function/Method */
1283+
bool found_callable = false;
1284+
for (int i = 0; i < node_count; i++) {
1285+
const char *lbl = nodes[i].label;
1286+
if (lbl && (strcmp(lbl, "Function") == 0 || strcmp(lbl, "Method") == 0)) {
1287+
/* Skip if this is a constructor (same name as a Class in results) */
1288+
if (has_class) continue;
1289+
best_idx = i;
1290+
found_callable = true;
1291+
break;
1292+
}
1293+
}
1294+
/* If no non-constructor callable was found but we have a Class, use the Class */
1295+
if (!found_callable && class_idx >= 0) {
1296+
best_idx = class_idx;
1297+
}
1298+
1299+
/* Determine if the selected node is a Class or Interface. If so, we need to
1300+
* resolve through DEFINES_METHOD edges to find the actual callable methods,
1301+
* then run BFS from each method and merge results. */
1302+
bool is_class_like = false;
1303+
const char *best_label = nodes[best_idx].label;
1304+
if (best_label &&
1305+
(strcmp(best_label, "Class") == 0 || strcmp(best_label, "Interface") == 0)) {
1306+
is_class_like = true;
1307+
}
1308+
1309+
/* Collect BFS start IDs: either the single node, or all methods of the class */
1310+
int64_t *start_ids = NULL;
1311+
int start_id_count = 0;
1312+
1313+
if (is_class_like) {
1314+
/* Find all DEFINES_METHOD targets of this class */
1315+
cbm_edge_t *dm_edges = NULL;
1316+
int dm_count = 0;
1317+
cbm_store_find_edges_by_source_type(store, nodes[best_idx].id, "DEFINES_METHOD",
1318+
&dm_edges, &dm_count);
1319+
if (dm_count > 0) {
1320+
start_ids = malloc((size_t)dm_count * sizeof(int64_t));
1321+
for (int i = 0; i < dm_count; i++) {
1322+
start_ids[i] = dm_edges[i].target_id;
1323+
}
1324+
start_id_count = dm_count;
1325+
}
1326+
/* Free edge data */
1327+
for (int i = 0; i < dm_count; i++) {
1328+
free((void *)dm_edges[i].project);
1329+
free((void *)dm_edges[i].type);
1330+
free((void *)dm_edges[i].properties_json);
1331+
}
1332+
free(dm_edges);
1333+
1334+
/* If no methods found, fall back to the class node itself */
1335+
if (start_id_count == 0) {
1336+
start_ids = malloc(sizeof(int64_t));
1337+
start_ids[0] = nodes[best_idx].id;
1338+
start_id_count = 1;
1339+
}
1340+
} else {
1341+
start_ids = malloc(sizeof(int64_t));
1342+
start_ids[0] = nodes[best_idx].id;
1343+
start_id_count = 1;
1344+
}
1345+
12681346
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
12691347
yyjson_mut_val *root = yyjson_mut_obj(doc);
12701348
yyjson_mut_doc_set_root(doc, root);
12711349

12721350
yyjson_mut_obj_add_str(doc, root, "function", func_name);
12731351
yyjson_mut_obj_add_str(doc, root, "direction", direction);
12741352

1275-
const char *edge_types[] = {"CALLS"};
1276-
int edge_type_count = 1;
1353+
/* Include HTTP_CALLS and ASYNC_CALLS alongside CALLS for broader coverage */
1354+
const char *edge_types[] = {"CALLS", "HTTP_CALLS", "ASYNC_CALLS"};
1355+
int edge_type_count = 3;
12771356

12781357
/* Run BFS for each requested direction.
12791358
* IMPORTANT: yyjson_mut_obj_add_str borrows pointers — we must keep
@@ -1283,41 +1362,59 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
12831362
// NOLINTNEXTLINE(readability-implicit-bool-conversion)
12841363
bool do_inbound = strcmp(direction, "inbound") == 0 || strcmp(direction, "both") == 0;
12851364

1286-
cbm_traverse_result_t tr_out = {0};
1287-
cbm_traverse_result_t tr_in = {0};
1365+
/* For class resolution, we run BFS from each method and merge results.
1366+
* We keep all traversal results alive until after JSON serialization. */
1367+
cbm_traverse_result_t *all_tr_out = NULL;
1368+
cbm_traverse_result_t *all_tr_in = NULL;
1369+
int tr_out_count = 0;
1370+
int tr_in_count = 0;
12881371

12891372
if (do_outbound) {
1290-
cbm_store_bfs(store, nodes[0].id, "outbound", edge_types, edge_type_count, depth, 100,
1291-
&tr_out);
1373+
all_tr_out = calloc((size_t)start_id_count, sizeof(cbm_traverse_result_t));
1374+
tr_out_count = start_id_count;
12921375

12931376
yyjson_mut_val *callees = yyjson_mut_arr(doc);
1294-
for (int i = 0; i < tr_out.visited_count; i++) {
1295-
yyjson_mut_val *item = yyjson_mut_obj(doc);
1296-
yyjson_mut_obj_add_str(doc, item, "name",
1297-
tr_out.visited[i].node.name ? tr_out.visited[i].node.name : "");
1298-
yyjson_mut_obj_add_str(
1299-
doc, item, "qualified_name",
1300-
tr_out.visited[i].node.qualified_name ? tr_out.visited[i].node.qualified_name : "");
1301-
yyjson_mut_obj_add_int(doc, item, "hop", tr_out.visited[i].hop);
1302-
yyjson_mut_arr_add_val(callees, item);
1377+
for (int s = 0; s < start_id_count; s++) {
1378+
cbm_store_bfs(store, start_ids[s], "outbound", edge_types, edge_type_count, depth, 100,
1379+
&all_tr_out[s]);
1380+
for (int i = 0; i < all_tr_out[s].visited_count; i++) {
1381+
yyjson_mut_val *item = yyjson_mut_obj(doc);
1382+
yyjson_mut_obj_add_str(
1383+
doc, item, "name",
1384+
all_tr_out[s].visited[i].node.name ? all_tr_out[s].visited[i].node.name : "");
1385+
yyjson_mut_obj_add_str(
1386+
doc, item, "qualified_name",
1387+
all_tr_out[s].visited[i].node.qualified_name
1388+
? all_tr_out[s].visited[i].node.qualified_name
1389+
: "");
1390+
yyjson_mut_obj_add_int(doc, item, "hop", all_tr_out[s].visited[i].hop);
1391+
yyjson_mut_arr_add_val(callees, item);
1392+
}
13031393
}
13041394
yyjson_mut_obj_add_val(doc, root, "callees", callees);
13051395
}
13061396

13071397
if (do_inbound) {
1308-
cbm_store_bfs(store, nodes[0].id, "inbound", edge_types, edge_type_count, depth, 100,
1309-
&tr_in);
1398+
all_tr_in = calloc((size_t)start_id_count, sizeof(cbm_traverse_result_t));
1399+
tr_in_count = start_id_count;
13101400

13111401
yyjson_mut_val *callers = yyjson_mut_arr(doc);
1312-
for (int i = 0; i < tr_in.visited_count; i++) {
1313-
yyjson_mut_val *item = yyjson_mut_obj(doc);
1314-
yyjson_mut_obj_add_str(doc, item, "name",
1315-
tr_in.visited[i].node.name ? tr_in.visited[i].node.name : "");
1316-
yyjson_mut_obj_add_str(
1317-
doc, item, "qualified_name",
1318-
tr_in.visited[i].node.qualified_name ? tr_in.visited[i].node.qualified_name : "");
1319-
yyjson_mut_obj_add_int(doc, item, "hop", tr_in.visited[i].hop);
1320-
yyjson_mut_arr_add_val(callers, item);
1402+
for (int s = 0; s < start_id_count; s++) {
1403+
cbm_store_bfs(store, start_ids[s], "inbound", edge_types, edge_type_count, depth, 100,
1404+
&all_tr_in[s]);
1405+
for (int i = 0; i < all_tr_in[s].visited_count; i++) {
1406+
yyjson_mut_val *item = yyjson_mut_obj(doc);
1407+
yyjson_mut_obj_add_str(
1408+
doc, item, "name",
1409+
all_tr_in[s].visited[i].node.name ? all_tr_in[s].visited[i].node.name : "");
1410+
yyjson_mut_obj_add_str(
1411+
doc, item, "qualified_name",
1412+
all_tr_in[s].visited[i].node.qualified_name
1413+
? all_tr_in[s].visited[i].node.qualified_name
1414+
: "");
1415+
yyjson_mut_obj_add_int(doc, item, "hop", all_tr_in[s].visited[i].hop);
1416+
yyjson_mut_arr_add_val(callers, item);
1417+
}
13211418
}
13221419
yyjson_mut_obj_add_val(doc, root, "callers", callers);
13231420
}
@@ -1327,13 +1424,16 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
13271424
yyjson_mut_doc_free(doc);
13281425

13291426
/* Now safe to free traversal data */
1330-
if (do_outbound) {
1331-
cbm_store_traverse_free(&tr_out);
1427+
for (int s = 0; s < tr_out_count; s++) {
1428+
cbm_store_traverse_free(&all_tr_out[s]);
13321429
}
1333-
if (do_inbound) {
1334-
cbm_store_traverse_free(&tr_in);
1430+
free(all_tr_out);
1431+
for (int s = 0; s < tr_in_count; s++) {
1432+
cbm_store_traverse_free(&all_tr_in[s]);
13351433
}
1434+
free(all_tr_in);
13361435

1436+
free(start_ids);
13371437
cbm_store_free_nodes(nodes, node_count);
13381438
free(func_name);
13391439
free(project);

0 commit comments

Comments
 (0)