navra + custom MCP client
Prerequisites
- navra running (
navra serveor systemd service) - A bearer token
- An MCP client that supports Streamable HTTP or SSE transport
Protocol
navra exposes a standard MCP endpoint at:
http://localhost:9315/mcp
The transport is Streamable HTTP (the default MCP transport for HTTP-based servers). SSE clients also work against this endpoint.
Authentication uses a bearer token in the Authorization header.
Minimal client example (Python)
Using the mcp Python SDK:
import asyncio
from mcp.client.streamable_http import streamablehttp_client
from mcp import ClientSession
async def main():
headers = {"Authorization": "Bearer mcd_your_token_here"}
async with streamablehttp_client(
"http://localhost:9315/mcp",
headers=headers,
) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
# List available tools
tools = await session.list_tools()
for tool in tools.tools:
print(f" {tool.name}: {tool.description}")
# Call a tool
result = await session.call_tool("file_read", {"path": "/etc/hostname"})
print(result.content[0].text)
asyncio.run(main())Minimal client example (TypeScript)
Using the @modelcontextprotocol/sdk package:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport(
new URL("http://localhost:9315/mcp"),
{
requestInit: {
headers: {
Authorization: "Bearer mcd_your_token_here",
},
},
}
);
const client = new Client({ name: "my-client", version: "1.0" });
await client.connect(transport);
const tools = await client.listTools();
console.log("Tools:", tools.tools.map(t => t.name));
const result = await client.callTool({
name: "file_read",
arguments: { path: "/etc/hostname" },
});
console.log(result.content);
await client.close();Minimal client example (Rust)
Using the rmcp crate:
use rmcp::service::ServiceExt;
use rmcp::transport::StreamableHttpClientTransport;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let transport = StreamableHttpClientTransport::from_uri(
"http://localhost:9315/mcp"
);
let client = ServiceExt::<rmcp::RoleClient>::serve((), transport).await?;
let peer = client.peer().clone();
let tools = peer.list_all_tools().await?;
for tool in &tools {
println!(" {}: {:?}", tool.name, tool.description);
}
Ok(())
}
Note: the Rust client does not yet support header-based auth on
Streamable HTTP. Use the token query parameter or configure
navra with --dev-mode for development.
What the client sees
navra merges tools from all registered upstream MCP servers and its
own built-in modules. The client sees a single flat tool list.
Tool names are prefixed with the module name to avoid collisions
(e.g., file_read, git_status, rag_search).
Authentication
All requests must include a bearer token:
Authorization: Bearer mcd_<token>
Tokens are created with navra token create --name my-client and
bound to a permission set that controls which tools the client can
call.
Troubleshooting
"unauthorized" response
- Check the token is valid:
navra token list - Verify the
Authorizationheader format (must beBearer mcd_...) - Check the agent config in
config.tomlhas the correcttoken_hash
Tool call denied
The audit log shows why:
navra audit --limit 10 --detail
Common reasons: tool not in the allow list, path outside allowed
directories, or tool requires approval.
Connection refused
navra defaults to a Unix socket. If your client needs TCP, ensure
config.toml has:
[server]
tcp = "127.0.0.1:9315"