fix(mcp): preserve explicit missing project errors

Keep explicit non-path project names authoritative in resolve_project_store so a bad slug cannot fall back to the active session root after slug-based context hydration. Implicit project resolution and session/dependency aliases can still use session-root autoindex, while explicit filesystem paths keep their path-based autoindex path.

Add a focused regression that first hydrates session context from a valid project slug, then verifies a different missing slug returns a structured error/not-found response.

Verified with: make -j8 -f Makefile.cbm build/c/test-runner; CBM_ONLY_SUITE=tool_consolidation ./build/c/test-runner; CBM_ONLY_SUITE=mcp ./build/c/test-runner; bash scripts/check-source-safety.sh; bash scripts/test-source-safety.sh; git diff --check; escalated CBM_ONLY_SUITE=incremental ./build/c/test-runner.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-07-01 11:29:36 -04:00
parent 8f164dd9fa
commit e6e54a516e
2 changed files with 27 additions and 6 deletions
+11 -3
View File
@@ -2521,8 +2521,10 @@ static cbm_store_t *resolve_project_store(cbm_mcp_server_t *srv,
/* Save the resolved filesystem path BEFORE expand_project_param consumes
* raw_project. Used below to auto-index paths that aren't under session_root
* (e.g. .gitignore-excluded subdirs that are separate git repos). */
bool raw_project_explicit = raw_project != NULL;
bool raw_project_path = raw_project_explicit && project_is_path(raw_project);
char *_raw_path = NULL;
if (raw_project && project_is_path(raw_project)) {
if (raw_project_path) {
char *_exp = expand_tilde(raw_project);
_raw_path = realpath(_exp ? _exp : raw_project, NULL);
if (!_raw_path && (_exp || raw_project[0] == '/')) {
@@ -2551,9 +2553,15 @@ static cbm_store_t *resolve_project_store(cbm_mcp_server_t *srv,
}
}
cbm_store_t *store = resolve_store(srv, db_project);
bool session_store_selected = db_project && srv->session_project[0] &&
strcmp(db_project, srv->session_project) == 0;
bool may_use_session_root = !raw_project_explicit || session_store_selected;
/* Auto-index on first use (same enablement as REQUIRE_STORE). */
if (!store && srv->session_root[0] && access(srv->session_root, F_OK) == 0) {
/* Auto-index on first use (same enablement as REQUIRE_STORE). Explicit
* non-path project names are authoritative: a missing slug must report
* not-found instead of silently searching the active session project. */
if (!store && may_use_session_root && srv->session_root[0] &&
access(srv->session_root, F_OK) == 0) {
if (srv->autoindex_active) {
cbm_thread_join(&srv->autoindex_tid);
srv->autoindex_active = false;
+16 -3
View File
@@ -727,12 +727,16 @@ TEST(search_graph_slug_project_sets_session_context) {
* 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);
char *root_path = th_mktempdir("cbm_tc_ctx_slug");
ASSERT_NOT_NULL(root_path);
char db_path[CBM_SZ_1K];
int npath = snprintf(db_path, sizeof(db_path), "%s/%s.db", cbm_resolve_cache_dir(), proj);
ASSERT_GT(npath, 0);
ASSERT((size_t)npath < sizeof(db_path));
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);
ASSERT_EQ(cbm_store_upsert_project(s, proj, root_path), CBM_STORE_OK);
cbm_node_t n = {.project = proj,
.label = "Function",
.name = "tc_ctx_slug_fn",
@@ -752,8 +756,17 @@ TEST(search_graph_slug_project_sets_session_context) {
ASSERT_NOT_NULL(strstr(result, "\\\"nodes\\\":1"));
free(result);
result = cbm_mcp_handle_tool(
srv, "search_graph",
"{\"project\":\"_tc_ctx_slug_missing_\",\"name_pattern\":\"tc_ctx_slug_fn\",\"limit\":1}");
ASSERT_NOT_NULL(result);
ASSERT(strstr(result, "error") != NULL || strstr(result, "not found") != NULL ||
strstr(result, "not_found") != NULL);
free(result);
cbm_mcp_server_free(srv);
(void)unlink(db_path);
th_cleanup(root_path);
PASS();
}