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 documents: Vec::new(),
106 duration_ms: None,
107 })
108 }
109}
110
111fn format_mcp_content(content: &[McpContent]) -> String {
113 let mut output = String::new();
114
115 for item in content {
116 match item {
117 McpContent::Text { text } => {
118 output.push_str(text);
119 output.push('\n');
120 }
121 McpContent::Image { mime_type, .. } => {
122 let _ = writeln!(output, "[Image: {mime_type}]");
123 }
124 McpContent::Resource { uri, text, .. } => {
125 if let Some(text) = text {
126 output.push_str(text);
127 output.push('\n');
128 } else {
129 let _ = writeln!(output, "[Resource: {uri}]");
130 }
131 }
132 }
133 }
134
135 output.trim_end().to_string()
136}
137
138pub async fn register_mcp_tools<T: McpTransport + 'static>(
162 registry: &mut ToolRegistry<()>,
163 client: Arc<McpClient<T>>,
164) -> Result<()> {
165 let tools = client
166 .list_tools()
167 .await
168 .context("Failed to list MCP tools")?;
169
170 for definition in tools {
171 let bridge = McpToolBridge::new(Arc::clone(&client), definition);
172 registry.register(bridge);
173 }
174
175 Ok(())
176}
177
178pub async fn register_mcp_tools_with_tiers<T, F>(
190 registry: &mut ToolRegistry<()>,
191 client: Arc<McpClient<T>>,
192 tier_fn: F,
193) -> Result<()>
194where
195 T: McpTransport + 'static,
196 F: Fn(&McpToolDefinition) -> ToolTier,
197{
198 let tools = client
199 .list_tools()
200 .await
201 .context("Failed to list MCP tools")?;
202
203 for definition in tools {
204 let tier = tier_fn(&definition);
205 let bridge = McpToolBridge::new(Arc::clone(&client), definition).with_tier(tier);
206 registry.register(bridge);
207 }
208
209 Ok(())
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn test_format_mcp_content_text() {
218 let content = vec![McpContent::Text {
219 text: "Hello, world!".to_string(),
220 }];
221
222 let output = format_mcp_content(&content);
223 assert_eq!(output, "Hello, world!");
224 }
225
226 #[test]
227 fn test_format_mcp_content_multiple() {
228 let content = vec![
229 McpContent::Text {
230 text: "First line".to_string(),
231 },
232 McpContent::Text {
233 text: "Second line".to_string(),
234 },
235 ];
236
237 let output = format_mcp_content(&content);
238 assert_eq!(output, "First line\nSecond line");
239 }
240
241 #[test]
242 fn test_format_mcp_content_image() {
243 let content = vec![McpContent::Image {
244 data: "base64data".to_string(),
245 mime_type: "image/png".to_string(),
246 }];
247
248 let output = format_mcp_content(&content);
249 assert_eq!(output, "[Image: image/png]");
250 }
251
252 #[test]
253 fn test_format_mcp_content_resource() {
254 let content = vec![McpContent::Resource {
255 uri: "file:///path/to/file".to_string(),
256 mime_type: Some("text/plain".to_string()),
257 text: None,
258 }];
259
260 let output = format_mcp_content(&content);
261 assert!(output.contains("file:///path/to/file"));
262 }
263
264 #[test]
265 fn test_format_mcp_content_resource_with_text() {
266 let content = vec![McpContent::Resource {
267 uri: "file:///path/to/file".to_string(),
268 mime_type: Some("text/plain".to_string()),
269 text: Some("File contents".to_string()),
270 }];
271
272 let output = format_mcp_content(&content);
273 assert_eq!(output, "File contents");
274 }
275
276 #[test]
277 fn test_format_mcp_content_empty() {
278 let content: Vec<McpContent> = vec![];
279 let output = format_mcp_content(&content);
280 assert!(output.is_empty());
281 }
282}