Skip to main content

act_types/
mcp.rs

1//! MCP (Model Context Protocol) wire-format types.
2//!
3//! All types derive both `Serialize` and `Deserialize` so they can be used
4//! by MCP servers (act-cli), MCP clients (mcp-bridge), and SDKs alike.
5//!
6//! Binary fields (`ImageContent.data`, `EmbeddedResource.blob`) are stored as
7//! `Vec<u8>` and automatically base64-encoded/decoded via `serde_with`.
8//!
9//! JSON-RPC envelope types are re-exported from [`crate::jsonrpc`].
10
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use serde_with::{base64::Base64, serde_as, skip_serializing_none};
14
15/// MCP protocol version supported by this crate.
16pub const PROTOCOL_VERSION: &str = "2025-11-25";
17
18// Re-export JSON-RPC types for convenience.
19pub use crate::jsonrpc::{
20    Body as JsonRpcBody, Error as JsonRpcError, Request as JsonRpcRequest,
21    Response as JsonRpcResponse, Version as JsonRpcVersion,
22};
23
24// ── Initialize ──
25
26/// Server info returned in the `initialize` response.
27#[skip_serializing_none]
28#[derive(Debug, Clone, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase")]
30pub struct ServerInfo {
31    pub name: String,
32    #[serde(default)]
33    pub version: Option<String>,
34}
35
36/// Capabilities declared by the server in the `initialize` response.
37#[skip_serializing_none]
38#[derive(Debug, Clone, Default, Serialize, Deserialize)]
39pub struct ServerCapabilities {
40    #[serde(default)]
41    pub tools: Option<Value>,
42    #[serde(default)]
43    pub resources: Option<Value>,
44    #[serde(default)]
45    pub prompts: Option<Value>,
46}
47
48/// The `result` payload of an `initialize` response.
49#[skip_serializing_none]
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct InitializeResult {
53    pub protocol_version: String,
54    pub server_info: ServerInfo,
55    #[serde(default)]
56    pub capabilities: Option<ServerCapabilities>,
57}
58
59// ── Tools ──
60
61/// MCP tool definition returned in `tools/list`.
62#[skip_serializing_none]
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct ToolDefinition {
66    pub name: String,
67    #[serde(default)]
68    pub description: Option<String>,
69    #[serde(default = "default_object_schema")]
70    pub input_schema: Value,
71    #[serde(default)]
72    pub annotations: Option<ToolAnnotations>,
73}
74
75fn default_object_schema() -> Value {
76    serde_json::json!({"type": "object"})
77}
78
79/// Tool annotations (behavioral hints).
80#[skip_serializing_none]
81#[derive(Debug, Clone, Default, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct ToolAnnotations {
84    #[serde(default)]
85    pub read_only_hint: Option<bool>,
86    #[serde(default)]
87    pub idempotent_hint: Option<bool>,
88    #[serde(default)]
89    pub destructive_hint: Option<bool>,
90    #[serde(default)]
91    pub open_world_hint: Option<bool>,
92}
93
94/// Response payload for `tools/list`.
95#[skip_serializing_none]
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct ListToolsResult {
98    pub tools: Vec<ToolDefinition>,
99    #[serde(default)]
100    pub next_cursor: Option<String>,
101}
102
103/// Parameters for `tools/call`.
104#[skip_serializing_none]
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct CallToolParams {
107    pub name: String,
108    #[serde(default)]
109    pub arguments: Option<Value>,
110}
111
112/// Response payload for `tools/call`.
113#[skip_serializing_none]
114#[derive(Debug, Clone, Serialize, Deserialize)]
115#[serde(rename_all = "camelCase")]
116pub struct CallToolResult {
117    pub content: Vec<ContentItem>,
118    #[serde(default)]
119    pub is_error: Option<bool>,
120}
121
122// ── Content items ──
123
124/// A content item in tool results.
125///
126/// Internally-tagged enum matching MCP's `type`-discriminated content items.
127#[derive(Debug, Clone, Serialize, Deserialize)]
128#[serde(tag = "type", rename_all = "lowercase")]
129pub enum ContentItem {
130    Text(TextContent),
131    Image(ImageContent),
132    Resource(ResourceContent),
133}
134
135/// Text content item.
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct TextContent {
138    pub text: String,
139}
140
141/// Image content item.
142///
143/// `data` is stored as raw bytes and automatically base64-encoded on the wire.
144#[serde_as]
145#[derive(Debug, Clone, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct ImageContent {
148    #[serde_as(as = "Base64")]
149    pub data: Vec<u8>,
150    pub mime_type: String,
151}
152
153/// Embedded resource content item.
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct ResourceContent {
156    pub resource: EmbeddedResource,
157}
158
159/// An embedded resource within a content item.
160///
161/// `blob` is stored as raw bytes and automatically base64-encoded on the wire.
162#[serde_as]
163#[skip_serializing_none]
164#[derive(Debug, Clone, Serialize, Deserialize)]
165#[serde(rename_all = "camelCase")]
166pub struct EmbeddedResource {
167    pub uri: String,
168    #[serde(default)]
169    pub mime_type: Option<String>,
170    #[serde(default)]
171    pub text: Option<String>,
172    #[serde_as(as = "Option<Base64>")]
173    #[serde(default)]
174    pub blob: Option<Vec<u8>>,
175}
176
177// ── Error mapping ──
178
179/// Map an ACT error kind to a JSON-RPC error code.
180pub fn error_kind_to_jsonrpc_code(kind: &str) -> i32 {
181    use crate::constants::*;
182    match kind {
183        ERR_NOT_FOUND => -32601,
184        ERR_INVALID_ARGS => -32602,
185        ERR_INTERNAL => -32603,
186        _ => -32000,
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use serde_json::json;
194
195    #[test]
196    fn tool_definition_deserialize() {
197        let json = json!({
198            "name": "get_weather",
199            "description": "Get weather",
200            "inputSchema": {
201                "type": "object",
202                "properties": { "city": { "type": "string" } }
203            },
204            "annotations": {
205                "readOnlyHint": true,
206                "destructiveHint": false
207            }
208        });
209        let tool: ToolDefinition = serde_json::from_value(json).unwrap();
210        assert_eq!(tool.name, "get_weather");
211        assert_eq!(tool.description.as_deref(), Some("Get weather"));
212        let ann = tool.annotations.unwrap();
213        assert_eq!(ann.read_only_hint, Some(true));
214        assert_eq!(ann.destructive_hint, Some(false));
215        assert_eq!(ann.idempotent_hint, None);
216    }
217
218    #[test]
219    fn tool_definition_minimal() {
220        let json = json!({ "name": "simple" });
221        let tool: ToolDefinition = serde_json::from_value(json).unwrap();
222        assert_eq!(tool.name, "simple");
223        assert_eq!(tool.input_schema, json!({"type": "object"}));
224        assert!(tool.annotations.is_none());
225    }
226
227    #[test]
228    fn tool_definition_omits_none_fields() {
229        let tool = ToolDefinition {
230            name: "x".to_string(),
231            description: None,
232            input_schema: default_object_schema(),
233            annotations: None,
234        };
235        let json = serde_json::to_string(&tool).unwrap();
236        assert!(!json.contains("\"description\""));
237        assert!(!json.contains("\"annotations\""));
238    }
239
240    #[test]
241    fn annotations_omits_none_hints() {
242        let ann = ToolAnnotations {
243            read_only_hint: Some(true),
244            ..Default::default()
245        };
246        let json = serde_json::to_string(&ann).unwrap();
247        assert!(json.contains("readOnlyHint"));
248        assert!(!json.contains("idempotentHint"));
249        assert!(!json.contains("destructiveHint"));
250        assert!(!json.contains("openWorldHint"));
251    }
252
253    #[test]
254    fn content_item_text() {
255        let item: ContentItem = serde_json::from_value(json!({
256            "type": "text",
257            "text": "hello"
258        }))
259        .unwrap();
260        match item {
261            ContentItem::Text(t) => assert_eq!(t.text, "hello"),
262            _ => panic!("expected text"),
263        }
264    }
265
266    #[test]
267    fn content_item_image_roundtrip() {
268        let original = ImageContent {
269            data: b"\x89PNG\r\n".to_vec(),
270            mime_type: "image/png".to_string(),
271        };
272        let json = serde_json::to_value(&ContentItem::Image(original.clone())).unwrap();
273        assert_eq!(json["data"], "iVBORw0K");
274        assert_eq!(json["mimeType"], "image/png");
275
276        let item: ContentItem = serde_json::from_value(json).unwrap();
277        match item {
278            ContentItem::Image(i) => {
279                assert_eq!(i.data, b"\x89PNG\r\n");
280                assert_eq!(i.mime_type, "image/png");
281            }
282            _ => panic!("expected image"),
283        }
284    }
285
286    #[test]
287    fn content_item_resource_text() {
288        let item: ContentItem = serde_json::from_value(json!({
289            "type": "resource",
290            "resource": {
291                "uri": "file:///tmp/test.txt",
292                "text": "contents",
293                "mimeType": "text/plain"
294            }
295        }))
296        .unwrap();
297        match item {
298            ContentItem::Resource(r) => {
299                assert_eq!(r.resource.uri, "file:///tmp/test.txt");
300                assert_eq!(r.resource.text.as_deref(), Some("contents"));
301                assert!(r.resource.blob.is_none());
302            }
303            _ => panic!("expected resource"),
304        }
305    }
306
307    #[test]
308    fn content_item_resource_blob_roundtrip() {
309        let resource = EmbeddedResource {
310            uri: "file:///tmp/data.bin".to_string(),
311            mime_type: Some("application/octet-stream".to_string()),
312            text: None,
313            blob: Some(b"\x00\x01\x02".to_vec()),
314        };
315        let json = serde_json::to_value(&ResourceContent { resource }).unwrap();
316        assert_eq!(json["resource"]["blob"], "AAEC");
317        assert!(json["resource"].get("text").is_none());
318
319        let item: ContentItem = serde_json::from_value(json!({
320            "type": "resource",
321            "resource": {
322                "uri": "file:///tmp/data.bin",
323                "blob": "AAEC",
324                "mimeType": "application/octet-stream"
325            }
326        }))
327        .unwrap();
328        match item {
329            ContentItem::Resource(r) => {
330                assert_eq!(r.resource.blob.as_deref(), Some(b"\x00\x01\x02".as_slice()));
331            }
332            _ => panic!("expected resource"),
333        }
334    }
335
336    #[test]
337    fn call_tool_result_with_error() {
338        let result: CallToolResult = serde_json::from_value(json!({
339            "content": [{ "type": "text", "text": "oops" }],
340            "isError": true
341        }))
342        .unwrap();
343        assert_eq!(result.is_error, Some(true));
344        assert_eq!(result.content.len(), 1);
345    }
346
347    #[test]
348    fn call_tool_result_omits_is_error_when_none() {
349        let result = CallToolResult {
350            content: vec![],
351            is_error: None,
352        };
353        let json = serde_json::to_string(&result).unwrap();
354        assert!(!json.contains("isError"));
355    }
356
357    #[test]
358    fn call_tool_params_serialize() {
359        let params = CallToolParams {
360            name: "test".to_string(),
361            arguments: Some(json!({"key": "value"})),
362        };
363        let json = serde_json::to_value(&params).unwrap();
364        assert_eq!(json["name"], "test");
365        assert_eq!(json["arguments"]["key"], "value");
366    }
367
368    #[test]
369    fn initialize_result_serialize() {
370        let result = InitializeResult {
371            protocol_version: "2025-11-25".to_string(),
372            server_info: ServerInfo {
373                name: "test".to_string(),
374                version: Some("1.0".to_string()),
375            },
376            capabilities: Some(ServerCapabilities {
377                tools: Some(json!({})),
378                ..Default::default()
379            }),
380        };
381        let json = serde_json::to_value(&result).unwrap();
382        assert_eq!(json["protocolVersion"], "2025-11-25");
383        assert_eq!(json["serverInfo"]["name"], "test");
384        assert!(json["capabilities"]["tools"].is_object());
385        assert!(json["capabilities"].get("resources").is_none());
386    }
387}