Skip to main content

embacle_server/mcp/
protocol.rs

1// ABOUTME: MCP JSON-RPC protocol types for request/response handling
2// ABOUTME: Defines wire format for initialize, tools/list, tools/call, and error responses
3//
4// SPDX-License-Identifier: Apache-2.0
5// Copyright (c) 2026 dravr.ai
6
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10/// MCP protocol version supported by this server
11pub const PROTOCOL_VERSION: &str = "2024-11-05";
12
13/// Server name reported during MCP handshake
14pub const SERVER_NAME: &str = "embacle-server";
15
16/// Server version reported during MCP handshake
17pub const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
18
19// ============================================================================
20// JSON-RPC Error Codes
21// ============================================================================
22
23/// JSON-RPC parse error: invalid JSON received
24pub const PARSE_ERROR: i32 = -32_700;
25
26/// JSON-RPC invalid request (e.g. wrong protocol version)
27pub const INVALID_REQUEST: i32 = -32_600;
28
29/// JSON-RPC method not found
30pub const METHOD_NOT_FOUND: i32 = -32_601;
31
32/// JSON-RPC invalid parameters
33pub const INVALID_PARAMS: i32 = -32_602;
34
35/// JSON-RPC internal error
36pub const INTERNAL_ERROR: i32 = -32_603;
37
38// ============================================================================
39// JSON-RPC Messages
40// ============================================================================
41
42/// Incoming JSON-RPC request from MCP client
43#[derive(Debug, Deserialize)]
44pub struct JsonRpcRequest {
45    /// Protocol version marker (always "2.0", validated by JSON-RPC clients)
46    pub jsonrpc: String,
47    /// Request identifier (None for notifications)
48    pub id: Option<Value>,
49    /// Method name
50    pub method: String,
51    /// Method parameters
52    #[serde(default)]
53    pub params: Option<Value>,
54}
55
56/// Outgoing JSON-RPC response to MCP client
57#[derive(Debug, Serialize)]
58pub struct JsonRpcResponse {
59    /// Always "2.0"
60    pub jsonrpc: String,
61    /// Matching request identifier
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub id: Option<Value>,
64    /// Success payload
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub result: Option<Value>,
67    /// Error payload
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub error: Option<JsonRpcError>,
70}
71
72/// JSON-RPC error object
73#[derive(Debug, Serialize)]
74pub struct JsonRpcError {
75    /// Numeric error code
76    pub code: i32,
77    /// Human-readable error message
78    pub message: String,
79    /// Additional error data
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub data: Option<Value>,
82}
83
84impl JsonRpcResponse {
85    /// Build a success response with the given result
86    pub fn success(id: Option<Value>, result: Value) -> Self {
87        Self {
88            jsonrpc: "2.0".to_owned(),
89            id,
90            result: Some(result),
91            error: None,
92        }
93    }
94
95    /// Build an error response with the given code and message
96    pub fn error(id: Option<Value>, code: i32, message: String) -> Self {
97        Self {
98            jsonrpc: "2.0".to_owned(),
99            id,
100            result: None,
101            error: Some(JsonRpcError {
102                code,
103                message,
104                data: None,
105            }),
106        }
107    }
108}
109
110// ============================================================================
111// MCP Initialize
112// ============================================================================
113
114/// Parameters for the `initialize` request
115#[derive(Debug, Deserialize)]
116pub struct InitializeParams {
117    /// Protocol version requested by the client
118    #[serde(rename = "protocolVersion")]
119    pub protocol_version: String,
120    /// Client capabilities
121    #[serde(default)]
122    pub capabilities: Value,
123    /// Client identification
124    #[serde(rename = "clientInfo")]
125    pub client_info: ClientInfo,
126}
127
128/// Client identification sent during initialization
129#[derive(Debug, Deserialize)]
130pub struct ClientInfo {
131    /// Client name
132    pub name: String,
133    /// Client version
134    #[serde(default)]
135    pub version: Option<String>,
136}
137
138/// Result of a successful `initialize` response
139#[derive(Debug, Serialize)]
140pub struct InitializeResult {
141    /// Protocol version the server supports
142    #[serde(rename = "protocolVersion")]
143    pub protocol_version: String,
144    /// Server capabilities
145    pub capabilities: ServerCapabilities,
146    /// Server identification
147    #[serde(rename = "serverInfo")]
148    pub server_info: ServerInfo,
149}
150
151/// Server identification
152#[derive(Debug, Serialize)]
153pub struct ServerInfo {
154    /// Server name
155    pub name: String,
156    /// Server version
157    pub version: String,
158}
159
160/// Server capability declarations
161#[derive(Debug, Serialize)]
162pub struct ServerCapabilities {
163    /// Tool support (presence signals tools are available)
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub tools: Option<ToolsCapability>,
166}
167
168/// Marker type indicating the server supports MCP tools
169#[derive(Debug, Serialize)]
170pub struct ToolsCapability {}
171
172// ============================================================================
173// MCP Tools
174// ============================================================================
175
176/// Tool definition exposed via `tools/list`
177#[derive(Debug, Clone, Serialize)]
178pub struct ToolDefinition {
179    /// Unique tool name
180    pub name: String,
181    /// Human-readable tool description
182    pub description: String,
183    /// JSON Schema describing the tool's input
184    #[serde(rename = "inputSchema")]
185    pub input_schema: Value,
186}
187
188/// Result of a `tools/list` call
189#[derive(Debug, Serialize)]
190pub struct ToolsListResult {
191    /// Available tool definitions
192    pub tools: Vec<ToolDefinition>,
193}
194
195/// Parameters for a `tools/call` request
196#[derive(Debug, Deserialize)]
197pub struct CallToolParams {
198    /// Name of the tool to invoke
199    pub name: String,
200    /// Tool arguments
201    #[serde(default)]
202    pub arguments: Option<Value>,
203}
204
205/// Result of a `tools/call` invocation
206#[derive(Debug, Serialize)]
207pub struct CallToolResult {
208    /// Response content parts
209    pub content: Vec<ContentPart>,
210    /// Whether this result represents an error
211    #[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
212    pub is_error: Option<bool>,
213}
214
215/// A content part within a tool result
216#[derive(Debug, Serialize)]
217pub struct ContentPart {
218    /// Content type (always "text" for now)
219    #[serde(rename = "type")]
220    pub content_type: String,
221    /// Text content
222    pub text: String,
223}
224
225impl CallToolResult {
226    /// Build a successful text result
227    pub fn text(content: String) -> Self {
228        Self {
229            content: vec![ContentPart {
230                content_type: "text".to_owned(),
231                text: content,
232            }],
233            is_error: None,
234        }
235    }
236
237    /// Build an error result with the given message
238    pub fn error(message: String) -> Self {
239        Self {
240            content: vec![ContentPart {
241                content_type: "text".to_owned(),
242                text: message,
243            }],
244            is_error: Some(true),
245        }
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn serialize_success_response() {
255        let resp = JsonRpcResponse::success(Some(Value::from(1)), serde_json::json!({"ok": true}));
256        let json = serde_json::to_string(&resp).expect("serialize");
257        assert!(json.contains("\"result\""));
258        assert!(!json.contains("\"error\""));
259    }
260
261    #[test]
262    fn serialize_error_response() {
263        let resp = JsonRpcResponse::error(Some(Value::from(1)), PARSE_ERROR, "bad json".to_owned());
264        let json = serde_json::to_string(&resp).expect("serialize");
265        assert!(json.contains("\"error\""));
266        assert!(json.contains("-32700"));
267        assert!(!json.contains("\"result\""));
268    }
269
270    #[test]
271    fn deserialize_request() {
272        let raw = r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#;
273        let req: JsonRpcRequest = serde_json::from_str(raw).expect("deserialize");
274        assert_eq!(req.method, "tools/list");
275        assert!(req.params.is_none());
276    }
277
278    #[test]
279    fn call_tool_result_text() {
280        let result = CallToolResult::text("hello".to_owned());
281        assert!(result.is_error.is_none());
282        assert_eq!(result.content.len(), 1);
283        assert_eq!(result.content[0].text, "hello");
284    }
285
286    #[test]
287    fn call_tool_result_error() {
288        let result = CallToolResult::error("oops".to_owned());
289        assert_eq!(result.is_error, Some(true));
290        assert_eq!(result.content[0].text, "oops");
291    }
292}