Memory leaks fixed (0 leaks confirmed via leaks --atExit):
- mcp.c resolve_store: cbm_project_free_fields was gated on
proj.root_path[0] — empty string paths silently skipped free.
Separated free from the watcher call; now always frees after
successful cbm_store_get_project.
- mcp.c handle_index_status: cbm_store_search_free skipped when
dep_out.count==0 — cbm_store_search allocates even for empty results.
Restructured to free whenever search succeeds. Same fix for
cbm_project_free_fields call in ecosystem detection path.
- pagerank.c: node_labels leaked on two early return paths (N==0 and
id_map_init failure). Both paths now free node_ids and node_labels
(with per-element free for strdup'd entries before the N==0 branch
assigns any).
- pass_envscan.c: 8 static regexes compiled once by compile_patterns()
were never freed. Added cbm_envscan_free_patterns() that calls
cbm_regfree on each and resets patterns_compiled=0.
- pipeline.h/pipeline.c: public cbm_pipeline_global_cleanup() wraps
cbm_envscan_free_patterns(). Called in main.c after ALL server
threads joined (HTTP + stdio) to avoid racing with autoindex threads.
Also called in run_cli() path and test_pipeline.c teardown.
Ecosystem detection expanded from 8 to 17 package managers:
- depindex.h: added CBM_PKG_MAKE, CBM_PKG_CMAKE, CBM_PKG_MESON,
CBM_PKG_CONAN (C/C++ build systems). Expanded CBM_MANIFEST_FILES
with build.gradle.kts, bun.lockb, global.json, Directory.Build.props,
NuGet.Config, Makefile, GNUmakefile, Makefile.cbm, CMakeLists.txt,
meson.build, conanfile.txt, conanfile.py, vcpkg.json.
- depindex.c: rewrote cbm_detect_ecosystem to cover all 17 managers
using CHECK() macro for exact filename matches and dir_contains_suffix()
for wildcard patterns (*.csproj, *.fsproj). Added has_vendored_deps_dir()
helper. Added discover_vendored_deps() which scans vendor/ vendored/
third_party/ thirdparty/ deps/ external/ ext/ contrib/ lib/ _vendor/
submodules/ for C/C++ and CBM_PKG_CUSTOM build systems.
dep search hint in handle_search_graph:
- When a dep project search (project:"dep", expanded to prefix
"<session>.dep") returns 0 results, emits a "hint" field with an
ecosystem-aware actionable message. If cbm_detect_ecosystem succeeds,
the hint names the detected build system and instructs to re-run
index_repository. If no ecosystem detected, lists all 17 supported
manifest file types.
Compact output improvements in mcp.c:
- handle_search_graph: skip emitting "name" when it equals the last
segment of qualified_name (ends_with_segment check) or when empty.
- handle_trace_call_path: same fix for both outbound (callees) and
inbound (callers) node arrays. Added callers_total emission to match
callees_total (was documented in tool description but never emitted).
- build_snippet_response: skip empty name, label, and file_path fields.
Compact param now wired through all six call sites in handle_get_code.
Zero-value numeric fields skipped in compact mode.
- handle_get_architecture / build_resource_architecture: skip redundant
name (when equals last qualified_name segment) and empty label/fp
in key_functions arrays.
Test coverage:
- test_token_reduction.c: 504-line new file covering compact suppression
of redundant name/label/empty fields, callers_total presence,
get_code compact param propagation, architecture key_functions,
and dep search hint emission.
- test_mcp.c, test_pipeline.c: minor additions for new behaviors.
Makefile.cbm:
- Added nosan build (CFLAGS_NOSAN, LDFLAGS_NOSAN, MONGOOSE_CFLAGS_NOSAN,
per-object NOSAN variants for sqlite3/lsp/grammar/ts_runtime/mongoose).
- Added test-leak target: macOS uses leaks --atExit on test-runner-nosan;
Linux uses ASAN_OPTIONS=detect_leaks=1 on regular test-runner.
- Added test-analyze target: Clang --analyze on production + test sources
(skipped with message when IS_GCC=yes).
- Updated .PHONY with test-leak, test-analyze, test-runner-nosan.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Replace hardcoded /Users/martinvogel path (and intermediate ~ which
MCP clients don't expand) with sh -c "exec \$HOME/.local/bin/..." so
the shell expands \$HOME at launch time on any machine.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
search_graph compact: enumerate all omitted fields explicitly (name, empty
label/file_path, zero degrees) with concrete example and absent-field defaults,
replacing ambiguous "Absent:" footnote that didn't connect omission to compact.
search_graph include_dependencies: remove redundant "Default: true" restatement
(already in schema) and duplicate "dep sub-projects" mention.
trace_call_path compact: add missing omission condition (name == qualified_name
last segment) and example, replacing unexplained "redundant" jargon.
query_graph max_rows: tighten prose without losing the "default: unlimited" fact
(absent from schema) or the scanned-vs-returned distinction.
search_code case_sensitive: consolidate into single clause
"Match case-sensitively (default: case-insensitive)."
Also includes (from prior commits on this branch):
- search_graph: omit empty label/file_path fields instead of emitting ""
- search_graph: omit zero in_degree/out_degree instead of emitting 0
- trace_call_path candidates: omit empty file_path instead of emitting ""
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Previous behavior: search_graph accepted qn_pattern, relationship, exclude_entry_points,
include_connected, and include_dependencies in its JSON schema but never extracted or
applied them — all 5 were silently ignored. trace_call_path hardcoded edge_types=["CALLS"]
regardless of user input, and its compact default (true) disagreed with the schema (false).
include_dependencies schema default was false, opposite to the prefix-match behavior that
already included dep sub-projects by default.
What changed:
- src/mcp/mcp.c: extract qn_pattern and relationship in handle_search_graph Phase 1
(after name_pattern, before file_pattern); extract exclude_entry_points, include_connected,
include_dependencies as bools after max_degree; wire all 5 into cbm_search_params_t;
add include_dependencies=false guard: sets project_exact=true when project is set without
glob pattern, scoping results to exact project name (excludes .dep.* sub-projects);
add free(qn_pattern) and free(relationship) to cleanup block
- src/mcp/mcp.c: replace hardcoded edge_types[]={"CALLS"} in handle_trace_call_path with
user-supplied edge_types array extracted after all three early-return guards (lines 2062,
2069, 2086) to avoid memory leaks on those paths; use free_string_array() for cleanup;
fix compact default from false to true (matches schema); fix include_dependencies schema
default from false to true with updated description
- src/store/store.c: add qn_pattern REGEXP/iregexp dual-branch WHERE clause after
name_pattern block (same pattern as name_pattern at lines 1835-1844); add relationship
EXISTS filter using local rel_cond[128] (exceeds bind_buf[64]) with both edge directions
(source OR target); merge exclude_entry_points "in_deg > 0" condition into the existing
degree-filter subquery block to avoid double subquery nesting; fix has_degree_wrap to
include exclude_entry_points so ORDER BY uses bare column names in the outer wrapped query
- tests/test_token_reduction.c: add setup_sp_server() fixture (4 nodes: main, process_request,
fetch_data, dep_helper; 2 edges: CALLS main->process_request, HTTP_CALLS fetch_data->process_request);
add 12 new parameterization accuracy tests in token_reduction suite covering qn_pattern filter,
relationship filter, exclude_entry_points, include_dependencies=true/false, compact default,
edge_types traversal
Why: parameters declared in the MCP schema but not implemented silently accept user input
and return wrong results — AI agents and users passing these params get misleading output.
The include_dependencies schema default disagreed with actual behavior. The trace edge_types
hardcoding prevented traversal of non-CALLS relationships (HTTP_CALLS, IMPORTS, etc.).
Testable: make -f Makefile.cbm test (2213 passed, 0 failed)
search_graph '{"qn_pattern":".*handlers.*","project":"sp-test"}' returns only handlers
search_graph '{"relationship":"HTTP_CALLS","project":"sp-test"}' returns nodes with HTTP edges
search_graph '{"exclude_entry_points":true}' removes nodes with in_deg=0 (CALLS)
search_graph '{"include_dependencies":false,"project":"myapp"}' excludes myapp.dep.* nodes
trace_call_path '{"function_name":"f","edge_types":["HTTP_CALLS"]}' follows HTTP edges
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
MEMBER_OF edges (Method→Class):
- Pipeline inserts MEMBER_OF reverse edge alongside each DEFINES_METHOD
edge in both parallel (pass_parallel.c) and sequential (pass_definitions.c)
paths. PageRank power iteration naturally propagates member importance
to parent classes via the graph structure — no post-hoc hacks.
- Config: edge_weight_member_of (default 0.5, 0=disabled)
Edge weight tuning:
- USAGE: 0.2→0.7 (type refs dominant in Python/JS)
- DEFINES: 0.5→0.1 (structural noise)
- DEFINES_METHOD: 0.8→0.5
- default_weight: 0.3→0.1
- New explicit: TESTS=0.05, WRITES=0.15, DECORATES=0.2
Result on autorun (no hacks, pure algorithm):
EventContext #5, SessionStateManager #4, classes throughout top 10
Test functions dampened, structural noise reduced
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Root cause: pass_configlink.c allocated ~4.2MB on the stack:
- config_entries[4096] × 520 bytes = 2.0MB
- code_entries[8192] × 264 bytes = 2.1MB
- deps[2048] × 264 bytes = 0.5MB
Background threads get 512KB stack (macOS default) → SIGBUS.
Fix: heap-allocate all three arrays with calloc, free on every return
path. Verified: autorun repo (311 files, 6766 nodes) completes in 409ms.
Also fix: main.c shutdown order — join autoindex thread BEFORE freeing
watcher and watch_store. Previously watcher was freed while autoindex
thread still had a reference to srv->watcher, causing use-after-free.
Tested: CBM_AUTO_INDEX=true on ~/.claude/autorun — clean completion,
no SIGBUS, no hang. 2201 tests pass.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Config registry (CBM_CONFIG_REGISTRY in cli.c):
- 25 config keys across 5 categories: Indexing, Search, Tools, PageRank,
Dependencies. Each entry has key, default, env var name, category,
description.
- All defaults verified against code-level #define values.
cbm_config_get_effective(): priority chain env > DB > default.
- Checks registry for env var name, reads env first, falls back to DB.
- Used by config get CLI and auto_index in maybe_auto_index.
Env var overrides for key settings:
- CBM_AUTO_INDEX (bool), CBM_AUTO_INDEX_LIMIT (int)
- CBM_REINDEX_ON_STARTUP (bool)
- CBM_KEY_FUNCTIONS_EXCLUDE (comma-separated globs)
- CBM_TOOL_MODE (streamlined/classic)
config list output:
- Grouped by category with [Category] headers
- Shows (env) when env var is active, (set) when DB value differs from default
- All 25 keys visible (was: only 2)
config help:
- Shows storage location (~/.cache/codebase-memory-mcp/_config.db)
- Priority explanation (env > config set > default)
- Examples for config set and env var usage
- Keys grouped by category with [env: VAR_NAME] annotation
Fixed: auto_dep_limit default 5→20, dep_max_files default 5000→1000
to match code-level CBM_DEFAULT_AUTO_DEP_LIMIT and CBM_DEFAULT_DEP_MAX_FILES.
Fixed: hint message provides complete commands, not fragments.
Improved: dependency config descriptions explain what packages/files mean.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
exclude param (search_code_graph, trace_call_path, get_architecture):
- Accepts array of glob patterns to filter results by file_path
- Converted to SQL NOT LIKE via cbm_glob_to_like in store.c
- New cbm_search_params_t.exclude_paths field (NULL-terminated array)
- Helper: cbm_mcp_get_string_array_arg() parses JSON array → C string array
- 4 TDD tests: filters paths, empty array no-op, exclude-all, schema presence
Config-driven key_functions (get_architecture tool + codebase://architecture):
- build_key_functions_sql() shared helper: builds PageRank query with
config + param exclude patterns applied via NOT LIKE clauses
- CBM_CONFIG_KEY_FUNCTIONS_EXCLUDE: comma-separated globs persisted in config
(e.g. "scripts/**,tools/**,tests/**") — no hardcoded path assumptions
- Labels filtered to Function/Class/Method/Interface (code entities only)
- Both get_architecture tool and build_resource_architecture use same helper
auto_index default changed from false to true:
- maybe_auto_index() now indexes on first startup by default
- Ensures codebase://schema/architecture/status resources have data at first read
- Configurable: set auto_index=false to disable for large repos
Tests: 2197 → 2201 (4 new exclude param tests)
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Move "third_party", "thirdparty", "3rdparty", "external" from
FAST_SKIP_DIRS to ALWAYS_SKIP_DIRS so they're excluded in FULL mode too.
Add "vendored" (new) to ALWAYS_SKIP_DIRS.
Add prefix-based matching via has_vendored_prefix() for naming variations
like "vendored_libs", "vendor-bundle", "third_party_deps". Matches
vendor*, 3rdparty*, third_party*, thirdparty* followed by separator or
end-of-string.
Before: FULL mode indexed vendored grammars → 22,935 nodes, PageRank
dominated by vendored scanner functions (eof, seq, View.size).
After: 5,300 nodes, PageRank correctly shows core pipeline/store/mcp
functions at the top.
No entries removed from skip lists — 4 entries promoted FAST→ALWAYS,
1 entry added. DEP mode unaffected (has its own minimal skip list).
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Bugs fixed:
- _hidden_tools dispatch: returns tool list instead of "unknown tool" error
- trace_call_path: accepts project paths via expand_project_param (DRY
resolve_project_store helper shared with search_code_graph)
- Resources scoping: codebase://schema/architecture/status now reflect the
most-recently-queried project (active_project_name) instead of always
returning the empty session CWD project
- compact default: search/trace default to compact=true via new
cbm_mcp_get_bool_arg_default() — omits redundant name field
- PageRank precision: add_pagerank_val() writes raw JSON via %.4g format
(e.g. 4.755e-05) instead of 17-digit doubles, no float round-trip
- Empty DB skip: maybe_auto_index now checks db_has_content() (SELECT 1
FROM nodes LIMIT 1) instead of just stat(). Empty DBs trigger reindex.
New features:
- db_is_stale(): compares DB mtime vs git HEAD commit time, with
configurable max_age_seconds (reindex_stale_seconds config key)
- reindex_on_startup config: when true + stale DB, triggers reindex at
server start. Default false for large project safety.
DRY refactors:
- resolve_project_store(): extracted from handle_search_graph, reused by
handle_trace_call_path. Handles expand_project_param + DB selection +
prefix collision avoidance + auto-index on first use.
Tests (2193 → 2197):
- hidden_tools_returns_info_not_error
- compact_defaults_to_true
- pagerank_output_has_limited_precision
- empty_db_not_treated_as_indexed
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Previously only the CWD project at startup was watched for file changes.
Now any project the AI session interacts with gets registered:
- handle_index_repository: call cbm_watcher_watch() after successful
index+pagerank, so explicitly indexed projects get auto-reindexed on
file changes (same pattern as auto-index thread at lines 3503-3505)
- resolve_store: call cbm_store_get_project() to get root_path from DB,
then cbm_watcher_watch() when a new store is opened. Only runs on the
new-store path (early-return skips already-cached projects). Covers
all data-access tool paths: search_code_graph, trace_call_path, get_code.
TDD: 3 new tests in test_tool_consolidation.c (all pass, 2193 total):
watcher_registered_after_index_repository
watcher_registered_on_resolve_store
watcher_not_registered_for_unknown_path
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
macOS 25+ enforces ad-hoc code signatures. Copying a binary with cp
invalidates the existing signature and the binary gets SIGKILL at startup.
Changes:
- Detect platform with UNAME_S := $(shell uname -s) at Makefile top
- codesign_binary() make function: calls codesign --force --sign - on macOS,
prints warning if codesign not found, no-op + informational message on Linux
- cbm target: calls codesign_binary after linking (build/c/... is always signed)
- install target: new target — builds, copies to INSTALL_DIR (~/.local/bin),
re-signs the copy (required because cp invalidates the build signature)
- Clear status lines on every outcome:
✓ signed <path> (ad-hoc, macOS 25+ compatible)
✗ WARNING: codesign failed — <path> may crash on macOS 25+
✗ WARNING: codesign not found — install Xcode CLT
(signing skipped — not macOS)
- Updated usage comment with install target and macOS signing note
To install: make -f Makefile.cbm install
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Three bugs fixed in handle_get_code_snippet:
Bug A (root cause): Tiers 1-3 all use WHERE project = ?1 AND ... in SQL.
When project param is NULL, SQLite binds NULL and the comparison is always
false — all exact/suffix/name lookups silently return 0 rows.
Bug B: Tier 4 fuzzy search found 1 result but snippet_suggestions() always
sets status=ambiguous regardless of count.
Bug C: Dedup block with cand_count==1 after dedup fell through to ambiguous
instead of resolving.
Fixes:
1. extract_project_from_qn(): scans each dot-prefix of the QN and tests for
a matching ~/.cache/codebase-memory-mcp/{prefix}.db file (O(n), ~5-15
access() calls). Returns longest matching prefix — the QN is
self-describing so this works even on cold start (no prior search call).
Single malloc+memcpy pattern: best_end offset avoids repeated strdup.
2. handle_get_code_snippet: when project param is NULL, calls
extract_project_from_qn(qn) and opens the correct DB via resolve_store.
Falls back to srv->current_project if no DB found. Assigns result into
project (was NULL) so all existing free(project) exit paths own the memory.
3. Tier 4 fuzzy: fuzzy_count==1 now resolves directly (build_snippet_response)
instead of calling snippet_suggestions.
4. Dedup block: cand_count==1 after dedup now resolves directly.
Tests added (3 new, total 2190):
- get_code_no_project_uses_open_store_tier1: after search_graph opens a
store, get_code without project resolves via Tier 1 exact QN + eff_project
- get_code_single_fuzzy_result_resolves_not_ambiguous: wrong-prefix QN forces
Tier 4 fuzzy; single result must not return status=ambiguous
- get_code_cold_start_parses_project_from_qn: fresh server, no prior call,
extract_project_from_qn finds the DB and resolves the symbol
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Tests the fix for the bug where session "Users-athundt-.claude" matched
project "Users-athundt-.claude-codebase-memory-mcp-..." due to strncmp
prefix match without checking the separator character.
Tests cover:
- cross_project_search_not_confused_by_prefix: core bug regression
- session_dep_search_uses_session_store: "myapp.dep.lib" → session DB
- exact_session_name_uses_session_store: "myapp" → session DB
- prefix_collision_dash_after_session_name: "myapp-v2" → NOT session
- prefix_collision_underscore_after_session_name: "myapp_test" → NOT session
- prefix_collision_longer_name_with_dot_not_dep: "myapp.config" → session (by design)
- prefix_collision_completely_different_project: "other-project" → NOT session
- prefix_collision_session_is_substring_of_project: "ab" vs "abc" → NOT session
All tests clean up DB files created by resolve_store via unlink().
Total: 2187 tests passing.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Root cause: handle_search_graph used strncmp(pe.value, session_project, sp_len)
to decide whether to use the session DB. When session is "Users-athundt-.claude"
and the requested project is "Users-athundt-.claude-codebase-memory-mcp-...",
the first 22 chars match (shared path prefix), so search incorrectly opened
the empty session DB instead of the requested project's 22K-node DB.
Fix: after strncmp, also check that pe.value[sp_len] is '.' (dep separator)
or '\0' (exact match). This prevents "myapp" from matching "myapp-other-project"
while still correctly matching "myapp.dep.pandas".
Found by dogfooding: search_code_graph with explicit project name returned 0
results despite DB having 22828 nodes. Binary test from the same CWD worked
because session_project matched the target project.
2179 tests passing.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Root cause: _hidden_tools entry used yyjson_mut_obj_add_str for inputSchema,
producing a JSON string value instead of a JSON object. MCP spec requires
inputSchema to be a JSON Schema object. Claude Code validated the tools/list
response and rejected the ENTIRE list when one tool had a malformed schema,
making all 3 real tools (search_code_graph, trace_call_path, get_code)
invisible to the AI.
Fix: build inputSchema as a proper yyjson object (yyjson_mut_obj with
"type":"object" and empty "properties":{}), matching the pattern used by
emit_tool() for real tools.
Found by: dogfooding — server showed "connected" in /mcp but ToolSearch
returned nothing. Binary testing confirmed inputSchema was str not dict.
MCP best practices reference (memory/mcp-best-practices.md) confirmed
the spec requirement.
Test: all_tools_have_object_inputSchema — parses tools/list JSON response
and asserts every tool's inputSchema is yyjson_is_obj (not string/null/array).
This test would have caught this bug immediately.
Total: 2179 tests passing.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
dep_search_explicit_dep_project_name created nonexistent.db via
resolve_store → cbm_store_open_path(SQLITE_OPEN_CREATE), causing
tool_delete_project_not_found to find the file and return "deleted"
instead of "not_found".
Fix: use unique name "_tc_deptest_proj_" + unlink cleanup after test.
2178 tests passing.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Bugs found via binary dogfooding (index project + deps → search):
1. resolve_store opened wrong DB for dep projects
"myapp.dep.pandas" → opened myapp.dep.pandas.db (empty) instead of myapp.db
Fix: parent_project_for_db() strips .dep.* to find parent DB
2. store.c prefix match was actually exact match
search(project="myapp") used "n.project = ?" → missed dep nodes
Fix: "(n.project = ? OR n.project LIKE ?||'.%')" includes deps
3. cbm_is_dep_project failed for cross-project deps
"otherapp.dep.pandas" with session "myapp" → false (early return)
Fix: Fall through to generic .dep. strstr when session prefix mismatches
4. Package name extraction used session_project offset
Wrong offset when session != indexed project → truncated package names
Fix: Use strstr(".dep.") to find separator position directly
Tests (5 new, 2178 total):
- dep_search_explicit_dep_project_name: resolve_store routes to parent DB
- store_prefix_match_includes_deps: prefix returns project + dep nodes
- store_exact_match_excludes_deps: exact match returns project only
- is_dep_project_cross_project_detection: .dep. detected across projects
- e2e_dep_search_returns_project_and_dep_results: full workflow with tags
Binary verified: 18 results (9 project + 9 dependency), correct source tags,
correct package:"testlib" extraction.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Bug 1: resolve_store opens wrong DB for dep projects
Root cause: resolve_store("myapp.dep.pandas") opens myapp.dep.pandas.db
instead of myapp.db where deps actually live (same-db design).
Fix: parent_project_for_db() strips .dep.* suffix to find parent DB.
Bug 2: store.c prefix match was actually exact match
Root cause: cbm_store_search with params->project set (non-exact mode)
used "n.project = ?" — exact match, not prefix. Deps invisible.
Fix: SQL now uses "(n.project = ? OR n.project LIKE ?||'.%')" for
prefix mode, so search_graph(project="myapp") returns project + deps.
Bug 3: cbm_is_dep_project fails for cross-project deps
Root cause: Early return when session_project doesn't match prefix.
"otherapp.dep.pandas" with session "myapp" → false (should be true).
Fix: Fall through to generic .dep. strstr check when session prefix
doesn't match. Any project containing ".dep." is a dependency.
Bug 4: Package name extraction used wrong offset
Root cause: Used strlen(session_project) as offset into project name,
but session_project is CWD-detected, not the indexed project.
Fix: Use strstr(project, ".dep.") to find separator position directly.
Binary verification (all confirmed working):
- search_graph(project="myapp") → 18 results (9 project + 9 dep)
- source:"project" vs source:"dependency" correctly tagged
- package:"testlib" correctly extracted
- Multiple deps in one index_dependencies call works
Tests: 2173 passing (updated test_depindex cross-project assertion).
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Root cause: handle_resources_read returned a pre-formatted JSON-RPC error
(via cbm_jsonrpc_format_error), but cbm_mcp_server_handle wrapped it again
in cbm_jsonrpc_format_response. Result: {result: {jsonrpc, id:0, error: {...}}}
instead of the correct {error: {...}}.
Fix: handle_resources_read now takes req_id + err_out params. On error, sets
*err_out to a properly-formatted JSON-RPC error with the correct request id.
The dispatch code returns err_out directly, bypassing the result wrapper.
On success, returns raw result JSON for normal wrapping.
Also: resolve_resource_store() opens the session project DB on demand so
resources work even before any tool call.
New tests (6):
- resource_error_is_top_level_not_nested_in_result: verifies error at top
level with correct request id (the exact bug that was found)
- resource_error_missing_uri_is_top_level: same for missing uri
- resource_error_no_params_is_top_level: same for no params
- resource_success_has_result_not_error: complement — success has "result"
- resource_schema_returns_real_data_when_indexed: schema has node_labels
- resource_status_returns_not_indexed_when_no_store: fresh server status
Total: 2173 tests passing.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Bug: handle_resources_read created content_obj = yyjson_mut_obj(doc) and passed
it to build_resource_{schema,architecture,status}, but content_obj was never
added to the document root. yy_doc_to_str(doc) serialized the empty root → "{}".
Fix: pass root directly to builders instead of the orphan content_obj. Also add
resolve_resource_store() helper that opens the session project DB on demand so
resources return data even before any tool call (resources/read can be the first
call after initialize).
Verified with real indexed codebase (22,828 nodes):
- codebase://status → {"project":"...","status":"ready","nodes":22828,"edges":50639}
- codebase://schema → {"node_labels":[{"label":"Function","count":12695},...]}
- codebase://architecture → {"total_nodes":22828,"total_edges":50639,...}
Tests: 2167 passing.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Per MCP best practices (modelcontextprotocol.io/specification/2025-06-18/server/tools):
tool descriptions are the primary way LLMs discover capabilities. Descriptions must
be self-sufficient — an AI reading just the tool list should know the full workflow.
Tool description changes:
- search_code_graph: now says "Read codebase://schema for available node labels
and edge types before writing Cypher queries"
- trace_call_path: now says "Read codebase://architecture for key functions
to start tracing from"
- get_code: now says "Get qualified_name values from search_code_graph results"
- _hidden_tools hint: now lists all 3 resource URIs with usage guidance
Resource description changes (more actionable):
- codebase://schema: lists example labels, says "Read this before writing Cypher"
- codebase://architecture: mentions PageRank, says "Read this first to understand
codebase structure and find important entry points"
- codebase://status: lists all fields, says "Read this to check if project is indexed"
Tests: 2 new tests verify tool descriptions reference resources and _hidden_tools
hint mentions all 3 resource URIs. Total: 2167 tests passing.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Error messages: Every error now includes:
- WHAT failed (the specific input that caused the error)
- HOW to fix it (actionable "hint" field with next step)
- WHERE to look (tool names, param examples, valid options)
Fixed errors (18):
- "no project loaded" (3x) → + hint:"Run index_repository..."
- "function not found" → includes searched function name + hint
- "symbol not found" → includes searched qualified_name + hint
- "query is required" → + hint with Cypher syntax example
- "function_name is required" → + hint with param example
- "qualified_name is required" → + hint with format + "Use search_code_graph"
- "pattern is required" → + hint about regex vs literal
- "repo_path is required" → + hint about absolute path
- "project_name is required" → + hint:"Use list_projects"
- "project not found" (2x) → + hint:"Run index_repository or list_projects"
- "project not found or not indexed" → + hint with both options
- "failed to create pipeline" → + hint about path/permissions
- "search failed: temp file" → + hint about /tmp disk space
- "search failed" → + hint about grep installation
- "git diff failed" → + hint about git installation
- "missing tool name" → + lists available tools + "Use tools/list"
- "unknown tool: X" → + lists available tools + "Use tools/list"
New tests (16):
- MCP spec compliance: protocol version, subscribe:false, listChanged:true,
resources/list fields, resources/read contents array, missing uri, no params
- Client behavioral differences: resource client never gets _context (3 calls),
legacy client gets _context only first call, empty resources:{} counts as
support, no-initialize defaults to legacy
- Error message quality: hint field present on no-project, function-not-found
includes name, symbol-not-found includes qn, all required-param errors have
hints, unknown-tool lists valid options, resource -32002 is actionable
Total: 2165 tests passing.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Audited against https://modelcontextprotocol.io/docs/concepts/resources
F1: notifications/resources/updated → notifications/resources/list_changed
We declared listChanged:true in server capabilities, not subscribe:true.
list_changed is for data changes; updated is for per-resource subscriptions.
F2: Error code -32602 → -32002 for unknown resource URI
MCP spec Error Handling section specifies -32002 for "Resource not found".
-32602 is "Invalid params" which is wrong — the URI param is valid, the
resource just doesn't exist.
F3: Error message now actionable — includes the bad URI and lists all 3
valid resource URIs (codebase://schema, codebase://architecture,
codebase://status) with hint to use resources/list.
Tests: 2149 passing (assertions updated for new error code and message).
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Phase 10: Replace one-shot static context injection with persistent MCP resources
for clients that support them (Claude Code, VS Code Copilot, OpenCode).
Resources exposed:
- codebase://schema — node labels and edge types with counts
- codebase://architecture — graph size, key functions by PageRank, relationship patterns
- codebase://status — index status, PageRank stats, ecosystem, dependencies
Implementation:
- resources/list returns 3 resource URIs with descriptions
- resources/read dispatches by URI to build_resource_{schema,architecture,status}
- Server advertises resources capability in initialize response (listChanged:true)
- Client capabilities.resources parsed from initialize params (client_has_resources flag)
- inject_context_once skipped when client supports resources (0 token overhead)
- notifications/resources/updated sent after index_repository, index_dependencies, autoindex
- Fallback: legacy clients without resources support still get _context injection
Tests: 8 new tests (resources_list, resources_read x4, initialize_advertises,
client_capability_parsing, fallback_injection). Total: 2149 tests passing.
Also: add .claude/ to .gitignore for local project memory.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Auto-index on first use (REQUIRE_STORE + search_graph):
When store is NULL and session_root is a valid directory:
1. If autoindex_active: join background thread, re-resolve store
2. If still NULL: run cbm_pipeline_run() synchronously, then
cbm_dep_auto_index() + cbm_pagerank_compute_with_config()
Handles all 3 paths: CWD (detect_session), explicit path (Rule 0),
MCP roots (future). access(session_root, F_OK) guard prevents
triggering on non-existent paths in tests.
inject_context_once(): auto-provide architecture/schema on first response.
First tool response gets _context header with: status, nodes, edges,
node_labels, edge_types, ranked_nodes, pagerank_computed_at,
detected_ecosystem. Subsequent responses only get session_project.
Fix: use-after-free in inject_context_once (ASAN crash at mcp.c:937).
cbm_store_schema_free() freed label/type strings while yyjson still
held borrowed pointers. Fix: yyjson_mut_obj_add_strcpy() copies strings
into yyjson's allocator before schema is freed.
Fix: _hidden_tools count corrected from "12" to "14" (14 tools hidden).
Tests: 2 new (first_response_has_context_header, context_has_schema_info).
Total: 2141 tests passing.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Progressive disclosure: _hidden_tools entry in streamlined tool list tells AI
which 12 tools are hidden and how to enable them (CBM_TOOL_MODE=classic env
var or config set tool_mode classic or per-tool config set tool_<name> true).
Hidden tools still dispatch normally — AI can call them after discovery.
Env var override: CBM_TOOL_MODE env var takes precedence over config for
tool_mode. Enables backwards compat without needing a config store.
Session context: add session_project to trace_call_path and get_architecture
responses. Now all major tool responses include session_project so AI always
knows which project it's working with.
Tests: 4 new in test_tool_consolidation.c:
- streamlined_mode_has_hidden_tools_hint
- hidden_tools_still_dispatch
- search_graph_has_session_project
- index_status_has_session_project
Total: 2139 tests passing (13 new Phase 9 tests total)
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Gap 3 (trace boundary tagging): trace_call_path now tags each caller
and callee with source:"project"|"dependency" and read_only:true for
dep nodes. Uses cbm_is_dep_project() for consistent tagging.
Gap 4 (snippet provenance): build_snippet_response adds source and
read_only fields so get_code_snippet results indicate whether code
is from the project or a dependency.
Cross-edges: cbm_dep_link_cross_edges implemented — searches project
Variable nodes, looks for matching Module nodes in dep projects
(project.dep.%), creates IMPORTS edges to link them. Enables
trace_call_path to follow imports across project/dep boundary.
Gap 1 (watcher dep re-index) was already done in prior commit.
Files: src/mcp/mcp.c (trace + snippet tagging),
src/depindex/depindex.c (cross-edge implementation)
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Gap 3 (trace boundary tagging): trace_call_path now tags each caller
and callee with source:"project"|"dependency" and read_only:true for
dep nodes. Uses cbm_is_dep_project() for consistent tagging.
Gap 4 (snippet provenance): build_snippet_response adds source and
read_only fields so get_code_snippet results indicate whether code
is from the project or a dependency.
Cross-edges: cbm_dep_link_cross_edges implemented — searches project
Variable nodes, looks for matching Module nodes in dep projects
(project.dep.%), creates IMPORTS edges to link them. Enables
trace_call_path to follow imports across project/dep boundary.
Gap 1 (watcher dep re-index) was already done in prior commit.
Files: src/mcp/mcp.c (trace + snippet tagging),
src/depindex/depindex.c (cross-edge implementation)
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Root cause: handle_list_projects opens every .db file in
~/.cache/codebase-memory-mcp/ via cbm_store_open_path (which runs
CREATE TABLE IF NOT EXISTS, modifying foreign databases). With 62
stale .db files (1.3GB) including a corrupt 223MB "..db" (empty
project name), the server hung during Claude Code health checks.
Fixes:
- Add validate_cbm_db(): read-only SQLite validation with magic byte
check + 'nodes' table schema check + 1s busy_timeout. Never modifies
foreign databases. Logs actionable warnings on skip.
- Guard detect_session() against empty/dot project names that produce
the corrupt "..db" filename
- Skip "..db" and ".db" filenames in handle_list_projects
- Skip empty/dot project names after filename-to-name extraction
- Force unbuffered stdin/stdout via setvbuf for MCP stdio protocol
- Add #include <sqlite3.h> for read-only validation queries
Files: src/main.c (setvbuf), src/mcp/mcp.c (validate_cbm_db,
detect_session guard, list_projects guards, sqlite3.h include)
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Root cause: handle_list_projects opens every .db file in
~/.cache/codebase-memory-mcp/ via cbm_store_open_path (which runs
CREATE TABLE IF NOT EXISTS, modifying foreign databases). With 62
stale .db files (1.3GB) including a corrupt 223MB "..db" (empty
project name), the server hung during Claude Code health checks.
Fixes:
- Add validate_cbm_db(): read-only SQLite validation with magic byte
check + 'nodes' table schema check + 1s busy_timeout. Never modifies
foreign databases. Logs actionable warnings on skip.
- Guard detect_session() against empty/dot project names that produce
the corrupt "..db" filename
- Skip "..db" and ".db" filenames in handle_list_projects
- Skip empty/dot project names after filename-to-name extraction
- Force unbuffered stdin/stdout via setvbuf for MCP stdio protocol
- Add #include <sqlite3.h> for read-only validation queries
Files: src/main.c (setvbuf), src/mcp/mcp.c (validate_cbm_db,
detect_session guard, list_projects guards, sqlite3.h include)
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Merge latest reference-api-indexing (bd09623) with gap implementations:
Conflicts resolved (2 in mcp.c search_graph handler):
1. Param parsing: keep reduce-token-usage compact/summary/mode params,
use fill_project_params() from reference-api-indexing for smart project
2. Result loop: keep reduce-token-usage compact mode + pagination_hint,
add unconditional source/package/read_only tagging from ref-api-indexing
Superset features in merged handler:
- expand_project_param (self/dep/deps/glob/prefix resolution)
- compact mode (omit redundant name when suffix of QN)
- summary mode (aggregate counts by label/file)
- pagination_hint with offset/limit guidance
- session_project in all responses
- source:"project"/"dependency" on every result
- package + read_only:true on dep results
- dep auto-reindex in all 3 paths (handler, watcher, autoindex)
- index_status reports dependencies[] + detected_ecosystem
All 2081 tests pass (2042 base + 22 token-reduction + 17 dep-indexing).
expand_project_param() (mcp.c:764-840):
- "self" → session project exact match
- "dep"/"deps" → session.dep prefix match
- "dep.pandas" → session.dep.pandas prefix
- "myapp.pandas" → myapp.dep.pandas (auto-insert .dep.)
- Glob "*" → SQL LIKE with % substitution
- fill_project_params() helper sets cbm_search_params_t fields
search_graph result tagging (mcp.c:930-960):
- Every result tagged source:"project" or source:"dependency"
- Dep results get package name + read_only:true
- session_project added to response for AI project name awareness
- Uses cbm_is_dep_project() with session context for precision
handle_index_status (mcp.c:1046-1100):
- Reports dependencies[] array with package names and node counts
- Reports detected_ecosystem from project root marker files
- session_project in response
Dep auto-reindex in all 3 re-index paths:
- handle_index_repository (mcp.c:1472): cbm_dep_auto_index after dump
- watcher_index_fn (main.c:86-96): cbm_dep_auto_index after dump
- autoindex_thread (mcp.c:2496-2501): cbm_dep_auto_index after dump
All use DRY cbm_dep_auto_index() with CBM_DEFAULT_AUTO_DEP_LIMIT
cbm_mcp_server_set_session_project() added (mcp.h:128, mcp.c:526)
Fix: yyjson_mut_obj_add_strcpy for dep package names from search results
(heap-use-after-free when cbm_store_search_free frees borrowed strings)
Fix: db_project selection when session_project is empty (integration test
integ_mcp_delete_project was failing — resolve_store got NULL instead of
project name after expand_project_param)
Tests: 29 depindex tests (2059 total, all passing)
- test_search_results_have_source_field: project results tagged
- test_search_dep_results_tagged_dependency: dep results have package+read_only
- test_search_response_has_session_project: session_project in response
- test_index_status_shows_deps: dependencies[] in index_status response
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Merge reduce-token-usage branch into token-reduction-and-reference-indexing.
Conflict in codebase-memory-reference/SKILL.md resolved by taking the
reduce-token-usage version (has complete token reduction documentation).
New module src/depindex/ with package resolution (uv/cargo/npm/bun),
ecosystem detection, dep discovery from indexed graph, auto-index helper,
and cross-boundary edge creation stub. Dependencies stored in same db
with {project}.dep.{package} naming convention.
Pipeline changes:
- Add CBM_MODE_DEP index mode (keeps vendor/, .d.ts for dep source)
- Add cbm_pipeline_set_project_name() to override auto-derived name
- Add cbm_pipeline_set_flush_store() for upsert vs fresh dump
- Conditional dump/flush at pipeline.c:646
Store changes:
- Add project_pattern (LIKE) and project_exact fields to cbm_search_params_t
- Support LIKE queries for glob-style project filtering
- Add project-first ORDER BY for mixed project+dep results
- Stable pagination via ORDER BY name, id
MCP changes:
- Replace index_dependencies stub with full implementation
(source_paths[] primary interface, package_manager optional shortcut)
- Fix detect_session() to use cbm_project_name_from_path (Bug #12)
- REQUIRE_STORE error now includes actionable hint field
- search_code: fix -m limit exhaustion (limit*50 min 500 vs limit*3)
- search_code: add case_sensitive param (default false = case-insensitive)
DRY improvements:
- CBM_MANIFEST_FILES shared list in depindex.h used by pass_configlink.c
and dep discovery (adds pyproject.toml, setup.py, Pipfile)
- Remove package.json and composer.json from IGNORED_JSON_FILES
(needed by pass_configlink and dep auto-discovery)
Tests: 25 depindex tests (2055 total, all passing)
- Package manager parse/str roundtrip, dep naming, is_dep detection
- Ecosystem detection (python/rust/none), manifest path matching
- npm resolution with fixture, pipeline set_project_name
- MCP tool validation, AI grounding, dep reindex replaces
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Comprehensive feature matrix documenting:
- Branch availability for all 13 existing + new features
- Composability matrix showing how features interact when combined
- Detailed interaction table with justifications for each combination
- Strengths and limitations of each feature with specific measurements
- AI grounding 7-layer defense failure mode analysis
- Architecture diagram showing composable pipeline stages
- 5 generalizable design patterns extracted from the implementation
Key composability findings:
- summary mode overrides limit (uses 10K for accurate aggregation)
- signature mode overrides max_lines (no file I/O needed)
- compact applies independently at serialization stage
- include_dependencies composes with all token reduction features
- _result_bytes/_est_tokens always reflects final output size
Tests: 2064 passed, 0 failed
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
The include_dependencies parameter was parsed in the handler (line 776)
but not declared in the TOOLS[] schema JSON. This meant LLMs could not
discover the parameter from tool descriptions — it was silently accepted
but undiscoverable.
Added include_dependencies boolean property with description to the
search_graph tool schema, matching the merged branch's schema.
Tests: 2042 passed, 0 failed
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
Three defensive guards for out-of-memory conditions:
1. trace_call_path: calloc for seen_out/seen_in dedup arrays now
gracefully degrades — if calloc returns NULL, dedup is skipped
(may return duplicates) instead of NULL-dereference crash
2. build_snippet_response: head_tail combined buffer malloc is
NULL-checked — on OOM, falls back to outputting head portion
only instead of passing NULL to snprintf
All guards are idiomatic C (if-pointer-check, no gotos). Existing
tests cover the functional behavior; OOM paths are defensive safety
nets for production resilience.
Tests: 2052 passed, 0 failed
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
- Token metadata comment: explain _result_bytes (byte length of inner
JSON text) and _est_tokens (bytes/4, same heuristic as RTK's
estimate_tokens function in tracking.rs)
- Pagination hint: add comment explaining the pagination_hint field
purpose (tells caller how to get next page)
- Head/tail mode: document the 60/40 split rationale (60% head captures
signature/setup, 40% tail captures return/cleanup; middle
implementation detail is what gets omitted)
Tests: 2064 passed, 0 failed
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>