1use std::collections::HashMap;
144
145use serde::de::DeserializeOwned;
146use serde_json::Value;
147
148use crate::context::{NotificationReceiver, ServerNotification, notification_channel};
149use crate::jsonrpc::JsonRpcService;
150use crate::protocol::{
151 CallToolResult, GetPromptResult, JsonRpcRequest, JsonRpcResponse, McpNotification,
152 ReadResourceResult,
153};
154use crate::router::McpRouter;
155
156pub struct TestClient {
173 service: JsonRpcService<McpRouter>,
174 router: McpRouter,
175 notification_rx: NotificationReceiver,
176 next_id: i64,
177}
178
179impl TestClient {
180 pub fn from_router(router: McpRouter) -> Self {
185 let (tx, rx) = notification_channel(256);
186 let router = router.with_notification_sender(tx);
187 let service = JsonRpcService::new(router.clone());
188 Self {
189 service,
190 router,
191 notification_rx: rx,
192 next_id: 1,
193 }
194 }
195
196 fn next_id(&mut self) -> i64 {
197 let id = self.next_id;
198 self.next_id += 1;
199 id
200 }
201
202 pub async fn initialize(&mut self) -> Value {
207 let id = self.next_id();
208 let req = JsonRpcRequest::new(id, "initialize").with_params(serde_json::json!({
209 "protocolVersion": "2025-11-25",
210 "capabilities": {},
211 "clientInfo": {
212 "name": "test-client",
213 "version": "1.0.0"
214 }
215 }));
216
217 let result = self.send_request_inner(req).await;
218 self.router
219 .handle_notification(McpNotification::Initialized);
220 result
221 }
222
223 pub async fn list_tools(&mut self) -> Vec<Value> {
227 let result = self.send_request("tools/list", None).await;
228 result
229 .get("tools")
230 .and_then(|v| v.as_array())
231 .cloned()
232 .unwrap_or_default()
233 }
234
235 pub async fn call_tool(&mut self, name: &str, args: Value) -> CallToolResult {
250 let raw = self.call_tool_raw(name, args).await;
251 serde_json::from_value(raw).expect("failed to deserialize CallToolResult")
252 }
253
254 pub async fn call_tool_raw(&mut self, name: &str, args: Value) -> Value {
258 self.send_request(
259 "tools/call",
260 Some(serde_json::json!({
261 "name": name,
262 "arguments": args,
263 })),
264 )
265 .await
266 }
267
268 pub async fn call_tool_json(&mut self, name: &str, args: Value) -> Value {
283 let result = self.call_tool(name, args).await;
284 assert!(
285 !result.is_error,
286 "tool '{}' returned an error: {}",
287 name,
288 result.all_text()
289 );
290 result
291 .as_json()
292 .expect("no parseable content in tool result")
293 .expect("failed to parse tool result as JSON")
294 }
295
296 pub async fn call_tool_typed<T: DeserializeOwned>(&mut self, name: &str, args: Value) -> T {
314 let result = self.call_tool(name, args).await;
315 assert!(
316 !result.is_error,
317 "tool '{}' returned an error: {}",
318 name,
319 result.all_text()
320 );
321 result
322 .deserialize()
323 .expect("no parseable content in tool result")
324 .expect("failed to deserialize tool result")
325 }
326
327 pub async fn call_tool_expect_error(&mut self, name: &str, args: Value) -> Value {
333 let id = self.next_id();
334 let req = JsonRpcRequest::new(id, "tools/call").with_params(serde_json::json!({
335 "name": name,
336 "arguments": args,
337 }));
338
339 let resp = self
340 .service
341 .call_single(req)
342 .await
343 .expect("transport error");
344
345 match resp {
346 JsonRpcResponse::Error(e) => {
347 serde_json::to_value(&e.error).expect("failed to serialize error")
348 }
349 JsonRpcResponse::Result(r) => {
350 if r.result.get("isError").and_then(|v| v.as_bool()) == Some(true) {
352 r.result
353 } else {
354 panic!(
355 "expected tool call to '{}' to fail, but it succeeded: {:?}",
356 name, r.result
357 );
358 }
359 }
360 _ => panic!("unexpected response variant"),
361 }
362 }
363
364 pub async fn list_resources(&mut self) -> Vec<Value> {
368 let result = self.send_request("resources/list", None).await;
369 result
370 .get("resources")
371 .and_then(|v| v.as_array())
372 .cloned()
373 .unwrap_or_default()
374 }
375
376 pub async fn read_resource(&mut self, uri: &str) -> ReadResourceResult {
380 let raw = self
381 .send_request("resources/read", Some(serde_json::json!({ "uri": uri })))
382 .await;
383 serde_json::from_value(raw).expect("failed to deserialize ReadResourceResult")
384 }
385
386 pub async fn list_prompts(&mut self) -> Vec<Value> {
390 let result = self.send_request("prompts/list", None).await;
391 result
392 .get("prompts")
393 .and_then(|v| v.as_array())
394 .cloned()
395 .unwrap_or_default()
396 }
397
398 pub async fn get_prompt(
402 &mut self,
403 name: &str,
404 args: HashMap<String, String>,
405 ) -> GetPromptResult {
406 let raw = self
407 .send_request(
408 "prompts/get",
409 Some(serde_json::json!({
410 "name": name,
411 "arguments": args,
412 })),
413 )
414 .await;
415 serde_json::from_value(raw).expect("failed to deserialize GetPromptResult")
416 }
417
418 pub async fn complete(&mut self, params: Value) -> Value {
422 self.send_request("completion/complete", Some(params)).await
423 }
424
425 pub async fn send_request(&mut self, method: &str, params: Option<Value>) -> Value {
430 let id = self.next_id();
431 let mut req = JsonRpcRequest::new(id, method);
432 if let Some(p) = params {
433 req = req.with_params(p);
434 }
435 self.send_request_inner(req).await
436 }
437
438 pub async fn send_request_expect_error(
442 &mut self,
443 method: &str,
444 params: Option<Value>,
445 ) -> Value {
446 let id = self.next_id();
447 let mut req = JsonRpcRequest::new(id, method);
448 if let Some(p) = params {
449 req = req.with_params(p);
450 }
451
452 let resp = self
453 .service
454 .call_single(req)
455 .await
456 .expect("transport error");
457
458 match resp {
459 JsonRpcResponse::Error(e) => {
460 serde_json::to_value(&e.error).expect("failed to serialize error")
461 }
462 JsonRpcResponse::Result(r) => {
463 panic!(
464 "expected request '{}' to fail, but it succeeded: {:?}",
465 method, r.result
466 );
467 }
468 _ => panic!("unexpected response variant"),
469 }
470 }
471
472 pub fn try_recv_notification(&mut self) -> Option<ServerNotification> {
476 self.notification_rx.try_recv().ok()
477 }
478
479 pub fn drain_notifications(&mut self) -> Vec<ServerNotification> {
483 let mut notifications = Vec::new();
484 while let Ok(n) = self.notification_rx.try_recv() {
485 notifications.push(n);
486 }
487 notifications
488 }
489
490 async fn send_request_inner(&mut self, req: JsonRpcRequest) -> Value {
491 let method = req.method.clone();
492 let resp = self
493 .service
494 .call_single(req)
495 .await
496 .expect("transport error");
497
498 match resp {
499 JsonRpcResponse::Result(r) => r.result,
500 JsonRpcResponse::Error(e) => {
501 panic!(
502 "expected request '{}' to succeed, but got error: {} (code {})",
503 method, e.error.message, e.error.code,
504 );
505 }
506 _ => panic!("unexpected response variant"),
507 }
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514 use crate::{CallToolResult, GetPromptResult, PromptBuilder, ResourceBuilder, ToolBuilder};
515 use schemars::JsonSchema;
516 use serde::Deserialize;
517 use serde_json::json;
518
519 #[derive(Debug, Deserialize, JsonSchema)]
520 struct EchoInput {
521 message: String,
522 }
523
524 #[derive(Debug, Deserialize, JsonSchema)]
525 struct AddInput {
526 a: i64,
527 b: i64,
528 }
529
530 #[derive(Debug, Clone, Deserialize, serde::Serialize, JsonSchema, PartialEq)]
531 struct AddResult {
532 sum: i64,
533 }
534
535 fn create_test_router() -> McpRouter {
536 let echo = ToolBuilder::new("echo")
537 .description("Echo a message")
538 .handler(|input: EchoInput| async move { Ok(CallToolResult::text(input.message)) })
539 .build();
540
541 let add = ToolBuilder::new("add")
542 .description("Add two numbers")
543 .handler(|input: AddInput| async move {
544 Ok(CallToolResult::text(format!("{}", input.a + input.b)))
545 })
546 .build();
547
548 let add_json = ToolBuilder::new("add_json")
549 .description("Add two numbers and return JSON")
550 .handler(|input: AddInput| async move {
551 Ok(CallToolResult::from_serialize(&AddResult {
552 sum: input.a + input.b,
553 })
554 .unwrap())
555 })
556 .build();
557
558 let readme = ResourceBuilder::new("file:///README.md")
559 .name("README")
560 .description("Project readme")
561 .text("# My Project");
562
563 let greet = PromptBuilder::new("greet")
564 .description("Greet someone")
565 .required_arg("name", "Name to greet")
566 .handler(|args: HashMap<String, String>| async move {
567 let name = args.get("name").map(|s| s.as_str()).unwrap_or("World");
568 Ok(GetPromptResult::user_message(format!(
569 "Please greet {} warmly.",
570 name
571 )))
572 })
573 .build();
574
575 McpRouter::new()
576 .server_info("test-server", "1.0.0")
577 .tool(echo)
578 .tool(add)
579 .tool(add_json)
580 .resource(readme)
581 .prompt(greet)
582 }
583
584 #[tokio::test]
585 async fn test_client_initialize() {
586 let router = create_test_router();
587 let mut client = TestClient::from_router(router);
588
589 let init = client.initialize().await;
590
591 assert!(init.get("protocolVersion").is_some());
592 assert!(init.get("serverInfo").is_some());
593 assert_eq!(
594 init.get("serverInfo")
595 .and_then(|s| s.get("name"))
596 .and_then(|n| n.as_str()),
597 Some("test-server")
598 );
599 }
600
601 #[tokio::test]
602 async fn test_client_list_tools() {
603 let router = create_test_router();
604 let mut client = TestClient::from_router(router);
605 client.initialize().await;
606
607 let tools = client.list_tools().await;
608
609 assert_eq!(tools.len(), 3);
610 let names: Vec<&str> = tools
611 .iter()
612 .filter_map(|t| t.get("name").and_then(|n| n.as_str()))
613 .collect();
614 assert!(names.contains(&"echo"));
615 assert!(names.contains(&"add"));
616 assert!(names.contains(&"add_json"));
617 }
618
619 #[tokio::test]
620 async fn test_client_call_tool() {
621 let router = create_test_router();
622 let mut client = TestClient::from_router(router);
623 client.initialize().await;
624
625 let result = client.call_tool("echo", json!({"message": "hello"})).await;
626
627 assert_eq!(result.all_text(), "hello");
628 assert_eq!(result.first_text(), Some("hello"));
629 assert!(!result.is_error);
630 }
631
632 #[tokio::test]
633 async fn test_client_call_tool_with_computation() {
634 let router = create_test_router();
635 let mut client = TestClient::from_router(router);
636 client.initialize().await;
637
638 let result = client.call_tool("add", json!({"a": 40, "b": 2})).await;
639
640 assert_eq!(result.all_text(), "42");
641 }
642
643 #[tokio::test]
644 async fn test_client_call_tool_expect_error() {
645 let router = create_test_router();
646 let mut client = TestClient::from_router(router);
647 client.initialize().await;
648
649 let error = client
650 .call_tool_expect_error("nonexistent", json!({}))
651 .await;
652
653 assert!(error.get("code").is_some());
654 }
655
656 #[tokio::test]
657 async fn test_client_list_resources() {
658 let router = create_test_router();
659 let mut client = TestClient::from_router(router);
660 client.initialize().await;
661
662 let resources = client.list_resources().await;
663
664 assert_eq!(resources.len(), 1);
665 assert_eq!(
666 resources[0].get("uri").and_then(|u| u.as_str()),
667 Some("file:///README.md")
668 );
669 }
670
671 #[tokio::test]
672 async fn test_client_read_resource() {
673 let router = create_test_router();
674 let mut client = TestClient::from_router(router);
675 client.initialize().await;
676
677 let result = client.read_resource("file:///README.md").await;
678
679 assert_eq!(result.first_text(), Some("# My Project"));
680 assert_eq!(result.first_uri(), Some("file:///README.md"));
681 }
682
683 #[tokio::test]
684 async fn test_client_list_prompts() {
685 let router = create_test_router();
686 let mut client = TestClient::from_router(router);
687 client.initialize().await;
688
689 let prompts = client.list_prompts().await;
690
691 assert_eq!(prompts.len(), 1);
692 assert_eq!(
693 prompts[0].get("name").and_then(|n| n.as_str()),
694 Some("greet")
695 );
696 }
697
698 #[tokio::test]
699 async fn test_client_get_prompt() {
700 let router = create_test_router();
701 let mut client = TestClient::from_router(router);
702 client.initialize().await;
703
704 let mut args = HashMap::new();
705 args.insert("name".to_string(), "Alice".to_string());
706 let result = client.get_prompt("greet", args).await;
707
708 assert!(result.first_message_text().unwrap().contains("Alice"));
709 }
710
711 #[tokio::test]
712 async fn test_client_send_request_expect_error() {
713 let router = create_test_router();
714 let mut client = TestClient::from_router(router);
715 client.initialize().await;
716
717 let error = client
718 .send_request_expect_error("unknown/method", None)
719 .await;
720
721 assert_eq!(error.get("code").and_then(|v| v.as_i64()), Some(-32601));
723 }
724
725 #[tokio::test]
726 async fn test_client_ping() {
727 let router = create_test_router();
728 let mut client = TestClient::from_router(router);
729 client.initialize().await;
730
731 let pong = client.send_request("ping", None).await;
732
733 assert_eq!(pong, json!({}));
734 }
735
736 #[tokio::test]
737 async fn test_client_id_increments() {
738 let router = create_test_router();
739 let mut client = TestClient::from_router(router);
740
741 assert_eq!(client.next_id(), 1);
743 assert_eq!(client.next_id(), 2);
744 assert_eq!(client.next_id(), 3);
745 }
746
747 #[tokio::test]
748 async fn test_client_call_tool_raw() {
749 let router = create_test_router();
750 let mut client = TestClient::from_router(router);
751 client.initialize().await;
752
753 let raw = client
754 .call_tool_raw("echo", json!({"message": "test"}))
755 .await;
756
757 assert!(raw.get("content").is_some());
759 assert!(raw.get("content").unwrap().is_array());
760 }
761
762 #[tokio::test]
763 async fn test_client_call_tool_json() {
764 let router = create_test_router();
765 let mut client = TestClient::from_router(router);
766 client.initialize().await;
767
768 let value = client
769 .call_tool_json("add_json", json!({"a": 10, "b": 20}))
770 .await;
771 assert_eq!(value["sum"], 30);
772 }
773
774 #[tokio::test]
775 async fn test_client_call_tool_typed() {
776 let router = create_test_router();
777 let mut client = TestClient::from_router(router);
778 client.initialize().await;
779
780 let result: AddResult = client
781 .call_tool_typed("add_json", json!({"a": 10, "b": 20}))
782 .await;
783 assert_eq!(result, AddResult { sum: 30 });
784 }
785}