1use fastmcp_protocol::{JSONRPC_VERSION, JsonRpcMessage, JsonRpcRequest, JsonRpcResponse};
6
7pub fn assert_json_rpc_valid(message: &JsonRpcMessage) {
25 match message {
26 JsonRpcMessage::Request(req) => {
27 assert_eq!(
28 req.jsonrpc.as_ref(),
29 JSONRPC_VERSION,
30 "JSON-RPC version must be 2.0"
31 );
32 assert!(
33 !req.method.is_empty(),
34 "JSON-RPC request must have a method"
35 );
36 }
37 JsonRpcMessage::Response(resp) => {
38 assert_eq!(
39 resp.jsonrpc.as_ref(),
40 JSONRPC_VERSION,
41 "JSON-RPC version must be 2.0"
42 );
43 let has_result = resp.result.is_some();
45 let has_error = resp.error.is_some();
46 assert!(
47 has_result || has_error,
48 "JSON-RPC response must have either result or error"
49 );
50 assert!(
51 !(has_result && has_error),
52 "JSON-RPC response cannot have both result and error"
53 );
54 }
55 }
56}
57
58pub fn assert_json_rpc_success(response: &JsonRpcResponse) {
71 assert!(
72 response.error.is_none(),
73 "Expected success response but got error: {:?}",
74 response.error
75 );
76 assert!(
77 response.result.is_some(),
78 "Success response must have a result"
79 );
80}
81
82pub fn assert_json_rpc_error(response: &JsonRpcResponse, expected_code: Option<i32>) {
103 assert!(
104 response.error.is_some(),
105 "Expected error response but got success"
106 );
107 assert!(
108 response.result.is_none(),
109 "Error response should not have a result"
110 );
111
112 if let Some(expected) = expected_code {
113 let actual = response.error.as_ref().unwrap().code;
114 assert_eq!(
115 actual, expected,
116 "Expected error code {expected} but got {actual}"
117 );
118 }
119}
120
121pub fn assert_mcp_compliant(method: &str, response: &JsonRpcResponse) {
148 assert_json_rpc_valid(&JsonRpcMessage::Response(response.clone()));
150
151 if response.error.is_some() {
153 return;
154 }
155
156 let result = response.result.as_ref().expect("Response must have result");
157
158 match method {
160 "initialize" => {
161 assert!(
162 result.get("protocolVersion").is_some(),
163 "Initialize response must have protocolVersion"
164 );
165 assert!(
166 result.get("capabilities").is_some(),
167 "Initialize response must have capabilities"
168 );
169 assert!(
170 result.get("serverInfo").is_some(),
171 "Initialize response must have serverInfo"
172 );
173 }
174 "tools/list" => {
175 assert!(
176 result.get("tools").is_some(),
177 "tools/list response must have tools array"
178 );
179 assert!(result["tools"].is_array(), "tools must be an array");
180 }
181 "tools/call" => {
182 assert!(
183 result.get("content").is_some(),
184 "tools/call response must have content"
185 );
186 assert!(result["content"].is_array(), "content must be an array");
187 }
188 "resources/list" => {
189 assert!(
190 result.get("resources").is_some(),
191 "resources/list response must have resources array"
192 );
193 assert!(result["resources"].is_array(), "resources must be an array");
194 }
195 "resources/read" => {
196 assert!(
197 result.get("contents").is_some(),
198 "resources/read response must have contents"
199 );
200 assert!(result["contents"].is_array(), "contents must be an array");
201 }
202 "resources/templates/list" => {
203 assert!(
204 result.get("resourceTemplates").is_some(),
205 "resources/templates/list response must have resourceTemplates"
206 );
207 assert!(
208 result["resourceTemplates"].is_array(),
209 "resourceTemplates must be an array"
210 );
211 }
212 "prompts/list" => {
213 assert!(
214 result.get("prompts").is_some(),
215 "prompts/list response must have prompts array"
216 );
217 assert!(result["prompts"].is_array(), "prompts must be an array");
218 }
219 "prompts/get" => {
220 assert!(
221 result.get("messages").is_some(),
222 "prompts/get response must have messages"
223 );
224 assert!(result["messages"].is_array(), "messages must be an array");
225 }
226 _ => {
227 }
229 }
230}
231
232pub fn assert_tool_valid(tool: &serde_json::Value) {
238 assert!(tool.get("name").is_some(), "Tool must have a name");
239 assert!(tool["name"].is_string(), "Tool name must be a string");
240 if let Some(schema) = tool.get("inputSchema") {
242 assert!(schema.is_object(), "inputSchema must be an object");
243 }
244}
245
246pub fn assert_resource_valid(resource: &serde_json::Value) {
252 assert!(resource.get("uri").is_some(), "Resource must have a uri");
253 assert!(resource["uri"].is_string(), "Resource uri must be a string");
254 assert!(resource.get("name").is_some(), "Resource must have a name");
255 assert!(
256 resource["name"].is_string(),
257 "Resource name must be a string"
258 );
259}
260
261pub fn assert_prompt_valid(prompt: &serde_json::Value) {
267 assert!(prompt.get("name").is_some(), "Prompt must have a name");
268 assert!(prompt["name"].is_string(), "Prompt name must be a string");
269}
270
271pub fn assert_content_valid(content: &serde_json::Value) {
277 assert!(content.get("type").is_some(), "Content must have a type");
278 let content_type = content["type"].as_str().expect("type must be a string");
279 match content_type {
280 "text" => {
281 assert!(
282 content.get("text").is_some(),
283 "Text content must have text field"
284 );
285 }
286 "image" => {
287 assert!(
288 content.get("data").is_some(),
289 "Image content must have data field"
290 );
291 assert!(
292 content.get("mimeType").is_some(),
293 "Image content must have mimeType field"
294 );
295 }
296 "audio" => {
297 assert!(
298 content.get("data").is_some(),
299 "Audio content must have data field"
300 );
301 assert!(
302 content.get("mimeType").is_some(),
303 "Audio content must have mimeType field"
304 );
305 }
306 "resource" => {
307 assert!(
308 content.get("resource").is_some(),
309 "Resource content must have resource field"
310 );
311 }
312 _ => {
313 }
315 }
316}
317
318pub fn assert_is_notification(request: &JsonRpcRequest) {
326 assert!(
327 request.id.is_none(),
328 "Notification must not have an id field"
329 );
330}
331
332pub fn assert_is_request(request: &JsonRpcRequest) {
340 assert!(request.id.is_some(), "Request must have an id field");
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use fastmcp_protocol::RequestId;
347
348 #[test]
349 fn test_valid_request() {
350 let request = JsonRpcRequest::new("test/method", None, 1i64);
351 assert_json_rpc_valid(&JsonRpcMessage::Request(request));
352 }
353
354 #[test]
355 fn test_valid_success_response() {
356 let response = JsonRpcResponse::success(RequestId::Number(1), serde_json::json!({}));
357 assert_json_rpc_valid(&JsonRpcMessage::Response(response.clone()));
358 assert_json_rpc_success(&response);
359 }
360
361 #[test]
362 fn test_valid_error_response() {
363 let error = fastmcp_protocol::JsonRpcError {
364 code: -32601,
365 message: "Method not found".to_string(),
366 data: None,
367 };
368 let response = JsonRpcResponse {
369 jsonrpc: std::borrow::Cow::Borrowed(JSONRPC_VERSION),
370 id: Some(RequestId::Number(1)),
371 result: None,
372 error: Some(error),
373 };
374 assert_json_rpc_valid(&JsonRpcMessage::Response(response.clone()));
375 assert_json_rpc_error(&response, Some(-32601));
376 }
377
378 #[test]
379 fn test_mcp_compliant_tools_list() {
380 let response = JsonRpcResponse::success(
381 RequestId::Number(1),
382 serde_json::json!({
383 "tools": []
384 }),
385 );
386 assert_mcp_compliant("tools/list", &response);
387 }
388
389 #[test]
390 fn test_mcp_compliant_initialize() {
391 let response = JsonRpcResponse::success(
392 RequestId::Number(1),
393 serde_json::json!({
394 "protocolVersion": "2024-11-05",
395 "capabilities": {},
396 "serverInfo": {
397 "name": "test",
398 "version": "1.0"
399 }
400 }),
401 );
402 assert_mcp_compliant("initialize", &response);
403 }
404
405 #[test]
406 fn test_valid_tool() {
407 let tool = serde_json::json!({
408 "name": "my_tool",
409 "description": "A test tool",
410 "inputSchema": {
411 "type": "object"
412 }
413 });
414 assert_tool_valid(&tool);
415 }
416
417 #[test]
418 fn test_valid_resource() {
419 let resource = serde_json::json!({
420 "uri": "file:///test.txt",
421 "name": "Test File"
422 });
423 assert_resource_valid(&resource);
424 }
425
426 #[test]
427 fn test_valid_prompt() {
428 let prompt = serde_json::json!({
429 "name": "greeting",
430 "description": "A greeting prompt"
431 });
432 assert_prompt_valid(&prompt);
433 }
434
435 #[test]
436 fn test_valid_text_content() {
437 let content = serde_json::json!({
438 "type": "text",
439 "text": "Hello, world!"
440 });
441 assert_content_valid(&content);
442 }
443
444 #[test]
445 fn test_is_notification() {
446 let notification = JsonRpcRequest::notification("test", None);
447 assert_is_notification(¬ification);
448 }
449
450 #[test]
451 fn test_is_request() {
452 let request = JsonRpcRequest::new("test", None, 1i64);
453 assert_is_request(&request);
454 }
455
456 #[test]
461 fn error_response_without_expected_code() {
462 let error = fastmcp_protocol::JsonRpcError {
463 code: -32600,
464 message: "Invalid request".to_string(),
465 data: None,
466 };
467 let response = JsonRpcResponse {
468 jsonrpc: std::borrow::Cow::Borrowed(JSONRPC_VERSION),
469 id: Some(RequestId::Number(1)),
470 result: None,
471 error: Some(error),
472 };
473 assert_json_rpc_error(&response, None);
475 }
476
477 #[test]
478 fn mcp_compliant_tools_call() {
479 let response = JsonRpcResponse::success(
480 RequestId::Number(1),
481 serde_json::json!({ "content": [{"type": "text", "text": "hello"}] }),
482 );
483 assert_mcp_compliant("tools/call", &response);
484 }
485
486 #[test]
487 fn mcp_compliant_resources_list() {
488 let response =
489 JsonRpcResponse::success(RequestId::Number(1), serde_json::json!({ "resources": [] }));
490 assert_mcp_compliant("resources/list", &response);
491 }
492
493 #[test]
494 fn mcp_compliant_resources_read() {
495 let response = JsonRpcResponse::success(
496 RequestId::Number(1),
497 serde_json::json!({ "contents": [{"uri": "file:///a", "text": "data"}] }),
498 );
499 assert_mcp_compliant("resources/read", &response);
500 }
501
502 #[test]
503 fn mcp_compliant_resource_templates_list() {
504 let response = JsonRpcResponse::success(
505 RequestId::Number(1),
506 serde_json::json!({ "resourceTemplates": [] }),
507 );
508 assert_mcp_compliant("resources/templates/list", &response);
509 }
510
511 #[test]
512 fn mcp_compliant_prompts_list() {
513 let response =
514 JsonRpcResponse::success(RequestId::Number(1), serde_json::json!({ "prompts": [] }));
515 assert_mcp_compliant("prompts/list", &response);
516 }
517
518 #[test]
519 fn mcp_compliant_prompts_get() {
520 let response = JsonRpcResponse::success(
521 RequestId::Number(1),
522 serde_json::json!({ "messages": [{"role": "user", "content": {}}] }),
523 );
524 assert_mcp_compliant("prompts/get", &response);
525 }
526
527 #[test]
528 fn mcp_compliant_error_response_early_return() {
529 let error = fastmcp_protocol::JsonRpcError {
530 code: -32601,
531 message: "Method not found".to_string(),
532 data: None,
533 };
534 let response = JsonRpcResponse {
535 jsonrpc: std::borrow::Cow::Borrowed(JSONRPC_VERSION),
536 id: Some(RequestId::Number(1)),
537 result: None,
538 error: Some(error),
539 };
540 assert_mcp_compliant("tools/list", &response);
542 }
543
544 #[test]
545 fn mcp_compliant_unknown_method() {
546 let response = JsonRpcResponse::success(
547 RequestId::Number(1),
548 serde_json::json!({ "anything": true }),
549 );
550 assert_mcp_compliant("custom/method", &response);
552 }
553
554 #[test]
555 fn content_valid_image() {
556 let content = serde_json::json!({
557 "type": "image",
558 "data": "base64data",
559 "mimeType": "image/png"
560 });
561 assert_content_valid(&content);
562 }
563
564 #[test]
565 fn content_valid_audio() {
566 let content = serde_json::json!({
567 "type": "audio",
568 "data": "base64data",
569 "mimeType": "audio/wav"
570 });
571 assert_content_valid(&content);
572 }
573
574 #[test]
575 fn content_valid_resource() {
576 let content = serde_json::json!({
577 "type": "resource",
578 "resource": { "uri": "file:///test.txt", "text": "data" }
579 });
580 assert_content_valid(&content);
581 }
582
583 #[test]
584 fn content_valid_unknown_type() {
585 let content = serde_json::json!({
586 "type": "custom_extension",
587 "data": "whatever"
588 });
589 assert_content_valid(&content);
591 }
592
593 #[test]
594 fn tool_valid_without_input_schema() {
595 let tool = serde_json::json!({
596 "name": "simple_tool"
597 });
598 assert_tool_valid(&tool);
600 }
601}