#![allow(missing_docs)]
#![allow(clippy::unwrap_used)]
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerEntry {
pub command: Option<String>,
pub args: Option<Vec<String>>,
pub env: Option<HashMap<String, String>>,
pub cwd: Option<String>,
pub url: Option<String>,
pub headers: Option<HashMap<String, String>>,
pub lifecycle: Option<LifecycleMode>,
pub idle_timeout: Option<u64>,
pub debug: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LifecycleMode {
KeepAlive,
Lazy,
Eager,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpSettings {
pub tool_prefix: Option<ToolPrefix>,
pub idle_timeout: Option<u64>,
pub failure_backoff_secs: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ToolPrefix {
Server,
None,
Short,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpConfig {
pub mcp_servers: HashMap<String, ServerEntry>,
pub settings: Option<McpSettings>,
}
impl Default for McpConfig {
fn default() -> Self {
Self {
mcp_servers: HashMap::new(),
settings: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolDef {
pub name: String,
pub description: Option<String>,
pub input_schema: Option<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct ToolMetadata {
pub name: String,
pub original_name: String,
pub server_name: String,
pub description: String,
pub input_schema: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum McpContent {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "image")]
Image {
data: String,
#[serde(default)]
mime_type: Option<String>,
},
#[serde(rename = "resource")]
Resource { resource: ResourceContent },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceContent {
pub uri: String,
pub text: Option<String>,
pub blob: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ServerInfo {
pub name: String,
pub version: Option<String>,
pub protocol_version: String,
}
#[derive(Debug, Clone)]
pub enum ServerStatus {
Connected,
Failed(String),
NotConnected,
}
#[derive(Debug, Clone)]
pub struct McpCallResult {
pub content: Vec<McpContent>,
pub is_error: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct JsonRpcRequest {
pub jsonrpc: &'static str,
pub id: u64,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize)]
pub struct JsonRpcNotification {
pub jsonrpc: &'static str,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RawJsonRpcMessage {
pub jsonrpc: String,
pub id: Option<u64>,
pub method: Option<String>,
pub result: Option<serde_json::Value>,
pub error: Option<JsonRpcError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcError {
pub code: i64,
pub message: String,
#[serde(default)]
pub data: Option<serde_json::Value>,
}
pub fn get_server_prefix(server_name: &str, mode: &ToolPrefix) -> String {
match mode {
ToolPrefix::None => String::new(),
ToolPrefix::Short => {
let short = server_name
.trim_end_matches("-mcp")
.trim_end_matches("_mcp")
.replace('-', "_");
if short.is_empty() {
"mcp".to_string()
} else {
short
}
}
ToolPrefix::Server => server_name.replace('-', "_"),
}
}
pub fn format_tool_name(tool_name: &str, server_name: &str, mode: &ToolPrefix) -> String {
let prefix = get_server_prefix(server_name, mode);
if prefix.is_empty() {
tool_name.to_string()
} else {
format!("{}_{}", prefix, tool_name)
}
}
pub fn effective_prefix_mode(settings: Option<&McpSettings>) -> ToolPrefix {
settings
.and_then(|s| s.tool_prefix.clone())
.unwrap_or(ToolPrefix::Server)
}
pub fn format_schema(schema: &serde_json::Value, indent: &str) -> String {
let s = match schema.as_object() {
Some(obj) => obj,
None => return format!("{indent}(no schema)"),
};
let schema_type = s.get("type").and_then(|t| t.as_str()).unwrap_or("");
let properties = s.get("properties").and_then(|p| p.as_object());
let required = s
.get("required")
.and_then(|r| r.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect::<Vec<_>>()
})
.unwrap_or_default();
if schema_type == "object" {
if let Some(props) = properties {
if props.is_empty() {
return format!("{indent}(no parameters)");
}
let mut lines = Vec::new();
for (name, prop_schema) in props {
let is_required = required.iter().any(|r| r == name);
let type_str = prop_schema
.get("type")
.and_then(|t| t.as_str())
.unwrap_or("any");
let desc = prop_schema
.get("description")
.and_then(|d| d.as_str())
.unwrap_or("");
let req_mark = if is_required { " *required*" } else { "" };
let desc_part = if desc.is_empty() {
String::new()
} else {
format!(" - {desc}")
};
lines.push(format!("{indent}{name} ({type_str}){req_mark}{desc_part}"));
}
return lines.join("\n");
}
}
format!("{indent}({schema_type})")
}