mermaid_cli/providers/tool/
mcp.rs1use async_trait::async_trait;
15
16use crate::domain::{ToolDefinition, ToolMetadata, ToolOutcome, ToolRunMetadata};
17use crate::mcp::{McpServerManager, manager_ref};
18
19use super::super::ctx::ExecContext;
20use super::ToolExecutor;
21
22pub struct McpToolProxy;
26
27#[async_trait]
28impl ToolExecutor for McpToolProxy {
29 fn name(&self) -> &'static str {
30 "mcp_proxy"
31 }
32
33 fn is_internal(&self) -> bool {
34 true
35 }
36
37 fn schema(&self) -> ToolDefinition {
38 ToolDefinition {
43 name: "mcp_proxy".to_string(),
44 description: "Internal dispatch target for mcp__* tool calls.".to_string(),
45 input_schema: serde_json::json!({
46 "type": "object",
47 "properties": {
48 "server_name": { "type": "string" },
49 "tool_name": { "type": "string" },
50 "arguments": { "type": "object" }
51 },
52 "required": ["server_name", "tool_name"]
53 }),
54 }
55 }
56
57 async fn execute(&self, args: serde_json::Value, ctx: ExecContext) -> ToolOutcome {
58 let Some(server_name) = args.get("server_name").and_then(|v| v.as_str()) else {
61 return ToolOutcome::error("mcp_proxy requires 'server_name'", 0.0);
62 };
63 let Some(tool_name) = args.get("tool_name").and_then(|v| v.as_str()) else {
64 return ToolOutcome::error("mcp_proxy requires 'tool_name'", 0.0);
65 };
66 let tool_args = args
67 .get("arguments")
68 .cloned()
69 .unwrap_or(serde_json::json!({}));
70
71 if !manager_ref::is_ready() {
76 let _ = tokio::time::timeout(
77 std::time::Duration::from_secs(10),
78 manager_ref::wait_ready(),
79 )
80 .await;
81 }
82 let Some(manager) = manager_ref::get() else {
83 return ToolOutcome::error("MCP servers not initialized", 0.0);
84 };
85
86 let start = std::time::Instant::now();
87 let call = manager.call_tool(server_name, tool_name, &tool_args);
88
89 tokio::select! {
90 biased;
91 _ = ctx.token.cancelled() => ToolOutcome::cancelled(),
92 result = call => match result {
93 Ok(tool_result) => {
94 let (text, images) = McpServerManager::format_tool_result(&tool_result);
95 let mut outcome = ToolOutcome::success(
96 text,
97 format!("{}:{} completed", server_name, tool_name),
98 start.elapsed().as_secs_f64(),
99 )
100 .with_metadata(mcp_metadata(server_name, tool_name));
101 if let Some(images) = images {
102 outcome = outcome.with_images(images);
103 }
104 outcome
105 },
106 Err(e) => ToolOutcome::error(
107 format!("mcp_proxy({}:{}): {}", server_name, tool_name, e),
108 start.elapsed().as_secs_f64(),
109 )
110 .with_metadata(mcp_metadata(server_name, tool_name)),
111 },
112 }
113 }
114}
115
116fn mcp_metadata(server_name: &str, tool_name: &str) -> ToolRunMetadata {
117 ToolRunMetadata {
118 detail: ToolMetadata::Mcp {
119 server: server_name.to_string(),
120 tool: tool_name.to_string(),
121 },
122 ..ToolRunMetadata::default()
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::domain::{ToolCallId, TurnId};
130 use crate::providers::ctx::test_exec_context;
131 use std::path::PathBuf;
132
133 #[tokio::test]
134 async fn missing_server_name_errors() {
135 let (ctx, _rx) = test_exec_context(TurnId(1), ToolCallId(1), PathBuf::from("/tmp"));
136 let outcome = McpToolProxy
137 .execute(serde_json::json!({"tool_name": "x"}), ctx)
138 .await;
139 assert_eq!(outcome.status, crate::domain::ToolStatus::Error);
140 }
141
142 #[tokio::test]
143 async fn missing_tool_name_errors() {
144 let (ctx, _rx) = test_exec_context(TurnId(1), ToolCallId(1), PathBuf::from("/tmp"));
145 let outcome = McpToolProxy
146 .execute(serde_json::json!({"server_name": "x"}), ctx)
147 .await;
148 assert_eq!(outcome.status, crate::domain::ToolStatus::Error);
149 }
150
151 #[tokio::test]
152 async fn uninitialized_manager_errors_cleanly() {
153 let (ctx, _rx) = test_exec_context(TurnId(1), ToolCallId(1), PathBuf::from("/tmp"));
156 let outcome = McpToolProxy
157 .execute(
158 serde_json::json!({"server_name": "s", "tool_name": "t"}),
159 ctx,
160 )
161 .await;
162 assert_eq!(outcome.status, crate::domain::ToolStatus::Error);
165 }
166}