use serde::{Deserialize, Serialize};
use serde_json::Value;
pub use crate::version::*;
pub mod method {
pub const INITIALIZE: &str = "initialize";
pub const INITIALIZED: &str = "notifications/initialized";
pub const PING: &str = "ping";
pub const TOOLS_LIST: &str = "tools/list";
pub const TOOLS_CALL: &str = "tools/call";
pub const RESOURCES_LIST: &str = "resources/list";
pub const RESOURCES_READ: &str = "resources/read";
pub const RESOURCES_SUBSCRIBE: &str = "resources/subscribe";
pub const RESOURCES_UNSUBSCRIBE: &str = "resources/unsubscribe";
pub const RESOURCES_TEMPLATES_LIST: &str = "resources/templates/list";
pub const PROMPTS_LIST: &str = "prompts/list";
pub const PROMPTS_GET: &str = "prompts/get";
pub const COMPLETION_COMPLETE: &str = "completion/complete";
pub const LOGGING_SET_LEVEL: &str = "logging/setLevel";
pub const TASKS_GET: &str = "tasks/get";
pub const TASKS_UPDATE: &str = "tasks/update";
pub const TASKS_CANCEL: &str = "tasks/cancel";
pub const NOTIFY_TASKS: &str = "notifications/tasks";
pub const SERVER_DISCOVER: &str = "server/discover";
pub const SUBSCRIPTIONS_LISTEN: &str = "subscriptions/listen";
pub const NOTIFY_RESOURCES_UPDATED: &str = "notifications/resources/updated";
pub const NOTIFY_RESOURCES_LIST_CHANGED: &str = "notifications/resources/list_changed";
pub const NOTIFY_TOOLS_LIST_CHANGED: &str = "notifications/tools/list_changed";
pub const NOTIFY_SUBSCRIPTIONS_ACK: &str = "notifications/subscriptions/acknowledged";
pub const NOTIFY_CANCELLED: &str = "notifications/cancelled";
pub const NOTIFY_PROGRESS: &str = "notifications/progress";
pub const NOTIFY_MESSAGE: &str = "notifications/message";
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Implementation {
pub name: String,
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ClientCapabilities {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub experimental: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub elicitation: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub roots: Option<Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeParams {
pub protocol_version: String,
pub capabilities: ClientCapabilities,
pub client_info: Implementation,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeResult {
pub protocol_version: String,
#[serde(default)]
pub capabilities: ServerCapabilities,
#[serde(default)]
pub server_info: Option<Implementation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoverResult {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result_type: Option<String>,
#[serde(default)]
pub supported_versions: Vec<String>,
#[serde(default)]
pub capabilities: ServerCapabilities,
#[serde(default)]
pub server_info: Option<Implementation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ttl_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_scope: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
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<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logging: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub completions: Option<Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolsCapability {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub list_changed: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesCapability {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subscribe: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub list_changed: Option<bool>,
}
impl ServerCapabilities {
pub fn supports_tools(&self) -> bool {
self.tools.is_some()
}
pub fn supports_resources(&self) -> bool {
self.resources.is_some()
}
pub fn supports_subscribe(&self) -> bool {
self.resources
.as_ref()
.and_then(|r| r.subscribe)
.unwrap_or(false)
}
pub fn supports_prompts(&self) -> bool {
self.prompts.is_some()
}
pub fn supports_completions(&self) -> bool {
self.completions.is_some()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Tool {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, 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>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListToolsResult {
pub tools: Vec<Tool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallToolParams {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub arguments: Option<Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallToolResult {
#[serde(default)]
pub content: Vec<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub structured_content: Option<Value>,
}
impl CallToolResult {
pub fn is_error(&self) -> bool {
self.is_error.unwrap_or(false)
}
pub fn text(&self) -> String {
content_text(&self.content)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Resource {
pub uri: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListResourcesResult {
pub resources: Vec<Resource>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadResourceParams {
pub uri: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ReadResourceResult {
#[serde(default)]
pub contents: Vec<Value>,
}
impl ReadResourceResult {
pub fn text(&self) -> String {
content_text(&self.contents)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceTemplate {
pub uri_template: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListResourceTemplatesResult {
pub resource_templates: Vec<ResourceTemplate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubscribeParams {
pub uri: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceUpdatedParams {
pub uri: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
}
pub const TASKS_EXTENSION: &str = "io.modelcontextprotocol/tasks";
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Task {
pub task_id: String,
#[serde(default)]
pub status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status_message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ttl_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub poll_interval_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_requests: Option<Value>,
}
impl Task {
pub fn is_terminal(&self) -> bool {
matches!(self.status.as_str(), "completed" | "failed" | "cancelled")
}
pub fn needs_input(&self) -> bool {
self.status == "input_required"
}
}
pub fn as_task_result(result: &Value) -> Option<Task> {
if result.get("resultType").and_then(Value::as_str) == Some("task") {
serde_json::from_value(result.clone()).ok()
} else {
None
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Prompt {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub arguments: Vec<PromptArgument>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptArgument {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListPromptsResult {
pub prompts: Vec<Prompt>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetPromptParams {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub arguments: Option<Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GetPromptResult {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
pub messages: Vec<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompleteParams {
#[serde(rename = "ref")]
pub reference: Value,
pub argument: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context: Option<Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CompleteResult {
#[serde(default)]
pub completion: Completion,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Completion {
#[serde(default)]
pub values: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub total: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub has_more: Option<bool>,
}
fn content_text(items: &[Value]) -> String {
let mut parts: Vec<String> = Vec::new();
for item in items {
match item.get("type").and_then(Value::as_str) {
Some("text") | None => {
if let Some(t) = item.get("text").and_then(Value::as_str) {
parts.push(t.to_string());
}
}
Some(other) => parts.push(format!("[{other} content]")),
}
}
parts.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn initialize_result_parses_capabilities() {
let json = r#"{
"protocolVersion": "2025-11-25",
"capabilities": {"tools": {"listChanged": true}, "resources": {"subscribe": true}},
"serverInfo": {"name": "fs", "version": "1.0"}
}"#;
let r: InitializeResult = serde_json::from_str(json).unwrap();
assert_eq!(r.protocol_version, "2025-11-25");
assert!(r.capabilities.supports_tools());
assert!(r.capabilities.supports_subscribe());
}
#[test]
fn capability_gating_defaults_closed() {
let caps = ServerCapabilities::default();
assert!(!caps.supports_tools());
assert!(!caps.supports_subscribe());
let json = r#"{"tools": {}, "resources": {"listChanged": true}}"#;
let caps: ServerCapabilities = serde_json::from_str(json).unwrap();
assert!(caps.supports_tools());
assert!(!caps.supports_subscribe());
}
#[test]
fn call_tool_result_text_and_error() {
let json = r#"{"content": [{"type": "text", "text": "hello"}, {"type": "image", "data": "..."}], "isError": false}"#;
let r: CallToolResult = serde_json::from_str(json).unwrap();
assert!(!r.is_error());
assert!(r.text().contains("hello"));
assert!(r.text().contains("[image content]"));
}
#[test]
fn updated_notification_is_uri_only() {
let json = r#"{"uri": "file:///data/in.json"}"#;
let p: ResourceUpdatedParams = serde_json::from_str(json).unwrap();
assert_eq!(p.uri, "file:///data/in.json");
assert!(p.title.is_none());
}
#[test]
fn tool_list_pagination_cursor() {
let json = r#"{"tools": [{"name": "read_file", "inputSchema": {"type": "object"}}], "nextCursor": "abc"}"#;
let r: ListToolsResult = serde_json::from_str(json).unwrap();
assert_eq!(r.tools.len(), 1);
assert_eq!(r.next_cursor.as_deref(), Some("abc"));
}
#[test]
fn discover_result_parses() {
let json = r#"{
"resultType": "complete",
"supportedVersions": ["2026-07-28", "2025-11-25"],
"capabilities": {"tools": {}, "resources": {"subscribe": true}, "prompts": {}},
"serverInfo": {"name": "s", "version": "1"},
"ttlMs": 3600000, "cacheScope": "public"
}"#;
let d: DiscoverResult = serde_json::from_str(json).unwrap();
assert_eq!(d.supported_versions, ["2026-07-28", "2025-11-25"]);
assert!(d.capabilities.supports_tools());
assert!(d.capabilities.supports_subscribe());
assert!(d.capabilities.supports_prompts());
assert_eq!(d.ttl_ms, Some(3_600_000));
}
#[test]
fn task_result_detected_and_lifecycle() {
let create = json!({"resultType": "task", "taskId": "t-1", "status": "working",
"pollIntervalMs": 250, "ttlMs": 60000});
let t = as_task_result(&create).expect("is a task result");
assert_eq!(t.task_id, "t-1");
assert_eq!(t.poll_interval_ms, Some(250));
assert!(!t.is_terminal());
assert!(as_task_result(&json!({"content": []})).is_none());
let done: Task = serde_json::from_value(
json!({"taskId": "t-1", "status": "completed", "result": {"content": []}}),
)
.unwrap();
assert!(done.is_terminal() && !done.needs_input());
let ask: Task = serde_json::from_value(
json!({"taskId": "t-1", "status": "input_required", "inputRequests": {}}),
)
.unwrap();
assert!(ask.needs_input() && !ask.is_terminal());
}
#[test]
fn prompts_and_completion_parse() {
let list: ListPromptsResult = serde_json::from_str(
r#"{"prompts": [{"name": "greet", "arguments": [{"name": "who", "required": true}]}]}"#,
)
.unwrap();
assert_eq!(list.prompts[0].name, "greet");
assert_eq!(list.prompts[0].arguments[0].name, "who");
assert_eq!(list.prompts[0].arguments[0].required, Some(true));
let got: GetPromptResult = serde_json::from_str(
r#"{"description": "d", "messages": [{"role": "user", "content": {"type": "text", "text": "hi"}}]}"#,
)
.unwrap();
assert_eq!(got.messages.len(), 1);
let comp: CompleteResult = serde_json::from_str(
r#"{"completion": {"values": ["alice", "bob"], "hasMore": false}}"#,
)
.unwrap();
assert_eq!(comp.completion.values, ["alice", "bob"]);
assert_eq!(comp.completion.has_more, Some(false));
}
}