Navigator
navree mcp: the MCP server for your coding agent, with your app's routes, module graph, providers, database schema, read-only SQL, logs, configuration and docs matched to your Navree version.
On this page
Introduction
A coding agent that reads your source files still has to guess at what the running app looks like: which routes exist after every module is wired, what the deals table really has in it, what failed in the last request. Navigator answers those questions from the app itself. It is an MCP server built into the Navree CLI, started with one command:
navree mcpConnect your agent to it once, and it can ask your app:
| Tool | Answers |
|---|---|
app_info | Navree and Rust versions, the facade features, the modules, the database driver |
list_routes | Every route with its method, path, access rule, handler and module |
module_graph | Modules with their imports, exports, providers and commands |
list_providers | Providers with their scope, module, visibility and dependencies |
list_commands | The app's commands, as navree list shows them |
describe_schema | Tables, columns, indexes, foreign keys and migration status |
query_database | The rows of a read-only SQL query |
read_logs | Recent log lines, filtered by level, target or request id |
last_error | The most recent error, with its request and the log lines around it |
read_config | The merged configuration, secrets redacted |
search_docs, read_doc | These docs, for the Navree version your app uses |
When the app includes Lighthouse (a file under src/ adds LighthouseModule, as navree new apps do), Navigator adds its five read tools (lighthouse_list_issues, lighthouse_get_issue, lighthouse_slow_routes, lighthouse_slow_queries and lighthouse_failed_jobs), reading Lighthouse's tables in your local database. See Lighthouse's tools.
Everything is read-only, and Navigator runs only on your machine, over stdio, for the project you start it in. It is for development; your production binary doesn't contain it. The code lives in the navree-navigator crate, which the CLI runs; the facade re-exports it as navree::navigator behind the opt-in navigator feature, for tools and tests.
Connecting your agent
Navigator is a stdio server: your agent starts navree mcp as a child process in your app's directory. Install the CLI first, then register Navigator with your agent.
From your app's directory:
claude mcp add navree -- navree mcpTo share the setup with your team, add it at project scope instead. Claude Code writes .mcp.json, which you commit:
claude mcp add --scope project navree -- navree mcp{
"mcpServers": {
"navree": {
"command": "navree",
"args": ["mcp"]
}
}
}[mcp_servers.navree]
command = "navree"
args = ["mcp"]If Codex starts the server outside your app's directory, add the path: args = ["mcp", "--path", "/home/ada/code/crm"].
{
"servers": {
"navree": {
"type": "stdio",
"command": "navree",
"args": ["mcp"]
}
}
}Any other MCP client that launches stdio servers works the same way: the command is navree, the arguments are ["mcp"]. When a client doesn't start servers in your project's directory, name the project:
navree mcp --path /home/ada/code/crmAsk your agent "Which Navigator tools do you have?" to check the connection. In Claude Code, /mcp lists the server and its tools.
How it works
Navigator runs next to your app, not inside it:
- It finds the app: the nearest Cargo package from the directory it starts in (or
--path), and thenavreeversion in itsCargo.lock. - For the module graph, providers and commands, it builds your app with
cargo build --message-format=json(the debug profile, sharing yourtarget/directory) and runs the binary withintrospect --json. That built-in command validates the module graph without constructing anything, asnavree checkdoes, and prints it as JSON. - For routes, it runs the binary with
routes --json, which boots the app without serving traffic, asnavree routes --jsondoes. Navigator addsAPP_DATABASE__MIGRATE_ON_BOOT=falseandAPP_LOG__LEVEL=errorto that process's environment, so asking never migrates your database and adds nothing to your log but errors. Answers are cached untilcargo buildproduces a new binary, that is until a source file changes. - For the database, it reads your configuration exactly as the app does (
.env,config/app.toml,APP_*variables, plus the settings of yourdock.tomlservices, asnavree devpasses them) and connects todatabase.urlitself, read-only. For pending migrations it runs the binary withmigrate:status --json. - For logs, it reads the log file your app writes (
log.file). - For the docs, it searches the documentation of the Navree version in your lockfile. Navigator ships with the docs of its own version; for another version it downloads
https://navree.com/v/<version>/docs.jsononce and caches it under~/.navree/docs/<version>/(NAVREE_HOMEreplaces~/.navree, andNAVREE_DOCS_URLreplaceshttps://navree.com, for a mirror).
A build that fails doesn't stop the other tools: list_routes and friends return the compiler's first error, which is often what the agent needed to see, while describe_schema, read_logs and search_docs keep working.
The tools
app_info
The first call an agent makes in a new project. It returns the package and its path, the Navree version from Cargo.lock, the Rust toolchain (rustc --version in the app's directory), the facade features your Cargo.toml turns on (with default when default features are on), the modules in boot order, the database driver with its URL (password masked) and the configuration files that exist (.env, .env.example, config/app.toml, dock.toml).
list_routes
Every route of the booted app, with the information of navree routes --json: method, path, name, access rule (public, auth, can:read Deal from id), the module, the handler (#[controller] records the controller method) and the route's throttle, timeout, body limit and audit action. Arguments filter by path prefix, method or module.
[
{"method": "GET", "path": "/api/v1/deals", "access": "can:read Deal", "module": "DealsModule", "handler": "DealsController::index"},
{"method": "POST", "path": "/api/v1/deals", "access": "can:create Deal", "module": "DealsModule", "handler": "DealsController::store"},
{"method": "POST", "path": "/api/v1/deals/{id}/move", "access": "can:move Deal from id", "module": "DealsModule", "handler": "DealsController::move_deal"}
]An agent that checks the route table before adding a route doesn't create a second GET /deals/{id}, and sees the access rule the neighbors use.
module_graph and list_providers
- imports or uses
module_graph returns the graph navree check --graph prints, as data: each module in dependency order (the implicit root App, which imports your root modules, comes last), whether it is global, what it imports and exports, its providers (controllers among them) and its commands. With a module argument it returns one module. list_providers lists providers across the app with their type, scope (singleton, request, transient), owning module, whether the module exports them, and their dependencies.
This is what an agent needs to wire a new service correctly: where DealService lives, that DealsModule exports it, and that a new module using it must import DealsModule. When the graph doesn't validate, both tools return the kernel's error, which names the module and the missing type.
list_commands
The app's commands with their one-line help, as navree list shows them: the built-ins (check, help, introspect), then what the agent can run, such as migrate, queue:work or your own crm:task-reminders, each with the module that registers it.
describe_schema
The database as it is, not as the code says it should be: tables, columns with their types, nullability, defaults and primary keys, indexes and foreign keys. It also lists the migrations that have run (the rows of navree_migrations) and those that haven't (from migrate:status --json), so the agent knows when a column it sees in an entity doesn't exist yet. When the app can't answer (it doesn't build), pending says why. A table argument returns one table.
query_database
Runs one read-only SQL statement against your development database and returns at most 100 rows:
{
"sql": "SELECT stage, count(*) AS deals, sum(amount_cents) AS total FROM deals GROUP BY stage ORDER BY stage"
}{
"columns": ["stage", "deals", "total"],
"rows": [["lead", 4, 1840000], ["negotiation", 2, 6050000], ["won", 3, 9100000]],
"truncated": false,
"duration_ms": 3
}truncated is true when the statement had more rows than the 100 returned. The limits below keep it read-only. Values in columns whose names contain password, secret, token, api_key or private_key come back as [redacted].
read_logs and last_error
read_logs returns recent lines of the log file your app writes, newest last, with the file it read. Arguments filter by level (the lowest level: warn returns warnings and errors), target (navree::job also matches navree::job::worker), request_id, since (an RFC 3339 time, or a duration back from now: 30s, 15m, 2h, 1d) and limit (default 100, at most 1000). Navigator reads JSON and pretty lines alike, but JSON keeps every field, and it reads rotated files (navree.2026-10-27.log) newest first.
last_error returns the most recent error line, its request id, the request's method, route and status (from its request completed line), and every other line of the same request: the error chain, the slow queries before it. "Why did that request fail?" is one call.
Both need the app to write a log file, which is off by default. Turn it on for development:
[log]
file = { path = "storage/logs/navree.log", rotation = "daily", keep_days = 7 }Navigator reads log.file.path from your configuration, so a different path works too. Without a log file, both tools say how to turn it on.
read_config
The configuration tree your app sees, merged from .env, config/app.toml and APP_* variables (plus your dock.toml services' settings), with secrets redacted the way Config::redacted() does it: any key containing password, secret, token, key, authorization or cookie, and the password in any URL. A key argument returns one section or value (queue, database.url).
search_docs and read_doc
search_docs searches these docs for the Navree version in your Cargo.lock and returns the best sections with an excerpt each, their address on navree.com and the slug read_doc takes. read_doc returns one page as Markdown: give it a slug (basics/validation), a /docs/... path or a navree.com URL, and optionally a section (its heading or anchor) to read only that part. An agent working on an app pinned to an older Navree reads the docs of that version, not of the latest release, so the APIs it finds are the APIs it has. When those docs can't be had (offline and not cached), Navigator answers from the docs it ships with and says so in a note.
A version's docs are one JSON file, docs.json: {"version": "0.1.0", "pages": [{"slug": "ai/navigator", "title": "Navigator", "description": "...", "planned": false, "sections": [{"heading": "Options", "anchor": "options", "markdown": "..."}]}]}, each page split at its ## headings, the site's components written as plain Markdown.
Lighthouse's tools
When a file under your app's src/ adds LighthouseModule, Navigator lists five more tools. They read Lighthouse's tables (lighthouse_issues, lighthouse_occurrences, lighthouse_aggregates, lighthouse_alerts) in the database Lighthouse writes to: lighthouse.database_url when it is set, otherwise database.url, with the same rules as query_database (local databases only unless --allow-remote-database, read-only, 5 seconds). Your app doesn't have to be running, and nothing needs an access token: the data is your local database's.
| Tool | Arguments | Answers |
|---|---|---|
lighthouse_list_issues | status (open, the default, resolved, resolved_in_next_release, ignored or all), kind, level, search (text in the title or culprit, any case), sort (last_seen, the default, first_seen or count), limit (default 20, at most 100) | Issues, most recently seen first: id, kind, level, status, title, culprit, count, first_seen, last_seen, last_release, environment |
lighthouse_get_issue | id | The issue with its message, source chain, location, error_type, releases and resolution; its five latest occurrences (time, request id, method, route, path, status, user, workspace, release, backtrace and context); the alerts sent for it |
lighthouse_slow_routes | period (1h, 24h, the default, or 7d), limit (default 10, at most 50) | Routes, slowest p95 first: endpoint, requests, errors, error_rate, avg_ms, p50_ms, p95_ms, p99_ms, max_ms |
lighthouse_slow_queries | period, limit | Normalized statements by total time: statement, count, errors, total_ms, avg_ms, p95_ms, max_ms |
lighthouse_failed_jobs | period, limit | Job types with failed attempts, most failures first (job_type, attempts, failed_attempts, failed_jobs, avg_ms, p95_ms), and the open failed_job issues |
The figures are the dashboard's: Navigator merges the per-hour rows of every process (per-minute rows for 1h) and their latency histograms, so a p95 covers every recorded request, not a sample. When the tables don't exist yet, the tools say to run navree migrate. To change an issue (resolve, ignore, reopen), use the dashboard or Lighthouse's JSON API; Navigator only reads.
Working with Navigator
Agents use tools they know about. Two things help them reach for Navigator at the right moments:
- Skills. The agent skills tell the agent what to check before a task: the route table before adding a route, the module graph before wiring a provider, the schema before writing a migration. The
navree-navigatorskill covers Navigator itself. - AGENTS.md. The
AGENTS.mdthatnavree newwrites describes the project's conventions. Add a line about Navigator if your team relies on it, for example "Use the navree MCP tools to check routes, providers and the schema before changing them".
A typical exchange, in Claude Code:
> The deals page is empty since I pulled main, and the API returns 500. Fix it.
● navree - last_error()
⎿ 500 GET /api/v1/deals request_id=0199c4b1-...
error: no such column: deals.expected_close_date
● navree - describe_schema(table: "deals")
⎿ deals: id, title, amount_cents, currency, stage, position, ...
pending migrations: crm/m20261026_000004_add_expected_close_date
● navree - list_commands()
⎿ ... migrate Run every pending migration ...
The code you pulled reads deals.expected_close_date, but the migration that
adds it hasn't run on your database. Run `navree migrate` and reload the page.Options
| Option | What it does |
|---|---|
--path <dir> | The app to serve, instead of the current directory. |
--no-build | Never run cargo build; use the last built binary (target/debug/<app>), or answer without one. |
--allow-remote-database | Let query_database, describe_schema and Lighthouse's tools connect to a database that isn't on this machine. |
--docs-offline | Never download docs; use the cache, or the docs Navigator ships with (and say so). |
Security
Navigator gives a model a view into your app, so it is built to show and never to change.
- Development only. It runs on your machine, as you, over stdio. It opens no port, and it isn't part of your app's binary, so nothing you deploy contains it.
- Read-only. No tool writes anything.
query_databaseparses the statement before sending it and refuses anything but oneSELECT,WITH,EXPLAINorSHOW: writes, DDL,PRAGMA,ATTACH,SET, transaction control, several statements, data-changingWITHclauses,EXPLAINof a write,SELECT ... INTO,FOR UPDATEandFOR SHARE, and functions that act outside the transaction (pg_sleep,pg_terminate_backend,pg_read_file, advisory locks,dblink,lo_*,set_config,load_extension, ...). What is sent is the statement as parsed, never the raw text. It then runs read-only: on PostgreSQL in a session whose transactions default to read-only, insideBEGIN READ ONLYwith a 5 secondstatement_timeout, through a cursor that fetches at most 101 rows, then rolled back; on SQLite on a connection withPRAGMA query_onlyon, interrupted after 5 seconds. At most 100 rows come back. - Local databases. Unless you pass
--allow-remote-database, Navigator only connects to SQLite files and to databases onlocalhost, a loopback address (127.0.0.1,::1) or a Unix socket. The same goes forlighthouse.database_url. Adatabase.urlpointing at a server elsewhere, such as a staging database in a.envyou forgot to change, is refused with a message that says why. Navigator never creates a SQLite file that doesn't exist. It reads SQLite and PostgreSQL databases; other drivers are refused. - Asking the app.
routes --jsonandmigrate:status --jsonboot your app the waynavree routesdoes, withmigrate_on_bootturned off for that process;introspect --jsonconstructs nothing. - Secrets redacted. Configuration is redacted like
Config::redacted(), query results mask secret-looking columns, and logs carry what your app'slog.redactalready left out. - What leaves your machine. Tool results go to the model your agent uses, like any file the agent reads. Keep customer data out of your development database: seed it with factories instead of copying production. Navigator itself only reaches the network to download the docs of your Navree version once.