# 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://" }` | 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 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>>`. - 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.