diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index 552d4aea..692a704d 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -1,11 +1,12 @@ -use utoipa::OpenApi; - use goose::agents::extension::Envs; +use goose::agents::extension::ToolInfo; use goose::agents::ExtensionConfig; +use goose::config::permission::PermissionLevel; use goose::config::ExtensionEntry; use goose::providers::base::ConfigKey; use goose::providers::base::ProviderMetadata; use mcp_core::tool::{Tool, ToolAnnotations}; +use utoipa::OpenApi; #[allow(dead_code)] // Used by utoipa for OpenAPI generation #[derive(OpenApi)] @@ -36,6 +37,8 @@ use mcp_core::tool::{Tool, ToolAnnotations}; Envs, Tool, ToolAnnotations, + ToolInfo, + PermissionLevel, )) )] pub struct ApiDoc; diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index 6c400ce6..cd78a2f5 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -1,13 +1,15 @@ use crate::state::AppState; use axum::{ - extract::State, + extract::{Query, State}, http::{HeaderMap, StatusCode}, routing::{get, post}, Json, Router, }; -use goose::config::Config; -use goose::{agents::AgentFactory, model::ModelConfig, providers}; -use mcp_core::Tool; +use goose::{agents::AgentFactory, config::PermissionManager, model::ModelConfig, providers}; +use goose::{ + agents::{capabilities::get_parameter_names, extension::ToolInfo}, + config::Config, +}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::env; @@ -62,6 +64,11 @@ struct ProviderList { details: ProviderDetails, } +#[derive(Deserialize)] +pub struct GetToolsQuery { + extension_name: Option, +} + async fn get_versions() -> Json { let versions = AgentFactory::available_versions(); let default_version = AgentFactory::default_version().to_string(); @@ -167,6 +174,9 @@ async fn list_providers() -> Json> { #[utoipa::path( get, path = "/agent/tools", + params( + ("extension_name" = Option, Query, description = "Optional extension name to filter tools") + ), responses( (status = 200, description = "Tools retrieved successfully", body = Vec), (status = 401, description = "Unauthorized - invalid secret key"), @@ -177,7 +187,8 @@ async fn list_providers() -> Json> { async fn get_tools( State(state): State, headers: HeaderMap, -) -> Result>, StatusCode> { + Query(query): Query, +) -> Result>, StatusCode> { let secret_key = headers .get("X-Secret-Key") .and_then(|value| value.to_str().ok()) @@ -189,11 +200,30 @@ async fn get_tools( let mut agent = state.agent.write().await; let agent = agent.as_mut().ok_or(StatusCode::PRECONDITION_REQUIRED)?; + let permission_manager = PermissionManager::default(); - // Since list_tools() now returns Vec directly, not a Result - let tools = agent.list_tools().await; + let tools = agent + .list_tools() + .await + .into_iter() + .filter(|tool| { + // Apply the filter only if the extension name is present in the query + if let Some(extension_name) = &query.extension_name { + tool.name.starts_with(extension_name) + } else { + true + } + }) + .map(|tool| { + ToolInfo::new( + &tool.name, + &tool.description, + get_parameter_names(&tool), + permission_manager.get_user_permission(&tool.name), + ) + }) + .collect(); - // Return the tools directly Ok(Json(tools)) } diff --git a/crates/goose/src/agents/extension.rs b/crates/goose/src/agents/extension.rs index 838f8bbe..ebb9f61f 100644 --- a/crates/goose/src/agents/extension.rs +++ b/crates/goose/src/agents/extension.rs @@ -9,6 +9,7 @@ use utoipa::ToSchema; use crate::config; use crate::config::extensions::name_to_key; +use crate::config::permission::PermissionLevel; /// Errors from Extension operation #[derive(Error, Debug)] @@ -277,19 +278,26 @@ impl ExtensionInfo { } /// Information about the tool used for building prompts -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Serialize, ToSchema)] pub struct ToolInfo { name: String, description: String, parameters: Vec, + permission: Option, } impl ToolInfo { - pub fn new(name: &str, description: &str, parameters: Vec) -> Self { + pub fn new( + name: &str, + description: &str, + parameters: Vec, + permission: Option, + ) -> Self { Self { name: name.to_string(), description: description.to_string(), parameters, + permission, } } } diff --git a/crates/goose/src/agents/reference.rs b/crates/goose/src/agents/reference.rs index f8518397..983f9381 100644 --- a/crates/goose/src/agents/reference.rs +++ b/crates/goose/src/agents/reference.rs @@ -289,7 +289,14 @@ impl Agent for ReferenceAgent { let tools = capabilities.get_prefixed_tools().await?; let tools_info = tools .into_iter() - .map(|tool| ToolInfo::new(&tool.name, &tool.description, get_parameter_names(&tool))) + .map(|tool| { + ToolInfo::new( + &tool.name, + &tool.description, + get_parameter_names(&tool), + None, + ) + }) .collect(); let plan_prompt = capabilities.get_planning_prompt(tools_info).await; diff --git a/crates/goose/src/agents/summarize.rs b/crates/goose/src/agents/summarize.rs index f988b3c1..16d1d006 100644 --- a/crates/goose/src/agents/summarize.rs +++ b/crates/goose/src/agents/summarize.rs @@ -489,7 +489,14 @@ impl Agent for SummarizeAgent { let tools = capabilities.get_prefixed_tools().await?; let tools_info = tools .into_iter() - .map(|tool| ToolInfo::new(&tool.name, &tool.description, get_parameter_names(&tool))) + .map(|tool| { + ToolInfo::new( + &tool.name, + &tool.description, + get_parameter_names(&tool), + None, + ) + }) .collect(); let plan_prompt = capabilities.get_planning_prompt(tools_info).await; diff --git a/crates/goose/src/agents/truncate.rs b/crates/goose/src/agents/truncate.rs index 848e65fb..dfa3dbfa 100644 --- a/crates/goose/src/agents/truncate.rs +++ b/crates/goose/src/agents/truncate.rs @@ -774,7 +774,14 @@ impl Agent for TruncateAgent { let tools = capabilities.get_prefixed_tools().await?; let tools_info = tools .into_iter() - .map(|tool| ToolInfo::new(&tool.name, &tool.description, get_parameter_names(&tool))) + .map(|tool| { + ToolInfo::new( + &tool.name, + &tool.description, + get_parameter_names(&tool), + None, + ) + }) .collect(); let plan_prompt = capabilities.get_planning_prompt(tools_info).await; diff --git a/crates/goose/src/config/permission.rs b/crates/goose/src/config/permission.rs index 62c7a142..5b2b4cc6 100644 --- a/crates/goose/src/config/permission.rs +++ b/crates/goose/src/config/permission.rs @@ -5,9 +5,10 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; +use utoipa::ToSchema; /// Enum representing the possible permission levels for a tool. -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, ToSchema)] #[serde(rename_all = "snake_case")] pub enum PermissionLevel { AlwaysAllow, // Tool can always be used without prompt diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 4d904118..c69d0f92 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -19,6 +19,18 @@ "super::routes::agent" ], "operationId": "get_tools", + "parameters": [ + { + "name": "extension_name", + "in": "query", + "description": "Optional extension name to filter tools", + "required": false, + "schema": { + "type": "string", + "nullable": true + } + } + ], "responses": { "200": { "description": "Tools retrieved successfully", @@ -547,6 +559,15 @@ } } }, + "PermissionLevel": { + "type": "string", + "description": "Enum representing the possible permission levels for a tool.", + "enum": [ + "always_allow", + "ask_before", + "never_allow" + ] + }, "ProviderDetails": { "type": "object", "required": [ @@ -688,6 +709,37 @@ } } }, + "ToolInfo": { + "type": "object", + "description": "Information about the tool used for building prompts", + "required": [ + "name", + "description", + "parameters" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "type": "string" + } + }, + "permission": { + "allOf": [ + { + "$ref": "#/components/schemas/PermissionLevel" + } + ], + "nullable": true + } + } + }, "UpsertConfigQuery": { "type": "object", "required": [ diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index d422d53b..3392d5e7 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -84,6 +84,11 @@ export type ExtensionResponse = { extensions: Array; }; +/** + * Enum representing the possible permission levels for a tool. + */ +export type PermissionLevel = 'always_allow' | 'ask_before' | 'never_allow'; + export type ProviderDetails = { /** * Indicates whether the provider is fully configured @@ -204,6 +209,16 @@ export type ToolAnnotations = { title?: string | null; }; +/** + * Information about the tool used for building prompts + */ +export type ToolInfo = { + description: string; + name: string; + parameters: Array; + permission?: PermissionLevel | null; +}; + export type UpsertConfigQuery = { is_secret: boolean; key: string; @@ -213,7 +228,12 @@ export type UpsertConfigQuery = { export type GetToolsData = { body?: never; path?: never; - query?: never; + query?: { + /** + * Optional extension name to filter tools + */ + extension_name?: string | null; + }; url: '/agent/tools'; };