Skip to content

Commit 1f86186

Browse files
committed
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. 8 new integration tests.
1 parent 007980c commit 1f86186

4 files changed

Lines changed: 346 additions & 84 deletions

File tree

CHANGELOG.md

Lines changed: 0 additions & 69 deletions
This file was deleted.

src/mcp/mcp.c

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,44 @@ bool cbm_mcp_get_bool_arg(const char *args_json, const char *key) {
494494
return result;
495495
}
496496

497+
/* Extract a JSON string array. Returns heap-allocated array of heap strings.
498+
* Sets *out_count. Returns NULL only when the key is absent or not an array.
499+
* For an empty array [], returns a non-NULL pointer with *out_count = 0
500+
* so callers can distinguish "not provided" from "explicitly empty". */
501+
static char **cbm_mcp_get_string_array_arg(const char *args_json, const char *key, int *out_count) {
502+
*out_count = 0;
503+
yyjson_doc *doc = yyjson_read(args_json, strlen(args_json), 0);
504+
if (!doc) {
505+
return NULL;
506+
}
507+
yyjson_val *root = yyjson_doc_get_root(doc);
508+
yyjson_val *arr = yyjson_obj_get(root, key);
509+
if (!arr || !yyjson_is_arr(arr)) {
510+
yyjson_doc_free(doc);
511+
return NULL;
512+
}
513+
int count = (int)yyjson_arr_size(arr);
514+
if (count == 0) {
515+
yyjson_doc_free(doc);
516+
return calloc(1, sizeof(char *));
517+
}
518+
char **result = malloc((size_t)count * sizeof(char *));
519+
int n = 0;
520+
size_t idx, max;
521+
yyjson_val *val;
522+
yyjson_arr_foreach(arr, idx, max, val) {
523+
if (yyjson_is_str(val)) {
524+
result[n++] = heap_strdup(yyjson_get_str(val));
525+
}
526+
}
527+
yyjson_doc_free(doc);
528+
if (n == 0) {
529+
return result;
530+
}
531+
*out_count = n;
532+
return result;
533+
}
534+
497535
/* ══════════════════════════════════════════════════════════════════
498536
* MCP SERVER
499537
* ══════════════════════════════════════════════════════════════════ */
@@ -940,6 +978,9 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
940978
char *label = cbm_mcp_get_string_arg(args, "label");
941979
char *name_pattern = cbm_mcp_get_string_arg(args, "name_pattern");
942980
char *file_pattern = cbm_mcp_get_string_arg(args, "file_pattern");
981+
char *relationship = cbm_mcp_get_string_arg(args, "relationship");
982+
bool exclude_entry_points = cbm_mcp_get_bool_arg(args, "exclude_entry_points");
983+
bool include_connected = cbm_mcp_get_bool_arg(args, "include_connected");
943984
int limit = cbm_mcp_get_int_arg(args, "limit", 500000);
944985
int offset = cbm_mcp_get_int_arg(args, "offset", 0);
945986
int min_degree = cbm_mcp_get_int_arg(args, "min_degree", -1);
@@ -950,6 +991,9 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
950991
.label = label,
951992
.name_pattern = name_pattern,
952993
.file_pattern = file_pattern,
994+
.relationship = relationship,
995+
.exclude_entry_points = exclude_entry_points,
996+
.include_connected = include_connected,
953997
.limit = limit,
954998
.offset = offset,
955999
.min_degree = min_degree,
@@ -977,6 +1021,16 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
9771021
sr->node.file_path ? sr->node.file_path : "");
9781022
yyjson_mut_obj_add_int(doc, item, "in_degree", sr->in_degree);
9791023
yyjson_mut_obj_add_int(doc, item, "out_degree", sr->out_degree);
1024+
/* Include connected node names if populated */
1025+
if (sr->connected_count > 0 && sr->connected_names) {
1026+
yyjson_mut_val *conn = yyjson_mut_arr(doc);
1027+
for (int j = 0; j < sr->connected_count; j++) {
1028+
if (sr->connected_names[j]) {
1029+
yyjson_mut_arr_add_strcpy(doc, conn, sr->connected_names[j]);
1030+
}
1031+
}
1032+
yyjson_mut_obj_add_val(doc, item, "connected_names", conn);
1033+
}
9801034
yyjson_mut_arr_add_val(results, item);
9811035
}
9821036
yyjson_mut_obj_add_val(doc, root, "results", results);
@@ -990,6 +1044,7 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
9901044
free(label);
9911045
free(name_pattern);
9921046
free(file_pattern);
1047+
free(relationship);
9931048

9941049
char *result = cbm_mcp_text_result(json, false);
9951050
free(json);
@@ -1225,9 +1280,17 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
12251280
char *direction = cbm_mcp_get_string_arg(args, "direction");
12261281
int depth = cbm_mcp_get_int_arg(args, "depth", 3);
12271282

1283+
/* Extract edge_types array; fall back to {"CALLS"} if not provided */
1284+
int user_edge_type_count = 0;
1285+
char **user_edge_types =
1286+
cbm_mcp_get_string_array_arg(args, "edge_types", &user_edge_type_count);
1287+
12281288
if (!func_name) {
12291289
free(project);
12301290
free(direction);
1291+
for (int i = 0; i < user_edge_type_count; i++)
1292+
free(user_edge_types[i]);
1293+
free(user_edge_types);
12311294
return cbm_mcp_text_result("function_name is required", true);
12321295
}
12331296
if (!store) {
@@ -1237,6 +1300,9 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
12371300
free(func_name);
12381301
free(project);
12391302
free(direction);
1303+
for (int i = 0; i < user_edge_type_count; i++)
1304+
free(user_edge_types[i]);
1305+
free(user_edge_types);
12401306
return _res;
12411307
}
12421308

@@ -1245,6 +1311,9 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
12451311
free(func_name);
12461312
free(project);
12471313
free(direction);
1314+
for (int i = 0; i < user_edge_type_count; i++)
1315+
free(user_edge_types[i]);
1316+
free(user_edge_types);
12481317
return not_indexed;
12491318
}
12501319

@@ -1261,6 +1330,9 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
12611330
free(func_name);
12621331
free(project);
12631332
free(direction);
1333+
for (int i = 0; i < user_edge_type_count; i++)
1334+
free(user_edge_types[i]);
1335+
free(user_edge_types);
12641336
cbm_store_free_nodes(nodes, 0);
12651337
return cbm_mcp_text_result("{\"error\":\"function not found\"}", true);
12661338
}
@@ -1272,8 +1344,16 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
12721344
yyjson_mut_obj_add_str(doc, root, "function", func_name);
12731345
yyjson_mut_obj_add_str(doc, root, "direction", direction);
12741346

1275-
const char *edge_types[] = {"CALLS"};
1276-
int edge_type_count = 1;
1347+
const char *default_edge_types[] = {"CALLS"};
1348+
const char **edge_types;
1349+
int edge_type_count;
1350+
if (user_edge_types) {
1351+
edge_types = (const char **)user_edge_types;
1352+
edge_type_count = user_edge_type_count;
1353+
} else {
1354+
edge_types = default_edge_types;
1355+
edge_type_count = 1;
1356+
}
12771357

12781358
/* Run BFS for each requested direction.
12791359
* IMPORTANT: yyjson_mut_obj_add_str borrows pointers — we must keep
@@ -1287,8 +1367,10 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
12871367
cbm_traverse_result_t tr_in = {0};
12881368

12891369
if (do_outbound) {
1290-
cbm_store_bfs(store, nodes[0].id, "outbound", edge_types, edge_type_count, depth, 100,
1291-
&tr_out);
1370+
if (edge_type_count > 0) {
1371+
cbm_store_bfs(store, nodes[0].id, "outbound", edge_types, edge_type_count, depth, 100,
1372+
&tr_out);
1373+
}
12921374

12931375
yyjson_mut_val *callees = yyjson_mut_arr(doc);
12941376
for (int i = 0; i < tr_out.visited_count; i++) {
@@ -1305,8 +1387,10 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
13051387
}
13061388

13071389
if (do_inbound) {
1308-
cbm_store_bfs(store, nodes[0].id, "inbound", edge_types, edge_type_count, depth, 100,
1309-
&tr_in);
1390+
if (edge_type_count > 0) {
1391+
cbm_store_bfs(store, nodes[0].id, "inbound", edge_types, edge_type_count, depth, 100,
1392+
&tr_in);
1393+
}
13101394

13111395
yyjson_mut_val *callers = yyjson_mut_arr(doc);
13121396
for (int i = 0; i < tr_in.visited_count; i++) {
@@ -1338,6 +1422,9 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
13381422
free(func_name);
13391423
free(project);
13401424
free(direction);
1425+
for (int i = 0; i < user_edge_type_count; i++)
1426+
free(user_edge_types[i]);
1427+
free(user_edge_types);
13411428

13421429
char *result = cbm_mcp_text_result(json, false);
13431430
free(json);

0 commit comments

Comments
 (0)