diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index 526aa447..0fa5cc47 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -498,8 +498,13 @@ pub fn configure_extensions_dialog() -> Result<(), Box> { ) .item( "sse", - "Remote Extension", - "Connect to a remote extension via SSE", + "Remote Extension (SSE)", + "Connect to a remote extension via Server-Sent Events", + ) + .item( + "streamable_http", + "Remote Extension (Streaming HTTP)", + "Connect to a remote extension via MCP Streaming HTTP", ) .interact()?; @@ -767,6 +772,133 @@ pub fn configure_extensions_dialog() -> Result<(), Box> { cliclack::outro(format!("Added {} extension", style(name).green()))?; } + "streamable_http" => { + let extensions = ExtensionConfigManager::get_all_names()?; + let name: String = cliclack::input("What would you like to call this extension?") + .placeholder("my-remote-extension") + .validate(move |input: &String| { + if input.is_empty() { + Err("Please enter a name") + } else if extensions.contains(input) { + Err("An extension with this name already exists") + } else { + Ok(()) + } + }) + .interact()?; + + let uri: String = cliclack::input("What is the Streaming HTTP endpoint URI?") + .placeholder("http://localhost:8000/messages") + .validate(|input: &String| { + if input.is_empty() { + Err("Please enter a URI") + } else if !(input.starts_with("http://") || input.starts_with("https://")) { + Err("URI should start with http:// or https://") + } else { + Ok(()) + } + }) + .interact()?; + + let timeout: u64 = cliclack::input("Please set the timeout for this tool (in secs):") + .placeholder(&goose::config::DEFAULT_EXTENSION_TIMEOUT.to_string()) + .validate(|input: &String| match input.parse::() { + Ok(_) => Ok(()), + Err(_) => Err("Please enter a valid timeout"), + }) + .interact()?; + + let add_desc = cliclack::confirm("Would you like to add a description?").interact()?; + + let description = if add_desc { + let desc = cliclack::input("Enter a description for this extension:") + .placeholder("Description") + .validate(|input: &String| { + if input.trim().is_empty() { + Err("Please enter a valid description") + } else { + Ok(()) + } + }) + .interact()?; + Some(desc) + } else { + None + }; + + let add_headers = + cliclack::confirm("Would you like to add custom headers?").interact()?; + + let mut headers = HashMap::new(); + if add_headers { + loop { + let key: String = cliclack::input("Header name:") + .placeholder("Authorization") + .interact()?; + + let value: String = cliclack::input("Header value:") + .placeholder("Bearer token123") + .interact()?; + + headers.insert(key, value); + + if !cliclack::confirm("Add another header?").interact()? { + break; + } + } + } + + let add_env = false; // No env prompt for Streaming HTTP + + let mut envs = HashMap::new(); + let mut env_keys = Vec::new(); + let config = Config::global(); + + if add_env { + loop { + let key: String = cliclack::input("Environment variable name:") + .placeholder("API_KEY") + .interact()?; + + let value: String = cliclack::password("Environment variable value:") + .mask('▪') + .interact()?; + + // Try to store in keychain + let keychain_key = key.to_string(); + match config.set_secret(&keychain_key, Value::String(value.clone())) { + Ok(_) => { + // Successfully stored in keychain, add to env_keys + env_keys.push(keychain_key); + } + Err(_) => { + // Failed to store in keychain, store directly in envs + envs.insert(key, value); + } + } + + if !cliclack::confirm("Add another environment variable?").interact()? { + break; + } + } + } + + ExtensionConfigManager::set(ExtensionEntry { + enabled: true, + config: ExtensionConfig::StreamableHttp { + name: name.clone(), + uri, + envs: Envs::new(envs), + env_keys, + headers, + description, + timeout: Some(timeout), + bundled: None, + }, + })?; + + cliclack::outro(format!("Added {} extension", style(name).green()))?; + } _ => unreachable!(), };