use serde::Serialize;
use serde_json::Value;
pub const PROTOCOL_VERSION: &str = "2026-07-28";
pub const SUPPORTED_VERSIONS: &[&str] = &[PROTOCOL_VERSION];
pub const META_PROTOCOL_VERSION: &str = "io.modelcontextprotocol/protocolVersion";
pub const META_CLIENT_INFO: &str = "io.modelcontextprotocol/clientInfo";
pub const META_SERVER_INFO: &str = "io.modelcontextprotocol/serverInfo";
pub mod method {
pub const DISCOVER: &str = "server/discover";
pub const INITIALIZE: &str = "initialize";
pub const PING: &str = "ping";
pub const TOOLS_CALL: &str = "tools/call";
pub const TOOLS_LIST: &str = "tools/list";
pub const RESOURCES_LIST: &str = "resources/list";
pub const RESOURCES_READ: &str = "resources/read";
pub const PROMPTS_LIST: &str = "prompts/list";
pub const PROMPTS_GET: &str = "prompts/get";
}
#[must_use]
pub fn is_action(method: &str) -> bool {
matches!(
method,
method::TOOLS_CALL | method::RESOURCES_READ | method::PROMPTS_GET
)
}
#[must_use]
pub fn is_protocol_notification(method: &str) -> bool {
method.starts_with("notifications/")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ResultType {
Complete,
InputRequired,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CacheScope {
Public,
Private,
}
#[derive(Debug, Clone, Serialize)]
pub struct Tool {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "inputSchema", skip_serializing_if = "Option::is_none")]
pub input_schema: Option<Value>,
#[serde(rename = "outputSchema", skip_serializing_if = "Option::is_none")]
pub output_schema: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<ToolAnnotations>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct ToolAnnotations {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(rename = "readOnlyHint", skip_serializing_if = "Option::is_none")]
pub read_only_hint: Option<bool>,
#[serde(rename = "destructiveHint", skip_serializing_if = "Option::is_none")]
pub destructive_hint: Option<bool>,
#[serde(rename = "idempotentHint", skip_serializing_if = "Option::is_none")]
pub idempotent_hint: Option<bool>,
#[serde(rename = "openWorldHint", skip_serializing_if = "Option::is_none")]
pub open_world_hint: Option<bool>,
}
impl ToolAnnotations {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn title(mut self, title: &str) -> Self {
self.title = Some(title.to_string());
self
}
#[must_use]
pub fn read_only(mut self, hint: bool) -> Self {
self.read_only_hint = Some(hint);
self
}
#[must_use]
pub fn destructive(mut self, hint: bool) -> Self {
self.destructive_hint = Some(hint);
self
}
#[must_use]
pub fn idempotent(mut self, hint: bool) -> Self {
self.idempotent_hint = Some(hint);
self
}
#[must_use]
pub fn open_world(mut self, hint: bool) -> Self {
self.open_world_hint = Some(hint);
self
}
}
#[derive(Debug, Serialize)]
pub struct ToolsListResult {
#[serde(rename = "resultType")]
pub result_type: ResultType,
#[serde(rename = "ttlMs")]
pub ttl_ms: u64,
#[serde(rename = "cacheScope")]
pub cache_scope: CacheScope,
pub tools: Vec<Tool>,
#[serde(rename = "_meta")]
pub meta: Value,
}
#[derive(Debug, Serialize)]
pub struct TextContent {
#[serde(rename = "type")]
pub kind: &'static str,
pub text: String,
}
#[derive(Debug, Serialize)]
pub struct CallToolResult {
#[serde(rename = "resultType")]
pub result_type: ResultType,
pub content: Vec<TextContent>,
#[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
#[serde(rename = "structuredContent", skip_serializing_if = "Option::is_none")]
pub structured_content: Option<Value>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Value>,
}
impl CallToolResult {
#[must_use]
pub fn from_structured(structured: Value) -> Self {
Self {
result_type: ResultType::Complete,
content: vec![TextContent {
kind: "text",
text: structured.to_string(),
}],
is_error: None,
structured_content: Some(structured),
meta: None,
}
}
}
impl From<Result<Value, String>> for CallToolResult {
fn from(result: Result<Value, String>) -> Self {
match result {
Ok(value) => {
let structured = if value.is_object() {
value
} else {
serde_json::json!({ "result": value })
};
Self::from_structured(structured)
}
Err(message) => Self {
result_type: ResultType::Complete,
content: vec![TextContent {
kind: "text",
text: message,
}],
is_error: Some(true),
structured_content: None,
meta: None,
},
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Resource {
pub uri: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct ResourcesListResult {
#[serde(rename = "resultType")]
pub result_type: ResultType,
#[serde(rename = "ttlMs")]
pub ttl_ms: u64,
#[serde(rename = "cacheScope")]
pub cache_scope: CacheScope,
pub resources: Vec<Resource>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ResourceContents {
pub uri: String,
#[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
pub text: String,
}
#[derive(Debug, Serialize)]
pub struct ReadResourceResult {
#[serde(rename = "resultType")]
pub result_type: ResultType,
#[serde(rename = "ttlMs")]
pub ttl_ms: u64,
#[serde(rename = "cacheScope")]
pub cache_scope: CacheScope,
pub contents: Vec<ResourceContents>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Prompt {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct PromptsListResult {
#[serde(rename = "resultType")]
pub result_type: ResultType,
#[serde(rename = "ttlMs")]
pub ttl_ms: u64,
#[serde(rename = "cacheScope")]
pub cache_scope: CacheScope,
pub prompts: Vec<Prompt>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Serialize)]
pub struct PromptContent {
#[serde(rename = "type")]
pub kind: &'static str,
pub text: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct PromptMessage {
pub role: &'static str,
pub content: PromptContent,
}
impl PromptMessage {
#[must_use]
pub fn user(text: impl Into<String>) -> Self {
Self {
role: "user",
content: PromptContent {
kind: "text",
text: text.into(),
},
}
}
#[must_use]
pub fn assistant(text: impl Into<String>) -> Self {
Self {
role: "assistant",
content: PromptContent {
kind: "text",
text: text.into(),
},
}
}
}
#[derive(Debug, Serialize)]
pub struct GetPromptResult {
#[serde(rename = "resultType")]
pub result_type: ResultType,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub messages: Vec<PromptMessage>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Value>,
}
#[derive(Debug, Serialize)]
pub struct ServerInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Serialize)]
pub struct DiscoverResult {
#[serde(rename = "resultType")]
pub result_type: ResultType,
#[serde(rename = "supportedVersions")]
pub supported_versions: Vec<&'static str>,
pub capabilities: Value,
#[serde(rename = "_meta")]
pub meta: Value,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn non_object_results_are_wrapped_into_an_object() {
let result = CallToolResult::from(Ok::<Value, String>(json!(42)));
let v = serde_json::to_value(&result).unwrap();
assert_eq!(v["structuredContent"], json!({ "result": 42 }));
assert_eq!(v["content"][0]["text"], r#"{"result":42}"#);
assert_eq!(v["resultType"], "complete");
let result = CallToolResult::from(Ok::<Value, String>(Value::Null));
let v = serde_json::to_value(&result).unwrap();
assert_eq!(v["structuredContent"], json!({ "result": null }));
let result = CallToolResult::from(Ok::<Value, String>(json!([1, 2])));
let v = serde_json::to_value(&result).unwrap();
assert_eq!(v["structuredContent"], json!({ "result": [1, 2] }));
let result = CallToolResult::from(Ok::<Value, String>(json!({ "somme": 12 })));
let v = serde_json::to_value(&result).unwrap();
assert_eq!(v["structuredContent"], json!({ "somme": 12 }));
assert_eq!(v["content"][0]["text"], r#"{"somme":12}"#);
}
#[test]
fn every_result_carries_a_result_type() {
let call = CallToolResult::from(Ok::<Value, String>(json!({})));
let v = serde_json::to_value(&call).unwrap();
assert_eq!(v["resultType"], "complete");
}
}