mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-11 09:25:35 +08:00
fix(mcp): accept project_name alias + clearer no-store error (#640)
Query tools resolved the project only via the exact "project" key, but list_projects surfaces the field as "name" and the not-found hint said "pass the project name", so callers naturally pass "project_name". With no alias the arg was NULL and resolve_store bailed before opening any .db, so every query returned "project not found or not indexed" even though the project was indexed -- the reporter's strace shows the cache dir scanned but no .db opened. Reproduced on macOS; not Linux-specific. Add get_project_arg() accepting project | project_name | project_id | projectName across the 11 query handlers (not bare "name" -- index_repository uses it for an explicit project-name override). Split the no-store error so a missing argument reports "missing required argument: project" instead of masquerading as "not indexed", and name the literal "project" key in the not-found hint. Reproduce-first: tool_get_architecture_accepts_project_name_alias_issue640 and tool_search_graph_accepts_project_name_alias_issue640 (RED before -> GREEN after). Full suite 5724/0, no regressions. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
+61
-22
@@ -705,6 +705,26 @@ char *cbm_mcp_get_string_arg(const char *args_json, const char *key) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/* 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
|
||||
* "project_name" is the usual guess; "project_id" / "projectName" are accepted
|
||||
* too. NOT bare "name" — index_repository uses "name" for an explicit
|
||||
* project-name override. Caller must free() the result. */
|
||||
static char *get_project_arg(const char *args_json) {
|
||||
char *p = cbm_mcp_get_string_arg(args_json, "project");
|
||||
if (!p) {
|
||||
p = cbm_mcp_get_string_arg(args_json, "project_name");
|
||||
}
|
||||
if (!p) {
|
||||
p = cbm_mcp_get_string_arg(args_json, "project_id");
|
||||
}
|
||||
if (!p) {
|
||||
p = cbm_mcp_get_string_arg(args_json, "projectName");
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
int cbm_mcp_get_int_arg(const char *args_json, const char *key, int default_val) {
|
||||
yyjson_doc *doc = yyjson_read(args_json, strlen(args_json), 0);
|
||||
if (!doc) {
|
||||
@@ -1039,7 +1059,8 @@ static char *build_project_list_error(const char *reason) {
|
||||
if (count > 0) {
|
||||
snprintf(buf, sizeof(buf),
|
||||
"{\"error\":\"%s\",\"hint\":\"Use list_projects to see all indexed projects, "
|
||||
"then pass the project name.\",\"available_projects\":[%s],\"count\":%d}",
|
||||
"then pass it as the \\\"project\\\" "
|
||||
"argument.\",\"available_projects\":[%s],\"count\":%d}",
|
||||
reason, projects, count);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf),
|
||||
@@ -1050,16 +1071,34 @@ static char *build_project_list_error(const char *reason) {
|
||||
return heap_strdup(buf);
|
||||
}
|
||||
|
||||
/* Bail with project list when no store is available. */
|
||||
#define REQUIRE_STORE(store, project) \
|
||||
do { \
|
||||
if (!(store)) { \
|
||||
char *_err = build_project_list_error("project not found or not indexed"); \
|
||||
char *_res = cbm_mcp_text_result(_err, true); \
|
||||
free(_err); \
|
||||
free(project); \
|
||||
return _res; \
|
||||
} \
|
||||
/* Distinct from "unknown project": the caller omitted the project argument
|
||||
* entirely (no recognized key). Name the literal "project" key so the fix is
|
||||
* obvious (#640). Caller must free() result. */
|
||||
static char *build_missing_project_error(void) {
|
||||
return heap_strdup("{\"error\":\"missing required argument: project\",\"hint\":\"Pass "
|
||||
"the project as the \\\"project\\\" argument, e.g. "
|
||||
"{\\\"project\\\":\\\"<name from list_projects>\\\"}. Run "
|
||||
"list_projects to see indexed projects.\"}");
|
||||
}
|
||||
|
||||
/* Pick the right no-store error: a NULL project means the argument was missing
|
||||
* (clearer message); a non-NULL project that didn't resolve means it's
|
||||
* unknown/unindexed (list the available ones). */
|
||||
static char *build_no_store_error(const char *project) {
|
||||
return project ? build_project_list_error("project not found or not indexed")
|
||||
: build_missing_project_error();
|
||||
}
|
||||
|
||||
/* Bail with the right error when no store is available. */
|
||||
#define REQUIRE_STORE(store, project) \
|
||||
do { \
|
||||
if (!(store)) { \
|
||||
char *_err = build_no_store_error(project); \
|
||||
char *_res = cbm_mcp_text_result(_err, true); \
|
||||
free(_err); \
|
||||
free(project); \
|
||||
return _res; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* ── Tool handler implementations ─────────────────────────────── */
|
||||
@@ -1200,7 +1239,7 @@ static char *verify_project_indexed(cbm_store_t *store, const char *project) {
|
||||
}
|
||||
|
||||
static char *handle_get_graph_schema(cbm_mcp_server_t *srv, const char *args) {
|
||||
char *project = cbm_mcp_get_string_arg(args, "project");
|
||||
char *project = get_project_arg(args);
|
||||
cbm_store_t *store = resolve_store(srv, project);
|
||||
REQUIRE_STORE(store, project);
|
||||
|
||||
@@ -1668,7 +1707,7 @@ static bool run_semantic_query(yyjson_mut_doc *doc, yyjson_mut_val *root, const
|
||||
}
|
||||
|
||||
static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
|
||||
char *project = cbm_mcp_get_string_arg(args, "project");
|
||||
char *project = get_project_arg(args);
|
||||
cbm_store_t *store = resolve_store(srv, project);
|
||||
REQUIRE_STORE(store, project);
|
||||
|
||||
@@ -1813,7 +1852,7 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
|
||||
|
||||
static char *handle_query_graph(cbm_mcp_server_t *srv, const char *args) {
|
||||
char *query = cbm_mcp_get_string_arg(args, "query");
|
||||
char *project = cbm_mcp_get_string_arg(args, "project");
|
||||
char *project = get_project_arg(args);
|
||||
cbm_store_t *store = resolve_store(srv, project);
|
||||
int max_rows = cbm_mcp_get_int_arg(args, "max_rows", 0);
|
||||
|
||||
@@ -1891,7 +1930,7 @@ static char *handle_query_graph(cbm_mcp_server_t *srv, const char *args) {
|
||||
}
|
||||
|
||||
static char *handle_index_status(cbm_mcp_server_t *srv, const char *args) {
|
||||
char *project = cbm_mcp_get_string_arg(args, "project");
|
||||
char *project = get_project_arg(args);
|
||||
cbm_store_t *store = resolve_store(srv, project);
|
||||
REQUIRE_STORE(store, project);
|
||||
|
||||
@@ -1935,7 +1974,7 @@ static char *handle_index_status(cbm_mcp_server_t *srv, const char *args) {
|
||||
|
||||
/* delete_project: just erase the .db file (and WAL/SHM). */
|
||||
static char *handle_delete_project(cbm_mcp_server_t *srv, const char *args) {
|
||||
char *name = cbm_mcp_get_string_arg(args, "project");
|
||||
char *name = get_project_arg(args);
|
||||
if (!name) {
|
||||
return cbm_mcp_text_result("project is required", true);
|
||||
}
|
||||
@@ -2046,7 +2085,7 @@ static void append_cross_repo_summary(yyjson_mut_doc *doc, yyjson_mut_val *root,
|
||||
}
|
||||
|
||||
static char *handle_get_architecture(cbm_mcp_server_t *srv, const char *args) {
|
||||
char *project = cbm_mcp_get_string_arg(args, "project");
|
||||
char *project = get_project_arg(args);
|
||||
char *scope_path = cbm_mcp_get_string_arg(args, "path");
|
||||
cbm_store_t *store = resolve_store(srv, project);
|
||||
REQUIRE_STORE(store, project);
|
||||
@@ -2598,7 +2637,7 @@ static void bfs_union_same_name(cbm_store_t *store, const cbm_node_t *nodes, int
|
||||
|
||||
static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
|
||||
char *func_name = cbm_mcp_get_string_arg(args, "function_name");
|
||||
char *project = cbm_mcp_get_string_arg(args, "project");
|
||||
char *project = get_project_arg(args);
|
||||
cbm_store_t *store = resolve_store(srv, project);
|
||||
char *direction = cbm_mcp_get_string_arg(args, "direction");
|
||||
char *mode = cbm_mcp_get_string_arg(args, "mode");
|
||||
@@ -3460,7 +3499,7 @@ static char *build_snippet_response(cbm_mcp_server_t *srv, cbm_node_t *node,
|
||||
|
||||
static char *handle_get_code_snippet(cbm_mcp_server_t *srv, const char *args) {
|
||||
char *qn = cbm_mcp_get_string_arg(args, "qualified_name");
|
||||
char *project = cbm_mcp_get_string_arg(args, "project");
|
||||
char *project = get_project_arg(args);
|
||||
bool include_neighbors = cbm_mcp_get_bool_arg(args, "include_neighbors");
|
||||
|
||||
if (!qn) {
|
||||
@@ -4176,7 +4215,7 @@ static bool compile_path_filter(const char *filter, cbm_regex_t *re) {
|
||||
|
||||
static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
|
||||
char *pattern = cbm_mcp_get_string_arg(args, "pattern");
|
||||
char *project = cbm_mcp_get_string_arg(args, "project");
|
||||
char *project = get_project_arg(args);
|
||||
char *file_pattern = cbm_mcp_get_string_arg(args, "file_pattern");
|
||||
char *path_filter = cbm_mcp_get_string_arg(args, "path_filter");
|
||||
char *mode_str = cbm_mcp_get_string_arg(args, "mode");
|
||||
@@ -4434,7 +4473,7 @@ static void detect_add_impacted_symbols(cbm_store_t *store, const char *project,
|
||||
}
|
||||
|
||||
static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {
|
||||
char *project = cbm_mcp_get_string_arg(args, "project");
|
||||
char *project = get_project_arg(args);
|
||||
char *base_branch = cbm_mcp_get_string_arg(args, "base_branch");
|
||||
char *since = cbm_mcp_get_string_arg(args, "since");
|
||||
char *scope = cbm_mcp_get_string_arg(args, "scope");
|
||||
@@ -4674,7 +4713,7 @@ static char *adr_read_legacy_file(const char *root_path) {
|
||||
"PATTERNS, TRADEOFFS, PHILOSOPHY."
|
||||
|
||||
static char *handle_manage_adr(cbm_mcp_server_t *srv, const char *args) {
|
||||
char *project = cbm_mcp_get_string_arg(args, "project");
|
||||
char *project = get_project_arg(args);
|
||||
char *mode_str = cbm_mcp_get_string_arg(args, "mode");
|
||||
char *content = cbm_mcp_get_string_arg(args, "content");
|
||||
|
||||
|
||||
@@ -894,6 +894,95 @@ TEST(tool_get_architecture_emits_populated_sections) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Reproduce-first for #640: query handlers must accept the `project_name`
|
||||
* alias, not only the canonical `project` key. list_projects surfaces the field
|
||||
* as "name" and the error hint says "pass the project name", so a caller
|
||||
* naturally passes `project_name`. With no alias, the handler reads key
|
||||
* "project" -> NULL -> resolve_store bails before opening any .db -> "project
|
||||
* not found or not indexed" even though the project is indexed. Mirrors
|
||||
* tool_get_architecture_emits_populated_sections but with the alias key. */
|
||||
TEST(tool_get_architecture_accepts_project_name_alias_issue640) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
|
||||
cbm_store_t *st = cbm_mcp_server_store(srv);
|
||||
ASSERT_NOT_NULL(st);
|
||||
|
||||
const char *proj = "alias640";
|
||||
cbm_mcp_server_set_project(srv, proj);
|
||||
cbm_store_upsert_project(st, proj, "/tmp/alias640");
|
||||
|
||||
cbm_node_t main_fn = {0};
|
||||
main_fn.project = proj;
|
||||
main_fn.label = "Function";
|
||||
main_fn.name = "main";
|
||||
main_fn.qualified_name = "alias640.cmd.main";
|
||||
main_fn.file_path = "cmd/main.go";
|
||||
main_fn.start_line = 1;
|
||||
main_fn.end_line = 3;
|
||||
main_fn.properties_json = "{\"is_entry_point\":true}";
|
||||
ASSERT_GT(cbm_store_upsert_node(st, &main_fn), 0);
|
||||
|
||||
/* Caller passes `project_name` (the natural guess) instead of `project`. */
|
||||
char *resp = cbm_mcp_server_handle(
|
||||
srv, "{\"jsonrpc\":\"2.0\",\"id\":640,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"get_architecture\","
|
||||
"\"arguments\":{\"project_name\":\"alias640\",\"aspects\":[\"all\"]}}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
char *inner = extract_text_content(resp);
|
||||
ASSERT_NOT_NULL(inner);
|
||||
|
||||
/* RED before the alias: inner is the "project not found" error.
|
||||
* GREEN after: the alias resolves and architecture sections surface. */
|
||||
ASSERT_NULL(strstr(inner, "project not found"));
|
||||
ASSERT_NOT_NULL(strstr(inner, "\"entry_points\""));
|
||||
|
||||
free(inner);
|
||||
free(resp);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Reproduce-first for #640: the alias must apply across query handlers, not
|
||||
* just get_architecture. search_graph with `project_name` must resolve too. */
|
||||
TEST(tool_search_graph_accepts_project_name_alias_issue640) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
|
||||
cbm_store_t *st = cbm_mcp_server_store(srv);
|
||||
ASSERT_NOT_NULL(st);
|
||||
|
||||
const char *proj = "alias640b";
|
||||
cbm_mcp_server_set_project(srv, proj);
|
||||
cbm_store_upsert_project(st, proj, "/tmp/alias640b");
|
||||
|
||||
cbm_node_t fn = {0};
|
||||
fn.project = proj;
|
||||
fn.label = "Function";
|
||||
fn.name = "WidgetHandler";
|
||||
fn.qualified_name = "alias640b.svc.WidgetHandler";
|
||||
fn.file_path = "svc/widget.go";
|
||||
fn.start_line = 1;
|
||||
fn.end_line = 2;
|
||||
ASSERT_GT(cbm_store_upsert_node(st, &fn), 0);
|
||||
|
||||
char *resp = cbm_mcp_server_handle(
|
||||
srv, "{\"jsonrpc\":\"2.0\",\"id\":641,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"search_graph\","
|
||||
"\"arguments\":{\"project_name\":\"alias640b\",\"name_pattern\":\"Widget.*\"}}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
char *inner = extract_text_content(resp);
|
||||
ASSERT_NOT_NULL(inner);
|
||||
|
||||
ASSERT_NULL(strstr(inner, "project not found"));
|
||||
ASSERT_NOT_NULL(strstr(inner, "WidgetHandler"));
|
||||
|
||||
free(inner);
|
||||
free(resp);
|
||||
cbm_mcp_server_free(srv);
|
||||
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);
|
||||
@@ -2488,6 +2577,8 @@ SUITE(mcp) {
|
||||
RUN_TEST(tool_delete_project_not_found);
|
||||
RUN_TEST(tool_get_architecture_empty);
|
||||
RUN_TEST(tool_get_architecture_emits_populated_sections);
|
||||
RUN_TEST(tool_get_architecture_accepts_project_name_alias_issue640);
|
||||
RUN_TEST(tool_search_graph_accepts_project_name_alias_issue640);
|
||||
RUN_TEST(tool_get_architecture_path_scoping);
|
||||
RUN_TEST(tool_query_graph_missing_query);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user