Skip to content

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

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 widen max_chars if 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>; raise max_chars to 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 Emits skipped when 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 when RERANKER_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, or web
  • <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 via read_file (or run reindex) 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.


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 by structure alone. Pass section="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, set since (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 delete deploy/data/feedback.db.


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.


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\n in the regex engine. grep_search operates on normalized chunks, so mixed-line-ending codebases match identically to clean-LF ones.
  • No shell overhead. Runs against in-memory chunk text — no find traversal, 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 the codenavi chat CLI uses
  • Chat output is still scanned (chat_output scope), and every [code] file:line citation 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_client port 9091 (CODENAVI_METRICS_PORT, bound to CODENAVI_HOST — default 127.0.0.1, unauthenticated) — the standard path for a co-located Prometheus. Use this bearer-gated /metrics on 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 missing Content-Type: application/json header 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's curl once by hand without >/dev/null and confirm you get {"started": true, ...} (or a 409) 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 deployment block: codebase names, scoped /mcp/{codebase} endpoints, the default codebase, per-codebase index_last_write timestamps (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_incremental tool and the /codenavi:reindex slash command, this CLI/HTTP path ignores default_codebase — omitting --codebase always reindexes all codebases5, so reindex.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.


  1. Vendor change reference CN-217. ↩

  2. Vendor change reference CN-218. ↩

  3. Vendor change reference CN-232. ↩

  4. Vendor change reference CN-236. ↩

  5. Vendor change reference CN-241. ↩↩↩↩↩↩

  6. Vendor change reference CN-261. ↩

  7. Vendor change reference CN-337. ↩↩↩

  8. Vendor change reference CN-396. ↩↩

  9. Vendor change reference CN-397. ↩↩

  10. Vendor change reference CN-401. ↩↩↩

  11. Vendor change reference CN-402. ↩

  12. Vendor change reference CN-414. ↩↩↩

  13. Vendor change reference CN-415. ↩↩

  14. Vendor change reference CN-425. ↩

  15. Vendor change reference CN-561. ↩

  16. Vendor change reference CN-679. ↩

  17. Vendor change reference CN-686. ↩

  18. Vendor change reference CN-564. ↩

  19. Vendor change reference CN-638. ↩

  20. Vendor change reference CN-748. ↩