Merge pull request #1181 from tmonestudio/codex/fix-list-projects-coverage-scope

fix(mcp): restore scalable project discovery
This commit is contained in:
Martin Vogel authored and GitHub committed 2026-08-18 00:08:28 +02:00
commit 3dab881b66
2 files changed
+118 -10

No files matched your search

+86 -10
View File
@@ -605,8 +605,14 @@ static const tool_def_t TOOLS[] = {
"offset parameter — raise limit or narrow with file_pattern / path_filter to see more."
"\",\"default\":10,\"minimum\":1}},\"required\":[\"pattern\",\"project\"]}"},
{"list_projects", "List projects", "List all indexed projects",
"{\"type\":\"object\",\"properties\":{}}"},
{"list_projects", "List projects", "List indexed projects with deterministic pagination",
"{\"type\":\"object\",\"properties\":{\"offset\":{\"type\":\"integer\","
"\"minimum\":0,\"default\":0},"
"\"limit\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":100,\"default\":50},"
"\"include_details\":{\"type\":\"boolean\",\"default\":false,"
"\"description\":\"Include branch, node/edge counts and database size. Slower.\"},"
"\"metadata_only\":{\"type\":\"boolean\",\"description\":\"Deprecated compatibility "
"alias for include_details=false.\"}}}"},
{"delete_project", "Delete project", "Delete a project from the index",
"{\"type\":\"object\",\"properties\":{\"project\":{\"type\":\"string\"}},\"required\":["
"\"project\"]}"},
@@ -2500,7 +2506,7 @@ static cbm_store_t *resolve_store_fallback_scan(const char *project) {
* then append a JSON entry to arr. */
static void build_project_json_entry(yyjson_mut_doc *doc, yyjson_mut_val *arr, const char *dir_path,
const char *name, size_t name_len, int64_t size_bytes,
bool metadata_only) {
bool include_details) {
(void)name_len;
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
int nodes = 0;
int edges = 0;
if (!metadata_only) {
if (include_details) {
nodes = cbm_store_count_nodes(pstore, project_name);
edges = cbm_store_count_edges(pstore, project_name);
}
@@ -2540,7 +2546,7 @@ static void build_project_json_entry(yyjson_mut_doc *doc, yyjson_mut_val *arr, c
* disambiguates same-repo projects). The 12-field git block — mostly
* null for non-git roots — cost ~10KB across a full cache and is one
* index_status call away for the project you actually care about. */
if (!metadata_only && root_path_buf[0]) {
if (include_details && root_path_buf[0]) {
cbm_git_context_t gctx = {0};
(void)cbm_git_context_resolve(root_path_buf, &gctx);
if (gctx.is_git && gctx.branch) {
@@ -2548,7 +2554,7 @@ static void build_project_json_entry(yyjson_mut_doc *doc, yyjson_mut_val *arr, c
}
cbm_git_context_free(&gctx);
}
if (!metadata_only) {
if (include_details) {
yyjson_mut_obj_add_int(doc, p, "nodes", nodes);
yyjson_mut_obj_add_int(doc, p, "edges", edges);
yyjson_mut_obj_add_int(doc, p, "size_bytes", size_bytes);
@@ -2556,19 +2562,37 @@ static void build_project_json_entry(yyjson_mut_doc *doc, yyjson_mut_val *arr, c
yyjson_mut_arr_add_val(arr, p);
}
static int project_db_name_cmp(const void *a, const void *b) {
const char *const *sa = (const char *const *)a;
const char *const *sb = (const char *const *)b;
return strcmp(*sa, *sb);
}
/* list_projects: scan cache directory for .db files.
* Each project is a single .db file — no central registry needed. */
static char *handle_list_projects(cbm_mcp_server_t *srv, const char *args) {
(void)srv;
bool metadata_only = false;
int offset = cbm_mcp_get_int_arg(args, "offset", 0);
int limit = cbm_mcp_get_int_arg(args, "limit", 50);
bool include_details = cbm_mcp_get_bool_arg(args, "include_details");
yyjson_doc *args_doc = args ? yyjson_read(args, strlen(args), 0) : NULL;
yyjson_val *args_root = args_doc ? yyjson_doc_get_root(args_doc) : NULL;
yyjson_val *metadata_value =
args_root && yyjson_is_obj(args_root) ? yyjson_obj_get(args_root, "metadata_only") : NULL;
metadata_only = metadata_value && yyjson_is_true(metadata_value);
if (metadata_value && yyjson_is_true(metadata_value)) {
include_details = false;
}
if (args_doc) {
yyjson_doc_free(args_doc);
}
if (offset < 0) {
offset = 0;
}
if (limit < 1) {
limit = 1;
} else if (limit > 100) {
limit = 100;
}
char dir_path[CBM_SZ_1K];
cache_dir(dir_path, sizeof(dir_path));
@@ -2590,6 +2614,10 @@ static char *handle_list_projects(cbm_mcp_server_t *srv, const char *args) {
return cbm_mcp_text_result(msg, true);
}
char **db_names = NULL;
size_t db_count = 0;
size_t db_cap = 0;
bool name_collection_failed = false;
cbm_dirent_t *entry;
while ((entry = cbm_readdir(d)) != NULL) {
const char *name = entry->name;
@@ -2597,17 +2625,65 @@ static char *handle_list_projects(cbm_mcp_server_t *srv, const char *args) {
if (!is_project_db_file(name, len)) {
continue;
}
if (db_count == db_cap) {
size_t next_cap = db_cap ? db_cap * 2 : 64;
char **grown = realloc(db_names, next_cap * sizeof(*db_names));
if (!grown) {
name_collection_failed = true;
break;
}
db_names = grown;
db_cap = next_cap;
}
db_names[db_count] = heap_strdup(name);
if (!db_names[db_count]) {
name_collection_failed = true;
break;
}
db_count++;
}
cbm_closedir(d);
if (name_collection_failed) {
for (size_t i = 0; i < db_count; i++) {
free(db_names[i]);
}
free(db_names);
yyjson_mut_doc_free(doc);
return cbm_mcp_text_result(
"{\"error\":\"out of memory while collecting indexed projects\"}", true);
}
if (db_count > 1) {
qsort(db_names, db_count, sizeof(*db_names), project_db_name_cmp);
}
size_t start = (size_t)offset < db_count ? (size_t)offset : db_count;
size_t end = start + (size_t)limit;
if (end > db_count) {
end = db_count;
}
for (size_t i = start; i < end; i++) {
const char *name = db_names[i];
size_t len = strlen(name);
char full_path[CBM_SZ_2K];
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, name);
int64_t size_bytes = cbm_file_size(full_path);
if (size_bytes < 0) {
continue;
}
build_project_json_entry(doc, arr, dir_path, name, len, size_bytes, metadata_only);
build_project_json_entry(doc, arr, dir_path, name, len, size_bytes, include_details);
}
cbm_closedir(d);
for (size_t i = 0; i < db_count; i++) {
free(db_names[i]);
}
free(db_names);
yyjson_mut_obj_add_val(doc, root, "projects", arr);
yyjson_mut_obj_add_uint(doc, root, "total", db_count);
yyjson_mut_obj_add_int(doc, root, "offset", offset);
yyjson_mut_obj_add_int(doc, root, "limit", limit);
yyjson_mut_obj_add_uint(doc, root, "returned", yyjson_mut_arr_size(arr));
yyjson_mut_obj_add_bool(doc, root, "has_more", end < db_count);
/* Guide user when no projects are indexed */
if (yyjson_mut_arr_size(arr) == 0) {
+32
View File
@@ -8525,6 +8525,38 @@ TEST(tool_resolve_store_by_internal_name_issue704) {
ASSERT_NULL(strstr(list, "ghost704")); /* 0-byte ghost filtered (RED before) */
free(list);
char *page = cbm_mcp_server_handle(
srv, "{\"jsonrpc\":\"2.0\",\"id\":11,\"method\":\"tools/call\","
"\"params\":{\"name\":\"list_projects\","
"\"arguments\":{\"offset\":0,\"limit\":1}}}");
ASSERT_NOT_NULL(page);
ASSERT_NOT_NULL(strstr(page, "\\\"total\\\":3"));
ASSERT_NOT_NULL(strstr(page, "\\\"limit\\\":1"));
ASSERT_NOT_NULL(strstr(page, "\\\"returned\\\":1"));
ASSERT_NOT_NULL(strstr(page, "\\\"has_more\\\":true"));
ASSERT_NOT_NULL(strstr(page, "alpha704"));
ASSERT_NULL(strstr(page, "beta704"));
ASSERT_NULL(strstr(page, "\\\"nodes\\\"")); /* details are opt-in */
free(page);
char *next_page = cbm_mcp_server_handle(
srv, "{\"jsonrpc\":\"2.0\",\"id\":12,\"method\":\"tools/call\","
"\"params\":{\"name\":\"list_projects\","
"\"arguments\":{\"offset\":1,\"limit\":1,\"metadata_only\":true}}}");
ASSERT_NOT_NULL(next_page);
ASSERT_NOT_NULL(strstr(next_page, "beta704"));
ASSERT_NULL(strstr(next_page, "alpha704"));
ASSERT_NULL(strstr(next_page, "\\\"nodes\\\"")); /* compatibility alias stays lean */
free(next_page);
char *details = cbm_mcp_server_handle(
srv, "{\"jsonrpc\":\"2.0\",\"id\":13,\"method\":\"tools/call\","
"\"params\":{\"name\":\"list_projects\","
"\"arguments\":{\"offset\":0,\"limit\":1,\"include_details\":true}}}");
ASSERT_NOT_NULL(details);
ASSERT_NOT_NULL(strstr(details, "\\\"nodes\\\""));
free(details);
/* ── B: the drifted project resolves by its INTERNAL name ──────── */
char *q_beta = cbm_mcp_server_handle(
srv, "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\","