Skip to main content

brainwires_mcp_client/
types.rs

1//! MCP Protocol Types
2//!
3//! This module provides type definitions for the Model Context Protocol.
4//! It now uses the official `rmcp` crate with compatibility aliases for
5//! backward compatibility during migration.
6
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10// Re-export rmcp types with compatibility aliases (native only)
11#[cfg(feature = "native")]
12pub use rmcp::model::{
13    CallToolRequestParams, CallToolResult, Content, Prompt as RmcpPrompt, ProtocolVersion,
14    Resource as RmcpResource, Tool as RmcpTool,
15};
16
17// Re-export capabilities (native only)
18#[cfg(feature = "native")]
19pub use rmcp::model::{
20    ClientCapabilities as RmcpClientCapabilities, PromptsCapability, ResourcesCapability,
21    ServerCapabilities as RmcpServerCapabilities, ToolsCapability,
22};
23
24// ===========================================================================
25// BACKWARD COMPATIBILITY ALIASES (native only - require rmcp)
26// ===========================================================================
27
28#[cfg(feature = "native")]
29/// Compatibility alias for Tool
30pub type McpTool = RmcpTool;
31
32#[cfg(feature = "native")]
33/// Compatibility alias for Resource
34pub type McpResource = RmcpResource;
35
36#[cfg(feature = "native")]
37/// Compatibility alias for Prompt
38pub type McpPrompt = RmcpPrompt;
39
40#[cfg(feature = "native")]
41/// Compatibility alias for CallToolParams
42pub type CallToolParams = CallToolRequestParams;
43
44#[cfg(feature = "native")]
45/// Compatibility alias for ServerCapabilities
46pub type ServerCapabilities = RmcpServerCapabilities;
47
48#[cfg(feature = "native")]
49/// Compatibility alias for ClientCapabilities
50pub type ClientCapabilities = RmcpClientCapabilities;
51
52// ===========================================================================
53// ADDITIONAL TYPES NOT DIRECTLY PROVIDED BY RMCP
54// ===========================================================================
55// These types are still custom as they handle JSON-RPC layer or are
56// specific to our implementation
57
58/// JSON-RPC 2.0 Request
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct JsonRpcRequest {
61    /// JSON-RPC version (always "2.0").
62    pub jsonrpc: String,
63    /// Request identifier.
64    pub id: serde_json::Value,
65    /// Method name.
66    pub method: String,
67    /// Optional parameters.
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub params: Option<Value>,
70}
71
72impl JsonRpcRequest {
73    /// Create a new JSON-RPC request.
74    /// Returns an error if params cannot be serialized to JSON.
75    pub fn new<T: Serialize>(
76        id: impl Into<Value>,
77        method: String,
78        params: Option<T>,
79    ) -> Result<Self, serde_json::Error> {
80        let params_value = match params {
81            Some(p) => Some(serde_json::to_value(p)?),
82            None => None,
83        };
84        Ok(Self {
85            jsonrpc: "2.0".to_string(),
86            id: id.into(),
87            method,
88            params: params_value,
89        })
90    }
91
92    /// Create a new JSON-RPC request, panicking if serialization fails.
93    /// Use this only when you're certain serialization cannot fail.
94    pub fn new_unchecked<T: Serialize>(
95        id: impl Into<Value>,
96        method: String,
97        params: Option<T>,
98    ) -> Self {
99        Self::new(id, method, params).expect("Failed to serialize JSON-RPC request params")
100    }
101}
102
103/// JSON-RPC 2.0 Response
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct JsonRpcResponse {
106    /// JSON-RPC version (always "2.0").
107    pub jsonrpc: String,
108    /// Response identifier matching the request.
109    pub id: serde_json::Value,
110    /// Result value on success.
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub result: Option<Value>,
113    /// Error object on failure.
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub error: Option<JsonRpcError>,
116}
117
118/// JSON-RPC 2.0 Error
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct JsonRpcError {
121    /// Error code.
122    pub code: i32,
123    /// Error message.
124    pub message: String,
125    /// Optional additional data.
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub data: Option<Value>,
128}
129
130/// JSON-RPC 2.0 Notification
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct JsonRpcNotification {
133    /// JSON-RPC version (always "2.0").
134    pub jsonrpc: String,
135    /// Notification method name.
136    pub method: String,
137    /// Optional parameters.
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub params: Option<Value>,
140}
141
142impl JsonRpcNotification {
143    /// Create a new JSON-RPC notification (no id field).
144    /// Returns an error if params cannot be serialized to JSON.
145    pub fn new<T: Serialize>(
146        method: impl Into<String>,
147        params: Option<T>,
148    ) -> Result<Self, serde_json::Error> {
149        let params_value = match params {
150            Some(p) => Some(serde_json::to_value(p)?),
151            None => None,
152        };
153        Ok(Self {
154            jsonrpc: "2.0".to_string(),
155            method: method.into(),
156            params: params_value,
157        })
158    }
159
160    /// Create a new JSON-RPC notification, panicking if serialization fails.
161    /// Use this only when you're certain serialization cannot fail.
162    pub fn new_unchecked<T: Serialize>(method: impl Into<String>, params: Option<T>) -> Self {
163        Self::new(method, params).expect("Failed to serialize JSON-RPC notification params")
164    }
165}
166
167/// Generic JSON-RPC message that could be a response or notification
168/// Used for bidirectional MCP communication where servers can send notifications
169#[derive(Debug, Clone)]
170pub enum JsonRpcMessage {
171    /// A response to a request (has id field)
172    Response(JsonRpcResponse),
173    /// A server-initiated notification (no id field)
174    Notification(JsonRpcNotification),
175}
176
177impl JsonRpcMessage {
178    /// Check if this is a response
179    pub fn is_response(&self) -> bool {
180        matches!(self, JsonRpcMessage::Response(_))
181    }
182
183    /// Check if this is a notification
184    pub fn is_notification(&self) -> bool {
185        matches!(self, JsonRpcMessage::Notification(_))
186    }
187
188    /// Try to get the response if this is one
189    pub fn as_response(self) -> Option<JsonRpcResponse> {
190        match self {
191            JsonRpcMessage::Response(r) => Some(r),
192            _ => None,
193        }
194    }
195
196    /// Try to get the notification if this is one
197    pub fn as_notification(self) -> Option<JsonRpcNotification> {
198        match self {
199            JsonRpcMessage::Notification(n) => Some(n),
200            _ => None,
201        }
202    }
203}
204
205// ===========================================================================
206// MCP PROGRESS NOTIFICATION TYPES
207// ===========================================================================
208
209/// Progress notification parameters from MCP server
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct ProgressParams {
212    /// Token identifying which request this progress is for
213    #[serde(rename = "progressToken")]
214    pub progress_token: String,
215    /// Current progress value
216    pub progress: f64,
217    /// Total expected value (for calculating percentage)
218    pub total: Option<f64>,
219    /// Human-readable progress message
220    pub message: Option<String>,
221}
222
223/// Parsed MCP notification types
224#[derive(Debug, Clone)]
225pub enum McpNotification {
226    /// Progress update for a long-running operation
227    Progress(ProgressParams),
228    /// Unknown/unhandled notification type
229    Unknown {
230        /// The notification method name.
231        method: String,
232        /// Optional notification parameters.
233        params: Option<Value>,
234    },
235}
236
237impl McpNotification {
238    /// Parse a JsonRpcNotification into a typed McpNotification
239    pub fn from_notification(notif: &JsonRpcNotification) -> Self {
240        match notif.method.as_str() {
241            "notifications/progress" => {
242                if let Some(ref params) = notif.params
243                    && let Ok(progress) = serde_json::from_value::<ProgressParams>(params.clone())
244                {
245                    return McpNotification::Progress(progress);
246                }
247                McpNotification::Unknown {
248                    method: notif.method.clone(),
249                    params: notif.params.clone(),
250                }
251            }
252            _ => McpNotification::Unknown {
253                method: notif.method.clone(),
254                params: notif.params.clone(),
255            },
256        }
257    }
258}
259
260// ===========================================================================
261// MCP INITIALIZATION TYPES
262// ===========================================================================
263
264// ===========================================================================
265// MCP TYPES (require rmcp - native only)
266// ===========================================================================
267
268#[cfg(feature = "native")]
269/// MCP Initialize Request Parameters
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct InitializeParams {
272    /// Protocol version string.
273    #[serde(rename = "protocolVersion")]
274    pub protocol_version: String,
275    /// Client capabilities.
276    pub capabilities: ClientCapabilities,
277    /// Client identification info.
278    #[serde(rename = "clientInfo")]
279    pub client_info: ClientInfo,
280}
281
282/// MCP client identification.
283#[cfg(feature = "native")]
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct ClientInfo {
286    /// Client name.
287    pub name: String,
288    /// Client version.
289    pub version: String,
290}
291
292#[cfg(feature = "native")]
293/// MCP Initialize Result
294#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct InitializeResult {
296    /// Protocol version string.
297    #[serde(rename = "protocolVersion")]
298    pub protocol_version: String,
299    /// Server capabilities.
300    pub capabilities: ServerCapabilities,
301    /// Server identification info.
302    #[serde(rename = "serverInfo")]
303    pub server_info: ServerInfo,
304}
305
306/// MCP server identification.
307#[cfg(feature = "native")]
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct ServerInfo {
310    /// Server name.
311    pub name: String,
312    /// Server version.
313    pub version: String,
314}
315
316#[cfg(feature = "native")]
317/// Tools List Response
318#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct ListToolsResult {
320    /// List of available tools.
321    pub tools: Vec<McpTool>,
322}
323
324#[cfg(feature = "native")]
325/// Resources List Response
326#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct ListResourcesResult {
328    /// List of available resources.
329    pub resources: Vec<McpResource>,
330}
331
332#[cfg(feature = "native")]
333/// Prompts List Response
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct ListPromptsResult {
336    /// List of available prompts.
337    pub prompts: Vec<McpPrompt>,
338}
339
340#[cfg(feature = "native")]
341/// Resource Read Request
342#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct ReadResourceParams {
344    /// Resource URI to read.
345    pub uri: String,
346}
347
348#[cfg(feature = "native")]
349/// Resource Read Result
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct ReadResourceResult {
352    /// Resource contents.
353    pub contents: Vec<ResourceContent>,
354}
355
356/// Content of a resource (text or binary blob).
357#[cfg(feature = "native")]
358#[derive(Debug, Clone, Serialize, Deserialize)]
359#[serde(tag = "type", rename_all = "lowercase")]
360pub enum ResourceContent {
361    /// Text content.
362    Text {
363        /// Resource URI.
364        uri: String,
365        /// MIME type.
366        mime_type: Option<String>,
367        /// Text content.
368        text: String,
369    },
370    /// Binary blob content (base64-encoded).
371    Blob {
372        /// Resource URI.
373        uri: String,
374        /// MIME type.
375        mime_type: Option<String>,
376        /// Base64-encoded blob data.
377        blob: String,
378    },
379}
380
381#[cfg(feature = "native")]
382/// Prompt Get Request
383#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct GetPromptParams {
385    /// Prompt name.
386    pub name: String,
387    /// Optional arguments for the prompt.
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub arguments: Option<Value>,
390}
391
392#[cfg(feature = "native")]
393/// Prompt Get Result
394#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct GetPromptResult {
396    /// Prompt description.
397    pub description: String,
398    /// Prompt messages.
399    pub messages: Vec<PromptMessage>,
400}
401
402/// A message within a prompt.
403#[cfg(feature = "native")]
404#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct PromptMessage {
406    /// Message role (e.g., "user", "assistant").
407    pub role: String,
408    /// Message content.
409    pub content: PromptContent,
410}
411
412/// Content type within a prompt message.
413#[cfg(feature = "native")]
414#[derive(Debug, Clone, Serialize, Deserialize)]
415#[serde(tag = "type", rename_all = "lowercase")]
416// reason: `Resource` carries an `McpResource` (~hundreds of bytes); boxing it
417// would change this enum's public layout and serialization expectations.
418#[allow(clippy::large_enum_variant)]
419pub enum PromptContent {
420    /// Text content.
421    Text {
422        /// The text.
423        text: String,
424    },
425    /// Image content.
426    Image {
427        /// Base64-encoded image data.
428        data: String,
429        /// MIME type of the image.
430        mime_type: String,
431    },
432    /// Embedded resource content.
433    Resource {
434        /// The resource.
435        resource: McpResource,
436    },
437}
438
439#[cfg(feature = "native")]
440/// Prompt Argument Definition
441#[derive(Debug, Clone, Serialize, Deserialize)]
442pub struct PromptArgument {
443    /// Argument name.
444    pub name: String,
445    /// Argument description.
446    pub description: String,
447    /// Whether the argument is required.
448    pub required: bool,
449}
450
451/// Content type within a tool result.
452#[cfg(feature = "native")]
453#[derive(Debug, Clone, Serialize, Deserialize)]
454#[serde(tag = "type", rename_all = "lowercase")]
455// reason: `Resource` carries an `McpResource` (~hundreds of bytes); boxing it
456// would change this enum's public layout and serialization expectations.
457#[allow(clippy::large_enum_variant)]
458pub enum ToolResultContent {
459    /// Text result.
460    Text {
461        /// The text.
462        text: String,
463    },
464    /// Image result.
465    Image {
466        /// Base64-encoded image data.
467        data: String,
468        /// MIME type of the image.
469        mime_type: String,
470    },
471    /// Resource result.
472    Resource {
473        /// The resource.
474        resource: McpResource,
475    },
476}
477
478// ===========================================================================
479// TESTS
480// ===========================================================================
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485    use serde_json::json;
486
487    #[test]
488    fn test_json_rpc_request_new() {
489        let request =
490            JsonRpcRequest::new(1, "test_method".to_string(), Some(json!({"key": "value"})))
491                .unwrap();
492
493        assert_eq!(request.jsonrpc, "2.0");
494        assert_eq!(request.id, json!(1));
495        assert_eq!(request.method, "test_method");
496        assert!(request.params.is_some());
497    }
498
499    #[test]
500    fn test_json_rpc_request_serialization() {
501        let request = JsonRpcRequest::new(1, "test".to_string(), None::<()>).unwrap();
502        let json = serde_json::to_string(&request).unwrap();
503
504        assert!(json.contains("jsonrpc"));
505        assert!(json.contains("2.0"));
506        assert!(json.contains("test"));
507    }
508
509    #[test]
510    fn test_json_rpc_response_success() {
511        let response = JsonRpcResponse {
512            jsonrpc: "2.0".to_string(),
513            id: json!(1),
514            result: Some(json!({"status": "ok"})),
515            error: None,
516        };
517
518        assert!(response.result.is_some());
519        assert!(response.error.is_none());
520    }
521
522    #[test]
523    fn test_json_rpc_response_error() {
524        let response = JsonRpcResponse {
525            jsonrpc: "2.0".to_string(),
526            id: json!(1),
527            result: None,
528            error: Some(JsonRpcError {
529                code: -32600,
530                message: "Invalid Request".to_string(),
531                data: None,
532            }),
533        };
534
535        assert!(response.result.is_none());
536        assert!(response.error.is_some());
537    }
538
539    #[cfg(feature = "native")]
540    #[test]
541    fn test_type_aliases_work() {
542        // Test that our type aliases are properly set up
543        let _tool: McpTool;
544        let _resource: McpResource;
545        let _prompt: McpPrompt;
546        // If this compiles, the aliases are working
547    }
548}