Skip to main content

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