Skip to content

Commit 93041d2

Browse files
author
Your Name
committed
fix(mcp): crash on 0-edge nodes + fuzzy name fallback in trace
Fix crash (double-free) when tracing nodes with 0 in-degree and 0 out-degree (e.g. Type nodes, empty Class stubs). Detect early via cbm_store_node_degree and return basic match info without attempting BFS traversal. Also move the traversal result array from stack to heap to prevent stack smashing with many start IDs. Add fuzzy name fallback: when exact name match returns 0 results, run a regex search with '.*name.*' pattern and return up to 10 suggestions with name, label, file_path, line. This handles cases like searching for 'RecordingSession' when only 'ContinuousRecordingSessionDataGen' exists.
1 parent a7b60cb commit 93041d2

1 file changed

Lines changed: 68 additions & 5 deletions

File tree

src/mcp/mcp.c

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1660,9 +1660,53 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
16601660
cbm_store_find_nodes_by_name(store, project, func_name, &nodes, &node_count);
16611661

16621662
if (node_count == 0) {
1663-
free(func_name);
1664-
free(project);
1665-
free(direction);
1663+
/* Fuzzy fallback: try substring match when exact name not found.
1664+
* This handles cases like searching for "RecordingSession" when only
1665+
* "ContinuousRecordingSessionDataGen" exists. */
1666+
cbm_search_params_t fuzzy = {0};
1667+
char pattern[512];
1668+
snprintf(pattern, sizeof(pattern), ".*%s.*", func_name);
1669+
fuzzy.project = project;
1670+
fuzzy.name_pattern = pattern;
1671+
fuzzy.limit = 10;
1672+
cbm_search_output_t fuzzy_results = {0};
1673+
cbm_store_search(store, &fuzzy, &fuzzy_results);
1674+
1675+
if (fuzzy_results.count > 0) {
1676+
/* Return fuzzy matches as suggestions */
1677+
yyjson_mut_doc *fdoc = yyjson_mut_doc_new(NULL);
1678+
yyjson_mut_val *froot = yyjson_mut_obj(fdoc);
1679+
yyjson_mut_doc_set_root(fdoc, froot);
1680+
yyjson_mut_obj_add_str(fdoc, froot, "status", "not_found_exact");
1681+
char msg[512];
1682+
snprintf(msg, sizeof(msg),
1683+
"No exact match for '%s'. Found %d partial matches — "
1684+
"use one of these exact names:", func_name, fuzzy_results.count);
1685+
yyjson_mut_obj_add_strcpy(fdoc, froot, "message", msg);
1686+
yyjson_mut_val *suggestions = yyjson_mut_arr(fdoc);
1687+
for (int i = 0; i < fuzzy_results.count; i++) {
1688+
yyjson_mut_val *si = yyjson_mut_obj(fdoc);
1689+
yyjson_mut_obj_add_strcpy(fdoc, si, "name",
1690+
fuzzy_results.results[i].node.name ? fuzzy_results.results[i].node.name : "");
1691+
yyjson_mut_obj_add_strcpy(fdoc, si, "label",
1692+
fuzzy_results.results[i].node.label ? fuzzy_results.results[i].node.label : "");
1693+
yyjson_mut_obj_add_strcpy(fdoc, si, "file_path",
1694+
fuzzy_results.results[i].node.file_path ? fuzzy_results.results[i].node.file_path : "");
1695+
yyjson_mut_obj_add_int(fdoc, si, "line", fuzzy_results.results[i].node.start_line);
1696+
yyjson_mut_arr_add_val(suggestions, si);
1697+
}
1698+
yyjson_mut_obj_add_val(fdoc, froot, "suggestions", suggestions);
1699+
char *fjson = yy_doc_to_str(fdoc);
1700+
yyjson_mut_doc_free(fdoc);
1701+
cbm_store_search_free(&fuzzy_results);
1702+
free(func_name); free(project); free(direction);
1703+
cbm_store_free_nodes(nodes, 0);
1704+
char *result = cbm_mcp_text_result(fjson, false);
1705+
free(fjson);
1706+
return result;
1707+
}
1708+
cbm_store_search_free(&fuzzy_results);
1709+
free(func_name); free(project); free(direction);
16661710
cbm_store_free_nodes(nodes, 0);
16671711
return cbm_mcp_text_result("{\"error\":\"function not found\"}", true);
16681712
}
@@ -1792,6 +1836,25 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
17921836
yyjson_mut_obj_add_val(doc, root, "candidates", cands);
17931837
}
17941838

1839+
/* Check if the node has any edges at all. If not, return basic info only.
1840+
* This avoids BFS crashes on nodes with 0 edges (e.g. Type nodes, empty Classes). */
1841+
{
1842+
int in_deg = 0;
1843+
int out_deg = 0;
1844+
cbm_store_node_degree(store, nodes[best_idx].id, &in_deg, &out_deg);
1845+
if (in_deg == 0 && out_deg == 0) {
1846+
/* No edges — return basic info */
1847+
char *json = yy_doc_to_str(doc);
1848+
yyjson_mut_doc_free(doc);
1849+
free(start_ids);
1850+
cbm_store_free_nodes(nodes, node_count);
1851+
free(func_name); free(project); free(direction);
1852+
char *result = cbm_mcp_text_result(json, false);
1853+
free(json);
1854+
return result;
1855+
}
1856+
}
1857+
17951858
/* ── Categorized edge query: like GitNexus context() ──
17961859
* Instead of flat BFS, query each edge type separately and return
17971860
* categorized results: incoming.calls, incoming.imports, incoming.extends,
@@ -1810,8 +1873,7 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
18101873

18111874
/* Collect all traversal results for lifetime management */
18121875
#define MAX_TR 64
1813-
cbm_traverse_result_t all_tr[MAX_TR];
1814-
memset(all_tr, 0, sizeof(all_tr));
1876+
cbm_traverse_result_t *all_tr = calloc(MAX_TR, sizeof(cbm_traverse_result_t));
18151877
int tr_count = 0;
18161878

18171879
if (do_inbound) {
@@ -2047,6 +2109,7 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
20472109
for (int t = 0; t < tr_count; t++) {
20482110
cbm_store_traverse_free(&all_tr[t]);
20492111
}
2112+
free(all_tr);
20502113
#undef EDGE_QUERY_MAX
20512114
#undef MAX_TR
20522115

0 commit comments

Comments
 (0)