Skip to content

Commit 9412f09

Browse files
DeusDataahundtmaplenk
committed
Wire up 6 silently-ignored search_graph/architecture/detect_changes params
search_graph: qn_pattern (regex on qualified_name), relationship (EXISTS filter on edges), exclude_entry_points (filter nodes with no inbound CALLS but outbound CALLS), include_connected (1-hop neighbor names in response). get_architecture: aspects array filters which sections are returned (structure, dependencies, routes, or all). detect_changes: scope param ("files" = just changed files, "symbols" = files + impacted symbols). Split functions to stay under CC=25 threshold. Co-Authored-By: ahundt <ahundt@users.noreply.github.com> Co-Authored-By: maplenk <maplenk@users.noreply.github.com>
1 parent f25dafa commit 9412f09

File tree

2 files changed

+203
-46
lines changed

2 files changed

+203
-46
lines changed

src/mcp/mcp.c

Lines changed: 142 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,37 @@ static char *handle_get_graph_schema(cbm_mcp_server_t *srv, const char *args) {
976976
return result;
977977
}
978978

979+
/* Validate edge type: uppercase letters + underscore only, max 64 chars. */
980+
static bool validate_edge_type(const char *s) {
981+
if (!s || strlen(s) > CBM_SZ_64) {
982+
return false;
983+
}
984+
for (const char *c = s; *c; c++) {
985+
if (!(*c >= 'A' && *c <= 'Z') && *c != '_') {
986+
return false;
987+
}
988+
}
989+
return true;
990+
}
991+
992+
/* Enrich search result with 1-hop connected node names. */
993+
static void enrich_connected(yyjson_mut_doc *doc, yyjson_mut_val *item, cbm_store_t *store,
994+
int64_t node_id, const char *relationship) {
995+
cbm_traverse_result_t tr = {0};
996+
const char *et[] = {relationship ? relationship : "CALLS"};
997+
cbm_store_bfs(store, node_id, "both", et, SKIP_ONE, SKIP_ONE, MCP_DEFAULT_LIMIT, &tr);
998+
if (tr.visited_count > 0) {
999+
yyjson_mut_val *conn = yyjson_mut_arr(doc);
1000+
for (int j = 0; j < tr.visited_count; j++) {
1001+
if (tr.visited[j].node.name) {
1002+
yyjson_mut_arr_add_str(doc, conn, tr.visited[j].node.name);
1003+
}
1004+
}
1005+
yyjson_mut_obj_add_val(doc, item, "connected_names", conn);
1006+
}
1007+
cbm_store_traverse_free(&tr);
1008+
}
1009+
9791010
static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
9801011
char *project = cbm_mcp_get_string_arg(args, "project");
9811012
cbm_store_t *store = resolve_store(srv, project);
@@ -989,17 +1020,35 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
9891020

9901021
char *label = cbm_mcp_get_string_arg(args, "label");
9911022
char *name_pattern = cbm_mcp_get_string_arg(args, "name_pattern");
1023+
char *qn_pattern = cbm_mcp_get_string_arg(args, "qn_pattern");
9921024
char *file_pattern = cbm_mcp_get_string_arg(args, "file_pattern");
1025+
char *relationship = cbm_mcp_get_string_arg(args, "relationship");
1026+
bool exclude_entry_points = cbm_mcp_get_bool_arg(args, "exclude_entry_points");
1027+
bool include_connected = cbm_mcp_get_bool_arg(args, "include_connected");
9931028
int limit = cbm_mcp_get_int_arg(args, "limit", MCP_HALF_SEC_US);
9941029
int offset = cbm_mcp_get_int_arg(args, "offset", 0);
9951030
int min_degree = cbm_mcp_get_int_arg(args, "min_degree", CBM_NOT_FOUND);
9961031
int max_degree = cbm_mcp_get_int_arg(args, "max_degree", CBM_NOT_FOUND);
9971032

1033+
if (relationship && !validate_edge_type(relationship)) {
1034+
free(project);
1035+
free(label);
1036+
free(name_pattern);
1037+
free(qn_pattern);
1038+
free(file_pattern);
1039+
free(relationship);
1040+
return cbm_mcp_text_result("relationship must be uppercase letters and underscores", true);
1041+
}
1042+
9981043
cbm_search_params_t params = {
9991044
.project = project,
10001045
.label = label,
10011046
.name_pattern = name_pattern,
1047+
.qn_pattern = qn_pattern,
10021048
.file_pattern = file_pattern,
1049+
.relationship = relationship,
1050+
.exclude_entry_points = exclude_entry_points,
1051+
.include_connected = include_connected,
10031052
.limit = limit,
10041053
.offset = offset,
10051054
.min_degree = min_degree,
@@ -1027,6 +1076,11 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
10271076
sr->node.file_path ? sr->node.file_path : "");
10281077
yyjson_mut_obj_add_int(doc, item, "in_degree", sr->in_degree);
10291078
yyjson_mut_obj_add_int(doc, item, "out_degree", sr->out_degree);
1079+
1080+
if (include_connected && sr->node.id > 0) {
1081+
enrich_connected(doc, item, store, sr->node.id, relationship);
1082+
}
1083+
10301084
yyjson_mut_arr_add_val(results, item);
10311085
}
10321086
yyjson_mut_obj_add_val(doc, root, "results", results);
@@ -1039,7 +1093,9 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
10391093
free(project);
10401094
free(label);
10411095
free(name_pattern);
1096+
free(qn_pattern);
10421097
free(file_pattern);
1098+
free(relationship);
10431099

10441100
char *result = cbm_mcp_text_result(json, false);
10451101
free(json);
@@ -1202,6 +1258,24 @@ static char *handle_delete_project(cbm_mcp_server_t *srv, const char *args) {
12021258
return result;
12031259
}
12041260

1261+
/* Check if an aspect is requested (NULL aspects = all, or array contains "all" or the name). */
1262+
static bool aspect_wanted(yyjson_doc *aspects_doc, yyjson_val *aspects_arr, const char *name) {
1263+
if (!aspects_arr) {
1264+
return true; /* no filter = all */
1265+
}
1266+
yyjson_arr_iter iter;
1267+
yyjson_arr_iter_init(aspects_arr, &iter);
1268+
yyjson_val *val;
1269+
while ((val = yyjson_arr_iter_next(&iter)) != NULL) {
1270+
const char *s = yyjson_get_str(val);
1271+
if (s && (strcmp(s, "all") == 0 || strcmp(s, name) == 0)) {
1272+
return true;
1273+
}
1274+
}
1275+
(void)aspects_doc;
1276+
return false;
1277+
}
1278+
12051279
static char *handle_get_architecture(cbm_mcp_server_t *srv, const char *args) {
12061280
char *project = cbm_mcp_get_string_arg(args, "project");
12071281
cbm_store_t *store = resolve_store(srv, project);
@@ -1213,6 +1287,22 @@ static char *handle_get_architecture(cbm_mcp_server_t *srv, const char *args) {
12131287
return not_indexed;
12141288
}
12151289

1290+
/* Parse aspects array from args */
1291+
yyjson_doc *aspects_doc = NULL;
1292+
yyjson_val *aspects_arr = NULL;
1293+
{
1294+
yyjson_doc *args_doc = yyjson_read(args, strlen(args), 0);
1295+
if (args_doc) {
1296+
yyjson_val *aval = yyjson_obj_get(yyjson_doc_get_root(args_doc), "aspects");
1297+
if (yyjson_is_arr(aval)) {
1298+
aspects_doc = args_doc; /* keep alive */
1299+
aspects_arr = aval;
1300+
} else {
1301+
yyjson_doc_free(args_doc);
1302+
}
1303+
}
1304+
}
1305+
12161306
cbm_schema_info_t schema = {0};
12171307
cbm_store_get_schema(store, project, &schema);
12181308

@@ -1230,27 +1320,31 @@ static char *handle_get_architecture(cbm_mcp_server_t *srv, const char *args) {
12301320
yyjson_mut_obj_add_int(doc, root, "total_edges", edge_count);
12311321

12321322
/* Node label summary */
1233-
yyjson_mut_val *labels = yyjson_mut_arr(doc);
1234-
for (int i = 0; i < schema.node_label_count; i++) {
1235-
yyjson_mut_val *item = yyjson_mut_obj(doc);
1236-
yyjson_mut_obj_add_str(doc, item, "label", schema.node_labels[i].label);
1237-
yyjson_mut_obj_add_int(doc, item, "count", schema.node_labels[i].count);
1238-
yyjson_mut_arr_add_val(labels, item);
1323+
if (aspect_wanted(aspects_doc, aspects_arr, "structure")) {
1324+
yyjson_mut_val *labels = yyjson_mut_arr(doc);
1325+
for (int i = 0; i < schema.node_label_count; i++) {
1326+
yyjson_mut_val *item = yyjson_mut_obj(doc);
1327+
yyjson_mut_obj_add_str(doc, item, "label", schema.node_labels[i].label);
1328+
yyjson_mut_obj_add_int(doc, item, "count", schema.node_labels[i].count);
1329+
yyjson_mut_arr_add_val(labels, item);
1330+
}
1331+
yyjson_mut_obj_add_val(doc, root, "node_labels", labels);
12391332
}
1240-
yyjson_mut_obj_add_val(doc, root, "node_labels", labels);
12411333

12421334
/* Edge type summary */
1243-
yyjson_mut_val *types = yyjson_mut_arr(doc);
1244-
for (int i = 0; i < schema.edge_type_count; i++) {
1245-
yyjson_mut_val *item = yyjson_mut_obj(doc);
1246-
yyjson_mut_obj_add_str(doc, item, "type", schema.edge_types[i].type);
1247-
yyjson_mut_obj_add_int(doc, item, "count", schema.edge_types[i].count);
1248-
yyjson_mut_arr_add_val(types, item);
1335+
if (aspect_wanted(aspects_doc, aspects_arr, "dependencies")) {
1336+
yyjson_mut_val *types = yyjson_mut_arr(doc);
1337+
for (int i = 0; i < schema.edge_type_count; i++) {
1338+
yyjson_mut_val *item = yyjson_mut_obj(doc);
1339+
yyjson_mut_obj_add_str(doc, item, "type", schema.edge_types[i].type);
1340+
yyjson_mut_obj_add_int(doc, item, "count", schema.edge_types[i].count);
1341+
yyjson_mut_arr_add_val(types, item);
1342+
}
1343+
yyjson_mut_obj_add_val(doc, root, "edge_types", types);
12491344
}
1250-
yyjson_mut_obj_add_val(doc, root, "edge_types", types);
12511345

12521346
/* Relationship patterns */
1253-
if (schema.rel_pattern_count > 0) {
1347+
if (aspect_wanted(aspects_doc, aspects_arr, "routes") && schema.rel_pattern_count > 0) {
12541348
yyjson_mut_val *pats = yyjson_mut_arr(doc);
12551349
for (int i = 0; i < schema.rel_pattern_count; i++) {
12561350
yyjson_mut_arr_add_str(doc, pats, schema.rel_patterns[i]);
@@ -1261,6 +1355,9 @@ static char *handle_get_architecture(cbm_mcp_server_t *srv, const char *args) {
12611355
char *json = yy_doc_to_str(doc);
12621356
yyjson_mut_doc_free(doc);
12631357
cbm_store_schema_free(&schema);
1358+
if (aspects_doc) {
1359+
yyjson_doc_free(aspects_doc);
1360+
}
12641361
free(project);
12651362

12661363
char *result = cbm_mcp_text_result(json, false);
@@ -2648,11 +2745,34 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
26482745

26492746
/* ── detect_changes ───────────────────────────────────────────── */
26502747

2748+
/* Find symbols defined in a file and add them to the impacted array. */
2749+
static void detect_add_impacted_symbols(cbm_store_t *store, const char *project, const char *file,
2750+
yyjson_mut_doc *doc, yyjson_mut_val *impacted) {
2751+
cbm_node_t *nodes = NULL;
2752+
int ncount = 0;
2753+
cbm_store_find_nodes_by_file(store, project, file, &nodes, &ncount);
2754+
for (int i = 0; i < ncount; i++) {
2755+
if (nodes[i].label && strcmp(nodes[i].label, "File") != 0 &&
2756+
strcmp(nodes[i].label, "Folder") != 0 && strcmp(nodes[i].label, "Project") != 0) {
2757+
yyjson_mut_val *item = yyjson_mut_obj(doc);
2758+
yyjson_mut_obj_add_strcpy(doc, item, "name", nodes[i].name ? nodes[i].name : "");
2759+
yyjson_mut_obj_add_strcpy(doc, item, "label", nodes[i].label);
2760+
yyjson_mut_obj_add_strcpy(doc, item, "file", file);
2761+
yyjson_mut_arr_add_val(impacted, item);
2762+
}
2763+
}
2764+
cbm_store_free_nodes(nodes, ncount);
2765+
}
2766+
26512767
static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {
26522768
char *project = cbm_mcp_get_string_arg(args, "project");
26532769
char *base_branch = cbm_mcp_get_string_arg(args, "base_branch");
2770+
char *scope = cbm_mcp_get_string_arg(args, "scope");
26542771
int depth = cbm_mcp_get_int_arg(args, "depth", MCP_DEFAULT_BFS_DEPTH);
26552772

2773+
/* scope: "files" = just changed files, "symbols" = files + symbols (default) */
2774+
bool want_symbols = !scope || strcmp(scope, "symbols") == 0 || strcmp(scope, "impact") == 0;
2775+
26562776
if (!base_branch) {
26572777
base_branch = heap_strdup("main");
26582778
}
@@ -2661,20 +2781,23 @@ static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {
26612781
if (!cbm_validate_shell_arg(base_branch)) {
26622782
free(project);
26632783
free(base_branch);
2784+
free(scope);
26642785
return cbm_mcp_text_result("base_branch contains invalid characters", true);
26652786
}
26662787

26672788
char *root_path = get_project_root(srv, project);
26682789
if (!root_path) {
26692790
free(project);
26702791
free(base_branch);
2792+
free(scope);
26712793
return cbm_mcp_text_result("project not found", true);
26722794
}
26732795

26742796
if (!cbm_validate_shell_arg(root_path)) {
26752797
free(root_path);
26762798
free(project);
26772799
free(base_branch);
2800+
free(scope);
26782801
return cbm_mcp_text_result("project path contains invalid characters", true);
26792802
}
26802803

@@ -2697,6 +2820,7 @@ static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {
26972820
free(root_path);
26982821
free(project);
26992822
free(base_branch);
2823+
free(scope);
27002824
return cbm_mcp_text_result("git diff failed", true);
27012825
}
27022826

@@ -2725,22 +2849,9 @@ static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {
27252849
yyjson_mut_arr_add_strcpy(doc, changed, line);
27262850
file_count++;
27272851

2728-
/* Find symbols defined in this file */
2729-
cbm_node_t *nodes = NULL;
2730-
int ncount = 0;
2731-
cbm_store_find_nodes_by_file(store, project, line, &nodes, &ncount);
2732-
2733-
for (int i = 0; i < ncount; i++) {
2734-
if (nodes[i].label && strcmp(nodes[i].label, "File") != 0 &&
2735-
strcmp(nodes[i].label, "Folder") != 0 && strcmp(nodes[i].label, "Project") != 0) {
2736-
yyjson_mut_val *item = yyjson_mut_obj(doc);
2737-
yyjson_mut_obj_add_strcpy(doc, item, "name", nodes[i].name ? nodes[i].name : "");
2738-
yyjson_mut_obj_add_strcpy(doc, item, "label", nodes[i].label);
2739-
yyjson_mut_obj_add_strcpy(doc, item, "file", line);
2740-
yyjson_mut_arr_add_val(impacted, item);
2741-
}
2852+
if (want_symbols) {
2853+
detect_add_impacted_symbols(store, project, line, doc, impacted);
27422854
}
2743-
cbm_store_free_nodes(nodes, ncount);
27442855
}
27452856
cbm_pclose(fp);
27462857

@@ -2754,6 +2865,7 @@ static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {
27542865
free(root_path);
27552866
free(project);
27562867
free(base_branch);
2868+
free(scope);
27572869

27582870
char *result = cbm_mcp_text_result(json, false);
27592871
free(json);

0 commit comments

Comments
 (0)