microclaw_engine/tools/
mcp.rs1use std::sync::Arc;
2
3use async_trait::async_trait;
4
5use crate::mcp::{McpServer, McpToolInfo};
6use microclaw_core::llm_types::ToolDefinition;
7
8use super::{Tool, ToolResult};
9
10pub struct McpTool {
11 server: Arc<McpServer>,
12 tool_info: McpToolInfo,
13 qualified_name: String,
14}
15
16impl McpTool {
17 pub fn new(server: Arc<McpServer>, tool_info: McpToolInfo) -> Self {
18 let qualified_name = format!("mcp_{}_{}", tool_info.server_name, tool_info.name);
20 let qualified_name: String = qualified_name
22 .chars()
23 .map(|c| {
24 if c.is_alphanumeric() || c == '_' || c == '-' {
25 c
26 } else {
27 '_'
28 }
29 })
30 .take(64)
31 .collect();
32
33 McpTool {
34 server,
35 tool_info,
36 qualified_name,
37 }
38 }
39
40 fn classify_mcp_error_type(err: &str) -> &'static str {
41 let lower = err.to_ascii_lowercase();
42 if lower.contains("rate-limited") {
43 "mcp_rate_limited"
44 } else if lower.contains("busy; exceeded queue wait") {
45 "mcp_bulkhead_rejected"
46 } else if lower.contains("circuit open") {
47 "mcp_circuit_open"
48 } else {
49 "mcp_error"
50 }
51 }
52
53 fn sanitize_input_for_mcp(input: serde_json::Value) -> serde_json::Value {
54 let mut map = input.as_object().cloned().unwrap_or_default();
55 map.retain(|k, _| !k.starts_with("__microclaw_"));
56 serde_json::Value::Object(map)
57 }
58}
59
60#[async_trait]
61impl Tool for McpTool {
62 fn name(&self) -> &str {
63 &self.qualified_name
64 }
65
66 fn definition(&self) -> ToolDefinition {
67 ToolDefinition {
68 name: self.qualified_name.clone(),
69 description: format!(
70 "[MCP:{}] {}",
71 self.tool_info.server_name, self.tool_info.description
72 ),
73 input_schema: self.tool_info.input_schema.clone(),
74 }
75 }
76
77 async fn execute(&self, input: serde_json::Value) -> ToolResult {
78 let sanitized_input = Self::sanitize_input_for_mcp(input);
79 match self
80 .server
81 .call_tool(&self.tool_info.name, sanitized_input)
82 .await
83 {
84 Ok(output) => ToolResult::success(output),
85 Err(e) => ToolResult::error(format!("MCP tool error: {e}"))
86 .with_error_type(Self::classify_mcp_error_type(&e)),
87 }
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::McpTool;
94 use serde_json::json;
95
96 #[test]
97 fn test_classify_mcp_error_type() {
98 assert_eq!(
99 McpTool::classify_mcp_error_type("rate-limited; retry in 3s"),
100 "mcp_rate_limited"
101 );
102 assert_eq!(
103 McpTool::classify_mcp_error_type("busy; exceeded queue wait of 200ms"),
104 "mcp_bulkhead_rejected"
105 );
106 assert_eq!(
107 McpTool::classify_mcp_error_type("circuit open; retry in 10s"),
108 "mcp_circuit_open"
109 );
110 assert_eq!(
111 McpTool::classify_mcp_error_type("transport disconnected"),
112 "mcp_error"
113 );
114 }
115
116 #[test]
117 fn test_sanitize_input_for_mcp_strips_internal_keys() {
118 let input = json!({
119 "query": "status",
120 "__microclaw_auth": {"caller_chat_id": 1},
121 "__microclaw_async": true,
122 "__microclaw_other": "x"
123 });
124 let out = McpTool::sanitize_input_for_mcp(input);
125 assert_eq!(out, json!({"query": "status"}));
126 }
127}