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