16 KiB
Managed MCP Resolution for CLI Agent Runs - Tech Spec
Context
This spec covers the Warp client work needed for CLI-based agent runs to consume managed MCP servers. The authoritative server-side behavior is documented in ../warp-server/specs/oauth-managed-mcp; this client spec focuses on how agent run --mcp resolves a warp_id that may refer to a managed MCP installation and turns it into runtime MCP config without persisting resolved secrets, proxy headers, or rendered command config. There is intentionally no sibling PRODUCT.md for this spec.
References were inspected at:
warpcommit1cdb4794e6d30f9c60d76167ec9b4ee823bc6a10warp-servercommitb2149361735cc756d54f26522287e79200f37f07
Relevant current client flow:
crates/warp_cli/src/mcp.rs:14-24 @ 1cdb4794definesMCPSpecas eitherUuidor raw JSON, andcrates/warp_cli/src/mcp.rs:51-70 @ 1cdb4794parses UUID-looking--mcpvalues before falling back to file/inline JSON.app/src/ai/agent_sdk/mcp_config.rs:7-60 @ 1cdb4794builds themcp_serversmap sent to the ambient-agent API. UUID specs become{"warp_id": "<uuid>"}; JSON specs are unpacked and validated.app/src/ai/agent_sdk/config_file.rs:99-136 @ 1cdb4794converts persistedmcp_serversback into runtimeMCPSpecs. Any entry withwarp_idbecomesMCPSpec::Uuid, so the driver sees the same UUID shape whether the input came from CLI, config file, or server task metadata.app/src/ai/agent_sdk/driver.rs:971-1045 @ 1cdb4794resolves runtime MCP specs. UUIDs are treated only as local templatable MCP installations today; JSON specs become ephemeral installations.app/src/ai/agent_sdk/driver.rs:1310-1387 @ 1cdb4794starts local installed MCPs separately from ephemeral MCPs.app/src/ai/agent_sdk/driver.rs:1897-1942 @ 1cdb4794wires the Oz harness startup path through that split.app/src/ai/agent_sdk/driver.rs:2440-2526 @ 1cdb4794resolves MCP specs for third-party harnesses intoJSONMCPServervalues after applying local secrets.app/src/ai/mcp/parsing.rs:256-374 @ 1cdb4794parses user JSON into templatable MCP installations. It only derives install variable values fromenvandheaders; placeholders elsewhere in the config become template variables without values and currently produceMCPMissingVariables.app/src/ai/mcp/templatable_installation.rs:100-155 @ 1cdb4794applies local raw managed secrets to MCP install variables, including explicit{{secret_name}}references.app/src/server/server_api.rs:1630-1680 @ 1cdb4794exposes domain-specific GraphQL-backed clients fromServerApiProvider; managed MCP should follow this pattern rather than passing rawServerApiaround new call sites.
Relevant server contract:
graphql/v2/mutations/managed_mcp.graphqls:69-77 @ b2149361definesCreateManagedMcpClientConfigOutputwithtransportKind,mcpConfigJson, proxy fields, and expiry.graphql/v2/mutations/managed_mcp.graphqls:119-122 @ b2149361exposes thecreateManagedMcpClientConfigmutation.logic/ai/ambient_agents/managed_mcp/client_config.go:26-30 @ b2149361states the use-time contract: URL-backed rows use proxy sessions, command-backed rows raw-render command config while preserving managed-secret placeholders for local client resolution.logic/ai/ambient_agents/managed_mcp/client_config.go:51-74 @ b2149361implements URL vs command behavior.logic/ai/ambient_agents/managed_mcp/client_config.go:92-110 @ b2149361returns canonical portable MCP JSON in the{"mcpServers": {...}}shape.
The core client bug today is that a managed MCP warp_id reaches AgentDriver as a UUID, then fails as MCPServerNotFound unless a local templatable installation with the same UUID exists. The client needs a runtime fallback that asks the server to resolve that UUID as a managed MCP installation.
Proposed Changes
GraphQL schema and client API
- Sync
crates/warp_graphql_schema/api/schema.graphqlwith the server schema needed forcreateManagedMcpClientConfig. - Add
crates/graphql/src/api/mutations/create_managed_mcp_client_config.rswith Cynic types for:CreateManagedMcpClientConfigVariablesCreateManagedMcpClientConfigInput { uid: cynic::Id }CreateManagedMcpClientConfigCreateManagedMcpClientConfigOutputCreateManagedMcpClientConfigResultManagedMcpTransportKind
- Register the mutation module in
crates/graphql/src/api/mutations/mod.rs. - Add
app/src/server/server_api/managed_mcp.rswith aManagedMcpClienttrait:async fn create_managed_mcp_client_config(&self, uid: uuid::Uuid) -> anyhow::Result<ManagedMcpClientConfigOutput>- map
UserFacingErrorthroughget_user_facing_error_message - return an explicit error for
Unknownso schema drift is visible.
- Add
pub mod managed_mcp;andServerApiProvider::get_managed_mcp_client()returningArc<dyn ManagedMcpClient>.
Runtime resolution model
Replace AgentDriver::resolve_mcp_specs and resolve_mcp_specs_to_json with a managed-aware resolver. Keep the current local/ephemeral split, but make it capable of async GraphQL calls.
Recommended domain shape:
struct ResolvedMcpSpecs {
local_uuids: Vec<Uuid>,
ephemeral_installations: Vec<TemplatableMCPServerInstallation>,
}
Resolution rules:
- Parse
MCPSpec::Jsonexactly as today and append resulting ephemeral installations. - For each
MCPSpec::Uuid:- If
TemplatableMCPServerManager::get_installed_server(&uuid).is_some(), append it tolocal_uuids. - Otherwise call
ManagedMcpClient::create_managed_mcp_client_config(uuid). - Parse returned
mcpConfigJsonas portable MCP config and append it as ephemeral installation(s).
- If
- Preserve local-first behavior. This avoids changing existing offline/local MCP flows and keeps backwards compatibility with UUIDs that refer to local templatable MCP installs.
- Do not write the returned
mcpConfigJson, proxy token, authorization header, or rendered command/env values back toAgentConfigSnapshot, task config, config files, or logs. - Treat GraphQL user-facing failures as fatal setup errors for that managed UUID. Add:
#[error("Failed to resolve managed MCP server {uid}: {message}")]
ManagedMcpResolutionFailed { uid: Uuid, message: String }
to AgentDriverError.
Managed returned config parsing
The server returns mcpConfigJson in the same portable wrapper shape the client already accepts for user JSON. That should be parsed through the same MCP templating machinery where possible, but managed config requires one additional helper because command-backed managed installs may preserve placeholders outside env and headers.
Add a helper near the existing MCP parsing/resolution code:
fn installations_from_managed_client_config_json(
json: &str,
) -> Result<Vec<TemplatableMCPServerInstallation>, AgentDriverError>
Behavior:
- Normalize and parse the JSON using
ParsedTemplatableMCPServerResult::from_user_json. - If
templatable_mcp_server_installationis present, use it. - If it is missing because the template has variables that were not captured from env/headers, preserve any captured env/header variable values and synthesize only missing variable values as
VariableValue { variable_type: Text, value: format!("{{{{{key}}}}}") }. - Construct a
TemplatableMCPServerInstallationfrom the parsed template, preserved captured values, and synthesized missing values. - Let the existing
apply_secretsstep resolve{{secret_name}}placeholders against local/task raw managed secrets beforeresolve_json.
This keeps hardcoded JSON and managed JSON on the same launch path while filling the parser gap for managed command args such as ["--token={{API_TOKEN}}"].
Oz harness path
Update the Oz setup section to resolve MCP specs asynchronously before startup:
- Retrieve
Arc<dyn ManagedMcpClient>fromServerApiProvider. - Spawn the resolver with access to:
- raw
task.mcp_specs TemplatableMCPServerManager- managed MCP client
- raw
- Start
ResolvedMcpSpecs.local_uuidsthroughstart_mcp_servers. - Start
ResolvedMcpSpecs.ephemeral_installationsthroughstart_ephemeral_mcp_servers. - Preserve existing strict/degraded MCP startup behavior after resolution. Resolution failures are not degraded startup; they are invalid setup because the requested MCP could not be materialized.
Third-party harness path
Update prepare_harness so the third-party path uses the same managed-aware resolver:
- Resolve MCP specs into
ResolvedMcpSpecs. - For
local_uuids, load each local installation fromTemplatableMCPServerManager, apply secrets, render JSON, deserialize toJSONMCPServer, and extend the harness config map. - For managed/inline ephemeral installations, apply secrets, render JSON, deserialize to
JSONMCPServer, and extend the harness config map. - Duplicate names should continue to behave like existing map extension semantics unless the implementation finds an existing helper that can produce better diagnostics without changing current behavior.
Preserved invariants
agent run --mcp <uuid>still stores and sends{"warp_id":"<uuid>"}in agent/task config.- Managed MCP resolution is use-time only and run-scoped.
- Existing direct JSON
commandandurlconfigs behave unchanged. - Existing local templatable MCP UUIDs behave unchanged.
- Server-side task launches that pass stored
warp_idvalues through CLI automatically get the same resolution path becausemcp_specs_from_mcp_serversalready convertswarp_identries intoMCPSpec::Uuid.
Testing and Validation
Add targeted unit tests around the new helpers and driver resolution path:
build_mcp_servers_from_specsandmcp_specs_from_mcp_serverscontinue to serialize/deserialize UUID specs aswarp_id; update existing tests only if schema names move.- Managed resolver with a locally installed UUID returns that UUID in
local_uuidsand does not callManagedMcpClient. - Managed resolver with a non-local UUID calls
create_managed_mcp_client_configand turns returned command config into an ephemeral installation. - Managed command config with env placeholder:
- input
{"mcpServers":{"GitHub MCP":{"command":"npx","env":{"API_TOKEN":"{{API_TOKEN}}"}}}} - with secret
API_TOKEN=real - rendered JSON contains
API_TOKEN=real.
- input
- Managed command config with arg placeholder:
- input
{"mcpServers":{"GitHub MCP":{"command":"npx","args":["--token={{API_TOKEN}}"]}}} - with secret
API_TOKEN=real - rendered JSON contains
--token=real.
- input
- Managed URL config returned from server:
- input includes proxy
urlandAuthorizationheader - third-party harness JSON contains the proxy URL and header value unchanged.
- input includes proxy
- Missing managed secret leaves the placeholder in the rendered command config, matching existing
apply_secretsbehavior. - GraphQL
UserFacingErrormaps toAgentDriverError::ManagedMcpResolutionFailedwith the UID and message. UnknownGraphQL response also fails with a clear managed MCP resolution error, notMCPServerNotFound.
Run targeted validation:
cargo test -p warp_graphqlcargo test -p warp mcp_config_testscargo test -p warp driver_testscargo check -p warpif Cynic/schema changes touch generated schema usage broadly.
Manual smoke validation once implementation exists:
- Use
agent run --mcp <managed-command-uid>with a command-backed managed MCP whose env/args reference a managed secret. Confirm the MCP starts and the secret is substituted locally. - Use
agent run --mcp <managed-url-uid>with a URL-backed managed MCP. Confirm the server mints proxy config and the local run can see the MCP tools. - Use a local templatable MCP UUID. Confirm no managed GraphQL resolution is attempted and the MCP starts as before.
- Start from an existing server-side task whose stored config has
mcp_servers.<name>.warp_id. Confirm the CLI worker path resolves the same way as direct CLI input.
Parallelization
Parallelization is optional for implementation but not useful for this spec itself. If implementation is split, use two local agents in separate worktrees so schema/client changes and runtime driver changes can progress independently:
- Agent A: GraphQL client wiring
- Execution mode: local.
- Worktree:
/Users/bens/Desktop/warp-managed-mcp-graphql. - Branch:
managed-mcp-cli-resolution-graphql. - Owns
crates/warp_graphql_schema,crates/graphql, andapp/src/server/server_api/managed_mcp.rs.
- Agent B: Driver/runtime resolution
- Execution mode: local.
- Worktree:
/Users/bens/Desktop/warp-managed-mcp-driver. - Branch:
managed-mcp-cli-resolution-driver. - Owns
app/src/ai/agent_sdk/driver.rs, MCP parsing helpers, and driver tests.
Merge Agent A first because Agent B needs the ManagedMcpClient trait and GraphQL output type. Land as one combined PR after both branches are reconciled and targeted tests pass.
Risks and Mitigations
- Secret leakage: server-returned config can contain proxy headers or unresolved placeholders. Keep all resolution in memory, avoid logging
mcpConfigJson, and do not write resolved config back to snapshots or config files. - Parser mismatch: managed command configs can contain placeholders outside env/headers. Add the managed-specific synthesis helper instead of weakening the general user-json parser behavior.
- UUID ambiguity: a UUID could theoretically exist both locally and as a managed MCP. Resolve local first to preserve current behavior and avoid surprising users with network-dependent resolution for existing local installs.
- Proxy token expiry: this change mints proxy config once per run. Do not add token refresh in v1; document failures as MCP startup/runtime failures and revisit if long-running managed URL MCPs need refresh.
Follow-ups
- Consider a future
MCPSpec::ManagedUuidor config-level discriminator only if local-first UUID ambiguity becomes a real issue. - Consider shared parsing utilities between user JSON and managed returned config if more server-produced MCP config shapes appear.