CodeNavi — API Reference¶
All tools are accessed via the Model Context Protocol (MCP). Connect to http://localhost:8765/mcp using any MCP-compatible client.
Tools are called as MCP tools/call requests. Parameters are passed as JSON objects. All tools return plain text (formatted for LLM consumption, not raw JSON).
41 tools total: 33 core tools + 8 crawler tools (when CRAWLER_ENABLED=true). The machine-readable catalog — grouped by skill, with connection details and usage rules — is the agent card at GET /.well-known/agent-card.json.
Tool profiles:6 by default (CODENAVI_TOOL_PROFILE=lean, since 1.3.23) tools/list advertises an 11-tool core — the search/browse/read loop (semantic_search, file_search, grep_search, symbol_search, list_directory, read_file, codebase_overview, list_codebases) plus the everyday index-ops tools (reindex_incremental, reindex_status, index_stats) — and a reveal_tools() meta-tool, shrinking the per-conversation schema ~−57% (measured 2026-09-02: 52.6k → 22.9k compact chars on a 42-tool crawler-enabled install; the three index-ops tools cost ~650 tokens). Set CODENAVI_TOOL_PROFILE=full to advertise every tool below up front (the pre-1.3.23 default). Nothing is lost: call reveal_tools() for a grouped catalog of the hidden tools, then reveal_tools(group="relations") or reveal_tools(names="find_callers,diff_impact") to load them into the live session on demand — the server re-registers them and emits tools/list_changed. Note that a client re-lists tools only when it receives tools/list_changed, and the default stateless HTTP transport has no open stream to deliver that on — so a revealed tool may never appear in the client's list. To use a hidden tool, pass reveal_tools(call="find_callers", args='{"symbol": "handle_login"}'): the server reveals it, runs it, and returns its result in the same call (since 1.3.30).19
Search Tools¶
semantic_search¶
Search across code, documentation, and crawled web pages by meaning. Queries all three LanceDB tables in parallel, merges results, and reranks with a cross-encoder.
Works with natural language ("how does the login flow work?"), function names ("verifyCredentials"), or mixed queries ("PayPal payment error handling").
Zero-result responses carry a coverage note12 when the queried codebase has materially unindexed file types — e.g. note: some files in this codebase are not indexed — 301 .tmpl files … were skipped at index time — in the text envelope and as coverage_note in the structured payload, so an empty answer is distinguishable from an unindexed corpus.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
query |
string |
(required) | The search query — natural language or code identifiers |
limit |
integer |
10 |
Maximum results to return. Use 5 for focused queries, 8-10 for broad exploration. |
source_filter |
string |
"" |
Filter by file path prefix. Example: "public_html/site/" |
language_filter |
string |
"" |
Filter by programming language: php, js, ts, py, go, java, ruby, rust, csharp, fortran, sql, smarty, kotlin, etc. |
source_type_filter |
string |
"" |
Filter by source: "code" (code only), "doc" (docs only), "web" (web only), or "" (all three) |
max_chars |
integer |
0 |
Per-query response budget in characters. 0 = use server default (15,000). Lower values save context window — chunks exceeding the budget return metadata only (file, lines, score) without source code, and chunks that would not fit even as metadata are dropped and counted in <chunks_omitted>. The top hit is always returned in full, so a budget smaller than one chunk is exceeded by that chunk alone. |
codebase |
string |
"" |
Name of a specific codebase to search, or "" for the server's default_codebase (every codebase when that is unset). Use list_codebases to see available names. |
Returns: XML-formatted results.
The response opens with a <budget> envelope giving the caller a read on
response shape:
<budget>
<chars_used>5521</chars_used>
<chars_budget>15000</chars_budget>
<chunks_truncated>0</chunks_truncated>
<chunks_omitted>0</chunks_omitted>
<chunks_returned>5</chunks_returned>
<diagnostic>concentrated</diagnostic> <!-- conditional, see below -->
</budget>
<chars_used>/<chars_budget>— actual vs allowed response size in characters. Agents can widenmax_charsif they need more.<chunks_truncated>— chunks returned as metadata-only (no<code>block) because the budget was exhausted.<chunks_omitted>— chunks dropped entirely because even a metadata-only entry would have breached the budget. They still count in<chunks_returned>; raisemax_charsto see them.<chunks_returned>— number of semantic chunks (excludes any keyword-fallback stubs appended afterwards).<diagnostic>— optional, may repeat.concentrated: all top-3 results come from the same file (the old score-spread co-condition was dropped9 — its scale died with hybrid retrieval) — the query was probably too narrow and broadening it may surface adjacent paths.low_signal8: the top hit's rerank logit is below the measured 5th percentile of queries with a ground-truth hit in their window (calibrated across five testbeds, 2026-07-29) — likely nothing on-topic; reformulate in the codebase's own vocabulary instead of trusting the window or falling back to grep.<rerank>— optional.9 Emitsskippedwhen a configured reranker returned no scores for this query (load failure or per-query timeout). Result order is then raw vector order and<score>values are retrieval cosine similarities rather than cross-encoder output. Absent when the reranker ran, and whenRERANKER_MODEL=none(an opt-out is not degradation).
After the envelope, each result contains:
<source_file>— relative file path (or URL for web results)<lines>— line range (start-end)<chunk_id>— unique identifier (used for feedback)<language>— detected language<source_type>—code,doc, orweb<codebase>— codebase name (only present when multiple codebases are configured)<code>— the matching snippet<score>— ranking score (0-1): sigmoid of the cross-encoder rerank logit. Relative within one response, not calibrated confidence — the reranker is trained on web prose, so correct code hits routinely score 0.01–0.20. Judge by order and gaps, not absolute level.8<stale>— present only when the chunk's source file has been modified or deleted since indexing; agents should re-verify the snippet viaread_file(or runreindex) before quoting it
Some queries also append keyword-fallback entries below the main results —
files that match alias-expanded keywords but didn't show up via vector
search. These have <match_type>keyword</match_type> and <matched_keywords>
instead of <code>. Doc-flavored files (.md / .rst / .txt) are
suppressed from this fallback unless source_type_filter="doc".
Structured output:10 alongside the XML text, the tool result carries
a structuredContent object validated against the tool's advertised
outputSchema (core MCP protocol, 2025-06-18 spec). The payload is a
self-sufficient mirror of the envelope — clients may feed the model either
channel (Claude Code feeds structuredContent instead of the text when it is
present), so each must stand alone. Hits carry file,
line_start/line_end, chunk_type/chunk_name, language, source_type,
score, chunk_id, and the code snippet (absent on budget-truncated
entries, mirroring the text channel; JSON-raw — escape it before inserting
into a DOM), plus optional codebase/flagged/stale; the top level adds
diagnostics, rerank_skipped, the budget block, index_freshness
(mirroring the <index_freshness> line), and any keyword_fallback entries.
On error/edge paths the payload is
{"ok": false, "error": "<the same message the text carries>", "results": []}.
Six tools emit structured output: semantic_search, find_callers,
impact_analysis, graph_neighborhood, wiki_pages, wiki_page. All other tools deliberately
advertise no outputSchema (registered with structured_output=False):
without it, the SDK auto-derives a {result: string} wrapper and structured-
preferring clients would feed the model an escaped JSON blob instead of the
formatted text. A tool gains a schema only when it gains a real payload.
file_search¶
Find files by name, path, or description.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
query |
string |
(required) | File search query |
limit |
integer |
15 |
Maximum files to return |
source_filter |
string |
"" |
Filter by path prefix |
language_filter |
string |
"" |
Filter by language |
source_type_filter |
string |
"" |
"code", "doc", or "" |
codebase |
string |
"" |
Name of a specific codebase to search, or "" for the server's default_codebase (every codebase when that is unset) |
Returns: File paths with content previews.
read_file¶
Read a file from the codebase by its relative path. Returns the file content with line numbers. Use after semantic_search or file_search to read the full file or a specific section.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
path |
string |
"" |
Relative file path (required — or use file_path) |
file_path |
string |
"" |
Alias for path (matches Claude Code's built-in Read tool) |
start_line |
integer |
0 |
Start reading from this line (1-based) |
end_line |
integer |
0 |
Stop at this line. 0 reads to end of file, capped at DEFAULT_READ_LINES (2000) with a "call again with start_line=…" continuation note |
codebase |
string |
"" |
Which codebase to read from, or "" for the server's default_codebase (the only codebase on a single-codebase install; a multi-codebase server with no default refuses the unscoped call and names the codebases) |
Returns: File content with line numbers, secrets redacted.
When to use: After a search hit, to read the full file or a specific range.
list_directory¶
List files and subdirectories at a path in the codebase. Use to explore project structure before reading specific files.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
path |
string |
"" |
Directory to list ("" = codebase root) |
depth |
integer |
2 |
How many levels deep to show (max 5) |
codebase |
string |
"" |
Which codebase to list, or "" for the server's default_codebase (the only codebase on a single-codebase install; a multi-codebase server with no default refuses the unscoped call and names the codebases) |
Returns: Tree-style directory listing.
codebase_overview¶
Get a structural overview of the indexed codebase. Auto-detects languages, directory structure, file distribution, and symbol counts from the index — works for any codebase without configuration.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
section |
string |
"structure" |
structure (default — every top-level dir with subtree chunk totals + detected entry points, ~1k tokens), all (full overview, ~10k tokens on a large codebase), languages, files, symbols, entrypoints (front controllers, main.* files, script dirs, vhost→docroot pairs mined from indexed nginx/apache configs) |
codebase |
string |
"" |
Name of a specific codebase, or "" for all |
Returns: With the default structure section, top directories by chunk count. Pass section="all" for the full overview (language breakdown, top directories, largest files, function/class counts).
Default changed in v1.2.4 (was
"all"). The full overview burns ~10k tokens on a large codebase; most "what is this codebase?" questions are answered bystructurealone. Passsection="all"explicitly when you want everything.
reindex_incremental¶
Re-encode only files modified since the last index — safe and fast (~5 sec on a mid-sized codebase). Returns immediately; the work runs in the background. Auto-allowed by the recommended plugin allowlist (no permission prompt).
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
codebase |
string |
"" |
A specific codebase name, all (or *) for every codebase, or "" to use the server's default_codebase setting — which falls back to all when unset5. MCP tools only; the ./codenavi reindex CLI / reindex.sh path keeps ""=all. |
Returns: Confirmation message, or rate-limit notice (minimum 5 minutes between calls).
When to use: When a search response's <index_freshness> envelope flags a stale index (a <recommendation>reindex_incremental</recommendation> appears once it's >1h old), or after editing files you're about to search. Runs in the background — confirm completion with reindex_status. Default for the /codenavi:reindex slash command. On a multi-codebase server, set default_codebase in data/settings.json so a no-arg call reindexes just that codebase rather than rebuilding every codebase's vector index; pass codebase=all to force the full sweep.5
reindex_full¶
Re-encode the entire codebase from scratch (~6-40 min depending on codebase size). Drops and rebuilds all chunks. Returns immediately; the work runs in the background. Always prompts for permission — intentional given the runtime cost and search downtime while vectors rewrite.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
codebase |
string |
"" |
A specific codebase name, all (or *) for every codebase, or "" to use the server's default_codebase setting — which falls back to all when unset5. MCP tools only; the ./codenavi reindex CLI / reindex.sh path keeps ""=all. |
Returns: Confirmation message, or rate-limit notice (minimum 5 minutes between calls).
When to use: Suspected index corruption, embedding model changed, or directories renamed such that incremental tracking is confused. Use reindex_incremental for everyday staleness.
reindex (deprecated)¶
Deprecated alias for reindex_incremental (full=False) and reindex_full (full=True). Prefer the explicit tools — they are clearer in tool-call logs and let plugin allowlists auto-approve the safe incremental path while keeping a permission prompt on the destructive full path.
Kept for backward compatibility with reindex.sh and existing agents; will not be removed without notice.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
full |
boolean |
false |
false = incremental (delegates to reindex_incremental). true = full rebuild (delegates to reindex_full). |
codebase |
string |
"" |
A specific codebase name, all (or *) for every codebase, or "" to use the server's default_codebase setting — which falls back to all when unset5. MCP tools only; the ./codenavi reindex CLI / reindex.sh path keeps ""=all. |
Returns: Confirmation message, or rate-limit notice (minimum 5 minutes between calls).
reindex_status¶
Check the progress of the most recent background reindex.
Parameters: None.
Returns: Running status, progress, duration, and any errors. A completed run's summary includes a Skipped: line12 counting every file the scan dropped, by reason. If the previous reindex died without recording an outcome (server restart, OOM-kill), the response says so explicitly — ⚠ … was INTERRUPTED14 — instead of implying nothing ran; the notice self-clears when a run completes.
cancel_reindex¶
Cancel a running background reindex. The reindex stops after the current encoding batch completes. The existing index remains unchanged (no partial data). A cancel that arrives during the scan or the structural (call-graph) phase takes effect at the first encoding checkpoint, so on a large first index the stop can take a minute or two — the call-graph sidecar is built before the encode since 1.3.38 (CN-706).
Parameters: None.
Returns: Confirmation with elapsed time, or "No reindex is currently running."
doc_sources¶
List all indexed documentation files with chunk counts.
Parameters: None.
Returns:
Indexed documentation (3 files, 12 chunks):
docs/architecture.pdf: 5 chunks
docs/api-guide.docx: 4 chunks
README.md: 3 chunks
reindex_docs¶
Ingest PDF and DOCX files from DOCS_DIR. Scans for security threats before ingestion. Existing records from DOCS_DIR are replaced (deduplication). .md and .txt files within the codebase are indexed automatically during reindex — this tool is only needed for external PDF/DOCX files. On a multi-codebase server, when DOCS_DIR/<codebase>/ exists it is scanned instead of the flat DOCS_DIR root, so each codebase ingests only its own external docs; the flat layout is a legacy fallback (logged warning).
Parameters: None.
Returns: Summary: chunks ingested, files skipped, errors.
Security checks: Encrypted PDFs, embedded JS (/JS, /JavaScript), auto-execute actions (/OpenAction, /AA), launch commands, embedded files, metadata scanning.
submit_feedback¶
Rate a search result chunk. Down-voted chunks (net rating < -2) are demoted in future searches. Rate-limited to 5 submissions per chunk per 5-minute window to prevent abuse.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
query |
string |
(required) | Original search query |
chunk_id |
string |
(required) | From <chunk_id> in search result |
source_file |
string |
(required) | From <source_file> in search result |
rating |
integer |
(required) | +1 (helpful) or -1 (not helpful) |
comment |
string |
"" |
Optional explanation |
Returns: Confirmation, or rate-limit notice.
feedback_stats¶
View aggregated feedback: total votes, top/bottom-rated chunks, demotion list, recent comments.
Parameters: None.
index_stats¶
View index statistics: file count, chunk count, breakdown by chunk type, files with the most chunks — plus the skip ledger12: a skipped-by-reason table from the last reindex (per-extension counts, ignore-file attribution, oversize examples, scanner rejects), answering "is a language missing from the breakdown because the repo has none, or because the scan dropped them?"
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
codebase |
string |
"" |
Name of a specific codebase, or "" for all |
search_analytics¶
View query statistics from the search log: most common queries, zero-result queries, alias expansion stats, and retry patterns (same query within 5 minutes).
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
since |
string |
"" |
Only include queries after this date (YYYY-MM-DD). Empty = all time. |
top |
integer |
20 |
Number of entries to show in each category (top queries, zero-result, etc.) |
Returns: Text report with sections: Top Queries, Zero-Result Queries, Alias Expansions, Retries.
Clearing or scoping the data. Analytics is read from the append-only query log (
deploy/logs/queries.log*) — there is no separate reset command. To exclude benchmark/eval traffic, setsince(CLI:--since) to just after your last benchmark run; it filters without deleting. To hard-reset, delete the query log (rm deploy/logs/queries.log*) — see PRIVACY.md → How to delete all data. Ratings work the same way: there is no clear command — remove rows or deletedeploy/data/feedback.db.
symbol_search¶
Search for symbols (functions, classes, methods) by name. Uses FTS lookup — no GPU encoding needed. Fast alternative to semantic_search when you know the symbol name.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
name |
string |
(required) | Symbol name to search for (e.g. "createUser", "UserAuth") |
kind |
string |
"" |
Filter by symbol kind: function, class, method, template, or "" (all) |
language_filter |
string |
"" |
Filter by language: php, js, py, etc. |
source_filter |
string |
"" |
Filter by file path prefix |
limit |
integer |
20 |
Maximum results |
include_source |
boolean |
false |
If true, includes the full source code of each symbol |
codebase |
string |
"" |
Name of a specific codebase to search, or "" for the server's default_codebase (every codebase when that is unset) |
Returns: XML-formatted results:
<symbol>
<name>Auth::verifyCredentials</name>
<kind>function</kind>
<file>public_html/lib/Authorize.php</file>
<lines>16-82</lines>
<language>php</language>
<code>...</code> <!-- only if include_source=true -->
</symbol>
When to use: You know the function or class name and want its definition. Faster than semantic_search for exact name lookups.
find_references¶
Find all references to a symbol name across the indexed codebase. Like grep but uses the search index — returns results grouped by file.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
name |
string |
(required) | Symbol name to find references for |
language_filter |
string |
"" |
Filter by language |
source_filter |
string |
"" |
Filter by file path prefix |
limit |
integer |
30 |
Maximum results |
codebase |
string |
"" |
Name of a specific codebase to search, or "" for the server's default_codebase (every codebase when that is unset) |
Returns: References grouped by file:
Found 10 reference(s) to 'authenticate' across 8 file(s) [64ms]
**public_html/lib/Authorize.php**
L16-82 (function: Auth::verifyCredentials)
L90-120 (function: Auth::checkSession)
**public_html/site/members/act_login.php**
L5-45 (file_part: act_login.php:1-45)
When to use: You found a function with symbol_search and want to know where it's called from. Also useful for impact analysis before refactoring.
grep_search¶
Regex-search the indexed source. Faster and cheaper than semantic_search for exact matches — variable names, function calls, config values, error messages, imports, SQL fragments. No GPU cost.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
pattern |
string |
(required) | Regex pattern to match. Accepts query as an alias, matching semantic_search's naming. |
path_filter |
string |
"" |
Only search files whose path starts with this literal prefix, anchored at the codebase root (e.g. public_html/admin/, or a file path like core/graph.py). Not a substring and not a glob — blacklist will not match public_html/site/blacklist/, and *blacklist* is rejected outright. When the prefix excludes every file the response says so and suggests matching directories, rather than reporting a no-match. |
path_exclude |
string \| string[] |
"" |
Skip files under this path prefix, or any of a list of prefixes (e.g. ["tests/", "vendor/"]). Same literal-prefix rules as path_filter. Combine with path_filter to scope tightly. |
language_filter |
string |
"" |
php, js, py, etc. |
case_sensitive |
boolean |
false |
If true, match exact case |
max_results |
integer |
50 |
Max matches to return (hard cap 200) |
context_lines |
integer |
0 |
Include N lines before and after each match (capped at 5). Useful when you'd otherwise follow up with read_file. |
codebase |
string |
"" |
Name of a specific codebase to search, or "" for the server's default_codebase (every codebase when that is unset) |
Returns: Match list with file paths, line numbers, and matched text. Header reports total matches found (even when capped) so callers know whether to widen or narrow the search.
Why prefer this over the built-in Grep tool / shell grep:
- CRLF immunity. The CodeNavi index normalizes line endings during ingest. Files with mixed CRLF/LF (common in Windows-origin or long-lived legacy codebases) silently break shell
grep— patterns spanning line ends miss matches because\n≠\r\nin the regex engine.grep_searchoperates on normalized chunks, so mixed-line-ending codebases match identically to clean-LF ones. - No shell overhead. Runs against in-memory chunk text — no
findtraversal, no per-file process spawn. - Same exclusions as the index.
vendor/,node_modules/,__pycache__/, etc. are already excluded; you don't need to remember the per-codebase ignore list.
When to use built-in Grep instead: trivial known-string match in a small directory the user just told you about, or when the file isn't indexed (e.g. recent untracked file).
list_codebases¶
List all configured codebases with their paths and index status (chunk counts).
Parameters: None.
Returns:
- **myapp**: /var/www/myapp (8234 code chunks)
- **api**: /var/www/api (3102 code chunks)
When to use: Before using the codebase filter on other tools — shows you what names are available.
generate_claude_md¶
Auto-generate a CLAUDE.md project summary from the indexed codebase. Analyzes directory structure, language breakdown, key classes and functions, entry points, and configuration files.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
codebase |
string |
"" |
Name of a specific codebase, or "" for default |
Returns: Markdown-formatted project summary:
# CLAUDE.md — Project Summary
*Auto-generated from 3723 indexed files, 14415 code chunks.*
## Languages
- **php**: 8748 chunks (61%)
- **js**: 532 chunks (4%)
...
## Directory Structure
- `public_html/admin/` — 846 files
...
## Entry Points
- `public_html/index.php`
...
## Key Classes
- **UserAuth** — `public_html/lib/Login.php` (120 lines)
...
## Key Functions
- `verifyCredentials` — `public_html/lib/Authorize.php` (66 lines)
...
When to use: Onboarding to a new codebase, generating context for AI agents, or sharing project structure with team members.
Wiki Tools¶
Two read-only tools that mirror the web UI's Wiki screen — LLM-compiled knowledge pages over a codebase's own indexed documents (never code). Neither tool triggers a compile.
wiki_pages¶
List compiled wiki pages for a codebase.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
codebase |
string |
"default" |
Which codebase's wiki to list |
Returns: structuredContent with each page's slug, title, and kind (concept/entity). Multi-source pages only by default (≥2 contributing documents), with a note on how many single-source pages were hidden.
wiki_page¶
Fetch one compiled wiki page.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
slug |
string |
(required) | The page's slug, from wiki_pages |
codebase |
string |
"default" |
Which codebase's wiki to read from |
Returns: The page's rendered markdown, including its Sources section (one entry per contributing document with a locator back to the source).
Investigation Notes Tools¶
Agent-written investigation notes (CN-550), stored per codebase and shared across every agent surface. Writes are security-scanned; every note is anchored to ≥1 indexed file and automatically goes stale when an anchor file changes (re-hashed after every reindex) or archived when it leaves the index. Fresh notes also blend into semantic_search results as clearly-labeled advisory hits. Disable the whole surface with CODENAVI_NOTES_ENABLED=false. On lean-profile installs the four tools load via reveal_tools(group="notes").
save_note¶
Save (or update) an investigation note. Notes are pointers, not copies — summarize what you learned and reference code as file:line; bodies that are mostly pasted code, oversized bodies (>4096 bytes), secrets, and injection patterns are rejected with a reason. Embeds at save time, so it takes the search lock — never call in parallel with other search tools.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
title |
string |
(required) | Short note title |
body |
string |
(required) | Markdown body, max 4096 bytes; pointers not copies |
anchors |
array |
(required) | [{"file": "rel/path.py", "start_line": N, "end_line": M}] — ≥1 required, lines optional |
category |
string |
"" |
flow, pattern, gotcha, or domain |
tags |
array |
[] |
Free-form string tags |
slug |
string |
"" |
Pass an existing slug to update that note in place (the refresh path for stale notes) |
codebase |
string |
"" |
Target codebase ("" = the default codebase) |
Returns: Saved note '<slug>' (status: fresh). or REJECTED: <reason> — fix the reason and re-save. Per-codebase cap: 500 notes.
search_notes¶
Vector search over saved notes only. Stale hits carry a [stale: <file> changed since written] marker — re-verify against the current code, then save_note with the same slug. Archived notes never appear here.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
query |
string |
(required) | Natural-language query |
limit |
integer |
5 |
Maximum results |
include_stale |
boolean |
true |
Include [stale]-marked notes |
codebase |
string |
"" |
Target codebase |
list_notes¶
Browse view of all notes — the operator surface. Includes archived notes.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
status |
string |
"" |
Filter: fresh, stale, archived, or "" (all) |
limit |
integer |
50 |
Maximum rows |
codebase |
string |
"" |
Target codebase |
Returns: one line per note: slug, status, title, author agent, updated time.
delete_note¶
Permanently delete one note by slug — the purge path for wrong or poisoned notes. No undo.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
slug |
string |
(required) | Slug returned by save_note / shown by list_notes |
codebase |
string |
"" |
Target codebase |
Structural / Relations Tools¶
Structural call-graph queries over the per-codebase relations sidecar (relations_{codebase}.db, built from the same tree-sitter pass as the index). Edges are name-based, not type-resolved — an edge records "this function contains a call spelled foo(...)"; when several definitions share a name the output says so rather than guessing. Callers must be in the same language family as the callee's definition (JS/TS count as one) — a Python x.init() never reaches a JavaScript init(); set CODENAVI_GRAPH_CROSS_LANGUAGE=true to walk across languages anyway.15 Treat results as a lower bound: dynamic dispatch, reflection, route tables, and templates are invisible to static edges. In the lean tool profile these are hidden behind reveal_tools(group="relations").
find_callers¶
Who calls a symbol? Reverse-walks the precomputed call graph up to depth hops. Applies same-file shadowing (a call site in a file that locally defines the name resolves to that local definition) and notes when a name has multiple definitions.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
symbol |
string |
(required) | Bare name (sendMail) or qualified (Class::method) |
codebase |
string |
"" |
Which codebase, or "" for the server's default_codebase (the only codebase on a single-codebase install; a multi-codebase server with no default refuses the unscoped call and names the codebases) |
depth |
integer |
1 |
Hops to walk (max 4) |
limit |
integer |
50 |
Max call sites (max 200) |
Returns: the symbol's definition location(s), an ambiguity note if the name is overloaded, and per-hop call sites (path:line in <caller>). structuredContent10 mirrors this as definitions (name/qualified/kind/file/lines), call_sites (depth/file/line/caller), and ambiguous/capped flags. The output ends with the same Graph coverage: line impact_analysis appends (below) whenever the graph has blind spots, carried as coverage_note in the structured payload17 — a caller list is evidence, and a caller the graph cannot see would otherwise read as not called.
When to use: trace who depends on a function before you change it.
impact_analysis¶
Blast radius: what is affected if a symbol or a file changes? Reverse-walks the call graph depth hops, groups affected files, and flags test files separately. It also reads per-symbol centrality7 (degree + PageRank): the report leads with a ⚠ Hub warning when the target is one of the most-connected symbols in the codebase, and adds a "Hubs in the blast radius" section calling out high-importance symbols the change reaches, so a load-bearing change is obvious at a glance. (Hub annotations appear only once the codebase has been reindexed on a build with centrality support; otherwise the output is unchanged.)
Coverage-aware:13 every verdict appends a Graph coverage: line quantifying what the call graph cannot see (never-indexed extensions from the skip ledger, scanner rejects, indexed-but-no-extractor languages, and any language where 60% or more of its call sites have no named caller16 — top-level script code has none to name, and a definition shape the extractor does not recognise loses the calls in its body), and a file-path target whose extension was skipped at index time is refused — Cannot assess impact … treat this as UNKNOWN, not low impact — instead of returning a confident empty result. The structured payload carries the caveat as coverage_note.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
target |
string |
(required) | A symbol name (Mailer::send) or a relative file path (src/auth/login.php) |
codebase |
string |
"" |
Which codebase, or "" for the server's default_codebase (the only codebase on a single-codebase install; a multi-codebase server with no default refuses the unscoped call and names the codebases) |
depth |
integer |
2 |
Hops to walk (max 4) |
limit |
integer |
100 |
Max affected files listed (max 500) |
Returns:
Impact of changing symbol 'Facade::shouldReceive' — codebase: laravel
⚠ Hub: 'Facade::shouldReceive' is one of the most-connected symbols in the call graph (degree 1575: callers + callees). Changes here tend to have wide reach — review the blast radius carefully.
Affected files within 2 hop(s): 5 (4 test file(s))
Code:
- src/Illuminate/Support/Facades/Facade.php (via Facade::shouldReceive)
Tests:
- ...
Hubs in the blast radius (high-importance symbols affected — verify these first):
- Facade::shouldReceive (src/Illuminate/Support/Facades/Facade.php)
Caveat: edges are static name-based call sites … treat this as a lower bound, not a proof of safety.
structuredContent10 mirrors the report as target/target_kind/hub_target plus an affected list (file, is_test, via caller symbols, and hubs when god-nodes are reached).
When to use: before a refactor or rename, to gauge how far the change reaches and whether it touches load-bearing code.
graph_neighborhood¶
Bounded caller/callee sub-graph around a symbol, as structured nodes/edges — the same engine behind the web UI's Graph screen12 (core/graph.py), including class↔method contains edges and centrality7 (degree/pagerank) per node.
MCP App (SEP-1865):11 the tool declares _meta.ui.resourceUri = ui://codenavi/graph. On hosts that render MCP Apps (Claude web/desktop, VS Code, Goose), the result appears as an interactive explorer — click a node for an inspector, Expand pulls its 1-hop neighborhood via tools/call, and Tell the model shares the focused node into the conversation via ui/update-model-context; exploration clicks otherwise cost no context. The bundle is fully self-contained (vendored Cytoscape inlined; the spec's default CSP allows no external fetches) and renders indexed content via textContent/canvas only — never parsed as HTML. On text-only clients the tool is still useful as data: the text envelope carries a bounded summary (top 30 nodes by degree, 40-edge sample), structuredContent the full sub-graph.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
seed |
string |
(required) | A symbol name (sendMail) or qualified Class::method |
codebase |
string |
"" |
Which codebase, or "" for the server's default_codebase (the only codebase on a single-codebase install; a multi-codebase server with no default refuses the unscoped call and names the codebases) |
depth |
integer |
2 |
Hops to walk (max 4) |
direction |
string |
"both" |
"in" (callers), "out" (callees), or "both" |
node_cap |
integer |
300 |
Max nodes (max 1000); truncated: true flags a partial sub-graph |
Returns: structuredContent with nodes (id, label, kind, file, line, ambiguous, degree, pagerank), edges (source, target, relation: calls|contains, line), plus truncated/node_cap/notes. Name-based edges — treat as a lower bound.
When to use: visual/structural exploration around a symbol; the data twin of the web UI Graph screen for agents and MCP Apps hosts.
diff_impact¶
Blast radius of a unified diff. Maps each changed hunk's line range to the symbols it overlaps (via the sidecar's line ranges), then reverse-walks the call graph — separating ripples inside the changed files from the external blast radius. Also surfaces hubs.7 Changed files the graph cannot assess are attributed precisely13 — NOT indexed (<ext> skipped at index time) vs no call-graph extractor (<language>) — under a heading stating their impact is unknown, not zero.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
diff |
string |
(required) | Unified diff text (e.g. the output of git diff) |
codebase |
string |
"" |
Which codebase, or "" for the server's default_codebase (the only codebase on a single-codebase install; a multi-codebase server with no default refuses the unscoped call and names the codebases) |
depth |
integer |
2 |
Hops to walk (max 4) |
limit |
integer |
100 |
Max affected files listed (max 500) |
When to use: pre-commit or PR review — paste a git diff to see what the change ripples into before you push.
Chat & Backup Tools¶
chat — retired as an MCP tool (CN-558)¶
The chat MCP tool was removed in v1.3.22. An MCP caller is itself an LLM with
the retrieval tools in hand — a second LLM synthesizing on its behalf answered
worse than the caller would (and was the one agent-triggerable tool that sent
retrieved source code off-host when a remote provider was configured).
Grounded Q&A itself is unchanged and stays on the human-facing surfaces, all
backed by the same pipeline (core/chat.py):
- SPA Chat screen (
/ui) — pick the LLM in the chat rail POST /chat— see HTTP Endpoints; this is what thecodenavi chatCLI uses- Chat output is still scanned (
chat_outputscope), and every[code] file:linecitation is validated against the chunks retrieved for that turn — citations the context does not support are flagged in the answer instead of presented as grounded
Agents wanting an answer about the codebase should run semantic_search /
read_file themselves and synthesize — that is the supported pattern.
backup¶
Snapshot all LanceDB tables, SQLite databases, chat history files, and settings to a timestamped backup directory.
Parameters: None.
Returns: Backup created: backup_20260326_143012
Notes:
- Keeps the last 3 backups (older ones are rotated out) — calling backup purely
to discover existing IDs therefore destroys the oldest snapshot. Use
restore with an empty backup_id to list them instead.
- Backups are stored in index/lancedb-backup-*
restore¶
Restore from a previous backup. Fails if a crawl is currently active.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
backup_id |
string |
(required) | Backup directory name (e.g. backup_20260326_143012). Pass "" to list the available snapshots instead of restoring — listing is read-only and works even while a crawl is active. |
Returns: Confirmation; the available backup IDs when backup_id is empty; or
an error naming the available IDs if the requested one does not exist.
Crawler Tools¶
These tools are only registered when CRAWLER_ENABLED=true. They query the crawler's LanceDB tables and run history database.
Required environment configuration (per env):
| Variable | Required for | Default |
|---|---|---|
CRAWL_LOCAL_BASE_URL |
env=local |
http://localhost (container's localhost — usually not what you want; set to http://host.docker.internal:<port> to reach the host) |
CRAWL_DEV_BASE_URL |
env=dev |
unset → crawler exits with a clear error |
CRAWL_PROD_BASE_URL |
env=prod |
unset → crawler exits with a clear error |
CRAWL_DEV_API_BASE / CRAWL_PROD_API_BASE |
optional | empty (only set if you have a separate API host) |
Set these in deploy/.env. The crawler validates that base_url has an http:// or https:// scheme at startup and refuses to run with a relative or empty value (avoids the silent "0 URLs crawled" failure mode).
Site route mappers (extension point)¶
Two crawler capabilities need to know which URL renders a given source file:
source-code URL discovery (seeding from route references in source) and
crawl changed (re-fetching only the pages whose code moved since the last run).
That mapping is framework-specific — there is no general filesystem→URL
correspondence — so no mapper ships by default. Without one the crawler seeds
from base_url and discovers by links and sitemap, which is the normal mode of
operation and needs no configuration.
What degrades, and how:
| Capability | With a mapper | Without |
|---|---|---|
crawl |
Seeds from switch/route discovery + base_url, URLs tagged by circuit |
Seeds from base_url, link/sitemap discovery, all URLs tagged public |
crawl changed |
Re-crawls only URLs whose source changed | Exits 2 — a diff-driven crawl is the mapping |
| Post-reindex auto-crawl | Fires crawl changed |
Skipped (no child process spawned) |
sync-api-routes, scan-safety |
Run | Report unavailable, exit 2 |
POST /ui/api/safety/scan |
Runs the scan | 501 with an explanatory message |
| Destructive-action block floor | In force | In force — it does not depend on a mapper |
The block floor is deliberately independent: it lives in crawler/safety_scanner.py
and is imported by crawler/url_guard.py, which every fetch passes through. A
missing mapper never weakens the crawl guard.
Installing one. Provide a Python package named site_routes on the
container's import path (a read-only directory bind-mount to /app/site_routes
is the simplest route) exposing three submodules:
| Submodule | Must expose | Contract |
|---|---|---|
discoverer |
APP_CONFIG, discover_from_switches(root, base_url, api_base, api_routes_path, timeout=…), discover_from_source_refs(root, base_url), sync_api_routes(root, out_path), load_api_routes(path, api_base) |
Returns lists of absolute URLs; APP_CONFIG maps app name → (dir, entry_script, circuit) |
diff_mapper |
map_files(paths, …) |
Changed repo-relative paths → URLs to re-crawl. Returning [] is valid and means "nothing to do" |
classifier |
classify_url(url) -> str |
One of public, member, affiliate, admin — drives which session the fetcher uses |
safety_scan |
scan_safety(public_html, app_config=…, config_path=…), write_report(report, path=None) |
Walks the app's action files and grades each one, then applies the layer-3 config. May import crawler.safety_scanner's load_safety_config / is_action_blocked — that layer-3 API is the stable surface a mapper is allowed to depend on |
crawler/routes.py performs the optional import and exposes AVAILABLE. All of
the above are pure functions with no DB or HTTP access; a mapper that raises on an
unfamiliar tree is a bug — return an empty list instead.
Extending CodeNavi generally: this is an in-process seam and deliberately narrow. For custom behaviour on indexing/crawl/chat events — notifications, downstream builds, syncing to another system — use the event hooks instead (
HOOK_URL/HOOK_SCRIPT_DIR, see Webhooks & hooks). Hooks run out of process, carry no dependency on CodeNavi internals, and cannot break on an upgrade. Reach for a route mapper only when you specifically need file→URL mapping.
crawl¶
Trigger a crawl run as a subprocess. Returns immediately with a run ID.
Full crawl runs automatically:
1. Source code URL discovery (requires an installed site route mapper; skipped otherwise) — scans source files for route references and adds them as seeds, catching URLs that appear only in email templates, redirects and link builders. Without a mapper the crawl seeds from base_url and discovers by links and sitemap.
2. Destructive-action filtering — refuses any fuseaction matching the built-in block floor (*.delete, *.remove, *.destroy, *.purge, *.wipe, *.suspend, *.ban, *.disable, *.deactivate, *.revoke, *.terminate, *.void), while keeping past-tense and view pages crawlable (*.deleted, *.archived, *.view_*, *.list_*). The floor lives in code (crawler/safety_scanner.py), so it is always in force — no config file required. data/crawl_safety.json is optional and only extends or relaxes it; see Crawler safety config.
3. Safety report loading — if data/crawl_safety_report.json exists (written by scan-safety, which combines action-name heuristics with PHP GET-safe static analysis), it blocks additional critical/high-risk URLs. Those two analyses run only during scan-safety, not during a crawl. scan-safety enumerates action files through the site route mapper, so it requires one installed; the block floor in item 2 does not and is always in force.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
env |
string |
"local" |
Target environment: local, dev, or prod. Any other value is refused — nothing is spawned. |
mode |
string |
"full" |
full (all pages), changed (SVN diff only), or url (single page). Any other value is refused. |
url |
string |
"" |
Required when mode="url". Omitting it is refused — it must never be allowed to widen a spot-check into a full crawl. |
All three arguments are validated before the subprocess is spawned, so a rejected call never starts a crawl and never reports a false "started".
Returns: Crawl started for env=dev (mode=full), run_id=abc12345. Progress:
get_run_summary / list_runs. Stop it: cancel_crawl.
For mode="url" the response reports the pid instead: a spot-check is a single
request that registers no run, so there is no run_id and nothing to cancel.
cancel_crawl¶
Send SIGTERM to a running crawl subprocess.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
run_id |
string |
(required) | Run ID from crawl or list_runs |
Returns: Confirmation or "process not found" if already finished.
search_errors¶
Search crawl errors by semantic query. Returns errors similar in meaning to the query.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
query |
string |
(required) | Description of the error to find |
env |
string |
"" |
Filter by environment (local, dev, prod) |
run_id |
string |
"" |
Filter by specific run ID |
limit |
integer |
5 |
Maximum results |
Returns: JSON array of error records with url, error_type, error_message, context_for_llm, http_status, run_id, crawled_at.
search_urls¶
Find crawled pages by URL pattern or description.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
query |
string |
(required) | Page description or URL fragment |
env |
string |
"" |
Filter by environment |
run_id |
string |
"" |
Filter by run ID |
limit |
integer |
10 |
Maximum results |
Returns: JSON array with url, http_status, response_time_ms, content_hash, crawled_at, auth_level.
compare_runs¶
Diff two crawl runs: which errors are new, which were resolved.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
run_id_a |
string |
(required) | Baseline run (older) |
run_id_b |
string |
(required) | Comparison run (newer) |
Returns: JSON with run summaries, new_errors (appeared in B but not A), resolved_errors (in A but not B), and counts.
get_run_summary¶
Get statistics for a specific crawl run, or the latest run if no ID is provided.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
run_id |
string |
"" |
Run ID (omit for latest) |
Returns: JSON with run_id, env, status, total_urls, total_errors, error_breakdown, started_at, completed_at.
list_runs¶
List crawl history.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
env |
string |
"" |
Filter by environment |
limit |
integer |
10 |
Maximum runs to return |
Returns: JSON array of run summaries.
get_referrers¶
Find pages that link to a given URL. Answers "where is X linked from?"
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
url |
string |
(required) | The target URL to find referrers for |
env |
string |
"" |
Filter by environment |
run_id |
string |
"" |
Filter by run ID |
limit |
integer |
20 |
Maximum results |
Returns: JSON array with source_url, http_status, env, run_id. Empty result means the URL is a seed URL or predates referrer tracking (introduced 2026-03-21).
Crawler safety config¶
The crawler ships with two guards, both on by default and needing no configuration.
Destructive-action guard. The block floor lives in crawler/safety_scanner.py::_DEFAULT_SAFETY_CONFIG, in code, so a missing, empty, or corrupt config cannot disarm it. data/crawl_safety.json is optional; copy data/crawl_safety.json.example to create it.
| Key | Effect |
|---|---|
block |
Exact fuseactions to always block. Beats everything, including allow. |
block_patterns |
fnmatch globs to block. Unioned with the built-in floor — entries here add to it. Deleting an entry, or setting this to [], does not lower the floor. |
allow |
Exact fuseactions exempt from block_patterns. Replaces the default (empty) list when present. |
allow_patterns |
fnmatch globs exempt from block_patterns. Omit this key to keep the built-in exemptions (*.deleted, *.archived, *.view_*, *.list_*, …); defining it replaces them wholesale. |
Precedence: block > allow > block_patterns > allow_patterns. To permit an action the floor blocks, add it to allow — do not try to remove it from block_patterns.
SSRF guard. Each hostname is resolved and rejected if it lands on a private, loopback, or cloud-metadata address. In env=local loopback and RFC1918 are permitted — that is what local crawling means — but cloud-metadata ranges (169.254.0.0/16, fe80::/10, fd00:ec2::/32) are refused in every env. The /ui/api/crawler/crawl HTTP route does not inherit the local relaxation: it accepts a caller-supplied URL and so applies the strict blocklist unconditionally. Set CODENAVI_CRAWLER_SSRF_GUARD=0 to disable the guard entirely — do not do this in production, it re-opens fetch-any-internal-address.
HTTP Endpoints¶
Every endpoint below, and the MCP transport itself, can answer 429 Too Many Requests with a Retry-After header when the per-client throttle refuses the request (a client over its request budget, or an address locked out for rejected tokens). /health and /.well-known/* are exempt. Limits and the reasoning: Configuration → Throttling.
GET /health¶
Health check. Always unauthenticated.
Response:
{
"status": "ok",
"version": "1.3.51",
"build": "r1044",
"mcp_endpoint": "http://localhost:8765/mcp",
"gpu": {
"available": true,
"name": "NVIDIA GeForce RTX 4090",
"device_count": 1,
"memory_used_mb": 8214.3,
"memory_total_mb": 24564.0,
"memory_torch_allocated_mb": 7488.1,
"memory_torch_reserved_mb": 7812.0
},
"compute_device": "cuda",
"compute_backend": "cuda",
"gpu_present_but_unused": false,
"embedding_models": {
"code": {"model": "/models/Buric-E4B", "status": "loaded"},
"content": {"model": "/models/Buric-E4B", "status": "idle"}
},
"llm": {"provider": "ollama", "model": "llama3.1", "status": "ok"},
"tables": {
"code_chunks": {"chunks": 12379},
"doc_chunks": {"chunks": 45},
"web_chunks": {"chunks": 8821}
},
"crawler": {"enabled": true},
"tier": "Unlimited",
"uptime_sec": 3600,
"index_integrity": {
"status": "ok",
"last_check": 1789575398.1,
"codebases": {"default": {"status": "ok", "checked_at": 1789575398.1, "mismatches": [], "written_at": 1789575100.4}}
}
}
index_integrity20 is the last verdict of the stored-index check: every reindex records a fingerprint of the active code table, the doc table and the call-graph sidecar (index/integrity_<codebase>.json); the server compares it with what is on disk after start-up and every INDEX_INTEGRITY_CHECK_INTERVAL seconds. ok — everything matches; mismatch — the mismatches list names what differs (rows, Lance version, missing or extra fragment files, sidecar bytes, PRAGMA quick_check) and the server log carries the same line as a WARNING — search keeps serving the index as stored, and a full reindex rebuilds and re-records it; unverified — no manifest yet (the next reindex writes one) or the check has not run. The overall status is mismatch if any codebase is, ok only when every codebase is. Cached — /health never hashes anything itself. Same verdict as the codenavi_index_integrity gauge.
The version field is read from the VERSION file at the project root. gpu.available/name/device_count come from nvidia-smi/MPS detection; the two memory_* pairs are deliberately different measurements — used/total is the whole card (every CUDA client), torch_allocated/torch_reserved is this server process only (the only per-process VRAM attribution that works under WSL2). All four read 0 when there's no CUDA device. compute_device is the embedder's actual device (cuda/mps/cpu) and is the field to key logic off; compute_backend (cuda/rocm/mps/cpu) is its display companion and is the one to show a human — torch-on-ROCm reports the CUDA device type, so an AMD card reads cuda in compute_device and rocm here. gpu_present_but_unused fires when a GPU is reachable but the embedder is running on CPU anyway. See observability.md for the same fields as Prometheus metrics.
When multiple codebases are configured (CODEBASE_CONFIG), the response includes a codebases field with per-codebase stats:
{
"codebases": {
"myapp": {"path": "/var/www/myapp", "tables": {"code_chunks": {"chunks": 8234}, ...}},
"api": {"path": "/var/www/api", "tables": {"code_chunks": {"chunks": 3102}, ...}}
}
}
The top-level tables field shows aggregated totals across all codebases.
Since 1.3.38 (CN-706) each codebases.<name> entry also carries:
| key | meaning |
|---|---|
structural_ready |
the call-graph sidecar exists — find_callers, impact_analysis, diff_impact and symbol_search can answer |
vectors_ready |
an active vector table exists — semantic_search answers with results |
structural_coverage |
{"covered": [languages with a call-graph extractor], "uncovered": {language: files}} — Perl, docs, SQL and templates have no extractor, so a Perl tree can be structural-ready with no Perl symbols |
encode_progress |
{"done", "total"} chunks, present only while this codebase is in the encoding phase |
Status codes: 200 healthy, 503 degraded (model loading or index empty).
GET /metrics¶
Prometheus exposition of the live server metrics registry.4 Serves the process-wide prometheus_client registry (metrics.py) via generate_latest() — metrics incremented in the hot path (search.py, indexing.py). Bearer-gated when CODENAVI_AUTH_TOKEN is set — scrape with the token in the Prometheus job's authorization. Counts/histograms/gauges only (no query text or code), so it is safe to expose to an on-prem Prometheus.
Two scrape endpoints, same registry. CodeNavi also exposes the same metrics on a dedicated
prometheus_clientport 9091 (CODENAVI_METRICS_PORT, bound toCODENAVI_HOST— default127.0.0.1, unauthenticated) — the standard path for a co-located Prometheus. Use this bearer-gated/metricson the API port for authenticated / remote scraping instead. Full setup + Grafana dashboard:observability.md.
Because the registry is per-process and the benchmark harness (tools/benchmark.py) runs in a separate process, these metrics reflect real server traffic only — benchmark runs do not pollute them.
Metrics:
| Metric | Type | Labels | Description |
|---|---|---|---|
codenavi_queries_total |
counter | tool, status, codebase |
Queries by tool (semantic/file/symbol/refs/overview/chat), status (ok/error), and codebase (queried codebase, or all for cross-codebase/unscoped) |
codenavi_query_duration_seconds |
histogram | tool, phase, codebase |
Query duration (seconds) by phase (search/rerank/total) and codebase |
codenavi_zero_results_total |
counter | tool, codebase |
Queries returning zero results, by tool and codebase |
codenavi_throttled_requests_total |
counter | reason |
Requests refused with 429 by the per-client throttle: auth (address locked out for rejected tokens) or load (request budget exceeded) |
codenavi_safety_gate_filtered_chunks_total |
counter | — | Chunks removed by the retrieval safety gate |
codenavi_alias_expanded_total |
counter | — | Queries expanded by an alias |
codenavi_feedback_votes |
gauge | rating |
Current feedback vote totals (up/down), refreshed on scrape |
codenavi_gpu_lock_wait_seconds, codenavi_search_lock_wait_seconds |
histogram | — | Lock-wait time (GPU file lock / search lock) |
codenavi_gpu_memory_bytes |
gauge | kind |
VRAM on device 0, read at scrape time. used/total are the device-wide CUDA view; torch_allocated/torch_reserved are this process only — use those to answer "how much is CodeNavi holding", since per-process attribution is otherwise unavailable (nvidia-smi --query-compute-apps returns [N/A] under WSL2, and its device total there also counts Windows-host clients). All four read 0 with no CUDA device, so total == 0 means "no GPU" |
codenavi_index_chunks, codenavi_index_files |
gauge | codebase |
Current index size per codebase (one series per indexed codebase; sum() for the total) |
codenavi_last_reindex_timestamp |
gauge | — | Unix time of last completed reindex |
codenavi_index_integrity |
gauge | codebase |
Stored-index integrity verdict: 1 ok, 0 mismatch (rebuild with a full reindex), -1 unverified |
codenavi_reindex_duration_seconds |
histogram | — | Reindex durations |
Prometheus scrape config:
scrape_configs:
- job_name: codenavi
metrics_path: /metrics
static_configs:
- targets: ["localhost:8765"]
authorization:
credentials: "<CODENAVI_AUTH_TOKEN>" # omit if auth is disabled
Status codes: 200 success, 401 bad/missing bearer token, 500 internal error (returned as {"error": "..."}).
POST /chat¶
Run a chat turn in-process and get the answer as JSON. Use this when you want chat from a script without going through the MCP protocol — e.g., the CLI wrapper, a shell one-liner, or an internal tool. Because the work runs inside the server process, the already-loaded embedding models are reused; the caller never loads models or touches the GPU.
Authenticated when CODENAVI_AUTH_TOKEN is set (same bearer-token scheme as MCP).
Request body:
| Field | Type | Default | Description |
|---|---|---|---|
message |
string |
(required) | The user's question |
codebase |
string |
"" |
Scope to a single codebase, or "" for all |
session_id |
string |
"cli" |
Per-session chat history file |
source_type_filter |
string |
"" |
"code", "doc", "web", or "" |
history_turns |
integer |
10 |
Prior turns to include as context |
verbose |
boolean |
false |
Include retrieval metadata in the response |
trace |
boolean |
false |
Include per-stage timings. Implies verbose. Developer use. |
Response:
{
"response": "...answer text with citations...",
"meta": {
"chunk_count": 8,
"by_source": {"code": 6, "doc": 2},
"by_codebase": {"laravel": 8},
"chunks": [
{"source": "code", "codebase": "laravel", "ref": "src/Illuminate/Auth/SessionGuard.php:142"},
...
],
"timings": {"embed_ms": 42, "search_ms": 87, "rerank_ms": 310, "generate_ms": 71200, "total_ms": 71639},
"generation": {"provider": "ollama", "model": "llama3.1", "prompt_tokens": 3421, "completion_tokens": 364, "tok_per_sec": 5.1}
}
}
meta is present only when verbose or trace is true. timings and generation are present only when trace is true.
Example:
curl -s -X POST http://localhost:8765/chat \
-H 'Content-Type: application/json' \
-d '{"message":"how does auth expire?","codebase":"laravel","verbose":true}' \
| jq .
Status codes: 200 success, 400 missing message, 401 bad bearer token, 500 internal error (returned as {"error": "..."}).
GET /sessions¶
List the chat session ids the server knows about. Lets a UI populate a session picker without filesystem access to the server's data dir.
Returns: {"sessions": ["default", "paypal-debug", ...]} — derived from the chat_history_*.json files in core.chat._HISTORY_DIR.
curl -s http://localhost:8765/sessions
GET /history?session_id=<id>¶
Return the saved messages for a chat session as a list of {role, content} dicts. The /chat endpoint writes this file on every turn — GET /history is its read-back counterpart.
Query params:
| Name | Required | Description |
|---|---|---|
session_id |
yes | Same id used with POST /chat (filename-sanitized server-side) |
Returns: [{role: "user"|"assistant", content: str}, ...] — empty list if the session has no history.
curl -s 'http://localhost:8765/history?session_id=default'
DELETE /history?session_id=<id>¶
Remove the on-disk chat_history_<id>.json for a session. Idempotent.
Returns: {"deleted": true} if a file was removed; {"deleted": false} if it didn't exist.
curl -s -X DELETE 'http://localhost:8765/history?session_id=paypal-debug'
Status codes: 200 always (idempotent), 400 missing session_id, 401 bad bearer token.
POST /reindex¶
Trigger an in-process background reindex (the same endpoint the codenavi reindex CLI and reindex.sh use). Bypasses the 5-minute MCP cooldown — treated as admin. Requires the bearer token when CODENAVI_AUTH_TOKEN is set.
Body (JSON, all optional):
| Field | Default | Description |
|---|---|---|
full |
false |
true re-encodes everything (~20 min); default is incremental (changed files only, typically seconds). |
codebase |
"" |
Restrict to one codebase. Empty = all configured (ignores default_codebase5). |
wait_for_completion |
false |
Block until done (incremental only; ignored when full=true3). |
max_wait_seconds |
120 |
Timeout for the wait, clamped 1–300. |
Returns: {"started": bool, "message": str} (+ completed/result when waiting). A reindex (or Docs Wiki compile) already in progress returns HTTP 409 with started: false — the request is dropped, not queued; the next call catches up. Safe to fire redundantly, and the 409 status means even a caller that discards the body can log the drop.
curl -s -X POST http://localhost:8765/reindex \
-H "Authorization: Bearer $CODENAVI_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"codebase": "myrepo"}'
⚠️ Fire-and-forget hooks fail silently — test the exact command once before trusting it. The idiomatic git-hook integration ends in
|| true(a reindex must never fail a push), which makes every error invisible: a missingContent-Type: application/jsonheader is a 415 on every push (the header is required as a CSRF guard and cannot be waived), and a wrong token-file path reads as an empty token — a 401 on every push. Both look identical to success from the hook's side, so the index silently never refreshes. Run your hook'scurlonce by hand without>/dev/nulland confirm you get{"started": true, ...}(or a409) before wiring it in.
Triggering from CI / your forge: unknown JSON keys are ignored, so a Git forge's push webhook (Forgejo, Gitea, GitLab, GitHub) can POST its native payload straight at this endpoint — no runner, no glue script. It falls through to the defaults: an incremental reindex of all codebases, which no-ops in seconds when nothing changed. Configure a webhook on push events with the target URL http://<codenavi-host>:8765/reindex and, if auth is enabled, an Authorization: Bearer <token> header (Forgejo/Gitea have a dedicated Authorization-header field in the webhook form). Poll GET /reindex-status if you need completion, or subscribe to the outbound reindex.completed event (Webhooks & hooks).
A webhook reindexes what is on disk — it does not update the clone. CODENAVI_GIT_PULL_BEFORE_REINDEX=true (env or data/settings.json, read live, default off) adds an opt-in git pull --ff-only in each git-checkout codebase root before scanning; a failed pull logs a warning and indexing proceeds on the tree as-is.18 It requires a read-write codebase mount and in-container git credentials — trade-offs and the recommended host-side alternative are covered in Running CodeNavi as a team server.
GET /reindex-status¶
Poll the progress of the most recent background reindex triggered via POST /reindex (or any other path — MCP tool, CLI, reindex.sh). Same bearer-token rule as /reindex.
curl -s http://localhost:8765/reindex-status -H "Authorization: Bearer $CODENAVI_AUTH_TOKEN"
Returns: Running status, progress, duration, and any errors — the same payload the reindex_status MCP tool and ./codenavi reindex-status CLI command return.
Since 1.3.38 (CN-706) the payload also carries phase (structural | encoding | finalizing | "") and encode_progress ({"done","total"}): the sidecar is built before the encode, so a reindex reports structural for seconds, then encoding with progress.
POST /cancel-reindex¶
Cancel a running background reindex. The reindex stops after the current encoding batch completes; the existing index is left unchanged (no partial data). Same bearer-token rule as /reindex. A cancel that arrives during the scan or the structural (call-graph) phase takes effect at the first encoding checkpoint, so on a large first index the stop can take a minute or two — the call-graph sidecar is built before the encode since 1.3.38 (CN-706).
curl -s -X POST http://localhost:8765/cancel-reindex -H "Authorization: Bearer $CODENAVI_AUTH_TOKEN"
GET /.well-known/oauth-protected-resource¶
RFC 9728 OAuth Protected Resource metadata for MCP auth discovery.
GET /.well-known/agent-card.json¶
The agent card — a JSON self-description AI agents use to onboard themselves:
identity, capabilities (every tool, grouped by skill), connection details, an
onboarding sequence with a paste-ready .mcp.json example, and the usage rules
(sequential search calls, read_file exemption, limits, filters). The version
field is injected from the running server at serve time.
Two disclosure tiers, same split as /health:
- Unauthenticated — the generic card only: product information identical for every install, nothing deployment-specific.
- With the bearer token — adds a
deploymentblock: codebase names, scoped/mcp/{codebase}endpoints, the default codebase, per-codebaseindex_last_writetimestamps (UTC ISO-8601; a stale one means reindex before trusting results), and whether the crawler is enabled.
curl -s -H "Authorization: Bearer $CODENAVI_AUTH_TOKEN" \
http://localhost:8765/.well-known/agent-card.json | python3 -m json.tool
GET /metrics (port 9091)¶
Prometheus metrics — the same full registry as the main GET /metrics section above (index gauges, query counts and durations, GPU, locks, feedback), served on the dedicated metrics port.
Error Handling¶
Auth failure (401):
{"error": "invalid_token", "error_description": "Authentication required"}
MCP protocol errors follow JSON-RPC 2.0:
{"jsonrpc": "2.0", "id": 1, "error": {"code": -32600, "message": "..."}}
Rate Limits¶
- Reindex: Minimum 5 minutes between calls
- Feedback: Maximum 5 ratings per chunk per 5-minute window
- Search/chat: No rate limit; the GPU phases serialize internally — keep one or two in flight for best latency
CLI Commands¶
The CLI (tools/cli.py) provides direct access without MCP. Run inside Docker or via the ./codenavi wrapper script (./codenavi-cli is a back-compat symlink).
init¶
Scan a codebase directory, detect languages, and generate a starter settings.json.
./codenavi init /path/to/your/project
./codenavi init /path/to/project --output /custom/data/dir
./codenavi init /path/to/project --force # overwrite existing settings.json
What it does:
1. Walks the directory tree, counts files by extension
2. Detects programming languages from file extensions
3. Auto-excludes known junk directories (node_modules, vendor, .git, etc.)
4. Auto-detects large asset directories (>200 files) to exclude
5. Generates data/settings.json with sensible defaults
Other CLI commands:
The CLI covers most MCP tools — search, read, indexing, docs, feedback, and crawler observation. The structural/relations tools (find_callers, impact_analysis, diff_impact, graph_neighborhood) the Wiki tools (wiki_pages, wiki_page) and the investigation-note tools (save_note, search_notes, list_notes, delete_note) are MCP/web-UI only today. Commands are grouped by category below.
Search & code intelligence:
| Command | Maps to MCP tool |
|---|---|
./codenavi search "query" |
semantic_search |
./codenavi file "query" |
file_search |
./codenavi symbol NAME [--kind ...] [--language ...] [--source] |
symbol_search |
./codenavi refs NAME [--language ...] [--path PREFIX] |
find_references |
./codenavi grep PATTERN [--path ...] [--exclude ...] [--language ...] [--context N] |
grep_search |
./codenavi chat ["message"] [--codebase NAME] |
POST /chat (no MCP tool since CN-558; REPL if no message) |
Files & structure:
| Command | Maps to MCP tool |
|---|---|
./codenavi read PATH [--start N] [--end N] |
read_file |
./codenavi ls [PATH] [--depth N] |
list_directory |
./codenavi overview [--section all\|languages\|structure\|files\|symbols] |
codebase_overview |
./codenavi codebases |
list_codebases |
./codenavi index-stats [--codebase NAME] |
index_stats |
./codenavi claude-md [--codebase NAME] |
generate_claude_md |
Indexing, docs, admin:
| Command | Maps to MCP tool |
|---|---|
./codenavi reindex [--full] [--codebase NAME] |
reindex |
./codenavi reindex-status |
reindex_status |
./codenavi reindex-cancel |
cancel_reindex (POSTs to new /cancel-reindex REST endpoint) |
./codenavi reindex-compare [--backup NAME] [--codebase NAME] [--list] |
(CLI-only — diffs current index vs a saved lancedb-backup-*) |
./codenavi docs-list [--codebase NAME] |
doc_sources |
./codenavi docs-reindex [--codebase NAME] |
reindex_docs |
./codenavi backup |
backup |
./codenavi restore BACKUP_ID |
restore |
Feedback & analytics:
| Command | Maps to MCP tool |
|---|---|
./codenavi feedback-submit QUERY CHUNK_ID FILE --rating {1,-1} [--comment ...] |
submit_feedback |
./codenavi feedback-stats |
feedback_stats |
./codenavi analytics [--since YYYY-MM-DD] [--top N] |
search_analytics |
Crawler — orchestration (passthrough to crawler.py):
| Command | Description |
|---|---|
./codenavi crawl <subcommand> |
Subcommands: run [local\|dev\|prod] [--max-depth N] [--no-embed], re-embed <run_id>, changed [local\|dev], status, sync-api-routes, scan-safety [--review]. Requires CRAWL_<ENV>_BASE_URL set in deploy/.env (see Crawler Tools for details). |
Crawler — observation (queries against crawl_runs.db):
| Command | Maps to MCP tool |
|---|---|
./codenavi runs [--env ENV] [--limit N] |
list_runs |
./codenavi run-summary [--run-id ID] |
get_run_summary |
./codenavi runs-compare RUN_A RUN_B |
compare_runs |
./codenavi errors QUERY [--env ...] [--run-id ...] |
search_errors |
./codenavi urls QUERY [--env ...] [--run-id ...] |
search_urls |
./codenavi referrers URL |
get_referrers |
./codenavi crawl-cancel RUN_ID |
cancel_crawl (SIGTERM via PID registry) |
Server status:
| Command | Description |
|---|---|
./codenavi status |
Server health check |
./codenavi version |
Server version (live via /health, falls back to the VERSION file) |
chat¶
chat posts to the server's /chat endpoint so the already-loaded embedding models are reused — the CLI itself never loads models or touches the GPU.
./codenavi chat "how does auth expire?"
./codenavi chat --codebase laravel "how does auth expire?"
./codenavi -v chat --codebase laravel "how does auth expire?"
Flags:
| Flag | Description |
|---|---|
--codebase NAME |
Scope chat to a single codebase (default: all configured codebases). |
-v, --verbose |
(top-level flag) Print total time, per-source/per-codebase chunk counts, and each retrieved chunk's ref to stderr. The answer on stdout stays pipeable. |
--trace |
Developer-only: per-stage timings (embed, search, rerank, generate) and LLM token stats. Implies --verbose. Not shown in --help. |
Environment:
| Variable | Default | Description |
|---|---|---|
CODENAVI_PORT |
8765 |
Port where the CodeNavi server listens. |
CODENAVI_AUTH_TOKEN |
(empty) | Bearer token sent in the Authorization header when set. |
reindex¶
reindex posts to the server's /reindex endpoint. The work runs inside the server process (not the CLI), so the already-loaded embedding models are reused and there's no second GPU consumer. Fire-and-forget: prints the server's start message and returns. For polling + post-reindex snapshots, use reindex.sh.
./codenavi reindex # incremental, all codebases
./codenavi reindex --full # full rebuild, all codebases
./codenavi reindex --codebase laravel # incremental, laravel only
./codenavi reindex --full --codebase laravel # full rebuild, laravel only
Flags:
| Flag | Description |
|---|---|
--full |
Re-encode every file from scratch (~20 min). Default is incremental (only changed files). |
--codebase NAME |
Reindex only the named codebase. Default: all configured. |
Same CODENAVI_PORT / CODENAVI_AUTH_TOKEN env vars as chat. The server enforces a 5-minute cooldown for the MCP reindex tool but not for this endpoint (CLI is treated as admin).
Unlike the MCP
reindex_incrementaltool and the/codenavi:reindexslash command, this CLI/HTTP path ignoresdefault_codebase— omitting--codebasealways reindexes all codebases5, soreindex.sh's documented full-sweep is unchanged.
Verbose output (example):
[v] POST http://localhost:8765/chat codebase=laravel len=20
<answer on stdout>
[v] total 71.34s chunks=8 by_source={'code': 6, 'doc': 2} by_codebase={'laravel': 8}
[v] [code] laravel: src/Illuminate/Auth/SessionGuard.php:142
[v] [code] laravel: src/Illuminate/Auth/Passwords/PasswordBroker.php:23
...
Trace output (adds, with --trace):
[t] embed=42ms search=87ms rerank=310ms generate=71200ms server_total=71639ms
[t] llm=ollama/llama3.1 prompt=3421tok completion=364tok 5.1tok/s
Use trace to separate retrieval cost from LLM cost when a chat turn is slow.
-
Vendor change reference CN-217. ↩
-
Vendor change reference CN-218. ↩
-
Vendor change reference CN-232. ↩
-
Vendor change reference CN-236. ↩
-
Vendor change reference CN-261. ↩
-
Vendor change reference CN-402. ↩
-
Vendor change reference CN-425. ↩
-
Vendor change reference CN-561. ↩
-
Vendor change reference CN-679. ↩
-
Vendor change reference CN-686. ↩
-
Vendor change reference CN-564. ↩
-
Vendor change reference CN-638. ↩
-
Vendor change reference CN-748. ↩