Skip to main content

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