1#![allow(missing_docs)]
5#![allow(clippy::unwrap_used)]
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ServerEntry {
17 pub command: Option<String>,
19 pub args: Option<Vec<String>>,
21 pub env: Option<HashMap<String, String>>,
23 pub cwd: Option<String>,
25 pub url: Option<String>,
27 pub headers: Option<HashMap<String, String>>,
29 pub lifecycle: Option<LifecycleMode>,
31 pub idle_timeout: Option<u64>,
33 pub debug: Option<bool>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(rename_all = "kebab-case")]
40pub enum LifecycleMode {
41 KeepAlive,
43 Lazy,
45 Eager,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct McpSettings {
52 pub tool_prefix: Option<ToolPrefix>,
54 pub idle_timeout: Option<u64>,
56 pub failure_backoff_secs: Option<u64>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62#[serde(rename_all = "kebab-case")]
63pub enum ToolPrefix {
64 Server,
66 None,
68 Short,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, Default)]
74pub struct McpConfig {
75 pub mcp_servers: HashMap<String, ServerEntry>,
77 pub settings: Option<McpSettings>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct McpToolDef {
86 pub name: String,
88 pub description: Option<String>,
90 pub input_schema: Option<serde_json::Value>,
92}
93
94#[derive(Debug, Clone)]
96pub struct ToolMetadata {
97 pub name: String,
99 pub original_name: String,
101 pub server_name: String,
103 pub description: String,
105 pub input_schema: Option<serde_json::Value>,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111#[serde(tag = "type")]
112pub enum McpContent {
113 #[serde(rename = "text")]
115 Text { text: String },
116 #[serde(rename = "image")]
118 Image {
119 data: String,
120 #[serde(default)]
121 mime_type: Option<String>,
122 },
123 #[serde(rename = "resource")]
125 Resource { resource: ResourceContent },
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ResourceContent {
131 pub uri: String,
132 pub text: Option<String>,
133 pub blob: Option<String>,
134}
135
136#[derive(Debug, Clone)]
138pub struct ServerInfo {
139 pub name: String,
140 pub version: Option<String>,
141 pub protocol_version: String,
142}
143
144#[derive(Debug, Clone)]
146pub enum ServerStatus {
147 Connected,
149 Failed(String),
151 NotConnected,
153}
154
155#[derive(Debug, Clone)]
157pub struct McpCallResult {
158 pub content: Vec<McpContent>,
160 pub is_error: bool,
162}
163
164#[derive(Debug, Clone, Serialize)]
168pub struct JsonRpcRequest {
169 pub jsonrpc: &'static str,
170 pub id: u64,
171 pub method: String,
172 #[serde(skip_serializing_if = "Option::is_none")]
173 pub params: Option<serde_json::Value>,
174}
175
176#[derive(Debug, Clone, Serialize)]
178pub struct JsonRpcNotification {
179 pub jsonrpc: &'static str,
180 pub method: String,
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub params: Option<serde_json::Value>,
183}
184
185#[derive(Debug, Clone, Deserialize)]
187pub struct RawJsonRpcMessage {
188 pub jsonrpc: String,
189 pub id: Option<u64>,
190
191 pub method: Option<String>,
192 pub result: Option<serde_json::Value>,
193 pub error: Option<JsonRpcError>,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct JsonRpcError {
199 pub code: i64,
200 pub message: String,
201 #[serde(default)]
202 pub data: Option<serde_json::Value>,
203}
204
205pub fn get_server_prefix(server_name: &str, mode: &ToolPrefix) -> String {
209 match mode {
210 ToolPrefix::None => String::new(),
211 ToolPrefix::Short => {
212 let short = server_name
213 .trim_end_matches("-mcp")
214 .trim_end_matches("_mcp")
215 .replace('-', "_");
216 if short.is_empty() {
217 "mcp".to_string()
218 } else {
219 short
220 }
221 }
222 ToolPrefix::Server => server_name.replace('-', "_"),
223 }
224}
225
226pub fn format_tool_name(tool_name: &str, server_name: &str, mode: &ToolPrefix) -> String {
228 let prefix = get_server_prefix(server_name, mode);
229 if prefix.is_empty() {
230 tool_name.to_string()
231 } else {
232 format!("{}_{}", prefix, tool_name)
233 }
234}
235
236pub fn effective_prefix_mode(settings: Option<&McpSettings>) -> ToolPrefix {
238 settings
239 .and_then(|s| s.tool_prefix.clone())
240 .unwrap_or(ToolPrefix::Server)
241}
242
243pub fn format_schema(schema: &serde_json::Value, indent: &str) -> String {
245 let s = match schema.as_object() {
246 Some(obj) => obj,
247 None => return format!("{indent}(no schema)"),
248 };
249
250 let schema_type = s.get("type").and_then(|t| t.as_str()).unwrap_or("");
251 let properties = s.get("properties").and_then(|p| p.as_object());
252 let required = s
253 .get("required")
254 .and_then(|r| r.as_array())
255 .map(|arr| {
256 arr.iter()
257 .filter_map(|v| v.as_str().map(String::from))
258 .collect::<Vec<_>>()
259 })
260 .unwrap_or_default();
261
262 if schema_type == "object" {
263 if let Some(props) = properties {
264 if props.is_empty() {
265 return format!("{indent}(no parameters)");
266 }
267 let mut lines = Vec::new();
268 for (name, prop_schema) in props {
269 let is_required = required.iter().any(|r| r == name);
270 let type_str = prop_schema
271 .get("type")
272 .and_then(|t| t.as_str())
273 .unwrap_or("any");
274 let desc = prop_schema
275 .get("description")
276 .and_then(|d| d.as_str())
277 .unwrap_or("");
278 let req_mark = if is_required { " *required*" } else { "" };
279 let desc_part = if desc.is_empty() {
280 String::new()
281 } else {
282 format!(" - {desc}")
283 };
284 lines.push(format!("{indent}{name} ({type_str}){req_mark}{desc_part}"));
285 }
286 return lines.join("\n");
287 }
288 }
289
290 format!("{indent}({schema_type})")
291}