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 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
16/// Wraps an MCP tool from a specific server as an `AgentTool`.
17pub struct McpToolWrapper {
18    /// The bridge — used to route tool calls to the correct server.
19    bridge: Arc<McpBridge>,
20    /// Full tool name `mcp:{server}:{tool}`.
21    full_name: String,
22    /// Name of the MCP server (used for routing).
23    server_name: String,
24    /// Tool name within the server.
25    tool_name: String,
26    /// Human-readable description from the MCP server.
27    description: String,
28    /// JSON Schema for the tool's input parameters.
29    input_schema: Value,
30}
31
32impl McpToolWrapper {
33    /// Create a new MCP tool wrapper.
34    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    /// Create an `McpToolWrapper` from a [`KernelHandle`] for a specific MCP tool.
53    ///
54    /// Extracts the MCP bridge from the kernel's MCP facade.
55    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
80/// Format an MCP content block for display.
81fn 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}