1use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::fmt;
13use thiserror::Error;
14
15pub const A2C_TOOL_META: &str = "a2c_tool_meta";
17pub const A2C_VRL_TRANSFORMED: &str = "a2c_vrl_transformed";
18
19pub type ServerName = String;
21pub type ToolName = String;
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
25pub struct ToolMeta {
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub auto_apply: Option<bool>,
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub alias: Option<String>,
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub tags: Option<Vec<String>>,
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub ret_object_mapper: Option<HashMap<String, String>>,
38}
39
40impl ToolMeta {
41 pub fn new() -> Self {
43 Self {
44 auto_apply: None,
45 alias: None,
46 tags: None,
47 ret_object_mapper: None,
48 }
49 }
50}
51
52impl Default for ToolMeta {
53 fn default() -> Self {
54 Self::new()
55 }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
60#[serde(tag = "type")]
61pub enum MCPServerConfig {
62 #[serde(alias = "stdio", alias = "STDIO")]
64 Stdio(StdioServerConfig),
65 #[serde(alias = "sse", alias = "SSE")]
67 Sse(SseServerConfig),
68 #[serde(alias = "http", alias = "HTTP")]
70 Http(HttpServerConfig),
71}
72
73impl MCPServerConfig {
74 pub fn name(&self) -> &str {
76 match self {
77 MCPServerConfig::Stdio(config) => &config.name,
78 MCPServerConfig::Sse(config) => &config.name,
79 MCPServerConfig::Http(config) => &config.name,
80 }
81 }
82
83 pub fn disabled(&self) -> bool {
85 match self {
86 MCPServerConfig::Stdio(config) => config.disabled,
87 MCPServerConfig::Sse(config) => config.disabled,
88 MCPServerConfig::Http(config) => config.disabled,
89 }
90 }
91
92 pub fn forbidden_tools(&self) -> &[String] {
94 match self {
95 MCPServerConfig::Stdio(config) => &config.forbidden_tools,
96 MCPServerConfig::Sse(config) => &config.forbidden_tools,
97 MCPServerConfig::Http(config) => &config.forbidden_tools,
98 }
99 }
100
101 pub fn tool_meta(&self) -> &HashMap<ToolName, ToolMeta> {
103 match self {
104 MCPServerConfig::Stdio(config) => &config.tool_meta,
105 MCPServerConfig::Sse(config) => &config.tool_meta,
106 MCPServerConfig::Http(config) => &config.tool_meta,
107 }
108 }
109
110 pub fn default_tool_meta(&self) -> Option<&ToolMeta> {
112 match self {
113 MCPServerConfig::Stdio(config) => config.default_tool_meta.as_ref(),
114 MCPServerConfig::Sse(config) => config.default_tool_meta.as_ref(),
115 MCPServerConfig::Http(config) => config.default_tool_meta.as_ref(),
116 }
117 }
118
119 pub fn vrl(&self) -> Option<&str> {
121 match self {
122 MCPServerConfig::Stdio(config) => config.vrl.as_deref(),
123 MCPServerConfig::Sse(config) => config.vrl.as_deref(),
124 MCPServerConfig::Http(config) => config.vrl.as_deref(),
125 }
126 }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
131pub struct StdioServerConfig {
132 pub name: ServerName,
134 #[serde(default)]
136 pub disabled: bool,
137 #[serde(default)]
139 pub forbidden_tools: Vec<ToolName>,
140 #[serde(default)]
142 pub tool_meta: HashMap<ToolName, ToolMeta>,
143 pub default_tool_meta: Option<ToolMeta>,
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub vrl: Option<String>,
148 pub server_parameters: StdioServerParameters,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
154pub struct SseServerConfig {
155 pub name: ServerName,
157 #[serde(default)]
159 pub disabled: bool,
160 #[serde(default)]
162 pub forbidden_tools: Vec<ToolName>,
163 #[serde(default)]
165 pub tool_meta: HashMap<ToolName, ToolMeta>,
166 pub default_tool_meta: Option<ToolMeta>,
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub vrl: Option<String>,
171 pub server_parameters: SseServerParameters,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
177pub struct HttpServerConfig {
178 pub name: ServerName,
180 #[serde(default)]
182 pub disabled: bool,
183 #[serde(default)]
185 pub forbidden_tools: Vec<ToolName>,
186 #[serde(default)]
188 pub tool_meta: HashMap<ToolName, ToolMeta>,
189 pub default_tool_meta: Option<ToolMeta>,
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub vrl: Option<String>,
194 pub server_parameters: HttpServerParameters,
196}
197
198fn null_to_empty_map<'de, D>(deserializer: D) -> Result<HashMap<String, String>, D::Error>
199where
200 D: serde::Deserializer<'de>,
201{
202 let opt = Option::<HashMap<String, String>>::deserialize(deserializer)?;
203 Ok(opt.unwrap_or_default())
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
208pub struct StdioServerParameters {
209 pub command: String,
211 #[serde(default)]
213 pub args: Vec<String>,
214 #[serde(default, deserialize_with = "null_to_empty_map")]
216 pub env: HashMap<String, String>,
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub cwd: Option<String>,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
224pub struct SseServerParameters {
225 pub url: String,
227 #[serde(default)]
229 pub headers: HashMap<String, String>,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
234pub struct HttpServerParameters {
235 pub url: String,
237 #[serde(default)]
239 pub headers: HashMap<String, String>,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
244#[serde(tag = "type")]
245pub enum MCPServerInput {
246 PromptString(PromptStringInput),
248 PickString(PickStringInput),
250 Command(CommandInput),
252}
253
254impl MCPServerInput {
255 pub fn id(&self) -> &str {
257 match self {
258 MCPServerInput::PromptString(input) => &input.id,
259 MCPServerInput::PickString(input) => &input.id,
260 MCPServerInput::Command(input) => &input.id,
261 }
262 }
263
264 pub fn description(&self) -> &str {
266 match self {
267 MCPServerInput::PromptString(input) => &input.description,
268 MCPServerInput::PickString(input) => &input.description,
269 MCPServerInput::Command(input) => &input.description,
270 }
271 }
272
273 pub fn default(&self) -> Option<serde_json::Value> {
275 match self {
276 MCPServerInput::PromptString(input) => input
277 .default
278 .as_ref()
279 .map(|s| serde_json::Value::String(s.clone())),
280 MCPServerInput::PickString(input) => input
281 .default
282 .as_ref()
283 .map(|s| serde_json::Value::String(s.clone())),
284 MCPServerInput::Command(_input) => {
285 None
288 }
289 }
290 }
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
295pub struct PromptStringInput {
296 pub id: String,
298 pub description: String,
300 #[serde(skip_serializing_if = "Option::is_none")]
302 pub default: Option<String>,
303 #[serde(skip_serializing_if = "Option::is_none")]
305 pub password: Option<bool>,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
310pub struct PickStringInput {
311 pub id: String,
313 pub description: String,
315 #[serde(default)]
317 pub options: Vec<String>,
318 #[serde(skip_serializing_if = "Option::is_none")]
320 pub default: Option<String>,
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
325pub struct CommandInput {
326 pub id: String,
328 pub description: String,
330 pub command: String,
332 #[serde(skip_serializing_if = "Option::is_none")]
334 pub args: Option<HashMap<String, String>>,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
339pub struct HealthCheckConfig {
340 #[serde(default = "default_health_check_interval")]
342 pub interval_secs: u64,
343 #[serde(default = "default_health_check_timeout")]
345 pub timeout_secs: u64,
346 #[serde(default = "default_health_check_enabled")]
348 pub enabled: bool,
349}
350
351fn default_health_check_interval() -> u64 {
352 30
353}
354
355fn default_health_check_timeout() -> u64 {
356 5
357}
358
359fn default_health_check_enabled() -> bool {
360 true
361}
362
363impl Default for HealthCheckConfig {
364 fn default() -> Self {
365 Self {
366 interval_secs: 30,
367 timeout_secs: 5,
368 enabled: true,
369 }
370 }
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
375pub struct ReconnectPolicy {
376 #[serde(default = "default_reconnect_enabled")]
378 pub enabled: bool,
379 #[serde(default = "default_max_retries")]
381 pub max_retries: u32,
382 #[serde(default = "default_initial_delay_ms")]
384 pub initial_delay_ms: u64,
385 #[serde(default = "default_max_delay_ms")]
387 pub max_delay_ms: u64,
388 #[serde(default = "default_backoff_factor")]
390 pub backoff_factor: f64,
391}
392
393fn default_reconnect_enabled() -> bool {
394 true
395}
396
397fn default_max_retries() -> u32 {
398 5
399}
400
401fn default_initial_delay_ms() -> u64 {
402 1000
403}
404
405fn default_max_delay_ms() -> u64 {
406 30000
407}
408
409fn default_backoff_factor() -> f64 {
410 2.0
411}
412
413impl Default for ReconnectPolicy {
414 fn default() -> Self {
415 Self {
416 enabled: true,
417 max_retries: 5,
418 initial_delay_ms: 1000,
419 max_delay_ms: 30000,
420 backoff_factor: 2.0,
421 }
422 }
423}
424
425impl ReconnectPolicy {
426 pub fn calculate_delay(&self, retry_count: u32) -> std::time::Duration {
428 let delay_ms = (self.initial_delay_ms as f64 * self.backoff_factor.powi(retry_count as i32))
429 .min(self.max_delay_ms as f64) as u64;
430 std::time::Duration::from_millis(delay_ms)
431 }
432
433 pub fn should_retry(&self, retry_count: u32) -> bool {
435 self.enabled && (self.max_retries == 0 || retry_count < self.max_retries)
436 }
437}
438
439#[derive(Debug, Clone)]
441pub struct HealthCheckResult {
442 pub is_healthy: bool,
444 pub checked_at: std::time::Instant,
446 pub error: Option<String>,
448 pub response_time_ms: Option<u64>,
450}
451
452#[async_trait::async_trait]
454pub trait MCPClientProtocol: Send + Sync {
455 fn state(&self) -> ClientState;
457
458 async fn connect(&self) -> Result<(), MCPClientError>;
460
461 async fn disconnect(&self) -> Result<(), MCPClientError>;
463
464 async fn list_tools(&self) -> Result<Vec<Tool>, MCPClientError>;
466
467 async fn call_tool(
469 &self,
470 tool_name: &str,
471 params: serde_json::Value,
472 ) -> Result<CallToolResult, MCPClientError>;
473
474 async fn list_windows(&self) -> Result<Vec<Resource>, MCPClientError>;
476
477 async fn get_window_detail(
479 &self,
480 resource: Resource,
481 ) -> Result<ReadResourceResult, MCPClientError>;
482
483 async fn subscribe_window(&self, resource: Resource) -> Result<(), MCPClientError>;
485
486 async fn unsubscribe_window(&self, resource: Resource) -> Result<(), MCPClientError>;
488
489 async fn health_check(&self) -> HealthCheckResult {
493 let start = std::time::Instant::now();
494
495 if self.state() != ClientState::Connected {
497 return HealthCheckResult {
498 is_healthy: false,
499 checked_at: start,
500 error: Some(format!("Client state is {:?}, not Connected", self.state())),
501 response_time_ms: None,
502 };
503 }
504
505 match tokio::time::timeout(std::time::Duration::from_secs(5), self.list_tools()).await {
507 Ok(Ok(_)) => {
508 let elapsed = start.elapsed();
509 HealthCheckResult {
510 is_healthy: true,
511 checked_at: start,
512 error: None,
513 response_time_ms: Some(elapsed.as_millis() as u64),
514 }
515 }
516 Ok(Err(e)) => HealthCheckResult {
517 is_healthy: false,
518 checked_at: start,
519 error: Some(format!("Health check failed: {}", e)),
520 response_time_ms: None,
521 },
522 Err(_) => HealthCheckResult {
523 is_healthy: false,
524 checked_at: start,
525 error: Some("Health check timed out".to_string()),
526 response_time_ms: None,
527 },
528 }
529 }
530}
531
532#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
534pub enum ClientState {
535 Initialized,
537 Connected,
539 Disconnected,
541 Error,
543}
544
545impl fmt::Display for ClientState {
546 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
547 match self {
548 ClientState::Initialized => write!(f, "initialized"),
549 ClientState::Connected => write!(f, "connected"),
550 ClientState::Disconnected => write!(f, "disconnected"),
551 ClientState::Error => write!(f, "error"),
552 }
553 }
554}
555
556#[derive(Debug, Error)]
558pub enum MCPClientError {
559 #[error("Connection error: {0}")]
561 ConnectionError(String),
562 #[error("Protocol error: {0}")]
564 ProtocolError(String),
565 #[error("IO error: {0}")]
567 IoError(#[from] std::io::Error),
568 #[error("JSON error: {0}")]
570 JsonError(#[from] serde_json::Error),
571 #[error("Timeout error: {0}")]
573 TimeoutError(String),
574 #[error("Other error: {0}")]
576 Other(String),
577}
578
579#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
581pub struct Tool {
582 pub name: String,
584 pub description: String,
586 #[serde(rename = "inputSchema")]
588 pub input_schema: serde_json::Value,
589 #[serde(skip_serializing_if = "Option::is_none")]
591 pub annotations: Option<ToolAnnotations>,
592 #[serde(skip_serializing_if = "Option::is_none")]
594 pub meta: Option<HashMap<String, serde_json::Value>>,
595}
596
597#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
599pub struct ToolAnnotations {
600 pub title: String,
602 #[serde(rename = "readOnlyHint")]
604 pub read_only_hint: bool,
605 #[serde(rename = "destructiveHint")]
607 pub destructive_hint: bool,
608 #[serde(rename = "openWorldHint")]
610 pub open_world_hint: bool,
611}
612
613#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
615pub struct Resource {
616 pub uri: String,
618 pub name: String,
620 #[serde(skip_serializing_if = "Option::is_none")]
622 pub description: Option<String>,
623 #[serde(skip_serializing_if = "Option::is_none")]
625 pub mime_type: Option<String>,
626}
627
628#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
630pub struct CallToolResult {
631 pub content: Vec<Content>,
633 #[serde(default)]
635 pub is_error: bool,
636 #[serde(skip_serializing_if = "Option::is_none")]
638 pub meta: Option<HashMap<String, serde_json::Value>>,
639}
640
641#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
643#[serde(tag = "type")]
644pub enum Content {
645 #[serde(rename = "text")]
647 Text { text: String },
648 #[serde(rename = "image")]
650 Image { data: String, mime_type: String },
651 #[serde(rename = "resource")]
653 Resource {
654 uri: String,
655 mime_type: Option<String>,
656 },
657}
658
659#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
661pub struct ReadResourceResult {
662 pub contents: Vec<TextResourceContents>,
664}
665
666#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
668pub struct TextResourceContents {
669 pub uri: String,
671 pub text: String,
673 #[serde(skip_serializing_if = "Option::is_none")]
675 pub mime_type: Option<String>,
676}
677
678#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
680pub struct ListResourcesResult {
681 pub resources: Vec<Resource>,
683 #[serde(skip_serializing_if = "Option::is_none")]
685 pub next_cursor: Option<String>,
686}