# 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.

## 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](https://modelcontextprotocol.io/) server built into the Navree CLI, started with one command:

```sh
navree mcp
```

Connect 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](https://navree.com/docs/tools/lighthouse.md#the-json-api-for-scripts-and-agents) (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](#lighthouses-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](https://navree.com/docs/digging-deeper/cli.md#installing-the-cli) first, then register Navigator with your agent.

### Claude Code

From your app's directory:

```sh
claude mcp add navree -- navree mcp
```

To share the setup with your team, add it at project scope instead. Claude Code writes `.mcp.json`, which you commit:

```sh
claude mcp add --scope project navree -- navree mcp
```

### Cursor

```json title=".cursor/mcp.json"
{
  "mcpServers": {
    "navree": {
      "command": "navree",
      "args": ["mcp"]
    }
  }
}
```

### Codex

```toml title="~/.codex/config.toml"
[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"]`.

### VS Code

```json title=".vscode/mcp.json"
{
  "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:

```sh
navree mcp --path /home/ada/code/crm
```

Ask 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:

1. It finds the app: the nearest Cargo package from the directory it starts in (or `--path`), and the `navree` version in its `Cargo.lock`.
2. For the module graph, providers and commands, it builds your app with `cargo build --message-format=json` (the debug profile, sharing your `target/` directory) and runs the binary with `introspect --json`. That built-in command validates the module graph without constructing anything, as `navree check` does, and prints it as JSON.
3. For routes, it runs the binary with `routes --json`, which boots the app without serving traffic, as `navree routes --json` does. Navigator adds `APP_DATABASE__MIGRATE_ON_BOOT=false` and `APP_LOG__LEVEL=error` to that process's environment, so asking never migrates your database and adds nothing to your log but errors. Answers are cached until `cargo build` produces a new binary, that is until a source file changes.
4. For the database, it reads your configuration exactly as the app does (`.env`, `config/app.toml`, `APP_*` variables, plus the settings of your `dock.toml` services, as `navree dev` passes them) and connects to `database.url` itself, read-only. For pending migrations it runs the binary with `migrate:status --json`.
5. For logs, it reads the log file your app writes (`log.file`).
6. 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.json` once and caches it under `~/.navree/docs/<version>/` (`NAVREE_HOME` replaces `~/.navree`, and `NAVREE_DOCS_URL` replaces `https://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`.

```json title="list_routes, path /api/v1/deals"
[
  {"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

*Diagram: The module graph: the root `AppModule` imports feature modules, each declaring the providers it registers, the modules it imports and the services it exports, and the container wires them at boot.*

`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:

```json title="query_database"
{
  "sql": "SELECT stage, count(*) AS deals, sum(amount_cents) AS total FROM deals GROUP BY stage ORDER BY stage"
}
```

```json
{
  "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](#security) 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:

```toml title="config/app.toml"
[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](https://navree.com/docs/tools/lighthouse.md#the-json-api-for-scripts-and-agents); 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](https://navree.com/docs/ai/agent-skills.md) 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-navigator` skill covers Navigator itself.
- **AGENTS.md.** The `AGENTS.md` that [`navree new`](https://navree.com/docs/ai/docs-for-agents.md#agentsmd-and-claudemd) writes 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:

```text
> 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_database` parses the statement before sending it and refuses anything but one `SELECT`, `WITH`, `EXPLAIN` or `SHOW`: writes, DDL, `PRAGMA`, `ATTACH`, `SET`, transaction control, several statements, data-changing `WITH` clauses, `EXPLAIN` of a write, `SELECT ... INTO`, `FOR UPDATE` and `FOR 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, inside `BEGIN READ ONLY` with a 5 second `statement_timeout`, through a cursor that fetches at most 101 rows, then rolled back; on SQLite on a connection with `PRAGMA query_only` on, 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 on `localhost`, a loopback address (`127.0.0.1`, `::1`) or a Unix socket. The same goes for `lighthouse.database_url`. A `database.url` pointing at a server elsewhere, such as a staging database in a `.env` you 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 --json` and `migrate:status --json` boot your app the way `navree routes` does, with `migrate_on_boot` turned off for that process; `introspect --json` constructs nothing.
- **Secrets redacted.** Configuration is redacted like `Config::redacted()`, query results mask secret-looking columns, and logs carry what your app's `log.redact` already 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](https://navree.com/docs/database/seeding-and-factories.md) instead of copying production. Navigator itself only reaches the network to download the docs of your Navree version once.

- [Agent skills](https://navree.com/docs/ai/agent-skills.md): Teach your agent how to build with Navree.
- [Docs for agents](https://navree.com/docs/ai/docs-for-agents.md): `llms.txt`, Markdown pages and AGENTS.md.
- [MCP servers](https://navree.com/docs/ai/mcp-servers.md): The MCP server your product offers its users.
