diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index 27282645..552d4aea 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -18,7 +18,8 @@ use mcp_core::tool::{Tool, ToolAnnotations}; super::routes::config_management::remove_extension, super::routes::config_management::get_extensions, super::routes::config_management::read_all_config, - super::routes::config_management::providers + super::routes::config_management::providers, + super::routes::agent::get_tools, ), components(schemas( super::routes::config_management::UpsertConfigQuery, diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index 54982f13..6c400ce6 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -7,6 +7,7 @@ use axum::{ }; use goose::config::Config; use goose::{agents::AgentFactory, model::ModelConfig, providers}; +use mcp_core::Tool; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::env; @@ -163,11 +164,45 @@ async fn list_providers() -> Json> { Json(response) } +#[utoipa::path( + get, + path = "/agent/tools", + responses( + (status = 200, description = "Tools retrieved successfully", body = Vec), + (status = 401, description = "Unauthorized - invalid secret key"), + (status = 424, description = "Agent not initialized"), + (status = 500, description = "Internal server error") + ) +)] +async fn get_tools( + State(state): State, + headers: HeaderMap, +) -> Result>, StatusCode> { + let secret_key = headers + .get("X-Secret-Key") + .and_then(|value| value.to_str().ok()) + .ok_or(StatusCode::UNAUTHORIZED)?; + + if secret_key != state.secret_key { + return Err(StatusCode::UNAUTHORIZED); + } + + let mut agent = state.agent.write().await; + let agent = agent.as_mut().ok_or(StatusCode::PRECONDITION_REQUIRED)?; + + // Since list_tools() now returns Vec directly, not a Result + let tools = agent.list_tools().await; + + // Return the tools directly + Ok(Json(tools)) +} + pub fn routes(state: AppState) -> Router { Router::new() .route("/agent/versions", get(get_versions)) .route("/agent/providers", get(list_providers)) .route("/agent/prompt", post(extend_prompt)) + .route("/agent/tools", get(get_tools)) .route("/agent", post(create_agent)) .with_state(state) } diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 2c239e0d..848a66d2 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -12,7 +12,7 @@ use super::extension::{ExtensionConfig, ExtensionResult}; use crate::providers::base::Provider; use crate::session; use crate::{message::Message, permission::PermissionConfirmation}; -use mcp_core::{prompt::Prompt, protocol::GetPromptResult, Content, ToolResult}; +use mcp_core::{prompt::Prompt, protocol::GetPromptResult, Content, Tool, ToolResult}; /// Session configuration for an agent #[derive(Debug, Clone, Serialize, Deserialize)] @@ -43,6 +43,9 @@ pub trait Agent: Send + Sync { // TODO this needs to also include status so we can tell if extensions are dropped async fn list_extensions(&self) -> Vec; + /// List the tools this agent has access to + async fn list_tools(&self) -> Vec; + /// Pass through a JSON-RPC request to a specific extension async fn passthrough(&self, extension: &str, request: Value) -> ExtensionResult; diff --git a/crates/goose/src/agents/mod.rs b/crates/goose/src/agents/mod.rs index d394a104..67295439 100644 --- a/crates/goose/src/agents/mod.rs +++ b/crates/goose/src/agents/mod.rs @@ -9,5 +9,5 @@ mod types; pub use agent::{Agent, SessionConfig}; pub use capabilities::Capabilities; -pub use extension::ExtensionConfig; +pub use extension::{ExtensionConfig, ExtensionResult}; pub use factory::{register_agent, AgentFactory}; diff --git a/crates/goose/src/agents/reference.rs b/crates/goose/src/agents/reference.rs index 7f06ffc9..f8518397 100644 --- a/crates/goose/src/agents/reference.rs +++ b/crates/goose/src/agents/reference.rs @@ -53,6 +53,11 @@ impl Agent for ReferenceAgent { capabilities.add_extension(extension).await } + async fn list_tools(&self) -> Vec { + let mut capabilities = self.capabilities.lock().await; + capabilities.get_prefixed_tools().await.unwrap_or_default() + } + async fn remove_extension(&mut self, name: &str) { let mut capabilities = self.capabilities.lock().await; capabilities diff --git a/crates/goose/src/agents/summarize.rs b/crates/goose/src/agents/summarize.rs index cb5a22e4..f988b3c1 100644 --- a/crates/goose/src/agents/summarize.rs +++ b/crates/goose/src/agents/summarize.rs @@ -139,6 +139,11 @@ impl Agent for SummarizeAgent { capabilities.add_extension(extension).await } + async fn list_tools(&self) -> Vec { + let mut capabilities = self.capabilities.lock().await; + capabilities.get_prefixed_tools().await.unwrap_or_default() + } + async fn remove_extension(&mut self, name: &str) { let mut capabilities = self.capabilities.lock().await; capabilities diff --git a/crates/goose/src/agents/truncate.rs b/crates/goose/src/agents/truncate.rs index 71e969c4..848e65fb 100644 --- a/crates/goose/src/agents/truncate.rs +++ b/crates/goose/src/agents/truncate.rs @@ -178,6 +178,11 @@ impl Agent for TruncateAgent { capabilities.add_extension(extension).await } + async fn list_tools(&self) -> Vec { + let mut capabilities = self.capabilities.lock().await; + capabilities.get_prefixed_tools().await.unwrap_or_default() + } + async fn remove_extension(&mut self, name: &str) { let mut capabilities = self.capabilities.lock().await; capabilities diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 6744e2cb..4d904118 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -13,6 +13,38 @@ "version": "1.0.17" }, "paths": { + "/agent/tools": { + "get": { + "tags": [ + "super::routes::agent" + ], + "operationId": "get_tools", + "responses": { + "200": { + "description": "Tools retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tool" + } + } + } + } + }, + "401": { + "description": "Unauthorized - invalid secret key" + }, + "424": { + "description": "Agent not initialized" + }, + "500": { + "description": "Internal server error" + } + } + } + }, "/config": { "get": { "tags": [ diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index bddb20a8..94058a79 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch'; -import type { ReadAllConfigData, ReadAllConfigResponse, GetExtensionsData, GetExtensionsResponse, AddExtensionData, AddExtensionResponse, RemoveExtensionData, RemoveExtensionResponse, ProvidersData, ProvidersResponse2, ReadConfigData, RemoveConfigData, RemoveConfigResponse, UpsertConfigData, UpsertConfigResponse } from './types.gen'; +import type { GetToolsData, GetToolsResponse, ReadAllConfigData, ReadAllConfigResponse, GetExtensionsData, GetExtensionsResponse, AddExtensionData, AddExtensionResponse, RemoveExtensionData, RemoveExtensionResponse, ProvidersData, ProvidersResponse2, ReadConfigData, RemoveConfigData, RemoveConfigResponse, UpsertConfigData, UpsertConfigResponse } from './types.gen'; import { client as _heyApiClient } from './client.gen'; export type Options = ClientOptions & { @@ -18,6 +18,13 @@ export type Options; }; +export const getTools = (options?: Options) => { + return (options?.client ?? _heyApiClient).get({ + url: '/agent/tools', + ...options + }); +}; + export const readAllConfig = (options?: Options) => { return (options?.client ?? _heyApiClient).get({ url: '/config', diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 4fc3647d..d422d53b 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -210,6 +210,37 @@ export type UpsertConfigQuery = { value: unknown; }; +export type GetToolsData = { + body?: never; + path?: never; + query?: never; + url: '/agent/tools'; +}; + +export type GetToolsErrors = { + /** + * Unauthorized - invalid secret key + */ + 401: unknown; + /** + * Agent not initialized + */ + 424: unknown; + /** + * Internal server error + */ + 500: unknown; +}; + +export type GetToolsResponses = { + /** + * Tools retrieved successfully + */ + 200: Array; +}; + +export type GetToolsResponse = GetToolsResponses[keyof GetToolsResponses]; + export type ReadAllConfigData = { body?: never; path?: never; diff --git a/ui/desktop/src/components/BottomMenu.tsx b/ui/desktop/src/components/BottomMenu.tsx index d09d2ced..6cd23cf7 100644 --- a/ui/desktop/src/components/BottomMenu.tsx +++ b/ui/desktop/src/components/BottomMenu.tsx @@ -8,6 +8,7 @@ import type { View } from '../App'; import { settingsV2Enabled } from '../flags'; import { BottomMenuModeSelection } from './BottomMenuModeSelection'; import ModelsBottomBar from './settings_v2/models/bottom_bar/ModelsBottomBar'; +import ToolCount from './ToolCount'; export default function BottomMenu({ hasMessages, @@ -78,77 +79,82 @@ export default function BottomMenu({ {/* Goose Mode Selector Dropdown */} - {/* Model Selector Dropdown */} - {settingsV2Enabled ? ( - - ) : ( -
-
setIsModelMenuOpen(!isModelMenuOpen)} - > - {(currentModel?.alias ?? currentModel?.name) || 'Select Model'} - {isModelMenuOpen ? ( - - ) : ( - - )} -
+ {/* Right-side section with ToolCount and Model Selector together */} +
+ {/* Tool count */} + + {/* Model Selector Dropdown */} + {settingsV2Enabled ? ( + + ) : ( +
+
setIsModelMenuOpen(!isModelMenuOpen)} + > + {(currentModel?.alias ?? currentModel?.name) || 'Select Model'} + {isModelMenuOpen ? ( + + ) : ( + + )} +
- {/* Dropdown Menu */} - {isModelMenuOpen && ( -
-
- ( -
- )} -
- )} + )} +
+ )} +
); } diff --git a/ui/desktop/src/components/ToolCount.tsx b/ui/desktop/src/components/ToolCount.tsx new file mode 100644 index 00000000..3f384b7f --- /dev/null +++ b/ui/desktop/src/components/ToolCount.tsx @@ -0,0 +1,80 @@ +import React, { useState, useEffect } from 'react'; +import { getTools } from '../api'; +import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; +import { HammerIcon } from 'lucide-react'; +import { Popover, PopoverContent, PopoverTrigger } from './ui/popover'; + +const SUGGESTED_MAX_TOOLS = 15; + +export default function ToolCount() { + const [toolCount, setToolCount] = useState(null); + const [error, setError] = useState(false); + + useEffect(() => { + const fetchTools = async () => { + try { + const response = await getTools(); + if (response.error) { + console.error('failed to get tool count'); + setError(true); + } else { + setToolCount(response.data.length); + } + } catch (err) { + console.error('Error fetching tools:', err); + setError(true); + } + }; + + fetchTools(); + }, []); + + if (error) { + return
; + } + + if (toolCount === null) { + return
...
; + } + + if (toolCount < SUGGESTED_MAX_TOOLS) { + return ( +
+ + + + + +
+

Tool count: {toolCount}

+
+
+
+
+ ); + } else { + return ( +
+ + + + + +
+

Warning: High Tool Count

+

+ Too many tools can degrade goose's performance. Consider turning a few extensions + off. +

+

Tool count: {toolCount}

+
+
+
+
+ ); + } +}