diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index a7585d7c9..a457e3a6b 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -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); diff --git a/tests/test_tool_consolidation.c b/tests/test_tool_consolidation.c index c4a2b6211..2bc0877e8 100644 --- a/tests/test_tool_consolidation.c +++ b/tests/test_tool_consolidation.c @@ -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= 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);