lash_plugin_mcp/
config.rs1use std::collections::BTreeMap;
2use std::path::PathBuf;
3use std::time::Duration;
4
5use serde::{Deserialize, Serialize};
6
7use crate::error::McpError;
8
9const DEFAULT_STARTUP_TIMEOUT_MS: u64 = 10_000;
10const DEFAULT_CALL_TIMEOUT_MS: u64 = 60_000;
11
12fn default_startup_timeout_ms() -> u64 {
13 DEFAULT_STARTUP_TIMEOUT_MS
14}
15
16fn default_call_timeout_ms() -> u64 {
17 DEFAULT_CALL_TIMEOUT_MS
18}
19
20fn is_default_startup_timeout_ms(value: &u64) -> bool {
21 *value == DEFAULT_STARTUP_TIMEOUT_MS
22}
23
24fn is_default_call_timeout_ms(value: &u64) -> bool {
25 *value == DEFAULT_CALL_TIMEOUT_MS
26}
27
28#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
31#[serde(tag = "transport", rename_all = "snake_case")]
32pub enum McpServerConfig {
33 Stdio {
35 command: String,
36 #[serde(default, skip_serializing_if = "Vec::is_empty")]
37 args: Vec<String>,
38 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
39 env: BTreeMap<String, String>,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 cwd: Option<PathBuf>,
42 #[serde(
43 default = "default_startup_timeout_ms",
44 skip_serializing_if = "is_default_startup_timeout_ms"
45 )]
46 startup_timeout_ms: u64,
47 #[serde(
48 default = "default_call_timeout_ms",
49 skip_serializing_if = "is_default_call_timeout_ms"
50 )]
51 call_timeout_ms: u64,
52 },
53 StreamableHttp {
55 url: String,
56 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
57 headers: BTreeMap<String, String>,
58 #[serde(
59 default = "default_startup_timeout_ms",
60 skip_serializing_if = "is_default_startup_timeout_ms"
61 )]
62 startup_timeout_ms: u64,
63 #[serde(
64 default = "default_call_timeout_ms",
65 skip_serializing_if = "is_default_call_timeout_ms"
66 )]
67 call_timeout_ms: u64,
68 },
69 Sse {
71 url: String,
72 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
73 headers: BTreeMap<String, String>,
74 #[serde(
75 default = "default_startup_timeout_ms",
76 skip_serializing_if = "is_default_startup_timeout_ms"
77 )]
78 startup_timeout_ms: u64,
79 #[serde(
80 default = "default_call_timeout_ms",
81 skip_serializing_if = "is_default_call_timeout_ms"
82 )]
83 call_timeout_ms: u64,
84 },
85}
86
87impl McpServerConfig {
88 pub fn stdio(command: impl Into<String>, args: Vec<String>) -> Self {
90 Self::Stdio {
91 command: command.into(),
92 args,
93 env: BTreeMap::new(),
94 cwd: None,
95 startup_timeout_ms: default_startup_timeout_ms(),
96 call_timeout_ms: default_call_timeout_ms(),
97 }
98 }
99
100 pub fn streamable_http(url: impl Into<String>) -> Self {
102 Self::StreamableHttp {
103 url: url.into(),
104 headers: BTreeMap::new(),
105 startup_timeout_ms: default_startup_timeout_ms(),
106 call_timeout_ms: default_call_timeout_ms(),
107 }
108 }
109
110 pub fn sse(url: impl Into<String>) -> Self {
112 Self::Sse {
113 url: url.into(),
114 headers: BTreeMap::new(),
115 startup_timeout_ms: default_startup_timeout_ms(),
116 call_timeout_ms: default_call_timeout_ms(),
117 }
118 }
119
120 pub fn startup_timeout(&self) -> Duration {
121 Duration::from_millis(match self {
122 Self::Stdio {
123 startup_timeout_ms, ..
124 }
125 | Self::StreamableHttp {
126 startup_timeout_ms, ..
127 }
128 | Self::Sse {
129 startup_timeout_ms, ..
130 } => *startup_timeout_ms,
131 })
132 }
133
134 pub fn call_timeout(&self) -> Duration {
135 Duration::from_millis(match self {
136 Self::Stdio {
137 call_timeout_ms, ..
138 }
139 | Self::StreamableHttp {
140 call_timeout_ms, ..
141 }
142 | Self::Sse {
143 call_timeout_ms, ..
144 } => *call_timeout_ms,
145 })
146 }
147
148 pub(crate) fn validate(&self, server_name: &str) -> Result<(), McpError> {
149 if server_name.trim().is_empty() {
150 return Err(McpError::Config(
151 "MCP server name cannot be empty".to_string(),
152 ));
153 }
154 if server_name.contains("__") {
155 return Err(McpError::Config(format!(
156 "MCP server `{server_name}` cannot contain `__`"
157 )));
158 }
159 match self {
160 Self::Stdio { command, .. } if command.trim().is_empty() => Err(McpError::Config(
161 format!("MCP server `{server_name}` command cannot be empty"),
162 )),
163 Self::StreamableHttp { url, .. } | Self::Sse { url, .. } if url.trim().is_empty() => {
164 Err(McpError::Config(format!(
165 "MCP server `{server_name}` URL cannot be empty"
166 )))
167 }
168 _ => Ok(()),
169 }
170 }
171}