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    ClientInfo, InitializeParams, InitializeResult, MappedResource, McpBridge, McpCapabilities,
8    McpClient, McpContentBlock, McpError, McpRequest, McpResponse, McpServer, McpServerConfig,
9    McpTool, McpToolCallResult, McpToolsResult, 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 =
40                    required_list.iter().any(|r| *r == name) && 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(
80    bridge: &McpBridge,
81    server_name: &str,
82) -> anyhow::Result<Vec<ToolDef>> {
83    let tools = bridge.refresh_tools(server_name).await?;
84    Ok(tools.iter().map(mcp_tool_to_tool_def).collect())
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn test_mcp_tool_to_tool_def() {
93        let mcp_tool = McpTool {
94            name: "test_tool".to_string(),
95            description: "A test tool".to_string(),
96            input_schema: serde_json::json!({
97                "type": "object",
98                "properties": {
99                    "arg1": {
100                        "type": "string",
101                        "description": "First argument"
102                    },
103                    "arg2": {
104                        "type": "number",
105                        "description": "Second argument",
106                        "default": "42"
107                    }
108                },
109                "required": ["arg1"]
110            }),
111        };
112
113        let tool_def = mcp_tool_to_tool_def(&mcp_tool);
114
115        assert_eq!(tool_def.name, "test_tool");
116        assert_eq!(tool_def.description, "A test tool");
117        assert_eq!(tool_def.arguments.len(), 2);
118
119        let arg1 = tool_def
120            .arguments
121            .iter()
122            .find(|a| a.name == "arg1")
123            .unwrap();
124        assert!(arg1.required);
125        assert_eq!(arg1.description, "First argument");
126
127        let arg2 = tool_def
128            .arguments
129            .iter()
130            .find(|a| a.name == "arg2")
131            .unwrap();
132        assert!(!arg2.required);
133        assert_eq!(arg2.default, Some("42".to_string()));
134    }
135
136    #[test]
137    fn test_mcp_tool_to_tool_def_no_properties() {
138        let mcp_tool = McpTool {
139            name: "simple".to_string(),
140            description: "No args".to_string(),
141            input_schema: serde_json::json!({"type": "object"}),
142        };
143
144        let tool_def = mcp_tool_to_tool_def(&mcp_tool);
145        assert!(tool_def.arguments.is_empty());
146        assert_eq!(tool_def.command, "");
147    }
148}