use serde::{Deserialize, Deserializer, Serialize};
use std::collections::HashMap;
pub const PROTOCOL_VERSION: &str = "2024-11-05";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
pub id: u64,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<serde_json::Value>,
}
impl JsonRpcRequest {
pub fn new(id: u64, method: &str, params: Option<serde_json::Value>) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id,
method: method.to_string(),
params,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcResponse {
pub jsonrpc: String,
pub id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcError {
pub code: i32,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcNotification {
pub jsonrpc: String,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<serde_json::Value>,
}
impl JsonRpcNotification {
pub fn new(method: &str, params: Option<serde_json::Value>) -> Self {
Self {
jsonrpc: "2.0".to_string(),
method: method.to_string(),
params,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ClientCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub roots: Option<RootsCapability>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sampling: Option<SamplingCapability>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RootsCapability {
#[serde(default)]
pub list_changed: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SamplingCapability {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeParams {
pub protocol_version: String,
pub capabilities: ClientCapabilities,
pub client_info: ClientInfo,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ServerCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<ToolsCapability>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resources: Option<ResourcesCapability>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompts: Option<PromptsCapability>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logging: Option<LoggingCapability>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolsCapability {
#[serde(default)]
pub list_changed: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesCapability {
#[serde(default)]
pub subscribe: bool,
#[serde(default)]
pub list_changed: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptsCapability {
#[serde(default)]
pub list_changed: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LoggingCapability {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeResult {
pub protocol_version: String,
pub capabilities: ServerCapabilities,
pub server_info: ServerInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpTool {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub input_schema: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListToolsResult {
pub tools: Vec<McpTool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallToolParams {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ToolContent {
Text {
text: String,
},
Image {
data: String,
#[serde(rename = "mimeType")]
mime_type: String,
},
Resource {
resource: ResourceContent,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceContent {
pub uri: String,
#[serde(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>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallToolResult {
pub content: Vec<ToolContent>,
#[serde(default)]
pub is_error: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpResource {
pub uri: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListResourcesResult {
pub resources: Vec<McpResource>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadResourceParams {
pub uri: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadResourceResult {
pub contents: Vec<ResourceContent>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpPrompt {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Vec<PromptArgument>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptArgument {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
pub required: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListPromptsResult {
pub prompts: Vec<McpPrompt>,
}
#[derive(Debug, Clone)]
pub enum McpNotification {
ToolsListChanged,
ResourcesListChanged,
PromptsListChanged,
Progress {
progress_token: String,
progress: f64,
total: Option<f64>,
},
Log {
level: String,
logger: Option<String>,
data: serde_json::Value,
},
Unknown {
method: String,
params: Option<serde_json::Value>,
},
}
impl McpNotification {
pub fn from_json_rpc(notification: &JsonRpcNotification) -> Self {
match notification.method.as_str() {
"notifications/tools/list_changed" => McpNotification::ToolsListChanged,
"notifications/resources/list_changed" => McpNotification::ResourcesListChanged,
"notifications/prompts/list_changed" => McpNotification::PromptsListChanged,
"notifications/progress" => {
if let Some(params) = ¬ification.params {
let progress_token = params
.get("progressToken")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let progress = params
.get("progress")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
let total = params.get("total").and_then(|v| v.as_f64());
McpNotification::Progress {
progress_token,
progress,
total,
}
} else {
McpNotification::Unknown {
method: notification.method.clone(),
params: notification.params.clone(),
}
}
}
"notifications/message" => {
if let Some(params) = ¬ification.params {
let level = params
.get("level")
.and_then(|v| v.as_str())
.unwrap_or("info")
.to_string();
let logger = params
.get("logger")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let data = params
.get("data")
.cloned()
.unwrap_or(serde_json::Value::Null);
McpNotification::Log {
level,
logger,
data,
}
} else {
McpNotification::Unknown {
method: notification.method.clone(),
params: notification.params.clone(),
}
}
}
_ => McpNotification::Unknown {
method: notification.method.clone(),
params: notification.params.clone(),
},
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct McpServerConfig {
pub name: String,
pub transport: McpTransportConfig,
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub env: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub oauth: Option<OAuthConfig>,
#[serde(default = "default_tool_timeout")]
pub tool_timeout_secs: u64,
}
impl<'de> Deserialize<'de> for McpServerConfig {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use serde::de::Error;
use serde_json::Value;
let mut map = serde_json::Map::deserialize(deserializer)?;
let transport = if let Some(t) = map.remove("transport") {
match &t {
Value::String(kind) => {
match kind.as_str() {
"stdio" => {
let command = map
.remove("command")
.and_then(|v| v.as_str().map(String::from))
.ok_or_else(|| D::Error::missing_field("command"))?;
let args = map
.remove("args")
.and_then(|v| serde_json::from_value(v).ok())
.unwrap_or_default();
McpTransportConfig::Stdio { command, args }
}
"http" => {
let url = map
.remove("url")
.and_then(|v| v.as_str().map(String::from))
.ok_or_else(|| D::Error::missing_field("url"))?;
let headers = map
.remove("headers")
.and_then(|v| serde_json::from_value(v).ok())
.unwrap_or_default();
McpTransportConfig::Http { url, headers }
}
"streamable-http" | "streamable_http" => {
let url = map
.remove("url")
.and_then(|v| v.as_str().map(String::from))
.ok_or_else(|| D::Error::missing_field("url"))?;
let headers = map
.remove("headers")
.and_then(|v| serde_json::from_value(v).ok())
.unwrap_or_default();
McpTransportConfig::StreamableHttp { url, headers }
}
other => {
return Err(D::Error::unknown_variant(
other,
&["stdio", "http", "streamable-http"],
));
}
}
}
Value::Object(_) => serde_json::from_value(t).map_err(D::Error::custom)?,
_ => return Err(D::Error::custom("transport must be a string or object")),
}
} else {
return Err(D::Error::missing_field("transport"));
};
let name = map
.remove("name")
.and_then(|v| v.as_str().map(String::from))
.ok_or_else(|| D::Error::missing_field("name"))?;
let enabled = map
.remove("enabled")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let env = map
.remove("env")
.and_then(|v| serde_json::from_value(v).ok())
.unwrap_or_default();
let oauth = map
.remove("oauth")
.and_then(|v| serde_json::from_value(v).ok());
let tool_timeout_secs = map
.remove("tool_timeout_secs")
.or_else(|| map.remove("toolTimeoutSecs"))
.and_then(|v| v.as_u64())
.unwrap_or(60);
Ok(McpServerConfig {
name,
transport,
enabled,
env,
oauth,
tool_timeout_secs,
})
}
}
#[allow(dead_code)] fn default_tool_timeout() -> u64 {
60
}
#[allow(dead_code)]
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum McpTransportConfig {
Stdio {
command: String,
#[serde(default)]
args: Vec<String>,
},
Http {
url: String,
#[serde(default)]
headers: HashMap<String, String>,
},
StreamableHttp {
url: String,
#[serde(default)]
headers: HashMap<String, String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OAuthConfig {
pub auth_url: String,
pub token_url: String,
pub client_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_secret: Option<String>,
#[serde(default)]
pub scopes: Vec<String>,
pub redirect_uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub access_token: Option<String>,
}
#[cfg(test)]
#[path = "protocol/tests.rs"]
mod tests;