Skip to main content

timeout_client/
timeout_client.rs

1//! Example MCP client that demonstrates timeout and retry behavior
2//!
3//! This client connects to the timeout_server and tests various scenarios:
4//! - Retrying operations that fail intermittently
5//! - Handling timeouts for slow operations
6//! - Dealing with non-retryable errors
7
8use std::env;
9use std::time::Duration;
10use tenx_mcp::{
11    client::{ClientConfig, MCPClient},
12    error::{MCPError, Result},
13    retry::RetryConfig,
14    schema::*,
15    transport::TcpTransport,
16};
17use tracing::{error, info};
18
19async fn test_reliable_operation(client: &mut MCPClient) -> Result<()> {
20    info!("\n=== Testing Reliable Operation ===");
21    info!("This should succeed immediately...");
22
23    match client
24        .call_tool("reliable_operation".to_string(), None)
25        .await
26    {
27        Ok(result) => {
28            info!("✓ Success: {:?}", result.content);
29        }
30        Err(e) => {
31            error!("✗ Unexpected failure: {}", e);
32        }
33    }
34
35    Ok(())
36}
37
38async fn test_flakey_operation(client: &mut MCPClient) -> Result<()> {
39    info!("\n=== Testing Flakey Operation ===");
40    info!("This operation fails 2 times before succeeding.");
41    info!("With retry enabled, it should eventually succeed...");
42
43    match client.call_tool("flakey_operation".to_string(), None).await {
44        Ok(result) => {
45            info!("✓ Success after retries: {:?}", result.content);
46        }
47        Err(e) => {
48            error!("✗ Failed even with retries: {}", e);
49        }
50    }
51
52    Ok(())
53}
54
55async fn test_slow_operation(client: &mut MCPClient) -> Result<()> {
56    info!("\n=== Testing Slow Operation ===");
57    info!("This operation takes 5 seconds, but our timeout is 2 seconds.");
58    info!("It should timeout and retry, but still fail...");
59
60    match client.call_tool("slow_operation".to_string(), None).await {
61        Ok(_) => {
62            error!("✗ Unexpected success - should have timed out");
63        }
64        Err(e) => {
65            info!("✓ Expected timeout: {}", e);
66            // Verify it's actually a timeout error
67            if !matches!(e, MCPError::Timeout { .. }) {
68                error!("Error was not a timeout: {:?}", e);
69            }
70        }
71    }
72
73    Ok(())
74}
75
76async fn test_broken_operation(client: &mut MCPClient) -> Result<()> {
77    info!("\n=== Testing Broken Operation ===");
78    info!("This operation always fails with a non-retryable error.");
79    info!("Should fail immediately without retries...");
80
81    match client.call_tool("broken_operation".to_string(), None).await {
82        Ok(_) => {
83            error!("✗ Unexpected success");
84        }
85        Err(e) => {
86            info!("✓ Expected non-retryable error: {}", e);
87            // Verify it's actually non-retryable
88            if e.is_retryable() {
89                error!("Error was retryable when it shouldn't be: {:?}", e);
90            }
91        }
92    }
93
94    Ok(())
95}
96
97async fn test_custom_retry_config(host: &str, port: u16) -> Result<()> {
98    info!("\n=== Testing Custom Retry Configuration ===");
99    info!("Creating a client with very short timeouts and few retries...");
100    
101    // Create a new client with custom configuration
102    let config = ClientConfig {
103        retry: RetryConfig {
104            max_attempts: 2,
105            initial_delay: Duration::from_millis(50),
106            max_delay: Duration::from_millis(100),
107            backoff_multiplier: 2.0,
108            timeout: Duration::from_millis(500), // Very short timeout
109        },
110        request_timeout: Duration::from_secs(1),
111    };
112    
113    let mut client = MCPClient::with_config(config);
114    let transport = TcpTransport::new(format!("{host}:{port}"));
115    
116    client.connect(Box::new(transport)).await?;
117    client.initialize(
118        Implementation {
119            name: "timeout-test-client-custom".to_string(),
120            version: "1.0.0".to_string(),
121        },
122        ClientCapabilities::default(),
123    ).await?;
124    
125    // This should timeout very quickly
126    match client.call_tool("slow_operation".to_string(), None).await {
127        Ok(_) => {
128            error!("✗ Unexpected success");
129        }
130        Err(e) => {
131            info!("✓ Quick timeout as expected: {}", e);
132        }
133    }
134    
135    Ok(())
136}
137
138#[tokio::main]
139async fn main() -> Result<()> {
140    // Initialize logging
141    tracing_subscriber::fmt().with_target(false).init();
142
143    // Parse command line arguments
144    let args: Vec<String> = env::args().collect();
145
146    let (host, port) = if args.len() == 3 {
147        (
148            args[1].clone(),
149            args[2].parse::<u16>().expect("Invalid port number"),
150        )
151    } else if args.len() == 1 {
152        // Default to localhost:3001 (matching timeout_server default)
153        ("127.0.0.1".to_string(), 3001)
154    } else {
155        eprintln!("Usage: {} [host] [port]", args[0]);
156        eprintln!("Example: {} 127.0.0.1 3001", args[0]);
157        eprintln!("If no arguments provided, defaults to 127.0.0.1:3001");
158        std::process::exit(1);
159    };
160
161    info!("Starting Timeout Test Client");
162    info!("Connecting to {}:{}", host, port);
163
164    // Create client with default retry configuration
165    let retry_config = RetryConfig {
166        max_attempts: 3,
167        initial_delay: Duration::from_millis(100),
168        max_delay: Duration::from_secs(2),
169        backoff_multiplier: 2.0,
170        timeout: Duration::from_secs(2), // 2 second timeout
171    };
172
173    let config = ClientConfig {
174        retry: retry_config,
175        request_timeout: Duration::from_secs(5),
176    };
177
178    let mut client = MCPClient::with_config(config);
179
180    // Create transport
181    let transport = TcpTransport::new(format!("{host}:{port}"));
182
183    // Connect and initialize
184    info!("Connecting to server...");
185    client.connect(Box::new(transport)).await?;
186
187    let init_result = client
188        .initialize(
189            Implementation {
190                name: "timeout-test-client".to_string(),
191                version: "1.0.0".to_string(),
192            },
193            ClientCapabilities::default(),
194        )
195        .await?;
196
197    info!(
198        "Connected! Server: {} v{}",
199        init_result.server_info.name, init_result.server_info.version
200    );
201
202    // List available tools
203    info!("\nAvailable tools:");
204    let tools = client.list_tools().await?;
205    for tool in &tools.tools {
206        info!(
207            "  - {}: {}",
208            tool.name,
209            tool.description.as_deref().unwrap_or("")
210        );
211    }
212
213    // Run tests
214    if let Err(e) = test_reliable_operation(&mut client).await {
215        error!("Reliable operation test failed: {}", e);
216    }
217    
218    if let Err(e) = test_flakey_operation(&mut client).await {
219        error!("Flakey operation test failed: {}", e);
220    }
221    
222    if let Err(e) = test_slow_operation(&mut client).await {
223        error!("Slow operation test failed: {}", e);
224    }
225    
226    if let Err(e) = test_broken_operation(&mut client).await {
227        error!("Broken operation test failed: {}", e);
228    }
229
230    // Test with custom configuration
231    info!("\n{}", "=".repeat(50));
232    if let Err(e) = test_custom_retry_config(&host, port).await {
233        error!("Custom retry config test failed: {}", e);
234    }
235
236    info!("\n=== All tests completed ===");
237    info!("Summary:");
238    info!("- Reliable operation: Should succeed immediately");
239    info!("- Flakey operation: Should fail 2 times then succeed");
240    info!("- Slow operation: Should timeout after 2 seconds");
241    info!("- Broken operation: Should fail with non-retryable error");
242    info!("- Custom retry config: Demonstrates configuration options");
243
244    Ok(())
245}