From d323705ce5082fc2c8a1784fcd5c4a8b584d1d90 Mon Sep 17 00:00:00 2001 From: Justin Wiley Date: Wed, 29 Apr 2026 15:10:32 -0400 Subject: [PATCH] Fix list_projects empty + trace_path qualified-name lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related bugs surfaced during a real-world tool sweep against an orca install (DGX Spark agent platform). Both patches are in src/mcp/mcp.c, the in-process MCP request handler. 1) list_projects returned an empty array even when projects were indexed and queryable. Root cause: is_project_db_file() (mcp.c:846, also duplicated inline in collect_db_project_names at mcp.c:791) excluded any filename with a "tmp-" prefix. But cbm_project_name_from_path() legitimately produces "tmp-..." project names for source roots under /tmp/ (covered by tests/test_pipeline.c:1903 — "/tmp/bench/erlang/lib/ stdlib/src" → "tmp-bench-erlang-lib-stdlib-src"). Any project rooted under /tmp got silently filtered from list_projects + the "available_projects" hint inside build_project_list_error. Fix: drop the "tmp-" prefix exclusion entirely. SQLite's actual journal/WAL files end with `-journal`, `-wal`, `-shm` (not `.db`) and never collide with the `.db` suffix the function already requires, so removing the prefix filter is safe. While here, refactor collect_db_project_names() to call is_project_db_file() so the filter logic lives in one place. 2) trace_path returned "function not found" when called with a fully-qualified function name like "Users-foo-dev-myrepo.src.module.func". Root cause: handle_trace_call_path() (mcp.c:1887) only called cbm_store_find_nodes_by_name(), which queries `WHERE name = ?2` (the bare name column). The tool's own error hint tells callers to "fully qualify" the name, but doing so caused the lookup to miss every time. Fix: when the bare-name lookup returns 0 nodes, fall back to cbm_store_find_node_by_qn() (already exists in store.c). If that resolves, wrap the single result into the same nodes[] array the downstream BFS expects, so the rest of the function is unchanged. Verified locally on macOS arm64 against a real 12,770-node /36,859-edge mode=full index — list_projects now lists all projects (including tmp-rooted ones), and trace_path with a fully-qualified name returns identical callees/callers to the bare-name lookup. --- src/mcp/mcp.c | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index ae5b5c31..2d63b120 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -772,6 +772,9 @@ static cbm_store_t *resolve_store(cbm_mcp_server_t *srv, const char *project) { return srv->store; } +/* Forward decl — definition lives below alongside list_projects. */ +static bool is_project_db_file(const char *name, size_t len); + /* Scan cache dir for .db files, writing comma-separated quoted names into out. * Returns the number of projects found. */ static int collect_db_project_names(const char *dir_path, char *out, size_t out_sz) { @@ -785,10 +788,7 @@ static int collect_db_project_names(const char *dir_path, char *out, size_t out_ while ((entry = cbm_readdir(d)) != NULL) { const char *n = entry->name; size_t len = strlen(n); - if (len < MCP_MIN_DB_NAME || strcmp(n + len - MCP_DB_EXT, ".db") != 0) { - continue; - } - if (strncmp(n, "tmp-", SLEN("tmp-")) == 0 || strncmp(n, "_", SLEN("_")) == 0) { + if (!is_project_db_file(n, len)) { continue; } if (count > 0 && offset < (int)out_sz - MCP_SEPARATOR) { @@ -842,12 +842,18 @@ static char *build_project_list_error(const char *reason) { /* ── Tool handler implementations ─────────────────────────────── */ -/* Return true if filename is a valid project .db file (not temp/internal). */ +/* Return true if filename is a valid project .db file (not temp/internal). + * + * NOTE: project names derived from `/tmp/...` source roots legitimately + * begin with `tmp-` (see cbm_project_name_from_path: "/tmp/bench/..." → + * "tmp-bench-..."), so we must NOT exclude the entire `tmp-` prefix. The + * `_` prefix is reserved for internal/hidden DBs, and `:memory:` is the + * SQLite in-memory marker (defensive — never appears as a real file). */ static bool is_project_db_file(const char *name, size_t len) { if (len < MCP_MIN_DB_NAME || strcmp(name + len - MCP_DB_EXT, ".db") != 0) { return false; } - if (strncmp(name, "tmp-", SLEN("tmp-")) == 0 || strncmp(name, "_", SLEN("_")) == 0 || + if (strncmp(name, "_", SLEN("_")) == 0 || strncmp(name, ":memory:", SLEN(":memory:")) == 0) { return false; } @@ -1928,11 +1934,22 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) { direction = heap_strdup("both"); } - /* Find the node by name */ + /* Find the node by name. If that misses (caller passed a fully-qualified + * name like "module.func"), fall back to qualified_name lookup so the + * documented "fully qualify for precise match" UX actually works. */ cbm_node_t *nodes = NULL; int node_count = 0; cbm_store_find_nodes_by_name(store, project, func_name, &nodes, &node_count); + if (node_count == 0) { + cbm_node_t qn_node = {0}; + if (cbm_store_find_node_by_qn(store, project, func_name, &qn_node) == CBM_STORE_OK) { + nodes = malloc(sizeof(cbm_node_t)); + nodes[0] = qn_node; + node_count = 1; + } + } + if (node_count == 0) { enum { HINT_BUF_SZ = 512 }; char hint[HINT_BUF_SZ];