diff --git a/src/apps/desktop/src/api/mcp_api.rs b/src/apps/desktop/src/api/mcp_api.rs index b14df51fd..8d8af547f 100644 --- a/src/apps/desktop/src/api/mcp_api.rs +++ b/src/apps/desktop/src/api/mcp_api.rs @@ -503,7 +503,7 @@ pub struct McpUiResourcePermissions { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FetchMCPAppResourceRequest { - /// MCP server ID (e.g. from tool name mcp__{server_id}__{tool_name}) + /// Authoritative MCP server ID for the tool/app. pub server_id: String, /// Full resource URI, e.g. "ui://my-server/widget" pub resource_uri: String, @@ -541,13 +541,16 @@ pub async fn get_mcp_tool_ui_uri( _state: State<'_, AppState>, tool_name: String, ) -> Result, String> { - if !tool_name.starts_with("mcp__") { - return Ok(None); - } let registry = bitfun_core::agentic::tools::registry::get_global_tool_registry(); let guard = registry.read().await; + let is_mcp_tool = guard + .get_dynamic_tool_info(&tool_name) + .is_some_and(|info| info.mcp.is_some()); let tool = guard.get_tool(&tool_name); drop(guard); + if !is_mcp_tool { + return Ok(None); + } Ok(tool.and_then(|t| t.ui_resource_uri())) } diff --git a/src/apps/desktop/src/api/tool_api.rs b/src/apps/desktop/src/api/tool_api.rs index dfb4dbcee..811ed78f3 100644 --- a/src/apps/desktop/src/api/tool_api.rs +++ b/src/apps/desktop/src/api/tool_api.rs @@ -4,6 +4,7 @@ use log::error; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; +use std::sync::Arc; use bitfun_core::agentic::{ tools::framework::ToolUseContext, @@ -26,6 +27,30 @@ pub struct ToolExecutionRequest { pub safe_mode: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetToolInfoRequest { + pub tool_name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DynamicMcpToolInfo { + pub server_id: String, + pub server_name: String, + pub tool_name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DynamicToolInfo { + pub provider_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_kind: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub mcp: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolInfo { pub name: String, @@ -34,6 +59,8 @@ pub struct ToolInfo { pub is_readonly: bool, pub is_concurrency_safe: bool, pub needs_permissions: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_info: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -159,6 +186,41 @@ async fn build_tool_context(workspace_path: Option<&str>) -> ToolUseContext { } } +fn to_dynamic_mcp_tool_info(info: bitfun_core::service::mcp::McpToolInfo) -> DynamicMcpToolInfo { + DynamicMcpToolInfo { + server_id: info.server_id, + server_name: info.server_name, + tool_name: info.tool_name, + } +} + +fn to_dynamic_tool_info( + info: bitfun_core::agentic::tools::framework::DynamicToolInfo, +) -> DynamicToolInfo { + DynamicToolInfo { + provider_id: info.provider_id, + provider_kind: info.provider_kind, + mcp: info.mcp.map(to_dynamic_mcp_tool_info), + } +} + +async fn build_tool_info(tool: &Arc) -> ToolInfo { + let description = tool + .description() + .await + .unwrap_or_else(|_| "No description available".to_string()); + + ToolInfo { + name: tool.name().to_string(), + description, + input_schema: tool.input_schema_for_model().await, + is_readonly: tool.is_readonly(), + is_concurrency_safe: tool.is_concurrency_safe(None), + needs_permissions: tool.needs_permissions(None), + dynamic_info: tool.dynamic_tool_info().map(to_dynamic_tool_info), + } +} + fn has_explicit_workspace_path(workspace_path: Option<&str>) -> bool { workspace_path.is_some_and(|path| !path.trim().is_empty()) } @@ -202,19 +264,7 @@ pub async fn get_all_tools_info() -> Result, String> { let mut tool_infos = Vec::new(); for tool in tools { - let description = tool - .description() - .await - .unwrap_or_else(|_| "No description available".to_string()); - - tool_infos.push(ToolInfo { - name: tool.name().to_string(), - description, - input_schema: tool.input_schema_for_model().await, - is_readonly: tool.is_readonly(), - is_concurrency_safe: tool.is_concurrency_safe(None), - needs_permissions: tool.needs_permissions(None), - }); + tool_infos.push(build_tool_info(&tool).await); } Ok(tool_infos) @@ -229,43 +279,19 @@ pub async fn get_readonly_tools_info() -> Result, String> { let mut tool_infos = Vec::new(); for tool in tools { - let description = tool - .description() - .await - .unwrap_or_else(|_| "No description available".to_string()); - - tool_infos.push(ToolInfo { - name: tool.name().to_string(), - description, - input_schema: tool.input_schema_for_model().await, - is_readonly: tool.is_readonly(), - is_concurrency_safe: tool.is_concurrency_safe(None), - needs_permissions: tool.needs_permissions(None), - }); + tool_infos.push(build_tool_info(&tool).await); } Ok(tool_infos) } #[tauri::command] -pub async fn get_tool_info(tool_name: String) -> Result, String> { +pub async fn get_tool_info(request: GetToolInfoRequest) -> Result, String> { let tools = get_all_tools().await; for tool in tools { - if tool.name() == tool_name { - let description = tool - .description() - .await - .unwrap_or_else(|_| "No description available".to_string()); - - return Ok(Some(ToolInfo { - name: tool.name().to_string(), - description, - input_schema: tool.input_schema_for_model().await, - is_readonly: tool.is_readonly(), - is_concurrency_safe: tool.is_concurrency_safe(None), - needs_permissions: tool.needs_permissions(None), - })); + if tool.name() == request.tool_name { + return Ok(Some(build_tool_info(&tool).await)); } } diff --git a/src/crates/core/src/agentic/tools/framework.rs b/src/crates/core/src/agentic/tools/framework.rs index 2df6dfec0..1c0bf95f0 100644 --- a/src/crates/core/src/agentic/tools/framework.rs +++ b/src/crates/core/src/agentic/tools/framework.rs @@ -14,6 +14,7 @@ use crate::agentic::workspace::WorkspaceServices; use crate::agentic::WorkspaceBinding; use crate::infrastructure::get_path_manager_arc; use crate::service::git::{GitDiffParams, GitService}; +use crate::service::mcp::McpToolInfo; use crate::service::remote_ssh::workspace_state::remote_workspace_runtime_root; use crate::service::{get_workspace_runtime_service_arc, WorkspaceRuntimeContext}; use crate::util::errors::BitFunResult; @@ -32,6 +33,13 @@ pub enum ToolPathBackend { RemoteWorkspace, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DynamicToolInfo { + pub provider_id: String, + pub provider_kind: Option, + pub mcp: Option, +} + #[derive(Debug, Clone)] pub struct ToolPathResolution { pub requested_path: String, @@ -606,6 +614,15 @@ pub trait Tool: Send + Sync { None } + /// Rich metadata for dynamic tools. Prefer this over encoding dynamic ownership in tool names. + fn dynamic_tool_info(&self) -> Option { + self.dynamic_provider_id().map(|provider_id| DynamicToolInfo { + provider_id: provider_id.to_string(), + provider_kind: None, + mcp: None, + }) + } + /// User friendly name fn user_facing_name(&self) -> String { self.name().to_string() diff --git a/src/crates/core/src/agentic/tools/registry.rs b/src/crates/core/src/agentic/tools/registry.rs index a6f878b7f..e77e83b26 100644 --- a/src/crates/core/src/agentic/tools/registry.rs +++ b/src/crates/core/src/agentic/tools/registry.rs @@ -1,6 +1,6 @@ //! Tool registry -use crate::agentic::tools::framework::Tool; +use crate::agentic::tools::framework::{DynamicToolInfo, Tool}; use crate::agentic::tools::implementations::*; use crate::util::errors::BitFunResult; use bitfun_runtime_ports::{DynamicToolDescriptor, DynamicToolProvider, ToolDecorator}; @@ -11,6 +11,12 @@ use std::sync::Arc; type ToolRef = Arc; type ToolDecoratorRef = Arc>; +#[derive(Debug, Clone)] +struct DynamicToolMetadata { + provider_id: String, + info: DynamicToolInfo, +} + struct SnapshotToolDecorator; impl ToolDecorator for SnapshotToolDecorator { @@ -22,6 +28,7 @@ impl ToolDecorator for SnapshotToolDecorator { /// Tool registry - manages all available tools (using IndexMap to maintain registration order) pub struct ToolRegistry { tools: IndexMap, + dynamic_tools: IndexMap, tool_decorator: ToolDecoratorRef, } @@ -45,6 +52,7 @@ impl ToolRegistry { pub fn with_tool_decorator(tool_decorator: ToolDecoratorRef) -> Self { let mut registry = Self { tools: IndexMap::new(), + dynamic_tools: IndexMap::new(), tool_decorator, }; @@ -93,8 +101,24 @@ impl ToolRegistry { /// Remove all tools from the MCP server pub fn unregister_mcp_server_tools(&mut self, server_id: &str) { - let prefix = format!("mcp__{}__", server_id); - self.unregister_tools_by_prefix(&prefix); + let to_remove: Vec = self + .dynamic_tools + .iter() + .filter(|(_, metadata)| { + metadata + .info + .mcp + .as_ref() + .is_some_and(|info| info.server_id == server_id) + }) + .map(|(tool_name, _)| tool_name.clone()) + .collect(); + + for key in to_remove { + info!("Unregistering dynamic tool: tool_name={}", key); + self.tools.shift_remove(&key); + self.dynamic_tools.shift_remove(&key); + } } /// Remove all tools whose registry name starts with the given prefix. @@ -110,6 +134,7 @@ impl ToolRegistry { for key in to_remove { info!("Unregistering dynamic tool: tool_name={}", key); self.tools.shift_remove(&key); + self.dynamic_tools.shift_remove(&key); } count } @@ -190,6 +215,25 @@ impl ToolRegistry { // subsequent lookup returns the same runtime implementation. let tool = self.tool_decorator.decorate(tool); let name = tool.name().to_string(); + let dynamic_info = tool.dynamic_tool_info().and_then(|info| { + if info.provider_id.trim().is_empty() { + None + } else { + Some(info) + } + }); + + if let Some(info) = dynamic_info { + self.dynamic_tools.insert( + name.clone(), + DynamicToolMetadata { + provider_id: info.provider_id.clone(), + info, + }, + ); + } else { + self.dynamic_tools.shift_remove(&name); + } self.tools.insert(name, tool); } @@ -198,6 +242,12 @@ impl ToolRegistry { self.tools.get(name).cloned() } + pub fn get_dynamic_tool_info(&self, name: &str) -> Option { + self.dynamic_tools + .get(name) + .map(|metadata| metadata.info.clone()) + } + /// Get all tool names pub fn get_tool_names(&self) -> Vec { self.tools.keys().cloned().collect() @@ -220,12 +270,8 @@ impl DynamicToolProvider for ToolRegistry { ) -> bitfun_runtime_ports::PortResult> { let mut descriptors = Vec::new(); - for tool in self.tools.values() { - let Some(provider_id) = tool - .dynamic_provider_id() - .filter(|value| !value.is_empty()) - .map(ToOwned::to_owned) - else { + for (name, tool) in self.tools.iter() { + let Some(metadata) = self.dynamic_tools.get(name) else { continue; }; let description = tool.description().await.map_err(|error| { @@ -239,7 +285,7 @@ impl DynamicToolProvider for ToolRegistry { name: tool.name().to_string(), description, input_schema: tool.input_schema_for_model().await, - provider_id: Some(provider_id), + provider_id: Some(metadata.provider_id.clone()), }); } @@ -252,7 +298,9 @@ mod tests { use super::create_tool_registry; use super::ToolRef; use super::ToolRegistry; - use crate::agentic::tools::framework::{Tool, ToolResult, ToolUseContext, ValidationResult}; + use crate::agentic::tools::framework::{ + DynamicToolInfo, Tool, ToolResult, ToolUseContext, ValidationResult, + }; use async_trait::async_trait; use bitfun_runtime_ports::DynamicToolProvider; use serde_json::json; @@ -261,7 +309,7 @@ mod tests { struct DynamicMetadataTool { name: String, - provider_id: Option, + dynamic_info: Option, } #[async_trait] @@ -279,7 +327,11 @@ mod tests { } fn dynamic_provider_id(&self) -> Option<&str> { - self.provider_id.as_deref() + self.dynamic_info.as_ref().map(|info| info.provider_id.as_str()) + } + + fn dynamic_tool_info(&self) -> Option { + self.dynamic_info.clone() } async fn validate_input( @@ -307,7 +359,32 @@ mod tests { fn dynamic_tool(name: &str, provider_id: Option<&str>) -> ToolRef { Arc::new(DynamicMetadataTool { name: name.to_string(), - provider_id: provider_id.map(ToOwned::to_owned), + dynamic_info: provider_id.map(|provider_id| DynamicToolInfo { + provider_id: provider_id.to_string(), + provider_kind: None, + mcp: None, + }), + }) + } + + fn mcp_dynamic_tool( + name: &str, + _provider_id: Option<&str>, + server_id: &str, + server_name: &str, + tool_name: &str, + ) -> ToolRef { + Arc::new(DynamicMetadataTool { + name: name.to_string(), + dynamic_info: Some(DynamicToolInfo { + provider_id: server_id.to_string(), + provider_kind: Some("mcp".to_string()), + mcp: Some(crate::service::mcp::McpToolInfo { + server_id: server_id.to_string(), + server_name: server_name.to_string(), + tool_name: tool_name.to_string(), + }), + }), }) } @@ -345,6 +422,38 @@ mod tests { ); } + #[tokio::test] + async fn dynamic_tool_provider_prefers_mcp_registry_metadata() { + let mut registry = ToolRegistry::new(); + registry.register_tool(mcp_dynamic_tool( + "mcp__github__search_repos", + Some("stale-provider-id"), + "github-server-id", + "GitHub", + "search_repos", + )); + + let descriptors = registry + .list_dynamic_tools() + .await + .expect("list dynamic tools"); + + let descriptor = descriptors + .into_iter() + .find(|item| item.name == "mcp__github__search_repos") + .expect("mcp descriptor"); + + assert_eq!(descriptor.provider_id.as_deref(), Some("github-server-id")); + assert_eq!( + registry + .get_dynamic_tool_info("mcp__github__search_repos") + .expect("mcp metadata") + .mcp + .expect("mcp subtype metadata") + .tool_name, + "search_repos" + ); + } #[test] fn registry_exposes_controlhub_and_computer_use() { let registry = create_tool_registry(); diff --git a/src/crates/core/src/service/mcp/adapter/tool.rs b/src/crates/core/src/service/mcp/adapter/tool.rs index 939f694dd..a15a092ed 100644 --- a/src/crates/core/src/service/mcp/adapter/tool.rs +++ b/src/crates/core/src/service/mcp/adapter/tool.rs @@ -3,8 +3,9 @@ //! Wraps MCP tools as implementations of BitFun's `Tool` trait. use crate::agentic::tools::framework::{ - Tool, ToolRenderOptions, ToolResult, ToolUseContext, ValidationResult, + DynamicToolInfo, Tool, ToolRenderOptions, ToolResult, ToolUseContext, ValidationResult, }; +use crate::service::mcp::{build_mcp_tool_name, McpToolInfo}; use crate::service::mcp::protocol::{MCPTool, MCPToolResult}; use crate::service::mcp::server::MCPConnection; use crate::util::errors::BitFunResult; @@ -17,7 +18,7 @@ use std::sync::Arc; pub struct MCPToolWrapper { mcp_tool: MCPTool, connection: Arc, - provider_id: String, + server_id: String, server_name: String, full_name: String, } @@ -32,11 +33,11 @@ impl MCPToolWrapper { server_id: String, server_name: String, ) -> Self { - let full_name = format!("mcp__{}__{}", server_id, mcp_tool.name); + let full_name = build_mcp_tool_name(&server_id, &mcp_tool.name); Self { mcp_tool, connection, - provider_id: server_id, + server_id, server_name, full_name, } @@ -127,13 +128,25 @@ impl Tool for MCPToolWrapper { } fn dynamic_provider_id(&self) -> Option<&str> { - Some(&self.provider_id) + Some(&self.server_id) } fn user_facing_name(&self) -> String { format!("{} ({})", self.tool_title(), self.server_name) } + fn dynamic_tool_info(&self) -> Option { + Some(DynamicToolInfo { + provider_id: self.server_id.clone(), + provider_kind: Some("mcp".to_string()), + mcp: Some(McpToolInfo { + server_id: self.server_id.clone(), + server_name: self.server_name.clone(), + tool_name: self.mcp_tool.name.clone(), + }), + }) + } + async fn is_enabled(&self) -> bool { true } diff --git a/src/crates/core/src/service/mcp/mod.rs b/src/crates/core/src/service/mcp/mod.rs index 843550e23..673a9cb19 100644 --- a/src/crates/core/src/service/mcp/mod.rs +++ b/src/crates/core/src/service/mcp/mod.rs @@ -14,6 +14,8 @@ pub mod auth; pub mod config; pub mod protocol; pub mod server; +mod tool_info; +mod tool_name; use std::sync::Arc; use std::sync::OnceLock; @@ -34,6 +36,10 @@ pub use adapter::{ }; pub use config::{ConfigLocation, MCPConfigService}; +pub use tool_info::McpToolInfo; +pub use tool_name::{ + build_mcp_tool_name, normalize_name_for_mcp, MCP_TOOL_DELIMITER, MCP_TOOL_PREFIX, +}; /// MCP service interface. pub struct MCPService { diff --git a/src/crates/core/src/service/mcp/tool_info.rs b/src/crates/core/src/service/mcp/tool_info.rs new file mode 100644 index 000000000..35f73b2e7 --- /dev/null +++ b/src/crates/core/src/service/mcp/tool_info.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct McpToolInfo { + pub server_id: String, + pub server_name: String, + pub tool_name: String, +} diff --git a/src/crates/core/src/service/mcp/tool_name.rs b/src/crates/core/src/service/mcp/tool_name.rs new file mode 100644 index 000000000..a626e189b --- /dev/null +++ b/src/crates/core/src/service/mcp/tool_name.rs @@ -0,0 +1,56 @@ +//! Shared MCP tool-name helpers. + +pub const MCP_TOOL_PREFIX: &str = "mcp__"; +pub const MCP_TOOL_DELIMITER: &str = "__"; + +/// Normalize MCP server/tool names to a wire-safe format aligned with claude-code. +pub fn normalize_name_for_mcp(name: &str) -> String { + name.chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' { + ch + } else { + '_' + } + }) + .collect() +} + +pub fn build_mcp_tool_name(server_id: &str, tool_name: &str) -> String { + format!( + "{}{}{}{}", + MCP_TOOL_PREFIX, + normalize_name_for_mcp(server_id), + MCP_TOOL_DELIMITER, + normalize_name_for_mcp(tool_name) + ) +} + +#[cfg(test)] +mod tests { + use super::{build_mcp_tool_name, normalize_name_for_mcp}; + + #[test] + fn normalize_name_for_mcp_replaces_spaces_and_symbols() { + assert_eq!( + normalize_name_for_mcp("Acme Search / Primary"), + "Acme_Search___Primary" + ); + } + + #[test] + fn normalize_name_for_mcp_keeps_ascii_word_chars_and_hyphen() { + assert_eq!( + normalize_name_for_mcp("github-enterprise_v2"), + "github-enterprise_v2" + ); + } + + #[test] + fn build_mcp_tool_name_normalizes_both_segments() { + assert_eq!( + build_mcp_tool_name("Claude Code", "search repos"), + "mcp__Claude_Code__search_repos" + ); + } +} diff --git a/src/crates/core/src/service/snapshot/manager.rs b/src/crates/core/src/service/snapshot/manager.rs index 85644a799..bb9590b44 100644 --- a/src/crates/core/src/service/snapshot/manager.rs +++ b/src/crates/core/src/service/snapshot/manager.rs @@ -1,4 +1,4 @@ -use crate::agentic::tools::framework::{Tool, ToolResult, ToolUseContext}; +use crate::agentic::tools::framework::{DynamicToolInfo, Tool, ToolResult, ToolUseContext}; use crate::agentic::tools::registry::ToolRegistry; use crate::service::remote_ssh::workspace_state::is_remote_path; use crate::service::snapshot::service::SnapshotService; @@ -389,6 +389,10 @@ impl Tool for WrappedTool { self.original_tool.dynamic_provider_id() } + fn dynamic_tool_info(&self) -> Option { + self.original_tool.dynamic_tool_info() + } + fn user_facing_name(&self) -> String { self.original_tool.user_facing_name().to_string() } diff --git a/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx b/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx index 95d7c491d..0c4857ad5 100644 --- a/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx +++ b/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx @@ -48,7 +48,7 @@ const CreateAgentPage: React.FC = () => { if (!name) return null; return { name, - isReadonly: Boolean(tool?.isReadonly ?? tool?.is_readonly), + isReadonly: Boolean(tool?.is_readonly), }; }) .filter((tool): tool is SubagentEditorToolInfo => Boolean(tool)); diff --git a/src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts b/src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts index c172540a2..d9b250545 100644 --- a/src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts +++ b/src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts @@ -5,6 +5,7 @@ import { SubagentAPI } from '@/infrastructure/api/service-api/SubagentAPI'; import { configAPI } from '@/infrastructure/api/service-api/ConfigAPI'; import type { ModeConfigItem, ModeSkillInfo } from '@/infrastructure/config/types'; import { useNotification } from '@/shared/notification-system'; +import type { DynamicToolInfo } from '@/shared/types/agent-api'; import type { AgentWithCapabilities } from '../agentsStore'; import { enrichCapabilities } from '../utils'; import { STATIC_HIDDEN_AGENT_IDS, isAgentInOverviewZone } from '../agentVisibility'; @@ -18,6 +19,7 @@ export interface ToolInfo { name: string; description: string; is_readonly: boolean; + dynamic_info?: DynamicToolInfo; } interface UseAgentsListOptions { diff --git a/src/web-ui/src/app/scenes/profile/views/TemplateConfigPage.tsx b/src/web-ui/src/app/scenes/profile/views/TemplateConfigPage.tsx index be87e88f2..ea7025835 100644 --- a/src/web-ui/src/app/scenes/profile/views/TemplateConfigPage.tsx +++ b/src/web-ui/src/app/scenes/profile/views/TemplateConfigPage.tsx @@ -18,14 +18,19 @@ import { configManager } from '@/infrastructure/config/services/ConfigManager'; import type { AIModelConfig, ModeConfigItem, ModeSkillInfo } from '@/infrastructure/config/types'; import { MCPAPI, type MCPServerInfo } from '@/infrastructure/api/service-api/MCPAPI'; import { notificationService } from '@/shared/notification-system'; +import type { DynamicToolInfo } from '@/shared/types/agent-api'; import { createLogger } from '@/shared/utils/logger'; -import { isMcpToolName, parseMcpToolName } from '@/infrastructure/mcp/toolName'; import { useNurseryStore } from '../nurseryStore'; import { formatTokenCount } from './useTokenEstimate'; const log = createLogger('TemplateConfigPage'); -interface ToolInfo { name: string; description: string; is_readonly: boolean; } +interface ToolInfo { + name: string; + description: string; + is_readonly: boolean; + dynamic_info?: DynamicToolInfo; +} type TemplateDetail = | { type: 'tool'; tool: ToolInfo; isMcp: boolean } @@ -34,16 +39,16 @@ type TemplateDetail = type ModelSlot = 'primary' | 'fast'; -function isMcpTool(name: string): boolean { - return isMcpToolName(name); +function isMcpTool(tool: ToolInfo): boolean { + return tool.dynamic_info?.provider_kind === 'mcp' && Boolean(tool.dynamic_info.mcp); } -function getMcpServerName(toolName: string): string { - return parseMcpToolName(toolName)?.serverId ?? toolName; +function getMcpServerName(tool: ToolInfo): string { + return tool.dynamic_info?.mcp?.server_id ?? tool.name; } -function getMcpShortName(toolName: string): string { - return parseMcpToolName(toolName)?.toolName ?? toolName; +function getMcpShortName(tool: ToolInfo): string { + return tool.dynamic_info?.mcp?.tool_name ?? tool.name; } type CtxSegKey = 'systemPrompt' | 'toolInjection' | 'rules' | 'memories'; @@ -171,7 +176,7 @@ const TemplateConfigPage: React.FC = () => { // Split tools into built-in vs MCP const builtinTools = useMemo( - () => availableTools.filter((t) => !isMcpTool(t.name)), + () => availableTools.filter((tool) => !isMcpTool(tool)), [availableTools], ); @@ -189,8 +194,8 @@ const TemplateConfigPage: React.FC = () => { const mcpToolsByServer = useMemo(() => { const map = new Map(); for (const tool of availableTools) { - if (!isMcpTool(tool.name)) continue; - const server = getMcpServerName(tool.name); + if (!isMcpTool(tool)) continue; + const server = getMcpServerName(tool); if (!map.has(server)) map.set(server, []); map.get(server)!.push(tool); } @@ -415,7 +420,7 @@ const TemplateConfigPage: React.FC = () => {
{tools.map((tool) => { const enabled = agenticConfig?.enabled_tools?.includes(tool.name) ?? false; - const displayName = isMcp ? getMcpShortName(tool.name) : tool.name; + const displayName = isMcp ? getMcpShortName(tool) : tool.name; const selected = detail?.type === 'tool' && detail.tool.name === tool.name; return (
{ if (detail.type === 'tool') { const { tool, isMcp } = detail; - const displayName = isMcp ? getMcpShortName(tool.name) : tool.name; + const displayName = isMcp ? getMcpShortName(tool) : tool.name; const enabled = agenticConfig?.enabled_tools?.includes(tool.name) ?? false; return (