agent_sdk/mcp/
tool_bridge.rs1use 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
14pub struct McpToolBridge<T: McpTransport> {
33 client: Arc<McpClient<T>>,
34 definition: McpToolDefinition,
35 tier: ToolTier,
36}
37
38impl<T: McpTransport> McpToolBridge<T> {
39 #[must_use]
41 pub const fn new(client: Arc<McpClient<T>>, definition: McpToolDefinition) -> Self {
42 Self {
43 client,
44 definition,
45 tier: ToolTier::Confirm, }
47 }
48
49 #[must_use]
51 pub const fn with_tier(mut self, tier: ToolTier) -> Self {
52 self.tier = tier;
53 self
54 }
55
56 #[must_use]
58 pub fn tool_name(&self) -> &str {
59 &self.definition.name
60 }
61
62 #[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 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 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
110fn 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
137pub 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
177pub 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}