1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
//! Configuration types for MCP client
use serde::{Deserialize, Serialize};
use std::time::Duration;
/// Main client configuration
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ClientConfig {
/// Client identification information
pub client_info: ClientInfo,
/// Timeout configurations
pub timeouts: TimeoutConfig,
/// Retry configurations
pub retry: RetryConfig,
/// Connection configurations
pub connection: ConnectionConfig,
/// Logging configuration
pub logging: LoggingConfig,
/// Explicit wire-spec hint. When set, `connect()` skips auto-detection and
/// drives the handshake for this version directly. `None` (default) =
/// try `server/discover` then fall back to `initialize` on JSON-RPC -32601.
pub mcp_protocol_version: Option<crate::version::McpVersion>,
/// When `true`, broadens the version-downgrade fallback to also accept HTTP
/// 404/405 from `server/discover` (for gateways that return those for unknown
/// methods). Off by default; enabling it weakens protocol-downgrade resistance.
pub allow_legacy_gateway_fallback: bool,
/// Capabilities this client declares to servers — in the 2025 `initialize`
/// handshake and in every 2026 request's `_meta` `clientCapabilities`.
/// All off by default: declare only what the application actually handles,
/// since servers may send MRTR input requests for declared capabilities.
pub declared_capabilities: DeclaredCapabilities,
}
/// Client-side capability declarations (see [`ClientConfig::declared_capabilities`]).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DeclaredCapabilities {
/// Can answer `elicitation/create` input requests (MRTR, SEP-2322).
/// Declares form-mode support ("an empty capabilities object is
/// equivalent to declaring support for form mode only").
pub elicitation: bool,
/// Also supports URL-mode elicitation (`elicitation.url`). Servers MUST
/// NOT send URL-mode requests unless this is declared. Implies
/// `elicitation` when set.
pub elicitation_url: bool,
/// Can answer `sampling/createMessage` input requests (deprecated per SEP-2577).
pub sampling: bool,
/// Also supports tool-enabled sampling (`sampling.tools`). Servers MUST
/// NOT send `tools`/`toolChoice` unless this is declared. Implies
/// `sampling` when set.
pub sampling_tools: bool,
/// Also supports `includeContext: "thisServer"/"allServers"` in sampling
/// (`sampling.context`). Implies `sampling` when set.
pub sampling_context: bool,
/// Can answer `roots/list` input requests (deprecated per SEP-2577).
pub roots: bool,
/// Declares the Tasks extension (`io.modelcontextprotocol/tasks`,
/// SEP-2663) in every 2026 request's `_meta` `clientCapabilities.extensions`
/// — servers may then answer task-electing calls with a `CreateTaskResult`
/// instead of the normal result. Use the `call_tool_or_task`/`task_*`
/// client APIs (requires the `ext-tasks` cargo feature).
pub ext_tasks: bool,
}
/// Client identification information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientInfo {
/// Client name
pub name: String,
/// Client version
pub version: String,
/// Client description
pub description: Option<String>,
/// Vendor information
pub vendor: Option<String>,
/// Additional metadata
pub metadata: Option<serde_json::Value>,
}
/// Timeout configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeoutConfig {
/// Connection timeout
#[serde(with = "duration_serde")]
pub connect: Duration,
/// Request timeout for individual operations
#[serde(with = "duration_serde")]
pub request: Duration,
/// Long operation timeout (for streaming, etc.)
#[serde(with = "duration_serde")]
pub long_operation: Duration,
/// Session initialization timeout
#[serde(with = "duration_serde")]
pub initialization: Duration,
/// Heartbeat interval for keep-alive
#[serde(with = "duration_serde")]
pub heartbeat: Duration,
}
/// Retry configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetryConfig {
/// Maximum number of retry attempts
pub max_attempts: u32,
/// Initial retry delay
#[serde(with = "duration_serde")]
pub initial_delay: Duration,
/// Maximum retry delay
#[serde(with = "duration_serde")]
pub max_delay: Duration,
/// Exponential backoff multiplier
pub backoff_multiplier: f64,
/// Jitter factor (0.0 to 1.0)
pub jitter: f64,
/// Whether to enable exponential backoff
pub exponential_backoff: bool,
}
/// Connection configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectionConfig {
/// User agent string
pub user_agent: Option<String>,
/// Custom headers to include in requests
pub headers: Option<std::collections::HashMap<String, String>>,
/// Whether to follow redirects
pub follow_redirects: bool,
/// Maximum number of redirects to follow (only applies when `follow_redirects = true`)
pub max_redirects: u32,
/// Keep-alive settings.
///
/// No reqwest equivalent — reqwest exposes `tcp_keepalive(Option<Duration>)`, not a
/// boolean. This field is scheduled for removal in 0.4.
#[deprecated(
since = "0.3.35",
note = "no reqwest equivalent; scheduled for removal in 0.4"
)]
pub keep_alive: bool,
/// Connection pool settings
pub pool_settings: PoolConfig,
}
/// Connection pool configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PoolConfig {
/// Maximum number of idle connections per host kept in the pool.
///
/// Wired to `reqwest::ClientBuilder::pool_max_idle_per_host`. The default was raised
/// from 5 to 32 in 0.3.35 so that callers relying on the previous default (which was
/// silently ignored and deferred to reqwest's internal default) are not regressed.
pub max_idle_per_host: u32,
/// Idle connection timeout.
///
/// Wired to `reqwest::ClientBuilder::pool_idle_timeout`.
#[serde(with = "duration_serde")]
pub idle_timeout: Duration,
/// Maximum connection lifetime.
///
/// No reqwest equivalent — reqwest exposes `pool_idle_timeout` but no per-connection
/// max lifetime. This field is scheduled for removal in 0.4.
#[deprecated(
since = "0.3.35",
note = "no reqwest equivalent; scheduled for removal in 0.4"
)]
#[serde(with = "duration_serde")]
pub max_lifetime: Duration,
}
/// Logging configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
/// Log level
pub level: String,
/// Whether to log requests
pub log_requests: bool,
/// Whether to log responses
pub log_responses: bool,
/// Whether to log transport events
pub log_transport: bool,
/// Whether to redact sensitive information
pub redact_sensitive: bool,
}
impl Default for ClientInfo {
fn default() -> Self {
Self {
name: "mcp-client".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
description: Some("Rust MCP Client Library".to_string()),
vendor: Some("MCP Framework".to_string()),
metadata: None,
}
}
}
impl Default for TimeoutConfig {
fn default() -> Self {
Self {
connect: Duration::from_secs(10),
request: Duration::from_secs(30),
long_operation: Duration::from_secs(300), // 5 minutes
initialization: Duration::from_secs(15),
heartbeat: Duration::from_secs(30),
}
}
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_attempts: 3,
initial_delay: Duration::from_millis(100),
max_delay: Duration::from_secs(10),
backoff_multiplier: 2.0,
jitter: 0.1,
exponential_backoff: true,
}
}
}
impl Default for ConnectionConfig {
#[allow(deprecated)] // keep_alive is deprecated but must remain in Default until 0.4
fn default() -> Self {
Self {
user_agent: Some(format!("mcp-client/{}", env!("CARGO_PKG_VERSION"))),
headers: None,
follow_redirects: true,
max_redirects: 5,
keep_alive: true,
pool_settings: PoolConfig::default(),
}
}
}
impl Default for PoolConfig {
#[allow(deprecated)] // max_lifetime is deprecated but must remain in Default until 0.4
fn default() -> Self {
Self {
// 32 matches typical HTTP client pool sizing. Previously 5, silently ignored
// (reqwest default was `usize::MAX`). 0.3.35 honors the value, so raising the
// default here avoids regressing callers who took the previous default.
max_idle_per_host: 32,
idle_timeout: Duration::from_secs(90),
max_lifetime: Duration::from_secs(300),
}
}
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: "info".to_string(),
log_requests: true,
log_responses: true,
log_transport: false,
redact_sensitive: true,
}
}
}
impl RetryConfig {
/// Calculate the delay for a given attempt number
pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
if attempt == 0 {
return Duration::from_millis(0);
}
let mut delay = self.initial_delay;
if self.exponential_backoff && attempt > 1 {
// Apply exponential backoff
let multiplier = self.backoff_multiplier.powi((attempt - 1) as i32);
delay = Duration::from_millis((delay.as_millis() as f64 * multiplier) as u64);
}
// Cap at max delay
if delay > self.max_delay {
delay = self.max_delay;
}
// Apply jitter
if self.jitter > 0.0 {
let jitter_ms = (delay.as_millis() as f64 * self.jitter) as u64;
let random_offset = rand::random::<f64>() * jitter_ms as f64;
delay = Duration::from_millis(delay.as_millis() as u64 + random_offset as u64);
}
// Ensure final delay never exceeds max_delay (even after jitter)
if delay > self.max_delay {
delay = self.max_delay;
}
delay
}
/// Check if an attempt should be retried
pub fn should_retry(&self, attempt: u32) -> bool {
attempt < self.max_attempts
}
}
// Helper module for Duration serialization
mod duration_serde {
use serde::{Deserialize, Deserializer, Serializer};
use std::time::Duration;
pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u64(duration.as_millis() as u64)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
let millis = u64::deserialize(deserializer)?;
Ok(Duration::from_millis(millis))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_retry_delay_calculation() {
let config = RetryConfig::default();
// First attempt should have no delay
assert_eq!(config.delay_for_attempt(0), Duration::from_millis(0));
// Second attempt should have initial delay
let delay1 = config.delay_for_attempt(1);
assert!(delay1 >= config.initial_delay);
// Third attempt should be longer with exponential backoff
let delay2 = config.delay_for_attempt(2);
assert!(delay2 > delay1);
// Should not exceed max delay
let large_delay = config.delay_for_attempt(20);
assert!(large_delay <= config.max_delay);
}
#[test]
fn test_retry_attempts() {
let config = RetryConfig::default();
assert!(config.should_retry(0));
assert!(config.should_retry(1));
assert!(config.should_retry(2));
assert!(!config.should_retry(3)); // Default max is 3
}
#[test]
fn test_config_serialization() {
let config = ClientConfig::default();
let json = serde_json::to_string(&config).unwrap();
let _deserialized: ClientConfig = serde_json::from_str(&json).unwrap();
}
}