Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 134 additions & 2 deletions crates/goose-cli/src/commands/configure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,8 +498,13 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
)
.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()?;

Expand Down Expand Up @@ -767,6 +772,133 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {

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::<u64>() {
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!(),
};

Expand Down
Loading