@@ -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+
9791010static 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+
12051279static 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+
26512767static 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