use std::collections::HashMap;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::error::JsonRpcError;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum MetaValidationError {
ExpectedObject,
MissingExtensionPrefix(String),
InvalidPrefix(String),
InvalidName(String),
InvalidExtensionSettings(String),
}
impl std::fmt::Display for MetaValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ExpectedObject => write!(f, "_meta must be a JSON object"),
Self::MissingExtensionPrefix(key) => {
write!(f, "extension identifier {key:?} requires a prefix")
}
Self::InvalidPrefix(key) => {
write!(f, "metadata key {key:?} has an invalid prefix")
}
Self::InvalidName(key) => write!(f, "metadata key {key:?} has an invalid name"),
Self::InvalidExtensionSettings(key) => {
write!(f, "extension {key:?} settings must be a JSON object")
}
}
}
}
impl std::error::Error for MetaValidationError {}
pub fn validate_meta_key(key: &str) -> Result<(), MetaValidationError> {
let (prefix, name) = match key.split_once('/') {
Some((prefix, name)) => (Some(prefix), name),
None => (None, key),
};
if let Some(prefix) = prefix
&& (prefix.is_empty()
|| prefix.split('.').any(|label| {
let bytes = label.as_bytes();
bytes.is_empty()
|| !bytes[0].is_ascii_alphabetic()
|| !bytes[bytes.len() - 1].is_ascii_alphanumeric()
|| !bytes
.iter()
.all(|byte| byte.is_ascii_alphanumeric() || *byte == b'-')
}))
{
return Err(MetaValidationError::InvalidPrefix(key.to_string()));
}
let bytes = name.as_bytes();
if !bytes.is_empty()
&& (!bytes[0].is_ascii_alphanumeric()
|| !bytes[bytes.len() - 1].is_ascii_alphanumeric()
|| !bytes
.iter()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(*byte, b'-' | b'_' | b'.')))
{
return Err(MetaValidationError::InvalidName(key.to_string()));
}
Ok(())
}
pub fn validate_extension_identifier(identifier: &str) -> Result<(), MetaValidationError> {
if !identifier.contains('/') {
return Err(MetaValidationError::MissingExtensionPrefix(
identifier.to_string(),
));
}
validate_meta_key(identifier)
}
pub fn validate_meta_object(meta: &Value) -> Result<(), MetaValidationError> {
let object = meta
.as_object()
.ok_or(MetaValidationError::ExpectedObject)?;
object.keys().try_for_each(|key| validate_meta_key(key))
}
pub fn validate_extensions(extensions: &HashMap<String, Value>) -> Result<(), MetaValidationError> {
for (identifier, settings) in extensions {
validate_extension_identifier(identifier)?;
if !settings.is_object() {
return Err(MetaValidationError::InvalidExtensionSettings(
identifier.clone(),
));
}
}
Ok(())
}
pub(crate) mod explicit_null_serde {
use serde::{Deserialize, Deserializer};
use serde_json::Value;
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
where
D: Deserializer<'de>,
{
Value::deserialize(deserializer).map(Some)
}
}
pub(crate) mod meta_object_serde {
use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
pub fn serialize<T, S>(meta: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
where
T: Serialize,
S: Serializer,
{
if let Some(meta) = meta {
let value = serde_json::to_value(meta).map_err(serde::ser::Error::custom)?;
super::validate_meta_object(&value).map_err(serde::ser::Error::custom)?;
}
meta.serialize(serializer)
}
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
where
T: serde::de::DeserializeOwned,
D: Deserializer<'de>,
{
let mut value = Option::<Value>::deserialize(deserializer)?;
match value.as_mut() {
Some(Value::Object(map)) => {
map.retain(|key, _| super::validate_meta_key(key).is_ok());
}
Some(other) => {
return Err(D::Error::custom(
super::validate_meta_object(other)
.expect_err("a non-object _meta must fail the object check"),
));
}
None => {}
}
let Some(value) = value else {
return Ok(None);
};
serde_json::from_value(value)
.map(Some)
.map_err(D::Error::custom)
}
}
mod extension_map_serde {
use std::collections::HashMap;
use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
pub fn serialize<S>(
extensions: &Option<HashMap<String, Value>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(extensions) = extensions {
super::validate_extensions(extensions).map_err(serde::ser::Error::custom)?;
}
extensions.serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<HashMap<String, Value>>, D::Error>
where
D: Deserializer<'de>,
{
let extensions = Option::<HashMap<String, Value>>::deserialize(deserializer)?;
if let Some(extensions) = extensions.as_ref() {
super::validate_extensions(extensions).map_err(D::Error::custom)?;
}
Ok(extensions)
}
}
pub const JSONRPC_VERSION: &str = "2.0";
pub const LATEST_PROTOCOL_VERSION: &str = "2025-11-25";
pub const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &["2025-11-25", "2025-06-18", "2025-03-26"];
pub const PROTOCOL_VERSION_2026_07_28: &str = "2026-07-28";
pub const KNOWN_PROTOCOL_VERSIONS: &[&str] = &[
PROTOCOL_VERSION_2026_07_28,
"2025-11-25",
"2025-06-18",
"2025-03-26",
];
#[deprecated(
since = "0.15.0",
note = "the 2026-07-28 implementation is released and opt-in; use PROTOCOL_VERSION_2026_07_28"
)]
pub const EXPERIMENTAL_PROTOCOL_VERSION: &str = PROTOCOL_VERSION_2026_07_28;
#[deprecated(
since = "0.15.0",
note = "the 2026-07-28 spec has shipped; use PROTOCOL_VERSION_2026_07_28"
)]
pub const UPCOMING_PROTOCOL_VERSION: &str = PROTOCOL_VERSION_2026_07_28;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
pub id: RequestId,
pub method: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
}
impl JsonRpcRequest {
pub fn new(id: impl Into<RequestId>, method: impl Into<String>) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
id: id.into(),
method: method.into(),
params: None,
}
}
pub fn with_params(mut self, params: Value) -> Self {
self.params = Some(params);
self
}
pub fn validate(&self) -> Result<(), JsonRpcError> {
if self.jsonrpc != JSONRPC_VERSION {
return Err(JsonRpcError::invalid_request(format!(
"Invalid JSON-RPC version: expected '{}', got '{}'",
JSONRPC_VERSION, self.jsonrpc
)));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcResultResponse {
pub jsonrpc: String,
pub id: RequestId,
pub result: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcErrorResponse {
pub jsonrpc: String,
#[serde(default)]
pub id: Option<RequestId>,
pub error: JsonRpcError,
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum JsonRpcResponse {
Result(JsonRpcResultResponse),
Error(JsonRpcErrorResponse),
}
impl<'de> Deserialize<'de> for JsonRpcResponse {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
let has_error = value.get("error").is_some_and(|error| !error.is_null());
let has_result_value = value.get("result").is_some_and(|result| !result.is_null());
if has_error && has_result_value {
return Err(serde::de::Error::custom(
"JSON-RPC response has both result and error",
));
}
if has_error {
return serde_json::from_value(value)
.map(JsonRpcResponse::Error)
.map_err(serde::de::Error::custom);
}
if value.get("result").is_some() {
return serde_json::from_value(value)
.map(JsonRpcResponse::Result)
.map_err(serde::de::Error::custom);
}
Err(serde::de::Error::custom(
"JSON-RPC response has neither result nor error",
))
}
}
impl JsonRpcResponse {
pub fn result(id: RequestId, result: Value) -> Self {
Self::Result(JsonRpcResultResponse {
jsonrpc: JSONRPC_VERSION.to_string(),
id,
result,
})
}
pub fn error(id: Option<RequestId>, error: JsonRpcError) -> Self {
Self::Error(JsonRpcErrorResponse {
jsonrpc: JSONRPC_VERSION.to_string(),
id,
error,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum JsonRpcMessage {
Single(JsonRpcRequest),
Batch(Vec<JsonRpcRequest>),
}
impl JsonRpcMessage {
pub fn is_batch(&self) -> bool {
matches!(self, JsonRpcMessage::Batch(_))
}
pub fn len(&self) -> usize {
match self {
JsonRpcMessage::Single(_) => 1,
JsonRpcMessage::Batch(batch) => batch.len(),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum JsonRpcResponseMessage {
Single(JsonRpcResponse),
Batch(Vec<JsonRpcResponse>),
}
impl JsonRpcResponseMessage {
pub fn is_batch(&self) -> bool {
matches!(self, JsonRpcResponseMessage::Batch(_))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcNotification {
pub jsonrpc: String,
pub method: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
}
impl JsonRpcNotification {
pub fn new(method: impl Into<String>) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
method: method.into(),
params: None,
}
}
pub fn with_params(mut self, params: Value) -> Self {
self.params = Some(params);
self
}
}
pub mod notifications {
pub const INITIALIZED: &str = "notifications/initialized";
pub const CANCELLED: &str = "notifications/cancelled";
pub const SUBSCRIPTIONS_ACKNOWLEDGED: &str = "notifications/subscriptions/acknowledged";
pub const PROGRESS: &str = "notifications/progress";
pub const TOOLS_LIST_CHANGED: &str = "notifications/tools/list_changed";
pub const RESOURCES_LIST_CHANGED: &str = "notifications/resources/list_changed";
pub const RESOURCE_UPDATED: &str = "notifications/resources/updated";
pub const PROMPTS_LIST_CHANGED: &str = "notifications/prompts/list_changed";
pub const ROOTS_LIST_CHANGED: &str = "notifications/roots/list_changed";
pub const MESSAGE: &str = "notifications/message";
pub const TASK_STATUS_CHANGED: &str = "notifications/tasks";
pub const ELICITATION_COMPLETE: &str = "notifications/elicitation/complete";
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum LogLevel {
Emergency,
Alert,
Critical,
Error,
Warning,
Notice,
#[default]
Info,
Debug,
}
impl std::fmt::Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LogLevel::Emergency => write!(f, "emergency"),
LogLevel::Alert => write!(f, "alert"),
LogLevel::Critical => write!(f, "critical"),
LogLevel::Error => write!(f, "error"),
LogLevel::Warning => write!(f, "warning"),
LogLevel::Notice => write!(f, "notice"),
LogLevel::Info => write!(f, "info"),
LogLevel::Debug => write!(f, "debug"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingMessageParams {
pub level: LogLevel,
#[serde(skip_serializing_if = "Option::is_none")]
pub logger: Option<String>,
#[serde(default)]
pub data: Value,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
impl LoggingMessageParams {
pub fn new(level: LogLevel, data: impl Into<Value>) -> Self {
Self {
level,
logger: None,
data: data.into(),
meta: None,
}
}
pub fn with_logger(mut self, logger: impl Into<String>) -> Self {
self.logger = Some(logger.into());
self
}
pub fn with_data(mut self, data: impl Into<Value>) -> Self {
self.data = data.into();
self
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct SetLogLevelParams {
pub level: LogLevel,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum RequestId {
String(String),
Number(i64),
}
impl RequestId {
pub fn from_json(value: &Value) -> Option<Self> {
match value {
Value::String(s) => Some(RequestId::String(s.clone())),
Value::Number(n) => n
.as_i64()
.or_else(|| n.as_f64().and_then(integral_float_id))
.map(RequestId::Number),
_ => None,
}
}
}
fn integral_float_id(f: f64) -> Option<i64> {
const MAX_EXACT: f64 = 9_007_199_254_740_992.0;
(f.is_finite() && f.fract() == 0.0 && f.abs() <= MAX_EXACT).then_some(f as i64)
}
impl<'de> Deserialize<'de> for RequestId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct IdVisitor;
impl serde::de::Visitor<'_> for IdVisitor {
type Value = RequestId;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a string or integer request id")
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<RequestId, E> {
Ok(RequestId::String(v.to_owned()))
}
fn visit_string<E: serde::de::Error>(self, v: String) -> Result<RequestId, E> {
Ok(RequestId::String(v))
}
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<RequestId, E> {
Ok(RequestId::Number(v))
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<RequestId, E> {
i64::try_from(v)
.map(RequestId::Number)
.map_err(|_| E::custom("request id is out of range for a signed integer"))
}
fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<RequestId, E> {
integral_float_id(v)
.map(RequestId::Number)
.ok_or_else(|| E::custom("request id must be a string or an integer"))
}
}
deserializer.deserialize_any(IdVisitor)
}
}
impl From<String> for RequestId {
fn from(s: String) -> Self {
RequestId::String(s)
}
}
impl From<&str> for RequestId {
fn from(s: &str) -> Self {
RequestId::String(s.to_string())
}
}
impl From<i64> for RequestId {
fn from(n: i64) -> Self {
RequestId::Number(n)
}
}
impl From<i32> for RequestId {
fn from(n: i32) -> Self {
RequestId::Number(n as i64)
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum McpRequest {
Initialize(InitializeParams),
ListTools(ListToolsParams),
CallTool(CallToolParams),
ListResources(ListResourcesParams),
ListResourceTemplates(ListResourceTemplatesParams),
ReadResource(ReadResourceParams),
SubscribeResource(SubscribeResourceParams),
UnsubscribeResource(UnsubscribeResourceParams),
ListPrompts(ListPromptsParams),
GetPrompt(GetPromptParams),
GetTaskInfo(GetTaskInfoParams),
UpdateTask(UpdateTaskParams),
CancelTask(CancelTaskParams),
Ping,
SetLoggingLevel(SetLogLevelParams),
Complete(CompleteParams),
Discover(DiscoverParams),
SubscriptionsListen(SubscriptionsListenParams),
Unknown {
method: String,
params: Option<Value>,
},
}
impl McpRequest {
pub fn method_name(&self) -> &str {
match self {
McpRequest::Initialize(_) => "initialize",
McpRequest::ListTools(_) => "tools/list",
McpRequest::CallTool(_) => "tools/call",
McpRequest::ListResources(_) => "resources/list",
McpRequest::ListResourceTemplates(_) => "resources/templates/list",
McpRequest::ReadResource(_) => "resources/read",
McpRequest::SubscribeResource(_) => "resources/subscribe",
McpRequest::UnsubscribeResource(_) => "resources/unsubscribe",
McpRequest::ListPrompts(_) => "prompts/list",
McpRequest::GetPrompt(_) => "prompts/get",
McpRequest::GetTaskInfo(_) => "tasks/get",
McpRequest::UpdateTask(_) => "tasks/update",
McpRequest::CancelTask(_) => "tasks/cancel",
McpRequest::Ping => "ping",
McpRequest::SetLoggingLevel(_) => "logging/setLevel",
McpRequest::Complete(_) => "completion/complete",
McpRequest::Discover(_) => "server/discover",
McpRequest::SubscriptionsListen(_) => "subscriptions/listen",
McpRequest::Unknown { method, .. } => method,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum McpNotification {
Initialized,
Cancelled(CancelledParams),
Progress(ProgressParams),
RootsListChanged,
Unknown {
method: String,
params: Option<Value>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CancelledParams {
#[serde(default)]
pub request_id: Option<RequestId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProgressParams {
pub progress_token: ProgressToken,
pub progress: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub total: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum ProgressToken {
String(String),
Number(i64),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RequestMeta {
#[serde(skip_serializing_if = "Option::is_none")]
pub progress_token: Option<ProgressToken>,
#[serde(
rename = "io.modelcontextprotocol/protocolVersion",
skip_serializing_if = "Option::is_none"
)]
pub protocol_version: Option<String>,
#[serde(
rename = "io.modelcontextprotocol/clientInfo",
skip_serializing_if = "Option::is_none"
)]
pub client_info: Option<Implementation>,
#[serde(
rename = "io.modelcontextprotocol/clientCapabilities",
skip_serializing_if = "Option::is_none"
)]
pub client_capabilities: Option<ClientCapabilities>,
#[serde(
rename = "io.modelcontextprotocol/logLevel",
skip_serializing_if = "Option::is_none"
)]
pub log_level: Option<LogLevel>,
}
impl RequestMeta {
pub fn validate_for_version(&self, protocol_version: &str) -> Result<(), MissingMetaKey> {
if protocol_version == PROTOCOL_VERSION_2026_07_28 {
if self.protocol_version.is_none() {
return Err(MissingMetaKey("io.modelcontextprotocol/protocolVersion"));
}
if self.client_capabilities.is_none() {
return Err(MissingMetaKey("io.modelcontextprotocol/clientCapabilities"));
}
}
Ok(())
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum McpResponse {
Initialize(InitializeResult),
ListTools(ListToolsResult),
CallTool(CallToolResult),
InputRequired(InputRequiredResult),
ListResources(ListResourcesResult),
ListResourceTemplates(ListResourceTemplatesResult),
ReadResource(ReadResourceResult),
SubscribeResource(EmptyResult),
UnsubscribeResource(EmptyResult),
ListPrompts(ListPromptsResult),
GetPrompt(GetPromptResult),
FinalCreateTask(crate::tasks::CreateTaskResult),
FinalGetTask(crate::tasks::GetTaskResult),
FinalTaskAck(crate::tasks::TaskAcknowledgement),
CreateTask(CreateTaskResult),
GetTaskInfo(TaskObject),
UpdateTask(EmptyResult),
CancelTask(EmptyResult),
SetLoggingLevel(EmptyResult),
Complete(CompleteResult),
Pong(EmptyResult),
Discover(DiscoverResult),
SubscriptionsAccepted(SubscriptionsAcceptedResult),
SubscriptionsListen(SubscriptionsListenResult),
Empty(EmptyResult),
Raw(Value),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeParams {
pub protocol_version: String,
pub capabilities: ClientCapabilities,
pub client_info: Implementation,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ClientCapabilities {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub roots: Option<RootsCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sampling: Option<SamplingCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub elicitation: Option<ElicitationCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tasks: Option<ClientTasksCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub experimental: Option<HashMap<String, serde_json::Value>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "extension_map_serde"
)]
pub extensions: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ElicitationCapability {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub form: Option<ElicitationFormCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<ElicitationUrlCapability>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ElicitationFormCapability {}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ElicitationUrlCapability {}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientTasksCapability {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub list: Option<ClientTasksListCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cancel: Option<ClientTasksCancelCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub requests: Option<ClientTasksRequestsCapability>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ClientTasksListCapability {}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ClientTasksCancelCapability {}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientTasksRequestsCapability {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sampling: Option<ClientTasksSamplingCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub elicitation: Option<ClientTasksElicitationCapability>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientTasksSamplingCapability {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub create_message: Option<ClientTasksSamplingCreateMessageCapability>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ClientTasksSamplingCreateMessageCapability {}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientTasksElicitationCapability {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub create: Option<ClientTasksElicitationCreateCapability>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ClientTasksElicitationCreateCapability {}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RootsCapability {
#[serde(default)]
pub list_changed: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deprecated: Option<DeprecationInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub uri: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
impl Root {
pub fn new(uri: impl Into<String>) -> Self {
Self {
uri: uri.into(),
name: None,
meta: None,
}
}
pub fn with_name(uri: impl Into<String>, name: impl Into<String>) -> Self {
Self {
uri: uri.into(),
name: Some(name.into()),
meta: None,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ListRootsParams {
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListRootsResult {
pub roots: Vec<Root>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SamplingCapability {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tools: Option<SamplingToolsCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context: Option<SamplingContextCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deprecated: Option<DeprecationInfo>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SamplingToolsCapability {}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SamplingContextCapability {}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CompletionsCapability {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptReference {
#[serde(rename = "type")]
pub ref_type: String,
pub name: String,
}
impl PromptReference {
pub fn new(name: impl Into<String>) -> Self {
Self {
ref_type: "ref/prompt".to_string(),
name: name.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceReference {
#[serde(rename = "type")]
pub ref_type: String,
pub uri: String,
}
impl ResourceReference {
pub fn new(uri: impl Into<String>) -> Self {
Self {
ref_type: "ref/resource".to_string(),
uri: uri.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
#[non_exhaustive]
pub enum CompletionReference {
#[serde(rename = "ref/prompt")]
Prompt {
name: String,
},
#[serde(rename = "ref/resource")]
Resource {
uri: String,
},
}
impl CompletionReference {
pub fn prompt(name: impl Into<String>) -> Self {
Self::Prompt { name: name.into() }
}
pub fn resource(uri: impl Into<String>) -> Self {
Self::Resource { uri: uri.into() }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompletionArgument {
pub name: String,
pub value: String,
}
impl CompletionArgument {
pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
Self {
name: name.into(),
value: value.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompleteParams {
#[serde(rename = "ref")]
pub reference: CompletionReference,
pub argument: CompletionArgument,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context: Option<CompletionContext>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionContext {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub arguments: Option<std::collections::HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Completion {
pub values: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub total: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub has_more: Option<bool>,
}
impl Completion {
pub fn new(values: Vec<String>) -> Self {
Self {
values,
total: None,
has_more: None,
}
}
pub fn with_pagination(values: Vec<String>, total: u32, has_more: bool) -> Self {
Self {
values,
total: Some(total),
has_more: Some(has_more),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompleteResult {
pub completion: Completion,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
impl CompleteResult {
pub fn new(values: Vec<String>) -> Self {
Self {
completion: Completion::new(values),
meta: None,
}
}
pub fn with_pagination(values: Vec<String>, total: u32, has_more: bool) -> Self {
Self {
completion: Completion::with_pagination(values, total, has_more),
meta: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelHint {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl ModelHint {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: Some(name.into()),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelPreferences {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub speed_priority: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub intelligence_priority: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost_priority: Option<f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hints: Vec<ModelHint>,
}
impl ModelPreferences {
pub fn new() -> Self {
Self::default()
}
pub fn speed(mut self, priority: f64) -> Self {
self.speed_priority = Some(priority.clamp(0.0, 1.0));
self
}
pub fn intelligence(mut self, priority: f64) -> Self {
self.intelligence_priority = Some(priority.clamp(0.0, 1.0));
self
}
pub fn cost(mut self, priority: f64) -> Self {
self.cost_priority = Some(priority.clamp(0.0, 1.0));
self
}
pub fn hint(mut self, name: impl Into<String>) -> Self {
self.hints.push(ModelHint::new(name));
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub enum IncludeContext {
#[deprecated(
since = "0.23.0",
note = "Deprecated in MCP 2025-11-25 by SEP-2596. Omit the field or use \
`IncludeContext::None`. Removal follows Sampling (SEP-2577), \
whose earliest removal is the first revision released on or \
after 2027-07-28."
)]
AllServers,
#[deprecated(
since = "0.23.0",
note = "Deprecated in MCP 2025-11-25 by SEP-2596. Omit the field or use \
`IncludeContext::None`. Removal follows Sampling (SEP-2577), \
whose earliest removal is the first revision released on or \
after 2027-07-28."
)]
ThisServer,
#[default]
None,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SamplingMessage {
pub role: ContentRole,
pub content: SamplingContentOrArray,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
impl SamplingMessage {
pub fn user(text: impl Into<String>) -> Self {
Self {
role: ContentRole::User,
content: SamplingContentOrArray::Single(SamplingContent::Text {
text: text.into(),
annotations: None,
meta: None,
}),
meta: None,
}
}
pub fn assistant(text: impl Into<String>) -> Self {
Self {
role: ContentRole::Assistant,
content: SamplingContentOrArray::Single(SamplingContent::Text {
text: text.into(),
annotations: None,
meta: None,
}),
meta: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SamplingTool {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub input_schema: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_schema: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<ToolIcon>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<ToolAnnotations>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub execution: Option<ToolExecution>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolChoice {
pub mode: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl ToolChoice {
pub fn auto() -> Self {
Self {
mode: "auto".to_string(),
name: None,
}
}
pub fn required() -> Self {
Self {
mode: "required".to_string(),
name: None,
}
}
pub fn none() -> Self {
Self {
mode: "none".to_string(),
name: None,
}
}
pub fn tool(name: impl Into<String>) -> Self {
Self {
mode: "tool".to_string(),
name: Some(name.into()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
#[non_exhaustive]
pub enum SamplingContent {
Text {
text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
annotations: Option<ContentAnnotations>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
meta: Option<Value>,
},
Image {
data: String,
#[serde(rename = "mimeType")]
mime_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
annotations: Option<ContentAnnotations>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
meta: Option<Value>,
},
Audio {
data: String,
#[serde(rename = "mimeType")]
mime_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
annotations: Option<ContentAnnotations>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
meta: Option<Value>,
},
#[serde(rename = "tool_use")]
ToolUse {
id: String,
name: String,
input: Value,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
meta: Option<Value>,
},
#[serde(rename = "tool_result")]
ToolResult {
#[serde(rename = "toolUseId")]
tool_use_id: String,
content: Vec<SamplingContent>,
#[serde(
default,
rename = "structuredContent",
skip_serializing_if = "Option::is_none"
)]
structured_content: Option<Value>,
#[serde(default, rename = "isError", skip_serializing_if = "Option::is_none")]
is_error: Option<bool>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
meta: Option<Value>,
},
}
impl SamplingContent {
pub fn as_text(&self) -> Option<&str> {
match self {
SamplingContent::Text { text, .. } => Some(text),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum SamplingContentOrArray {
Single(SamplingContent),
Array(Vec<SamplingContent>),
}
impl SamplingContentOrArray {
pub fn items(&self) -> Vec<&SamplingContent> {
match self {
Self::Single(c) => vec![c],
Self::Array(arr) => arr.iter().collect(),
}
}
pub fn into_items(self) -> Vec<SamplingContent> {
match self {
Self::Single(c) => vec![c],
Self::Array(arr) => arr,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateMessageParams {
pub messages: Vec<SamplingMessage>,
pub max_tokens: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system_prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub temperature: Option<f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub stop_sequences: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_preferences: Option<ModelPreferences>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub include_context: Option<IncludeContext>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Map<String, Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<SamplingTool>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ToolChoice>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub task: Option<TaskRequestParams>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
impl CreateMessageParams {
pub fn new(messages: Vec<SamplingMessage>, max_tokens: u32) -> Self {
Self {
messages,
max_tokens,
system_prompt: None,
temperature: None,
stop_sequences: Vec::new(),
model_preferences: None,
include_context: None,
metadata: None,
tools: None,
tool_choice: None,
task: None,
meta: None,
}
}
pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.system_prompt = Some(prompt.into());
self
}
pub fn temperature(mut self, temp: f64) -> Self {
self.temperature = Some(temp.clamp(0.0, 1.0));
self
}
pub fn stop_sequence(mut self, seq: impl Into<String>) -> Self {
self.stop_sequences.push(seq.into());
self
}
pub fn model_preferences(mut self, prefs: ModelPreferences) -> Self {
self.model_preferences = Some(prefs);
self
}
pub fn include_context(mut self, mode: IncludeContext) -> Self {
self.include_context = Some(mode);
self
}
pub fn tools(mut self, tools: Vec<SamplingTool>) -> Self {
self.tools = Some(tools);
self
}
pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
self.tool_choice = Some(choice);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateMessageResult {
pub content: SamplingContentOrArray,
pub model: String,
pub role: ContentRole,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<String>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
impl CreateMessageResult {
pub fn content_items(&self) -> Vec<&SamplingContent> {
self.content.items()
}
pub fn first_text(&self) -> Option<&str> {
self.content.items().iter().find_map(|c| c.as_text())
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Implementation {
pub name: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<ToolIcon>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub website_url: Option<String>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeResult {
pub protocol_version: String,
pub capabilities: ServerCapabilities,
pub server_info: Implementation,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DiscoverParams {
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoverResult {
pub supported_versions: Vec<String>,
pub capabilities: ServerCapabilities,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_lenient_ttl_ms"
)]
pub ttl_ms: Option<u64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_lenient_cache_scope"
)]
pub cache_scope: Option<CacheScope>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<ResultMeta>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SubscriptionsListenParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub notifications: Option<SubscriptionFilter>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubscriptionsAcceptedResult {
pub notifications: SubscriptionFilter,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubscriptionsListenResult {
#[serde(default)]
pub result_type: ResultType,
#[serde(rename = "_meta")]
pub meta: SubscriptionsListenResultMeta,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubscriptionsListenResultMeta {
#[serde(rename = "io.modelcontextprotocol/subscriptionId")]
pub subscription_id: RequestId,
#[serde(
rename = "io.modelcontextprotocol/serverInfo",
default,
skip_serializing_if = "Option::is_none"
)]
pub server_info: Option<Implementation>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ServerCapabilities {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tools: Option<ToolsCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resources: Option<ResourcesCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompts: Option<PromptsCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logging: Option<LoggingCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tasks: Option<TasksCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub completions: Option<CompletionsCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub experimental: Option<HashMap<String, serde_json::Value>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "extension_map_serde"
)]
pub extensions: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LoggingCapability {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deprecated: Option<DeprecationInfo>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TasksCapability {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub list: Option<TasksListCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cancel: Option<TasksCancelCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub requests: Option<TasksRequestsCapability>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TasksListCapability {}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TasksCancelCapability {}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TasksRequestsCapability {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tools: Option<TasksToolsRequestsCapability>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TasksToolsRequestsCapability {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub call: Option<TasksToolsCallCapability>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TasksToolsCallCapability {}
#[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, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum CacheScope {
Public,
Private,
}
fn deserialize_lenient_cache_scope<'de, D>(deserializer: D) -> Result<Option<CacheScope>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Option::<Value>::deserialize(deserializer)?;
Ok(value.and_then(|value| serde_json::from_value::<CacheScope>(value).ok()))
}
fn deserialize_lenient_ttl_ms<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Option::<Value>::deserialize(deserializer)?;
Ok(value.and_then(|value| serde_json::from_value::<u64>(value).ok()))
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeprecationInfo {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub since: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remove_in: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub replacement: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ListToolsParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListToolsResult {
pub tools: Vec<ToolDefinition>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_lenient_ttl_ms"
)]
pub ttl_ms: Option<u64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_lenient_cache_scope"
)]
pub cache_scope: Option<CacheScope>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolDefinition {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub input_schema: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_schema: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<ToolIcon>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<ToolAnnotations>,
#[serde(skip_serializing_if = "Option::is_none")]
pub execution: Option<ToolExecution>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum IconTheme {
Light,
Dark,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolIcon {
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>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolAnnotations {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub read_only_hint: bool,
#[serde(default = "default_true", skip_serializing_if = "is_true")]
pub destructive_hint: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub idempotent_hint: bool,
#[serde(default = "default_true", skip_serializing_if = "is_true")]
pub open_world_hint: bool,
}
impl Default for ToolAnnotations {
fn default() -> Self {
Self {
title: None,
read_only_hint: false,
destructive_hint: true,
idempotent_hint: false,
open_world_hint: true,
}
}
}
impl ToolAnnotations {
pub fn is_read_only(&self) -> bool {
self.read_only_hint
}
pub fn is_destructive(&self) -> bool {
self.destructive_hint
}
pub fn is_idempotent(&self) -> bool {
self.idempotent_hint
}
pub fn is_open_world(&self) -> bool {
self.open_world_hint
}
}
impl ToolDefinition {
pub fn is_read_only(&self) -> bool {
self.annotations.as_ref().is_some_and(|a| a.read_only_hint)
}
pub fn is_destructive(&self) -> bool {
self.annotations.as_ref().is_none_or(|a| a.destructive_hint)
}
pub fn is_idempotent(&self) -> bool {
self.annotations.as_ref().is_some_and(|a| a.idempotent_hint)
}
pub fn is_open_world(&self) -> bool {
self.annotations.as_ref().is_none_or(|a| a.open_world_hint)
}
}
fn default_true() -> bool {
true
}
fn is_true(v: &bool) -> bool {
*v
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallToolParams {
pub name: String,
#[serde(default)]
pub arguments: Value,
#[serde(
rename = "inputResponses",
default,
skip_serializing_if = "Option::is_none"
)]
pub input_responses: Option<InputResponses>,
#[serde(
rename = "requestState",
default,
skip_serializing_if = "Option::is_none"
)]
pub request_state: Option<String>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<RequestMeta>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub task: Option<TaskRequestParams>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallToolResult {
pub content: Vec<Content>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub is_error: bool,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::protocol::explicit_null_serde::deserialize"
)]
pub structured_content: Option<Value>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
impl CallToolResult {
pub fn text(text: impl Into<String>) -> Self {
Self {
content: vec![Content::Text {
text: text.into(),
annotations: None,
meta: None,
}],
is_error: false,
structured_content: None,
meta: None,
}
}
pub fn error(message: impl Into<String>) -> Self {
Self {
content: vec![Content::Text {
text: message.into(),
annotations: None,
meta: None,
}],
is_error: true,
structured_content: None,
meta: None,
}
}
pub fn json(value: Value) -> Self {
let text = serde_json::to_string_pretty(&value).unwrap_or_default();
Self {
content: vec![Content::Text {
text,
annotations: None,
meta: None,
}],
is_error: false,
structured_content: Some(value),
meta: None,
}
}
pub fn from_serialize(
value: &impl serde::Serialize,
) -> std::result::Result<Self, crate::error::Error> {
let json_value = serde_json::to_value(value)
.map_err(|e| crate::error::Error::tool(format!("Serialization failed: {}", e)))?;
Ok(Self::json(json_value))
}
pub fn from_list<T: serde::Serialize>(
key: &str,
items: &[T],
) -> std::result::Result<Self, crate::error::Error> {
Self::from_serialize(&serde_json::json!({ key: items, "count": items.len() }))
}
pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
Self {
content: vec![Content::Image {
data: data.into(),
mime_type: mime_type.into(),
annotations: None,
meta: None,
}],
is_error: false,
structured_content: None,
meta: None,
}
}
pub fn audio(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
Self {
content: vec![Content::Audio {
data: data.into(),
mime_type: mime_type.into(),
annotations: None,
meta: None,
}],
is_error: false,
structured_content: None,
meta: None,
}
}
pub fn resource_link(uri: impl Into<String>, name: impl Into<String>) -> Self {
Self {
content: vec![Content::ResourceLink {
uri: uri.into(),
name: name.into(),
title: None,
description: None,
mime_type: None,
size: None,
icons: None,
annotations: None,
meta: None,
}],
is_error: false,
structured_content: None,
meta: None,
}
}
pub fn resource_link_with_meta(
uri: impl Into<String>,
name: impl Into<String>,
description: Option<String>,
mime_type: Option<String>,
) -> Self {
Self {
content: vec![Content::ResourceLink {
uri: uri.into(),
name: name.into(),
title: None,
description,
mime_type,
size: None,
icons: None,
annotations: None,
meta: None,
}],
is_error: false,
structured_content: None,
meta: None,
}
}
pub fn resource(resource: ResourceContent) -> Self {
Self {
content: vec![Content::Resource {
resource,
annotations: None,
meta: None,
}],
is_error: false,
structured_content: None,
meta: None,
}
}
pub fn all_text(&self) -> String {
self.content.iter().filter_map(|c| c.as_text()).collect()
}
pub fn first_text(&self) -> Option<&str> {
self.content.iter().find_map(|c| c.as_text())
}
pub fn as_json(&self) -> Option<Result<Value, serde_json::Error>> {
if let Some(ref sc) = self.structured_content {
return Some(Ok(sc.clone()));
}
self.first_text().map(serde_json::from_str)
}
pub fn deserialize<T: DeserializeOwned>(&self) -> Option<Result<T, serde_json::Error>> {
if let Some(ref sc) = self.structured_content {
return Some(serde_json::from_value(sc.clone()));
}
self.first_text().map(serde_json::from_str)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum Content {
Text {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<ContentAnnotations>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
meta: Option<Value>,
},
Image {
data: String,
#[serde(rename = "mimeType")]
mime_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<ContentAnnotations>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
meta: Option<Value>,
},
Audio {
data: String,
#[serde(rename = "mimeType")]
mime_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<ContentAnnotations>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
meta: Option<Value>,
},
Resource {
resource: ResourceContent,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<ContentAnnotations>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
meta: Option<Value>,
},
ResourceLink {
uri: String,
name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
mime_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
size: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
icons: Option<Vec<ToolIcon>>,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<ContentAnnotations>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
meta: Option<Value>,
},
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ContentAnnotations {
#[serde(skip_serializing_if = "Option::is_none")]
pub audience: Option<Vec<ContentRole>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<f64>,
#[serde(rename = "lastModified", skip_serializing_if = "Option::is_none")]
pub last_modified: Option<String>,
}
impl Content {
pub fn text(text: impl Into<String>) -> Self {
Content::Text {
text: text.into(),
annotations: None,
meta: None,
}
}
pub fn as_text(&self) -> Option<&str> {
match self {
Content::Text { text, .. } => Some(text),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum ContentRole {
User,
Assistant,
}
#[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>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ListResourcesParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListResourcesResult {
pub resources: Vec<ResourceDefinition>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_lenient_ttl_ms"
)]
pub ttl_ms: Option<u64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_lenient_cache_scope"
)]
pub cache_scope: Option<CacheScope>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceDefinition {
pub uri: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<ToolIcon>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<ContentAnnotations>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadResourceParams {
pub uri: String,
#[serde(
rename = "inputResponses",
default,
skip_serializing_if = "Option::is_none"
)]
pub input_responses: Option<InputResponses>,
#[serde(
rename = "requestState",
default,
skip_serializing_if = "Option::is_none"
)]
pub request_state: Option<String>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ReadResourceResult {
pub contents: Vec<ResourceContent>,
#[serde(
rename = "ttlMs",
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_lenient_ttl_ms"
)]
pub ttl_ms: Option<u64>,
#[serde(
rename = "cacheScope",
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_lenient_cache_scope"
)]
pub cache_scope: Option<CacheScope>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
impl ReadResourceResult {
pub fn with_ttl_ms(mut self, ttl_ms: u64) -> Self {
self.ttl_ms = Some(ttl_ms);
self
}
pub fn with_cache_scope(mut self, scope: CacheScope) -> Self {
self.cache_scope = Some(scope);
self
}
pub fn text(uri: impl Into<String>, content: impl Into<String>) -> Self {
Self {
contents: vec![ResourceContent {
uri: uri.into(),
mime_type: Some("text/plain".to_string()),
text: Some(content.into()),
blob: None,
meta: None,
}],
meta: None,
..Default::default()
}
}
pub fn text_with_mime(
uri: impl Into<String>,
content: impl Into<String>,
mime_type: impl Into<String>,
) -> Self {
Self {
contents: vec![ResourceContent {
uri: uri.into(),
mime_type: Some(mime_type.into()),
text: Some(content.into()),
blob: None,
meta: None,
}],
meta: None,
..Default::default()
}
}
pub fn json<T: serde::Serialize>(uri: impl Into<String>, value: &T) -> Self {
let json_string =
serde_json::to_string_pretty(value).unwrap_or_else(|_| "null".to_string());
Self {
contents: vec![ResourceContent {
uri: uri.into(),
mime_type: Some("application/json".to_string()),
text: Some(json_string),
blob: None,
meta: None,
}],
meta: None,
..Default::default()
}
}
pub fn blob(uri: impl Into<String>, bytes: &[u8]) -> Self {
use base64::Engine;
let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
Self {
contents: vec![ResourceContent {
uri: uri.into(),
mime_type: Some("application/octet-stream".to_string()),
text: None,
blob: Some(encoded),
meta: None,
}],
meta: None,
..Default::default()
}
}
pub fn blob_with_mime(
uri: impl Into<String>,
bytes: &[u8],
mime_type: impl Into<String>,
) -> Self {
use base64::Engine;
let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
Self {
contents: vec![ResourceContent {
uri: uri.into(),
mime_type: Some(mime_type.into()),
text: None,
blob: Some(encoded),
meta: None,
}],
meta: None,
..Default::default()
}
}
pub fn first_text(&self) -> Option<&str> {
self.contents.first().and_then(|c| c.text.as_deref())
}
pub fn first_uri(&self) -> Option<&str> {
self.contents.first().map(|c| c.uri.as_str())
}
pub fn as_json(&self) -> Option<Result<Value, serde_json::Error>> {
self.first_text().map(serde_json::from_str)
}
pub fn deserialize<T: DeserializeOwned>(&self) -> Option<Result<T, serde_json::Error>> {
self.first_text().map(serde_json::from_str)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct SubscribeResourceParams {
pub uri: String,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct UnsubscribeResourceParams {
pub uri: String,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ListResourceTemplatesParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListResourceTemplatesResult {
pub resource_templates: Vec<ResourceTemplateDefinition>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_lenient_ttl_ms"
)]
pub ttl_ms: Option<u64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_lenient_cache_scope"
)]
pub cache_scope: Option<CacheScope>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceTemplateDefinition {
pub uri_template: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<ToolIcon>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<ContentAnnotations>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub arguments: Vec<PromptArgument>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ListPromptsParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListPromptsResult {
pub prompts: Vec<PromptDefinition>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_lenient_ttl_ms"
)]
pub ttl_ms: Option<u64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_lenient_cache_scope"
)]
pub cache_scope: Option<CacheScope>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptDefinition {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<ToolIcon>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub arguments: Vec<PromptArgument>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
#[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 GetPromptParams {
pub name: String,
#[serde(default)]
pub arguments: std::collections::HashMap<String, String>,
#[serde(
rename = "inputResponses",
default,
skip_serializing_if = "Option::is_none"
)]
pub input_responses: Option<InputResponses>,
#[serde(
rename = "requestState",
default,
skip_serializing_if = "Option::is_none"
)]
pub request_state: Option<String>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetPromptResult {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub messages: Vec<PromptMessage>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
impl GetPromptResult {
pub fn user_message(text: impl Into<String>) -> Self {
Self {
description: None,
messages: vec![PromptMessage {
role: PromptRole::User,
content: Content::Text {
text: text.into(),
annotations: None,
meta: None,
},
meta: None,
}],
meta: None,
}
}
pub fn user_message_with_description(
text: impl Into<String>,
description: impl Into<String>,
) -> Self {
Self {
description: Some(description.into()),
messages: vec![PromptMessage {
role: PromptRole::User,
content: Content::Text {
text: text.into(),
annotations: None,
meta: None,
},
meta: None,
}],
meta: None,
}
}
pub fn assistant_message(text: impl Into<String>) -> Self {
Self {
description: None,
messages: vec![PromptMessage {
role: PromptRole::Assistant,
content: Content::Text {
text: text.into(),
annotations: None,
meta: None,
},
meta: None,
}],
meta: None,
}
}
pub fn builder() -> GetPromptResultBuilder {
GetPromptResultBuilder::new()
}
pub fn first_message_text(&self) -> Option<&str> {
self.messages.first().and_then(|m| m.content.as_text())
}
pub fn as_json(&self) -> Option<Result<Value, serde_json::Error>> {
self.first_message_text().map(serde_json::from_str)
}
pub fn deserialize<T: DeserializeOwned>(&self) -> Option<Result<T, serde_json::Error>> {
self.first_message_text().map(serde_json::from_str)
}
}
#[derive(Debug, Clone, Default)]
pub struct GetPromptResultBuilder {
description: Option<String>,
messages: Vec<PromptMessage>,
}
impl GetPromptResultBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn user(mut self, text: impl Into<String>) -> Self {
self.messages.push(PromptMessage {
role: PromptRole::User,
content: Content::Text {
text: text.into(),
annotations: None,
meta: None,
},
meta: None,
});
self
}
pub fn assistant(mut self, text: impl Into<String>) -> Self {
self.messages.push(PromptMessage {
role: PromptRole::Assistant,
content: Content::Text {
text: text.into(),
annotations: None,
meta: None,
},
meta: None,
});
self
}
pub fn build(self) -> GetPromptResult {
GetPromptResult {
description: self.description,
messages: self.messages,
meta: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptMessage {
pub role: PromptRole,
pub content: Content,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum PromptRole {
User,
Assistant,
}
pub const TASKS_EXTENSION_ID: &str = "io.modelcontextprotocol/tasks";
pub const RESULT_TYPE_TASK: &str = "task";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub enum TaskSupportMode {
Required,
Optional,
#[default]
Forbidden,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolExecution {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub task_support: Option<TaskSupportMode>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskRequestParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ttl: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum TaskStatus {
Working,
InputRequired,
Completed,
Failed,
Cancelled,
}
impl std::fmt::Display for TaskStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TaskStatus::Working => write!(f, "working"),
TaskStatus::InputRequired => write!(f, "input_required"),
TaskStatus::Completed => write!(f, "completed"),
TaskStatus::Failed => write!(f, "failed"),
TaskStatus::Cancelled => write!(f, "cancelled"),
}
}
}
impl TaskStatus {
pub fn is_terminal(&self) -> bool {
matches!(
self,
TaskStatus::Completed | TaskStatus::Failed | TaskStatus::Cancelled
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskObject {
pub task_id: String,
pub status: TaskStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_message: Option<String>,
pub created_at: String,
pub last_updated_at: String,
pub ttl: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub poll_interval: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result: Option<CallToolResult>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<crate::error::JsonRpcError>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
#[deprecated(note = "Use TaskObject instead")]
pub type TaskInfo = TaskObject;
#[derive(Debug, Clone)]
pub struct CreateTaskResult {
pub task: TaskObject,
pub meta: Option<Value>,
}
impl CreateTaskResult {
pub const RESULT_TYPE: &'static str = RESULT_TYPE_TASK;
pub fn new(task: TaskObject) -> Self {
Self { task, meta: None }
}
}
impl Serialize for CreateTaskResult {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut value = serde_json::to_value(&self.task)
.map_err(|e| serde::ser::Error::custom(format!("task serialize: {e}")))?;
let obj = value
.as_object_mut()
.ok_or_else(|| serde::ser::Error::custom("task did not serialize to a JSON object"))?;
obj.insert(
"resultType".to_string(),
Value::String(Self::RESULT_TYPE.to_string()),
);
obj.insert(
"task".to_string(),
serde_json::to_value(&self.task)
.map_err(|e| serde::ser::Error::custom(format!("task mirror: {e}")))?,
);
if let Some(meta) = &self.meta {
validate_meta_object(meta).map_err(serde::ser::Error::custom)?;
obj.insert("_meta".to_string(), meta.clone());
}
value.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for CreateTaskResult {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
let meta = value.get("_meta").filter(|v| !v.is_null()).cloned();
if let Some(meta) = meta.as_ref() {
validate_meta_object(meta).map_err(serde::de::Error::custom)?;
}
if let Some(task_val) = value.get("task")
&& task_val.is_object()
&& task_val
.as_object()
.is_some_and(|o| o.contains_key("taskId"))
{
let task: TaskObject =
serde_json::from_value(task_val.clone()).map_err(serde::de::Error::custom)?;
return Ok(CreateTaskResult { task, meta });
}
let task: TaskObject = serde_json::from_value(value).map_err(serde::de::Error::custom)?;
Ok(CreateTaskResult { task, meta })
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListTasksParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<TaskStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListTasksResult {
pub tasks: Vec<TaskObject>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetTaskInfoParams {
pub task_id: String,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetTaskResultParams {
pub task_id: String,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CancelTaskParams {
pub task_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateTaskParams {
pub task_id: String,
#[serde(default)]
pub input_responses: HashMap<String, Value>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskStatusParams {
pub task_id: String,
pub status: TaskStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_message: Option<String>,
pub created_at: String,
pub last_updated_at: String,
pub ttl: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub poll_interval: Option<u64>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
pub type TaskStatusChangedParams = TaskStatusParams;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ElicitFormParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<ElicitMode>,
pub message: String,
pub requested_schema: ElicitFormSchema,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ElicitUrlParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<ElicitMode>,
pub elicitation_id: String,
pub message: String,
pub url: String,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum ElicitRequestParams {
Form(ElicitFormParams),
Url(ElicitUrlParams),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum ElicitMode {
Form,
Url,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElicitFormSchema {
#[serde(rename = "type")]
pub schema_type: String,
pub properties: indexmap::IndexMap<String, PrimitiveSchemaDefinition>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required: Vec<String>,
}
impl ElicitFormSchema {
pub fn new() -> Self {
Self {
schema_type: "object".to_string(),
properties: indexmap::IndexMap::new(),
required: Vec::new(),
}
}
pub fn string_field(mut self, name: &str, description: Option<&str>, required: bool) -> Self {
self.properties.insert(
name.to_string(),
PrimitiveSchemaDefinition::String(StringSchema {
schema_type: "string".to_string(),
title: None,
description: description.map(|s| s.to_string()),
format: None,
pattern: None,
min_length: None,
max_length: None,
default: None,
}),
);
if required {
self.required.push(name.to_string());
}
self
}
pub fn string_field_with_default(
mut self,
name: &str,
description: Option<&str>,
required: bool,
default: &str,
) -> Self {
self.properties.insert(
name.to_string(),
PrimitiveSchemaDefinition::String(StringSchema {
schema_type: "string".to_string(),
title: None,
description: description.map(|s| s.to_string()),
format: None,
pattern: None,
min_length: None,
max_length: None,
default: Some(default.to_string()),
}),
);
if required {
self.required.push(name.to_string());
}
self
}
pub fn integer_field(mut self, name: &str, description: Option<&str>, required: bool) -> Self {
self.properties.insert(
name.to_string(),
PrimitiveSchemaDefinition::Integer(IntegerSchema {
schema_type: "integer".to_string(),
title: None,
description: description.map(|s| s.to_string()),
minimum: None,
maximum: None,
default: None,
}),
);
if required {
self.required.push(name.to_string());
}
self
}
pub fn integer_field_with_default(
mut self,
name: &str,
description: Option<&str>,
required: bool,
default: i64,
) -> Self {
self.properties.insert(
name.to_string(),
PrimitiveSchemaDefinition::Integer(IntegerSchema {
schema_type: "integer".to_string(),
title: None,
description: description.map(|s| s.to_string()),
minimum: None,
maximum: None,
default: Some(default),
}),
);
if required {
self.required.push(name.to_string());
}
self
}
pub fn number_field(mut self, name: &str, description: Option<&str>, required: bool) -> Self {
self.properties.insert(
name.to_string(),
PrimitiveSchemaDefinition::Number(NumberSchema {
schema_type: "number".to_string(),
title: None,
description: description.map(|s| s.to_string()),
minimum: None,
maximum: None,
default: None,
}),
);
if required {
self.required.push(name.to_string());
}
self
}
pub fn number_field_with_default(
mut self,
name: &str,
description: Option<&str>,
required: bool,
default: f64,
) -> Self {
self.properties.insert(
name.to_string(),
PrimitiveSchemaDefinition::Number(NumberSchema {
schema_type: "number".to_string(),
title: None,
description: description.map(|s| s.to_string()),
minimum: None,
maximum: None,
default: Some(default),
}),
);
if required {
self.required.push(name.to_string());
}
self
}
pub fn boolean_field(mut self, name: &str, description: Option<&str>, required: bool) -> Self {
self.properties.insert(
name.to_string(),
PrimitiveSchemaDefinition::Boolean(BooleanSchema {
schema_type: "boolean".to_string(),
title: None,
description: description.map(|s| s.to_string()),
default: None,
}),
);
if required {
self.required.push(name.to_string());
}
self
}
pub fn boolean_field_with_default(
mut self,
name: &str,
description: Option<&str>,
required: bool,
default: bool,
) -> Self {
self.properties.insert(
name.to_string(),
PrimitiveSchemaDefinition::Boolean(BooleanSchema {
schema_type: "boolean".to_string(),
title: None,
description: description.map(|s| s.to_string()),
default: Some(default),
}),
);
if required {
self.required.push(name.to_string());
}
self
}
pub fn enum_field(
mut self,
name: &str,
description: Option<&str>,
options: Vec<String>,
required: bool,
) -> Self {
self.properties.insert(
name.to_string(),
PrimitiveSchemaDefinition::SingleSelectEnum(SingleSelectEnumSchema {
schema_type: "string".to_string(),
title: None,
description: description.map(|s| s.to_string()),
enum_values: options,
default: None,
}),
);
if required {
self.required.push(name.to_string());
}
self
}
pub fn enum_field_with_default(
mut self,
name: &str,
description: Option<&str>,
required: bool,
options: &[&str],
default: &str,
) -> Self {
self.properties.insert(
name.to_string(),
PrimitiveSchemaDefinition::SingleSelectEnum(SingleSelectEnumSchema {
schema_type: "string".to_string(),
title: None,
description: description.map(|s| s.to_string()),
enum_values: options.iter().map(|s| s.to_string()).collect(),
default: Some(default.to_string()),
}),
);
if required {
self.required.push(name.to_string());
}
self
}
pub fn raw_field(mut self, name: &str, schema: serde_json::Value, required: bool) -> Self {
self.properties
.insert(name.to_string(), PrimitiveSchemaDefinition::Raw(schema));
if required {
self.required.push(name.to_string());
}
self
}
}
impl Default for ElicitFormSchema {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum PrimitiveSchemaDefinition {
String(StringSchema),
Integer(IntegerSchema),
Number(NumberSchema),
Boolean(BooleanSchema),
SingleSelectEnum(SingleSelectEnumSchema),
MultiSelectEnum(MultiSelectEnumSchema),
Raw(serde_json::Value),
}
impl<'de> Deserialize<'de> for PrimitiveSchemaDefinition {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
let schema_type = value.get("type").and_then(Value::as_str);
let has_enum = value.get("enum").is_some();
fn parse<T, E>(value: Value) -> Result<T, E>
where
T: serde::de::DeserializeOwned,
E: serde::de::Error,
{
serde_json::from_value(value).map_err(serde::de::Error::custom)
}
Ok(match (schema_type, has_enum) {
(Some("string"), true) => Self::SingleSelectEnum(parse(value)?),
(Some("string"), false) => Self::String(parse(value)?),
(Some("integer"), _) => Self::Integer(parse(value)?),
(Some("number"), _) => Self::Number(parse(value)?),
(Some("boolean"), _) => Self::Boolean(parse(value)?),
(Some("array"), _) => Self::MultiSelectEnum(parse(value)?),
_ => Self::Raw(value),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StringSchema {
#[serde(rename = "type")]
pub schema_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_length: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_length: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IntegerSchema {
#[serde(rename = "type")]
pub schema_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maximum: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NumberSchema {
#[serde(rename = "type")]
pub schema_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maximum: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BooleanSchema {
#[serde(rename = "type")]
pub schema_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SingleSelectEnumSchema {
#[serde(rename = "type")]
pub schema_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "enum")]
pub enum_values: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MultiSelectEnumSchema {
#[serde(rename = "type")]
pub schema_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub items: MultiSelectEnumItems,
#[serde(skip_serializing_if = "Option::is_none")]
pub unique_items: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MultiSelectEnumItems {
#[serde(rename = "type")]
pub schema_type: String,
#[serde(rename = "enum")]
pub enum_values: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum ElicitAction {
Accept,
Decline,
Cancel,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElicitResult {
pub action: ElicitAction,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<std::collections::HashMap<String, ElicitFieldValue>>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
impl ElicitResult {
pub fn accept(content: std::collections::HashMap<String, ElicitFieldValue>) -> Self {
Self {
action: ElicitAction::Accept,
content: Some(content),
meta: None,
}
}
pub fn decline() -> Self {
Self {
action: ElicitAction::Decline,
content: None,
meta: None,
}
}
pub fn cancel() -> Self {
Self {
action: ElicitAction::Cancel,
content: None,
meta: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum ElicitFieldValue {
String(String),
Number(f64),
Integer(i64),
Boolean(bool),
StringArray(Vec<String>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ElicitationCompleteParams {
pub elicitation_id: String,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EmptyResult {}
impl McpRequest {
pub fn from_jsonrpc(req: &JsonRpcRequest) -> Result<Self, crate::error::Error> {
let params = req
.params
.clone()
.unwrap_or(Value::Object(Default::default()));
match req.method.as_str() {
"initialize" => {
let p: InitializeParams = serde_json::from_value(params)?;
Ok(McpRequest::Initialize(p))
}
"tools/list" => {
let p: ListToolsParams = serde_json::from_value(params).unwrap_or_default();
Ok(McpRequest::ListTools(p))
}
"tools/call" => {
let p: CallToolParams = serde_json::from_value(params)?;
Ok(McpRequest::CallTool(p))
}
"resources/list" => {
let p: ListResourcesParams = serde_json::from_value(params).unwrap_or_default();
Ok(McpRequest::ListResources(p))
}
"resources/templates/list" => {
let p: ListResourceTemplatesParams =
serde_json::from_value(params).unwrap_or_default();
Ok(McpRequest::ListResourceTemplates(p))
}
"resources/read" => {
let p: ReadResourceParams = serde_json::from_value(params)?;
Ok(McpRequest::ReadResource(p))
}
"resources/subscribe" => {
let p: SubscribeResourceParams = serde_json::from_value(params)?;
Ok(McpRequest::SubscribeResource(p))
}
"resources/unsubscribe" => {
let p: UnsubscribeResourceParams = serde_json::from_value(params)?;
Ok(McpRequest::UnsubscribeResource(p))
}
"prompts/list" => {
let p: ListPromptsParams = serde_json::from_value(params).unwrap_or_default();
Ok(McpRequest::ListPrompts(p))
}
"prompts/get" => {
let p: GetPromptParams = serde_json::from_value(params)?;
Ok(McpRequest::GetPrompt(p))
}
"tasks/get" => {
let p: GetTaskInfoParams = serde_json::from_value(params)?;
Ok(McpRequest::GetTaskInfo(p))
}
"tasks/update" => {
let p: UpdateTaskParams = serde_json::from_value(params)?;
Ok(McpRequest::UpdateTask(p))
}
"tasks/cancel" => {
let p: CancelTaskParams = serde_json::from_value(params)?;
Ok(McpRequest::CancelTask(p))
}
"ping" => Ok(McpRequest::Ping),
"logging/setLevel" => {
let p: SetLogLevelParams = serde_json::from_value(params)?;
Ok(McpRequest::SetLoggingLevel(p))
}
"completion/complete" => {
let p: CompleteParams = serde_json::from_value(params)?;
Ok(McpRequest::Complete(p))
}
"server/discover" => {
let p: DiscoverParams = serde_json::from_value(params).unwrap_or_default();
Ok(McpRequest::Discover(p))
}
"subscriptions/listen" => {
let p: SubscriptionsListenParams =
serde_json::from_value(params).unwrap_or_default();
Ok(McpRequest::SubscriptionsListen(p))
}
method => Ok(McpRequest::Unknown {
method: method.to_string(),
params: req.params.clone(),
}),
}
}
}
impl McpNotification {
pub fn from_jsonrpc(notif: &JsonRpcNotification) -> Result<Self, crate::error::Error> {
let params = notif
.params
.clone()
.unwrap_or(Value::Object(Default::default()));
match notif.method.as_str() {
notifications::INITIALIZED => Ok(McpNotification::Initialized),
notifications::CANCELLED => {
let p: CancelledParams = serde_json::from_value(params)?;
Ok(McpNotification::Cancelled(p))
}
notifications::PROGRESS => {
let p: ProgressParams = serde_json::from_value(params)?;
Ok(McpNotification::Progress(p))
}
notifications::ROOTS_LIST_CHANGED => Ok(McpNotification::RootsListChanged),
method => Ok(McpNotification::Unknown {
method: method.to_string(),
params: notif.params.clone(),
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ResultType {
#[default]
Complete,
InputRequired,
Other(String),
}
impl ResultType {
pub fn as_str(&self) -> &str {
match self {
ResultType::Complete => "complete",
ResultType::InputRequired => "input_required",
ResultType::Other(s) => s.as_str(),
}
}
pub fn is_complete(&self) -> bool {
matches!(self, ResultType::Complete)
}
pub fn from_result_value(value: &Value) -> ResultType {
match value.get("resultType").and_then(Value::as_str) {
None => ResultType::Complete,
Some(s) => ResultType::from(s.to_string()),
}
}
pub fn stamp_into(&self, result: &mut Value, protocol_version: &str) -> bool {
if !version_carries_result_type(protocol_version) {
return false;
}
let Some(obj) = result.as_object_mut() else {
return false;
};
if obj.contains_key("resultType") {
return false;
}
obj.insert(
"resultType".to_string(),
Value::String(self.as_str().to_string()),
);
true
}
}
pub fn version_carries_result_type(protocol_version: &str) -> bool {
protocol_version == PROTOCOL_VERSION_2026_07_28
}
impl From<String> for ResultType {
fn from(s: String) -> ResultType {
match s.as_str() {
"complete" => ResultType::Complete,
"input_required" => ResultType::InputRequired,
_ => ResultType::Other(s),
}
}
}
impl From<ResultType> for String {
fn from(r: ResultType) -> String {
match r {
ResultType::Complete => "complete".to_string(),
ResultType::InputRequired => "input_required".to_string(),
ResultType::Other(s) => s,
}
}
}
impl serde::Serialize for ResultType {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> serde::Deserialize<'de> for ResultType {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<ResultType, D::Error> {
let s = String::deserialize(deserializer)?;
Ok(ResultType::from(s))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "method", content = "params")]
#[non_exhaustive]
pub enum InputRequest {
#[serde(rename = "sampling/createMessage")]
CreateMessage(CreateMessageParams),
#[serde(rename = "roots/list")]
ListRoots(ListRootsParams),
#[serde(rename = "elicitation/create")]
Elicit(ElicitRequestParams),
}
impl InputRequest {
pub fn method_name(&self) -> &str {
match self {
InputRequest::CreateMessage(_) => "sampling/createMessage",
InputRequest::ListRoots(_) => "roots/list",
InputRequest::Elicit(_) => "elicitation/create",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum InputResponse {
CreateMessage(CreateMessageResult),
ListRoots(ListRootsResult),
Elicit(ElicitResult),
}
pub type InputRequests = std::collections::BTreeMap<String, InputRequest>;
pub type InputResponses = std::collections::BTreeMap<String, InputResponse>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InputRequiredResult {
#[serde(rename = "resultType", default = "result_type_input_required")]
pub result_type: ResultType,
#[serde(
rename = "inputRequests",
default,
skip_serializing_if = "Option::is_none"
)]
pub input_requests: Option<InputRequests>,
#[serde(
rename = "requestState",
default,
skip_serializing_if = "Option::is_none"
)]
pub request_state: Option<String>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<Value>,
}
fn result_type_input_required() -> ResultType {
ResultType::InputRequired
}
impl InputRequiredResult {
pub fn new() -> Self {
InputRequiredResult {
result_type: ResultType::InputRequired,
input_requests: None,
request_state: None,
meta: None,
}
}
pub fn with_requests(input_requests: InputRequests) -> Self {
Self {
input_requests: Some(input_requests),
..Self::new()
}
}
pub fn with_request_state(mut self, request_state: impl Into<String>) -> Self {
self.request_state = Some(request_state.into());
self
}
pub fn validate(&self) -> Result<(), &'static str> {
if self.result_type != ResultType::InputRequired {
return Err("resultType must be \"input_required\"");
}
if self.input_requests.is_none() && self.request_state.is_none() {
return Err("at least one of inputRequests or requestState must be present");
}
Ok(())
}
}
impl Default for InputRequiredResult {
fn default() -> Self {
InputRequiredResult::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RequestOutcome<T> {
Complete(T),
InputRequired(InputRequiredResult),
}
impl<T> RequestOutcome<T> {
pub fn input_required(result: InputRequiredResult) -> Self {
RequestOutcome::InputRequired(result)
}
pub fn is_complete(&self) -> bool {
matches!(self, RequestOutcome::Complete(_))
}
pub fn as_complete(&self) -> Option<&T> {
match self {
RequestOutcome::Complete(result) => Some(result),
RequestOutcome::InputRequired(_) => None,
}
}
pub fn as_input_required(&self) -> Option<&InputRequiredResult> {
match self {
RequestOutcome::Complete(_) => None,
RequestOutcome::InputRequired(result) => Some(result),
}
}
}
impl<T> From<T> for RequestOutcome<T> {
fn from(result: T) -> Self {
RequestOutcome::Complete(result)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NotificationMeta {
#[serde(
rename = "io.modelcontextprotocol/subscriptionId",
default,
skip_serializing_if = "Option::is_none"
)]
pub subscription_id: Option<RequestId>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ResultMeta {
#[serde(
rename = "io.modelcontextprotocol/serverInfo",
default,
skip_serializing_if = "Option::is_none"
)]
pub server_info: Option<Implementation>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MissingMetaKey(pub &'static str);
impl std::fmt::Display for MissingMetaKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"missing required _meta key for the negotiated protocol version: {}",
self.0
)
}
}
impl std::error::Error for MissingMetaKey {}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubscriptionFilter {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tools_list_changed: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompts_list_changed: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resources_list_changed: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resource_subscriptions: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub task_ids: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SubscriptionsAcknowledgedParams {
#[serde(
rename = "_meta",
default,
skip_serializing_if = "Option::is_none",
with = "crate::protocol::meta_object_serde"
)]
pub meta: Option<NotificationMeta>,
pub notifications: SubscriptionFilter,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::JsonRpcError;
#[test]
fn request_id_reads_an_integral_float_as_that_integer() {
for (raw, expected) in [
("1.0", 1),
("0.0", 0),
("-0.0", 0),
("-3.0", -3),
("1e2", 100),
("9007199254740992.0", 9_007_199_254_740_992),
("-9007199254740992.0", -9_007_199_254_740_992),
] {
let id: RequestId = serde_json::from_str(raw).unwrap();
assert_eq!(id, RequestId::Number(expected), "{raw}");
let value: Value = serde_json::from_str(raw).unwrap();
assert_eq!(
RequestId::from_json(&value),
Some(RequestId::Number(expected)),
"{raw}"
);
assert_eq!(
serde_json::from_value::<RequestId>(value).unwrap(),
RequestId::Number(expected),
"{raw}"
);
}
}
#[test]
fn request_id_rejects_fractional_and_oversized_numbers() {
for raw in [
"1.5",
"0.1",
"9007199254740994.0",
"1e300",
"18446744073709551615",
"9223372036854775808",
] {
assert!(serde_json::from_str::<RequestId>(raw).is_err(), "{raw}");
let value: Value = serde_json::from_str(raw).unwrap();
assert_eq!(RequestId::from_json(&value), None, "{raw}");
}
for raw in ["true", "null", "[1]", "{\"a\":1}"] {
assert!(serde_json::from_str::<RequestId>(raw).is_err(), "{raw}");
}
}
#[test]
fn request_id_integral_float_is_echoed_as_an_integer() {
let request: JsonRpcRequest =
serde_json::from_str(r#"{"jsonrpc":"2.0","id":1.0,"method":"ping"}"#).unwrap();
assert_eq!(request.id, RequestId::Number(1));
let response = JsonRpcResponse::result(request.id, serde_json::json!({}));
assert_eq!(serde_json::to_value(&response).unwrap()["id"], 1);
}
#[test]
fn request_id_strings_and_integers_are_unchanged() {
assert_eq!(
serde_json::from_str::<RequestId>("\"1.0\"").unwrap(),
RequestId::String("1.0".into())
);
assert_eq!(
serde_json::from_str::<RequestId>("-7").unwrap(),
RequestId::Number(-7)
);
}
#[test]
fn meta_key_validation_matches_the_spec_grammar() {
for key in [
"",
"progressToken",
"traceparent",
"io.modelcontextprotocol/protocolVersion",
"com.example/feature-name_1.2",
"vendor/",
] {
assert_eq!(validate_meta_key(key), Ok(()), "{key:?} should be valid");
}
for key in [
"/name",
"1vendor/name",
"com..example/name",
"com.example-/name",
"com_example/name",
"com.example//name",
"com.example/-name",
"name-",
"na:me",
"métadata",
] {
assert!(validate_meta_key(key).is_err(), "{key:?} should be invalid");
}
assert!(matches!(
validate_extension_identifier("tasks"),
Err(MetaValidationError::MissingExtensionPrefix(key)) if key == "tasks"
));
assert_eq!(
validate_extension_identifier("io.modelcontextprotocol/tasks"),
Ok(())
);
}
#[test]
fn every_typed_meta_field_rejects_invalid_objects_and_keys() {
let valid: InitializeParams = serde_json::from_value(serde_json::json!({
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": { "name": "client", "version": "1" },
"_meta": {
"progressToken": 1,
"com.example/feature": true
}
}))
.unwrap();
assert!(valid.meta.is_some());
let value = serde_json::json!({
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": { "name": "client", "version": "1" },
"_meta": []
});
assert!(serde_json::from_value::<InitializeParams>(value).is_err());
for meta in [
serde_json::json!({"bad/key/again": true}),
serde_json::json!({"-bad": true}),
] {
let value = serde_json::json!({
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": { "name": "client", "version": "1" },
"_meta": meta
});
let parsed = serde_json::from_value::<InitializeParams>(value)
.expect("a misnamed _meta key must not fail the request");
assert!(
parsed
.meta
.is_some_and(|m| m.as_object().is_some_and(|o| o.is_empty())),
"the offending entry is dropped"
);
}
let invalid_outbound = InitializeParams {
protocol_version: "2025-11-25".to_string(),
capabilities: ClientCapabilities::default(),
client_info: Implementation {
name: "client".to_string(),
version: "1".to_string(),
title: None,
description: None,
icons: None,
website_url: None,
meta: None,
},
meta: Some(serde_json::json!({"bad key": true})),
};
assert!(serde_json::to_value(invalid_outbound).is_err());
}
#[test]
fn capability_extensions_require_prefixed_keys_and_object_settings() {
let valid: ClientCapabilities = serde_json::from_value(serde_json::json!({
"extensions": {
"io.modelcontextprotocol/tasks": {},
"com.example/feature": { "version": 1 }
}
}))
.unwrap();
assert_eq!(valid.extensions.as_ref().map(HashMap::len), Some(2));
for extensions in [
serde_json::json!({"tasks": {}}),
serde_json::json!({"com.example/-feature": {}}),
serde_json::json!({"com.example/feature": true}),
] {
let value = serde_json::json!({ "extensions": extensions });
assert!(serde_json::from_value::<ClientCapabilities>(value).is_err());
}
let invalid_outbound = ServerCapabilities {
extensions: Some(HashMap::from([(
"unprefixed".to_string(),
serde_json::json!({}),
)])),
..ServerCapabilities::default()
};
assert!(serde_json::to_value(invalid_outbound).is_err());
}
#[test]
fn released_version_is_known_and_not_enabled_by_default() {
assert_eq!(PROTOCOL_VERSION_2026_07_28, "2026-07-28");
assert!(KNOWN_PROTOCOL_VERSIONS.contains(&PROTOCOL_VERSION_2026_07_28));
assert!(KNOWN_PROTOCOL_VERSIONS.contains(&"2025-06-18"));
assert!(SUPPORTED_PROTOCOL_VERSIONS.contains(&"2025-06-18"));
assert!(
!SUPPORTED_PROTOCOL_VERSIONS.contains(&PROTOCOL_VERSION_2026_07_28),
"the released 2026-07-28 implementation remains explicitly \
compile-time/runtime selectable; current default set: {:?}",
SUPPORTED_PROTOCOL_VERSIONS
);
assert!(SUPPORTED_PROTOCOL_VERSIONS.contains(&LATEST_PROTOCOL_VERSION));
}
#[test]
#[allow(deprecated)]
fn deprecated_version_aliases_remain_compatible() {
assert_eq!(EXPERIMENTAL_PROTOCOL_VERSION, PROTOCOL_VERSION_2026_07_28);
assert_eq!(UPCOMING_PROTOCOL_VERSION, PROTOCOL_VERSION_2026_07_28);
}
#[test]
fn discover_request_round_trips_with_no_params() {
let no_params = r#"{"jsonrpc":"2.0","id":1,"method":"server/discover"}"#;
let req: JsonRpcRequest = serde_json::from_str(no_params).unwrap();
let parsed = McpRequest::from_jsonrpc(&req).unwrap();
assert!(matches!(parsed, McpRequest::Discover(_)));
assert_eq!(parsed.method_name(), "server/discover");
let empty_params = r#"{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{}}"#;
let req: JsonRpcRequest = serde_json::from_str(empty_params).unwrap();
let parsed = McpRequest::from_jsonrpc(&req).unwrap();
assert!(matches!(parsed, McpRequest::Discover(_)));
}
#[test]
fn discover_result_serializes_supported_versions_array() {
let r = DiscoverResult {
supported_versions: vec!["2026-07-28".into(), "2025-11-25".into()],
capabilities: ServerCapabilities::default(),
ttl_ms: None,
cache_scope: None,
instructions: None,
meta: Some(ResultMeta {
server_info: Some(Implementation {
name: "test-server".into(),
version: "1.0.0".into(),
title: None,
description: None,
icons: None,
website_url: None,
meta: None,
}),
}),
};
let json = serde_json::to_value(&r).unwrap();
assert_eq!(
json["supportedVersions"],
serde_json::json!(["2026-07-28", "2025-11-25"])
);
assert!(
json.get("protocolVersion").is_none(),
"server/discover must use supportedVersions, not protocolVersion, got: {json}"
);
assert_eq!(
json["_meta"]["io.modelcontextprotocol/serverInfo"]["name"],
"test-server"
);
assert!(
json.get("instructions").is_none(),
"instructions must be omitted when None, got: {json}"
);
}
#[test]
fn discover_result_omits_meta_ttl_and_cache_scope_when_none() {
let r = DiscoverResult {
supported_versions: vec!["2026-07-28".into()],
capabilities: ServerCapabilities::default(),
ttl_ms: None,
cache_scope: None,
instructions: None,
meta: None,
};
let json = serde_json::to_value(&r).unwrap();
assert!(json.get("_meta").is_none());
assert!(json.get("ttlMs").is_none());
assert!(json.get("cacheScope").is_none());
}
#[test]
fn subscriptions_listen_request_round_trips_with_no_params() {
let no_params = r#"{"jsonrpc":"2.0","id":1,"method":"subscriptions/listen"}"#;
let req: JsonRpcRequest = serde_json::from_str(no_params).unwrap();
let parsed = McpRequest::from_jsonrpc(&req).unwrap();
assert!(matches!(parsed, McpRequest::SubscriptionsListen(_)));
assert_eq!(parsed.method_name(), "subscriptions/listen");
let empty_params =
r#"{"jsonrpc":"2.0","id":2,"method":"subscriptions/listen","params":{}}"#;
let req: JsonRpcRequest = serde_json::from_str(empty_params).unwrap();
let parsed = McpRequest::from_jsonrpc(&req).unwrap();
assert!(matches!(parsed, McpRequest::SubscriptionsListen(_)));
}
#[test]
fn subscriptions_listen_request_serializes_with_correct_method() {
let req = JsonRpcRequest::new(42i64, "subscriptions/listen");
let json = serde_json::to_string(&req).unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(v["method"], "subscriptions/listen");
assert_eq!(v["jsonrpc"], "2.0");
assert_eq!(v["id"], 42);
}
#[test]
fn list_tools_result_omits_ttl_and_cache_scope_by_default() {
let r = ListToolsResult {
tools: vec![],
next_cursor: None,
ttl_ms: None,
cache_scope: None,
meta: None,
};
let json = serde_json::to_value(&r).unwrap();
assert!(
json.get("ttlMs").is_none(),
"ttlMs must be omitted when None, got: {json}"
);
assert!(
json.get("cacheScope").is_none(),
"cacheScope must be omitted when None, got: {json}"
);
}
#[test]
fn list_tools_result_emits_ttl_and_cache_scope_when_set() {
let r = ListToolsResult {
tools: vec![],
next_cursor: None,
ttl_ms: Some(60_000),
cache_scope: Some(CacheScope::Public),
meta: None,
};
let json = serde_json::to_value(&r).unwrap();
assert_eq!(json["ttlMs"], 60_000);
assert_eq!(json["cacheScope"], "public");
}
#[test]
fn cache_scope_roundtrips_via_lowercase_strings() {
for (scope, wire) in [
(CacheScope::Public, "public"),
(CacheScope::Private, "private"),
] {
let s = serde_json::to_value(scope).unwrap();
assert_eq!(s, serde_json::json!(wire));
let back: CacheScope = serde_json::from_value(s).unwrap();
assert_eq!(back, scope);
}
}
#[test]
fn discover_result_accepts_bad_cache_scope_as_none() {
for cache_scope_json in ["\"\"", "\"bogus\"", "42"] {
let json = format!(
r#"{{"supportedVersions":["2026-07-28"],"capabilities":{{}},"cacheScope":{cache_scope_json}}}"#
);
let r: DiscoverResult = serde_json::from_str(&json).unwrap_or_else(|e| {
panic!("cacheScope {cache_scope_json} should not fail deserialization: {e}")
});
assert_eq!(r.cache_scope, None);
assert_eq!(r.supported_versions, vec!["2026-07-28".to_string()]);
}
}
#[test]
fn list_tools_result_accepts_bad_cache_scope_as_none() {
for cache_scope_json in ["\"\"", "\"bogus\"", "42"] {
let json = format!(r#"{{"tools":[],"cacheScope":{cache_scope_json}}}"#);
let r: ListToolsResult = serde_json::from_str(&json).unwrap_or_else(|e| {
panic!("cacheScope {cache_scope_json} should not fail deserialization: {e}")
});
assert_eq!(r.cache_scope, None);
assert!(r.tools.is_empty());
}
}
#[test]
fn list_resources_result_accepts_bad_cache_scope_as_none() {
for cache_scope_json in ["\"\"", "\"bogus\"", "42"] {
let json = format!(r#"{{"resources":[],"cacheScope":{cache_scope_json}}}"#);
let r: ListResourcesResult = serde_json::from_str(&json).unwrap_or_else(|e| {
panic!("cacheScope {cache_scope_json} should not fail deserialization: {e}")
});
assert_eq!(r.cache_scope, None);
assert!(r.resources.is_empty());
}
}
#[test]
fn read_resource_result_accepts_bad_cache_scope_as_none() {
for cache_scope_json in ["\"\"", "\"bogus\"", "42"] {
let json = format!(r#"{{"contents":[],"cacheScope":{cache_scope_json}}}"#);
let r: ReadResourceResult = serde_json::from_str(&json).unwrap_or_else(|e| {
panic!("cacheScope {cache_scope_json} should not fail deserialization: {e}")
});
assert_eq!(r.cache_scope, None);
assert!(r.contents.is_empty());
}
}
#[test]
fn list_resource_templates_result_accepts_bad_cache_scope_as_none() {
for cache_scope_json in ["\"\"", "\"bogus\"", "42"] {
let json = format!(r#"{{"resourceTemplates":[],"cacheScope":{cache_scope_json}}}"#);
let r: ListResourceTemplatesResult = serde_json::from_str(&json).unwrap_or_else(|e| {
panic!("cacheScope {cache_scope_json} should not fail deserialization: {e}")
});
assert_eq!(r.cache_scope, None);
assert!(r.resource_templates.is_empty());
}
}
#[test]
fn list_prompts_result_accepts_bad_cache_scope_as_none() {
for cache_scope_json in ["\"\"", "\"bogus\"", "42"] {
let json = format!(r#"{{"prompts":[],"cacheScope":{cache_scope_json}}}"#);
let r: ListPromptsResult = serde_json::from_str(&json).unwrap_or_else(|e| {
panic!("cacheScope {cache_scope_json} should not fail deserialization: {e}")
});
assert_eq!(r.cache_scope, None);
assert!(r.prompts.is_empty());
}
}
#[test]
fn cache_scope_valid_values_still_deserialize_and_serialize_unchanged() {
for (wire, scope) in [
("public", CacheScope::Public),
("private", CacheScope::Private),
] {
let json = format!(r#"{{"tools":[],"cacheScope":"{wire}"}}"#);
let r: ListToolsResult = serde_json::from_str(&json).unwrap();
assert_eq!(r.cache_scope, Some(scope));
let out = serde_json::to_value(&r).unwrap();
assert_eq!(out["cacheScope"], wire);
}
}
#[test]
fn ttl_ms_accepts_negative_or_non_integer_as_none() {
for ttl_ms_json in ["-1", "1.5", "\"60000\""] {
let json = format!(r#"{{"tools":[],"ttlMs":{ttl_ms_json}}}"#);
let r: ListToolsResult = serde_json::from_str(&json).unwrap_or_else(|e| {
panic!("ttlMs {ttl_ms_json} should not fail deserialization: {e}")
});
assert_eq!(r.ttl_ms, None);
}
for ttl_ms_json in ["-1", "1.5"] {
let json = format!(r#"{{"contents":[],"ttlMs":{ttl_ms_json}}}"#);
let r: ReadResourceResult = serde_json::from_str(&json).unwrap_or_else(|e| {
panic!("ttlMs {ttl_ms_json} should not fail deserialization: {e}")
});
assert_eq!(r.ttl_ms, None);
}
}
#[test]
fn ttl_ms_valid_value_still_deserializes() {
let json = r#"{"tools":[],"ttlMs":60000}"#;
let r: ListToolsResult = serde_json::from_str(json).unwrap();
assert_eq!(r.ttl_ms, Some(60_000));
}
#[test]
fn roots_capability_omits_deprecated_by_default() {
let cap = RootsCapability {
list_changed: true,
deprecated: None,
};
let json = serde_json::to_value(&cap).unwrap();
assert!(
json.get("deprecated").is_none(),
"deprecated must be omitted when None to avoid changing wire output for existing servers, got: {json}"
);
}
#[test]
fn capability_emits_deprecation_info_when_flagged() {
let cap = LoggingCapability {
deprecated: Some(DeprecationInfo {
since: Some("2026-07-28".into()),
remove_in: Some("2027-07-28".into()),
message: Some("Logging moves to OpenTelemetry per SEP-2577".into()),
replacement: Some(
"https://github.com/modelcontextprotocol/modelcontextprotocol/issues/2577"
.into(),
),
}),
};
let json = serde_json::to_value(&cap).unwrap();
let dep = &json["deprecated"];
assert_eq!(dep["since"], "2026-07-28");
assert_eq!(dep["removeIn"], "2027-07-28");
assert!(dep["message"].as_str().unwrap().contains("OpenTelemetry"));
assert!(dep["replacement"].as_str().unwrap().contains("2577"));
}
#[test]
fn deprecation_info_round_trip_with_partial_fields() {
let wire = r#"{"since":"2026-07-28"}"#;
let info: DeprecationInfo = serde_json::from_str(wire).unwrap();
assert_eq!(info.since.as_deref(), Some("2026-07-28"));
assert!(info.remove_in.is_none());
let back = serde_json::to_string(&info).unwrap();
assert_eq!(back, wire);
}
#[test]
fn cancelled_params_serializes_request_id_when_present() {
let p = CancelledParams {
request_id: Some(RequestId::Number(42)),
reason: Some("user abort".into()),
meta: None,
};
let json = serde_json::to_value(&p).unwrap();
assert_eq!(json["requestId"], serde_json::json!(42));
assert_eq!(json["reason"], serde_json::json!("user abort"));
}
#[test]
fn cancelled_params_emits_null_request_id_when_absent() {
let p = CancelledParams {
request_id: None,
reason: None,
meta: None,
};
let json = serde_json::to_value(&p).unwrap();
assert!(
json.get("requestId").is_some(),
"requestId field must be present per MCP spec, got: {json}"
);
assert!(
json["requestId"].is_null(),
"requestId must serialize as null when unset, got: {}",
json["requestId"]
);
}
#[test]
fn cancelled_params_deserializes_null_request_id() {
let wire = r#"{"requestId":null,"reason":"x"}"#;
let p: CancelledParams = serde_json::from_str(wire).unwrap();
assert!(p.request_id.is_none());
assert_eq!(p.reason.as_deref(), Some("x"));
}
#[test]
fn cancelled_params_deserializes_missing_request_id() {
let wire = r#"{"reason":"x"}"#;
let p: CancelledParams = serde_json::from_str(wire).unwrap();
assert!(p.request_id.is_none());
}
#[test]
fn error_response_serializes_id_null_when_unknown() {
let resp = JsonRpcResponse::error(None, JsonRpcError::parse_error("bad json"));
let json = serde_json::to_value(&resp).unwrap();
assert!(
json.get("id").is_some(),
"id field must be present per JSON-RPC 2.0, got: {json}"
);
assert!(
json["id"].is_null(),
"id must serialize as null when unknown, got: {}",
json["id"]
);
}
#[test]
fn error_response_serializes_id_when_known() {
let resp =
JsonRpcResponse::error(Some(RequestId::Number(7)), JsonRpcError::parse_error("bad"));
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["id"], serde_json::json!(7));
}
#[test]
fn error_response_deserializes_null_id() {
let wire = r#"{"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"x"}}"#;
let resp: JsonRpcResponse = serde_json::from_str(wire).unwrap();
match resp {
JsonRpcResponse::Error(e) => assert!(e.id.is_none()),
_ => panic!("expected Error variant"),
}
}
#[test]
fn response_rejects_both_result_and_error() {
let wire = r#"{"jsonrpc":"2.0","id":1,"result":{},"error":{"code":-32603,"message":"x"}}"#;
let err = serde_json::from_str::<JsonRpcResponse>(wire).unwrap_err();
assert!(
err.to_string().contains("both result and error"),
"unexpected error message: {err}"
);
}
#[test]
fn response_treats_an_explicit_null_as_absent() {
let wire =
r#"{"jsonrpc":"2.0","id":1,"result":null,"error":{"code":-32603,"message":"x"}}"#;
assert!(matches!(
serde_json::from_str::<JsonRpcResponse>(wire).unwrap(),
JsonRpcResponse::Error(_)
));
let wire = r#"{"jsonrpc":"2.0","id":1,"result":{"ok":true},"error":null}"#;
assert!(matches!(
serde_json::from_str::<JsonRpcResponse>(wire).unwrap(),
JsonRpcResponse::Result(_)
));
}
#[test]
fn response_rejects_neither_result_nor_error() {
let wire = r#"{"jsonrpc":"2.0","id":1}"#;
let err = serde_json::from_str::<JsonRpcResponse>(wire).unwrap_err();
assert!(
err.to_string().contains("neither result nor error"),
"unexpected error message: {err}"
);
}
#[test]
fn response_still_decodes_result_only() {
let wire = r#"{"jsonrpc":"2.0","id":1,"result":{"ok":true}}"#;
let resp: JsonRpcResponse = serde_json::from_str(wire).unwrap();
match resp {
JsonRpcResponse::Result(r) => assert_eq!(r.result, serde_json::json!({"ok": true})),
JsonRpcResponse::Error(_) => panic!("expected Result variant"),
}
}
#[test]
fn response_still_decodes_error_only() {
let wire = r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"x"}}"#;
let resp: JsonRpcResponse = serde_json::from_str(wire).unwrap();
match resp {
JsonRpcResponse::Error(e) => assert_eq!(e.error.code, -32603),
JsonRpcResponse::Result(_) => panic!("expected Error variant"),
}
}
#[test]
fn response_round_trip_unchanged() {
let result = JsonRpcResponse::result(RequestId::Number(1), serde_json::json!({"a": 1}));
let wire = serde_json::to_string(&result).unwrap();
let back: JsonRpcResponse = serde_json::from_str(&wire).unwrap();
assert_eq!(
serde_json::to_value(&result).unwrap(),
serde_json::to_value(&back).unwrap()
);
let error =
JsonRpcResponse::error(Some(RequestId::Number(2)), JsonRpcError::parse_error("bad"));
let wire = serde_json::to_string(&error).unwrap();
let back: JsonRpcResponse = serde_json::from_str(&wire).unwrap();
assert_eq!(
serde_json::to_value(&error).unwrap(),
serde_json::to_value(&back).unwrap()
);
}
#[test]
fn response_message_does_not_reinterpret_both_fields_frame() {
let wire = r#"{"jsonrpc":"2.0","id":1,"result":{},"error":{"code":-32603,"message":"x"}}"#;
let result = serde_json::from_str::<JsonRpcResponseMessage>(wire);
assert!(
result.is_err(),
"expected a both-fields frame to be rejected, got: {result:?}"
);
}
#[test]
fn test_content_text_constructor() {
let content = Content::text("hello world");
assert_eq!(content.as_text(), Some("hello world"));
match &content {
Content::Text {
text, annotations, ..
} => {
assert_eq!(text, "hello world");
assert!(annotations.is_none());
}
_ => panic!("expected Content::Text"),
}
let content = Content::text(String::from("owned"));
assert_eq!(content.as_text(), Some("owned"));
}
#[test]
fn test_elicit_form_schema_builder() {
let schema = ElicitFormSchema::new()
.string_field("name", Some("Your name"), true)
.number_field("age", Some("Your age"), false)
.boolean_field("agree", Some("Do you agree?"), true)
.enum_field(
"color",
Some("Favorite color"),
vec!["red".to_string(), "green".to_string(), "blue".to_string()],
false,
);
assert_eq!(schema.schema_type, "object");
assert_eq!(schema.properties.len(), 4);
assert_eq!(schema.required.len(), 2);
assert!(schema.required.contains(&"name".to_string()));
assert!(schema.required.contains(&"agree".to_string()));
}
#[test]
fn test_elicit_form_schema_serialization() {
let schema = ElicitFormSchema::new().string_field("username", Some("Enter username"), true);
let json = serde_json::to_value(&schema).unwrap();
assert_eq!(json["type"], "object");
assert!(json["properties"]["username"]["type"] == "string");
assert!(
json["required"]
.as_array()
.unwrap()
.contains(&serde_json::json!("username"))
);
}
#[test]
fn test_elicit_result_accept() {
let mut content = std::collections::HashMap::new();
content.insert(
"name".to_string(),
ElicitFieldValue::String("Alice".to_string()),
);
content.insert("age".to_string(), ElicitFieldValue::Integer(30));
let result = ElicitResult::accept(content);
assert_eq!(result.action, ElicitAction::Accept);
assert!(result.content.is_some());
}
#[test]
fn test_elicit_result_decline() {
let result = ElicitResult::decline();
assert_eq!(result.action, ElicitAction::Decline);
assert!(result.content.is_none());
}
#[test]
fn test_elicit_result_cancel() {
let result = ElicitResult::cancel();
assert_eq!(result.action, ElicitAction::Cancel);
assert!(result.content.is_none());
}
#[test]
fn test_elicit_mode_serialization() {
assert_eq!(
serde_json::to_string(&ElicitMode::Form).unwrap(),
"\"form\""
);
assert_eq!(serde_json::to_string(&ElicitMode::Url).unwrap(), "\"url\"");
}
#[test]
fn test_elicit_action_serialization() {
assert_eq!(
serde_json::to_string(&ElicitAction::Accept).unwrap(),
"\"accept\""
);
assert_eq!(
serde_json::to_string(&ElicitAction::Decline).unwrap(),
"\"decline\""
);
assert_eq!(
serde_json::to_string(&ElicitAction::Cancel).unwrap(),
"\"cancel\""
);
}
#[test]
fn test_elicitation_capability() {
let cap = ElicitationCapability {
form: Some(ElicitationFormCapability {}),
url: None,
};
let json = serde_json::to_value(&cap).unwrap();
assert!(json["form"].is_object());
assert!(json.get("url").is_none());
}
#[test]
fn test_client_capabilities_with_elicitation() {
let caps = ClientCapabilities {
roots: None,
sampling: None,
elicitation: Some(ElicitationCapability {
form: Some(ElicitationFormCapability {}),
url: Some(ElicitationUrlCapability {}),
}),
tasks: None,
experimental: None,
extensions: None,
};
let json = serde_json::to_value(&caps).unwrap();
assert!(json["elicitation"]["form"].is_object());
assert!(json["elicitation"]["url"].is_object());
}
#[test]
fn test_elicit_url_params() {
let params = ElicitUrlParams {
mode: Some(ElicitMode::Url),
elicitation_id: "abc123".to_string(),
message: "Please authorize".to_string(),
url: "https://example.com/auth".to_string(),
meta: None,
};
let json = serde_json::to_value(¶ms).unwrap();
assert_eq!(json["mode"], "url");
assert_eq!(json["elicitationId"], "abc123");
assert_eq!(json["message"], "Please authorize");
assert_eq!(json["url"], "https://example.com/auth");
}
#[test]
fn test_elicitation_complete_params() {
let params = ElicitationCompleteParams {
elicitation_id: "xyz789".to_string(),
meta: None,
};
let json = serde_json::to_value(¶ms).unwrap();
assert_eq!(json["elicitationId"], "xyz789");
}
#[test]
fn test_root_new() {
let root = Root::new("file:///home/user/project");
assert_eq!(root.uri, "file:///home/user/project");
assert!(root.name.is_none());
}
#[test]
fn test_root_with_name() {
let root = Root::with_name("file:///home/user/project", "My Project");
assert_eq!(root.uri, "file:///home/user/project");
assert_eq!(root.name.as_deref(), Some("My Project"));
}
#[test]
fn test_root_serialization() {
let root = Root::with_name("file:///workspace", "Workspace");
let json = serde_json::to_value(&root).unwrap();
assert_eq!(json["uri"], "file:///workspace");
assert_eq!(json["name"], "Workspace");
}
#[test]
fn test_root_serialization_without_name() {
let root = Root::new("file:///workspace");
let json = serde_json::to_value(&root).unwrap();
assert_eq!(json["uri"], "file:///workspace");
assert!(json.get("name").is_none());
}
#[test]
fn test_root_deserialization() {
let json = serde_json::json!({
"uri": "file:///home/user",
"name": "Home"
});
let root: Root = serde_json::from_value(json).unwrap();
assert_eq!(root.uri, "file:///home/user");
assert_eq!(root.name.as_deref(), Some("Home"));
}
#[test]
fn test_list_roots_result() {
let result = ListRootsResult {
roots: vec![
Root::new("file:///project1"),
Root::with_name("file:///project2", "Project 2"),
],
meta: None,
};
let json = serde_json::to_value(&result).unwrap();
let roots = json["roots"].as_array().unwrap();
assert_eq!(roots.len(), 2);
assert_eq!(roots[0]["uri"], "file:///project1");
assert_eq!(roots[1]["name"], "Project 2");
}
#[test]
fn test_roots_capability_serialization() {
let cap = RootsCapability {
list_changed: true,
deprecated: None,
};
let json = serde_json::to_value(&cap).unwrap();
assert_eq!(json["listChanged"], true);
}
#[test]
fn test_client_capabilities_with_roots() {
let caps = ClientCapabilities {
roots: Some(RootsCapability {
list_changed: true,
deprecated: None,
}),
sampling: None,
elicitation: None,
tasks: None,
experimental: None,
extensions: None,
};
let json = serde_json::to_value(&caps).unwrap();
assert_eq!(json["roots"]["listChanged"], true);
}
#[test]
fn test_roots_list_changed_notification_parsing() {
let notif = JsonRpcNotification {
jsonrpc: "2.0".to_string(),
method: notifications::ROOTS_LIST_CHANGED.to_string(),
params: None,
};
let mcp_notif = McpNotification::from_jsonrpc(¬if).unwrap();
assert!(matches!(mcp_notif, McpNotification::RootsListChanged));
}
#[test]
fn test_prompt_reference() {
let ref_ = PromptReference::new("my-prompt");
assert_eq!(ref_.ref_type, "ref/prompt");
assert_eq!(ref_.name, "my-prompt");
let json = serde_json::to_value(&ref_).unwrap();
assert_eq!(json["type"], "ref/prompt");
assert_eq!(json["name"], "my-prompt");
}
#[test]
fn test_resource_reference() {
let ref_ = ResourceReference::new("file:///path/to/file");
assert_eq!(ref_.ref_type, "ref/resource");
assert_eq!(ref_.uri, "file:///path/to/file");
let json = serde_json::to_value(&ref_).unwrap();
assert_eq!(json["type"], "ref/resource");
assert_eq!(json["uri"], "file:///path/to/file");
}
#[test]
fn test_completion_reference_prompt() {
let ref_ = CompletionReference::prompt("test-prompt");
let json = serde_json::to_value(&ref_).unwrap();
assert_eq!(json["type"], "ref/prompt");
assert_eq!(json["name"], "test-prompt");
}
#[test]
fn test_completion_reference_resource() {
let ref_ = CompletionReference::resource("file:///test");
let json = serde_json::to_value(&ref_).unwrap();
assert_eq!(json["type"], "ref/resource");
assert_eq!(json["uri"], "file:///test");
}
#[test]
fn test_completion_argument() {
let arg = CompletionArgument::new("query", "SELECT * FROM");
assert_eq!(arg.name, "query");
assert_eq!(arg.value, "SELECT * FROM");
}
#[test]
fn test_complete_params_serialization() {
let params = CompleteParams {
reference: CompletionReference::prompt("sql-prompt"),
argument: CompletionArgument::new("query", "SEL"),
context: None,
meta: None,
};
let json = serde_json::to_value(¶ms).unwrap();
assert_eq!(json["ref"]["type"], "ref/prompt");
assert_eq!(json["ref"]["name"], "sql-prompt");
assert_eq!(json["argument"]["name"], "query");
assert_eq!(json["argument"]["value"], "SEL");
assert!(json.get("context").is_none()); }
#[test]
fn test_completion_new() {
let completion = Completion::new(vec!["SELECT".to_string(), "SET".to_string()]);
assert_eq!(completion.values.len(), 2);
assert!(completion.total.is_none());
assert!(completion.has_more.is_none());
}
#[test]
fn test_completion_with_pagination() {
let completion =
Completion::with_pagination(vec!["a".to_string(), "b".to_string()], 100, true);
assert_eq!(completion.values.len(), 2);
assert_eq!(completion.total, Some(100));
assert_eq!(completion.has_more, Some(true));
}
#[test]
fn test_complete_result() {
let result = CompleteResult::new(vec!["option1".to_string(), "option2".to_string()]);
let json = serde_json::to_value(&result).unwrap();
assert!(json["completion"]["values"].is_array());
assert_eq!(json["completion"]["values"][0], "option1");
}
#[test]
fn test_model_hint() {
let hint = ModelHint::new("claude-3-opus");
assert_eq!(hint.name, Some("claude-3-opus".to_string()));
}
#[test]
fn test_model_preferences_builder() {
let prefs = ModelPreferences::new()
.speed(0.8)
.intelligence(0.9)
.cost(0.5)
.hint("gpt-4")
.hint("claude-3");
assert_eq!(prefs.speed_priority, Some(0.8));
assert_eq!(prefs.intelligence_priority, Some(0.9));
assert_eq!(prefs.cost_priority, Some(0.5));
assert_eq!(prefs.hints.len(), 2);
}
#[test]
fn test_model_preferences_clamping() {
let prefs = ModelPreferences::new().speed(1.5).cost(-0.5);
assert_eq!(prefs.speed_priority, Some(1.0)); assert_eq!(prefs.cost_priority, Some(0.0)); }
#[allow(deprecated)]
#[test]
fn test_include_context_serialization() {
assert_eq!(
serde_json::to_string(&IncludeContext::AllServers).unwrap(),
"\"allServers\""
);
assert_eq!(
serde_json::to_string(&IncludeContext::ThisServer).unwrap(),
"\"thisServer\""
);
assert_eq!(
serde_json::to_string(&IncludeContext::None).unwrap(),
"\"none\""
);
}
#[test]
fn test_sampling_message_user() {
let msg = SamplingMessage::user("Hello, how are you?");
assert_eq!(msg.role, ContentRole::User);
assert!(
matches!(msg.content, SamplingContentOrArray::Single(SamplingContent::Text { ref text, .. }) if text == "Hello, how are you?")
);
}
#[test]
fn test_sampling_message_assistant() {
let msg = SamplingMessage::assistant("I'm doing well!");
assert_eq!(msg.role, ContentRole::Assistant);
}
#[test]
fn test_sampling_content_text_serialization() {
let content = SamplingContent::Text {
text: "Hello".to_string(),
annotations: None,
meta: None,
};
let json = serde_json::to_value(&content).unwrap();
assert_eq!(json["type"], "text");
assert_eq!(json["text"], "Hello");
}
#[test]
fn test_sampling_content_image_serialization() {
let content = SamplingContent::Image {
data: "base64data".to_string(),
mime_type: "image/png".to_string(),
annotations: None,
meta: None,
};
let json = serde_json::to_value(&content).unwrap();
assert_eq!(json["type"], "image");
assert_eq!(json["data"], "base64data");
assert_eq!(json["mimeType"], "image/png");
}
#[allow(deprecated)]
#[test]
fn test_create_message_params() {
let params = CreateMessageParams::new(
vec![
SamplingMessage::user("What is 2+2?"),
SamplingMessage::assistant("4"),
SamplingMessage::user("And 3+3?"),
],
100,
)
.system_prompt("You are a math tutor")
.temperature(0.7)
.stop_sequence("END")
.include_context(IncludeContext::ThisServer);
assert_eq!(params.messages.len(), 3);
assert_eq!(params.max_tokens, 100);
assert_eq!(
params.system_prompt.as_deref(),
Some("You are a math tutor")
);
assert_eq!(params.temperature, Some(0.7));
assert_eq!(params.stop_sequences.len(), 1);
assert_eq!(params.include_context, Some(IncludeContext::ThisServer));
}
#[test]
fn test_create_message_params_serialization() {
let params = CreateMessageParams::new(vec![SamplingMessage::user("Hello")], 50);
let json = serde_json::to_value(¶ms).unwrap();
assert!(json["messages"].is_array());
assert_eq!(json["maxTokens"], 50);
}
#[test]
fn test_create_message_result_deserialization() {
let json = serde_json::json!({
"content": {
"type": "text",
"text": "The answer is 42"
},
"model": "claude-3-opus",
"role": "assistant",
"stopReason": "end_turn"
});
let result: CreateMessageResult = serde_json::from_value(json).unwrap();
assert_eq!(result.model, "claude-3-opus");
assert_eq!(result.role, ContentRole::Assistant);
assert_eq!(result.stop_reason.as_deref(), Some("end_turn"));
}
#[test]
fn test_completions_capability_serialization() {
let cap = CompletionsCapability {};
let json = serde_json::to_value(&cap).unwrap();
assert!(json.is_object());
}
#[test]
fn test_server_capabilities_with_completions() {
let caps = ServerCapabilities {
completions: Some(CompletionsCapability {}),
..Default::default()
};
let json = serde_json::to_value(&caps).unwrap();
assert!(json["completions"].is_object());
}
#[test]
fn test_content_resource_link_serialization() {
let content = Content::ResourceLink {
uri: "file:///test.txt".to_string(),
name: "test.txt".to_string(),
title: None,
description: Some("A test file".to_string()),
mime_type: Some("text/plain".to_string()),
size: None,
icons: None,
annotations: None,
meta: None,
};
let json = serde_json::to_value(&content).unwrap();
assert_eq!(json["type"], "resource_link");
assert_eq!(json["uri"], "file:///test.txt");
assert_eq!(json["name"], "test.txt");
assert_eq!(json["description"], "A test file");
assert_eq!(json["mimeType"], "text/plain");
}
#[test]
fn test_call_tool_result_resource_link() {
let result = CallToolResult::resource_link("file:///output.json", "output.json");
assert_eq!(result.content.len(), 1);
assert!(!result.is_error);
match &result.content[0] {
Content::ResourceLink { uri, .. } => assert_eq!(uri, "file:///output.json"),
_ => panic!("Expected ResourceLink content"),
}
}
#[test]
fn test_call_tool_result_image() {
let result = CallToolResult::image("base64data", "image/png");
assert_eq!(result.content.len(), 1);
match &result.content[0] {
Content::Image {
data, mime_type, ..
} => {
assert_eq!(data, "base64data");
assert_eq!(mime_type, "image/png");
}
_ => panic!("Expected Image content"),
}
}
#[test]
fn test_call_tool_result_audio() {
let result = CallToolResult::audio("audiodata", "audio/wav");
assert_eq!(result.content.len(), 1);
match &result.content[0] {
Content::Audio {
data, mime_type, ..
} => {
assert_eq!(data, "audiodata");
assert_eq!(mime_type, "audio/wav");
}
_ => panic!("Expected Audio content"),
}
}
#[test]
fn test_sampling_tool_serialization() {
let tool = SamplingTool {
name: "get_weather".to_string(),
title: None,
description: Some("Get current weather".to_string()),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"location": { "type": "string" }
}
}),
output_schema: None,
icons: None,
annotations: None,
execution: None,
};
let json = serde_json::to_value(&tool).unwrap();
assert_eq!(json["name"], "get_weather");
assert_eq!(json["description"], "Get current weather");
assert!(json["inputSchema"]["properties"]["location"].is_object());
}
#[test]
fn test_tool_choice_modes() {
let auto = ToolChoice::auto();
assert_eq!(auto.mode, "auto");
assert!(auto.name.is_none());
let required = ToolChoice::required();
assert_eq!(required.mode, "required");
let none = ToolChoice::none();
assert_eq!(none.mode, "none");
let tool = ToolChoice::tool("get_weather");
assert_eq!(tool.mode, "tool");
assert_eq!(tool.name.as_deref(), Some("get_weather"));
let json = serde_json::to_value(&auto).unwrap();
assert_eq!(json["mode"], "auto");
assert!(json.get("name").is_none());
let json = serde_json::to_value(&tool).unwrap();
assert_eq!(json["mode"], "tool");
assert_eq!(json["name"], "get_weather");
}
#[test]
fn test_sampling_content_tool_use() {
let content = SamplingContent::ToolUse {
id: "tool_123".to_string(),
name: "get_weather".to_string(),
input: serde_json::json!({"location": "San Francisco"}),
meta: None,
};
let json = serde_json::to_value(&content).unwrap();
assert_eq!(json["type"], "tool_use");
assert_eq!(json["id"], "tool_123");
assert_eq!(json["name"], "get_weather");
assert_eq!(json["input"]["location"], "San Francisco");
}
#[test]
fn test_sampling_content_tool_result() {
let content = SamplingContent::ToolResult {
tool_use_id: "tool_123".to_string(),
content: vec![SamplingContent::Text {
text: "72F, sunny".to_string(),
annotations: None,
meta: None,
}],
structured_content: None,
is_error: None,
meta: None,
};
let json = serde_json::to_value(&content).unwrap();
assert_eq!(json["type"], "tool_result");
assert_eq!(json["toolUseId"], "tool_123");
assert_eq!(json["content"][0]["type"], "text");
}
#[test]
fn test_sampling_content_or_array_single() {
let json = serde_json::json!({
"type": "text",
"text": "Hello"
});
let content: SamplingContentOrArray = serde_json::from_value(json).unwrap();
let items = content.items();
assert_eq!(items.len(), 1);
match items[0] {
SamplingContent::Text { text, .. } => assert_eq!(text, "Hello"),
_ => panic!("Expected text content"),
}
}
#[test]
fn test_sampling_content_or_array_multiple() {
let json = serde_json::json!([
{ "type": "text", "text": "Hello" },
{ "type": "text", "text": "World" }
]);
let content: SamplingContentOrArray = serde_json::from_value(json).unwrap();
let items = content.items();
assert_eq!(items.len(), 2);
}
#[test]
fn test_create_message_params_with_tools() {
let tool = SamplingTool {
name: "calculator".to_string(),
title: None,
description: Some("Do math".to_string()),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icons: None,
annotations: None,
execution: None,
};
let params = CreateMessageParams::new(vec![], 100)
.tools(vec![tool])
.tool_choice(ToolChoice::auto());
let json = serde_json::to_value(¶ms).unwrap();
assert!(json["tools"].is_array());
assert_eq!(json["tools"][0]["name"], "calculator");
assert_eq!(json["toolChoice"]["mode"], "auto");
}
#[test]
fn test_create_message_result_content_items() {
let result = CreateMessageResult {
content: SamplingContentOrArray::Array(vec![
SamplingContent::Text {
text: "First".to_string(),
annotations: None,
meta: None,
},
SamplingContent::Text {
text: "Second".to_string(),
annotations: None,
meta: None,
},
]),
model: "test".to_string(),
role: ContentRole::Assistant,
stop_reason: None,
meta: None,
};
let items = result.content_items();
assert_eq!(items.len(), 2);
}
#[test]
fn test_sampling_content_as_text() {
let text_content = SamplingContent::Text {
text: "Hello".to_string(),
annotations: None,
meta: None,
};
assert_eq!(text_content.as_text(), Some("Hello"));
let image_content = SamplingContent::Image {
data: "base64data".to_string(),
mime_type: "image/png".to_string(),
annotations: None,
meta: None,
};
assert_eq!(image_content.as_text(), None);
let audio_content = SamplingContent::Audio {
data: "base64audio".to_string(),
mime_type: "audio/wav".to_string(),
annotations: None,
meta: None,
};
assert_eq!(audio_content.as_text(), None);
}
#[test]
fn test_create_message_result_first_text_single() {
let result = CreateMessageResult {
content: SamplingContentOrArray::Single(SamplingContent::Text {
text: "Hello, world!".to_string(),
annotations: None,
meta: None,
}),
model: "test".to_string(),
role: ContentRole::Assistant,
stop_reason: None,
meta: None,
};
assert_eq!(result.first_text(), Some("Hello, world!"));
}
#[test]
fn test_create_message_result_first_text_array() {
let result = CreateMessageResult {
content: SamplingContentOrArray::Array(vec![
SamplingContent::Text {
text: "First".to_string(),
annotations: None,
meta: None,
},
SamplingContent::Text {
text: "Second".to_string(),
annotations: None,
meta: None,
},
]),
model: "test".to_string(),
role: ContentRole::Assistant,
stop_reason: None,
meta: None,
};
assert_eq!(result.first_text(), Some("First"));
}
#[test]
fn test_create_message_result_first_text_skips_non_text() {
let result = CreateMessageResult {
content: SamplingContentOrArray::Array(vec![
SamplingContent::Image {
data: "base64data".to_string(),
mime_type: "image/png".to_string(),
annotations: None,
meta: None,
},
SamplingContent::Text {
text: "After image".to_string(),
annotations: None,
meta: None,
},
]),
model: "test".to_string(),
role: ContentRole::Assistant,
stop_reason: None,
meta: None,
};
assert_eq!(result.first_text(), Some("After image"));
}
#[test]
fn test_create_message_result_first_text_none() {
let result = CreateMessageResult {
content: SamplingContentOrArray::Single(SamplingContent::Image {
data: "base64data".to_string(),
mime_type: "image/png".to_string(),
annotations: None,
meta: None,
}),
model: "test".to_string(),
role: ContentRole::Assistant,
stop_reason: None,
meta: None,
};
assert_eq!(result.first_text(), None);
}
#[test]
fn test_tool_annotations_accessors() {
let annotations = ToolAnnotations {
read_only_hint: true,
destructive_hint: false,
idempotent_hint: true,
open_world_hint: false,
..Default::default()
};
assert!(annotations.is_read_only());
assert!(!annotations.is_destructive());
assert!(annotations.is_idempotent());
assert!(!annotations.is_open_world());
}
#[test]
fn test_tool_annotations_defaults() {
let annotations = ToolAnnotations::default();
assert!(!annotations.is_read_only());
assert!(annotations.is_destructive());
assert!(!annotations.is_idempotent());
assert!(annotations.is_open_world());
}
#[test]
fn test_tool_annotations_serde_defaults() {
let annotations: ToolAnnotations = serde_json::from_str("{}").unwrap();
assert!(!annotations.is_read_only());
assert!(annotations.is_destructive());
assert!(!annotations.is_idempotent());
assert!(annotations.is_open_world());
}
#[test]
fn test_tool_definition_accessors_with_annotations() {
let def = ToolDefinition {
name: "test".to_string(),
title: None,
description: None,
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icons: None,
annotations: Some(ToolAnnotations {
read_only_hint: true,
idempotent_hint: true,
destructive_hint: false,
open_world_hint: false,
..Default::default()
}),
execution: None,
meta: None,
};
assert!(def.is_read_only());
assert!(!def.is_destructive());
assert!(def.is_idempotent());
assert!(!def.is_open_world());
}
#[test]
fn test_tool_definition_accessors_without_annotations() {
let def = ToolDefinition {
name: "test".to_string(),
title: None,
description: None,
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icons: None,
annotations: None,
execution: None,
meta: None,
};
assert!(!def.is_read_only());
assert!(def.is_destructive());
assert!(!def.is_idempotent());
assert!(def.is_open_world());
}
#[test]
fn test_call_tool_result_from_list() {
#[derive(serde::Serialize)]
struct Item {
name: String,
}
let items = vec![
Item {
name: "a".to_string(),
},
Item {
name: "b".to_string(),
},
Item {
name: "c".to_string(),
},
];
let result = CallToolResult::from_list("items", &items).unwrap();
assert!(!result.is_error);
let structured = result.structured_content.unwrap();
assert_eq!(structured["count"], 3);
assert_eq!(structured["items"].as_array().unwrap().len(), 3);
assert_eq!(structured["items"][0]["name"], "a");
}
#[test]
fn test_call_tool_result_from_list_empty() {
let items: Vec<String> = vec![];
let result = CallToolResult::from_list("results", &items).unwrap();
assert!(!result.is_error);
let structured = result.structured_content.unwrap();
assert_eq!(structured["count"], 0);
assert_eq!(structured["results"].as_array().unwrap().len(), 0);
}
#[test]
fn call_tool_result_round_trips_absent_null_and_object_structured_content() {
let absent: CallToolResult = serde_json::from_value(serde_json::json!({
"content": []
}))
.unwrap();
assert!(absent.structured_content.is_none());
assert!(
!serde_json::to_value(&absent)
.unwrap()
.as_object()
.unwrap()
.contains_key("structuredContent")
);
let null: CallToolResult = serde_json::from_value(serde_json::json!({
"content": [],
"structuredContent": null
}))
.unwrap();
assert_eq!(null.structured_content, Some(Value::Null));
let encoded = serde_json::to_value(&null).unwrap();
assert_eq!(
encoded.as_object().unwrap().get("structuredContent"),
Some(&Value::Null)
);
let object: CallToolResult = serde_json::from_value(serde_json::json!({
"content": [],
"structuredContent": {"a": 1}
}))
.unwrap();
assert_eq!(object.structured_content, Some(serde_json::json!({"a": 1})));
assert_eq!(
serde_json::to_value(&object).unwrap()["structuredContent"],
serde_json::json!({"a": 1})
);
}
#[test]
fn test_call_tool_result_as_json() {
let result = CallToolResult::json(serde_json::json!({"key": "value"}));
let value = result.as_json().unwrap().unwrap();
assert_eq!(value["key"], "value");
}
#[test]
fn test_call_tool_result_as_json_from_text() {
let result = CallToolResult::text(r#"{"key": "value"}"#);
let value = result.as_json().unwrap().unwrap();
assert_eq!(value["key"], "value");
}
#[test]
fn test_call_tool_result_as_json_none() {
let result = CallToolResult::text("not json");
let parsed = result.as_json().unwrap();
assert!(parsed.is_err());
}
#[test]
fn test_call_tool_result_deserialize() {
#[derive(Debug, serde::Deserialize, PartialEq)]
struct Output {
key: String,
}
let result = CallToolResult::json(serde_json::json!({"key": "value"}));
let output: Output = result.deserialize().unwrap().unwrap();
assert_eq!(output.key, "value");
}
#[test]
fn test_call_tool_result_as_json_empty() {
let result = CallToolResult {
content: vec![],
is_error: false,
structured_content: None,
meta: None,
};
assert!(result.as_json().is_none());
}
#[test]
fn test_call_tool_result_deserialize_from_text() {
#[derive(Debug, serde::Deserialize, PartialEq)]
struct Output {
key: String,
}
let result = CallToolResult::text(r#"{"key": "from_text"}"#);
let output: Output = result.deserialize().unwrap().unwrap();
assert_eq!(output.key, "from_text");
}
#[test]
fn test_read_resource_result_as_json() {
let result = ReadResourceResult::json("data://config", &serde_json::json!({"port": 8080}));
let value = result.as_json().unwrap().unwrap();
assert_eq!(value["port"], 8080);
}
#[test]
fn test_read_resource_result_deserialize() {
#[derive(Debug, serde::Deserialize, PartialEq)]
struct Config {
port: u16,
}
let result = ReadResourceResult::json("data://config", &serde_json::json!({"port": 8080}));
let config: Config = result.deserialize().unwrap().unwrap();
assert_eq!(config.port, 8080);
}
#[test]
fn test_get_prompt_result_as_json() {
let result = GetPromptResult::user_message(r#"{"action": "analyze"}"#);
let value = result.as_json().unwrap().unwrap();
assert_eq!(value["action"], "analyze");
}
#[test]
fn test_get_prompt_result_deserialize() {
#[derive(Debug, serde::Deserialize, PartialEq)]
struct Params {
action: String,
}
let result = GetPromptResult::user_message(r#"{"action": "analyze"}"#);
let params: Params = result.deserialize().unwrap().unwrap();
assert_eq!(params.action, "analyze");
}
#[test]
fn test_get_prompt_result_as_json_empty() {
let result = GetPromptResult {
description: None,
messages: vec![],
meta: None,
};
assert!(result.as_json().is_none());
}
#[test]
fn test_mcp_response_serde_roundtrip() {
let response = McpResponse::CallTool(CallToolResult {
content: vec![Content::text("hello")],
structured_content: None,
is_error: false,
meta: None,
});
let json = serde_json::to_string(&response).unwrap();
let deserialized: McpResponse = serde_json::from_str(&json).unwrap();
match deserialized {
McpResponse::CallTool(result) => {
assert_eq!(result.content[0].as_text(), Some("hello"));
}
_ => panic!("expected CallTool variant"),
}
let response = McpResponse::ListTools(ListToolsResult {
tools: vec![],
next_cursor: Some("cursor123".to_string()),
ttl_ms: None,
cache_scope: None,
meta: None,
});
let json = serde_json::to_string(&response).unwrap();
let deserialized: McpResponse = serde_json::from_str(&json).unwrap();
match deserialized {
McpResponse::ListTools(result) => {
assert_eq!(result.next_cursor.as_deref(), Some("cursor123"));
}
_ => panic!("expected ListTools variant"),
}
}
fn task_obj_for_tests() -> TaskObject {
TaskObject {
task_id: "task-786512e2".into(),
status: TaskStatus::Working,
status_message: None,
created_at: "2025-11-25T10:30:00Z".into(),
last_updated_at: "2025-11-25T10:30:00Z".into(),
ttl: Some(60_000),
poll_interval: Some(5_000),
result: None,
error: None,
meta: None,
}
}
#[test]
fn tasks_extension_id_matches_sep_2663() {
assert_eq!(TASKS_EXTENSION_ID, "io.modelcontextprotocol/tasks");
}
#[test]
fn create_task_result_serializes_with_sep_2663_discriminator() {
let result = CreateTaskResult::new(task_obj_for_tests());
let json = serde_json::to_value(&result).unwrap();
assert_eq!(json["resultType"], serde_json::json!("task"));
assert_eq!(json["taskId"], serde_json::json!("task-786512e2"));
assert_eq!(json["status"], serde_json::json!("working"));
assert_eq!(json["pollInterval"], serde_json::json!(5_000));
assert_eq!(json["ttl"], serde_json::json!(60_000));
assert_eq!(json["createdAt"], serde_json::json!("2025-11-25T10:30:00Z"));
assert_eq!(json["task"]["taskId"], serde_json::json!("task-786512e2"));
}
#[test]
fn create_task_result_round_trips_via_flat_layout() {
let original = CreateTaskResult::new(task_obj_for_tests());
let json = serde_json::to_string(&original).unwrap();
let parsed: CreateTaskResult = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.task.task_id, "task-786512e2");
assert_eq!(parsed.task.status, TaskStatus::Working);
assert_eq!(parsed.task.ttl, Some(60_000));
}
#[test]
fn create_task_result_accepts_legacy_nested_layout() {
let legacy = serde_json::json!({
"task": {
"taskId": "legacy-id",
"status": "working",
"createdAt": "2025-11-25T10:30:00Z",
"lastUpdatedAt": "2025-11-25T10:30:00Z",
"ttl": null
}
});
let parsed: CreateTaskResult = serde_json::from_value(legacy).unwrap();
assert_eq!(parsed.task.task_id, "legacy-id");
assert_eq!(parsed.task.status, TaskStatus::Working);
}
#[test]
fn tasks_update_method_parses_via_from_jsonrpc() {
let body = r#"{
"jsonrpc": "2.0",
"id": 6,
"method": "tasks/update",
"params": {
"taskId": "abc-123",
"inputResponses": {
"name": {
"action": "accept",
"content": { "input": "Luca" }
}
}
}
}"#;
let req: JsonRpcRequest = serde_json::from_str(body).unwrap();
let parsed = McpRequest::from_jsonrpc(&req).unwrap();
match parsed {
McpRequest::UpdateTask(p) => {
assert_eq!(p.task_id, "abc-123");
let name_resp = p.input_responses.get("name").expect("has 'name' key");
assert_eq!(name_resp["action"], "accept");
}
other => panic!("expected UpdateTask, got {:?}", other.method_name()),
}
let parsed = McpRequest::from_jsonrpc(&req).unwrap();
assert_eq!(parsed.method_name(), "tasks/update");
}
#[test]
fn final_tasks_methods_parse() {
for (method, params, expected_name) in [
(
"tasks/get",
serde_json::json!({ "taskId": "x" }),
"tasks/get",
),
(
"tasks/cancel",
serde_json::json!({ "taskId": "x" }),
"tasks/cancel",
),
] {
let req = JsonRpcRequest::new(1, method).with_params(params);
let parsed = McpRequest::from_jsonrpc(&req)
.unwrap_or_else(|e| panic!("failed to parse {method}: {e:?}"));
assert_eq!(parsed.method_name(), expected_name);
}
}
#[test]
fn removed_tasks_methods_fall_through_to_unknown() {
for (method, params) in [
("tasks/list", serde_json::json!({})),
("tasks/result", serde_json::json!({ "taskId": "x" })),
] {
let req = JsonRpcRequest::new(1, method).with_params(params);
let parsed = McpRequest::from_jsonrpc(&req)
.unwrap_or_else(|e| panic!("failed to parse {method}: {e:?}"));
assert!(
matches!(parsed, McpRequest::Unknown { .. }),
"{method} must fall through to Unknown, got {}",
parsed.method_name()
);
}
}
#[test]
fn result_type_task_discriminator_constant() {
assert_eq!(RESULT_TYPE_TASK, "task");
assert_eq!(CreateTaskResult::RESULT_TYPE, RESULT_TYPE_TASK);
}
#[test]
fn server_capabilities_extensions_keys_match_tasks_extension_id() {
let mut extensions = HashMap::new();
extensions.insert(TASKS_EXTENSION_ID.to_string(), serde_json::json!({}));
let caps = ServerCapabilities {
extensions: Some(extensions),
..Default::default()
};
let json = serde_json::to_value(&caps).unwrap();
assert_eq!(
json["extensions"]["io.modelcontextprotocol/tasks"],
serde_json::json!({})
);
}
}
#[cfg(test)]
mod draft_2026_07_28_tests {
use super::*;
use serde_json::json;
#[test]
fn result_type_wire_forms_and_backcompat() {
assert_eq!(
serde_json::to_value(ResultType::Complete).unwrap(),
json!("complete")
);
assert_eq!(
serde_json::to_value(ResultType::InputRequired).unwrap(),
json!("input_required")
);
assert_eq!(
serde_json::to_value(ResultType::Other("task".into())).unwrap(),
json!("task")
);
assert_eq!(
serde_json::from_value::<ResultType>(json!("input_required")).unwrap(),
ResultType::InputRequired
);
assert_eq!(
serde_json::from_value::<ResultType>(json!("task")).unwrap(),
ResultType::Other("task".into())
);
assert_eq!(
ResultType::from_result_value(&json!({"content": []})),
ResultType::Complete
);
assert_eq!(
ResultType::from_result_value(&json!({"resultType": "input_required"})),
ResultType::InputRequired
);
assert!(ResultType::default().is_complete());
}
#[test]
fn input_required_result_round_trips() {
let requests: InputRequests =
serde_json::from_value(json!({ "r1": {"method": "roots/list", "params": {}} }))
.unwrap();
let result = InputRequiredResult {
input_requests: Some(requests),
request_state: Some("opaque-blob".to_string()),
..InputRequiredResult::new()
};
let v = serde_json::to_value(&result).unwrap();
assert_eq!(v["resultType"], json!("input_required"));
assert_eq!(v["inputRequests"]["r1"]["method"], json!("roots/list"));
assert_eq!(v["requestState"], json!("opaque-blob"));
let back: InputRequiredResult = serde_json::from_value(v).unwrap();
assert_eq!(back.result_type, ResultType::InputRequired);
assert_eq!(back.request_state.as_deref(), Some("opaque-blob"));
}
#[test]
fn input_required_builders_validate_and_request_outcome_preserves_variant() {
let requests: InputRequests = [(
"roots".to_string(),
InputRequest::ListRoots(ListRootsParams::default()),
)]
.into_iter()
.collect();
let result =
InputRequiredResult::with_requests(requests).with_request_state("opaque-state");
assert!(result.validate().is_ok());
let outcome: RequestOutcome<CallToolResult> = RequestOutcome::input_required(result);
assert!(!outcome.is_complete());
assert_eq!(
outcome
.as_input_required()
.and_then(|result| result.request_state.as_deref()),
Some("opaque-state")
);
assert!(InputRequiredResult::new().validate().is_err());
let complete: RequestOutcome<CallToolResult> = CallToolResult::text("done").into();
assert!(complete.is_complete());
assert!(complete.as_complete().is_some());
}
#[test]
fn input_request_is_adjacently_tagged_and_response_untagged() {
let ir: InputRequest =
serde_json::from_value(json!({"method": "roots/list", "params": {}})).unwrap();
assert_eq!(ir.method_name(), "roots/list");
assert!(matches!(ir, InputRequest::ListRoots(_)));
assert_eq!(
serde_json::to_value(&ir).unwrap(),
json!({"method": "roots/list", "params": {}})
);
let resp: InputResponse = serde_json::from_value(json!({"roots": []})).unwrap();
assert!(matches!(resp, InputResponse::ListRoots(_)));
}
#[test]
fn request_meta_carries_sep2575_keys_and_validates_by_version() {
let meta = RequestMeta {
protocol_version: Some(PROTOCOL_VERSION_2026_07_28.to_string()),
client_capabilities: Some(ClientCapabilities::default()),
..Default::default()
};
let v = serde_json::to_value(&meta).unwrap();
assert_eq!(
v["io.modelcontextprotocol/protocolVersion"],
json!(PROTOCOL_VERSION_2026_07_28)
);
assert!(
v.get("io.modelcontextprotocol/clientCapabilities")
.is_some()
);
assert!(
meta.validate_for_version(PROTOCOL_VERSION_2026_07_28)
.is_ok()
);
assert!(
RequestMeta::default()
.validate_for_version(PROTOCOL_VERSION_2026_07_28)
.is_err()
);
assert!(
RequestMeta::default()
.validate_for_version("2025-11-25")
.is_ok()
);
}
#[test]
fn subscription_types_round_trip() {
let filter = SubscriptionFilter {
tools_list_changed: Some(true),
resource_subscriptions: Some(vec!["file:///a".to_string()]),
..Default::default()
};
let v = serde_json::to_value(&filter).unwrap();
assert_eq!(v["toolsListChanged"], json!(true));
assert_eq!(v["resourceSubscriptions"], json!(["file:///a"]));
assert!(v.get("promptsListChanged").is_none());
let ack = SubscriptionsAcknowledgedParams {
meta: None,
notifications: filter,
};
let back: SubscriptionsAcknowledgedParams =
serde_json::from_value(serde_json::to_value(&ack).unwrap()).unwrap();
assert_eq!(back.notifications.tools_list_changed, Some(true));
let result = SubscriptionsListenResult {
result_type: ResultType::Complete,
meta: SubscriptionsListenResultMeta {
subscription_id: RequestId::Number(7),
server_info: Some(Implementation {
name: "test-server".to_string(),
version: "1.0.0".to_string(),
title: None,
description: None,
icons: None,
website_url: None,
meta: None,
}),
},
};
let value = serde_json::to_value(&result).unwrap();
assert_eq!(value["resultType"], json!("complete"));
assert_eq!(
value["_meta"]["io.modelcontextprotocol/subscriptionId"],
json!(7)
);
assert_eq!(
value["_meta"]["io.modelcontextprotocol/serverInfo"]["name"],
json!("test-server")
);
let back: SubscriptionsListenResult = serde_json::from_value(value).unwrap();
assert!(back.result_type.is_complete());
assert_eq!(back.meta.subscription_id, RequestId::Number(7));
assert_eq!(
back.meta.server_info.map(|info| info.name),
Some("test-server".to_string())
);
assert!(
serde_json::from_value::<SubscriptionsListenResult>(serde_json::json!({
"resultType": "complete"
}))
.is_err(),
"final subscription results require _meta and a subscription ID"
);
}
#[test]
fn additions_keep_2025_11_25_output_byte_identical() {
let params = CallToolParams {
name: "echo".to_string(),
arguments: json!({"message": "hi"}),
input_responses: None,
request_state: None,
meta: None,
task: None,
};
assert_eq!(
serde_json::to_value(¶ms).unwrap(),
json!({"name": "echo", "arguments": {"message": "hi"}})
);
let meta = RequestMeta {
progress_token: Some(ProgressToken::String("p".into())),
..Default::default()
};
assert_eq!(
serde_json::to_value(&meta).unwrap(),
json!({"progressToken": "p"})
);
}
#[test]
fn meta_survives_on_a_request_with_no_other_params() {
let params = ListToolsParams {
cursor: None,
meta: Some(RequestMeta {
protocol_version: Some(PROTOCOL_VERSION_2026_07_28.to_string()),
..Default::default()
}),
};
let v = serde_json::to_value(¶ms).unwrap();
assert_eq!(
v["_meta"]["io.modelcontextprotocol/protocolVersion"],
json!(PROTOCOL_VERSION_2026_07_28)
);
let back: ListToolsParams = serde_json::from_value(v).unwrap();
assert_eq!(
back.meta.unwrap().protocol_version.as_deref(),
Some(PROTOCOL_VERSION_2026_07_28)
);
}
#[test]
fn result_type_stamp_is_gated_on_the_negotiated_version() {
let result = CallToolResult::text("hi");
let baseline = serde_json::to_value(&result).unwrap();
let mut v = baseline.clone();
assert!(!ResultType::Complete.stamp_into(&mut v, "2025-11-25"));
assert_eq!(v, baseline);
let mut v = baseline.clone();
assert!(ResultType::Complete.stamp_into(&mut v, PROTOCOL_VERSION_2026_07_28));
assert_eq!(v["resultType"], json!("complete"));
assert_eq!(v["content"], baseline["content"]);
let mut v = baseline.clone();
assert!(!ResultType::Complete.stamp_into(&mut v, "2027-01-01"));
assert_eq!(v, baseline);
assert!(!version_carries_result_type("2025-11-25"));
assert!(version_carries_result_type(PROTOCOL_VERSION_2026_07_28));
assert!(!version_carries_result_type("2027-01-01"));
}
#[test]
fn result_type_stamp_leaves_owned_discriminators_alone() {
let mut v = serde_json::to_value(InputRequiredResult::new()).unwrap();
assert!(!ResultType::Complete.stamp_into(&mut v, PROTOCOL_VERSION_2026_07_28));
assert_eq!(v["resultType"], json!("input_required"));
let mut v = json!({"resultType": RESULT_TYPE_TASK, "taskId": "t1"});
assert!(!ResultType::Complete.stamp_into(&mut v, PROTOCOL_VERSION_2026_07_28));
assert_eq!(v["resultType"], json!("task"));
let mut v = json!(null);
assert!(!ResultType::Complete.stamp_into(&mut v, PROTOCOL_VERSION_2026_07_28));
assert_eq!(v, json!(null));
}
#[test]
fn stamped_result_round_trips_and_reads_back_as_complete() {
let example = json!({
"resultType": "complete",
"content": [{"type": "text", "text": "42"}],
"isError": false
});
assert_eq!(
ResultType::from_result_value(&example),
ResultType::Complete
);
let parsed: CallToolResult = serde_json::from_value(example).unwrap();
let mut round_tripped = serde_json::to_value(&parsed).unwrap();
assert!(ResultType::Complete.stamp_into(&mut round_tripped, PROTOCOL_VERSION_2026_07_28));
assert_eq!(round_tripped["resultType"], json!("complete"));
assert_eq!(round_tripped["content"][0]["text"], json!("42"));
let example = json!({
"resultType": "input_required",
"inputRequests": {"r1": {"method": "elicitation/create", "params": {
"message": "which file?",
"requestedSchema": {"type": "object", "properties": {}}
}}}
});
assert_eq!(
ResultType::from_result_value(&example),
ResultType::InputRequired
);
let parsed: InputRequiredResult = serde_json::from_value(example).unwrap();
assert_eq!(parsed.result_type, ResultType::InputRequired);
assert!(parsed.input_requests.is_some_and(|r| r.contains_key("r1")));
}
}
#[cfg(test)]
mod elicitation_field_order_tests {
use super::*;
const DECLARED: &str = r#"{"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"age":{"type":"integer"}},"required":["firstName"]}"#;
fn parse_declared() -> ElicitFormSchema {
serde_json::from_str(DECLARED).expect("declared schema parses")
}
#[test]
fn declared_field_order_survives_a_round_trip() {
let parsed = parse_declared();
let order: Vec<&str> = parsed.properties.keys().map(String::as_str).collect();
assert_eq!(order, ["firstName", "lastName", "email", "age"]);
let direct: ElicitFormSchema =
serde_json::from_str(&serde_json::to_string(&parsed).expect("serialize"))
.expect("reparse");
assert_eq!(
direct
.properties
.keys()
.map(String::as_str)
.collect::<Vec<_>>(),
["firstName", "lastName", "email", "age"],
);
let round = serde_json::to_value(&parsed).unwrap();
let wire: Vec<&str> = round["properties"]
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect();
assert_eq!(
wire,
["firstName", "lastName", "email", "age"],
"the order a server declared must reach the wire unchanged"
);
}
#[test]
fn field_order_is_deterministic_across_decodes() {
let first: Vec<String> = parse_declared().properties.keys().cloned().collect();
for _ in 0..16 {
let again: Vec<String> = parse_declared().properties.keys().cloned().collect();
assert_eq!(first, again);
}
}
#[test]
fn builder_insertion_order_is_preserved() {
let schema = ElicitFormSchema::new()
.string_field("zulu", None, true)
.string_field("alpha", None, false)
.string_field("mike", None, false);
let order: Vec<&str> = schema.properties.keys().map(String::as_str).collect();
assert_eq!(
order,
["zulu", "alpha", "mike"],
"builder order must not be re-sorted"
);
}
#[test]
fn ordering_does_not_disturb_variant_dispatch() {
let parsed = parse_declared();
assert!(matches!(
parsed.properties.get("age"),
Some(PrimitiveSchemaDefinition::Integer(_))
));
assert!(matches!(
parsed.properties.get("firstName"),
Some(PrimitiveSchemaDefinition::String(_))
));
}
}
#[cfg(test)]
mod primitive_schema_dispatch_tests {
use super::*;
fn variant(json: serde_json::Value) -> &'static str {
match serde_json::from_value::<PrimitiveSchemaDefinition>(json).unwrap() {
PrimitiveSchemaDefinition::String(_) => "String",
PrimitiveSchemaDefinition::Integer(_) => "Integer",
PrimitiveSchemaDefinition::Number(_) => "Number",
PrimitiveSchemaDefinition::Boolean(_) => "Boolean",
PrimitiveSchemaDefinition::SingleSelectEnum(_) => "SingleSelectEnum",
PrimitiveSchemaDefinition::MultiSelectEnum(_) => "MultiSelectEnum",
PrimitiveSchemaDefinition::Raw(_) => "Raw",
}
}
#[test]
fn each_declared_type_parses_to_its_own_variant() {
assert_eq!(variant(serde_json::json!({"type": "string"})), "String");
assert_eq!(variant(serde_json::json!({"type": "integer"})), "Integer");
assert_eq!(variant(serde_json::json!({"type": "number"})), "Number");
assert_eq!(variant(serde_json::json!({"type": "boolean"})), "Boolean");
assert_eq!(
variant(serde_json::json!({"type": "string", "enum": ["a", "b"]})),
"SingleSelectEnum"
);
assert_eq!(
variant(serde_json::json!({
"type": "array",
"items": {"type": "string", "enum": ["a", "b"]}
})),
"MultiSelectEnum"
);
}
#[test]
fn single_select_choices_survive_a_round_trip() {
let json = serde_json::json!({"type": "string", "enum": ["a", "b"]});
let parsed: PrimitiveSchemaDefinition = serde_json::from_value(json.clone()).unwrap();
match &parsed {
PrimitiveSchemaDefinition::SingleSelectEnum(schema) => {
assert_eq!(schema.enum_values, vec!["a".to_string(), "b".to_string()]);
}
other => panic!("expected SingleSelectEnum, got {other:?}"),
}
assert_eq!(serde_json::to_value(&parsed).unwrap(), json);
}
#[test]
fn multi_select_choices_survive_a_round_trip() {
let json = serde_json::json!({
"type": "array",
"items": {"type": "string", "enum": ["x", "y"]}
});
let parsed: PrimitiveSchemaDefinition = serde_json::from_value(json.clone()).unwrap();
match &parsed {
PrimitiveSchemaDefinition::MultiSelectEnum(schema) => {
assert_eq!(
schema.items.enum_values,
vec!["x".to_string(), "y".to_string()]
);
}
other => panic!("expected MultiSelectEnum, got {other:?}"),
}
assert_eq!(serde_json::to_value(&parsed).unwrap(), json);
}
#[test]
fn variant_fields_are_preserved() {
let json = serde_json::json!({
"type": "integer",
"title": "Count",
"minimum": 1,
"maximum": 10
});
let parsed: PrimitiveSchemaDefinition = serde_json::from_value(json.clone()).unwrap();
match &parsed {
PrimitiveSchemaDefinition::Integer(schema) => {
assert_eq!(schema.title.as_deref(), Some("Count"));
assert_eq!(schema.minimum, Some(1));
assert_eq!(schema.maximum, Some(10));
}
other => panic!("expected Integer, got {other:?}"),
}
assert_eq!(serde_json::to_value(&parsed).unwrap(), json);
}
#[test]
fn unknown_shapes_fall_back_to_raw() {
assert_eq!(variant(serde_json::json!({"type": "null"})), "Raw");
assert_eq!(variant(serde_json::json!({"anyOf": []})), "Raw");
let json = serde_json::json!({"type": "null", "title": "Nothing"});
let parsed: PrimitiveSchemaDefinition = serde_json::from_value(json.clone()).unwrap();
assert_eq!(serde_json::to_value(&parsed).unwrap(), json);
}
}
#[cfg(test)]
mod tolerant_meta_tests {
use super::*;
#[test]
fn a_nonconforming_meta_key_does_not_discard_the_response() {
let wire = r#"{
"tools": [{"name": "ask", "inputSchema": {"type": "object"}}],
"_meta": {"_fastmcp": {"version": "2"}, "io.example/ok": 1}
}"#;
let result: ListToolsResult =
serde_json::from_str(wire).expect("a bad _meta key must not lose the tools");
assert_eq!(result.tools.len(), 1, "the useful data must survive");
let meta = result.meta.expect("conforming entries remain");
assert!(
meta.get("_fastmcp").is_none(),
"the offending entry is dropped, not propagated: {meta}"
);
assert_eq!(
meta.get("io.example/ok"),
Some(&serde_json::json!(1)),
"conforming entries are untouched: {meta}"
);
}
#[test]
fn a_meta_of_only_bad_keys_deserializes_empty() {
let wire = r#"{"tools": [], "_meta": {"_a": 1, "_b": 2}}"#;
let result: ListToolsResult = serde_json::from_str(wire).expect("must not fail");
assert!(
result
.meta
.is_some_and(|m| m.as_object().unwrap().is_empty()),
"all entries dropped, value retained"
);
}
#[test]
fn serializing_a_nonconforming_meta_key_still_fails() {
let result = ListToolsResult {
tools: vec![],
next_cursor: None,
ttl_ms: None,
cache_scope: None,
meta: Some(serde_json::json!({"_fastmcp": 1})),
};
let error = serde_json::to_string(&result)
.expect_err("emitting a bad _meta key must remain an error");
assert!(
error.to_string().contains("invalid name"),
"the error must name the problem: {error}"
);
}
#[test]
fn conforming_prefixed_keys_round_trip() {
let wire =
r#"{"tools": [], "_meta": {"io.modelcontextprotocol/serverInfo": {"name": "s"}}}"#;
let result: ListToolsResult = serde_json::from_str(wire).unwrap();
assert!(
result
.meta
.unwrap()
.get("io.modelcontextprotocol/serverInfo")
.is_some()
);
}
}
#[cfg(test)]
mod absent_optional_param_tests {
use super::*;
fn keys(value: &serde_json::Value) -> Vec<&str> {
value
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect()
}
#[test]
fn a_first_page_list_request_omits_the_cursor() {
for value in [
serde_json::to_value(ListToolsParams::default()).unwrap(),
serde_json::to_value(ListResourcesParams::default()).unwrap(),
serde_json::to_value(ListResourceTemplatesParams::default()).unwrap(),
serde_json::to_value(ListPromptsParams::default()).unwrap(),
] {
assert!(
!keys(&value).contains(&"cursor"),
"an absent cursor must not be sent at all: {value}"
);
}
}
#[test]
fn a_cursor_that_is_set_is_still_sent() {
let value = serde_json::to_value(ListResourceTemplatesParams {
cursor: Some("page-2".to_string()),
..Default::default()
})
.unwrap();
assert_eq!(value["cursor"], "page-2");
}
#[test]
fn other_absent_optional_params_are_omitted() {
let tasks = serde_json::to_value(ListTasksParams::default()).unwrap();
assert!(!keys(&tasks).contains(&"status"), "{tasks}");
assert!(!keys(&tasks).contains(&"cursor"), "{tasks}");
let cancel = serde_json::to_value(CancelTaskParams {
task_id: "t".to_string(),
reason: None,
meta: None,
})
.unwrap();
assert!(!keys(&cancel).contains(&"reason"), "{cancel}");
}
#[test]
fn meaningful_nulls_are_preserved() {
let cancelled = serde_json::to_value(CancelledParams {
request_id: None,
reason: None,
meta: None,
})
.unwrap();
assert!(
cancelled["requestId"].is_null(),
"a malformed cancellation must remain visibly malformed: {cancelled}"
);
let status = serde_json::to_value(TaskStatusParams {
task_id: "t".to_string(),
status: TaskStatus::Working,
status_message: None,
created_at: "now".to_string(),
last_updated_at: "now".to_string(),
ttl: None,
poll_interval: None,
meta: None,
})
.unwrap();
assert!(
status["ttl"].is_null(),
"null ttl means unlimited, not absent: {status}"
);
}
}