1#![allow(missing_docs)]
5#![allow(clippy::unwrap_used)]
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
16pub struct ServerEntry {
17 #[serde(default)]
19 pub command: Option<String>,
20 #[serde(default)]
22 pub args: Option<Vec<String>>,
23 #[serde(default)]
25 pub env: Option<HashMap<String, String>>,
26 #[serde(default)]
28 pub cwd: Option<String>,
29 #[serde(default)]
31 pub url: Option<String>,
32 #[serde(default)]
34 pub headers: Option<HashMap<String, String>>,
35 #[serde(default)]
37 pub lifecycle: Option<LifecycleMode>,
38 #[serde(default, rename = "idleTimeout", alias = "idle_timeout")]
40 pub idle_timeout: Option<u64>,
41 #[serde(default)]
43 pub debug: Option<bool>,
44 #[serde(default, rename = "directTools", alias = "direct_tools")]
46 pub direct_tools: Option<DirectToolsConfig>,
47 #[serde(default, rename = "excludeTools", alias = "exclude_tools")]
49 pub exclude_tools: Option<Vec<String>>,
50 #[serde(default)]
52 pub timeout: Option<u64>,
53 #[serde(default)]
58 pub oauth: Option<OAuthConfig>,
59}
60
61#[derive(Debug, Clone, Default, Serialize, Deserialize)]
63pub struct OAuthConfig {
64 #[serde(rename = "tokenUrl", alias = "token_url")]
66 pub token_url: String,
67 #[serde(rename = "clientId", alias = "client_id")]
69 pub client_id: String,
70 #[serde(rename = "clientSecret", alias = "client_secret")]
72 pub client_secret: String,
73 #[serde(default)]
75 pub scope: Option<String>,
76}
77#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(rename_all = "kebab-case")]
80pub enum LifecycleMode {
81 KeepAlive,
83 Lazy,
85 Eager,
87}
88
89#[derive(Debug, Clone, Default, Serialize, Deserialize)]
91pub struct McpSettings {
92 #[serde(default, rename = "toolPrefix", alias = "tool_prefix")]
94 pub tool_prefix: Option<ToolPrefix>,
95 #[serde(default, rename = "idleTimeout", alias = "idle_timeout")]
97 pub idle_timeout: Option<u64>,
98 #[serde(default, rename = "failureBackoffSecs", alias = "failure_backoff_secs")]
100 pub failure_backoff_secs: Option<u64>,
101 #[serde(default, rename = "directTools", alias = "direct_tools")]
103 pub direct_tools: Option<DirectToolsConfig>,
104 #[serde(default, rename = "disableProxyTool", alias = "disable_proxy_tool")]
107 pub disable_proxy_tool: Option<bool>,
108 #[serde(
114 default,
115 rename = "discoverExternalConfigs",
116 alias = "discover_external_configs"
117 )]
118 pub discover_external_configs: Option<bool>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123#[serde(rename_all = "kebab-case")]
124pub enum ToolPrefix {
125 Server,
127 None,
129 Short,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, Default)]
135pub struct McpConfig {
136 #[serde(rename = "mcpServers", alias = "mcp_servers")]
138 pub mcp_servers: HashMap<String, ServerEntry>,
139 pub settings: Option<McpSettings>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct McpToolDef {
148 pub name: String,
150 pub description: Option<String>,
152 pub input_schema: Option<serde_json::Value>,
154}
155
156#[derive(Debug, Clone)]
158pub struct ToolMetadata {
159 pub name: String,
161 pub original_name: String,
163 pub server_name: String,
165 pub description: String,
167 pub input_schema: Option<serde_json::Value>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(tag = "type")]
174pub enum McpContent {
175 #[serde(rename = "text")]
177 Text { text: String },
178 #[serde(rename = "image")]
180 Image {
181 data: String,
182 #[serde(default)]
183 mime_type: Option<String>,
184 },
185 #[serde(rename = "resource")]
187 Resource { resource: ResourceContent },
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct ResourceContent {
193 pub uri: String,
194 pub text: Option<String>,
195 pub blob: Option<String>,
196}
197
198#[derive(Debug, Clone)]
200pub struct ServerInfo {
201 pub name: String,
202 pub version: Option<String>,
203 pub protocol_version: String,
204}
205
206#[derive(Debug, Clone)]
208pub enum ServerStatus {
209 Connected,
211 Failed(String),
213 NotConnected,
215}
216
217#[derive(Debug, Clone)]
219pub struct McpCallResult {
220 pub content: Vec<McpContent>,
222 pub is_error: bool,
224}
225
226#[derive(Debug, Clone, Serialize)]
230pub struct JsonRpcRequest {
231 pub jsonrpc: &'static str,
232 pub id: u64,
233 pub method: String,
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub params: Option<serde_json::Value>,
236}
237
238#[derive(Debug, Clone, Serialize)]
240pub struct JsonRpcNotification {
241 pub jsonrpc: &'static str,
242 pub method: String,
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub params: Option<serde_json::Value>,
245}
246
247#[derive(Debug, Clone, Deserialize)]
249pub struct RawJsonRpcMessage {
250 pub jsonrpc: String,
251 pub id: Option<u64>,
252
253 pub method: Option<String>,
254 pub result: Option<serde_json::Value>,
255 pub error: Option<JsonRpcError>,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct JsonRpcError {
261 pub code: i64,
262 pub message: String,
263 #[serde(default)]
264 pub data: Option<serde_json::Value>,
265}
266
267pub fn get_server_prefix(server_name: &str, mode: &ToolPrefix) -> String {
271 match mode {
272 ToolPrefix::None => String::new(),
273 ToolPrefix::Short => {
274 let short = server_name
275 .trim_end_matches("-mcp")
276 .trim_end_matches("_mcp")
277 .replace('-', "_");
278 if short.is_empty() {
279 "mcp".to_string()
280 } else {
281 short
282 }
283 }
284 ToolPrefix::Server => server_name.replace('-', "_"),
285 }
286}
287
288pub fn format_tool_name(tool_name: &str, server_name: &str, mode: &ToolPrefix) -> String {
290 let prefix = get_server_prefix(server_name, mode);
291 if prefix.is_empty() {
292 tool_name.to_string()
293 } else {
294 format!("{}_{}", prefix, tool_name)
295 }
296}
297
298pub fn effective_prefix_mode(settings: Option<&McpSettings>) -> ToolPrefix {
300 settings
301 .and_then(|s| s.tool_prefix.clone())
302 .unwrap_or(ToolPrefix::Server)
303}
304
305pub fn format_schema(schema: &serde_json::Value, indent: &str) -> String {
307 let s = match schema.as_object() {
308 Some(obj) => obj,
309 None => return format!("{indent}(no schema)"),
310 };
311
312 let schema_type = s.get("type").and_then(|t| t.as_str()).unwrap_or("");
313 let properties = s.get("properties").and_then(|p| p.as_object());
314 let required = s
315 .get("required")
316 .and_then(|r| r.as_array())
317 .map(|arr| {
318 arr.iter()
319 .filter_map(|v| v.as_str().map(String::from))
320 .collect::<Vec<_>>()
321 })
322 .unwrap_or_default();
323
324 if schema_type == "object"
325 && let Some(props) = properties
326 {
327 if props.is_empty() {
328 return format!("{indent}(no parameters)");
329 }
330 let mut lines = Vec::new();
331 for (name, prop_schema) in props {
332 let is_required = required.iter().any(|r| r == name);
333 let type_str = prop_schema
334 .get("type")
335 .and_then(|t| t.as_str())
336 .unwrap_or("any");
337 let desc = prop_schema
338 .get("description")
339 .and_then(|d| d.as_str())
340 .unwrap_or("");
341 let req_mark = if is_required { " *required*" } else { "" };
342 let desc_part = if desc.is_empty() {
343 String::new()
344 } else {
345 format!(" - {desc}")
346 };
347 lines.push(format!("{indent}{name} ({type_str}){req_mark}{desc_part}"));
348 }
349 return lines.join("\n");
350 }
351
352 format!("{indent}({schema_type})")
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
361#[serde(untagged)]
362pub enum DirectToolsConfig {
363 All(bool),
365 Specific(Vec<String>),
367}
368
369impl Default for DirectToolsConfig {
370 fn default() -> Self {
371 DirectToolsConfig::All(false)
372 }
373}
374
375#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
385#[serde(rename_all = "lowercase")]
386pub enum ConsentState {
387 #[default]
389 Allow,
390 Deny,
392 Ask,
397}
398
399#[derive(Debug, Clone)]
401pub struct DirectToolDef {
402 pub prefixed_name: String,
405 pub original_name: String,
407 pub server_name: String,
409 pub description: String,
411 pub input_schema: Option<serde_json::Value>,
413}
414
415#[derive(Debug, Clone, PartialEq, Eq)]
419pub enum McpConnectionStatus {
420 Connected,
422 Disconnected,
424 Connecting,
426 Error(String),
428}
429
430#[derive(Debug, Clone)]
432pub struct McpServerInfo {
433 pub name: String,
434 pub status: McpConnectionStatus,
435 pub lifecycle: String,
437 pub tool_count: usize,
439 pub tools: Vec<McpToolInfo>,
441}
442
443#[derive(Debug, Clone)]
445pub struct McpToolInfo {
446 pub name: String,
448 pub original_name: String,
450 pub description: String,
451 pub is_direct: bool,
453 pub consent: ConsentState,
455}
456
457#[derive(Debug, Clone)]
459pub struct McpSettingsView {
460 pub tool_prefix: String,
462 pub idle_timeout: Option<u64>,
464 pub total_servers: usize,
466 pub connected_servers: usize,
468 pub total_tools: usize,
470}
471
472#[derive(Debug, Clone)]
474pub struct McpDashboardData {
475 pub servers: Vec<McpServerInfo>,
476 pub settings: McpSettingsView,
477}