Skip to main content

forge_sdk/mcp/
protocol.rs

1//! JSON-RPC 2.0 message types used by MCP.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6/// MCP protocol revisions this server speaks, newest first.
7///
8/// 2025-03-26 is deliberately absent: it mandates JSON-RPC batching, which
9/// `control_dispatch` cannot parse, and 2025-06-18 removed the requirement
10/// again. Advertising a revision whose MUST we do not implement is the same
11/// untruth as the version this list replaced.
12pub const SUPPORTED_PROTOCOL_VERSIONS: [&str; 3] = ["2025-11-25", "2025-06-18", "2024-11-05"];
13
14/// Answered when the client asks for a revision we do not speak.
15pub const LATEST_PROTOCOL_VERSION: &str = SUPPORTED_PROTOCOL_VERSIONS[0];
16
17/// A JSON-RPC request from the `claude` binary.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct JsonRpcRequest {
20    /// Always `"2.0"`.
21    pub jsonrpc: String,
22    /// Request id; absent for notifications.
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub id: Option<Value>,
25    /// Method name (e.g. `"initialize"`, `"tools/list"`, `"tools/call"`).
26    pub method: String,
27    /// Method-specific params.
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub params: Option<Value>,
30}
31
32/// A JSON-RPC response from the MCP server back to the `claude` binary.
33/// Outbound only - we construct these, never parse them.
34#[derive(Debug, Clone, Serialize)]
35pub struct JsonRpcResponse {
36    /// Always `"2.0"`.
37    pub jsonrpc: String,
38    /// Request id this response correlates to.
39    pub id: Value,
40    /// Success payload, mutually exclusive with `error`.
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub result: Option<JsonRpcResult>,
43    /// Error payload, mutually exclusive with `result`.
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub error: Option<McpError>,
46}
47
48impl JsonRpcResponse {
49    /// Build a successful response.
50    pub fn success(id: Value, result: JsonRpcResult) -> Self {
51        Self { jsonrpc: "2.0".into(), id, result: Some(result), error: None }
52    }
53
54    /// Build an error response.
55    pub fn error(id: Value, error: McpError) -> Self {
56        Self { jsonrpc: "2.0".into(), id, result: None, error: Some(error) }
57    }
58}
59
60/// Typed successful-response payload. Outbound only, like its envelope: a
61/// `Deserialize` here would make the fieldless `Empty` match any object, so
62/// every unrecognised result would parse successfully rather than erroring.
63#[derive(Debug, Clone, Serialize)]
64#[serde(untagged)]
65pub enum JsonRpcResult {
66    /// Response to `initialize`.
67    Initialize {
68        /// Protocol version the server speaks.
69        #[serde(rename = "protocolVersion")]
70        protocol_version: String,
71        /// Server capabilities (narrow for now; tools only).
72        capabilities: Value,
73        /// Server metadata.
74        #[serde(rename = "serverInfo")]
75        server_info: ServerInfo,
76        /// How to use this server, delivered to the client once at session
77        /// start. Omitted when the server has none, so a client old enough
78        /// not to know the field sees exactly the old shape.
79        #[serde(skip_serializing_if = "Option::is_none")]
80        instructions: Option<String>,
81    },
82    /// Response to `tools/list`.
83    ToolsList {
84        /// Registered tool descriptions.
85        tools: Vec<ToolDescription>,
86    },
87    /// Response to `tools/call`.
88    ToolsCall {
89        /// The content blocks the tool produced.
90        content: Vec<Value>,
91        /// Whether the call was an error.
92        #[serde(rename = "isError", skip_serializing_if = "std::ops::Not::not")]
93        is_error: bool,
94    },
95    /// The spec's empty `{}` result, used by `ping`.
96    Empty {},
97}
98
99/// Server metadata in `initialize` response.
100#[derive(Debug, Clone, Serialize)]
101pub struct ServerInfo {
102    /// Human-readable server name (e.g. `"forge-sdk-in-process"`).
103    pub name: String,
104    /// Server version string.
105    pub version: String,
106}
107
108/// Standard MCP error body.
109#[derive(Debug, Clone, Serialize)]
110pub struct McpError {
111    /// JSON-RPC error code. Common values: -32700 parse, -32600 invalid request,
112    /// -32601 method not found, -32602 invalid params, -32603 internal error.
113    pub code: i32,
114    /// Human-readable error message.
115    pub message: String,
116    /// Optional structured data.
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub data: Option<Value>,
119}
120
121/// A tool as described in the `tools/list` response.
122#[derive(Debug, Clone, Serialize)]
123pub struct ToolDescription {
124    /// Tool name (e.g. `"greet"` - will be exposed to the model as
125    /// `mcp__<server-name>__greet`).
126    pub name: String,
127    /// One-line human-readable description.
128    pub description: String,
129    /// JSON-Schema describing the tool's `arguments` object.
130    #[serde(rename = "inputSchema")]
131    pub input_schema: Value,
132}
133
134#[cfg(test)]
135mod tests_mcp_protocol {
136    // Test-mod `use super::*;` brings the parent's full surface in; not every test consumes every item.
137    #[allow(unused_imports)]
138    use super::*;
139
140    use crate::mcp::McpError;
141    use crate::mcp::protocol::{
142        JsonRpcRequest, JsonRpcResponse, JsonRpcResult, ServerInfo, ToolDescription,
143    };
144    use serde_json::json;
145
146    #[test]
147    fn initialize_request_parse() {
148        let raw = json!({
149            "jsonrpc": "2.0",
150            "id": 1,
151            "method": "initialize",
152            "params": {"protocolVersion": "2024-11-05"}
153        });
154        let req: JsonRpcRequest = serde_json::from_value(raw).expect("parse");
155        assert_eq!(req.jsonrpc, "2.0");
156        assert_eq!(req.method, "initialize");
157        assert_eq!(req.id, Some(json!(1)));
158    }
159
160    #[test]
161    fn tools_list_response_shape() {
162        let tools = vec![ToolDescription {
163            name: "greet".into(),
164            description: "Greet by name".into(),
165            input_schema: json!({"type": "object", "properties": {"name": {"type": "string"}}}),
166        }];
167        let resp = JsonRpcResponse::success(json!(1), JsonRpcResult::ToolsList { tools });
168        let raw = serde_json::to_value(&resp).expect("ser");
169        assert_eq!(raw["jsonrpc"], "2.0");
170        assert_eq!(raw["id"], 1);
171        assert!(raw["result"]["tools"].is_array());
172        assert_eq!(raw["result"]["tools"][0]["name"], "greet");
173    }
174
175    #[test]
176    fn error_response_shape() {
177        let resp = JsonRpcResponse::error(
178            json!(2),
179            McpError { code: -32601, message: "Method not found".into(), data: None },
180        );
181        let raw = serde_json::to_value(&resp).expect("ser");
182        assert_eq!(raw["error"]["code"], -32601);
183        assert_eq!(raw["error"]["message"], "Method not found");
184        assert!(raw["result"].is_null());
185    }
186
187    /// Untagged serialisation still has to put each variant on the wire
188    /// under its own shape, `Empty` included.
189    #[test]
190    fn each_result_variant_serialises_to_its_own_shape() {
191        let call = JsonRpcResult::ToolsCall {
192            content: vec![json!({"type": "text", "text": "hi"})],
193            is_error: true,
194        };
195        let raw = serde_json::to_value(&call).expect("ser");
196        assert_eq!(raw["content"][0]["text"], "hi");
197        assert_eq!(raw["isError"], true);
198    }
199
200    #[test]
201    fn ping_result_serialises_to_an_empty_object() {
202        let resp = JsonRpcResponse::success(json!(9), JsonRpcResult::Empty {});
203        let raw = serde_json::to_value(&resp).expect("ser");
204        assert_eq!(raw["result"], json!({}));
205    }
206
207    #[test]
208    fn initialize_response_shape() {
209        let resp = JsonRpcResponse::success(
210            json!(1),
211            JsonRpcResult::Initialize {
212                protocol_version: "2024-11-05".into(),
213                capabilities: json!({"tools": {}}),
214                server_info: ServerInfo { name: "probe".into(), version: "0.0.1".into() },
215                instructions: None,
216            },
217        );
218        let raw = serde_json::to_value(&resp).expect("ser");
219        assert_eq!(raw["result"]["protocolVersion"], "2024-11-05");
220        assert_eq!(raw["result"]["serverInfo"]["name"], "probe");
221    }
222}