webpuppet_mcp/
protocol.rs

1//! MCP protocol types and message handling.
2//!
3//! Implements the Model Context Protocol (MCP) as specified at:
4//! https://spec.modelcontextprotocol.io/
5
6use serde::{Deserialize, Serialize};
7
8/// JSON-RPC 2.0 request.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct JsonRpcRequest {
11    /// Protocol version (always "2.0").
12    pub jsonrpc: String,
13    /// Request ID.
14    pub id: Option<JsonRpcId>,
15    /// Method name.
16    pub method: String,
17    /// Parameters (if any).
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub params: Option<serde_json::Value>,
20}
21
22/// JSON-RPC 2.0 response.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct JsonRpcResponse {
25    /// Protocol version (always "2.0").
26    pub jsonrpc: String,
27    /// Request ID (matches request).
28    pub id: Option<JsonRpcId>,
29    /// Result (success case).
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub result: Option<serde_json::Value>,
32    /// Error (failure case).
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub error: Option<JsonRpcError>,
35}
36
37impl JsonRpcResponse {
38    /// Create a success response.
39    pub fn success(id: Option<JsonRpcId>, result: impl Serialize) -> Self {
40        Self {
41            jsonrpc: "2.0".into(),
42            id,
43            result: Some(serde_json::to_value(result).unwrap_or(serde_json::Value::Null)),
44            error: None,
45        }
46    }
47
48    /// Create an error response.
49    pub fn error(id: Option<JsonRpcId>, code: i32, message: impl Into<String>) -> Self {
50        Self {
51            jsonrpc: "2.0".into(),
52            id,
53            result: None,
54            error: Some(JsonRpcError {
55                code,
56                message: message.into(),
57                data: None,
58            }),
59        }
60    }
61
62    /// Create an error response with data.
63    pub fn error_with_data(
64        id: Option<JsonRpcId>,
65        code: i32,
66        message: impl Into<String>,
67        data: serde_json::Value,
68    ) -> Self {
69        Self {
70            jsonrpc: "2.0".into(),
71            id,
72            result: None,
73            error: Some(JsonRpcError {
74                code,
75                message: message.into(),
76                data: Some(data),
77            }),
78        }
79    }
80}
81
82/// JSON-RPC error object.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct JsonRpcError {
85    /// Error code.
86    pub code: i32,
87    /// Error message.
88    pub message: String,
89    /// Additional data.
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub data: Option<serde_json::Value>,
92}
93
94/// JSON-RPC request ID.
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
96#[serde(untagged)]
97pub enum JsonRpcId {
98    /// String ID.
99    String(String),
100    /// Numeric ID.
101    Number(i64),
102}
103
104/// MCP message types.
105#[derive(Debug, Clone, Serialize, Deserialize)]
106#[serde(untagged)]
107pub enum McpMessage {
108    /// Request message.
109    Request(JsonRpcRequest),
110    /// Response message.
111    Response(JsonRpcResponse),
112    /// Notification (no ID, no response expected).
113    Notification(JsonRpcRequest),
114}
115
116impl McpMessage {
117    /// Parse a JSON string into an MCP message.
118    pub fn parse(json: &str) -> crate::Result<Self> {
119        let value: serde_json::Value = serde_json::from_str(json)?;
120
121        // Check if it's a request or response
122        if value.get("method").is_some() {
123            let request: JsonRpcRequest = serde_json::from_value(value)?;
124            if request.id.is_some() {
125                Ok(McpMessage::Request(request))
126            } else {
127                Ok(McpMessage::Notification(request))
128            }
129        } else if value.get("result").is_some() || value.get("error").is_some() {
130            let response: JsonRpcResponse = serde_json::from_value(value)?;
131            Ok(McpMessage::Response(response))
132        } else {
133            Err(crate::Error::InvalidParams("invalid MCP message".into()))
134        }
135    }
136
137    /// Serialize to JSON string.
138    pub fn to_json(&self) -> crate::Result<String> {
139        Ok(serde_json::to_string(self)?)
140    }
141}
142
143// ============================================================================
144// MCP-specific protocol types
145// ============================================================================
146
147/// MCP initialization request parameters.
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct InitializeParams {
150    /// Protocol version.
151    #[serde(rename = "protocolVersion")]
152    pub protocol_version: String,
153    /// Client capabilities.
154    pub capabilities: ClientCapabilities,
155    /// Client info.
156    #[serde(rename = "clientInfo")]
157    pub client_info: ClientInfo,
158}
159
160/// MCP initialization result.
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct InitializeResult {
163    /// Protocol version.
164    #[serde(rename = "protocolVersion")]
165    pub protocol_version: String,
166    /// Server capabilities.
167    pub capabilities: ServerCapabilities,
168    /// Server info.
169    #[serde(rename = "serverInfo")]
170    pub server_info: ServerInfo,
171}
172
173/// Client capabilities.
174#[derive(Debug, Clone, Default, Serialize, Deserialize)]
175pub struct ClientCapabilities {
176    /// Roots capability.
177    #[serde(default)]
178    pub roots: Option<RootsCapability>,
179    /// Sampling capability.
180    #[serde(default)]
181    pub sampling: Option<serde_json::Value>,
182}
183
184/// Roots capability.
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct RootsCapability {
187    /// Whether list changed notifications are supported.
188    #[serde(rename = "listChanged", default)]
189    pub list_changed: bool,
190}
191
192/// Server capabilities.
193#[derive(Debug, Clone, Default, Serialize, Deserialize)]
194pub struct ServerCapabilities {
195    /// Tools capability.
196    #[serde(default, skip_serializing_if = "Option::is_none")]
197    pub tools: Option<ToolsCapability>,
198    /// Resources capability.
199    #[serde(default, skip_serializing_if = "Option::is_none")]
200    pub resources: Option<ResourcesCapability>,
201    /// Prompts capability.
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub prompts: Option<PromptsCapability>,
204    /// Logging capability.
205    #[serde(default, skip_serializing_if = "Option::is_none")]
206    pub logging: Option<serde_json::Value>,
207}
208
209/// Tools capability.
210#[derive(Debug, Clone, Default, Serialize, Deserialize)]
211pub struct ToolsCapability {
212    /// Whether tool list changed notifications are supported.
213    #[serde(rename = "listChanged", default)]
214    pub list_changed: bool,
215}
216
217/// Resources capability.
218#[derive(Debug, Clone, Default, Serialize, Deserialize)]
219pub struct ResourcesCapability {
220    /// Whether subscription is supported.
221    #[serde(default)]
222    pub subscribe: bool,
223    /// Whether list changed notifications are supported.
224    #[serde(rename = "listChanged", default)]
225    pub list_changed: bool,
226}
227
228/// Prompts capability.
229#[derive(Debug, Clone, Default, Serialize, Deserialize)]
230pub struct PromptsCapability {
231    /// Whether list changed notifications are supported.
232    #[serde(rename = "listChanged", default)]
233    pub list_changed: bool,
234}
235
236/// Client information.
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct ClientInfo {
239    /// Client name.
240    pub name: String,
241    /// Client version.
242    pub version: String,
243}
244
245/// Server information.
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct ServerInfo {
248    /// Server name.
249    pub name: String,
250    /// Server version.
251    pub version: String,
252}
253
254/// Tool definition for listing.
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct ToolDefinition {
257    /// Tool name.
258    pub name: String,
259    /// Tool description.
260    pub description: String,
261    /// Input schema (JSON Schema).
262    #[serde(rename = "inputSchema")]
263    pub input_schema: serde_json::Value,
264}
265
266/// Tool call request.
267#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct ToolCallParams {
269    /// Tool name.
270    pub name: String,
271    /// Tool arguments.
272    #[serde(default)]
273    pub arguments: serde_json::Value,
274}
275
276/// Tool call result.
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct ToolCallResult {
279    /// Result content.
280    pub content: Vec<ContentItem>,
281    /// Whether the tool encountered an error.
282    #[serde(rename = "isError", default)]
283    pub is_error: bool,
284}
285
286/// Content item in tool results.
287#[derive(Debug, Clone, Serialize, Deserialize)]
288#[serde(tag = "type")]
289pub enum ContentItem {
290    /// Text content.
291    #[serde(rename = "text")]
292    Text {
293        /// Text value.
294        text: String,
295    },
296    /// Image content.
297    #[serde(rename = "image")]
298    Image {
299        /// Base64-encoded image data.
300        data: String,
301        /// MIME type.
302        #[serde(rename = "mimeType")]
303        mime_type: String,
304    },
305    /// Resource content.
306    #[serde(rename = "resource")]
307    Resource {
308        /// Resource URI.
309        uri: String,
310        /// MIME type.
311        #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
312        mime_type: Option<String>,
313        /// Resource text.
314        #[serde(skip_serializing_if = "Option::is_none")]
315        text: Option<String>,
316    },
317}
318
319impl ContentItem {
320    /// Create a text content item.
321    pub fn text(text: impl Into<String>) -> Self {
322        ContentItem::Text { text: text.into() }
323    }
324
325    /// Create an image content item.
326    pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
327        ContentItem::Image {
328            data: data.into(),
329            mime_type: mime_type.into(),
330        }
331    }
332}
333
334/// List tools result.
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct ListToolsResult {
337    /// Available tools.
338    pub tools: Vec<ToolDefinition>,
339}