agent_sdk/mcp/
tool_bridge.rs1use crate::tools::{Tool, ToolContext, ToolRegistry};
4use crate::types::{ToolResult, ToolTier};
5use anyhow::{Context, Result};
6use async_trait::async_trait;
7use serde_json::Value;
8use std::fmt::Write;
9use std::sync::Arc;
10
11use super::client::McpClient;
12use super::protocol::{McpContent, McpToolDefinition};
13use super::transport::McpTransport;
14
15pub struct McpToolBridge<T: McpTransport> {
34 client: Arc<McpClient<T>>,
35 definition: McpToolDefinition,
36 tier: ToolTier,
37}
38
39impl<T: McpTransport> McpToolBridge<T> {
40 #[must_use]
42 pub const fn new(client: Arc<McpClient<T>>, definition: McpToolDefinition) -> Self {
43 Self {
44 client,
45 definition,
46 tier: ToolTier::Confirm, }
48 }
49
50 #[must_use]
52 pub const fn with_tier(mut self, tier: ToolTier) -> Self {
53 self.tier = tier;
54 self
55 }
56
57 #[must_use]
59 pub fn tool_name(&self) -> &str {
60 &self.definition.name
61 }
62
63 #[must_use]
65 pub const fn definition(&self) -> &McpToolDefinition {
66 &self.definition
67 }
68}
69
70#[async_trait]
71impl<T: McpTransport + 'static> Tool<()> for McpToolBridge<T> {
72 fn name(&self) -> &'static str {
73 Box::leak(self.definition.name.clone().into_boxed_str())
76 }
77
78 fn description(&self) -> &'static str {
79 let desc = self.definition.description.clone().unwrap_or_default();
80 Box::leak(desc.into_boxed_str())
81 }
82
83 fn input_schema(&self) -> Value {
84 self.definition.input_schema.clone()
85 }
86
87 fn tier(&self) -> ToolTier {
88 self.tier
89 }
90
91 async fn execute(&self, _ctx: &ToolContext<()>, input: Value) -> Result<ToolResult> {
92 let result = self.client.call_tool(&self.definition.name, input).await?;
93
94 let output = format_mcp_content(&result.content);
96
97 Ok(ToolResult {
98 success: !result.is_error,
99 output,
100 data: Some(serde_json::to_value(&result).unwrap_or_default()),
101 duration_ms: None,
102 })
103 }
104}
105
106fn format_mcp_content(content: &[McpContent]) -> String {
108 let mut output = String::new();
109
110 for item in content {
111 match item {
112 McpContent::Text { text } => {
113 output.push_str(text);
114 output.push('\n');
115 }
116 McpContent::Image { mime_type, .. } => {
117 let _ = writeln!(output, "[Image: {mime_type}]");
118 }
119 McpContent::Resource { uri, text, .. } => {
120 if let Some(text) = text {
121 output.push_str(text);
122 output.push('\n');
123 } else {
124 let _ = writeln!(output, "[Resource: {uri}]");
125 }
126 }
127 }
128 }
129
130 output.trim_end().to_string()
131}
132
133pub async fn register_mcp_tools<T: McpTransport + 'static>(
157 registry: &mut ToolRegistry<()>,
158 client: Arc<McpClient<T>>,
159) -> Result<()> {
160 let tools = client
161 .list_tools()
162 .await
163 .context("Failed to list MCP tools")?;
164
165 for definition in tools {
166 let bridge = McpToolBridge::new(Arc::clone(&client), definition);
167 registry.register(bridge);
168 }
169
170 Ok(())
171}
172
173pub async fn register_mcp_tools_with_tiers<T, F>(
185 registry: &mut ToolRegistry<()>,
186 client: Arc<McpClient<T>>,
187 tier_fn: F,
188) -> Result<()>
189where
190 T: McpTransport + 'static,
191 F: Fn(&McpToolDefinition) -> ToolTier,
192{
193 let tools = client
194 .list_tools()
195 .await
196 .context("Failed to list MCP tools")?;
197
198 for definition in tools {
199 let tier = tier_fn(&definition);
200 let bridge = McpToolBridge::new(Arc::clone(&client), definition).with_tier(tier);
201 registry.register(bridge);
202 }
203
204 Ok(())
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_format_mcp_content_text() {
213 let content = vec![McpContent::Text {
214 text: "Hello, world!".to_string(),
215 }];
216
217 let output = format_mcp_content(&content);
218 assert_eq!(output, "Hello, world!");
219 }
220
221 #[test]
222 fn test_format_mcp_content_multiple() {
223 let content = vec![
224 McpContent::Text {
225 text: "First line".to_string(),
226 },
227 McpContent::Text {
228 text: "Second line".to_string(),
229 },
230 ];
231
232 let output = format_mcp_content(&content);
233 assert_eq!(output, "First line\nSecond line");
234 }
235
236 #[test]
237 fn test_format_mcp_content_image() {
238 let content = vec![McpContent::Image {
239 data: "base64data".to_string(),
240 mime_type: "image/png".to_string(),
241 }];
242
243 let output = format_mcp_content(&content);
244 assert_eq!(output, "[Image: image/png]");
245 }
246
247 #[test]
248 fn test_format_mcp_content_resource() {
249 let content = vec![McpContent::Resource {
250 uri: "file:///path/to/file".to_string(),
251 mime_type: Some("text/plain".to_string()),
252 text: None,
253 }];
254
255 let output = format_mcp_content(&content);
256 assert!(output.contains("file:///path/to/file"));
257 }
258
259 #[test]
260 fn test_format_mcp_content_resource_with_text() {
261 let content = vec![McpContent::Resource {
262 uri: "file:///path/to/file".to_string(),
263 mime_type: Some("text/plain".to_string()),
264 text: Some("File contents".to_string()),
265 }];
266
267 let output = format_mcp_content(&content);
268 assert_eq!(output, "File contents");
269 }
270
271 #[test]
272 fn test_format_mcp_content_empty() {
273 let content: Vec<McpContent> = vec![];
274 let output = format_mcp_content(&content);
275 assert!(output.is_empty());
276 }
277}