oxios_kernel/tools/
mcp_tool.rs1use std::sync::Arc;
8
9use async_trait::async_trait;
10use oxi_sdk::{AgentTool, AgentToolResult, ToolContext};
11use serde_json::Value;
12use tokio::sync::oneshot;
13
14use crate::mcp::{McpBridge, McpContentBlock};
15
16pub struct McpToolWrapper {
18 bridge: Arc<McpBridge>,
20 full_name: String,
22 server_name: String,
24 tool_name: String,
26 description: String,
28 input_schema: Value,
30}
31
32impl McpToolWrapper {
33 pub fn new(
35 bridge: Arc<McpBridge>,
36 server_name: &str,
37 tool_name: &str,
38 description: String,
39 input_schema: Value,
40 ) -> Self {
41 let full_name = format!("mcp:{}:{}", server_name, tool_name);
42 Self {
43 bridge,
44 full_name,
45 server_name: server_name.to_string(),
46 tool_name: tool_name.to_string(),
47 description,
48 input_schema,
49 }
50 }
51
52 pub fn from_kernel(
56 kernel: &crate::kernel_handle::KernelHandle,
57 server_name: &str,
58 tool_name: &str,
59 description: String,
60 input_schema: Value,
61 ) -> Self {
62 Self::new(
63 kernel.mcp.bridge().clone(),
64 server_name,
65 tool_name,
66 description,
67 input_schema,
68 )
69 }
70}
71
72impl std::fmt::Debug for McpToolWrapper {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 f.debug_struct("McpToolWrapper")
75 .field("full_name", &self.full_name)
76 .finish()
77 }
78}
79
80fn format_content_block(block: &McpContentBlock) -> String {
82 match block {
83 McpContentBlock::Text { text } => text.clone(),
84 McpContentBlock::Image { data, mime_type } => {
85 format!(
86 "[Image ({}): {} bytes]",
87 mime_type.as_deref().unwrap_or("?"),
88 data.len()
89 )
90 }
91 McpContentBlock::Resource { resource } => {
92 format!("[Resource: {}]", resource.uri)
93 }
94 }
95}
96
97#[async_trait]
98impl AgentTool for McpToolWrapper {
99 fn name(&self) -> &str {
100 &self.full_name
101 }
102
103 fn label(&self) -> &str {
104 &self.tool_name
105 }
106
107 fn description(&self) -> &str {
108 &self.description
109 }
110
111 fn parameters_schema(&self) -> Value {
112 self.input_schema.clone()
113 }
114
115 async fn execute(
116 &self,
117 _tool_call_id: &str,
118 params: Value,
119 _signal: Option<oneshot::Receiver<()>>,
120 _ctx: &ToolContext,
121 ) -> Result<AgentToolResult, String> {
122 match self
123 .bridge
124 .call_tool(&self.server_name, &self.tool_name, params)
125 .await
126 {
127 Ok(result) => {
128 let output = if result.content.is_empty() {
129 "(no output)".to_string()
130 } else {
131 result
132 .content
133 .iter()
134 .map(format_content_block)
135 .collect::<Vec<_>>()
136 .join("\n")
137 };
138
139 let is_error = result.is_error.unwrap_or(false);
140 if is_error {
141 Ok(AgentToolResult::error(output))
142 } else {
143 Ok(AgentToolResult::success(output))
144 }
145 }
146 Err(e) => {
147 tracing::error!(
148 server = %self.server_name,
149 tool = %self.tool_name,
150 error = %e,
151 "MCP tool call failed"
152 );
153 Ok(AgentToolResult::error(format!(
154 "MCP tool '{}/{}' failed: {}",
155 self.server_name, self.tool_name, e
156 )))
157 }
158 }
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn test_tool_wrapper_debug() {
168 let wrapper = McpToolWrapper::new(
169 Arc::new(McpBridge::new()),
170 "test-server",
171 "test_tool",
172 "A test tool".to_string(),
173 serde_json::json!({
174 "type": "object",
175 "properties": {
176 "arg": {
177 "type": "string",
178 "description": "An argument"
179 }
180 }
181 }),
182 );
183 let debug = format!("{:?}", wrapper);
184 assert!(debug.contains("test-server"));
185 assert!(debug.contains("test_tool"));
186 }
187
188 #[test]
189 fn test_name_format() {
190 let wrapper = McpToolWrapper::new(
191 Arc::new(McpBridge::new()),
192 "github",
193 "create_pr",
194 "Create a PR".to_string(),
195 serde_json::json!({"type": "object", "properties": {}}),
196 );
197 assert_eq!(wrapper.name(), "mcp:github:create_pr");
198 assert_eq!(wrapper.label(), "create_pr");
199 assert_eq!(wrapper.description(), "Create a PR");
200 }
201}