Skip to main content

oxios_kernel/mcp/
mod.rs

1//! MCP integration — adapters between `oxios-mcp` and the kernel.
2//!
3//! Re-exports all types from the `oxios-mcp` crate and provides conversion
4//! functions between MCP-native types and Oxios kernel types (`ToolDef`).
5
6pub use oxios_mcp::{
7    McpBridge, McpCapabilities, McpClient, McpContentBlock, McpError, McpRequest, McpResponse,
8    McpServer, McpServerConfig, McpTool, McpToolCallResult, McpToolsResult, MappedResource,
9    ClientInfo, InitializeParams, InitializeResult, ServerInfo,
10};
11
12use crate::program::{ArgumentDef, ToolDef};
13
14/// Convert an MCP tool to an Oxios `ToolDef`.
15///
16/// Parses the `input_schema` as a JSON Schema object, extracting
17/// properties from the top-level `"properties"` key.
18pub fn mcp_tool_to_tool_def(tool: &McpTool) -> ToolDef {
19    let arguments = if let Some(properties) = tool
20        .input_schema()
21        .get("properties")
22        .and_then(|p| p.as_object())
23    {
24        let required_list: Vec<&str> = tool
25            .input_schema()
26            .get("required")
27            .and_then(|r| r.as_array())
28            .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
29            .unwrap_or_default();
30
31        properties
32            .iter()
33            .map(|(name, schema)| {
34                let description = schema
35                    .get("description")
36                    .and_then(|d| d.as_str())
37                    .unwrap_or("No description")
38                    .to_string();
39                let required = required_list.iter().any(|r| *r == name)
40                    && schema.get("default").is_none();
41
42                ArgumentDef {
43                    name: name.clone(),
44                    description,
45                    required,
46                    default: schema
47                        .get("default")
48                        .and_then(|d| d.as_str().map(String::from)),
49                }
50            })
51            .collect()
52    } else {
53        Vec::new()
54    };
55
56    ToolDef {
57        name: tool.name().to_string(),
58        description: tool.description().to_string(),
59        arguments,
60        command: String::new(),
61    }
62}
63
64/// List all MCP tools as Oxios `ToolDef`s (existing API compatibility).
65pub async fn list_tool_defs(bridge: &McpBridge) -> anyhow::Result<Vec<ToolDef>> {
66    let tools = bridge.list_tools().await?;
67    Ok(tools.iter().map(mcp_tool_to_tool_def).collect())
68}
69
70/// Get cached MCP tools as Oxios `ToolDef`s for a specific server.
71pub async fn cached_tool_defs(bridge: &McpBridge, server_name: &str) -> Option<Vec<ToolDef>> {
72    bridge
73        .cached_tools(server_name)
74        .await
75        .map(|tools| tools.iter().map(mcp_tool_to_tool_def).collect())
76}
77
78/// Refresh and return MCP tools as Oxios `ToolDef`s for a specific server.
79pub async fn refresh_tool_defs(bridge: &McpBridge, server_name: &str) -> anyhow::Result<Vec<ToolDef>> {
80    let tools = bridge.refresh_tools(server_name).await?;
81    Ok(tools.iter().map(mcp_tool_to_tool_def).collect())
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn test_mcp_tool_to_tool_def() {
90        let mcp_tool = McpTool {
91            name: "test_tool".to_string(),
92            description: "A test tool".to_string(),
93            input_schema: serde_json::json!({
94                "type": "object",
95                "properties": {
96                    "arg1": {
97                        "type": "string",
98                        "description": "First argument"
99                    },
100                    "arg2": {
101                        "type": "number",
102                        "description": "Second argument",
103                        "default": "42"
104                    }
105                },
106                "required": ["arg1"]
107            }),
108        };
109
110        let tool_def = mcp_tool_to_tool_def(&mcp_tool);
111
112        assert_eq!(tool_def.name, "test_tool");
113        assert_eq!(tool_def.description, "A test tool");
114        assert_eq!(tool_def.arguments.len(), 2);
115
116        let arg1 = tool_def
117            .arguments
118            .iter()
119            .find(|a| a.name == "arg1")
120            .unwrap();
121        assert!(arg1.required);
122        assert_eq!(arg1.description, "First argument");
123
124        let arg2 = tool_def
125            .arguments
126            .iter()
127            .find(|a| a.name == "arg2")
128            .unwrap();
129        assert!(!arg2.required);
130        assert_eq!(arg2.default, Some("42".to_string()));
131    }
132
133    #[test]
134    fn test_mcp_tool_to_tool_def_no_properties() {
135        let mcp_tool = McpTool {
136            name: "simple".to_string(),
137            description: "No args".to_string(),
138            input_schema: serde_json::json!({"type": "object"}),
139        };
140
141        let tool_def = mcp_tool_to_tool_def(&mcp_tool);
142        assert!(tool_def.arguments.is_empty());
143        assert_eq!(tool_def.command, "");
144    }
145}