From f7a01c2fc09bbd1443a882d823a3c9212e63d53e Mon Sep 17 00:00:00 2001 From: Jarrod Sibbison Date: Mon, 11 Aug 2025 15:35:35 +1000 Subject: [PATCH 1/8] Recipe can specify which tools are visible in extensions --- crates/goose-cli/src/commands/configure.rs | 12 +++ .../goose-cli/src/recipes/secret_discovery.rs | 12 +++ crates/goose-cli/src/session/mod.rs | 8 ++ crates/goose-server/src/routes/extension.rs | 11 +++ crates/goose/src/agents/agent.rs | 2 + crates/goose/src/agents/extension.rs | 77 +++++++++++++++++++ crates/goose/src/agents/extension_manager.rs | 31 ++++++-- crates/goose/src/config/extensions.rs | 2 + 8 files changed, 147 insertions(+), 8 deletions(-) diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index ff218f408e02..065ab616d7cc 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -113,6 +113,8 @@ pub async fn handle_configure() -> Result<(), Box> { timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), bundled: Some(true), description: None, + tools_are_visible_default: true, + tools: HashMap::new(), }, })?; } @@ -641,6 +643,8 @@ pub fn configure_extensions_dialog() -> Result<(), Box> { timeout: Some(timeout), bundled: Some(true), description: None, + tools_are_visible_default: true, + tools: HashMap::new(), }, })?; @@ -748,6 +752,8 @@ pub fn configure_extensions_dialog() -> Result<(), Box> { description, timeout: Some(timeout), bundled: None, + tools_are_visible_default: true, + tools: HashMap::new(), }, })?; @@ -850,6 +856,8 @@ pub fn configure_extensions_dialog() -> Result<(), Box> { description, timeout: Some(timeout), bundled: None, + tools_are_visible_default: true, + tools: HashMap::new(), }, })?; @@ -977,6 +985,8 @@ pub fn configure_extensions_dialog() -> Result<(), Box> { description, timeout: Some(timeout), bundled: None, + tools_are_visible_default: true, + tools: HashMap::new(), }, })?; @@ -1619,6 +1629,8 @@ pub async fn handle_openrouter_auth() -> Result<(), Box> { timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), bundled: Some(true), description: None, + tools_are_visible_default: true, + tools: HashMap::new(), }, }) { Ok(_) => println!("✓ Developer extension enabled"), diff --git a/crates/goose-cli/src/recipes/secret_discovery.rs b/crates/goose-cli/src/recipes/secret_discovery.rs index 673eb16a06f3..b6a4a0ca99e7 100644 --- a/crates/goose-cli/src/recipes/secret_discovery.rs +++ b/crates/goose-cli/src/recipes/secret_discovery.rs @@ -143,6 +143,8 @@ mod tests { description: None, timeout: None, bundled: None, + tools_are_visible_default: true, + tools: HashMap::new(), }, ExtensionConfig::Stdio { name: "slack-mcp".to_string(), @@ -153,6 +155,8 @@ mod tests { timeout: None, description: None, bundled: None, + tools_are_visible_default: true, + tools: HashMap::new(), }, ExtensionConfig::Builtin { name: "builtin-ext".to_string(), @@ -160,6 +164,8 @@ mod tests { description: None, timeout: None, bundled: None, + tools_are_visible_default: true, + tools: HashMap::new(), }, ]), context: None, @@ -237,6 +243,8 @@ mod tests { description: None, timeout: None, bundled: None, + tools_are_visible_default: true, + tools: HashMap::new(), }, ExtensionConfig::Stdio { name: "service-b".to_string(), @@ -247,6 +255,8 @@ mod tests { timeout: None, description: None, bundled: None, + tools_are_visible_default: true, + tools: HashMap::new(), }, ]), context: None, @@ -294,6 +304,8 @@ mod tests { description: None, timeout: None, bundled: None, + tools_are_visible_default: true, + tools: HashMap::new(), }]), sub_recipes: Some(vec![SubRecipe { name: "child-recipe".to_string(), diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 82f5328bf4f2..fc6f1b5d5a6b 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -211,6 +211,8 @@ impl Session { // TODO: should set timeout timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), bundled: None, + tools_are_visible_default: true, + tools: HashMap::new(), }; self.agent @@ -244,6 +246,8 @@ impl Session { // TODO: should set timeout timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), bundled: None, + tools_are_visible_default: true, + tools: HashMap::new(), }; self.agent @@ -278,6 +282,8 @@ impl Session { // TODO: should set timeout timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), bundled: None, + tools_are_visible_default: true, + tools: HashMap::new(), }; self.agent @@ -304,6 +310,8 @@ impl Session { timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), bundled: None, description: None, + tools_are_visible_default: true, + tools: HashMap::new(), }; self.agent .add_extension(config) diff --git a/crates/goose-server/src/routes/extension.rs b/crates/goose-server/src/routes/extension.rs index 17e006754a88..268b21e25c3c 100644 --- a/crates/goose-server/src/routes/extension.rs +++ b/crates/goose-server/src/routes/extension.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::env; use std::path::Path; use std::sync::Arc; @@ -194,6 +195,8 @@ async fn add_extension( description: None, timeout, bundled: None, + tools_are_visible_default: true, + tools: HashMap::new(), }, ExtensionConfigRequest::StreamableHttp { name, @@ -211,6 +214,8 @@ async fn add_extension( description: None, timeout, bundled: None, + tools_are_visible_default: true, + tools: HashMap::new(), }, ExtensionConfigRequest::Stdio { name, @@ -241,6 +246,8 @@ async fn add_extension( env_keys, timeout, bundled: None, + tools_are_visible_default: true, + tools: HashMap::new(), } } ExtensionConfigRequest::Builtin { @@ -253,6 +260,8 @@ async fn add_extension( timeout, bundled: None, description: None, + tools_are_visible_default: true, + tools: HashMap::new(), }, ExtensionConfigRequest::Frontend { name, @@ -263,6 +272,8 @@ async fn add_extension( tools, instructions, bundled: None, + tools_are_visible_default: true, + tool_configs: HashMap::new(), }, }; diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 26521dcb6896..db26f8b0c669 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -616,6 +616,8 @@ impl Agent { tools, instructions, bundled: _, + tools_are_visible_default: _, + tool_configs: _, } => { // For frontend tools, just store them in the frontend_tools map let mut frontend_tools = self.frontend_tools.lock().await; diff --git a/crates/goose/src/agents/extension.rs b/crates/goose/src/agents/extension.rs index 3550ac4e7086..98940a7f7067 100644 --- a/crates/goose/src/agents/extension.rs +++ b/crates/goose/src/agents/extension.rs @@ -61,6 +61,13 @@ pub struct Envs { map: HashMap, } +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct ToolConfig { + /// Whether this tool is visible to the LLM + #[serde(default = "default_true")] + pub visible: bool, +} + impl Envs { /// List of sensitive env vars that should not be overridden const DISALLOWED_KEYS: [&'static str; 31] = [ @@ -163,6 +170,12 @@ pub enum ExtensionConfig { /// Whether this extension is bundled with Goose #[serde(default)] bundled: Option, + /// Default visibility for tools from this extension + #[serde(default = "default_true")] + tools_are_visible_default: bool, + /// Per-tool configuration overrides + #[serde(default)] + tools: HashMap, }, /// Standard I/O client with command and arguments #[serde(rename = "stdio")] @@ -180,6 +193,12 @@ pub enum ExtensionConfig { /// Whether this extension is bundled with Goose #[serde(default)] bundled: Option, + /// Default visibility for tools from this extension + #[serde(default = "default_true")] + tools_are_visible_default: bool, + /// Per-tool configuration overrides + #[serde(default)] + tools: HashMap, }, /// Built-in extension that is part of the goose binary #[serde(rename = "builtin")] @@ -192,6 +211,12 @@ pub enum ExtensionConfig { /// Whether this extension is bundled with Goose #[serde(default)] bundled: Option, + /// Default visibility for tools from this extension + #[serde(default = "default_true")] + tools_are_visible_default: bool, + /// Per-tool configuration overrides + #[serde(default)] + tools: HashMap, }, /// Streamable HTTP client with a URI endpoint using MCP Streamable HTTP specification #[serde(rename = "streamable_http")] @@ -212,6 +237,12 @@ pub enum ExtensionConfig { /// Whether this extension is bundled with Goose #[serde(default)] bundled: Option, + /// Default visibility for tools from this extension + #[serde(default = "default_true")] + tools_are_visible_default: bool, + /// Per-tool configuration overrides + #[serde(default)] + tools: HashMap, }, /// Frontend-provided tools that will be called through the frontend #[serde(rename = "frontend")] @@ -225,6 +256,12 @@ pub enum ExtensionConfig { /// Whether this extension is bundled with Goose #[serde(default)] bundled: Option, + /// Default visibility for tools from this extension + #[serde(default = "default_true")] + tools_are_visible_default: bool, + /// Per-tool configuration overrides + #[serde(default)] + tool_configs: HashMap, }, /// Inline Python code that will be executed using uvx #[serde(rename = "inline_python")] @@ -240,6 +277,12 @@ pub enum ExtensionConfig { /// Python package dependencies required by this extension #[serde(default)] dependencies: Option>, + /// Default visibility for tools from this extension + #[serde(default = "default_true")] + tools_are_visible_default: bool, + /// Per-tool configuration overrides + #[serde(default)] + tools: HashMap, }, } @@ -251,6 +294,8 @@ impl Default for ExtensionConfig { description: None, timeout: Some(config::DEFAULT_EXTENSION_TIMEOUT), bundled: Some(true), + tools_are_visible_default: true, + tools: HashMap::new(), } } } @@ -265,6 +310,8 @@ impl ExtensionConfig { description: Some(description.into()), timeout: Some(timeout.into()), bundled: None, + tools_are_visible_default: true, + tools: HashMap::new(), } } @@ -283,6 +330,8 @@ impl ExtensionConfig { description: Some(description.into()), timeout: Some(timeout.into()), bundled: None, + tools_are_visible_default: true, + tools: HashMap::new(), } } @@ -301,6 +350,8 @@ impl ExtensionConfig { description: Some(description.into()), timeout: Some(timeout.into()), bundled: None, + tools_are_visible_default: true, + tools: HashMap::new(), } } @@ -316,6 +367,8 @@ impl ExtensionConfig { description: Some(description.into()), timeout: Some(timeout.into()), dependencies: None, + tools_are_visible_default: true, + tools: HashMap::new(), } } @@ -333,6 +386,8 @@ impl ExtensionConfig { timeout, description, bundled, + tools_are_visible_default, + tools, .. } => Self::Stdio { name, @@ -343,6 +398,8 @@ impl ExtensionConfig { description, timeout, bundled, + tools_are_visible_default, + tools, }, other => other, } @@ -365,6 +422,26 @@ impl ExtensionConfig { } .to_string() } + + /// Check if a tool should be visible to the LLM + pub fn is_tool_visible(&self, tool_name: &str) -> bool { + match self { + Self::Sse { tools_are_visible_default, tools, .. } + | Self::StreamableHttp { tools_are_visible_default, tools, .. } + | Self::Stdio { tools_are_visible_default, tools, .. } + | Self::Builtin { tools_are_visible_default, tools, .. } + | Self::InlinePython { tools_are_visible_default, tools, .. } => { + tools.get(tool_name) + .map(|config| config.visible) + .unwrap_or(*tools_are_visible_default) + } + Self::Frontend { tools_are_visible_default, tool_configs, .. } => { + tool_configs.get(tool_name) + .map(|config| config.visible) + .unwrap_or(*tools_are_visible_default) + } + } + } } impl std::fmt::Display for ExtensionConfig { diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index 1d0f75ea3e1d..aa03066e2422 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -42,6 +42,7 @@ pub struct ExtensionManager { instructions: HashMap, resource_capable_extensions: HashSet, temp_dirs: HashMap, + extension_configs: HashMap, } /// A flattened representation of a resource used by the agent to prepare inference @@ -113,6 +114,7 @@ impl ExtensionManager { instructions: HashMap::new(), resource_capable_extensions: HashSet::new(), temp_dirs: HashMap::new(), + extension_configs: HashMap::new(), } } @@ -320,6 +322,8 @@ impl ExtensionManager { description: _, timeout, bundled: _, + tools_are_visible_default: _, + tools: _, } => { let cmd = std::env::current_exe() .expect("should find the current executable") @@ -390,7 +394,8 @@ impl ExtensionManager { .insert(sanitized_name.clone()); } - self.add_client(sanitized_name, client); + self.add_client(sanitized_name.clone(), client); + self.extension_configs.insert(sanitized_name, config); Ok(()) } @@ -420,6 +425,7 @@ impl ExtensionManager { self.instructions.remove(&sanitized_name); self.resource_capable_extensions.remove(&sanitized_name); self.temp_dirs.remove(&sanitized_name); + self.extension_configs.remove(&sanitized_name); Ok(()) } @@ -476,6 +482,7 @@ impl ExtensionManager { let client_futures = filtered_clients.map(|(name, client)| { let name = name.clone(); let client = client.clone(); + let extension_config = self.extension_configs.get(&name).cloned(); task::spawn(async move { let mut tools = Vec::new(); @@ -486,13 +493,21 @@ impl ExtensionManager { loop { for tool in client_tools.tools { - tools.push(Tool { - name: format!("{}__{}", name, tool.name).into(), - description: tool.description, - input_schema: tool.input_schema, - annotations: tool.annotations, - output_schema: tool.output_schema, - }); + // Check if tool should be visible + let is_visible = extension_config + .as_ref() + .map(|config| config.is_tool_visible(&tool.name)) + .unwrap_or(true); // Default to visible if no config + + if is_visible { + tools.push(Tool { + name: format!("{}__{}", name, tool.name).into(), + description: tool.description, + input_schema: tool.input_schema, + annotations: tool.annotations, + output_schema: tool.output_schema, + }); + } } // Exit loop when there are no more pages diff --git a/crates/goose/src/config/extensions.rs b/crates/goose/src/config/extensions.rs index 7a075714c5e4..bbb4a935a77d 100644 --- a/crates/goose/src/config/extensions.rs +++ b/crates/goose/src/config/extensions.rs @@ -47,6 +47,8 @@ impl ExtensionConfigManager { timeout: Some(DEFAULT_EXTENSION_TIMEOUT), bundled: Some(true), description: Some(DEFAULT_EXTENSION_DESCRIPTION.to_string()), + tools_are_visible_default: true, + tools: HashMap::new(), }, }, )]); From d187d14173639ed73503eb45836000204c3f77da Mon Sep 17 00:00:00 2001 From: Jarrod Sibbison Date: Mon, 11 Aug 2025 16:07:21 +1000 Subject: [PATCH 2/8] Add test for visible get_prefixed_tools --- crates/goose/src/agents/extension.rs | 4 ++ crates/goose/src/agents/extension_manager.rs | 68 +++++++++++++++++++- crates/goose/src/recipe/mod.rs | 1 + 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/crates/goose/src/agents/extension.rs b/crates/goose/src/agents/extension.rs index 98940a7f7067..a9081a3538e7 100644 --- a/crates/goose/src/agents/extension.rs +++ b/crates/goose/src/agents/extension.rs @@ -68,6 +68,10 @@ pub struct ToolConfig { pub visible: bool, } +fn default_true() -> bool { + true +} + impl Envs { /// List of sensitive env vars that should not be overridden const DISALLOWED_KEYS: [&'static str; 31] = [ diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index aa03066e2422..99dbeeec9f9a 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -980,7 +980,34 @@ mod tests { _next_cursor: Option, _cancellation_token: CancellationToken, ) -> Result { - Err(Error::TransportClosed) + use serde_json::json; + use std::sync::Arc; + Ok(ListToolsResult { + tools: vec![ + Tool { + name: "tool".into(), + description: Some("A basic tool".into()), + input_schema: Arc::new(json!({}).as_object().unwrap().clone()), + annotations: None, + output_schema: None, + }, + Tool { + name: "visible_tool".into(), + description: Some("A visible tool".into()), + input_schema: Arc::new(json!({}).as_object().unwrap().clone()), + annotations: None, + output_schema: None, + }, + Tool { + name: "hidden_tool".into(), + description: Some("A hidden tool".into()), + input_schema: Arc::new(json!({}).as_object().unwrap().clone()), + annotations: None, + output_schema: None, + }, + ], + next_cursor: None, + }) } async fn call_tool( @@ -990,7 +1017,7 @@ mod tests { _cancellation_token: CancellationToken, ) -> Result { match name { - "tool" | "test__tool" => Ok(CallToolResult { + "tool" | "test__tool" | "visible_tool" | "hidden_tool" => Ok(CallToolResult { content: Some(vec![]), is_error: None, structured_content: None, @@ -1176,4 +1203,41 @@ mod tests { panic!("Expected ToolError::NotFound"); } } + + #[tokio::test] + async fn test_tool_visibility_filtering() { + use crate::agents::extension::ToolConfig; + use std::collections::HashMap; + + let mut extension_manager = ExtensionManager::new(); + + let mut tools = HashMap::new(); + tools.insert("visible_tool".to_string(), ToolConfig { visible: true }); + tools.insert("hidden_tool".to_string(), ToolConfig { visible: false }); + + let config = ExtensionConfig::Builtin { + name: "test_extension".to_string(), + display_name: Some("Test Extension".to_string()), + description: Some("Test extension for visibility".to_string()), + timeout: Some(300), + bundled: Some(true), + tools_are_visible_default: true, + tools, + }; + + let sanitized_name = normalize("test_extension".to_string()); + extension_manager.extension_configs.insert(sanitized_name.clone(), config); + + extension_manager.clients.insert( + sanitized_name, + Arc::new(Mutex::new(Box::new(MockClient {}))), + ); + + let tools = extension_manager.get_prefixed_tools(None).await.unwrap(); + + let tool_names: Vec = tools.iter().map(|t| t.name.to_string()).collect(); + assert!(tool_names.iter().any(|name| name.contains("tool"))); // Default visible + assert!(tool_names.iter().any(|name| name.contains("visible_tool"))); + assert!(!tool_names.iter().any(|name| name.contains("hidden_tool"))); + } } diff --git a/crates/goose/src/recipe/mod.rs b/crates/goose/src/recipe/mod.rs index 09aac54a593b..8f50d8baed1e 100644 --- a/crates/goose/src/recipe/mod.rs +++ b/crates/goose/src/recipe/mod.rs @@ -685,6 +685,7 @@ sub_recipes: description, timeout, dependencies, + .. } => { assert_eq!(name, "test_python"); assert_eq!(code, "print('hello world')"); From dcd559f306be5613e4e902de77900f9697e06cf1 Mon Sep 17 00:00:00 2001 From: Jarrod Sibbison Date: Tue, 12 Aug 2025 11:18:00 +1000 Subject: [PATCH 3/8] fmt --- crates/goose/src/agents/extension.rs | 58 ++++++++++++-------- crates/goose/src/agents/extension_manager.rs | 9 +-- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/crates/goose/src/agents/extension.rs b/crates/goose/src/agents/extension.rs index a9081a3538e7..eefd15769f83 100644 --- a/crates/goose/src/agents/extension.rs +++ b/crates/goose/src/agents/extension.rs @@ -174,10 +174,8 @@ pub enum ExtensionConfig { /// Whether this extension is bundled with Goose #[serde(default)] bundled: Option, - /// Default visibility for tools from this extension #[serde(default = "default_true")] tools_are_visible_default: bool, - /// Per-tool configuration overrides #[serde(default)] tools: HashMap, }, @@ -197,10 +195,8 @@ pub enum ExtensionConfig { /// Whether this extension is bundled with Goose #[serde(default)] bundled: Option, - /// Default visibility for tools from this extension #[serde(default = "default_true")] tools_are_visible_default: bool, - /// Per-tool configuration overrides #[serde(default)] tools: HashMap, }, @@ -215,10 +211,8 @@ pub enum ExtensionConfig { /// Whether this extension is bundled with Goose #[serde(default)] bundled: Option, - /// Default visibility for tools from this extension #[serde(default = "default_true")] tools_are_visible_default: bool, - /// Per-tool configuration overrides #[serde(default)] tools: HashMap, }, @@ -241,10 +235,8 @@ pub enum ExtensionConfig { /// Whether this extension is bundled with Goose #[serde(default)] bundled: Option, - /// Default visibility for tools from this extension #[serde(default = "default_true")] tools_are_visible_default: bool, - /// Per-tool configuration overrides #[serde(default)] tools: HashMap, }, @@ -260,10 +252,8 @@ pub enum ExtensionConfig { /// Whether this extension is bundled with Goose #[serde(default)] bundled: Option, - /// Default visibility for tools from this extension #[serde(default = "default_true")] tools_are_visible_default: bool, - /// Per-tool configuration overrides #[serde(default)] tool_configs: HashMap, }, @@ -281,10 +271,8 @@ pub enum ExtensionConfig { /// Python package dependencies required by this extension #[serde(default)] dependencies: Option>, - /// Default visibility for tools from this extension #[serde(default = "default_true")] tools_are_visible_default: bool, - /// Per-tool configuration overrides #[serde(default)] tools: HashMap, }, @@ -430,20 +418,42 @@ impl ExtensionConfig { /// Check if a tool should be visible to the LLM pub fn is_tool_visible(&self, tool_name: &str) -> bool { match self { - Self::Sse { tools_are_visible_default, tools, .. } - | Self::StreamableHttp { tools_are_visible_default, tools, .. } - | Self::Stdio { tools_are_visible_default, tools, .. } - | Self::Builtin { tools_are_visible_default, tools, .. } - | Self::InlinePython { tools_are_visible_default, tools, .. } => { - tools.get(tool_name) - .map(|config| config.visible) - .unwrap_or(*tools_are_visible_default) + Self::Sse { + tools_are_visible_default, + tools, + .. + } + | Self::StreamableHttp { + tools_are_visible_default, + tools, + .. } - Self::Frontend { tools_are_visible_default, tool_configs, .. } => { - tool_configs.get(tool_name) - .map(|config| config.visible) - .unwrap_or(*tools_are_visible_default) + | Self::Stdio { + tools_are_visible_default, + tools, + .. } + | Self::Builtin { + tools_are_visible_default, + tools, + .. + } + | Self::InlinePython { + tools_are_visible_default, + tools, + .. + } => tools + .get(tool_name) + .map(|config| config.visible) + .unwrap_or(*tools_are_visible_default), + Self::Frontend { + tools_are_visible_default, + tool_configs, + .. + } => tool_configs + .get(tool_name) + .map(|config| config.visible) + .unwrap_or(*tools_are_visible_default), } } } diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index 99dbeeec9f9a..7325e1958537 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -493,11 +493,10 @@ impl ExtensionManager { loop { for tool in client_tools.tools { - // Check if tool should be visible let is_visible = extension_config .as_ref() .map(|config| config.is_tool_visible(&tool.name)) - .unwrap_or(true); // Default to visible if no config + .unwrap_or(true); if is_visible { tools.push(Tool { @@ -1226,7 +1225,9 @@ mod tests { }; let sanitized_name = normalize("test_extension".to_string()); - extension_manager.extension_configs.insert(sanitized_name.clone(), config); + extension_manager + .extension_configs + .insert(sanitized_name.clone(), config); extension_manager.clients.insert( sanitized_name, @@ -1234,7 +1235,7 @@ mod tests { ); let tools = extension_manager.get_prefixed_tools(None).await.unwrap(); - + let tool_names: Vec = tools.iter().map(|t| t.name.to_string()).collect(); assert!(tool_names.iter().any(|name| name.contains("tool"))); // Default visible assert!(tool_names.iter().any(|name| name.contains("visible_tool"))); From d251ad02180f4c1bcfee9bd0d8e9700415cf807a Mon Sep 17 00:00:00 2001 From: Jarrod Sibbison Date: Tue, 12 Aug 2025 12:06:13 +1000 Subject: [PATCH 4/8] update openapi schema --- crates/goose-server/src/openapi.rs | 2 + ui/desktop/openapi.json | 63 ++++++++++++++++++++++++++++++ ui/desktop/src/api/types.gen.ts | 31 +++++++++++++++ 3 files changed, 96 insertions(+) diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index d1c0305239c0..c022cdcf6c03 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -1,4 +1,5 @@ use goose::agents::extension::Envs; +use goose::agents::extension::ToolConfig; use goose::agents::extension::ToolInfo; use goose::agents::ExtensionConfig; use goose::config::permission::PermissionLevel; @@ -428,6 +429,7 @@ impl<'__s> ToSchema<'__s> for AnnotatedSchema { ExtensionConfig, ConfigKey, Envs, + ToolConfig, ToolSchema, ToolAnnotationsSchema, ToolInfo, diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 52642f80c7bf..38c16c89cc3a 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -1485,6 +1485,15 @@ "nullable": true, "minimum": 0 }, + "tools": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ToolConfig" + } + }, + "tools_are_visible_default": { + "type": "boolean" + }, "type": { "type": "string", "enum": [ @@ -1543,6 +1552,15 @@ "nullable": true, "minimum": 0 }, + "tools": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ToolConfig" + } + }, + "tools_are_visible_default": { + "type": "boolean" + }, "type": { "type": "string", "enum": [ @@ -1582,6 +1600,15 @@ "nullable": true, "minimum": 0 }, + "tools": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ToolConfig" + } + }, + "tools_are_visible_default": { + "type": "boolean" + }, "type": { "type": "string", "enum": [ @@ -1633,6 +1660,15 @@ "nullable": true, "minimum": 0 }, + "tools": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ToolConfig" + } + }, + "tools_are_visible_default": { + "type": "boolean" + }, "type": { "type": "string", "enum": [ @@ -1667,6 +1703,12 @@ "type": "string", "description": "The name used to identify this extension" }, + "tool_configs": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ToolConfig" + } + }, "tools": { "type": "array", "items": { @@ -1674,6 +1716,9 @@ }, "description": "The tools provided by the frontend" }, + "tools_are_visible_default": { + "type": "boolean" + }, "type": { "type": "string", "enum": [ @@ -1719,6 +1764,15 @@ "nullable": true, "minimum": 0 }, + "tools": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ToolConfig" + } + }, + "tools_are_visible_default": { + "type": "boolean" + }, "type": { "type": "string", "enum": [ @@ -2998,6 +3052,15 @@ } } }, + "ToolConfig": { + "type": "object", + "properties": { + "visible": { + "type": "boolean", + "description": "Whether this tool is visible to the LLM" + } + } + }, "ToolConfirmationRequest": { "type": "object", "required": [ diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index eeb5a691b218..ce9e483f72dc 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -159,6 +159,10 @@ export type ExtensionConfig = { */ name: string; timeout?: number | null; + tools?: { + [key: string]: ToolConfig; + }; + tools_are_visible_default?: boolean; type: 'sse'; uri: string; } | { @@ -176,6 +180,10 @@ export type ExtensionConfig = { */ name: string; timeout?: number | null; + tools?: { + [key: string]: ToolConfig; + }; + tools_are_visible_default?: boolean; type: 'stdio'; } | { /** @@ -189,6 +197,10 @@ export type ExtensionConfig = { */ name: string; timeout?: number | null; + tools?: { + [key: string]: ToolConfig; + }; + tools_are_visible_default?: boolean; type: 'builtin'; } | { /** @@ -206,6 +218,10 @@ export type ExtensionConfig = { */ name: string; timeout?: number | null; + tools?: { + [key: string]: ToolConfig; + }; + tools_are_visible_default?: boolean; type: 'streamable_http'; uri: string; } | { @@ -221,10 +237,14 @@ export type ExtensionConfig = { * The name used to identify this extension */ name: string; + tool_configs?: { + [key: string]: ToolConfig; + }; /** * The tools provided by the frontend */ tools: Array; + tools_are_visible_default?: boolean; type: 'frontend'; } | { /** @@ -247,6 +267,10 @@ export type ExtensionConfig = { * Timeout in seconds */ timeout?: number | null; + tools?: { + [key: string]: ToolConfig; + }; + tools_are_visible_default?: boolean; type: 'inline_python'; }; @@ -736,6 +760,13 @@ export type ToolAnnotations = { title?: string; }; +export type ToolConfig = { + /** + * Whether this tool is visible to the LLM + */ + visible?: boolean; +}; + export type ToolConfirmationRequest = { arguments: unknown; id: string; From e2e80bb600de9e48168078fcc2079e322725bd65 Mon Sep 17 00:00:00 2001 From: Jarrod Sibbison Date: Thu, 14 Aug 2025 20:48:58 +1000 Subject: [PATCH 5/8] Switch from tools config to available_tools list --- crates/goose-cli/src/commands/configure.rs | 18 +-- .../goose-cli/src/recipes/secret_discovery.rs | 18 +-- crates/goose-cli/src/session/mod.rs | 12 +- crates/goose-server/src/openapi.rs | 2 - crates/goose-server/src/routes/extension.rs | 16 +-- crates/goose/src/agents/agent.rs | 3 +- crates/goose/src/agents/extension.rs | 104 +++++------------- crates/goose/src/agents/extension_manager.rs | 77 +++++++++---- crates/goose/src/config/extensions.rs | 3 +- crates/goose/tests/mcp_integration_test.rs | 1 + ui/desktop/openapi.json | 99 ++++++----------- ui/desktop/src/api/types.gen.ts | 37 +------ 12 files changed, 152 insertions(+), 238 deletions(-) diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index f6c6204039da..939b8eb69e63 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -113,8 +113,7 @@ pub async fn handle_configure() -> Result<(), Box> { timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), bundled: Some(true), description: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), }, })?; } @@ -773,8 +772,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box> { timeout: Some(timeout), bundled: Some(true), description: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), }, })?; @@ -882,8 +880,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box> { description, timeout: Some(timeout), bundled: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), }, })?; @@ -986,8 +983,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box> { description, timeout: Some(timeout), bundled: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), }, })?; @@ -1115,8 +1111,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box> { description, timeout: Some(timeout), bundled: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), }, })?; @@ -1759,8 +1754,7 @@ pub async fn handle_openrouter_auth() -> Result<(), Box> { timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), bundled: Some(true), description: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), }, }) { Ok(_) => println!("✓ Developer extension enabled"), diff --git a/crates/goose-cli/src/recipes/secret_discovery.rs b/crates/goose-cli/src/recipes/secret_discovery.rs index b6a4a0ca99e7..31ed259e6c7f 100644 --- a/crates/goose-cli/src/recipes/secret_discovery.rs +++ b/crates/goose-cli/src/recipes/secret_discovery.rs @@ -143,8 +143,7 @@ mod tests { description: None, timeout: None, bundled: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), }, ExtensionConfig::Stdio { name: "slack-mcp".to_string(), @@ -155,8 +154,7 @@ mod tests { timeout: None, description: None, bundled: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), }, ExtensionConfig::Builtin { name: "builtin-ext".to_string(), @@ -164,8 +162,7 @@ mod tests { description: None, timeout: None, bundled: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), }, ]), context: None, @@ -243,8 +240,7 @@ mod tests { description: None, timeout: None, bundled: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), }, ExtensionConfig::Stdio { name: "service-b".to_string(), @@ -255,8 +251,7 @@ mod tests { timeout: None, description: None, bundled: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), }, ]), context: None, @@ -304,8 +299,7 @@ mod tests { description: None, timeout: None, bundled: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), }]), sub_recipes: Some(vec![SubRecipe { name: "child-recipe".to_string(), diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index cdb492852779..0a75bccd78d1 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -211,8 +211,7 @@ impl Session { // TODO: should set timeout timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), bundled: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), }; self.agent @@ -246,8 +245,7 @@ impl Session { // TODO: should set timeout timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), bundled: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), }; self.agent @@ -282,8 +280,7 @@ impl Session { // TODO: should set timeout timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), bundled: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), }; self.agent @@ -310,8 +307,7 @@ impl Session { timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), bundled: None, description: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), }; self.agent .add_extension(config) diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index c022cdcf6c03..d1c0305239c0 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -1,5 +1,4 @@ use goose::agents::extension::Envs; -use goose::agents::extension::ToolConfig; use goose::agents::extension::ToolInfo; use goose::agents::ExtensionConfig; use goose::config::permission::PermissionLevel; @@ -429,7 +428,6 @@ impl<'__s> ToSchema<'__s> for AnnotatedSchema { ExtensionConfig, ConfigKey, Envs, - ToolConfig, ToolSchema, ToolAnnotationsSchema, ToolInfo, diff --git a/crates/goose-server/src/routes/extension.rs b/crates/goose-server/src/routes/extension.rs index 268b21e25c3c..ddcea98ea6a0 100644 --- a/crates/goose-server/src/routes/extension.rs +++ b/crates/goose-server/src/routes/extension.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::env; use std::path::Path; use std::sync::Arc; @@ -195,8 +194,7 @@ async fn add_extension( description: None, timeout, bundled: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), }, ExtensionConfigRequest::StreamableHttp { name, @@ -214,8 +212,7 @@ async fn add_extension( description: None, timeout, bundled: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), }, ExtensionConfigRequest::Stdio { name, @@ -246,8 +243,7 @@ async fn add_extension( env_keys, timeout, bundled: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), } } ExtensionConfigRequest::Builtin { @@ -260,8 +256,7 @@ async fn add_extension( timeout, bundled: None, description: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), }, ExtensionConfigRequest::Frontend { name, @@ -272,8 +267,7 @@ async fn add_extension( tools, instructions, bundled: None, - tools_are_visible_default: true, - tool_configs: HashMap::new(), + available_tools: Vec::new(), }, }; diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 88e8a9aadb83..f21ffd790df6 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -691,8 +691,7 @@ impl Agent { tools, instructions, bundled: _, - tools_are_visible_default: _, - tool_configs: _, + available_tools: _, } => { // For frontend tools, just store them in the frontend_tools map let mut frontend_tools = self.frontend_tools.lock().await; diff --git a/crates/goose/src/agents/extension.rs b/crates/goose/src/agents/extension.rs index eefd15769f83..74f823cd6aee 100644 --- a/crates/goose/src/agents/extension.rs +++ b/crates/goose/src/agents/extension.rs @@ -61,17 +61,6 @@ pub struct Envs { map: HashMap, } -#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] -pub struct ToolConfig { - /// Whether this tool is visible to the LLM - #[serde(default = "default_true")] - pub visible: bool, -} - -fn default_true() -> bool { - true -} - impl Envs { /// List of sensitive env vars that should not be overridden const DISALLOWED_KEYS: [&'static str; 31] = [ @@ -174,10 +163,8 @@ pub enum ExtensionConfig { /// Whether this extension is bundled with Goose #[serde(default)] bundled: Option, - #[serde(default = "default_true")] - tools_are_visible_default: bool, #[serde(default)] - tools: HashMap, + available_tools: Vec, }, /// Standard I/O client with command and arguments #[serde(rename = "stdio")] @@ -195,10 +182,8 @@ pub enum ExtensionConfig { /// Whether this extension is bundled with Goose #[serde(default)] bundled: Option, - #[serde(default = "default_true")] - tools_are_visible_default: bool, #[serde(default)] - tools: HashMap, + available_tools: Vec, }, /// Built-in extension that is part of the goose binary #[serde(rename = "builtin")] @@ -211,10 +196,8 @@ pub enum ExtensionConfig { /// Whether this extension is bundled with Goose #[serde(default)] bundled: Option, - #[serde(default = "default_true")] - tools_are_visible_default: bool, #[serde(default)] - tools: HashMap, + available_tools: Vec, }, /// Streamable HTTP client with a URI endpoint using MCP Streamable HTTP specification #[serde(rename = "streamable_http")] @@ -235,10 +218,8 @@ pub enum ExtensionConfig { /// Whether this extension is bundled with Goose #[serde(default)] bundled: Option, - #[serde(default = "default_true")] - tools_are_visible_default: bool, #[serde(default)] - tools: HashMap, + available_tools: Vec, }, /// Frontend-provided tools that will be called through the frontend #[serde(rename = "frontend")] @@ -252,10 +233,8 @@ pub enum ExtensionConfig { /// Whether this extension is bundled with Goose #[serde(default)] bundled: Option, - #[serde(default = "default_true")] - tools_are_visible_default: bool, #[serde(default)] - tool_configs: HashMap, + available_tools: Vec, }, /// Inline Python code that will be executed using uvx #[serde(rename = "inline_python")] @@ -271,10 +250,8 @@ pub enum ExtensionConfig { /// Python package dependencies required by this extension #[serde(default)] dependencies: Option>, - #[serde(default = "default_true")] - tools_are_visible_default: bool, #[serde(default)] - tools: HashMap, + available_tools: Vec, }, } @@ -286,8 +263,7 @@ impl Default for ExtensionConfig { description: None, timeout: Some(config::DEFAULT_EXTENSION_TIMEOUT), bundled: Some(true), - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), } } } @@ -302,8 +278,7 @@ impl ExtensionConfig { description: Some(description.into()), timeout: Some(timeout.into()), bundled: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), } } @@ -322,8 +297,7 @@ impl ExtensionConfig { description: Some(description.into()), timeout: Some(timeout.into()), bundled: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), } } @@ -342,8 +316,7 @@ impl ExtensionConfig { description: Some(description.into()), timeout: Some(timeout.into()), bundled: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), } } @@ -359,8 +332,7 @@ impl ExtensionConfig { description: Some(description.into()), timeout: Some(timeout.into()), dependencies: None, - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), } } @@ -378,8 +350,7 @@ impl ExtensionConfig { timeout, description, bundled, - tools_are_visible_default, - tools, + available_tools, .. } => Self::Stdio { name, @@ -390,8 +361,7 @@ impl ExtensionConfig { description, timeout, bundled, - tools_are_visible_default, - tools, + available_tools, }, other => other, } @@ -415,46 +385,32 @@ impl ExtensionConfig { .to_string() } - /// Check if a tool should be visible to the LLM - pub fn is_tool_visible(&self, tool_name: &str) -> bool { - match self { + /// Check if a tool should be available to the LLM + pub fn is_tool_available(&self, tool_name: &str) -> bool { + let available_tools = match self { Self::Sse { - tools_are_visible_default, - tools, - .. + available_tools, .. } | Self::StreamableHttp { - tools_are_visible_default, - tools, - .. + available_tools, .. } | Self::Stdio { - tools_are_visible_default, - tools, - .. + available_tools, .. } | Self::Builtin { - tools_are_visible_default, - tools, - .. + available_tools, .. } | Self::InlinePython { - tools_are_visible_default, - tools, - .. - } => tools - .get(tool_name) - .map(|config| config.visible) - .unwrap_or(*tools_are_visible_default), - Self::Frontend { - tools_are_visible_default, - tool_configs, - .. - } => tool_configs - .get(tool_name) - .map(|config| config.visible) - .unwrap_or(*tools_are_visible_default), - } + available_tools, .. + } + | Self::Frontend { + available_tools, .. + } => available_tools, + }; + + // If no tools are specified, all tools are available + // If tools are specified, only those tools are available + available_tools.is_empty() || available_tools.contains(&tool_name.to_string()) } } diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index 3de1e9daa3e8..0e57baa64169 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -328,8 +328,7 @@ impl ExtensionManager { description: _, timeout, bundled: _, - tools_are_visible_default: _, - tools: _, + available_tools: _, } => { let cmd = std::env::current_exe() .expect("should find the current executable") @@ -481,12 +480,12 @@ impl ExtensionManager { loop { for tool in client_tools.tools { - let is_visible = extension_config + let is_available = extension_config .as_ref() - .map(|config| config.is_tool_visible(&tool.name)) + .map(|config| config.is_tool_available(&tool.name)) .unwrap_or(true); - if is_visible { + if is_available { tools.push(Tool { name: format!("{}__{}", name, tool.name).into(), description: tool.description, @@ -1000,8 +999,8 @@ mod tests { output_schema: None, }, Tool { - name: "visible_tool".into(), - description: Some("A visible tool".into()), + name: "available_tool".into(), + description: Some("An available tool".into()), input_schema: Arc::new(json!({}).as_object().unwrap().clone()), annotations: None, output_schema: None, @@ -1025,7 +1024,7 @@ mod tests { _cancellation_token: CancellationToken, ) -> Result { match name { - "tool" | "test__tool" | "visible_tool" | "hidden_tool" => Ok(CallToolResult { + "tool" | "test__tool" | "available_tool" | "hidden_tool" => Ok(CallToolResult { content: Some(vec![]), is_error: None, structured_content: None, @@ -1216,24 +1215,55 @@ mod tests { } #[tokio::test] - async fn test_tool_visibility_filtering() { - use crate::agents::extension::ToolConfig; - use std::collections::HashMap; - + async fn test_tool_availability_filtering() { let mut extension_manager = ExtensionManager::new(); - let mut tools = HashMap::new(); - tools.insert("visible_tool".to_string(), ToolConfig { visible: true }); - tools.insert("hidden_tool".to_string(), ToolConfig { visible: false }); + // Only "available_tool" should be available to the LLM + let available_tools = vec!["available_tool".to_string()]; + + let config = ExtensionConfig::Builtin { + name: "test_extension".to_string(), + display_name: Some("Test Extension".to_string()), + description: Some("Test extension for available tools".to_string()), + timeout: Some(300), + bundled: Some(true), + available_tools, + }; + + let sanitized_name = normalize("test_extension".to_string()); + extension_manager + .extension_configs + .insert(sanitized_name.clone(), config); + + extension_manager.clients.insert( + sanitized_name, + Arc::new(Mutex::new(Box::new(MockClient {}))), + ); + + let tools = extension_manager.get_prefixed_tools(None).await.unwrap(); + + let tool_names: Vec = tools.iter().map(|t| t.name.to_string()).collect(); + assert!(!tool_names.iter().any(|name| name == "test_extension__tool")); // Default unavailable + assert!(tool_names + .iter() + .any(|name| name == "test_extension__available_tool")); + assert!(!tool_names + .iter() + .any(|name| name == "test_extension__hidden_tool")); + assert!(tool_names.len() == 1); + } + + #[tokio::test] + async fn test_tool_availability_defaults_to_available() { + let mut extension_manager = ExtensionManager::new(); let config = ExtensionConfig::Builtin { name: "test_extension".to_string(), display_name: Some("Test Extension".to_string()), - description: Some("Test extension for visibility".to_string()), + description: Some("Test extension for available tools".to_string()), timeout: Some(300), bundled: Some(true), - tools_are_visible_default: true, - tools, + available_tools: vec![], }; let sanitized_name = normalize("test_extension".to_string()); @@ -1249,8 +1279,13 @@ mod tests { let tools = extension_manager.get_prefixed_tools(None).await.unwrap(); let tool_names: Vec = tools.iter().map(|t| t.name.to_string()).collect(); - assert!(tool_names.iter().any(|name| name.contains("tool"))); // Default visible - assert!(tool_names.iter().any(|name| name.contains("visible_tool"))); - assert!(!tool_names.iter().any(|name| name.contains("hidden_tool"))); + assert!(tool_names.iter().any(|name| name == "test_extension__tool")); + assert!(tool_names + .iter() + .any(|name| name == "test_extension__available_tool")); + assert!(tool_names + .iter() + .any(|name| name == "test_extension__hidden_tool")); + assert!(tool_names.len() == 3); } } diff --git a/crates/goose/src/config/extensions.rs b/crates/goose/src/config/extensions.rs index bbb4a935a77d..b03415752306 100644 --- a/crates/goose/src/config/extensions.rs +++ b/crates/goose/src/config/extensions.rs @@ -47,8 +47,7 @@ impl ExtensionConfigManager { timeout: Some(DEFAULT_EXTENSION_TIMEOUT), bundled: Some(true), description: Some(DEFAULT_EXTENSION_DESCRIPTION.to_string()), - tools_are_visible_default: true, - tools: HashMap::new(), + available_tools: Vec::new(), }, }, )]); diff --git a/crates/goose/tests/mcp_integration_test.rs b/crates/goose/tests/mcp_integration_test.rs index 36cd190fe577..bba17fc6844b 100644 --- a/crates/goose/tests/mcp_integration_test.rs +++ b/crates/goose/tests/mcp_integration_test.rs @@ -123,6 +123,7 @@ async fn test_replayed_session( env_keys: vec![], timeout: Some(30), bundled: Some(false), + available_tools: vec![], }; let mut extension_manager = ExtensionManager::new(); diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 009d345bf6ce..43959cbe8eee 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -1457,6 +1457,12 @@ "type" ], "properties": { + "available_tools": { + "type": "array", + "items": { + "type": "string" + } + }, "bundled": { "type": "boolean", "description": "Whether this extension is bundled with Goose", @@ -1485,15 +1491,6 @@ "nullable": true, "minimum": 0 }, - "tools": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/ToolConfig" - } - }, - "tools_are_visible_default": { - "type": "boolean" - }, "type": { "type": "string", "enum": [ @@ -1521,6 +1518,12 @@ "type": "string" } }, + "available_tools": { + "type": "array", + "items": { + "type": "string" + } + }, "bundled": { "type": "boolean", "description": "Whether this extension is bundled with Goose", @@ -1552,15 +1555,6 @@ "nullable": true, "minimum": 0 }, - "tools": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/ToolConfig" - } - }, - "tools_are_visible_default": { - "type": "boolean" - }, "type": { "type": "string", "enum": [ @@ -1577,6 +1571,12 @@ "type" ], "properties": { + "available_tools": { + "type": "array", + "items": { + "type": "string" + } + }, "bundled": { "type": "boolean", "description": "Whether this extension is bundled with Goose", @@ -1600,15 +1600,6 @@ "nullable": true, "minimum": 0 }, - "tools": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/ToolConfig" - } - }, - "tools_are_visible_default": { - "type": "boolean" - }, "type": { "type": "string", "enum": [ @@ -1626,6 +1617,12 @@ "type" ], "properties": { + "available_tools": { + "type": "array", + "items": { + "type": "string" + } + }, "bundled": { "type": "boolean", "description": "Whether this extension is bundled with Goose", @@ -1660,15 +1657,6 @@ "nullable": true, "minimum": 0 }, - "tools": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/ToolConfig" - } - }, - "tools_are_visible_default": { - "type": "boolean" - }, "type": { "type": "string", "enum": [ @@ -1689,6 +1677,12 @@ "type" ], "properties": { + "available_tools": { + "type": "array", + "items": { + "type": "string" + } + }, "bundled": { "type": "boolean", "description": "Whether this extension is bundled with Goose", @@ -1703,12 +1697,6 @@ "type": "string", "description": "The name used to identify this extension" }, - "tool_configs": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/ToolConfig" - } - }, "tools": { "type": "array", "items": { @@ -1716,9 +1704,6 @@ }, "description": "The tools provided by the frontend" }, - "tools_are_visible_default": { - "type": "boolean" - }, "type": { "type": "string", "enum": [ @@ -1736,6 +1721,12 @@ "type" ], "properties": { + "available_tools": { + "type": "array", + "items": { + "type": "string" + } + }, "code": { "type": "string", "description": "The Python code to execute" @@ -1764,15 +1755,6 @@ "nullable": true, "minimum": 0 }, - "tools": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/ToolConfig" - } - }, - "tools_are_visible_default": { - "type": "boolean" - }, "type": { "type": "string", "enum": [ @@ -3052,15 +3034,6 @@ } } }, - "ToolConfig": { - "type": "object", - "properties": { - "visible": { - "type": "boolean", - "description": "Whether this tool is visible to the LLM" - } - } - }, "ToolConfirmationRequest": { "type": "object", "required": [ diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index ce9e483f72dc..0ef21393f03a 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -147,6 +147,7 @@ export type Envs = { * Represents the different types of MCP extensions that can be added to the manager */ export type ExtensionConfig = { + available_tools?: Array; /** * Whether this extension is bundled with Goose */ @@ -159,14 +160,11 @@ export type ExtensionConfig = { */ name: string; timeout?: number | null; - tools?: { - [key: string]: ToolConfig; - }; - tools_are_visible_default?: boolean; type: 'sse'; uri: string; } | { args: Array; + available_tools?: Array; /** * Whether this extension is bundled with Goose */ @@ -180,12 +178,9 @@ export type ExtensionConfig = { */ name: string; timeout?: number | null; - tools?: { - [key: string]: ToolConfig; - }; - tools_are_visible_default?: boolean; type: 'stdio'; } | { + available_tools?: Array; /** * Whether this extension is bundled with Goose */ @@ -197,12 +192,9 @@ export type ExtensionConfig = { */ name: string; timeout?: number | null; - tools?: { - [key: string]: ToolConfig; - }; - tools_are_visible_default?: boolean; type: 'builtin'; } | { + available_tools?: Array; /** * Whether this extension is bundled with Goose */ @@ -218,13 +210,10 @@ export type ExtensionConfig = { */ name: string; timeout?: number | null; - tools?: { - [key: string]: ToolConfig; - }; - tools_are_visible_default?: boolean; type: 'streamable_http'; uri: string; } | { + available_tools?: Array; /** * Whether this extension is bundled with Goose */ @@ -237,16 +226,13 @@ export type ExtensionConfig = { * The name used to identify this extension */ name: string; - tool_configs?: { - [key: string]: ToolConfig; - }; /** * The tools provided by the frontend */ tools: Array; - tools_are_visible_default?: boolean; type: 'frontend'; } | { + available_tools?: Array; /** * The Python code to execute */ @@ -267,10 +253,6 @@ export type ExtensionConfig = { * Timeout in seconds */ timeout?: number | null; - tools?: { - [key: string]: ToolConfig; - }; - tools_are_visible_default?: boolean; type: 'inline_python'; }; @@ -760,13 +742,6 @@ export type ToolAnnotations = { title?: string; }; -export type ToolConfig = { - /** - * Whether this tool is visible to the LLM - */ - visible?: boolean; -}; - export type ToolConfirmationRequest = { arguments: unknown; id: string; From 203481b4a94b48a1531486a98c576e266ec06d89 Mon Sep 17 00:00:00 2001 From: Jarrod Sibbison Date: Thu, 14 Aug 2025 21:09:35 +1000 Subject: [PATCH 6/8] Error if llm guesses an unavailable tool name --- crates/goose/src/agents/extension_manager.rs | 67 ++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index 0e57baa64169..077ecea16544 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -756,6 +756,16 @@ impl ExtensionManager { })? .to_string(); + if let Some(extension_config) = self.extension_configs.get(client_name) { + if !extension_config.is_tool_available(&tool_name) { + return Err(ErrorData::new( + ErrorCode::RESOURCE_NOT_FOUND, + format!("Tool '{}' is not available for extension '{}'", tool_name, client_name), + None, + ).into()); + } + } + let arguments = tool_call.arguments.clone(); let client = client.clone(); let notifications_receiver = client.lock().await.subscribe().await; @@ -1288,4 +1298,61 @@ mod tests { .any(|name| name == "test_extension__hidden_tool")); assert!(tool_names.len() == 3); } + + #[tokio::test] + async fn test_dispatch_unavailable_tool_returns_error() { + let mut extension_manager = ExtensionManager::new(); + + let available_tools = vec!["available_tool".to_string()]; + + let config = ExtensionConfig::Builtin { + name: "test_extension".to_string(), + display_name: Some("Test Extension".to_string()), + description: Some("Test extension for tool dispatch".to_string()), + timeout: Some(300), + bundled: Some(true), + available_tools, + }; + + let sanitized_name = normalize("test_extension".to_string()); + extension_manager + .extension_configs + .insert(sanitized_name.clone(), config); + + extension_manager.clients.insert( + sanitized_name, + Arc::new(Mutex::new(Box::new(MockClient {}))), + ); + + // Try to call an unavailable tool + let unavailable_tool_call = ToolCall { + name: "test_extension__tool".to_string(), + arguments: json!({}), + }; + + let result = extension_manager + .dispatch_tool_call(unavailable_tool_call, CancellationToken::default()) + .await; + + // Should return RESOURCE_NOT_FOUND error + if let Err(err) = result { + let tool_err = err.downcast_ref::().expect("Expected ErrorData"); + assert_eq!(tool_err.code, ErrorCode::RESOURCE_NOT_FOUND); + assert!(tool_err.message.contains("is not available")); + } else { + panic!("Expected ErrorData with ErrorCode::RESOURCE_NOT_FOUND"); + } + + // Try to call an available tool - should succeed + let available_tool_call = ToolCall { + name: "test_extension__available_tool".to_string(), + arguments: json!({}), + }; + + let result = extension_manager + .dispatch_tool_call(available_tool_call, CancellationToken::default()) + .await; + + assert!(result.is_ok()); + } } From 5fe212ad2ccd3d2408c801a5ff52253e530766bd Mon Sep 17 00:00:00 2001 From: Jarrod Sibbison Date: Thu, 14 Aug 2025 21:11:41 +1000 Subject: [PATCH 7/8] format --- crates/goose/src/agents/extension_manager.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index 077ecea16544..2c88ee3d9668 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -760,9 +760,13 @@ impl ExtensionManager { if !extension_config.is_tool_available(&tool_name) { return Err(ErrorData::new( ErrorCode::RESOURCE_NOT_FOUND, - format!("Tool '{}' is not available for extension '{}'", tool_name, client_name), + format!( + "Tool '{}' is not available for extension '{}'", + tool_name, client_name + ), None, - ).into()); + ) + .into()); } } From 64bf5a7d5534f15637733eb79820a1c8c858bcfb Mon Sep 17 00:00:00 2001 From: Jarrod Sibbison Date: Mon, 18 Aug 2025 15:31:47 +1000 Subject: [PATCH 8/8] Update doco for available_tools --- documentation/docs/guides/recipes/recipe-reference.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/documentation/docs/guides/recipes/recipe-reference.md b/documentation/docs/guides/recipes/recipe-reference.md index 44337facd111..4e918ed4d1ca 100644 --- a/documentation/docs/guides/recipes/recipe-reference.md +++ b/documentation/docs/guides/recipes/recipe-reference.md @@ -144,6 +144,7 @@ The `extensions` field allows you to specify which Model Context Protocol (MCP) | `timeout` | Number | Timeout in seconds | | `bundled` | Boolean | (Optional) Whether the extension is bundled with Goose | | `description` | String | Description of what the extension does | +| `available_tools` | Array | List of tool names within the extension that will be available. When not specified all will be available | ### Example Extension Configuration @@ -164,6 +165,8 @@ extensions: cmd: uvx args: - 'mcp_presidio@latest' + available_tools: + - query_logs - type: stdio name: github-mcp