Files
galaxy/specs/REMOTE-1345/TECH.md
T

98 lines
7.5 KiB
Markdown

# REMOTE-1345: MCP Servers for Third-Party Harnesses
## Context
MCP server setup in the agent driver is gated on `HarnessKind::Oz` (`driver.rs:1258-1303`). Third-party harnesses (Claude Code, Codex, Gemini) receive zero MCP servers even when the user specifies them via `--mcp` or environment config. File-based MCP discovery (`driver.rs:1325-1341`) is also Oz-only.
The user's MCP config arrives as `Task.mcp_specs: Vec<MCPSpec>` (`driver.rs:336`), where each spec is either a `MCPSpec::Uuid` (reference to an installed server) or `MCPSpec::Json` (inline JSON config). For the Oz harness, `resolve_mcp_specs` (`driver.rs:826`) splits these into UUID refs and ephemeral `TemplatableMCPServerInstallation` objects, which are then spawned as child processes (stdio) or HTTP clients by `TemplatableMCPServerManager`.
Third-party harnesses have their own MCP client implementations:
- **Claude Code** reads `.mcp.json` or accepts `--mcp-config <json-file>` with `{ "mcpServers": { name: { command, args, env } } }` format.
- **Codex** reads `~/.codex/config.toml` with `[mcp_servers.name]` sections: `command`/`args`/`env` (stdio) or `url`/`bearer_token_env_var` (HTTP).
### Key files
- `app/src/ai/agent_sdk/driver.rs:1258-1303` — Oz-only MCP setup block
- `app/src/ai/agent_sdk/driver.rs:826-858``resolve_mcp_specs`
- `app/src/ai/agent_sdk/driver/harness/mod.rs:83-91``prepare_environment_config` trait method
- `app/src/ai/agent_sdk/driver/harness/claude_code.rs:66-78` — Claude impl
- `app/src/ai/agent_sdk/driver/harness/codex.rs:50-62` — Codex impl
- `app/src/ai/mcp/parsing.rs:382-397``resolve_json` (template → final JSON)
- `app/src/ai/mcp/templatable_installation.rs:113-156``apply_secrets`
- `app/src/ai/mcp/mod.rs:178-228``JSONTransportType`, `JSONMCPServer`
- `app/src/ai/agent_sdk/driver/environment.rs:289-300` — single-repo `cd` into repo subdir
## Proposed changes
### Approach: let each harness spawn its own MCP servers
Write resolved MCP configs into each harness's native config format before the CLI launches. The harness CLI spawns/connects to servers itself. This avoids multiplexing stdio pipes and keeps MCP lifecycle owned by the harness.
### 1. Resolve MCP specs to `HashMap<String, JSONMCPServer>` on the foreground spawner
New helper `resolve_mcp_specs_to_json` on `AgentDriver` (`driver.rs:788`), runs on the foreground spawner (model context required):
- Delegates to existing `resolve_mcp_specs` to split into `(existing_uuids, ephemeral_installations)`.
- For UUIDs: `TemplatableMCPServerManager::get_installed_server(uuid)` → clone → `apply_secrets``resolve_json``serde_json::from_str::<HashMap<String, JSONMCPServer>>`.
- For ephemeral: `apply_secrets``resolve_json``serde_json::from_str`.
- Collect all into `HashMap<String, JSONMCPServer>`.
Called in `prepare_harness` before `build_runner`. `prepare_harness` is only called from the `HarnessKind::ThirdParty` branch; the Oz path uses its own `resolve_mcp_specs``start_mcp_servers` unchanged.
### 2. Remove `prepare_environment_config` from the trait; fold config setup into `build_runner`
The `prepare_environment_config` method was removed from `ThirdPartyHarness` entirely. Each harness now does its config setup (auth, trust, onboarding, MCP) inside `build_runner`, which already receives all the context it needs.
I did this because we started to split handling for environment config across `build_runner` and `prepare_environment_config` depending on the harness, which resulted in us passing the same parameters
to both functions for all harnesses, but most harnesses would only use the parameters in one function or the other. `build_runner` and `prepare_environment_config` are always called
back-to-back.
`build_runner` signature gained two new params:
- `resolved_env_vars: &HashMap<OsString, OsString>` (previously passed to `prepare_environment_config`)
- `resolved_mcp_servers: &HashMap<String, JSONMCPServer>`
This consolidates per-run setup (temp files, MCP config) and static config (auth, trust) into one method per harness. Gemini currently ignores `resolved_mcp_servers`.
### 3. Claude Code: write MCP config in `ClaudeHarnessRunner::new`, pass via `--mcp-config`
Inside `build_runner``ClaudeHarnessRunner::new`:
- `prepare_claude_environment_config` is called first for static config (auth, trust, onboarding) — unchanged from before, just moved into `build_runner`.
- If `resolved_mcp_servers` is non-empty, `serialize_claude_mcp_config` produces `{ "mcpServers": { name: config } }` JSON.
- Written to a `NamedTempFile` with `.json` suffix, stored as `_temp_mcp_config_file` on the runner so it lives until the CLI exits.
- `claude_command()` gained a `mcp_config_path: Option<&str>` param; when present, appends `--mcp-config '<path>'`.
New types for serialization: `ClaudeMcpConfig` (wrapper with `mcp_servers` field) and `ClaudeMcpServerEntry` (tagged enum: `Stdio { command, args, env }` / `Http { url, headers }`).
Using `--mcp-config` instead of writing `.mcp.json` avoids a cwd problem: in the single-repo case, `environment.rs` does `cd_in_terminal(repo_name)` so the terminal cwd is `{working_dir}/{repo}`, not `working_dir`. Claude discovers `.mcp.json` relative to its cwd, so a file at `working_dir/.mcp.json` wouldn't be found. The flag is path-independent and also avoids conflicts with repo-owned `.mcp.json` files.
### 4. Codex: write `[mcp_servers]` entries to `config.toml`
`prepare_codex_environment_config` (now called inside `build_runner`) passes `resolved_mcp_servers` through to `prepare_codex_config_toml`, which calls the new `write_codex_mcp_servers` helper:
- `CLIServer { command, args, env }``[mcp_servers.name]` with `command`, `args`, `env` fields.
- `SSEServer { url, headers }``[mcp_servers.name]` with `url` and `http_headers` (inline table) fields.
Codex config is global (`~/.codex/config.toml`), not cwd-relative, so no path issues.
### 5. Transport mapping
Warp's `JSONTransportType` maps to each harness as follows:
**`CLIServer { command, args, env, working_directory }`:**
- Claude Code: `{ "type": "stdio", "command": "...", "args": [...], "env": {...} }`
- Codex: `command = "..."`, `args = [...]`, `env = { ... }` under `[mcp_servers.name]`
**`SSEServer { url, headers }`:**
- Claude Code: `{ "type": "http", "url": "...", "headers": {...} }`
- Codex: `url = "..."`, `http_headers = { ... }` under `[mcp_servers.name]`
## Testing and validation
1. **Unit tests for Claude MCP config serialization** (`claude_code_tests.rs`) — `serialize_claude_mcp_config_cli_server` and `serialize_claude_mcp_config_sse_server` verify `HashMap<String, JSONMCPServer>` produces valid `--mcp-config` JSON for both transport types.
2. **Unit tests for Codex MCP config serialization** (`codex_tests.rs`) — `write_codex_mcp_servers_cli_server` and `write_codex_mcp_servers_sse_server` verify `HashMap<String, JSONMCPServer>` produces valid `[mcp_servers]` TOML entries.
3. **Existing test updates**`claude_command` tests, `prepare_codex_config_toml` tests updated for new function signatures.
4. **Regression** — Oz harness MCP setup unchanged (existing tests cover this).
## Follow-ups
- **File-based MCP discovery for Codex**: Claude Code auto-discovers `.mcp.json` in cloned repos. Codex would need discovered configs translated to `config.toml` format.
- **Profile MCP servers**: `start_profile_mcp_servers` resolves UUID-based servers from the profile allowlist. Same resolution path as `task.mcp_specs` — could be included in a follow-up.
- **Gemini MCP support**: add when Gemini CLI supports MCP configuration.