7.5 KiB
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.jsonor accepts--mcp-config <json-file>with{ "mcpServers": { name: { command, args, env } } }format. - Codex reads
~/.codex/config.tomlwith[mcp_servers.name]sections:command/args/env(stdio) orurl/bearer_token_env_var(HTTP).
Key files
app/src/ai/agent_sdk/driver.rs:1258-1303— Oz-only MCP setup blockapp/src/ai/agent_sdk/driver.rs:826-858—resolve_mcp_specsapp/src/ai/agent_sdk/driver/harness/mod.rs:83-91—prepare_environment_configtrait methodapp/src/ai/agent_sdk/driver/harness/claude_code.rs:66-78— Claude implapp/src/ai/agent_sdk/driver/harness/codex.rs:50-62— Codex implapp/src/ai/mcp/parsing.rs:382-397—resolve_json(template → final JSON)app/src/ai/mcp/templatable_installation.rs:113-156—apply_secretsapp/src/ai/mcp/mod.rs:178-228—JSONTransportType,JSONMCPServerapp/src/ai/agent_sdk/driver/environment.rs:289-300— single-repocdinto 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_specsto 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 toprepare_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_configis called first for static config (auth, trust, onboarding) — unchanged from before, just moved intobuild_runner.- If
resolved_mcp_serversis non-empty,serialize_claude_mcp_configproduces{ "mcpServers": { name: config } }JSON. - Written to a
NamedTempFilewith.jsonsuffix, stored as_temp_mcp_config_fileon the runner so it lives until the CLI exits. claude_command()gained amcp_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]withcommand,args,envfields.SSEServer { url, headers }→[mcp_servers.name]withurlandhttp_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
- Unit tests for Claude MCP config serialization (
claude_code_tests.rs) —serialize_claude_mcp_config_cli_serverandserialize_claude_mcp_config_sse_serververifyHashMap<String, JSONMCPServer>produces valid--mcp-configJSON for both transport types. - Unit tests for Codex MCP config serialization (
codex_tests.rs) —write_codex_mcp_servers_cli_serverandwrite_codex_mcp_servers_sse_serververifyHashMap<String, JSONMCPServer>produces valid[mcp_servers]TOML entries. - Existing test updates —
claude_commandtests,prepare_codex_config_tomltests updated for new function signatures. - Regression — Oz harness MCP setup unchanged (existing tests cover this).
Follow-ups
- File-based MCP discovery for Codex: Claude Code auto-discovers
.mcp.jsonin cloned repos. Codex would need discovered configs translated toconfig.tomlformat. - Profile MCP servers:
start_profile_mcp_serversresolves UUID-based servers from the profile allowlist. Same resolution path astask.mcp_specs— could be included in a follow-up. - Gemini MCP support: add when Gemini CLI supports MCP configuration.