fix: extension command with quotes in cli (#8150)
This commit is contained in:
@@ -1106,9 +1106,13 @@ fn configure_stdio_extension() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let timeout = prompt_extension_timeout()?;
|
let timeout = prompt_extension_timeout()?;
|
||||||
|
|
||||||
let mut parts = command_str.split_whitespace();
|
let mut parts = crate::session::split_quoted(&command_str)?;
|
||||||
let cmd = parts.next().unwrap_or("").to_string();
|
let cmd = if parts.is_empty() {
|
||||||
let args: Vec<String> = parts.map(String::from).collect();
|
String::new()
|
||||||
|
} else {
|
||||||
|
parts.remove(0)
|
||||||
|
};
|
||||||
|
let args = parts;
|
||||||
|
|
||||||
let description = prompt_extension_description()?;
|
let description = prompt_extension_description()?;
|
||||||
let (envs, env_keys) = collect_env_vars()?;
|
let (envs, env_keys) = collect_env_vars()?;
|
||||||
|
|||||||
@@ -231,6 +231,36 @@ pub async fn classify_planner_response(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn split_quoted(input: &str) -> Result<Vec<String>> {
|
||||||
|
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 {
|
impl CliSession {
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
@@ -273,7 +303,7 @@ impl CliSession {
|
|||||||
/// Parse a stdio extension command string into an ExtensionConfig
|
/// Parse a stdio extension command string into an ExtensionConfig
|
||||||
/// Format: "ENV1=val1 ENV2=val2 command args..."
|
/// Format: "ENV1=val1 ENV2=val2 command args..."
|
||||||
pub fn parse_stdio_extension(extension_command: &str) -> Result<ExtensionConfig> {
|
pub fn parse_stdio_extension(extension_command: &str) -> Result<ExtensionConfig> {
|
||||||
let mut parts: Vec<&str> = extension_command.split_whitespace().collect();
|
let mut parts = split_quoted(extension_command)?;
|
||||||
let mut envs = HashMap::new();
|
let mut envs = HashMap::new();
|
||||||
|
|
||||||
while let Some(part) = parts.first() {
|
while let Some(part) = parts.first() {
|
||||||
@@ -289,7 +319,7 @@ impl CliSession {
|
|||||||
return Err(anyhow::anyhow!("No command provided in extension string"));
|
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)
|
let name = std::path::Path::new(&cmd)
|
||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|f| f.to_str())
|
.and_then(|f| f.to_str())
|
||||||
@@ -299,7 +329,7 @@ impl CliSession {
|
|||||||
Ok(ExtensionConfig::Stdio {
|
Ok(ExtensionConfig::Stdio {
|
||||||
name,
|
name,
|
||||||
cmd,
|
cmd,
|
||||||
args: parts.iter().map(|s| s.to_string()).collect(),
|
args: parts,
|
||||||
envs: Envs::new(envs),
|
envs: Envs::new(envs),
|
||||||
env_keys: Vec::new(),
|
env_keys: Vec::new(),
|
||||||
description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(),
|
description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(),
|
||||||
@@ -2014,6 +2044,21 @@ mod tests {
|
|||||||
}
|
}
|
||||||
; "env_prefix_name_from_cmd"
|
; "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) {
|
fn test_parse_stdio_extension(input: &str, expected: ExtensionConfig) {
|
||||||
assert_eq!(CliSession::parse_stdio_extension(input).unwrap(), expected);
|
assert_eq!(CliSession::parse_stdio_extension(input).unwrap(), expected);
|
||||||
}
|
}
|
||||||
@@ -2023,6 +2068,23 @@ mod tests {
|
|||||||
assert!(CliSession::parse_stdio_extension("").is_err());
|
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(
|
#[test_case(
|
||||||
"https://mcp.kiwi.com", 300,
|
"https://mcp.kiwi.com", 300,
|
||||||
ExtensionConfig::StreamableHttp {
|
ExtensionConfig::StreamableHttp {
|
||||||
|
|||||||
@@ -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).
|
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).
|
||||||
|
|
||||||
<!-- hide until parsing bugs for paths with spaces are fixed in Desktop and CLI
|
|
||||||
:::tip Quick Install
|
:::tip Quick Install
|
||||||
<Tabs groupId="interface">
|
<Tabs groupId="interface">
|
||||||
<TabItem value="ui" label="goose Desktop" default>
|
<TabItem value="ui" label="goose Desktop" default>
|
||||||
@@ -34,7 +33,6 @@ This tutorial covers how to add the JetBrains extension to integrate with any Je
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
:::
|
:::
|
||||||
-->
|
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
Configure the extension using your IDE's built-in MCP server support:
|
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:
|
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.
|
|
||||||
:::
|
|
||||||
|
|
||||||
<Tabs groupId="interface">
|
<Tabs groupId="interface">
|
||||||
<TabItem value="ui" label="goose Desktop" default>
|
<TabItem value="ui" label="goose Desktop" default>
|
||||||
1. Click the <PanelLeft className="inline" size={16} /> button in the top-left to open the sidebar
|
1. Click the <PanelLeft className="inline" size={16} /> 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
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="config" label="Config File">
|
|
||||||
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
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="earlier" label="2025.1 and earlier">
|
<TabItem value="earlier" label="2025.1 and earlier">
|
||||||
|
|||||||
Reference in New Issue
Block a user