diff --git a/app/src/ai/mcp/file_based_manager.rs b/app/src/ai/mcp/file_based_manager.rs index 2c3d1e02..9181444a 100644 --- a/app/src/ai/mcp/file_based_manager.rs +++ b/app/src/ai/mcp/file_based_manager.rs @@ -404,6 +404,39 @@ impl FileBasedMCPManager { .sorted() .collect() } + + /// Returns the directory a file-based MCP installation should be spawned from + /// when its config does not specify `working_directory`. + /// + /// The spawn root is the directory the config was discovered in, with one + /// exception: global Warp installs are discovered in `~/.warp/` (Warp's data + /// dir) which isn't a useful cwd for spawned processes, so they are remapped + /// to the home directory instead. + /// - Project-scoped installations: the repo root. + /// - Global installations (`~/.warp/.mcp.json`, `~/.claude.json`, etc.): the + /// home directory. + /// + /// If the installation is referenced from multiple roots, the lexicographically + /// smallest is returned for determinism. Returns `None` for installations that + /// are not tracked by `FileBasedMCPManager` (e.g. cloud-templated installs). + pub fn spawn_root_for_installation(&self, uuid: Uuid) -> Option { + let hash = self.get_hash_by_uuid(uuid)?; + let discovery_root = self + .file_based_servers_by_root + .iter() + .filter(|(_, provider_map)| provider_map.values().any(|hashes| hashes.contains(&hash))) + .map(|(root, _)| root.clone()) + .sorted() + .next()?; + + // Global Warp installs live under `~/.warp/`, which is internal Warp state + // rather than a meaningful working directory. Map them to the home dir so + // all global installs (Warp and third-party) share a consistent cwd. + if discovery_root == warp_data_dir() { + return dirs::home_dir().or(Some(discovery_root)); + } + Some(discovery_root) + } } pub enum FileBasedMCPManagerEvent { diff --git a/app/src/ai/mcp/mod_test.rs b/app/src/ai/mcp/mod_test.rs index 5ae5577b..122054c5 100644 --- a/app/src/ai/mcp/mod_test.rs +++ b/app/src/ai/mcp/mod_test.rs @@ -560,6 +560,32 @@ mod tests { } } + #[test] + fn test_parse_cli_server_preserves_explicit_working_directory() { + // An explicitly-set `working_directory` in a `.mcp.json`-style config must + // round-trip into `CLIServer.cwd_parameter` so the file-based spawner does + // not overwrite it with the discovery-root default. + let json = r#"{ + "my-server": { + "command": "node", + "args": ["./tooling/mcp/server.js"], + "working_directory": "/explicit/override/path" + } + }"#; + + let servers = MCPServer::from_user_json(json).expect("Failed to parse MCP servers"); + assert_eq!(servers.len(), 1); + + let TransportType::CLIServer(cli_server) = &servers[0].transport_type else { + panic!("Expected CLIServer transport type"); + }; + assert_eq!( + cli_server.cwd_parameter.as_deref(), + Some("/explicit/override/path"), + "Explicit working_directory must be preserved through parsing" + ); + } + #[test] fn test_parse_templatable_cli_server_without_args_and_resolve_json() { // Templatable MCP configs should work without an explicit "args" field. diff --git a/app/src/ai/mcp/templatable_manager/native.rs b/app/src/ai/mcp/templatable_manager/native.rs index 41cae851..10d034ef 100644 --- a/app/src/ai/mcp/templatable_manager/native.rs +++ b/app/src/ai/mcp/templatable_manager/native.rs @@ -782,6 +782,20 @@ impl TemplatableMCPServerManager { value: execution_path, }, ); + + // For file-based MCP installations without an explicit `working_directory`, + // default the spawn cwd to the directory the config was discovered in + // (repo root for project-scoped configs, ~/.warp/ or ~ for globals). This + // matches user expectations for repo-relative commands in `.mcp.json`. + // Cloud-templated installations (lookup returns None) are unaffected and + // continue to inherit Warp's process cwd. + if cli_server.cwd_parameter.is_none() { + if let Some(spawn_root) = + FileBasedMCPManager::as_ref(ctx).spawn_root_for_installation(installation_uuid) + { + cli_server.cwd_parameter = Some(spawn_root.to_string_lossy().into_owned()); + } + } } let executor = ctx.background_executor().clone(); @@ -1742,6 +1756,11 @@ async fn spawn_server( } } + // Capture the command and configured cwd for diagnostics before they're + // moved into the Command builder closure. + let command_for_log = command.clone(); + let cwd_for_log = cli_server.cwd_parameter.clone(); + // Try to spawn the child process. let (transport, stderr) = rmcp::transport::TokioChildProcess::builder( tokio::process::Command::new(command).configure(|cmd| { @@ -1768,6 +1787,17 @@ async fn spawn_server( .stderr(std::process::Stdio::piped()) .spawn() .map_err(|err| { + if err.kind() == std::io::ErrorKind::NotFound { + let cwd_display = cwd_for_log + .as_deref() + .unwrap_or(""); + logger.log(format!( + "[error] MCP: Failed to spawn '{server_name}': command '{command_for_log}' \ + not found (cwd: {cwd_display}). If your MCP server depends on a specific \ + working directory, set the `working_directory` field in your config to \ + override the default." + )); + } rmcp::RmcpError::transport_creation::(err) })?; diff --git a/resources/bundled/skills/add-mcp-server/SKILL.md b/resources/bundled/skills/add-mcp-server/SKILL.md index 7838264e..eaafdc41 100644 --- a/resources/bundled/skills/add-mcp-server/SKILL.md +++ b/resources/bundled/skills/add-mcp-server/SKILL.md @@ -61,6 +61,24 @@ Check whether the target config file exists. } ``` +By default, Warp spawns stdio servers from the directory the config was discovered in: +- Project-scoped configs (`{repo_root}/.warp/.mcp.json`) run from the repo root. +- Global configs (`~/.warp/.mcp.json`, `~/.claude.json`, etc.) run from the home directory. + +If the server's `command` or `args` are relative paths (e.g. `./tooling/mcp/server.js`) or the server expects a specific cwd, set `working_directory` to override the default: + +```json +{ + "mcpServers": { + "server-name": { + "command": "node", + "args": ["./tooling/mcp/server.js"], + "working_directory": "/absolute/path/to/repo" + } + } +} +``` + ### URL-based server (HTTP/SSE streaming transport) ```json