Skip to main content

oxios_kernel/tools/
mcp_tool.rs

1//! MCP tool wrapper — exposes MCP server tools as AgentTool implementations.
2//!
3//! MCP tools from external servers (via the Model Context Protocol) are wrapped
4//! to implement the `AgentTool` trait. The full tool name is namespaced as
5//! `mcp:{server_name}:{tool_name}` to avoid collisions with Tier 1-2 tools.
6
7use async_trait::async_trait;
8use std::sync::Arc;
9
10use oxi_sdk::{AgentTool, AgentToolResult, ToolContext};
11use serde_json::Value;
12
13use crate::mcp::{McpBridge, McpContentBlock};
14
15/// Wraps an MCP tool from a specific server as an `AgentTool`.
16pub struct McpToolWrapper {
17    /// The bridge — used to route tool calls to the correct server.
18    bridge: Arc<McpBridge>,
19    /// Full tool name `mcp:{server}:{tool}`.
20    full_name: String,
21    /// Name of the MCP server (used for routing).
22    server_name: String,
23    /// Tool name within the server.
24    tool_name: String,
25    /// Human-readable description from the MCP server.
26    description: String,
27    /// JSON Schema for the tool's input parameters.
28    input_schema: Value,
29}
30
31impl McpToolWrapper {
32    /// Create a new MCP tool wrapper.
33    pub fn new(
34        bridge: Arc<McpBridge>,
35        server_name: &str,
36        tool_name: &str,
37        description: String,
38        input_schema: Value,
39    ) -> Self {
40        let full_name = format!("mcp:{server_name}:{tool_name}");
41        Self {
42            bridge,
43            full_name,
44            server_name: server_name.to_string(),
45            tool_name: tool_name.to_string(),
46            description,
47            input_schema,
48        }
49    }
50
51    /// Create an `McpToolWrapper` from a [`KernelHandle`] for a specific MCP tool.
52    ///
53    /// Extracts the MCP bridge from the kernel's MCP facade.
54    pub fn from_kernel(
55        kernel: &crate::kernel_handle::KernelHandle,
56        server_name: &str,
57        tool_name: &str,
58        description: String,
59        input_schema: Value,
60    ) -> Self {
61        Self::new(
62            kernel.mcp.bridge().clone(),
63            server_name,
64            tool_name,
65            description,
66            input_schema,
67        )
68    }
69}
70
71impl std::fmt::Debug for McpToolWrapper {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        f.debug_struct("McpToolWrapper")
74            .field("full_name", &self.full_name)
75            .finish()
76    }
77}
78
79/// Format an MCP content block for display.
80fn format_content_block(block: &McpContentBlock) -> String {
81    match block {
82        McpContentBlock::Text { text } => text.clone(),
83        McpContentBlock::Image { data, mime_type } => {
84            format!(
85                "[Image ({}): {} bytes]",
86                mime_type.as_deref().unwrap_or("?"),
87                data.len()
88            )
89        }
90        McpContentBlock::Resource { resource } => {
91            format!("[Resource: {}]", resource.uri)
92        }
93    }
94}
95
96#[async_trait]
97
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<tokio::sync::oneshot::Receiver<()>>,
120        _ctx: &ToolContext,
121    ) -> Result<AgentToolResult, oxi_sdk::ToolError> {
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}