use std::collections::HashMap;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::error::JsonRpcError;
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-03-26"];
#[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(skip_serializing_if = "Option::is_none")]
pub id: Option<RequestId>,
pub error: JsonRpcError,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum JsonRpcResponse {
Result(JsonRpcResultResponse),
Error(JsonRpcErrorResponse),
}
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 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/status";
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")]
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")]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum RequestId {
String(String),
Number(i64),
}
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)
}
}
#[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),
ListTasks(ListTasksParams),
GetTaskInfo(GetTaskInfoParams),
GetTaskResult(GetTaskResultParams),
CancelTask(CancelTaskParams),
Ping,
SetLoggingLevel(SetLogLevelParams),
Complete(CompleteParams),
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::ListTasks(_) => "tasks/list",
McpRequest::GetTaskInfo(_) => "tasks/get",
McpRequest::GetTaskResult(_) => "tasks/result",
McpRequest::CancelTask(_) => "tasks/cancel",
McpRequest::Ping => "ping",
McpRequest::SetLoggingLevel(_) => "logging/setLevel",
McpRequest::Complete(_) => "completion/complete",
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, skip_serializing_if = "Option::is_none")]
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")]
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")]
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>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum McpResponse {
Initialize(InitializeResult),
ListTools(ListToolsResult),
CallTool(CallToolResult),
ListResources(ListResourcesResult),
ListResourceTemplates(ListResourceTemplatesResult),
ReadResource(ReadResourceResult),
SubscribeResource(EmptyResult),
UnsubscribeResource(EmptyResult),
ListPrompts(ListPromptsResult),
GetPrompt(GetPromptResult),
CreateTask(CreateTaskResult),
ListTasks(ListTasksResult),
GetTaskInfo(TaskObject),
GetTaskResult(CallToolResult),
CancelTask(TaskObject),
SetLoggingLevel(EmptyResult),
Complete(CompleteResult),
Pong(EmptyResult),
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")]
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")]
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,
}
#[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")]
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(default, rename = "_meta", skip_serializing_if = "Option::is_none")]
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")]
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>,
}
#[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")]
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")]
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 {
AllServers,
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")]
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")]
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")]
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")]
meta: Option<Value>,
},
#[serde(rename = "tool_use")]
ToolUse {
id: String,
name: String,
input: Value,
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
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")]
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")]
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")]
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")]
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")]
pub meta: Option<Value>,
}
#[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")]
pub extensions: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LoggingCapability {}
#[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, 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")]
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(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
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")]
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 = "_meta", default, skip_serializing_if = "Option::is_none")]
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")]
pub structured_content: Option<Value>,
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
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")]
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")]
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")]
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")]
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")]
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")]
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")]
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(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
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")]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadResourceParams {
pub uri: String,
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadResourceResult {
pub contents: Vec<ResourceContent>,
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option<Value>,
}
impl ReadResourceResult {
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,
}
}
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,
}
}
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,
}
}
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,
}
}
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,
}
}
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")]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct UnsubscribeResourceParams {
pub uri: String,
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ListResourceTemplatesParams {
#[serde(default)]
pub cursor: Option<String>,
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
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(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
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")]
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")]
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(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
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")]
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 = "_meta", default, skip_serializing_if = "Option::is_none")]
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")]
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")]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum PromptRole {
User,
Assistant,
}
#[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(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option<Value>,
}
#[deprecated(note = "Use TaskObject instead")]
pub type TaskInfo = TaskObject;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateTaskResult {
pub task: TaskObject,
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListTasksParams {
#[serde(default)]
pub status: Option<TaskStatus>,
#[serde(default)]
pub cursor: Option<String>,
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
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")]
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")]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CancelTaskParams {
pub task_id: String,
#[serde(default)]
pub reason: Option<String>,
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
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")]
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")]
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")]
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: std::collections::HashMap<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: std::collections::HashMap::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, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum PrimitiveSchemaDefinition {
String(StringSchema),
Integer(IntegerSchema),
Number(NumberSchema),
Boolean(BooleanSchema),
SingleSelectEnum(SingleSelectEnumSchema),
MultiSelectEnum(MultiSelectEnumSchema),
Raw(serde_json::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")]
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")]
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/list" => {
let p: ListTasksParams = serde_json::from_value(params).unwrap_or_default();
Ok(McpRequest::ListTasks(p))
}
"tasks/get" => {
let p: GetTaskInfoParams = serde_json::from_value(params)?;
Ok(McpRequest::GetTaskInfo(p))
}
"tasks/result" => {
let p: GetTaskResultParams = serde_json::from_value(params)?;
Ok(McpRequest::GetTaskResult(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))
}
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(),
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[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 };
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 }),
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)); }
#[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");
}
#[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 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()),
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"),
}
}
}