mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 08:13:37 +08:00
fix(mcp): hydrate context for slug project searches
When search_graph is called on a fresh server with project set to a list_projects slug, resolve_store opened the correct DB but left session_project empty. The first response then injected an empty _context even though results came from the indexed project. Sync session metadata from the opened project DB only when the session is unset, reuse the existing parent-project DB routing for dependency projects, and share the absolute root-path guard with watcher registration. Tests: failed-first search_graph_slug_project_sets_session_context, CBM_ONLY_SUITE=tool_consolidation, CBM_ONLY_SUITE=mcp, scripts/check-source-safety.sh, scripts/test-source-safety.sh, git diff --check, make -j8 -f Makefile.cbm cbm. Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
+37
-7
@@ -1494,6 +1494,38 @@ static const char *parent_project_for_db(const char *project, char *buf, size_t
|
||||
return project; /* no .dep → use as-is */
|
||||
}
|
||||
|
||||
static bool root_path_looks_usable(const char *root_path) {
|
||||
if (!root_path || !root_path[0]) {
|
||||
return false;
|
||||
}
|
||||
if (root_path[0] == '/' || root_path[0] == '\\') {
|
||||
return true;
|
||||
}
|
||||
return ((root_path[0] >= 'A' && root_path[0] <= 'Z') ||
|
||||
(root_path[0] >= 'a' && root_path[0] <= 'z')) &&
|
||||
root_path[1] == ':';
|
||||
}
|
||||
|
||||
static void sync_session_from_open_project(cbm_mcp_server_t *srv, cbm_store_t *store,
|
||||
const char *db_project,
|
||||
const cbm_project_t *opened_project) {
|
||||
if (!srv || srv->session_project[0] || !store || !db_project || !db_project[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char *root_path = opened_project ? opened_project->root_path : NULL;
|
||||
cbm_project_t parent = {0};
|
||||
if (cbm_store_get_project(store, db_project, &parent) == CBM_STORE_OK) {
|
||||
root_path = parent.root_path;
|
||||
}
|
||||
|
||||
snprintf(srv->session_project, sizeof(srv->session_project), "%s", db_project);
|
||||
if (root_path_looks_usable(root_path)) {
|
||||
snprintf(srv->session_root, sizeof(srv->session_root), "%s", root_path);
|
||||
}
|
||||
cbm_project_free_fields(&parent);
|
||||
}
|
||||
|
||||
static bool mcp_join_suffix(char *out, size_t out_sz, const char *base, const char *suffix) {
|
||||
int n = snprintf(out, out_sz, "%s%s", base ? base : "", suffix ? suffix : "");
|
||||
return n > 0 && (size_t)n < out_sz;
|
||||
@@ -1629,15 +1661,13 @@ static cbm_store_t *resolve_store(cbm_mcp_server_t *srv, const char *project) {
|
||||
return NULL;
|
||||
}
|
||||
/* Register newly-accessed project with watcher (root_path from DB).
|
||||
* Validate the path looks like a real path (starts with '/' or a drive
|
||||
* letter) before watching — a retained bad-path DB (#557) may store a
|
||||
* Validate that it looks like an absolute POSIX, UNC, or Windows-drive
|
||||
* path before watching — a retained bad-path DB (#557) may store a
|
||||
* numeric/empty root_path that would point the watcher at nothing. */
|
||||
if (srv->watcher && proj_verify.root_path && proj_verify.root_path[0]) {
|
||||
char c0 = proj_verify.root_path[0];
|
||||
if (c0 == '/' || (c0 >= 'A' && c0 <= 'Z') || (c0 >= 'a' && c0 <= 'z')) {
|
||||
cbm_watcher_watch(srv->watcher, project, proj_verify.root_path);
|
||||
}
|
||||
if (srv->watcher && root_path_looks_usable(proj_verify.root_path)) {
|
||||
cbm_watcher_watch(srv->watcher, project, proj_verify.root_path);
|
||||
}
|
||||
sync_session_from_open_project(srv, srv->store, db_project, &proj_verify);
|
||||
cbm_project_free_fields(&proj_verify);
|
||||
srv->owns_store = true;
|
||||
free(srv->current_project);
|
||||
|
||||
@@ -722,6 +722,41 @@ TEST(search_graph_has_session_project) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(search_graph_slug_project_sets_session_context) {
|
||||
/* A fresh CLI/MCP server may be called with project=<slug> from
|
||||
* list_projects. Results already came from that DB, but the first response
|
||||
* context used to stay empty unless project was passed as a filesystem path. */
|
||||
const char *proj = "_tc_ctx_slug_";
|
||||
char db_path[1024];
|
||||
snprintf(db_path, sizeof(db_path), "%s/%s.db", cbm_resolve_cache_dir(), proj);
|
||||
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
ASSERT_EQ(cbm_store_upsert_project(s, proj, "/tmp/tc_ctx_slug"), CBM_STORE_OK);
|
||||
cbm_node_t n = {.project = proj,
|
||||
.label = "Function",
|
||||
.name = "tc_ctx_slug_fn",
|
||||
.qualified_name = "_tc_ctx_slug_.tc_ctx_slug_fn",
|
||||
.file_path = "src/tc_ctx_slug.c"};
|
||||
ASSERT_GT(cbm_store_upsert_node(s, &n), 0);
|
||||
cbm_store_close(s);
|
||||
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
char *result = cbm_mcp_handle_tool(
|
||||
srv, "search_graph",
|
||||
"{\"project\":\"_tc_ctx_slug_\",\"name_pattern\":\"tc_ctx_slug_fn\",\"limit\":1}");
|
||||
ASSERT_NOT_NULL(result);
|
||||
ASSERT_NOT_NULL(strstr(result, "session_project"));
|
||||
ASSERT_NOT_NULL(strstr(result, "_tc_ctx_slug_"));
|
||||
ASSERT_NOT_NULL(strstr(result, "\\\"nodes\\\":1"));
|
||||
free(result);
|
||||
|
||||
cbm_mcp_server_free(srv);
|
||||
(void)unlink(db_path);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(index_status_has_session_project) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
@@ -2664,6 +2699,7 @@ SUITE(tool_consolidation) {
|
||||
RUN_TEST(hidden_tools_still_dispatch);
|
||||
/* Session context */
|
||||
RUN_TEST(search_graph_has_session_project);
|
||||
RUN_TEST(search_graph_slug_project_sets_session_context);
|
||||
RUN_TEST(index_status_has_session_project);
|
||||
/* Context injection */
|
||||
RUN_TEST(first_response_has_context_header);
|
||||
|
||||
Reference in New Issue
Block a user