Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions src/apps/desktop/src/api/mcp_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -541,13 +541,16 @@ pub async fn get_mcp_tool_ui_uri(
_state: State<'_, AppState>,
tool_name: String,
) -> Result<Option<String>, 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()))
}

Expand Down
108 changes: 67 additions & 41 deletions src/apps/desktop/src/api/tool_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,6 +27,30 @@ pub struct ToolExecutionRequest {
pub safe_mode: Option<bool>,
}

#[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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mcp: Option<DynamicMcpToolInfo>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolInfo {
pub name: String,
Expand All @@ -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<DynamicToolInfo>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -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<dyn bitfun_core::agentic::tools::framework::Tool>) -> 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())
}
Expand Down Expand Up @@ -202,19 +264,7 @@ pub async fn get_all_tools_info() -> Result<Vec<ToolInfo>, 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)
Expand All @@ -229,43 +279,19 @@ pub async fn get_readonly_tools_info() -> Result<Vec<ToolInfo>, 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<Option<ToolInfo>, String> {
pub async fn get_tool_info(request: GetToolInfoRequest) -> Result<Option<ToolInfo>, 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));
}
}

Expand Down
17 changes: 17 additions & 0 deletions src/crates/core/src/agentic/tools/framework.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,6 +33,13 @@ pub enum ToolPathBackend {
RemoteWorkspace,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DynamicToolInfo {
pub provider_id: String,
pub provider_kind: Option<String>,
pub mcp: Option<McpToolInfo>,
}

#[derive(Debug, Clone)]
pub struct ToolPathResolution {
pub requested_path: String,
Expand Down Expand Up @@ -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<DynamicToolInfo> {
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()
Expand Down
Loading
Loading