Skip to content

Commit 75be931

Browse files
maplenkclaude
andcommitted
fix: wire up silently-ignored search and trace parameters
Five parameters were accepted by the MCP schema but never passed through to the query layer — relationship, exclude_entry_points, include_connected on search_graph, and edge_types on trace_call_path. This wires them up with correct SQL, proper memory management, and defensive hardening. search_graph: - relationship: EXISTS filter on edges table (UNION index seeks) - exclude_entry_points: filters nodes where in_deg=0 AND out_deg>0, preserving dead code (degree=0) while removing true entry points - include_connected: populates connected_names via post-query (capped to 50 results, respects relationship edge type, excludes self-edges) - Degree subqueries now use the relationship edge type when set and exclude self-edges for correct recursive function classification trace_call_path: - edge_types extracted from JSON array instead of hardcoded {"CALLS"} - Explicit empty array [] produces empty traversal (no fallback to CALLS) while preserving response schema (empty callees/callers arrays emitted) Hardening: relationship length capped to 64 chars, validated to safe chars for SQL inlining, calloc for empty-array sentinel, estimator aligned with renderer for token budget accuracy. 8 new integration tests, 2665 total passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c7c5d60 commit 75be931

3 files changed

Lines changed: 413 additions & 34 deletions

File tree

src/mcp/mcp.c

Lines changed: 124 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,15 @@ static size_t estimate_search_result_chars(const cbm_search_result_t *sr, bool c
226226
} else {
227227
size += 24;
228228
}
229+
/* Account for connected_names array if populated (mirrors add_search_result_item) */
230+
if (sr->connected_count > 0 && sr->connected_names) {
231+
size += 24; /* "connected_names":[] overhead */
232+
for (int i = 0; i < sr->connected_count; i++) {
233+
if (sr->connected_names[i]) {
234+
size += strlen(sr->connected_names[i]) + 4; /* "name", */
235+
}
236+
}
237+
}
229238
return size;
230239
}
231240

@@ -254,6 +263,17 @@ static void add_search_result_item(yyjson_mut_doc *doc, yyjson_mut_val *results,
254263
yyjson_mut_obj_add_real(doc, item, "pagerank", sr->pagerank);
255264
}
256265

266+
/* Include connected node names if populated */
267+
if (sr->connected_count > 0 && sr->connected_names) {
268+
yyjson_mut_val *conn = yyjson_mut_arr(doc);
269+
for (int i = 0; i < sr->connected_count; i++) {
270+
if (sr->connected_names[i]) {
271+
yyjson_mut_arr_add_strcpy(doc, conn, sr->connected_names[i]);
272+
}
273+
}
274+
yyjson_mut_obj_add_val(doc, item, "connected_names", conn);
275+
}
276+
257277
yyjson_mut_arr_add_val(results, item);
258278
}
259279

@@ -1081,6 +1101,46 @@ static bool cbm_mcp_get_bool_arg_default(const char *args_json, const char *key,
10811101
return result;
10821102
}
10831103

1104+
/* Extract a JSON string array. Returns heap-allocated array of heap strings.
1105+
* Sets *out_count. Returns NULL only when the key is absent or not an array.
1106+
* For an empty array [], returns a non-NULL pointer with *out_count = 0
1107+
* so callers can distinguish "not provided" from "explicitly empty". */
1108+
static char **cbm_mcp_get_string_array_arg(const char *args_json, const char *key, int *out_count) {
1109+
*out_count = 0;
1110+
yyjson_doc *doc = yyjson_read(args_json, strlen(args_json), 0);
1111+
if (!doc) {
1112+
return NULL;
1113+
}
1114+
yyjson_val *root = yyjson_doc_get_root(doc);
1115+
yyjson_val *arr = yyjson_obj_get(root, key);
1116+
if (!arr || !yyjson_is_arr(arr)) {
1117+
yyjson_doc_free(doc);
1118+
return NULL;
1119+
}
1120+
int count = (int)yyjson_arr_size(arr);
1121+
if (count == 0) {
1122+
/* Key present but empty array — return non-NULL sentinel with count=0 */
1123+
yyjson_doc_free(doc);
1124+
return calloc(1, sizeof(char *)); /* non-NULL zero-initialized, caller frees */
1125+
}
1126+
char **result = malloc((size_t)count * sizeof(char *));
1127+
int n = 0;
1128+
size_t idx, max;
1129+
yyjson_val *val;
1130+
yyjson_arr_foreach(arr, idx, max, val) {
1131+
if (yyjson_is_str(val)) {
1132+
result[n++] = heap_strdup(yyjson_get_str(val));
1133+
}
1134+
}
1135+
yyjson_doc_free(doc);
1136+
if (n == 0) {
1137+
/* Array had elements but none were strings — still "provided" */
1138+
return result; /* non-NULL, count stays 0 */
1139+
}
1140+
*out_count = n;
1141+
return result;
1142+
}
1143+
10841144
/* ══════════════════════════════════════════════════════════════════
10851145
* MCP SERVER
10861146
* ══════════════════════════════════════════════════════════════════ */
@@ -1553,6 +1613,9 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
15531613
char *name_pattern = cbm_mcp_get_string_arg(args, "name_pattern");
15541614
char *qn_pattern = cbm_mcp_get_string_arg(args, "qn_pattern");
15551615
char *file_pattern = cbm_mcp_get_string_arg(args, "file_pattern");
1616+
char *relationship = cbm_mcp_get_string_arg(args, "relationship");
1617+
bool exclude_entry_points = cbm_mcp_get_bool_arg(args, "exclude_entry_points");
1618+
bool include_connected = cbm_mcp_get_bool_arg(args, "include_connected");
15561619
int limit = cbm_mcp_get_int_arg(args, "limit", 500000);
15571620
int offset = cbm_mcp_get_int_arg(args, "offset", 0);
15581621
int min_degree = cbm_mcp_get_int_arg(args, "min_degree", -1);
@@ -1567,6 +1630,9 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
15671630
.name_pattern = name_pattern,
15681631
.qn_pattern = qn_pattern,
15691632
.file_pattern = file_pattern,
1633+
.relationship = relationship,
1634+
.exclude_entry_points = exclude_entry_points,
1635+
.include_connected = include_connected,
15701636
.limit = limit,
15711637
.offset = offset,
15721638
.min_degree = min_degree,
@@ -1659,6 +1725,7 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
16591725
free(name_pattern);
16601726
free(qn_pattern);
16611727
free(file_pattern);
1728+
free(relationship);
16621729

16631730
char *result = cbm_mcp_text_result(json, false);
16641731
free(json);
@@ -2447,9 +2514,15 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
24472514
bool ranked = cbm_mcp_get_bool_arg_default(args, "ranked", true);
24482515
size_t char_budget = max_tokens_to_char_budget(max_tokens);
24492516

2517+
/* Extract edge_types array; fall back to {"CALLS"} if not provided */
2518+
int user_edge_type_count = 0;
2519+
char **user_edge_types = cbm_mcp_get_string_array_arg(args, "edge_types", &user_edge_type_count);
2520+
24502521
if (!func_name) {
24512522
free(project);
24522523
free(direction);
2524+
for (int i = 0; i < user_edge_type_count; i++) free(user_edge_types[i]);
2525+
free(user_edge_types);
24532526
return cbm_mcp_text_result("function_name is required", true);
24542527
}
24552528
if (!store) {
@@ -2459,6 +2532,8 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
24592532
free(func_name);
24602533
free(project);
24612534
free(direction);
2535+
for (int i = 0; i < user_edge_type_count; i++) free(user_edge_types[i]);
2536+
free(user_edge_types);
24622537
return _res;
24632538
}
24642539

@@ -2467,6 +2542,8 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
24672542
free(func_name);
24682543
free(project);
24692544
free(direction);
2545+
for (int i = 0; i < user_edge_type_count; i++) free(user_edge_types[i]);
2546+
free(user_edge_types);
24702547
return not_indexed;
24712548
}
24722549

@@ -2490,6 +2567,8 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
24902567
free(func_name);
24912568
free(project);
24922569
free(direction);
2570+
for (int i = 0; i < user_edge_type_count; i++) free(user_edge_types[i]);
2571+
free(user_edge_types);
24932572
cbm_store_free_nodes(nodes, 0);
24942573
return cbm_mcp_text_result("{\"error\":\"function not found\"}", true);
24952574
}
@@ -2501,26 +2580,42 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
25012580
yyjson_mut_obj_add_str(doc, root, "function", func_name);
25022581
yyjson_mut_obj_add_str(doc, root, "direction", direction);
25032582

2504-
const char *edge_types[] = {"CALLS"};
2505-
int edge_type_count = 1;
2583+
/* Use user-provided edge_types or default to {"CALLS"}.
2584+
* user_edge_types non-NULL with count==0 means explicit empty array [] —
2585+
* honor it by using an empty edge list (BFS traverses nothing). */
2586+
const char *default_edge_types[] = {"CALLS"};
2587+
const char **edge_types;
2588+
int edge_type_count;
2589+
if (user_edge_types) {
2590+
edge_types = (const char **)user_edge_types;
2591+
edge_type_count = user_edge_type_count;
2592+
} else {
2593+
edge_types = default_edge_types;
2594+
edge_type_count = 1;
2595+
}
25062596

2507-
/* Run BFS for each requested direction.
2508-
* IMPORTANT: yyjson_mut_obj_add_str borrows pointers — we must keep
2509-
* traversal results alive until after yy_doc_to_str serialization. */
2597+
/* Determine requested directions */
25102598
// NOLINTNEXTLINE(readability-implicit-bool-conversion)
2511-
bool do_outbound = strcmp(direction, "outbound") == 0 || strcmp(direction, "both") == 0;
2599+
bool want_outbound = strcmp(direction, "outbound") == 0 || strcmp(direction, "both") == 0;
25122600
// NOLINTNEXTLINE(readability-implicit-bool-conversion)
2513-
bool do_inbound = strcmp(direction, "inbound") == 0 || strcmp(direction, "both") == 0;
2601+
bool want_inbound = strcmp(direction, "inbound") == 0 || strcmp(direction, "both") == 0;
25142602

2603+
/* Run BFS for each requested direction.
2604+
* Skip BFS when edge_type_count == 0 (explicit empty array []),
2605+
* but still emit empty arrays to preserve the response schema.
2606+
* IMPORTANT: yyjson_mut_obj_add_str borrows pointers — we must keep
2607+
* traversal results alive until after yy_doc_to_str serialization. */
25152608
cbm_traverse_result_t tr_out = {0};
25162609
cbm_traverse_result_t tr_in = {0};
25172610

2518-
if (do_outbound) {
2519-
cbm_store_bfs(store, nodes[0].id, "outbound", edge_types, edge_type_count, depth, 100,
2520-
&tr_out);
2521-
if (ranked && tr_out.visited_count > 1) {
2522-
qsort(tr_out.visited, (size_t)tr_out.visited_count, sizeof(cbm_node_hop_t),
2523-
node_hop_rank_cmp);
2611+
if (want_outbound) {
2612+
if (edge_type_count > 0) {
2613+
cbm_store_bfs(store, nodes[0].id, "outbound", edge_types, edge_type_count, depth, 100,
2614+
&tr_out);
2615+
if (ranked && tr_out.visited_count > 1) {
2616+
qsort(tr_out.visited, (size_t)tr_out.visited_count, sizeof(cbm_node_hop_t),
2617+
node_hop_rank_cmp);
2618+
}
25242619
}
25252620

25262621
yyjson_mut_val *callees = yyjson_mut_arr(doc);
@@ -2530,12 +2625,14 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
25302625
yyjson_mut_obj_add_val(doc, root, "callees", callees);
25312626
}
25322627

2533-
if (do_inbound) {
2534-
cbm_store_bfs(store, nodes[0].id, "inbound", edge_types, edge_type_count, depth, 100,
2535-
&tr_in);
2536-
if (ranked && tr_in.visited_count > 1) {
2537-
qsort(tr_in.visited, (size_t)tr_in.visited_count, sizeof(cbm_node_hop_t),
2538-
node_hop_rank_cmp);
2628+
if (want_inbound) {
2629+
if (edge_type_count > 0) {
2630+
cbm_store_bfs(store, nodes[0].id, "inbound", edge_types, edge_type_count, depth, 100,
2631+
&tr_in);
2632+
if (ranked && tr_in.visited_count > 1) {
2633+
qsort(tr_in.visited, (size_t)tr_in.visited_count, sizeof(cbm_node_hop_t),
2634+
node_hop_rank_cmp);
2635+
}
25392636
}
25402637

25412638
yyjson_mut_val *callers = yyjson_mut_arr(doc);
@@ -2561,18 +2658,18 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
25612658
yyjson_mut_obj_add_bool(doc, root, "truncated", true);
25622659

25632660
int total_results = 0;
2564-
if (do_outbound) {
2661+
if (want_outbound) {
25652662
total_results += tr_out.visited_count;
25662663
}
2567-
if (do_inbound) {
2664+
if (want_inbound) {
25682665
total_results += tr_in.visited_count;
25692666
}
25702667
yyjson_mut_obj_add_int(doc, root, "total_results", total_results);
25712668

25722669
size_t used = 96 + strlen(func_name) + strlen(direction);
25732670
int shown = 0;
25742671

2575-
if (do_outbound) {
2672+
if (want_outbound) {
25762673
yyjson_mut_val *callees = yyjson_mut_arr(doc);
25772674
int shown_callees = 0;
25782675
int full_callees = 0;
@@ -2607,7 +2704,7 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
26072704
}
26082705
}
26092706

2610-
if (do_inbound) {
2707+
if (want_inbound) {
26112708
yyjson_mut_val *callers = yyjson_mut_arr(doc);
26122709
int shown_callers = 0;
26132710
int full_callers = 0;
@@ -2648,17 +2745,19 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
26482745
}
26492746

26502747
/* Now safe to free traversal data */
2651-
if (do_outbound) {
2748+
if (want_outbound) {
26522749
cbm_store_traverse_free(&tr_out);
26532750
}
2654-
if (do_inbound) {
2751+
if (want_inbound) {
26552752
cbm_store_traverse_free(&tr_in);
26562753
}
26572754

26582755
cbm_store_free_nodes(nodes, node_count);
26592756
free(func_name);
26602757
free(project);
26612758
free(direction);
2759+
for (int i = 0; i < user_edge_type_count; i++) free(user_edge_types[i]);
2760+
free(user_edge_types);
26622761

26632762
char *result = cbm_mcp_text_result(json, false);
26642763
free(json);

0 commit comments

Comments
 (0)