mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Merge pull request #1054 from DeusData/fix/1025-project-suffix-resolution
fix(mcp): resolve project arg by unique name tail when exact lookup misses
This commit is contained in:
2 files changed
+162
-1
No files matched your search
+62
-1
@@ -840,6 +840,67 @@ static char *normalize_project_arg(char *project) {
|
||||
return project;
|
||||
}
|
||||
|
||||
/* Forward decls — defined below alongside store resolution. */
|
||||
static const char *cache_dir(char *buf, size_t bufsz);
|
||||
static bool is_project_db_file(const char *name, size_t len);
|
||||
bool cbm_validate_project_name(const char *project);
|
||||
|
||||
/* #1025: agents naturally pass the repo FOLDER name ("codebase-memory-mcp"),
|
||||
* but indexed project names derive from the full path
|
||||
* (E:\project\graph\x -> "E-project-graph-x"), so the exact lookup fails
|
||||
* while list_projects clearly shows the project. When no <project>.db exists,
|
||||
* scan cache-dir FILENAMES for a segment-aligned tail match ("-<project>.db"):
|
||||
* exactly one match adopts the full name; zero or several keep the original so
|
||||
* the existing not-found error (which lists all candidates) fires. Filename-
|
||||
* level only — internal-name drift stays #704's fallback in resolve_store. */
|
||||
static char *resolve_project_tail(char *project) {
|
||||
if (!project || !cbm_validate_project_name(project)) {
|
||||
return project;
|
||||
}
|
||||
char dir[CBM_SZ_1K];
|
||||
cache_dir(dir, sizeof(dir));
|
||||
char exact[CBM_SZ_2K];
|
||||
snprintf(exact, sizeof(exact), "%s/%s.db", dir, project);
|
||||
if (cbm_file_exists(exact)) {
|
||||
return project; /* exact name — untouched fast path */
|
||||
}
|
||||
size_t plen = strlen(project);
|
||||
char match[CBM_SZ_1K] = "";
|
||||
int matches = 0;
|
||||
cbm_dir_t *d = cbm_opendir(dir);
|
||||
if (!d) {
|
||||
return project;
|
||||
}
|
||||
cbm_dirent_t *entry;
|
||||
while ((entry = cbm_readdir(d)) != NULL) {
|
||||
const char *n = entry->name;
|
||||
size_t len = strlen(n);
|
||||
if (!is_project_db_file(n, len)) {
|
||||
continue;
|
||||
}
|
||||
size_t stem_len = len - MCP_DB_EXT; /* strip ".db" */
|
||||
if (stem_len <= plen + 1 || stem_len >= sizeof(match)) {
|
||||
continue;
|
||||
}
|
||||
if (n[stem_len - plen - 1] != '-' || strncmp(n + stem_len - plen, project, plen) != 0) {
|
||||
continue;
|
||||
}
|
||||
matches++;
|
||||
if (matches > 1) {
|
||||
break; /* ambiguous — keep the original name */
|
||||
}
|
||||
memcpy(match, n, stem_len);
|
||||
match[stem_len] = '\0';
|
||||
}
|
||||
cbm_closedir(d);
|
||||
if (matches == 1) {
|
||||
cbm_log_info("mcp.project_tail_resolved", "passed", project, "resolved", match);
|
||||
free(project);
|
||||
return heap_strdup(match);
|
||||
}
|
||||
return project;
|
||||
}
|
||||
|
||||
/* Resolve the project argument, accepting the canonical "project" key plus the
|
||||
* aliases a caller naturally reaches for (#640): list_projects surfaces the
|
||||
* field as "name" and the not-found hint says "pass the project name", so
|
||||
@@ -857,7 +918,7 @@ static char *get_project_arg(const char *args_json) {
|
||||
if (!p) {
|
||||
p = cbm_mcp_get_string_arg(args_json, "projectName");
|
||||
}
|
||||
return normalize_project_arg(p);
|
||||
return resolve_project_tail(normalize_project_arg(p));
|
||||
}
|
||||
|
||||
int cbm_mcp_get_int_arg(const char *args_json, const char *key, int default_val) {
|
||||
|
||||
@@ -1634,6 +1634,105 @@ TEST(tool_search_graph_accepts_project_name_alias_issue640) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #1025: agents pass the repo FOLDER name ("codebase-memory-mcp"), but
|
||||
* indexed project names derive from the full path
|
||||
* (E:\project\graph\x -> "E-project-graph-x"), so exact lookup fails with
|
||||
* "project not found" while list_projects clearly shows the project. A
|
||||
* passed name that matches exactly ONE indexed project as a segment-aligned
|
||||
* tail ("-<name>" suffix) must resolve to it; zero or several matches keep
|
||||
* the existing error. Runs against real cache-dir .db files (the resolution
|
||||
* scans filenames), so this test indexes real fixtures under an overridden
|
||||
* CBM_CACHE_DIR. */
|
||||
static void i1025_write_repo(const char *dir, const char *fn_name) {
|
||||
char path[CBM_SZ_512];
|
||||
snprintf(path, sizeof(path), "%s/mod.py", dir);
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f)
|
||||
return;
|
||||
fprintf(f, "def %s(x):\n return x + 1\n", fn_name);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
TEST(tool_project_arg_resolves_unique_tail_issue1025) {
|
||||
char repo_a[CBM_SZ_256];
|
||||
char repo_b[CBM_SZ_256];
|
||||
char repo_c[CBM_SZ_256];
|
||||
char cache[CBM_SZ_256];
|
||||
snprintf(repo_a, sizeof(repo_a), "/tmp/cbm-i1025a-XXXXXX");
|
||||
snprintf(repo_b, sizeof(repo_b), "/tmp/cbm-i1025b-XXXXXX");
|
||||
snprintf(repo_c, sizeof(repo_c), "/tmp/cbm-i1025c-XXXXXX");
|
||||
snprintf(cache, sizeof(cache), "/tmp/cbm-i1025d-XXXXXX");
|
||||
if (!cbm_mkdtemp(repo_a) || !cbm_mkdtemp(repo_b) || !cbm_mkdtemp(repo_c) ||
|
||||
!cbm_mkdtemp(cache)) {
|
||||
FAIL("mkdtemp failed");
|
||||
}
|
||||
const char *saved_cache = getenv("CBM_CACHE_DIR");
|
||||
char *saved_cache_copy = saved_cache ? cbm_strdup(saved_cache) : NULL;
|
||||
cbm_setenv("CBM_CACHE_DIR", cache, 1);
|
||||
cbm_setenv("CBM_INDEX_SUPERVISOR", "0", 1);
|
||||
|
||||
i1025_write_repo(repo_a, "unique_tail_target");
|
||||
i1025_write_repo(repo_b, "amb_one");
|
||||
i1025_write_repo(repo_c, "amb_two");
|
||||
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
|
||||
char args[CBM_SZ_1K];
|
||||
snprintf(args, sizeof(args),
|
||||
"{\"repo_path\":\"%s\",\"name\":\"E-project-graph-suffix1025\"}", repo_a);
|
||||
char *r = cbm_mcp_handle_tool(srv, "index_repository", args);
|
||||
ASSERT_NOT_NULL(r);
|
||||
free(r);
|
||||
snprintf(args, sizeof(args), "{\"repo_path\":\"%s\",\"name\":\"F-alpha-amb1025\"}", repo_b);
|
||||
r = cbm_mcp_handle_tool(srv, "index_repository", args);
|
||||
ASSERT_NOT_NULL(r);
|
||||
free(r);
|
||||
snprintf(args, sizeof(args), "{\"repo_path\":\"%s\",\"name\":\"G-beta-amb1025\"}", repo_c);
|
||||
r = cbm_mcp_handle_tool(srv, "index_repository", args);
|
||||
ASSERT_NOT_NULL(r);
|
||||
free(r);
|
||||
|
||||
/* 1. Unique tail resolves (RED today: "project not found"). */
|
||||
r = cbm_mcp_handle_tool(srv, "search_graph",
|
||||
"{\"project\":\"suffix1025\",\"name_pattern\":\".*target.*\"}");
|
||||
ASSERT_NOT_NULL(r);
|
||||
if (strstr(r, "project not found")) {
|
||||
fprintf(stderr, " [1025] FAIL unique tail did not resolve: %.200s\n", r);
|
||||
}
|
||||
ASSERT_NULL(strstr(r, "project not found"));
|
||||
ASSERT_NOT_NULL(strstr(r, "unique_tail_target"));
|
||||
free(r);
|
||||
|
||||
/* 2. Ambiguous tail stays an error (never guess between projects). */
|
||||
r = cbm_mcp_handle_tool(srv, "search_graph",
|
||||
"{\"project\":\"amb1025\",\"name_pattern\":\".*\"}");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_NOT_NULL(strstr(r, "project not found"));
|
||||
free(r);
|
||||
|
||||
/* 3. Exact full name keeps working unchanged. */
|
||||
r = cbm_mcp_handle_tool(srv, "search_graph",
|
||||
"{\"project\":\"E-project-graph-suffix1025\","
|
||||
"\"name_pattern\":\".*target.*\"}");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_NULL(strstr(r, "project not found"));
|
||||
free(r);
|
||||
|
||||
cbm_mcp_server_free(srv);
|
||||
if (saved_cache_copy) {
|
||||
cbm_setenv("CBM_CACHE_DIR", saved_cache_copy, 1);
|
||||
free(saved_cache_copy);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_CACHE_DIR");
|
||||
}
|
||||
th_rmtree(repo_a);
|
||||
th_rmtree(repo_b);
|
||||
th_rmtree(repo_c);
|
||||
th_rmtree(cache);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Regression for #604: path scopes architecture totals and content. */
|
||||
TEST(tool_get_architecture_path_scoping) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
@@ -5679,6 +5778,7 @@ SUITE(mcp) {
|
||||
RUN_TEST(tool_get_architecture_rejects_unknown_aspect_pr560);
|
||||
RUN_TEST(tool_get_architecture_accepts_project_name_alias_issue640);
|
||||
RUN_TEST(tool_search_graph_accepts_project_name_alias_issue640);
|
||||
RUN_TEST(tool_project_arg_resolves_unique_tail_issue1025);
|
||||
RUN_TEST(tool_get_architecture_path_scoping);
|
||||
RUN_TEST(tool_query_graph_missing_query);
|
||||
|
||||
|
||||
Reference in new issue
Block a user