Skip to main content

tower_mcp/
testing.rs

1//! Test utilities for MCP servers.
2//!
3//! This module provides [`TestClient`], an ergonomic wrapper around [`McpRouter`]
4//! for writing concise MCP server tests without manual JSON-RPC construction.
5//!
6//! # Quick Start
7//!
8//! ```rust
9//! use tower_mcp::{CallToolResult, McpRouter, ToolBuilder, TestClient};
10//! use schemars::JsonSchema;
11//! use serde::Deserialize;
12//! use serde_json::json;
13//!
14//! #[derive(Debug, Deserialize, JsonSchema)]
15//! struct EchoInput {
16//!     message: String,
17//! }
18//!
19//! # #[tokio::main]
20//! # async fn main() {
21//! let echo = ToolBuilder::new("echo")
22//!     .description("Echo a message")
23//!     .handler(|input: EchoInput| async move {
24//!         Ok(CallToolResult::text(input.message))
25//!     })
26//!     .build();
27//!
28//! let router = McpRouter::new()
29//!     .server_info("test-server", "1.0.0")
30//!     .tool(echo);
31//!
32//! let mut client = TestClient::from_router(router);
33//! client.initialize().await;
34//!
35//! let result = client.call_tool("echo", json!({"message": "hello"})).await;
36//! assert_eq!(result.all_text(), "hello");
37//! # }
38//! ```
39//!
40//! # Full Example
41//!
42//! The following shows a complete test setup with tools, resources, and prompts:
43//!
44//! ```rust
45//! use std::collections::HashMap;
46//! use tower_mcp::{
47//!     CallToolResult, GetPromptResult, McpRouter, ReadResourceResult,
48//!     PromptBuilder, ResourceBuilder, TestClient, ToolBuilder,
49//! };
50//! use schemars::JsonSchema;
51//! use serde::Deserialize;
52//! use serde_json::json;
53//!
54//! #[derive(Debug, Deserialize, JsonSchema)]
55//! struct AddInput {
56//!     a: i64,
57//!     b: i64,
58//! }
59//!
60//! # #[tokio::main]
61//! # async fn main() {
62//! // -- Build the router ------------------------------------------------
63//! let add = ToolBuilder::new("add")
64//!     .description("Add two numbers")
65//!     .handler(|input: AddInput| async move {
66//!         Ok(CallToolResult::text(format!("{}", input.a + input.b)))
67//!     })
68//!     .build();
69//!
70//! let readme = ResourceBuilder::new("file:///README.md")
71//!     .name("README")
72//!     .description("Project readme")
73//!     .text("# My Project");
74//!
75//! let greet = PromptBuilder::new("greet")
76//!     .description("Greet someone")
77//!     .required_arg("name", "Name to greet")
78//!     .handler(|args: HashMap<String, String>| async move {
79//!         let name = args.get("name").map(|s| s.as_str()).unwrap_or("World");
80//!         Ok(GetPromptResult::user_message(
81//!             format!("Please greet {} warmly.", name),
82//!         ))
83//!     })
84//!     .build();
85//!
86//! let router = McpRouter::new()
87//!     .server_info("test-server", "1.0.0")
88//!     .tool(add)
89//!     .resource(readme)
90//!     .prompt(greet);
91//!
92//! // -- Create client and initialize ------------------------------------
93//! let mut client = TestClient::from_router(router);
94//! let init = client.initialize().await;
95//! assert!(init.get("protocolVersion").is_some());
96//!
97//! // -- Tools -----------------------------------------------------------
98//! let tools = client.list_tools().await;
99//! assert_eq!(tools.len(), 1);
100//!
101//! let result = client.call_tool("add", json!({"a": 2, "b": 3})).await;
102//! assert_eq!(result.all_text(), "5");
103//! assert_eq!(result.first_text(), Some("5"));
104//! assert!(!result.is_error);
105//!
106//! // -- Resources -------------------------------------------------------
107//! let resources = client.list_resources().await;
108//! assert_eq!(resources.len(), 1);
109//!
110//! let readme = client.read_resource("file:///README.md").await;
111//! assert_eq!(readme.first_text(), Some("# My Project"));
112//! assert_eq!(readme.first_uri(), Some("file:///README.md"));
113//!
114//! // -- Prompts ---------------------------------------------------------
115//! let prompts = client.list_prompts().await;
116//! assert_eq!(prompts.len(), 1);
117//!
118//! let mut args = HashMap::new();
119//! args.insert("name".to_string(), "Alice".to_string());
120//! let prompt = client.get_prompt("greet", args).await;
121//! assert!(prompt.first_message_text().unwrap().contains("Alice"));
122//!
123//! // -- Error handling --------------------------------------------------
124//! // Expect a JSON-RPC error for a non-existent tool:
125//! let error = client
126//!     .call_tool_expect_error("nonexistent", json!({}))
127//!     .await;
128//! assert!(error.get("code").is_some());
129//!
130//! // Expect a JSON-RPC error for an unknown method:
131//! let error = client
132//!     .send_request_expect_error("unknown/method", None)
133//!     .await;
134//! assert_eq!(error.get("code").and_then(|v| v.as_i64()), Some(-32601));
135//!
136//! // -- Raw escape hatch ------------------------------------------------
137//! // Use send_request for methods without typed helpers:
138//! let pong = client.send_request("ping", None).await;
139//! assert_eq!(pong, json!({}));
140//! # }
141//! ```
142
143use 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
156/// An ergonomic test client for MCP servers.
157///
158/// Wraps an [`McpRouter`] and [`JsonRpcService`] to provide typed, concise
159/// methods for testing MCP server behavior. All methods that expect successful
160/// responses will panic on JSON-RPC errors, which is appropriate for test code.
161///
162/// # Construction
163///
164/// Use [`TestClient::from_router`] to create a client from an existing router:
165///
166/// ```rust
167/// use tower_mcp::{McpRouter, TestClient};
168///
169/// let router = McpRouter::new().server_info("test", "1.0.0");
170/// let mut client = TestClient::from_router(router);
171/// ```
172pub struct TestClient {
173    service: JsonRpcService<McpRouter>,
174    router: McpRouter,
175    notification_rx: NotificationReceiver,
176    next_id: i64,
177}
178
179impl TestClient {
180    /// Create a new test client from an [`McpRouter`].
181    ///
182    /// Sets up a notification channel and wraps the router in a
183    /// [`JsonRpcService`] for JSON-RPC framing.
184    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    /// Send an initialize request and the initialized notification.
203    ///
204    /// Returns the raw JSON result from the initialize response.
205    /// Panics if initialization fails.
206    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    /// List all tools registered on the server.
224    ///
225    /// Returns the tools array from the response. Panics on error.
226    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    /// Call a tool by name with the given arguments.
236    ///
237    /// Returns a typed [`CallToolResult`]. Panics on JSON-RPC errors.
238    ///
239    /// # Example
240    ///
241    /// ```rust,no_run
242    /// # use tower_mcp::TestClient;
243    /// # use serde_json::json;
244    /// # async fn example(client: &mut TestClient) {
245    /// let result = client.call_tool("echo", json!({"message": "hi"})).await;
246    /// assert_eq!(result.first_text(), Some("hi"));
247    /// # }
248    /// ```
249    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    /// Call a tool and return the raw JSON response.
255    ///
256    /// Useful when you need to inspect fields not covered by [`CallToolResult`].
257    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    /// Call a tool and parse the result as a JSON [`Value`].
269    ///
270    /// Panics if the tool call fails, returns an error, or has no parseable content.
271    ///
272    /// # Example
273    ///
274    /// ```rust,no_run
275    /// # use tower_mcp::TestClient;
276    /// # use serde_json::json;
277    /// # async fn example(client: &mut TestClient) {
278    /// let value = client.call_tool_json("search", json!({"q": "rust"})).await;
279    /// assert!(value["results"].is_array());
280    /// # }
281    /// ```
282    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    /// Call a tool and deserialize the result into a typed value.
297    ///
298    /// Panics if the tool call fails, returns an error, or deserialization fails.
299    ///
300    /// # Example
301    ///
302    /// ```rust,no_run
303    /// # use tower_mcp::TestClient;
304    /// # use serde::Deserialize;
305    /// # use serde_json::json;
306    /// # #[derive(Deserialize)]
307    /// # struct SearchResult { count: usize }
308    /// # async fn example(client: &mut TestClient) {
309    /// let result: SearchResult = client.call_tool_typed("search", json!({"q": "rust"})).await;
310    /// assert!(result.count > 0);
311    /// # }
312    /// ```
313    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    /// Call a tool and assert that it returns an error.
328    ///
329    /// Panics if the tool call succeeds without an error. Returns the raw
330    /// JSON response body (which may be a `CallToolResult` with `isError: true`
331    /// or a JSON-RPC error).
332    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                // Check for isError flag in CallToolResult
351                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    /// List all resources registered on the server.
365    ///
366    /// Returns the resources array from the response. Panics on error.
367    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    /// Read a resource by URI.
377    ///
378    /// Returns a typed [`ReadResourceResult`]. Panics on JSON-RPC errors.
379    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    /// List all prompts registered on the server.
387    ///
388    /// Returns the prompts array from the response. Panics on error.
389    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    /// Get a prompt by name with the given arguments.
399    ///
400    /// Returns a typed [`GetPromptResult`]. Panics on JSON-RPC errors.
401    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    /// Send a completion request with raw parameters.
419    ///
420    /// Returns the raw JSON result. Panics on error.
421    pub async fn complete(&mut self, params: Value) -> Value {
422        self.send_request("completion/complete", Some(params)).await
423    }
424
425    /// Send an arbitrary request and expect success.
426    ///
427    /// This is an escape hatch for methods not covered by the typed helpers.
428    /// Panics on JSON-RPC errors.
429    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    /// Send an arbitrary request and expect a JSON-RPC error.
439    ///
440    /// Panics if the response is a success. Returns the error object as JSON.
441    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    /// Try to receive a notification without blocking.
473    ///
474    /// Returns `None` if no notification is available.
475    pub fn try_recv_notification(&mut self) -> Option<ServerNotification> {
476        self.notification_rx.try_recv().ok()
477    }
478
479    /// Drain all pending notifications.
480    ///
481    /// Returns all notifications that have been sent since the last drain.
482    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        // -32601 is "Method not found"
722        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        // Each call should increment the ID
742        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        // Raw response should have content array
758        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}