Recipe config to limit tool availability (#4020)
This commit is contained in:
@@ -113,6 +113,7 @@ pub async fn handle_configure() -> Result<(), Box<dyn Error>> {
|
|||||||
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
|
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
|
||||||
bundled: Some(true),
|
bundled: Some(true),
|
||||||
description: None,
|
description: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
},
|
},
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
@@ -771,6 +772,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
|
|||||||
timeout: Some(timeout),
|
timeout: Some(timeout),
|
||||||
bundled: Some(true),
|
bundled: Some(true),
|
||||||
description: None,
|
description: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
},
|
},
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -878,6 +880,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
|
|||||||
description,
|
description,
|
||||||
timeout: Some(timeout),
|
timeout: Some(timeout),
|
||||||
bundled: None,
|
bundled: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
},
|
},
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -980,6 +983,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
|
|||||||
description,
|
description,
|
||||||
timeout: Some(timeout),
|
timeout: Some(timeout),
|
||||||
bundled: None,
|
bundled: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
},
|
},
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -1107,6 +1111,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
|
|||||||
description,
|
description,
|
||||||
timeout: Some(timeout),
|
timeout: Some(timeout),
|
||||||
bundled: None,
|
bundled: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
},
|
},
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -1736,6 +1741,7 @@ pub async fn handle_openrouter_auth() -> Result<(), Box<dyn Error>> {
|
|||||||
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
|
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
|
||||||
bundled: Some(true),
|
bundled: Some(true),
|
||||||
description: None,
|
description: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
},
|
},
|
||||||
}) {
|
}) {
|
||||||
Ok(_) => println!("✓ Developer extension enabled"),
|
Ok(_) => println!("✓ Developer extension enabled"),
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ mod tests {
|
|||||||
description: None,
|
description: None,
|
||||||
timeout: None,
|
timeout: None,
|
||||||
bundled: None,
|
bundled: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
},
|
},
|
||||||
ExtensionConfig::Stdio {
|
ExtensionConfig::Stdio {
|
||||||
name: "slack-mcp".to_string(),
|
name: "slack-mcp".to_string(),
|
||||||
@@ -153,6 +154,7 @@ mod tests {
|
|||||||
timeout: None,
|
timeout: None,
|
||||||
description: None,
|
description: None,
|
||||||
bundled: None,
|
bundled: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
},
|
},
|
||||||
ExtensionConfig::Builtin {
|
ExtensionConfig::Builtin {
|
||||||
name: "builtin-ext".to_string(),
|
name: "builtin-ext".to_string(),
|
||||||
@@ -160,6 +162,7 @@ mod tests {
|
|||||||
description: None,
|
description: None,
|
||||||
timeout: None,
|
timeout: None,
|
||||||
bundled: None,
|
bundled: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
context: None,
|
context: None,
|
||||||
@@ -237,6 +240,7 @@ mod tests {
|
|||||||
description: None,
|
description: None,
|
||||||
timeout: None,
|
timeout: None,
|
||||||
bundled: None,
|
bundled: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
},
|
},
|
||||||
ExtensionConfig::Stdio {
|
ExtensionConfig::Stdio {
|
||||||
name: "service-b".to_string(),
|
name: "service-b".to_string(),
|
||||||
@@ -247,6 +251,7 @@ mod tests {
|
|||||||
timeout: None,
|
timeout: None,
|
||||||
description: None,
|
description: None,
|
||||||
bundled: None,
|
bundled: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
context: None,
|
context: None,
|
||||||
@@ -294,6 +299,7 @@ mod tests {
|
|||||||
description: None,
|
description: None,
|
||||||
timeout: None,
|
timeout: None,
|
||||||
bundled: None,
|
bundled: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
}]),
|
}]),
|
||||||
sub_recipes: Some(vec![SubRecipe {
|
sub_recipes: Some(vec![SubRecipe {
|
||||||
name: "child-recipe".to_string(),
|
name: "child-recipe".to_string(),
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ impl Session {
|
|||||||
// TODO: should set timeout
|
// TODO: should set timeout
|
||||||
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
|
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
|
||||||
bundled: None,
|
bundled: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.agent
|
self.agent
|
||||||
@@ -244,6 +245,7 @@ impl Session {
|
|||||||
// TODO: should set timeout
|
// TODO: should set timeout
|
||||||
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
|
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
|
||||||
bundled: None,
|
bundled: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.agent
|
self.agent
|
||||||
@@ -278,6 +280,7 @@ impl Session {
|
|||||||
// TODO: should set timeout
|
// TODO: should set timeout
|
||||||
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
|
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
|
||||||
bundled: None,
|
bundled: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.agent
|
self.agent
|
||||||
@@ -304,6 +307,7 @@ impl Session {
|
|||||||
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
|
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
|
||||||
bundled: None,
|
bundled: None,
|
||||||
description: None,
|
description: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
};
|
};
|
||||||
self.agent
|
self.agent
|
||||||
.add_extension(config)
|
.add_extension(config)
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ async fn add_extension(
|
|||||||
description: None,
|
description: None,
|
||||||
timeout,
|
timeout,
|
||||||
bundled: None,
|
bundled: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
},
|
},
|
||||||
ExtensionConfigRequest::StreamableHttp {
|
ExtensionConfigRequest::StreamableHttp {
|
||||||
name,
|
name,
|
||||||
@@ -211,6 +212,7 @@ async fn add_extension(
|
|||||||
description: None,
|
description: None,
|
||||||
timeout,
|
timeout,
|
||||||
bundled: None,
|
bundled: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
},
|
},
|
||||||
ExtensionConfigRequest::Stdio {
|
ExtensionConfigRequest::Stdio {
|
||||||
name,
|
name,
|
||||||
@@ -241,6 +243,7 @@ async fn add_extension(
|
|||||||
env_keys,
|
env_keys,
|
||||||
timeout,
|
timeout,
|
||||||
bundled: None,
|
bundled: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ExtensionConfigRequest::Builtin {
|
ExtensionConfigRequest::Builtin {
|
||||||
@@ -253,6 +256,7 @@ async fn add_extension(
|
|||||||
timeout,
|
timeout,
|
||||||
bundled: None,
|
bundled: None,
|
||||||
description: None,
|
description: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
},
|
},
|
||||||
ExtensionConfigRequest::Frontend {
|
ExtensionConfigRequest::Frontend {
|
||||||
name,
|
name,
|
||||||
@@ -263,6 +267,7 @@ async fn add_extension(
|
|||||||
tools,
|
tools,
|
||||||
instructions,
|
instructions,
|
||||||
bundled: None,
|
bundled: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -686,6 +686,7 @@ impl Agent {
|
|||||||
tools,
|
tools,
|
||||||
instructions,
|
instructions,
|
||||||
bundled: _,
|
bundled: _,
|
||||||
|
available_tools: _,
|
||||||
} => {
|
} => {
|
||||||
// For frontend tools, just store them in the frontend_tools map
|
// For frontend tools, just store them in the frontend_tools map
|
||||||
let mut frontend_tools = self.frontend_tools.lock().await;
|
let mut frontend_tools = self.frontend_tools.lock().await;
|
||||||
|
|||||||
@@ -163,6 +163,8 @@ pub enum ExtensionConfig {
|
|||||||
/// Whether this extension is bundled with Goose
|
/// Whether this extension is bundled with Goose
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
bundled: Option<bool>,
|
bundled: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
available_tools: Vec<String>,
|
||||||
},
|
},
|
||||||
/// Standard I/O client with command and arguments
|
/// Standard I/O client with command and arguments
|
||||||
#[serde(rename = "stdio")]
|
#[serde(rename = "stdio")]
|
||||||
@@ -180,6 +182,8 @@ pub enum ExtensionConfig {
|
|||||||
/// Whether this extension is bundled with Goose
|
/// Whether this extension is bundled with Goose
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
bundled: Option<bool>,
|
bundled: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
available_tools: Vec<String>,
|
||||||
},
|
},
|
||||||
/// Built-in extension that is part of the goose binary
|
/// Built-in extension that is part of the goose binary
|
||||||
#[serde(rename = "builtin")]
|
#[serde(rename = "builtin")]
|
||||||
@@ -192,6 +196,8 @@ pub enum ExtensionConfig {
|
|||||||
/// Whether this extension is bundled with Goose
|
/// Whether this extension is bundled with Goose
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
bundled: Option<bool>,
|
bundled: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
available_tools: Vec<String>,
|
||||||
},
|
},
|
||||||
/// Streamable HTTP client with a URI endpoint using MCP Streamable HTTP specification
|
/// Streamable HTTP client with a URI endpoint using MCP Streamable HTTP specification
|
||||||
#[serde(rename = "streamable_http")]
|
#[serde(rename = "streamable_http")]
|
||||||
@@ -212,6 +218,8 @@ pub enum ExtensionConfig {
|
|||||||
/// Whether this extension is bundled with Goose
|
/// Whether this extension is bundled with Goose
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
bundled: Option<bool>,
|
bundled: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
available_tools: Vec<String>,
|
||||||
},
|
},
|
||||||
/// Frontend-provided tools that will be called through the frontend
|
/// Frontend-provided tools that will be called through the frontend
|
||||||
#[serde(rename = "frontend")]
|
#[serde(rename = "frontend")]
|
||||||
@@ -225,6 +233,8 @@ pub enum ExtensionConfig {
|
|||||||
/// Whether this extension is bundled with Goose
|
/// Whether this extension is bundled with Goose
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
bundled: Option<bool>,
|
bundled: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
available_tools: Vec<String>,
|
||||||
},
|
},
|
||||||
/// Inline Python code that will be executed using uvx
|
/// Inline Python code that will be executed using uvx
|
||||||
#[serde(rename = "inline_python")]
|
#[serde(rename = "inline_python")]
|
||||||
@@ -240,6 +250,8 @@ pub enum ExtensionConfig {
|
|||||||
/// Python package dependencies required by this extension
|
/// Python package dependencies required by this extension
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
dependencies: Option<Vec<String>>,
|
dependencies: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
available_tools: Vec<String>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +263,7 @@ impl Default for ExtensionConfig {
|
|||||||
description: None,
|
description: None,
|
||||||
timeout: Some(config::DEFAULT_EXTENSION_TIMEOUT),
|
timeout: Some(config::DEFAULT_EXTENSION_TIMEOUT),
|
||||||
bundled: Some(true),
|
bundled: Some(true),
|
||||||
|
available_tools: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,6 +278,7 @@ impl ExtensionConfig {
|
|||||||
description: Some(description.into()),
|
description: Some(description.into()),
|
||||||
timeout: Some(timeout.into()),
|
timeout: Some(timeout.into()),
|
||||||
bundled: None,
|
bundled: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,6 +297,7 @@ impl ExtensionConfig {
|
|||||||
description: Some(description.into()),
|
description: Some(description.into()),
|
||||||
timeout: Some(timeout.into()),
|
timeout: Some(timeout.into()),
|
||||||
bundled: None,
|
bundled: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,6 +316,7 @@ impl ExtensionConfig {
|
|||||||
description: Some(description.into()),
|
description: Some(description.into()),
|
||||||
timeout: Some(timeout.into()),
|
timeout: Some(timeout.into()),
|
||||||
bundled: None,
|
bundled: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,6 +332,7 @@ impl ExtensionConfig {
|
|||||||
description: Some(description.into()),
|
description: Some(description.into()),
|
||||||
timeout: Some(timeout.into()),
|
timeout: Some(timeout.into()),
|
||||||
dependencies: None,
|
dependencies: None,
|
||||||
|
available_tools: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,6 +350,7 @@ impl ExtensionConfig {
|
|||||||
timeout,
|
timeout,
|
||||||
description,
|
description,
|
||||||
bundled,
|
bundled,
|
||||||
|
available_tools,
|
||||||
..
|
..
|
||||||
} => Self::Stdio {
|
} => Self::Stdio {
|
||||||
name,
|
name,
|
||||||
@@ -343,6 +361,7 @@ impl ExtensionConfig {
|
|||||||
description,
|
description,
|
||||||
timeout,
|
timeout,
|
||||||
bundled,
|
bundled,
|
||||||
|
available_tools,
|
||||||
},
|
},
|
||||||
other => other,
|
other => other,
|
||||||
}
|
}
|
||||||
@@ -365,6 +384,34 @@ impl ExtensionConfig {
|
|||||||
}
|
}
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
available_tools, ..
|
||||||
|
}
|
||||||
|
| Self::StreamableHttp {
|
||||||
|
available_tools, ..
|
||||||
|
}
|
||||||
|
| Self::Stdio {
|
||||||
|
available_tools, ..
|
||||||
|
}
|
||||||
|
| Self::Builtin {
|
||||||
|
available_tools, ..
|
||||||
|
}
|
||||||
|
| Self::InlinePython {
|
||||||
|
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for ExtensionConfig {
|
impl std::fmt::Display for ExtensionConfig {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ pub struct ExtensionManager {
|
|||||||
instructions: HashMap<String, String>,
|
instructions: HashMap<String, String>,
|
||||||
resource_capable_extensions: HashSet<String>,
|
resource_capable_extensions: HashSet<String>,
|
||||||
temp_dirs: HashMap<String, tempfile::TempDir>,
|
temp_dirs: HashMap<String, tempfile::TempDir>,
|
||||||
|
extension_configs: HashMap<String, ExtensionConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A flattened representation of a resource used by the agent to prepare inference
|
/// A flattened representation of a resource used by the agent to prepare inference
|
||||||
@@ -155,6 +156,7 @@ impl ExtensionManager {
|
|||||||
instructions: HashMap::new(),
|
instructions: HashMap::new(),
|
||||||
resource_capable_extensions: HashSet::new(),
|
resource_capable_extensions: HashSet::new(),
|
||||||
temp_dirs: HashMap::new(),
|
temp_dirs: HashMap::new(),
|
||||||
|
extension_configs: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,6 +335,7 @@ impl ExtensionManager {
|
|||||||
description: _,
|
description: _,
|
||||||
timeout,
|
timeout,
|
||||||
bundled: _,
|
bundled: _,
|
||||||
|
available_tools: _,
|
||||||
} => {
|
} => {
|
||||||
let cmd = std::env::current_exe()
|
let cmd = std::env::current_exe()
|
||||||
.expect("should find the current executable")
|
.expect("should find the current executable")
|
||||||
@@ -385,7 +388,8 @@ impl ExtensionManager {
|
|||||||
.insert(sanitized_name.clone());
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,6 +419,7 @@ impl ExtensionManager {
|
|||||||
self.instructions.remove(&sanitized_name);
|
self.instructions.remove(&sanitized_name);
|
||||||
self.resource_capable_extensions.remove(&sanitized_name);
|
self.resource_capable_extensions.remove(&sanitized_name);
|
||||||
self.temp_dirs.remove(&sanitized_name);
|
self.temp_dirs.remove(&sanitized_name);
|
||||||
|
self.extension_configs.remove(&sanitized_name);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,6 +476,7 @@ impl ExtensionManager {
|
|||||||
let client_futures = filtered_clients.map(|(name, client)| {
|
let client_futures = filtered_clients.map(|(name, client)| {
|
||||||
let name = name.clone();
|
let name = name.clone();
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
|
let extension_config = self.extension_configs.get(&name).cloned();
|
||||||
|
|
||||||
task::spawn(async move {
|
task::spawn(async move {
|
||||||
let mut tools = Vec::new();
|
let mut tools = Vec::new();
|
||||||
@@ -481,13 +487,20 @@ impl ExtensionManager {
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
for tool in client_tools.tools {
|
for tool in client_tools.tools {
|
||||||
tools.push(Tool {
|
let is_available = extension_config
|
||||||
name: format!("{}__{}", name, tool.name).into(),
|
.as_ref()
|
||||||
description: tool.description,
|
.map(|config| config.is_tool_available(&tool.name))
|
||||||
input_schema: tool.input_schema,
|
.unwrap_or(true);
|
||||||
annotations: tool.annotations,
|
|
||||||
output_schema: tool.output_schema,
|
if is_available {
|
||||||
});
|
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
|
// Exit loop when there are no more pages
|
||||||
@@ -750,6 +763,20 @@ impl ExtensionManager {
|
|||||||
})?
|
})?
|
||||||
.to_string();
|
.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 arguments = tool_call.arguments.clone();
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
let notifications_receiver = client.lock().await.subscribe().await;
|
let notifications_receiver = client.lock().await.subscribe().await;
|
||||||
@@ -981,7 +1008,34 @@ mod tests {
|
|||||||
_next_cursor: Option<String>,
|
_next_cursor: Option<String>,
|
||||||
_cancellation_token: CancellationToken,
|
_cancellation_token: CancellationToken,
|
||||||
) -> Result<ListToolsResult, Error> {
|
) -> Result<ListToolsResult, Error> {
|
||||||
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: "available_tool".into(),
|
||||||
|
description: Some("An available 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(
|
async fn call_tool(
|
||||||
@@ -991,7 +1045,7 @@ mod tests {
|
|||||||
_cancellation_token: CancellationToken,
|
_cancellation_token: CancellationToken,
|
||||||
) -> Result<CallToolResult, Error> {
|
) -> Result<CallToolResult, Error> {
|
||||||
match name {
|
match name {
|
||||||
"tool" | "test__tool" => Ok(CallToolResult {
|
"tool" | "test__tool" | "available_tool" | "hidden_tool" => Ok(CallToolResult {
|
||||||
content: Some(vec![]),
|
content: Some(vec![]),
|
||||||
is_error: None,
|
is_error: None,
|
||||||
structured_content: None,
|
structured_content: None,
|
||||||
@@ -1180,4 +1234,136 @@ mod tests {
|
|||||||
panic!("Expected ErrorData with ErrorCode::RESOURCE_NOT_FOUND");
|
panic!("Expected ErrorData with ErrorCode::RESOURCE_NOT_FOUND");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_tool_availability_filtering() {
|
||||||
|
let mut extension_manager = ExtensionManager::new();
|
||||||
|
|
||||||
|
// 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<String> = 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 available tools".to_string()),
|
||||||
|
timeout: Some(300),
|
||||||
|
bundled: Some(true),
|
||||||
|
available_tools: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
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<String> = tools.iter().map(|t| t.name.to_string()).collect();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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::<ErrorData>().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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ impl ExtensionConfigManager {
|
|||||||
timeout: Some(DEFAULT_EXTENSION_TIMEOUT),
|
timeout: Some(DEFAULT_EXTENSION_TIMEOUT),
|
||||||
bundled: Some(true),
|
bundled: Some(true),
|
||||||
description: Some(DEFAULT_EXTENSION_DESCRIPTION.to_string()),
|
description: Some(DEFAULT_EXTENSION_DESCRIPTION.to_string()),
|
||||||
|
available_tools: Vec::new(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)]);
|
)]);
|
||||||
|
|||||||
@@ -685,6 +685,7 @@ sub_recipes:
|
|||||||
description,
|
description,
|
||||||
timeout,
|
timeout,
|
||||||
dependencies,
|
dependencies,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
assert_eq!(name, "test_python");
|
assert_eq!(name, "test_python");
|
||||||
assert_eq!(code, "print('hello world')");
|
assert_eq!(code, "print('hello world')");
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ async fn test_replayed_session(
|
|||||||
env_keys: vec![],
|
env_keys: vec![],
|
||||||
timeout: Some(30),
|
timeout: Some(30),
|
||||||
bundled: Some(false),
|
bundled: Some(false),
|
||||||
|
available_tools: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut extension_manager = ExtensionManager::new();
|
let mut extension_manager = ExtensionManager::new();
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ The `extensions` field allows you to specify which Model Context Protocol (MCP)
|
|||||||
| `timeout` | Number | Timeout in seconds |
|
| `timeout` | Number | Timeout in seconds |
|
||||||
| `bundled` | Boolean | (Optional) Whether the extension is bundled with Goose |
|
| `bundled` | Boolean | (Optional) Whether the extension is bundled with Goose |
|
||||||
| `description` | String | Description of what the extension does |
|
| `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
|
### Example Extension Configuration
|
||||||
|
|
||||||
@@ -164,6 +165,8 @@ extensions:
|
|||||||
cmd: uvx
|
cmd: uvx
|
||||||
args:
|
args:
|
||||||
- 'mcp_presidio@latest'
|
- 'mcp_presidio@latest'
|
||||||
|
available_tools:
|
||||||
|
- query_logs
|
||||||
|
|
||||||
- type: stdio
|
- type: stdio
|
||||||
name: github-mcp
|
name: github-mcp
|
||||||
|
|||||||
@@ -1632,6 +1632,12 @@
|
|||||||
"type"
|
"type"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"available_tools": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
"bundled": {
|
"bundled": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Whether this extension is bundled with Goose",
|
"description": "Whether this extension is bundled with Goose",
|
||||||
@@ -1687,6 +1693,12 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"available_tools": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
"bundled": {
|
"bundled": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Whether this extension is bundled with Goose",
|
"description": "Whether this extension is bundled with Goose",
|
||||||
@@ -1734,6 +1746,12 @@
|
|||||||
"type"
|
"type"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"available_tools": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
"bundled": {
|
"bundled": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Whether this extension is bundled with Goose",
|
"description": "Whether this extension is bundled with Goose",
|
||||||
@@ -1774,6 +1792,12 @@
|
|||||||
"type"
|
"type"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"available_tools": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
"bundled": {
|
"bundled": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Whether this extension is bundled with Goose",
|
"description": "Whether this extension is bundled with Goose",
|
||||||
@@ -1828,6 +1852,12 @@
|
|||||||
"type"
|
"type"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"available_tools": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
"bundled": {
|
"bundled": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Whether this extension is bundled with Goose",
|
"description": "Whether this extension is bundled with Goose",
|
||||||
@@ -1866,6 +1896,12 @@
|
|||||||
"type"
|
"type"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"available_tools": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
"code": {
|
"code": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The Python code to execute"
|
"description": "The Python code to execute"
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ export type ExtendPromptResponse = {
|
|||||||
* Represents the different types of MCP extensions that can be added to the manager
|
* Represents the different types of MCP extensions that can be added to the manager
|
||||||
*/
|
*/
|
||||||
export type ExtensionConfig = {
|
export type ExtensionConfig = {
|
||||||
|
available_tools?: Array<string>;
|
||||||
/**
|
/**
|
||||||
* Whether this extension is bundled with Goose
|
* Whether this extension is bundled with Goose
|
||||||
*/
|
*/
|
||||||
@@ -175,6 +176,7 @@ export type ExtensionConfig = {
|
|||||||
uri: string;
|
uri: string;
|
||||||
} | {
|
} | {
|
||||||
args: Array<string>;
|
args: Array<string>;
|
||||||
|
available_tools?: Array<string>;
|
||||||
/**
|
/**
|
||||||
* Whether this extension is bundled with Goose
|
* Whether this extension is bundled with Goose
|
||||||
*/
|
*/
|
||||||
@@ -190,6 +192,7 @@ export type ExtensionConfig = {
|
|||||||
timeout?: number | null;
|
timeout?: number | null;
|
||||||
type: 'stdio';
|
type: 'stdio';
|
||||||
} | {
|
} | {
|
||||||
|
available_tools?: Array<string>;
|
||||||
/**
|
/**
|
||||||
* Whether this extension is bundled with Goose
|
* Whether this extension is bundled with Goose
|
||||||
*/
|
*/
|
||||||
@@ -203,6 +206,7 @@ export type ExtensionConfig = {
|
|||||||
timeout?: number | null;
|
timeout?: number | null;
|
||||||
type: 'builtin';
|
type: 'builtin';
|
||||||
} | {
|
} | {
|
||||||
|
available_tools?: Array<string>;
|
||||||
/**
|
/**
|
||||||
* Whether this extension is bundled with Goose
|
* Whether this extension is bundled with Goose
|
||||||
*/
|
*/
|
||||||
@@ -221,6 +225,7 @@ export type ExtensionConfig = {
|
|||||||
type: 'streamable_http';
|
type: 'streamable_http';
|
||||||
uri: string;
|
uri: string;
|
||||||
} | {
|
} | {
|
||||||
|
available_tools?: Array<string>;
|
||||||
/**
|
/**
|
||||||
* Whether this extension is bundled with Goose
|
* Whether this extension is bundled with Goose
|
||||||
*/
|
*/
|
||||||
@@ -239,6 +244,7 @@ export type ExtensionConfig = {
|
|||||||
tools: Array<Tool>;
|
tools: Array<Tool>;
|
||||||
type: 'frontend';
|
type: 'frontend';
|
||||||
} | {
|
} | {
|
||||||
|
available_tools?: Array<string>;
|
||||||
/**
|
/**
|
||||||
* The Python code to execute
|
* The Python code to execute
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user