diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index 4f9b29d7bf..585427c9b1 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -1106,9 +1106,13 @@ fn configure_stdio_extension() -> anyhow::Result<()> { let timeout = prompt_extension_timeout()?; - let mut parts = command_str.split_whitespace(); - let cmd = parts.next().unwrap_or("").to_string(); - let args: Vec = parts.map(String::from).collect(); + let mut parts = crate::session::split_quoted(&command_str)?; + let cmd = if parts.is_empty() { + String::new() + } else { + parts.remove(0) + }; + let args = parts; let description = prompt_extension_description()?; let (envs, env_keys) = collect_env_vars()?; diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index b3969a135d..8b37624f17 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -231,6 +231,36 @@ pub async fn classify_planner_response( } } +pub fn split_quoted(input: &str) -> Result> { + let mut parts = Vec::new(); + let mut current = String::new(); + let mut in_double_quote = false; + let mut in_single_quote = false; + + for c in input.chars() { + match c { + '"' if !in_single_quote => in_double_quote = !in_double_quote, + '\'' if !in_double_quote => in_single_quote = !in_single_quote, + c if c.is_whitespace() && !in_double_quote && !in_single_quote => { + if !current.is_empty() { + parts.push(std::mem::take(&mut current)); + } + } + _ => current.push(c), + } + } + + if in_double_quote || in_single_quote { + return Err(anyhow::anyhow!("Unmatched quote in command")); + } + + if !current.is_empty() { + parts.push(current); + } + + Ok(parts) +} + impl CliSession { #[allow(clippy::too_many_arguments)] pub async fn new( @@ -273,7 +303,7 @@ impl CliSession { /// Parse a stdio extension command string into an ExtensionConfig /// Format: "ENV1=val1 ENV2=val2 command args..." pub fn parse_stdio_extension(extension_command: &str) -> Result { - let mut parts: Vec<&str> = extension_command.split_whitespace().collect(); + let mut parts = split_quoted(extension_command)?; let mut envs = HashMap::new(); while let Some(part) = parts.first() { @@ -289,7 +319,7 @@ impl CliSession { return Err(anyhow::anyhow!("No command provided in extension string")); } - let cmd = parts.remove(0).to_string(); + let cmd = parts.remove(0); let name = std::path::Path::new(&cmd) .file_name() .and_then(|f| f.to_str()) @@ -299,7 +329,7 @@ impl CliSession { Ok(ExtensionConfig::Stdio { name, cmd, - args: parts.iter().map(|s| s.to_string()).collect(), + args: parts, envs: Envs::new(envs), env_keys: Vec::new(), description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(), @@ -2014,6 +2044,21 @@ mod tests { } ; "env_prefix_name_from_cmd" )] + #[test_case( + r#""/Applications/IntelliJ IDEA.app/Contents/jbr/Contents/Home/bin/java" -classpath "/path/with spaces/lib.jar" Main"#, + ExtensionConfig::Stdio { + name: "java".into(), + cmd: "/Applications/IntelliJ IDEA.app/Contents/jbr/Contents/Home/bin/java".into(), + args: vec!["-classpath".into(), "/path/with spaces/lib.jar".into(), "Main".into()], + envs: Envs::default(), + env_keys: vec![], + description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(), + timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), + bundled: None, + available_tools: vec![], + } + ; "quoted_path_with_spaces" + )] fn test_parse_stdio_extension(input: &str, expected: ExtensionConfig) { assert_eq!(CliSession::parse_stdio_extension(input).unwrap(), expected); } @@ -2023,6 +2068,23 @@ mod tests { assert!(CliSession::parse_stdio_extension("").is_err()); } + #[test] + fn test_split_quoted_windows_paths() { + assert_eq!( + split_quoted(r"C:\tools\mcp.exe --arg value").unwrap(), + vec![r"C:\tools\mcp.exe", "--arg", "value"] + ); + assert_eq!( + split_quoted(r#""C:\Program Files\server\mcp.exe" --arg"#).unwrap(), + vec![r"C:\Program Files\server\mcp.exe", "--arg"] + ); + } + + #[test] + fn test_split_quoted_unmatched_quote() { + assert!(split_quoted(r#""unmatched"#).is_err()); + } + #[test_case( "https://mcp.kiwi.com", 300, ExtensionConfig::StreamableHttp { diff --git a/documentation/docs/mcp/jetbrains-mcp.md b/documentation/docs/mcp/jetbrains-mcp.md index ebe6672aa8..10b3b41e40 100644 --- a/documentation/docs/mcp/jetbrains-mcp.md +++ b/documentation/docs/mcp/jetbrains-mcp.md @@ -23,7 +23,6 @@ This tutorial covers how to add the JetBrains extension to integrate with any Je Versions 2025.2 and later have built-in MCP server support and generate a dynamic configuration specific to your IDE instance. See your IDE's documentation for more details (e.g. [MCP Server](https://www.jetbrains.com/help/idea/mcp-server.html) for IntelliJ IDEA). -
Configure the extension using your IDE's built-in MCP server support: @@ -49,10 +47,6 @@ This tutorial covers how to add the JetBrains extension to integrate with any Je 2. Add the JetBrains extension to goose using the command from the config: - :::info - If the goose Desktop or goose CLI configuration steps aren't successful, follow the `Config File` steps. - ::: - 1. Click the button in the top-left to open the sidebar @@ -82,35 +76,6 @@ This tutorial covers how to add the JetBrains extension to integrate with any Je } /> - - 1. Open your goose [`config.yaml`](/docs/guides/config-files) file - 2. In the `extensions` section, add an entry that uses your IDE's Stdio config, for example: - - ```yaml - extensions: - jetbrains: - enabled: true - type: stdio - name: JetBrains - description: Integrate goose with any JetBrains IDE - cmd: /Applications/IntelliJ IDEA.app/Contents/jbr/Contents/Home/bin/java - args: - - -classpath - - /Applications/IntelliJ IDEA.app/Contents/plugins/mcpserver/lib/mcpserver-frontend.jar:/Applications/IntelliJ IDEA.app/Contents/lib/util-8.jar - - com.intellij.mcpserver.stdio.McpStdioRunnerKt - envs: - IJ_MCP_SERVER_PORT: "63342" - env_keys: - - IJ_MCP_SERVER_PORT - timeout: 300 - bundled: null - available_tools: [] - ``` - - Make sure to: - - Replace the `cmd` and `args` values in the example to match your JetBrains IDE installation - - Update `IJ_MCP_SERVER_PORT` to match your IDE's MCP server port -