pub mod client;
pub mod manager;
pub mod resources;
pub mod toolset;
pub use client::McpClient;
pub use manager::McpManager;
pub use resources::{ResourceManager, ResourceQuery};
pub use toolset::{McpToolset, McpToolsetRegistry, ToolLoadConfig};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
const MCP_TOOL_PREFIX: &str = "mcp__";
#[cfg(feature = "mcp")]
pub(crate) const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &["2024-11-05", "2025-03-26"];
#[cfg(feature = "mcp")]
pub(crate) const MCP_CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
#[cfg(feature = "mcp")]
pub(crate) const MCP_CALL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
#[cfg(feature = "mcp")]
pub(crate) const MCP_RESOURCE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum McpServerConfig {
Stdio {
command: String,
#[serde(default)]
args: Vec<String>,
#[serde(default)]
env: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
cwd: Option<String>,
},
Sse {
url: String,
#[serde(default)]
headers: HashMap<String, String>,
},
}
#[derive(Clone, Debug)]
pub struct ReconnectPolicy {
pub max_retries: u32,
pub base_delay_ms: u64,
pub max_delay_ms: u64,
pub jitter_factor: f64,
}
impl Default for ReconnectPolicy {
fn default() -> Self {
Self {
max_retries: 3,
base_delay_ms: 1000,
max_delay_ms: 30000,
jitter_factor: 0.3,
}
}
}
impl ReconnectPolicy {
pub fn delay_for_attempt(&self, attempt: u32) -> std::time::Duration {
let base = self.base_delay_ms * 2u64.pow(attempt.min(10));
let jitter = (base as f64 * self.jitter_factor * rand::random::<f64>()) as u64;
std::time::Duration::from_millis((base + jitter).min(self.max_delay_ms))
}
}
pub(crate) fn parse_mcp_name(name: &str) -> Option<(&str, &str)> {
name.strip_prefix(MCP_TOOL_PREFIX)?.split_once("__")
}
pub(crate) fn make_mcp_name(server: &str, tool: &str) -> String {
format!("{}{server}__{tool}", MCP_TOOL_PREFIX)
}
pub(crate) fn is_mcp_name(name: &str) -> bool {
name.starts_with(MCP_TOOL_PREFIX)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum McpConnectionStatus {
#[default]
Connecting,
Connected,
Disconnected,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpServerInfo {
pub name: String,
pub version: String,
#[serde(default)]
pub protocol_version: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpToolDefinition {
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub input_schema: serde_json::Value,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpResourceDefinition {
pub uri: String,
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub mime_type: Option<String>,
}
#[derive(Clone, Debug)]
pub struct McpServerState {
pub name: String,
pub config: McpServerConfig,
pub status: McpConnectionStatus,
pub server_info: Option<McpServerInfo>,
pub tools: Vec<McpToolDefinition>,
pub resources: Vec<McpResourceDefinition>,
}
impl McpServerState {
pub fn new(name: impl Into<String>, config: McpServerConfig) -> Self {
Self {
name: name.into(),
config,
status: McpConnectionStatus::Connecting,
server_info: None,
tools: Vec::new(),
resources: Vec::new(),
}
}
pub fn is_connected(&self) -> bool {
self.status == McpConnectionStatus::Connected
}
}
#[derive(Debug, thiserror::Error)]
pub enum McpError {
#[error("Connection failed: {message}")]
ConnectionFailed { message: String },
#[error("Protocol error: {message}")]
Protocol { message: String },
#[error("JSON-RPC error {code}: {message}")]
JsonRpc { code: i32, message: String },
#[error("Tool error: {message}")]
ToolError { message: String },
#[error("Server not found: {name}")]
ServerNotFound { name: String },
#[error("Tool not found: {name}")]
ToolNotFound { name: String },
#[error("Resource not found: {uri}")]
ResourceNotFound { uri: String },
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
}
pub type McpResult<T> = std::result::Result<T, McpError>;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct McpToolResult {
pub content: Vec<McpContent>,
#[serde(default)]
pub is_error: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum McpContent {
Text {
text: String,
},
Image {
data: String,
mime_type: String,
},
Resource {
uri: String,
#[serde(default)]
text: Option<String>,
#[serde(default)]
blob: Option<String>,
#[serde(default)]
mime_type: Option<String>,
},
}
impl McpContent {
pub fn as_text(&self) -> Option<&str> {
match self {
McpContent::Text { text } => Some(text),
_ => None,
}
}
}
impl McpToolResult {
pub fn to_string_content(&self) -> String {
self.content
.iter()
.filter_map(|c| c.as_text())
.collect::<Vec<_>>()
.join("\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_mcp_name() {
assert_eq!(
parse_mcp_name("mcp__server__tool"),
Some(("server", "tool"))
);
assert_eq!(
parse_mcp_name("mcp__fs__read_file"),
Some(("fs", "read_file"))
);
assert_eq!(
parse_mcp_name("mcp__my_server__tool"),
Some(("my_server", "tool"))
);
assert_eq!(parse_mcp_name("Read"), None);
assert_eq!(parse_mcp_name("mcp_invalid"), None);
}
#[test]
fn test_make_mcp_name() {
assert_eq!(make_mcp_name("server", "tool"), "mcp__server__tool");
assert_eq!(make_mcp_name("fs", "read_file"), "mcp__fs__read_file");
}
#[test]
fn test_is_mcp_name() {
assert!(is_mcp_name("mcp__server__tool"));
assert!(!is_mcp_name("Read"));
assert!(!is_mcp_name("mcp_invalid"));
}
#[test]
fn test_reconnect_policy_delay() {
let policy = ReconnectPolicy::default();
let d0 = policy.delay_for_attempt(0);
let d1 = policy.delay_for_attempt(1);
assert!(d1 > d0);
assert!(d0.as_millis() >= 1000);
assert!(d0.as_millis() <= 1300);
}
#[test]
fn test_mcp_server_config_serde() {
let config = McpServerConfig::Stdio {
command: "npx".to_string(),
args: vec!["server".to_string()],
env: HashMap::new(),
cwd: None,
};
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("stdio"));
assert!(json.contains("npx"));
}
#[test]
fn test_mcp_server_state_new() {
let state = McpServerState::new(
"test",
McpServerConfig::Stdio {
command: "test".to_string(),
args: vec![],
env: HashMap::new(),
cwd: None,
},
);
assert_eq!(state.name, "test");
assert_eq!(state.status, McpConnectionStatus::Connecting);
assert!(!state.is_connected());
}
#[test]
fn test_mcp_content_as_text() {
let content = McpContent::Text {
text: "hello".to_string(),
};
assert_eq!(content.as_text(), Some("hello"));
let image = McpContent::Image {
data: "base64".to_string(),
mime_type: "image/png".to_string(),
};
assert_eq!(image.as_text(), None);
}
}