Skip to main content

agent_sdk/mcp/
tool_bridge.rs

1//! Bridge MCP tools to SDK Tool trait.
2
3use crate::tools::{DynamicToolName, Tool, ToolContext, ToolRegistry};
4use crate::types::{ToolResult, ToolTier};
5use anyhow::{Context, Result};
6use serde_json::Value;
7use std::fmt::Write;
8use std::sync::Arc;
9
10use super::client::McpClient;
11use super::protocol::{McpContent, McpToolDefinition};
12use super::transport::McpTransport;
13
14/// Bridge an MCP tool to the SDK Tool trait.
15///
16/// This wrapper allows MCP tools to be used as regular SDK tools.
17///
18/// # Example
19///
20/// ```ignore
21/// use agent_sdk::mcp::{McpClient, McpToolBridge, StdioTransport};
22///
23/// let transport = StdioTransport::spawn("npx", &["-y", "mcp-server"]).await?;
24/// let client = Arc::new(McpClient::new(transport, "server".to_string()).await?);
25///
26/// let tools = client.list_tools().await?;
27/// for tool_def in tools {
28///     let tool = McpToolBridge::new(Arc::clone(&client), tool_def);
29///     registry.register(tool);
30/// }
31/// ```
32pub struct McpToolBridge<T: McpTransport> {
33    client: Arc<McpClient<T>>,
34    definition: McpToolDefinition,
35    tier: ToolTier,
36}
37
38impl<T: McpTransport> McpToolBridge<T> {
39    /// Create a new MCP tool bridge.
40    #[must_use]
41    pub const fn new(client: Arc<McpClient<T>>, definition: McpToolDefinition) -> Self {
42        Self {
43            client,
44            definition,
45            tier: ToolTier::Confirm, // Default to Confirm for safety
46        }
47    }
48
49    /// Set the tool tier.
50    #[must_use]
51    pub const fn with_tier(mut self, tier: ToolTier) -> Self {
52        self.tier = tier;
53        self
54    }
55
56    /// Get the tool name.
57    #[must_use]
58    pub fn tool_name(&self) -> &str {
59        &self.definition.name
60    }
61
62    /// Get the tool definition.
63    #[must_use]
64    pub const fn definition(&self) -> &McpToolDefinition {
65        &self.definition
66    }
67}
68
69impl<T: McpTransport + 'static> Tool<()> for McpToolBridge<T> {
70    type Name = DynamicToolName;
71
72    fn name(&self) -> DynamicToolName {
73        DynamicToolName::new(&self.definition.name)
74    }
75
76    fn display_name(&self) -> &'static str {
77        // We need to leak the string to get a 'static lifetime
78        // This is acceptable since tool definitions are typically long-lived
79        Box::leak(self.definition.name.clone().into_boxed_str())
80    }
81
82    fn description(&self) -> &'static str {
83        let desc = self.definition.description.clone().unwrap_or_default();
84        Box::leak(desc.into_boxed_str())
85    }
86
87    fn input_schema(&self) -> Value {
88        self.definition.input_schema.clone()
89    }
90
91    fn tier(&self) -> ToolTier {
92        self.tier
93    }
94
95    async fn execute(&self, _ctx: &ToolContext<()>, input: Value) -> Result<ToolResult> {
96        let result = self.client.call_tool(&self.definition.name, input).await?;
97
98        // Convert MCP content to output string
99        let output = format_mcp_content(&result.content);
100
101        Ok(ToolResult {
102            success: !result.is_error,
103            output,
104            data: Some(serde_json::to_value(&result).unwrap_or_default()),
105            duration_ms: None,
106        })
107    }
108}
109
110/// Format MCP content items as a string.
111fn format_mcp_content(content: &[McpContent]) -> String {
112    let mut output = String::new();
113
114    for item in content {
115        match item {
116            McpContent::Text { text } => {
117                output.push_str(text);
118                output.push('\n');
119            }
120            McpContent::Image { mime_type, .. } => {
121                let _ = writeln!(output, "[Image: {mime_type}]");
122            }
123            McpContent::Resource { uri, text, .. } => {
124                if let Some(text) = text {
125                    output.push_str(text);
126                    output.push('\n');
127                } else {
128                    let _ = writeln!(output, "[Resource: {uri}]");
129                }
130            }
131        }
132    }
133
134    output.trim_end().to_string()
135}
136
137/// Register all tools from an MCP client into a tool registry.
138///
139/// # Arguments
140///
141/// * `registry` - The tool registry to add tools to
142/// * `client` - The MCP client to get tools from
143///
144/// # Errors
145///
146/// Returns an error if listing tools fails.
147///
148/// # Example
149///
150/// ```ignore
151/// use agent_sdk::mcp::{register_mcp_tools, McpClient, StdioTransport};
152/// use agent_sdk::ToolRegistry;
153///
154/// let transport = StdioTransport::spawn("npx", &["-y", "mcp-server"]).await?;
155/// let client = Arc::new(McpClient::new(transport, "server".to_string()).await?);
156///
157/// let mut registry = ToolRegistry::new();
158/// register_mcp_tools(&mut registry, client).await?;
159/// ```
160pub async fn register_mcp_tools<T: McpTransport + 'static>(
161    registry: &mut ToolRegistry<()>,
162    client: Arc<McpClient<T>>,
163) -> Result<()> {
164    let tools = client
165        .list_tools()
166        .await
167        .context("Failed to list MCP tools")?;
168
169    for definition in tools {
170        let bridge = McpToolBridge::new(Arc::clone(&client), definition);
171        registry.register(bridge);
172    }
173
174    Ok(())
175}
176
177/// Register MCP tools with custom tier assignment.
178///
179/// # Arguments
180///
181/// * `registry` - The tool registry to add tools to
182/// * `client` - The MCP client to get tools from
183/// * `tier_fn` - Function to determine tier for each tool
184///
185/// # Errors
186///
187/// Returns an error if listing tools fails.
188pub async fn register_mcp_tools_with_tiers<T, F>(
189    registry: &mut ToolRegistry<()>,
190    client: Arc<McpClient<T>>,
191    tier_fn: F,
192) -> Result<()>
193where
194    T: McpTransport + 'static,
195    F: Fn(&McpToolDefinition) -> ToolTier,
196{
197    let tools = client
198        .list_tools()
199        .await
200        .context("Failed to list MCP tools")?;
201
202    for definition in tools {
203        let tier = tier_fn(&definition);
204        let bridge = McpToolBridge::new(Arc::clone(&client), definition).with_tier(tier);
205        registry.register(bridge);
206    }
207
208    Ok(())
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_format_mcp_content_text() {
217        let content = vec![McpContent::Text {
218            text: "Hello, world!".to_string(),
219        }];
220
221        let output = format_mcp_content(&content);
222        assert_eq!(output, "Hello, world!");
223    }
224
225    #[test]
226    fn test_format_mcp_content_multiple() {
227        let content = vec![
228            McpContent::Text {
229                text: "First line".to_string(),
230            },
231            McpContent::Text {
232                text: "Second line".to_string(),
233            },
234        ];
235
236        let output = format_mcp_content(&content);
237        assert_eq!(output, "First line\nSecond line");
238    }
239
240    #[test]
241    fn test_format_mcp_content_image() {
242        let content = vec![McpContent::Image {
243            data: "base64data".to_string(),
244            mime_type: "image/png".to_string(),
245        }];
246
247        let output = format_mcp_content(&content);
248        assert_eq!(output, "[Image: image/png]");
249    }
250
251    #[test]
252    fn test_format_mcp_content_resource() {
253        let content = vec![McpContent::Resource {
254            uri: "file:///path/to/file".to_string(),
255            mime_type: Some("text/plain".to_string()),
256            text: None,
257        }];
258
259        let output = format_mcp_content(&content);
260        assert!(output.contains("file:///path/to/file"));
261    }
262
263    #[test]
264    fn test_format_mcp_content_resource_with_text() {
265        let content = vec![McpContent::Resource {
266            uri: "file:///path/to/file".to_string(),
267            mime_type: Some("text/plain".to_string()),
268            text: Some("File contents".to_string()),
269        }];
270
271        let output = format_mcp_content(&content);
272        assert_eq!(output, "File contents");
273    }
274
275    #[test]
276    fn test_format_mcp_content_empty() {
277        let content: Vec<McpContent> = vec![];
278        let output = format_mcp_content(&content);
279        assert!(output.is_empty());
280    }
281}