diff --git a/docs/api.md b/docs/api.md index f1821ee..e6be3e4 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1552,6 +1552,45 @@ --- +### MCP user keys (BYOK) + +Per-user credentials for MCP servers whose config declares a `user_key` slot +(see [`mcp.md`](mcp.md)). Auth required. Keys are never returned in any +response — only presence and update time. + +#### `GET /mcp-keys` + +Servers that accept a user key **and** are referenced by at least one +profile. Response `200`: + +```json +{"items": [ + { + "server_name": "gnexus-creds", + "transport": "streamable_http", + "key_type": "header", + "key_location": "Authorization", + "prefix": "Bearer ", + "instructions": "...", + "has_key": false, + "updated_at": null + } +]} +``` + +#### `PUT /mcp-keys/{server_name}` + +Body `{"key": "..."}` (1..4096 chars, trimmed). Saves the caller's key +(Fernet-encrypted at rest) and invalidates the cached resolution. `404` for +an unknown or key-less server, `422` for an empty/oversized key. + +#### `DELETE /mcp-keys/{server_name}` + +Drops the caller's key (falls back to the config default). `204` always when +the server exists (even if no key was stored), `404` for unknown/key-less. + +--- + ## Files **Client static**: `GET /static/**` — served from `client/` directory. Header `Cache-Control: no-store`. diff --git a/docs/config.md b/docs/config.md index 2b7c99a..4908600 100644 --- a/docs/config.md +++ b/docs/config.md @@ -137,6 +137,25 @@ | `TOOLS_DIR` | str | `tools` | Directory for user-defined tools (auto-discovered at startup) | | `CONTEXT_PROVIDERS_DIR` | str | `context_providers` | Directory for user-defined context providers (auto-discovered at startup) | +### MCP `user_key` (BYOK) + +Per-server JSON config (`mcp_servers.d/.json`) — where a user's personal +credential is injected. Details in [`mcp.md`](mcp.md). + +```json +// HTTP server: key goes into an Authorization-style header +{ "transport": "streamable_http", "url": "https://…", + "user_key": { "header": "Authorization", "prefix": "Bearer " } } + +// stdio server: key goes into an env var +{ "transport": "stdio", "command": "uvx", "env": { "API_KEY": "default" }, + "user_key": { "env": "API_KEY" } } +``` + +Keys are stored Fernet-encrypted (`NAVI_AUTH_ENCRYPTION_KEY`, see +Authentication above). Users without a saved key fall back to the default +credential from the config file. + ## Session files | Variable | Type | Default | Description | diff --git a/docs/index.md b/docs/index.md index b5e79c4..00c1787 100644 --- a/docs/index.md +++ b/docs/index.md @@ -29,6 +29,7 @@ | [`architecture.md`](architecture.md) | Component diagram, data flow, dependency graph | | [`agent.md`](agent.md) | Agent loop, planning phase, tool execution, subagents, workers | | [`tools.md`](tools.md) | Built-in tools, user tool format, hot-reload | +| [`mcp.md`](mcp.md) | MCP servers — configs, tool exposure, per-user keys (BYOK) | | [`sessions.md`](sessions.md) | Session model, dual-buffer design, context compression | | [`store.md`](store.md) | KV store — per-session/per-user key-value persistence | | [`recall.md`](recall.md) | Scheduled callbacks — headless recall system | diff --git a/docs/mcp.md b/docs/mcp.md new file mode 100644 index 0000000..468a222 --- /dev/null +++ b/docs/mcp.md @@ -0,0 +1,108 @@ +# MCP (Model Context Protocol) + +How Navi connects to external MCP servers, how their tools are exposed, and +how per-user credentials (BYOK) work. + +## Configs + +Each server lives in its own file `mcp_servers.d/.json`: + +```json +{ + "transport": "streamable_http", + "url": "https://creds.example.com/mcp", + "headers": { "Authorization": "Bearer gcr_system_default" }, + "groups": { + "creds": ["search_secrets", "get_secret", "reveal_secret"] + }, + "instructions": "MANDATORY: ...", + "user_key": { "header": "Authorization", "prefix": "Bearer " } +} +``` + +| Field | Applies to | Meaning | +|---|---|---| +| `transport` | all | `stdio` \| `sse` \| `streamable_http` | +| `command`, `args`, `env`, `cwd` | `stdio` | Subprocess launch | +| `url`, `headers` | `sse`, `streamable_http` | Endpoint and HTTP headers | +| `groups` | all | Named tool groups profiles reference instead of listing tools | +| `instructions` | all | Overlay injected next to the server's own initialize instructions | +| `user_key` | all | Declares the per-user credential slot (BYOK); omit for key-less servers | + +The legacy monolithic `mcp_servers.json` is auto-migrated to `mcp_servers.d/` +on first load. Edits take effect on server restart or a `reload_tools`-triggered +`McpManager.reload_all()`. + +**Heads-up for admin config edits:** `PUT /admin/agents/mcp-servers` round-trips +the whole model — write back the config you read, or a `user_key` slot added by +hand is silently erased. + +## Runtime + +- `McpManager` (`navi/mcp/manager.py`) holds one `McpClient` per server, + reconnects dead ones in a 30 s health-check loop, and is the single + `call_tool` gateway. Connection headers/env are fixed at transport open + (`navi/mcp/client.py`) — a shared client cannot switch credentials per call. +- Tool names follow `mcp____`; profiles select servers via + `tools.agent.mcp` / `tools.subagent.mcp` group maps. Registered MCP tools + are process-wide, regardless of which profile uses them. +- `McpTool.execute` forwards `ctx.user_id` (falling back to the + `current_user_id` ContextVar) into `manager.call_tool(..., user_id=...)`. + +## Per-user keys (BYOK) + +A config with a `user_key` slot lets each user bring their own credential: + +```json +"user_key": { "header": "Authorization", "prefix": "Bearer " } +``` +or, for stdio servers: +```json +"user_key": { "env": "API_KEY" } +``` + +Exactly one destination (validator `navi/mcp/config.py::McpUserKey`); a +`header` is only valid on HTTP transports, an `env` only on stdio. + +Resolution flow (`McpManager._client_for`): + +1. `user_id is None`, or the config has no `user_key`, or the resolver has no + saved key → the **default shared client** is used with the plaintext + credential from the config file. This path never touches the database — + behaviour without BYOK is byte-for-byte unchanged. +2. Otherwise `KeyResolver.resolve(user_id, server)` reads the user's key + (30 s in-memory TTL; every failure — DB error, missing + `NAVI_AUTH_ENCRYPTION_KEY` — logs a warning and resolves to `None`, i.e. + falls back to the default rather than breaking the tool call). +3. The tool call runs on a **per-(server, user) `McpClient` clone** built from + `McpServerConfig.with_user_key(key)` (deep copy of the config with the key + injected into the header/env destination). Clones are cached per key + snapshot: a changed key transparently rebuilds the client. + +Cache bounds: hard LRU cap of 32 per-user clients; per-user clients idle +longer than 30 minutes are reaped by the health-check tick. Per-user clients +are outside the health-check's reconnect logic — they reconnect lazily with +backoff when next called. `disconnect_all` (reload/shutdown) tears user +clients down as well. + +Key changes (PUT/DELETE) fire the resolver's `on_change` callback, which drops +affected per-user clients immediately. + +### Storage + +`mcp_user_keys` table (postgres, `navi/mcp/_ddl.py`), Fernet-encrypted via +`NAVI_AUTH_ENCRYPTION_KEY` (`navi/mcp/keystore.py::McpKeyStore`). One row per +`(user_id, server_name)`; deleting a user cascades. Keys never leave the +backend through logs or REST responses. + +Anon mode (`NAVI_AUTH_ENABLED=false`) is one shared local user — its saved +key effectively becomes a system-level override, and requires +`NAVI_AUTH_ENCRYPTION_KEY` to be set (the Fernet encryptor refuses an empty +key). + +## REST + +Settings UI surface — see [`api.md`](api.md#mcp-user-keys-byok): +`GET /mcp-keys` (eligible servers, no key values), `PUT /mcp-keys/{server}` +(save/invalidate), `DELETE /mcp-keys/{server}` (reset to default). +The webclient renders the panel only when the eligible list is non-empty. \ No newline at end of file diff --git a/docs/tools.md b/docs/tools.md index b096a49..95492a5 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -30,6 +30,8 @@ MCP tools survive `reload_tools` because they are registered as external tools in `ToolRegistry`. +MCP configs, transports and per-user credentials (BYOK) are covered in [`mcp.md`](mcp.md). + | Tool | Name | Description | |---|---|---| | `FilesystemTool` | `filesystem` | Read/write/list/copy/grep/diff local files (path restrictions via config) |