pub mod context;
pub mod error_codes;
pub mod version;
use crate::types::capabilities::{ClientCapabilities, ServerCapabilities};
use serde::{Deserialize, Serialize};
pub use version::*;
pub use context::{ProtocolContext, TraceContext};
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>,
#[serde(flatten)]
pub other: serde_json::Map<String, serde_json::Value>,
}
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
}
#[must_use]
pub fn with_meta(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.other.insert(key.into(), value);
self
}
#[must_use]
pub fn get_meta(&self, key: &str) -> Option<&serde_json::Value> {
self.other.get(key)
}
}
#[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>),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct ServerDiscoverRequest {}
impl ServerDiscoverRequest {
pub fn new() -> Self {
Self {}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct ServerDiscoverResult {
pub protocol_version: String,
pub capabilities: crate::types::ServerCapabilities,
pub server_info: Implementation,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttl_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_scope: Option<crate::types::caching::CacheScope>,
}
#[derive(Debug, Clone)]
pub(crate) enum InternalClientRequest {
ServerDiscover(ServerDiscoverRequest),
TasksUpdate {
params: serde_json::Value,
},
}
pub(crate) const SERVER_DISCOVER_METHOD: &str = "server/discover";
pub(crate) use crate::types::mrtr::TASKS_UPDATE_METHOD;
pub(crate) fn classify_internal_method(
method: &str,
params: &serde_json::Value,
) -> Option<InternalClientRequest> {
match method {
SERVER_DISCOVER_METHOD => Some(InternalClientRequest::ServerDiscover(
ServerDiscoverRequest::new(),
)),
TASKS_UPDATE_METHOD => Some(InternalClientRequest::TasksUpdate {
params: params.clone(),
}),
_ => None,
}
}
#[cfg(test)]
#[allow(clippy::used_underscore_binding)]
mod tests {
use super::*;
#[test]
fn request_meta_empty_other_serialization_unchanged() {
let meta = RequestMeta::new()
.with_progress_token(super::super::notifications::ProgressToken::String(
"tok-1".to_string(),
))
.with_task_id("task-abc");
let json = serde_json::to_value(&meta).unwrap();
let expected = serde_json::json!({
"progressToken": "tok-1",
"_task_id": "task-abc",
});
assert_eq!(json, expected, "empty `other` must emit no extra keys");
let empty = serde_json::to_value(RequestMeta::new()).unwrap();
assert_eq!(empty, serde_json::json!({}));
}
#[test]
fn request_meta_custom_key_round_trips_via_other() {
let meta = RequestMeta::new().with_meta("x-pmcp-team-depth", serde_json::json!(3));
let json = serde_json::to_value(&meta).unwrap();
assert_eq!(json["x-pmcp-team-depth"], serde_json::json!(3));
let back: RequestMeta = serde_json::from_value(json).unwrap();
assert_eq!(
back.get_meta("x-pmcp-team-depth"),
Some(&serde_json::json!(3))
);
assert!(back.progress_token.is_none());
assert!(back._task_id.is_none());
}
#[test]
fn request_meta_typed_fields_do_not_leak_into_other() {
let json = serde_json::json!({
"progressToken": "p1",
"_task_id": "t1",
"io.modelcontextprotocol/related-task": {"taskId": "abc"},
});
let meta: RequestMeta = serde_json::from_value(json).unwrap();
assert!(meta.progress_token.is_some());
assert_eq!(meta._task_id.as_deref(), Some("t1"));
assert!(!meta.other.contains_key("progressToken"));
assert!(!meta.other.contains_key("_task_id"));
assert!(meta
.other
.contains_key("io.modelcontextprotocol/related-task"));
}
fn meta_probe() -> serde_json::Value {
serde_json::json!({ "ns/key": "v" })
}
fn with_meta_key(base: &serde_json::Value, key: &str) -> serde_json::Value {
let mut out = base.clone();
out.as_object_mut()
.expect("base is an object")
.insert(key.to_string(), meta_probe());
out
}
fn assert_meta_spelling<T>(base: &serde_json::Value)
where
T: serde::Serialize + serde::de::DeserializeOwned,
{
for (label, incoming) in [
("spec spelling `_meta`", with_meta_key(base, "_meta")),
("legacy spelling `meta`", with_meta_key(base, "meta")),
] {
let typed: T = serde_json::from_value(incoming.clone())
.unwrap_or_else(|e| panic!("{label} must deserialize ({incoming}): {e}"));
let wire = serde_json::to_value(&typed).expect("serializes");
assert_eq!(
wire.get("_meta"),
Some(&meta_probe()),
"{label}: the reserved object must be READ and re-emitted under \
the spec spelling `_meta`; got {wire}"
);
assert!(
wire.get("meta").is_none(),
"{label}: the camelCase-renamed `meta` spelling must NOT be \
emitted; got {wire}"
);
}
}
fn assert_absent_meta_emits_no_key<T>(base: &serde_json::Value)
where
T: serde::Serialize + serde::de::DeserializeOwned,
{
let typed: T = serde_json::from_value(base.clone()).expect("base deserializes");
let wire = serde_json::to_value(&typed).expect("serializes");
assert!(
wire.get("_meta").is_none() && wire.get("meta").is_none(),
"an absent _meta must emit neither spelling; got {wire}"
);
}
macro_rules! for_each_meta_bearing_request {
($assertion:ident) => {
$assertion::<super::super::tools::CallToolRequest>(
&serde_json::json!({ "name": "t", "arguments": {} }),
);
$assertion::<super::super::prompts::GetPromptRequest>(
&serde_json::json!({ "name": "p", "arguments": {} }),
);
$assertion::<super::super::resources::ReadResourceRequest>(
&serde_json::json!({ "uri": "mem://x" }),
);
};
}
#[test]
fn every_meta_bearing_request_uses_the_spec_spelling_and_accepts_the_legacy_alias() {
for_each_meta_bearing_request!(assert_meta_spelling);
}
#[test]
fn absent_meta_emits_no_key_on_any_request_type() {
for_each_meta_bearing_request!(assert_absent_meta_emits_no_key);
}
#[test]
fn server_discover_request_round_trips() {
let req = ServerDiscoverRequest::new();
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json, serde_json::json!({}));
let _back: ServerDiscoverRequest = serde_json::from_value(json).unwrap();
let _back2: ServerDiscoverRequest = serde_json::from_value(serde_json::json!({})).unwrap();
}
#[test]
fn classify_internal_method_routes_server_discover() {
let out = classify_internal_method("server/discover", &serde_json::json!({}));
assert!(matches!(
out,
Some(InternalClientRequest::ServerDiscover(_))
));
assert!(classify_internal_method("tools/list", &serde_json::json!({})).is_none());
assert!(classify_internal_method("initialize", &serde_json::json!({})).is_none());
assert!(classify_internal_method("server/discovery", &serde_json::json!({})).is_none());
}
#[test]
fn classify_internal_method_routes_tasks_update_with_raw_params() {
let garbage = serde_json::json!({ "taskId": 1, "inputResponses": "not-an-object" });
match classify_internal_method("tasks/update", &garbage) {
Some(InternalClientRequest::TasksUpdate { params }) => {
assert_eq!(params, garbage, "params must pass through undecoded");
},
other => panic!("tasks/update must classify as TasksUpdate, got {other:?}"),
}
assert!(matches!(
classify_internal_method("tasks/update", &serde_json::Value::Null),
Some(InternalClientRequest::TasksUpdate { .. })
));
assert!(classify_internal_method("tasks/updates", &serde_json::json!({})).is_none());
assert!(classify_internal_method("tasks/get", &serde_json::json!({})).is_none());
assert!(classify_internal_method("tasks/cancel", &serde_json::json!({})).is_none());
}
#[test]
fn the_tasks_update_method_spelling_is_single_sourced() {
assert_eq!(TASKS_UPDATE_METHOD, "tasks/update");
assert_eq!(
TASKS_UPDATE_METHOD,
crate::types::mrtr::TASKS_UPDATE_METHOD,
"these must be the SAME item, not two constants that happen to agree"
);
}
#[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);
}
}