use std::collections::BTreeMap;
use std::io;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use crate::config::McpTransport;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum JsonRpcId {
Number(u64),
String(String),
Null,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct JsonRpcRequest<T = JsonValue> {
pub jsonrpc: String,
pub id: JsonRpcId,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<T>,
}
impl<T> JsonRpcRequest<T> {
#[must_use]
pub fn new(id: JsonRpcId, method: impl Into<String>, params: Option<T>) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id,
method: method.into(),
params,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct JsonRpcError {
pub code: i64,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<JsonValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct JsonRpcResponse<T = JsonValue> {
pub jsonrpc: String,
pub id: JsonRpcId,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct McpInitializeParams {
pub protocol_version: String,
pub capabilities: JsonValue,
pub client_info: McpInitializeClientInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct McpInitializeClientInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct McpInitializeResult {
pub protocol_version: String,
pub capabilities: JsonValue,
pub server_info: McpInitializeServerInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct McpInitializeServerInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct McpListToolsParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct McpTool {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "inputSchema", skip_serializing_if = "Option::is_none")]
pub input_schema: Option<JsonValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<JsonValue>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<JsonValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct McpListToolsResult {
pub tools: Vec<McpTool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct McpToolCallParams {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<JsonValue>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<JsonValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct McpToolCallContent {
#[serde(rename = "type")]
pub kind: String,
#[serde(flatten)]
pub data: BTreeMap<String, JsonValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct McpToolCallResult {
#[serde(default)]
pub content: Vec<McpToolCallContent>,
#[serde(default)]
pub structured_content: Option<JsonValue>,
#[serde(default)]
pub is_error: Option<bool>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<JsonValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct McpListResourcesParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct McpResource {
pub uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<JsonValue>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<JsonValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct McpListResourcesResult {
pub resources: Vec<McpResource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct McpReadResourceParams {
pub uri: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct McpResourceContents {
pub uri: String,
#[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub blob: Option<String>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<JsonValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct McpReadResourceResult {
pub contents: Vec<McpResourceContents>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ManagedMcpTool {
pub server_name: String,
pub qualified_name: String,
pub raw_name: String,
pub tool: McpTool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnsupportedMcpServer {
pub server_name: String,
pub transport: McpTransport,
pub reason: String,
}
#[derive(Debug)]
pub enum McpServerManagerError {
Io(io::Error),
SpawnFailed {
server_name: String,
source: io::Error,
},
JsonRpc {
server_name: String,
method: &'static str,
error: JsonRpcError,
},
InvalidResponse {
server_name: String,
method: &'static str,
details: String,
},
UnknownTool {
qualified_name: String,
},
UnknownServer {
server_name: String,
},
}
impl std::fmt::Display for McpServerManagerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(error) => write!(f, "{error}"),
Self::SpawnFailed {
server_name,
source,
} => write!(
f,
"failed to connect to MCP server `{server_name}`: {source}"
),
Self::JsonRpc {
server_name,
method,
error,
} => write!(
f,
"MCP server `{server_name}` returned JSON-RPC error for {method}: {} ({})",
error.message, error.code
),
Self::InvalidResponse {
server_name,
method,
details,
} => write!(
f,
"MCP server `{server_name}` returned invalid response for {method}: {details}"
),
Self::UnknownTool { qualified_name } => {
write!(f, "unknown MCP tool `{qualified_name}`")
}
Self::UnknownServer { server_name } => write!(f, "unknown MCP server `{server_name}`"),
}
}
}
impl std::error::Error for McpServerManagerError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(error) | Self::SpawnFailed { source: error, .. } => Some(error),
Self::JsonRpc { .. }
| Self::InvalidResponse { .. }
| Self::UnknownTool { .. }
| Self::UnknownServer { .. } => None,
}
}
}
impl From<io::Error> for McpServerManagerError {
fn from(value: io::Error) -> Self {
Self::Io(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_display_covers_all_variants() {
let io_err = McpServerManagerError::Io(io::Error::new(io::ErrorKind::NotFound, "gone"));
assert!(io_err.to_string().contains("gone"));
let spawn_err = McpServerManagerError::SpawnFailed {
server_name: "test-srv".into(),
source: io::Error::new(io::ErrorKind::PermissionDenied, "denied"),
};
assert!(spawn_err.to_string().contains("test-srv"));
assert!(spawn_err.to_string().contains("denied"));
let rpc_err = McpServerManagerError::JsonRpc {
server_name: "rpc-srv".into(),
method: "initialize",
error: JsonRpcError {
code: -32600,
message: "bad request".into(),
data: None,
},
};
assert!(rpc_err.to_string().contains("rpc-srv"));
assert!(rpc_err.to_string().contains("bad request"));
let invalid = McpServerManagerError::InvalidResponse {
server_name: "inv-srv".into(),
method: "tools/list",
details: "missing tools".into(),
};
assert!(invalid.to_string().contains("inv-srv"));
assert!(invalid.to_string().contains("missing tools"));
let unknown_tool = McpServerManagerError::UnknownTool {
qualified_name: "srv__tool".into(),
};
assert!(unknown_tool.to_string().contains("srv__tool"));
let unknown_srv = McpServerManagerError::UnknownServer {
server_name: "missing".into(),
};
assert!(unknown_srv.to_string().contains("missing"));
}
#[test]
fn error_source_returns_io_for_io_and_spawn_variants() {
let io_err = McpServerManagerError::Io(io::Error::other("x"));
assert!(std::error::Error::source(&io_err).is_some());
let spawn_err = McpServerManagerError::SpawnFailed {
server_name: "s".into(),
source: io::Error::other("y"),
};
assert!(std::error::Error::source(&spawn_err).is_some());
let rpc_err = McpServerManagerError::JsonRpc {
server_name: "s".into(),
method: "m",
error: JsonRpcError {
code: 0,
message: String::new(),
data: None,
},
};
assert!(std::error::Error::source(&rpc_err).is_none());
}
}