Skip to main content

oxios_kernel/mcp/
protocol.rs

1//! JSON-RPC 2.0 protocol types and MCP domain types.
2//!
3//! This module defines the wire types for MCP communication (JSON-RPC requests,
4//! responses, errors) and the domain-specific types for tools, capabilities,
5//! and initialization negotiation.
6
7use std::collections::HashMap;
8
9use anyhow::{anyhow, Result};
10use serde::{Deserialize, Serialize};
11
12use crate::program::ToolDef;
13
14// ---------------------------------------------------------------------------
15// Unique ID generator for JSON-RPC requests
16// ---------------------------------------------------------------------------
17
18static REQUEST_ID: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(1);
19
20pub(crate) fn next_request_id() -> usize {
21    REQUEST_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
22}
23
24// ---------------------------------------------------------------------------
25// MCP Server Configuration
26// ---------------------------------------------------------------------------
27
28/// Type alias for backwards compatibility — use [McpServer] directly.
29pub type McpServerConfig = McpServer;
30
31/// MCP server capability definition
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct McpServer {
34    /// Server name (unique identifier)
35    pub name: String,
36    /// Command to execute (e.g., "npx", "python")
37    pub command: String,
38    /// Command arguments
39    pub args: Vec<String>,
40    /// Environment variables
41    pub env: HashMap<String, String>,
42    /// Whether this server is enabled
43    pub enabled: bool,
44}
45
46impl McpServer {
47    /// Create a new MCP server configuration
48    pub fn new(name: &str, command: &str) -> Self {
49        Self {
50            name: name.to_string(),
51            command: command.to_string(),
52            args: Vec::new(),
53            env: HashMap::new(),
54            enabled: true,
55        }
56    }
57
58    /// Set command arguments
59    pub fn with_args(mut self, args: Vec<String>) -> Self {
60        self.args = args;
61        self
62    }
63
64    /// Add an environment variable
65    pub fn with_env(mut self, key: &str, value: &str) -> Self {
66        self.env.insert(key.to_string(), value.to_string());
67        self
68    }
69}
70
71// ---------------------------------------------------------------------------
72// JSON-RPC 2.0 Protocol Types
73// ---------------------------------------------------------------------------
74
75/// MCP JSON-RPC request structure
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct McpRequest {
78    /// JSON-RPC version (always "2.0")
79    pub jsonrpc: String,
80    /// Request ID for correlation
81    pub id: serde_json::Value,
82    /// Method name to invoke
83    pub method: String,
84    /// Optional method parameters
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub params: Option<serde_json::Value>,
87}
88
89impl McpRequest {
90    /// Create a new JSON-RPC request with an auto-generated ID
91    pub fn new(method: &str) -> Self {
92        Self::with_id(next_request_id(), method)
93    }
94
95    /// Create a new JSON-RPC request with a specific ID
96    pub fn with_id(id: usize, method: &str) -> Self {
97        Self {
98            jsonrpc: "2.0".to_string(),
99            id: serde_json::json!(id),
100            method: method.to_string(),
101            params: None,
102        }
103    }
104
105    /// Add parameters to the request
106    pub fn with_params(mut self, params: serde_json::Value) -> Self {
107        self.params = Some(params);
108        self
109    }
110
111    /// Serialize to a JSON-line (JSONL) bytes ready for stdio write
112    pub fn to_jsonl(&self) -> Result<Vec<u8>> {
113        let mut buf = serde_json::to_vec(self)?;
114        buf.push(b'\n');
115        Ok(buf)
116    }
117}
118
119/// MCP JSON-RPC response structure
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct McpResponse {
122    /// JSON-RPC version (always "2.0")
123    pub jsonrpc: String,
124    /// Response ID (matches request ID)
125    pub id: serde_json::Value,
126    /// Response result if successful
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub result: Option<serde_json::Value>,
129    /// Error if request failed
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub error: Option<McpError>,
132}
133
134/// MCP JSON-RPC error structure
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct McpError {
137    /// Error code
138    pub code: i32,
139    /// Error message
140    pub message: String,
141    /// Additional error data
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub data: Option<serde_json::Value>,
144}
145
146impl std::fmt::Display for McpError {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        let error_type = match self.code {
149            -32700 => "parse error",
150            -32600 => "invalid request",
151            -32601 => "method not found",
152            -32602 => "invalid params",
153            -32603 => "internal error",
154            -32099..=-32000 => "server error",
155            _ => "unknown error",
156        };
157        write!(f, "{} (code {}): {}", error_type, self.code, self.message)
158    }
159}
160
161impl McpError {
162    /// Create a new MCP error
163    pub fn new(code: i32, message: &str) -> Self {
164        Self {
165            code,
166            message: message.to_string(),
167            data: None,
168        }
169    }
170
171    /// JSON-RPC parse error (-32700)
172    pub fn parse_error() -> Self {
173        Self::new(-32700, "Parse error")
174    }
175
176    /// JSON-RPC invalid request (-32600)
177    pub fn invalid_request(msg: &str) -> Self {
178        Self::new(-32600, msg)
179    }
180
181    /// JSON-RPC method not found (-32601)
182    pub fn method_not_found() -> Self {
183        Self::new(-32601, "Method not found")
184    }
185
186    /// JSON-RPC invalid params (-32602)
187    pub fn invalid_params() -> Self {
188        Self::new(-32602, "Invalid params")
189    }
190
191    /// JSON-RPC internal error (-32603)
192    pub fn internal_error(msg: &str) -> Self {
193        Self::new(-32603, msg)
194    }
195
196    /// Server error (codes -32000 to -32099)
197    pub fn server_error(msg: &str) -> Self {
198        Self::new(-32000, msg)
199    }
200}
201
202impl McpResponse {
203    /// Check if this response contains an error
204    pub fn is_error(&self) -> bool {
205        self.error.is_some()
206    }
207
208    /// Extract the result value, erroring if there is one
209    pub fn into_result(self) -> Result<serde_json::Value> {
210        if let Some(err) = self.error {
211            return Err(anyhow!("{}", err));
212        }
213        Ok(self.result.unwrap_or(serde_json::Value::Null))
214    }
215}
216
217// ---------------------------------------------------------------------------
218// MCP Capability Negotiation
219// ---------------------------------------------------------------------------
220
221/// MCP server capabilities
222#[derive(Debug, Clone, Serialize, Deserialize, Default)]
223pub struct McpCapabilities {
224    /// Whether the server supports tools
225    pub tools: bool,
226    /// Whether the server supports resources
227    pub resources: bool,
228    /// Whether the server supports prompts
229    pub prompts: bool,
230}
231
232/// Initialize request params
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct InitializeParams {
235    /// Protocol version string.
236    pub protocol_version: String,
237    /// Client capabilities.
238    pub capabilities: McpCapabilities,
239    /// Information about the connecting client.
240    pub client_info: ClientInfo,
241}
242
243/// Client info sent during initialize
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct ClientInfo {
246    /// Client name.
247    pub name: String,
248    /// Client version.
249    pub version: String,
250}
251
252impl Default for InitializeParams {
253    fn default() -> Self {
254        Self {
255            protocol_version: "2024-11-05".to_string(),
256            capabilities: McpCapabilities::default(),
257            client_info: ClientInfo {
258                name: "oxios".to_string(),
259                version: env!("CARGO_PKG_VERSION").to_string(),
260            },
261        }
262    }
263}
264
265/// Initialize response from the server
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct InitializeResult {
268    /// Protocol version agreed upon.
269    pub protocol_version: String,
270    /// Server capabilities.
271    pub capabilities: McpCapabilities,
272    /// Information about the server.
273    pub server_info: ServerInfo,
274}
275
276/// Server info from initialize response
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct ServerInfo {
279    /// Server name.
280    pub name: String,
281    /// Server version.
282    pub version: String,
283}
284
285// ---------------------------------------------------------------------------
286// MCP Tool Types
287// ---------------------------------------------------------------------------
288
289/// MCP tool definition from a server
290#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct McpTool {
292    /// Tool name (unique within the server)
293    pub name: String,
294    /// Brief description of what the tool does
295    pub description: String,
296    /// JSON Schema for tool input arguments
297    pub input_schema: serde_json::Value,
298}
299
300impl McpTool {
301    /// Convert an MCP tool to an oxios ToolDef.
302    ///
303    /// Parses the `input_schema` as a JSON Schema object, extracting
304    /// properties from the top-level `"properties"` key (not from the
305    /// root object, which contains `type`, `properties`, `required`, etc.).
306    pub fn to_tool_def(&self) -> ToolDef {
307        let arguments = if let Some(properties) = self
308            .input_schema
309            .get("properties")
310            .and_then(|p| p.as_object())
311        {
312            let required_list: Vec<&str> = self
313                .input_schema
314                .get("required")
315                .and_then(|r| r.as_array())
316                .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
317                .unwrap_or_default();
318
319            properties
320                .iter()
321                .map(|(name, schema)| {
322                    let description = schema
323                        .get("description")
324                        .and_then(|d| d.as_str())
325                        .unwrap_or("No description")
326                        .to_string();
327                    let required =
328                        required_list.iter().any(|r| *r == name) && schema.get("default").is_none();
329
330                    crate::program::ArgumentDef {
331                        name: name.clone(),
332                        description,
333                        required,
334                        default: schema
335                            .get("default")
336                            .and_then(|d| d.as_str().map(String::from)),
337                    }
338                })
339                .collect()
340        } else {
341            Vec::new()
342        };
343
344        ToolDef {
345            name: self.name.clone(),
346            description: self.description.clone(),
347            arguments,
348            command: String::new(), // MCP tools don't use command-based execution
349        }
350    }
351}
352
353/// MCP tools/list result
354#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct McpToolsResult {
356    /// Available tools from the server.
357    pub tools: Vec<McpTool>,
358}
359
360/// MCP tools/call result
361#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct McpToolCallResult {
363    /// Content blocks returned by the tool.
364    pub content: Vec<McpContentBlock>,
365    /// Whether the result is an error.
366    pub is_error: Option<bool>,
367}
368
369/// Content block in a tool call result
370#[derive(Debug, Clone, Serialize, Deserialize)]
371#[serde(tag = "type")]
372pub enum McpContentBlock {
373    /// Plain text content.
374    #[serde(rename = "text")]
375    Text {
376        /// The text content.
377        text: String,
378    },
379    /// Base64-encoded image data.
380    #[serde(rename = "image")]
381    Image {
382        /// Base64-encoded image data.
383        data: String,
384        /// MIME type of the image.
385        mime_type: Option<String>,
386    },
387    /// Embedded resource reference.
388    #[serde(rename = "resource")]
389    Resource {
390        /// The referenced resource.
391        resource: MappedResource,
392    },
393}
394
395/// Resource reference
396#[derive(Debug, Clone, Serialize, Deserialize)]
397pub struct MappedResource {
398    /// URI of the resource.
399    pub uri: String,
400    /// MIME type of the resource.
401    pub mime_type: Option<String>,
402}