navigation-agent-mcp
Minimal MCP server scaffold focused on code navigation and repository inspection.
Purpose
This project exposes a public, normalized MCP API for analysis tools under the code.* namespace.
V1 scope:
- Navigation / analysis / inspection only
- First public tool:
code.find_symbol - Tree inspection tool:
code.inspect_tree - Endpoint index tool:
code.list_endpoints - Forward trace tool:
code.trace_symbol - Reverse trace tool:
code.trace_callers - Text search tool:
code.search_text - Structured responses for agent-friendly automation
Tech Stack
- Python
uvproject layout- Official MCP Python SDK (
FastMCP, v1.x style) - Pydantic models for normalized contracts
Project Layout
src/navigation_mcp/
├── adapters/internal_tools/ # wrappers around existing internal analyzers
├── contracts/ # public request/response models
├── services/ # application orchestration and normalization
├── tools/ # public MCP tool registration
├── app.py # FastMCP assembly
└── server.py # CLI entrypoint
Documentation
docs/overview.md— product scope, philosophy, and public toolsdocs/v1-summary.md— shipped V1 surface, limitations, and tradeoffsdocs/release-checklist.md— future release checklistdocs/testing.md— test layout and commands
Run
Stdio
uv run navigation-mcp --transport stdio
Streamable HTTP
uv run navigation-mcp --transport streamable-http --host 127.0.0.1 --port 8000 --path /mcp
Optional environment variables
NAVIGATION_MCP_WORKSPACE_ROOT: workspace root to analyze. Defaults to the current working directory.NAVIGATION_MCP_FIND_SYMBOL_SCRIPT: override the internal adapter script path.NAVIGATION_MCP_LIST_ENDPOINTS_SCRIPT: override the internal list_endpoints adapter script path.NAVIGATION_MCP_TRACE_CALLERS_SCRIPT: override the internal trace_callers adapter script path.NAVIGATION_MCP_TRACE_SYMBOL_SCRIPT: override the internal trace_symbol adapter script path.
Test in local environments
If you want to try this MCP on another PC with the same OpenCode setup, the simplest path is to install it as a user command and then register it in OpenCode.
Required programs
You need these programs installed on the machine:
python3.12+uvripgrepopencode(if you want to use it from OpenCode)
Install on Arch Linux
sudo pacman -S python uv ripgrep opencode
If opencode is not available in your environment yet, install it using the official OpenCode method described in their docs.
Install this MCP locally
From the root of this repository:
uv tool install .
If it was already installed and you want to refresh it after changes:
uv tool install --reinstall .
This installs the navigation-mcp command in your user environment.
Quick local verification
Check that the command is available:
navigation-mcp --help
You can also start it manually over stdio:
navigation-mcp --transport stdio
Enable it in OpenCode
Add this to ~/.config/opencode/opencode.json on the target machine:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"navigation": {
"type": "local",
"command": ["navigation-mcp", "--transport", "stdio"],
"enabled": true
}
}
}
If you want to pin the analyzed workspace explicitly, add an environment variable:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"navigation": {
"type": "local",
"command": ["navigation-mcp", "--transport", "stdio"],
"enabled": true,
"environment": {
"NAVIGATION_MCP_WORKSPACE_ROOT": "/path/to/workspace"
}
}
}
}
Verify OpenCode detects it
opencode mcp list
Then open OpenCode in your project and ask it to use the navigation MCP for code analysis.
Repeat on another PC
If the other machine uses the same OpenCode configuration style, the migration steps are:
- Install
python,uv,ripgrep, andopencode - Clone this repository
- Run
uv tool install . - Copy or recreate the
opencode.jsonMCP entry - Copy your project skill/rules if you want the same behavior defaults
Public Tool Contract
All tools return the same envelope shape:
{
"tool": "code.find_symbol",
"status": "ok",
"summary": "Found 2 symbol definitions for 'loader'.",
"data": {},
"errors": [],
"meta": {
"query": {},
"resolvedPath": null,
"truncated": false,
"counts": {},
"detection": {}
}
}
Stable meta contract
query: normalized request payloadresolvedPath: workspace-relative resolved scope when apathwas providedtruncated:truewhen data was truncated or safety-prunedcounts: stable machine-readable count metadatadetection: normalized derived metadata such as effective language/framework when meaningful
Stable path semantics
If a scoped path is provided and it does not exist inside the configured workspace, the tool returns status: "error" with FILE_NOT_FOUND.
If the provided path resolves outside the workspace, the tool returns status: "error" with PATH_OUTSIDE_WORKSPACE.
code.find_symbol
Find symbol definitions in the workspace.
Input
| Field | Type | Required | Notes |
|---|---|---|---|
symbol | string | yes | Symbol name to locate |
language | typescript | javascript | java | no | Optional language filter |
framework | react-router | spring | no | Optional framework hint; can infer language |
kind | any | class | interface | function | method | type | enum | constructor | annotation | no | Stable public kind filter |
match | exact | fuzzy | no | Match mode; default exact |
path | string | no | Workspace-relative or absolute scope |
limit | integer | no | Default 50, max 200 |
Data semantics
count: compatibility field; equalstotalMatchedreturnedCount: number of returned itemstotalMatched: full matched count before limit truncationitems[*].kind: normalized to the stable public symbol kinds above
code.inspect_tree
Inspect the workspace tree without reading file contents.
Input
| Field | Type | Required | Notes |
|---|---|---|---|
path | string | no | Workspace-relative or absolute file/directory scope |
max_depth | integer | no | Default 3, max 20 |
extensions | string[] | no | File extension filter; directories remain visible |
file_pattern | string | no | Filename glob such as *.py |
include_stats | boolean | no | Include stat metadata |
include_hidden | boolean | no | Include hidden entries except hard-ignored directories |
Data semantics
entryCount: number of returned entriesmeta.counts.returnedCount: same asentryCountmeta.counts.totalMatched: present only when known; omitted asnullwhen the safety cap prevents a full count
code.list_endpoints
List backend endpoints and frontend routes in the workspace.
Input
| Field | Type | Required | Notes |
|---|---|---|---|
path | string | no | Workspace-relative or absolute scope |
language | typescript | javascript | java | no | Optional language filter |
framework | react-router | spring | no | Optional framework hint |
kind | any | graphql | rest | route | no | Stable public kind filter |
limit | integer | no | Default 50, max 200 |
Data semantics
totalCount: full matched count before limit truncationreturnedCount: number of returned itemscounts.byKind|byLanguage|byFramework: grouped counts across the full matched set
Backend-specific subtypes are normalized to the stable public kinds:
- GraphQL queries/mutations →
graphql - REST verbs and request mappings →
rest - React Router loaders/actions/layouts/resource routes/components →
route
code.search_text
Search plain text or regex patterns across workspace files.
Input
| Field | Type | Required | Notes |
|---|---|---|---|
query | string | yes | Plain text or regex pattern |
path | string | no | Workspace-relative or absolute scope |
language | typescript | javascript | java | no | Optional language filter |
framework | react-router | spring | no | Optional framework hint |
include | string | no | Additional include glob such as *.tsx or src/** |
regex | boolean | no | Default false |
context | integer | no | Default 1, max 10 |
limit | integer | no | Default 50, max 200 |
Data semantics
fileCount: returned matched file countmatchCount: returned matched line counttotalFileCount: full matched file count before limit truncationtotalMatchCount: full matched line count before limit truncation
code.trace_symbol
Trace a symbol forward from a known starting file.
Input
| Field | Type | Required | Notes |
|---|---|---|---|
path | string | yes | Workspace-relative or absolute starting file path |
symbol | string | yes | Symbol/function/method name to trace forward |
language | typescript | javascript | java | no | Optional language hint |
framework | react-router | spring | no | Optional framework hint |
Data semantics
fileCount: returned related file countmeta.counts.returnedCount: same asfileCount
code.trace_callers
Trace incoming callers from a known starting file.
Input
| Field | Type | Required | Notes |
|---|---|---|---|
path | string | yes | Workspace-relative or absolute starting file path |
symbol | string | yes | Symbol/function/method name to trace incoming callers for |
language | typescript | javascript | java | no | Optional language hint |
framework | react-router | spring | no | Optional framework hint |
recursive | boolean | no | Enable recursive reverse traversal |
max_depth | integer | no | Only used with recursive=true; default 3, min 1, max 8 |
Data semantics
count: compatibility field; equalsreturnedCountreturnedCount: direct caller count returned initems- Recursive mode is opt-in
- Recursive payloads may be safety-pruned; when that happens the tool returns
status: "partial",RESULT_TRUNCATED, and the recursive summary counts remain authoritative even if arrays were sliced
Status semantics
ok: request succeeded, including zero resultspartial: request succeeded with truncated or safety-pruned dataerror: request could not be completed
Error semantics
Errors are always structured with:
code: stable machine-readable codemessage: human-readable explanationretryable: whether retrying may helpsuggestion: concrete next step for the caller
Coverage notes
find_symbol,trace_symbol, andtrace_callerscurrently cover Java and TypeScript-family source files supported by the internal analyzerssearch_textis powered by ripgrep behind the normalized public contract- Internal analyzer paths, commands, raw payloads, and local implementation details are intentionally NOT exposed in the public contract
Sample usage
{
"name": "code.search_text",
"arguments": {
"query": "useLoaderData",
"language": "typescript",
"include": "app/routes/**/*.tsx",
"context": 1,
"limit": 20
}
}