Files
openhuman/docs/MCP_SETUP_AGENT.md
T

170 lines
8.0 KiB
Markdown

# MCP Setup Agent
A sub-agent that walks the user through installing, configuring, and
connecting an MCP server from one of the upstream registries
(`mcp_registry::registries`: Smithery, modelcontextprotocol/registry).
**Status: implemented** (issue #3039). The agent archetype, its five
`mcp_setup_*` tools, the opaque-secret request/submit flow, and the
`install_and_connect` commit path are all live. The orchestrator delegates to
it via the `setup_mcp_server` delegate (the `mcp_setup` entry in
`src/openhuman/agent_registry/agents/orchestrator/agent.toml`), so a chat turn
like *"set up the Notion MCP server"* routes here. The sections below describe
how the flow works; paths reflect the shipped layout.
---
## Goal
A non-technical user says *"set up the Notion MCP server for me"*. The
agent:
1. Browses the enabled registries, finds the candidate, summarises it.
2. Asks for any secrets the server requires (API keys, OAuth tokens, …)
**without ever pulling the values into the LLM context**.
3. Test-connects with the collected secrets, surfaces errors, lets the
user retry / change values.
4. On success, persists the install and the secrets, runs boot-spawn for
this one server, returns connection status + the tool list now
available to the main agent.
The agent owns the conversation; the core owns the secrets, the
subprocess, and the persistence.
---
## Tool surface
Five tools registered behind a `mcp_setup_*` namespace. All tool inputs
and outputs are JSON; secret values **never** appear in either direction.
| Tool | Input | Output | Notes |
| --- | --- | --- | --- |
| `mcp_setup_search` | `{ query?, page?, page_size?, source? }` | `{ servers: [Summary], total_pages }` | Thin wrapper over `mcp_registry::registry::registry_search`. `source` optionally scopes to one upstream. |
| `mcp_setup_get` | `{ qualified_name }` | `{ detail, required_env_keys }` | Wraps `registry_get`; pre-computes `required_env_keys` from the `config_schema` (same logic as `ops::collect_required_env_keys`). |
| `mcp_setup_request_secret` | `{ key_name, prompt }` | `{ ref: "secret://<opaque>" }` | Triggers an out-of-band UI prompt. Returns an opaque ref; raw value is held in a process-local in-memory map keyed by ref. |
| `mcp_setup_test_connection` | `{ qualified_name, env_refs: { KEY: "secret://…" } }` | `{ ok, tools?: [McpTool], error?: string }` | Spawns the candidate subprocess in a **scratch** workspace, resolves refs to values just-in-time, runs `initialize` + `tools/list`, tears it down. No persistence. |
| `mcp_setup_install_and_connect` | `{ qualified_name, env_refs }` | `{ server_id, status, tools: [McpTool] }` | Resolves refs, persists the install + `mcp_client_env` rows, calls `connections::connect`. Refs are always consumed (removed from the in-memory map) regardless of outcome — on failure the agent must re-prompt via `mcp_setup_request_secret`. |
---
## Secret flow — opaque refs
The hard requirement: **raw secret values must not enter LLM context**.
Opaque refs solve this cleanly:
```
agent: mcp_setup_request_secret({ key_name: "NOTION_API_KEY", prompt: "Notion integration token" })
core: → pushes prompt to UI; user types into a native input box
core: ← receives value, stores in SETUP_SECRETS: HashMap<RefId, String>
core: → returns { ref: "secret://7c9f2e" } ← the agent sees only this
agent: mcp_setup_test_connection({
qualified_name: "@notion/server",
env_refs: { "NOTION_API_KEY": "secret://7c9f2e" }
})
core: → for each ref, look up the value in SETUP_SECRETS, build the env
vector, spawn, init, list_tools, tear down
core: ← returns { ok: true, tools: [...] } ← still no raw value to agent
```
Lifecycle of `SETUP_SECRETS`:
- Process-local `OnceLock<RwLock<HashMap<RefId, SecretEntry>>>`.
- Entries TTL out after, say, 15 min (defends against stranded secrets if
the conversation is abandoned mid-flow).
- `mcp_setup_install_and_connect` consumes refs regardless of outcome:
pulls each value, writes it to the `mcp_client_env` table (existing
persistence, already keyed by `server_id`), and removes the ref from
the in-memory map. On failure the agent should re-prompt via
`mcp_setup_request_secret` to collect fresh refs for a retry.
- On core shutdown the map is dropped — refs do not survive restart.
`RefId` is a short random hex string. **No structure or hint of the
underlying value** so the agent has nothing useful to leak even if it
tries.
### Why not just take key names?
Considered (option 2 in the original AskUserQuestion). Rejected because:
- The agent can't decide between values it just collected — e.g. trying
two different tokens to pick the one that works requires distinguishing
them, which requires handles.
- Tying secrets to the `(server_id, key)` pair too early means a failed
test-connect leaves stale rows in `mcp_client_env` for an
uninstall-rolled-back server.
Opaque refs give the agent enough handle to iterate without exposing
values.
---
## Where the agent lives
Follows the existing sub-agent pattern (`src/openhuman/agent_registry/`):
- Archetype TOML at `src/openhuman/agent_registry/agents/mcp_setup/agent.toml`
(loaded by the agent registry loader).
- Prompt + tool allowlist scoped tight: only the five `mcp_setup_*` tools
plus `ask_user_clarification`. **No** general filesystem, network, or shell
tools — the agent shouldn't be able to exfiltrate a leaked ref even if one
shows up. (`submit_secret` is intentionally NOT in the agent allowlist — the
UI calls it out-of-band via the socket bridge.)
- Triggered by the orchestrator's `setup_mcp_server` delegate (the `mcp_setup`
entry in `agent_registry/agents/orchestrator/agent.toml`), or directly from
chat when the user asks to add/install/set up an MCP server.
---
## Implementation outline
Following the project's `Specify → Rust → JSON-RPC → UI → tests` flow:
1. **Rust core** (in `src/openhuman/mcp_registry/`):
- New module `setup.rs` owning `SETUP_SECRETS` (in-memory ref map with
TTL) and helpers `mint_ref`, `resolve_refs(env_refs) -> Vec<(K,V)>`,
`consume_refs(env_refs)`.
- New module `setup_ops.rs` with the four handlers.
- Wire schemas in `schemas.rs`, controllers in `core/all.rs`.
2. **Tool-side bridge** so the agent harness sees the four tools as
regular tool defs. Reuse the controller-to-tool generator already
used elsewhere.
3. **UI**: out-of-band secret prompt component (probably a `chat`-pinned
modal listening on a new socket event `mcp_setup_request_secret`),
submit POSTs the value to a Tauri command that calls into core to
register the ref.
4. **Archetype** + system prompt at
`src/openhuman/agent_registry/agents/mcp_setup/agent.toml`.
5. **Tests**:
- Unit: ref lifecycle (mint → resolve → consume → TTL expiry).
- Integration (`tests/mcp_registry_e2e.rs` style): full flow against
the existing `test-mcp-stub` binary, asserting refs vanish after
install + that test-connect failures leave refs intact.
---
## Open questions for the implementer
- TTL value — 15 min is a guess; calibrate against typical install flow.
- Should `test_connection` accept a partial env_refs (some refs, some
literal-by-name) for iteration? Current design says refs only, which
forces consistency.
- The official MCP registry returns servers with **multiple package
ecosystems** (`packages: [{ registry_name: "npm" | "pypi" | … }]`). The
setup agent needs to either pick one or ask the user. Add a
`package_choice` step or default to npm?
- Telemetry: log `mcp_setup_*` calls (`tracing::info!` is fine) but
never log ref values, never log env values, only key names.
---
## Anti-goals
- The setup agent is **not** a generic "ask user for any data" surface.
Its prompt tool is scoped to MCP env values, full stop.
- It does **not** persist anything until `install_and_connect` succeeds.
No half-installed rows in `mcp_servers` or `mcp_client_env`.
- It does **not** read back secrets. Once persisted into `mcp_client_env`
they are write-only from the agent's perspective; only the subprocess
spawn path in `connections::connect` reads them.