@@ -605,8 +605,14 @@ static const tool_def_t TOOLS[] = {
605605 "offset parameter — raise limit or narrow with file_pattern / path_filter to see more."
606606 "\",\"default\":10,\"minimum\":1}},\"required\":[\"pattern\",\"project\"]}" },
607607
608- {"list_projects" , "List projects" , "List all indexed projects" ,
609- "{\"type\":\"object\",\"properties\":{}}" },
608+ {"list_projects" , "List projects" , "List indexed projects with deterministic pagination" ,
609+ "{\"type\":\"object\",\"properties\":{\"offset\":{\"type\":\"integer\","
610+ "\"minimum\":0,\"default\":0},"
611+ "\"limit\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":100,\"default\":50},"
612+ "\"include_details\":{\"type\":\"boolean\",\"default\":false,"
613+ "\"description\":\"Include branch, node/edge counts and database size. Slower.\"},"
614+ "\"metadata_only\":{\"type\":\"boolean\",\"description\":\"Deprecated compatibility "
615+ "alias for include_details=false.\"}}}" },
610616 {"delete_project" , "Delete project" , "Delete a project from the index" ,
611617 "{\"type\":\"object\",\"properties\":{\"project\":{\"type\":\"string\"}},\"required\":["
612618 "\"project\"]}" },
@@ -2500,7 +2506,7 @@ static cbm_store_t *resolve_store_fallback_scan(const char *project) {
25002506 * then append a JSON entry to arr. */
25012507static void build_project_json_entry (yyjson_mut_doc * doc , yyjson_mut_val * arr , const char * dir_path ,
25022508 const char * name , size_t name_len , int64_t size_bytes ,
2503- bool metadata_only ) {
2509+ bool include_details ) {
25042510 (void )name_len ;
25052511
25062512 char full_path [CBM_SZ_2K ];
@@ -2519,7 +2525,7 @@ static void build_project_json_entry(yyjson_mut_doc *doc, yyjson_mut_val *arr, c
25192525
25202526 int nodes = 0 ;
25212527 int edges = 0 ;
2522- if (! metadata_only ) {
2528+ if (include_details ) {
25232529 nodes = cbm_store_count_nodes (pstore , project_name );
25242530 edges = cbm_store_count_edges (pstore , project_name );
25252531 }
@@ -2540,35 +2546,53 @@ static void build_project_json_entry(yyjson_mut_doc *doc, yyjson_mut_val *arr, c
25402546 * disambiguates same-repo projects). The 12-field git block — mostly
25412547 * null for non-git roots — cost ~10KB across a full cache and is one
25422548 * index_status call away for the project you actually care about. */
2543- if (! metadata_only && root_path_buf [0 ]) {
2549+ if (include_details && root_path_buf [0 ]) {
25442550 cbm_git_context_t gctx = {0 };
25452551 (void )cbm_git_context_resolve (root_path_buf , & gctx );
25462552 if (gctx .is_git && gctx .branch ) {
25472553 yyjson_mut_obj_add_strcpy (doc , p , "branch" , gctx .branch );
25482554 }
25492555 cbm_git_context_free (& gctx );
25502556 }
2551- if (! metadata_only ) {
2557+ if (include_details ) {
25522558 yyjson_mut_obj_add_int (doc , p , "nodes" , nodes );
25532559 yyjson_mut_obj_add_int (doc , p , "edges" , edges );
25542560 yyjson_mut_obj_add_int (doc , p , "size_bytes" , size_bytes );
25552561 }
25562562 yyjson_mut_arr_add_val (arr , p );
25572563}
25582564
2565+ static int project_db_name_cmp (const void * a , const void * b ) {
2566+ const char * const * sa = (const char * const * )a ;
2567+ const char * const * sb = (const char * const * )b ;
2568+ return strcmp (* sa , * sb );
2569+ }
2570+
25592571/* list_projects: scan cache directory for .db files.
25602572 * Each project is a single .db file — no central registry needed. */
25612573static char * handle_list_projects (cbm_mcp_server_t * srv , const char * args ) {
25622574 (void )srv ;
2563- bool metadata_only = false;
2575+ int offset = cbm_mcp_get_int_arg (args , "offset" , 0 );
2576+ int limit = cbm_mcp_get_int_arg (args , "limit" , 50 );
2577+ bool include_details = cbm_mcp_get_bool_arg (args , "include_details" );
25642578 yyjson_doc * args_doc = args ? yyjson_read (args , strlen (args ), 0 ) : NULL ;
25652579 yyjson_val * args_root = args_doc ? yyjson_doc_get_root (args_doc ) : NULL ;
25662580 yyjson_val * metadata_value =
25672581 args_root && yyjson_is_obj (args_root ) ? yyjson_obj_get (args_root , "metadata_only" ) : NULL ;
2568- metadata_only = metadata_value && yyjson_is_true (metadata_value );
2582+ if (metadata_value && yyjson_is_true (metadata_value )) {
2583+ include_details = false;
2584+ }
25692585 if (args_doc ) {
25702586 yyjson_doc_free (args_doc );
25712587 }
2588+ if (offset < 0 ) {
2589+ offset = 0 ;
2590+ }
2591+ if (limit < 1 ) {
2592+ limit = 1 ;
2593+ } else if (limit > 100 ) {
2594+ limit = 100 ;
2595+ }
25722596
25732597 char dir_path [CBM_SZ_1K ];
25742598 cache_dir (dir_path , sizeof (dir_path ));
@@ -2590,24 +2614,76 @@ static char *handle_list_projects(cbm_mcp_server_t *srv, const char *args) {
25902614 return cbm_mcp_text_result (msg , true);
25912615 }
25922616
2617+ char * * db_names = NULL ;
2618+ size_t db_count = 0 ;
2619+ size_t db_cap = 0 ;
2620+ bool name_collection_failed = false;
25932621 cbm_dirent_t * entry ;
25942622 while ((entry = cbm_readdir (d )) != NULL ) {
25952623 const char * name = entry -> name ;
25962624 size_t len = strlen (name );
25972625 if (!is_project_db_file (name , len )) {
25982626 continue ;
25992627 }
2628+ if (db_count == db_cap ) {
2629+ size_t next_cap = db_cap ? db_cap * 2 : 64 ;
2630+ char * * grown = realloc (db_names , next_cap * sizeof (* db_names ));
2631+ if (!grown ) {
2632+ name_collection_failed = true;
2633+ break ;
2634+ }
2635+ db_names = grown ;
2636+ db_cap = next_cap ;
2637+ }
2638+ db_names [db_count ] = heap_strdup (name );
2639+ if (!db_names [db_count ]) {
2640+ name_collection_failed = true;
2641+ break ;
2642+ }
2643+ db_count ++ ;
2644+ }
2645+ cbm_closedir (d );
2646+
2647+ if (name_collection_failed ) {
2648+ for (size_t i = 0 ; i < db_count ; i ++ ) {
2649+ free (db_names [i ]);
2650+ }
2651+ free (db_names );
2652+ yyjson_mut_doc_free (doc );
2653+ return cbm_mcp_text_result (
2654+ "{\"error\":\"out of memory while collecting indexed projects\"}" , true);
2655+ }
2656+
2657+ if (db_count > 1 ) {
2658+ qsort (db_names , db_count , sizeof (* db_names ), project_db_name_cmp );
2659+ }
2660+ size_t start = (size_t )offset < db_count ? (size_t )offset : db_count ;
2661+ size_t end = start + (size_t )limit ;
2662+ if (end > db_count ) {
2663+ end = db_count ;
2664+ }
2665+ for (size_t i = start ; i < end ; i ++ ) {
2666+ const char * name = db_names [i ];
2667+ size_t len = strlen (name );
26002668 char full_path [CBM_SZ_2K ];
26012669 snprintf (full_path , sizeof (full_path ), "%s/%s" , dir_path , name );
26022670 int64_t size_bytes = cbm_file_size (full_path );
26032671 if (size_bytes < 0 ) {
26042672 continue ;
26052673 }
2606- build_project_json_entry (doc , arr , dir_path , name , len , size_bytes , metadata_only );
2674+ build_project_json_entry (doc , arr , dir_path , name , len , size_bytes , include_details );
26072675 }
2608- cbm_closedir (d );
2676+ for (size_t i = 0 ; i < db_count ; i ++ ) {
2677+ free (db_names [i ]);
2678+ }
2679+ free (db_names );
26092680
26102681 yyjson_mut_obj_add_val (doc , root , "projects" , arr );
2682+ yyjson_mut_obj_add_uint (doc , root , "total" , db_count );
2683+ yyjson_mut_obj_add_int (doc , root , "offset" , offset );
2684+ yyjson_mut_obj_add_int (doc , root , "limit" , limit );
2685+ yyjson_mut_obj_add_uint (doc , root , "returned" , yyjson_mut_arr_size (arr ));
2686+ yyjson_mut_obj_add_bool (doc , root , "has_more" , end < db_count );
26112687
26122688 /* Guide user when no projects are indexed */
26132689 if (yyjson_mut_arr_size (arr ) == 0 ) {
0 commit comments