pub mod version;
use crate::types::capabilities::{ClientCapabilities, ServerCapabilities};
use serde::{Deserialize, Serialize};
pub use version::*;
pub use super::content::*;
pub use super::notifications::*;
pub use super::prompts::*;
pub use super::resources::*;
pub use super::sampling::*;
pub use super::tasks::*;
pub use super::tools::*;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ProtocolVersion(pub String);
impl Default for ProtocolVersion {
fn default() -> Self {
Self(crate::DEFAULT_PROTOCOL_VERSION.to_string())
}
}
impl ProtocolVersion {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for ProtocolVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct IconInfo {
#[serde(alias = "url")]
pub src: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sizes: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub theme: Option<IconTheme>,
}
impl IconInfo {
pub fn new(src: impl Into<String>) -> Self {
Self {
src: src.into(),
mime_type: None,
sizes: None,
theme: None,
}
}
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
self.mime_type = Some(mime_type.into());
self
}
pub fn with_sizes(mut self, sizes: Vec<String>) -> Self {
self.sizes = Some(sizes);
self
}
pub fn with_theme(mut self, theme: IconTheme) -> Self {
self.theme = Some(theme);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum IconTheme {
Light,
Dark,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProtocolErrorCode {
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct Implementation {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub website_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<IconInfo>>,
}
impl Implementation {
pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
Self {
name: name.into(),
title: None,
version: version.into(),
website_url: None,
description: None,
icons: None,
}
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_website_url(mut self, url: impl Into<String>) -> Self {
self.website_url = Some(url.into());
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_icons(mut self, icons: Vec<IconInfo>) -> Self {
self.icons = Some(icons);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct InitializeRequest {
pub protocol_version: String,
pub capabilities: ClientCapabilities,
pub client_info: Implementation,
}
impl InitializeRequest {
pub fn new(client_info: Implementation, capabilities: ClientCapabilities) -> Self {
Self {
protocol_version: crate::LATEST_PROTOCOL_VERSION.to_string(),
capabilities,
client_info,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct InitializeResult {
pub protocol_version: ProtocolVersion,
pub capabilities: ServerCapabilities,
pub server_info: Implementation,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
}
impl InitializeResult {
pub fn new(server_info: Implementation, capabilities: ServerCapabilities) -> Self {
Self {
protocol_version: ProtocolVersion::default(),
capabilities,
server_info,
instructions: None,
}
}
pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
self.instructions = Some(instructions.into());
self
}
}
pub type Cursor = Option<String>;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct RequestMeta {
#[serde(skip_serializing_if = "Option::is_none")]
pub progress_token: Option<super::notifications::ProgressToken>,
#[serde(skip_serializing_if = "Option::is_none", rename = "_task_id")]
#[allow(clippy::pub_underscore_fields)]
pub _task_id: Option<String>,
}
impl RequestMeta {
pub fn new() -> Self {
Self::default()
}
pub fn with_progress_token(mut self, token: super::notifications::ProgressToken) -> Self {
self.progress_token = Some(token);
self
}
#[allow(clippy::used_underscore_binding)]
pub fn with_task_id(mut self, task_id: impl Into<String>) -> Self {
self._task_id = Some(task_id.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompleteRequest {
pub r#ref: CompletionReference,
pub argument: CompletionArgument,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum CompletionReference {
#[serde(rename = "ref/resource")]
Resource {
uri: String,
},
#[serde(rename = "ref/prompt")]
Prompt {
name: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionArgument {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct CompleteResult {
pub completion: CompletionResult,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct CompletionResult {
pub values: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total: Option<usize>,
#[serde(default)]
pub has_more: bool,
}
impl CompletionResult {
pub fn new(values: Vec<String>) -> Self {
Self {
values,
total: None,
has_more: false,
}
}
pub fn with_total(mut self, total: usize) -> Self {
self.total = Some(total);
self
}
pub fn with_has_more(mut self, has_more: bool) -> Self {
self.has_more = has_more;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
#[allow(clippy::large_enum_variant)]
pub enum ClientRequest {
#[serde(rename = "initialize")]
Initialize(InitializeRequest),
#[serde(rename = "tools/list")]
ListTools(super::tools::ListToolsRequest),
#[serde(rename = "tools/call")]
CallTool(super::tools::CallToolRequest),
#[serde(rename = "prompts/list")]
ListPrompts(super::prompts::ListPromptsRequest),
#[serde(rename = "prompts/get")]
GetPrompt(super::prompts::GetPromptRequest),
#[serde(rename = "resources/list")]
ListResources(super::resources::ListResourcesRequest),
#[serde(rename = "resources/templates/list")]
ListResourceTemplates(super::resources::ListResourceTemplatesRequest),
#[serde(rename = "resources/read")]
ReadResource(super::resources::ReadResourceRequest),
#[serde(rename = "resources/subscribe")]
Subscribe(super::resources::SubscribeRequest),
#[serde(rename = "resources/unsubscribe")]
Unsubscribe(super::resources::UnsubscribeRequest),
#[serde(rename = "completion/complete")]
Complete(CompleteRequest),
#[serde(rename = "logging/setLevel")]
SetLoggingLevel {
level: super::notifications::LoggingLevel,
},
#[serde(rename = "ping")]
Ping,
#[serde(rename = "sampling/createMessage")]
CreateMessage(Box<super::sampling::CreateMessageParams>),
#[serde(rename = "tasks/get")]
TasksGet(crate::types::tasks::GetTaskRequest),
#[serde(rename = "tasks/result")]
TasksResult(crate::types::tasks::GetTaskPayloadRequest),
#[serde(rename = "tasks/list")]
TasksList(crate::types::tasks::ListTasksRequest),
#[serde(rename = "tasks/cancel")]
TasksCancel(crate::types::tasks::CancelTaskRequest),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
pub enum ServerRequest {
#[serde(rename = "sampling/createMessage")]
CreateMessage(Box<super::sampling::CreateMessageParams>),
#[serde(rename = "roots/list")]
ListRoots,
#[serde(rename = "elicitation/create")]
ElicitationCreate(Box<crate::types::elicitation::ElicitRequestParams>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Request {
Client(Box<ClientRequest>),
Server(Box<ServerRequest>),
}
#[cfg(test)]
#[allow(clippy::used_underscore_binding)]
mod tests {
use super::*;
#[test]
fn serialize_client_request() {
let req = ClientRequest::Ping;
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["method"], "ping");
let req = ClientRequest::ListTools(super::super::tools::ListToolsRequest::default());
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["method"], "tools/list");
}
#[test]
fn test_task_client_request_variants() {
let json_str = r#"{"method": "tasks/get", "params": {"taskId": "abc"}}"#;
let req: ClientRequest = serde_json::from_str(json_str).unwrap();
assert!(matches!(req, ClientRequest::TasksGet(_)));
let json_str = r#"{"method": "tasks/result", "params": {"taskId": "abc"}}"#;
let req: ClientRequest = serde_json::from_str(json_str).unwrap();
assert!(matches!(req, ClientRequest::TasksResult(_)));
let json_str = r#"{"method": "tasks/list", "params": {}}"#;
let req: ClientRequest = serde_json::from_str(json_str).unwrap();
assert!(matches!(req, ClientRequest::TasksList(_)));
let json_str = r#"{"method": "tasks/cancel", "params": {"taskId": "abc"}}"#;
let req: ClientRequest = serde_json::from_str(json_str).unwrap();
assert!(matches!(req, ClientRequest::TasksCancel(_)));
}
#[test]
fn test_task_client_request_roundtrip() {
let req = ClientRequest::TasksGet(crate::types::tasks::GetTaskRequest {
task_id: "t-123".to_string(),
});
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["method"], "tasks/get");
assert_eq!(json["params"]["taskId"], "t-123");
let deserialized: ClientRequest = serde_json::from_value(json).unwrap();
assert!(matches!(deserialized, ClientRequest::TasksGet(_)));
}
#[test]
fn request_meta_task_id_serializes_as_underscore() {
let meta = RequestMeta::new().with_task_id("abc");
let json = serde_json::to_value(&meta).unwrap();
assert_eq!(json["_task_id"], "abc");
assert!(
json.get("_taskId").is_none(),
"_task_id must not be camelCased"
);
}
#[test]
fn request_meta_task_id_omitted_when_none() {
let meta = RequestMeta::new();
let json = serde_json::to_value(&meta).unwrap();
assert!(
json.get("_task_id").is_none(),
"_task_id should be omitted when None"
);
}
#[test]
fn request_meta_task_id_deserialization() {
let json_str = r#"{"_task_id": "task-xyz"}"#;
let meta: RequestMeta = serde_json::from_str(json_str).unwrap();
assert_eq!(meta._task_id.as_deref(), Some("task-xyz"));
assert!(meta.progress_token.is_none());
}
#[test]
fn icon_info_serializes_as_src() {
let icon = IconInfo::new("https://example.com/icon.png").with_mime_type("image/png");
let json = serde_json::to_value(&icon).unwrap();
assert_eq!(json["src"].as_str(), Some("https://example.com/icon.png"));
assert_eq!(json["mimeType"].as_str(), Some("image/png"));
assert!(
json.get("url").is_none(),
"IconInfo must not emit `url` — MCP spec requires `src`"
);
}
#[test]
fn icon_info_deserializes_src() {
let j = serde_json::json!({"src": "https://example.com/a.png"});
let icon: IconInfo = serde_json::from_value(j).unwrap();
assert_eq!(icon.src, "https://example.com/a.png");
}
#[test]
fn icon_info_deserializes_legacy_url_alias() {
let j = serde_json::json!({"url": "https://example.com/b.png"});
let icon: IconInfo = serde_json::from_value(j).unwrap();
assert_eq!(icon.src, "https://example.com/b.png");
}
#[test]
fn icon_info_round_trip_preserves_value() {
let original = IconInfo::new("https://example.com/c.png")
.with_mime_type("image/svg+xml")
.with_sizes(vec!["32x32".to_string(), "64x64".to_string()]);
let json = serde_json::to_value(&original).unwrap();
let restored: IconInfo = serde_json::from_value(json).unwrap();
assert_eq!(restored.src, original.src);
assert_eq!(restored.mime_type, original.mime_type);
assert_eq!(restored.sizes, original.sizes);
}
}