1use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use serde_with::{base64::Base64, serde_as, skip_serializing_none};
14
15pub const PROTOCOL_VERSION: &str = "2025-11-25";
17
18pub use crate::jsonrpc::{
20 Body as JsonRpcBody, Error as JsonRpcError, Request as JsonRpcRequest,
21 Response as JsonRpcResponse, Version as JsonRpcVersion,
22};
23
24#[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#[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#[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#[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#[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#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct TextContent {
138 pub text: String,
139}
140
141#[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#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct ResourceContent {
156 pub resource: EmbeddedResource,
157}
158
159#[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
177pub 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(¶ms).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}