This manual describes how to create, test, register, and maintain MCP servers that extend Navi's capabilities. Read this before you start building a new server.
MCP servers run in isolated processes and communicate via the Model Context Protocol. They cannot crash Navi's core, they can be reloaded without restarting the server, and they scale to complex external integrations (APIs, databases, browsers, etc.). The trade-off is slightly more boilerplate than a single tools/foo.py file.
Rule: Every new capability that is not trivial (more than a simple datetime or notes lookup) should be built as an MCP server.
MCP servers live under:
mcp-servers/<server_name>/
├── pyproject.toml
├── README.md
└── app/
├── __init__.py
└── mcp_server.py
<server_name> — snake_case or kebab-case. Must match the key you will use in mcp_servers.d/<name>.json.pyproject.toml — Python package metadata and dependencies.app/mcp_server.py — the actual server code (FastMCP).Use the built-in tool create_mcp_server (preferred) or copy the template manually:
cp -r mcp-servers/_template mcp-servers/my_server cd mcp-servers/my_server # Edit pyproject.toml: change name, description, add dependencies # Edit app/mcp_server.py: add your tools and instructions
Minimal required fields:
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "mcp-server-myserver"
version = "0.1.0"
description = "What this server does"
requires-python = ">=3.11"
dependencies = [
"mcp>=1.27",
"pydantic>=2.0",
# add your own: httpx, asyncpg, playwright, etc.
]
[project.scripts]
mcp-server-myserver = "app.mcp_server:main"
[tool.setuptools.packages.find]
where = ["."]
include = ["app*"]
Read the template at mcp-servers/_template/app/mcp_server.py first. It contains a working hello-world server with extensive inline comments.
Key sections you must edit:
INSTRUCTIONS — These are injected into Navi's system prompt. Describe:
mcp = FastMCP("name", instructions=INSTRUCTIONS) — The name should match the directory key.
Tool functions — Each tool:
async def.@mcp.tool(name="tool_name").Annotated[T, Field(description="...")] — never plain types.str (JSON string for structured data is fine).Example:
@mcp.tool(name="search_docs")
async def search_docs_tool(
query: Annotated[str, Field(description="Search query string.")],
limit: Annotated[int, Field(description="Max results.")] = 10,
) -> str:
"""Search the documentation index."""
if not query.strip():
raise ValueError("query is required and cannot be empty.")
results = await _do_search(query, limit)
return json.dumps(results, ensure_ascii=False, indent=2)
After creating files, you must:
Create a virtual environment:
python -m venv .venv source .venv/bin/activate pip install -e .
Verify the server starts without crashing:
timeout 5 python -m app.mcp_server || true
If it prints a traceback, fix the code before proceeding.
Create a file mcp_servers.d/<name>.json in the project root. The filename (without .json) becomes the server name. Example for a server named my_server:
{
"transport": "stdio",
"command": "/absolute/path/to/mcp-servers/my_server/.venv/bin/python",
"args": ["-m", "app.mcp_server"],
"cwd": "/absolute/path/to/mcp-servers/my_server",
"env": {
"MCP_TRANSPORT": "stdio"
},
"groups": {
"default": ["search_docs", "read_doc"]
},
"instructions": "Optional extra instructions merged with the server's own INSTRUCTIONS."
}
Critical fields:
command — absolute path to the venv's Python binary.cwd — absolute path to the server directory.<name>.json (e.g. my_server.json).args — usually ["-m", "app.mcp_server"].groups — organize tools into named groups so profiles can reference them cleanly.After editing mcp_servers.d/<name>.json, call reload_tools to connect the server and register its tools.
Call mcp_status. You should see your server as connected with the correct tool count.
Call test_mcp_tool for every tool your server exposes:
test_mcp_tool(server_name="my_server", tool_name="search_docs", arguments={"query": "hello", "limit": 3})
If any tool fails, read the error output, fix the code in app/mcp_server.py, and repeat.
If mcp_status shows disconnected but the code looks correct, inspect stderr manually:
cd mcp-servers/my_server .venv/bin/python -m app.mcp_server 2>&1 | head -n 20
mcp-servers/<name>/app/mcp_server.py.pyproject.toml and run pip install -e . inside the venv.reload_tools to reconnect the server and re-register tools.test_mcp_tool to verify.mcp_servers.d/<name>.json.reload_tools.If the server was written by someone else:
mcp_servers.d/<name>.json with the correct command, cwd, args, and env.groups mapping the tools into logical sets.reload_tools.test_mcp_tool for a representative tool.| Symptom | Cause | Fix |
|---|---|---|
mcp_status shows disconnected |
Wrong command or cwd path |
Double-check absolute paths |
| Traceback on startup | Syntax error or missing import | Run python -m py_compile app/mcp_server.py |
test_mcp_tool returns is_error=True |
Tool raised an exception | Fix the tool logic; check parameter validation |
| Tool schema missing descriptions | Used plain types instead of Annotated[..., Field(...)] |
Add Field(description=...) to every parameter |
| Navi never calls the server | Profile does not map the server in mcp_servers |
Edit the profile's config.json and add the server groups |
| Navi bypasses MCP with filesystem | INSTRUCTIONS missing ABSOLUTE RULE |
Add explicit rule in server INSTRUCTIONS |
When asked to create a new MCP server:
manuals/write_mcp_server.md).mcp-servers/_template/app/mcp_server.py).create_mcp_server(name=..., description=...) to scaffold the directory.app/mcp_server.py iteratively using filesystem.code_exec or terminal with python -m py_compile ....terminal with timeout 5 python -m app.mcp_server.mcp_servers.d/<name>.json via filesystem to register the server.reload_tools.test_mcp_tool for every tool.INSTRUCTIONS inside mcp_server.py.