use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
#[serde(default)]
pub id: Option<RequestId>,
pub method: String,
#[serde(default)]
pub params: Option<Value>,
}
#[derive(Debug, Serialize)]
pub struct JsonRpcResponse {
pub jsonrpc: &'static str,
pub id: RequestId,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<RpcError>,
}
#[derive(Debug, Serialize)]
pub struct JsonRpcNotification {
pub jsonrpc: &'static str,
pub method: &'static str,
pub params: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RequestId {
Number(i64),
String(String),
}
#[derive(Debug, Serialize)]
pub struct RpcError {
pub code: i32,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
impl RpcError {
pub const PARSE_ERROR: i32 = -32_700;
pub const INVALID_REQUEST: i32 = -32_600;
pub const METHOD_NOT_FOUND: i32 = -32_601;
pub const INVALID_PARAMS: i32 = -32_602;
pub const INTERNAL_ERROR: i32 = -32_603;
pub const PROTOCOL_HEADER_REJECTED: i32 = -32_020;
pub const UNSUPPORTED_PROTOCOL_VERSION: i32 = -32_022;
#[must_use]
pub fn new(code: i32, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
data: None,
}
}
}
pub(crate) const ADVERTISED_PROTOCOL_VERSION: &str = "2025-11-25";
pub(crate) const SUPPORTED_PROTOCOL_VERSIONS: [&str; 2] = ["2026-07-28", "2025-11-25"];
pub(crate) const META_PROTOCOL_VERSION: &str = "io.modelcontextprotocol/protocolVersion";
pub(crate) const META_CLIENT_CAPABILITIES: &str = "io.modelcontextprotocol/clientCapabilities";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RequestClass {
Legacy,
Partial,
Modern,
}
#[cfg(feature = "http-transport")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RpcHttpStatus {
Ok,
BadRequest,
NotFound,
}
pub(crate) fn supported_versions_value() -> Value {
Value::Array(
SUPPORTED_PROTOCOL_VERSIONS
.iter()
.copied()
.map(|version| Value::String(version.to_owned()))
.collect(),
)
}
fn exempt_method(method: &str) -> bool {
matches!(
method,
"initialize"
| "notifications/initialized"
| "ping"
| "resources/subscribe"
| "resources/unsubscribe"
| "tasks/list"
| "tasks/result"
| "tasks/cancel"
)
}
fn meta_map(msg: &JsonRpcRequest) -> Option<&serde_json::Map<String, Value>> {
msg.params
.as_ref()
.and_then(Value::as_object)
.and_then(|params| params.get("_meta"))
.and_then(Value::as_object)
}
pub(crate) fn request_class(msg: &JsonRpcRequest) -> RequestClass {
if exempt_method(&msg.method) {
return RequestClass::Legacy;
}
let Some(meta) = meta_map(msg) else {
return RequestClass::Legacy;
};
if meta.contains_key(META_PROTOCOL_VERSION) {
RequestClass::Modern
} else if meta.contains_key(META_CLIENT_CAPABILITIES) {
RequestClass::Partial
} else {
RequestClass::Legacy
}
}
pub(crate) fn meta_protocol_version_str(msg: &JsonRpcRequest) -> Option<&str> {
meta_map(msg)?.get(META_PROTOCOL_VERSION)?.as_str()
}
#[cfg(feature = "http-transport")]
fn modern_check_invalid_params(msg: &JsonRpcRequest) -> bool {
let Some(meta) = meta_map(msg) else {
return false;
};
let version_not_string = meta
.get(META_PROTOCOL_VERSION)
.is_some_and(|version| !version.is_string());
let caps_absent = !meta.contains_key(META_CLIENT_CAPABILITIES);
version_not_string || caps_absent
}
#[cfg(feature = "http-transport")]
pub(crate) fn http_status_for(msg: &JsonRpcRequest, error_code: Option<i32>) -> RpcHttpStatus {
let Some(code) = error_code else {
return RpcHttpStatus::Ok;
};
match request_class(msg) {
RequestClass::Partial if code == RpcError::INVALID_PARAMS => RpcHttpStatus::BadRequest,
RequestClass::Modern => match code {
RpcError::PROTOCOL_HEADER_REJECTED | RpcError::UNSUPPORTED_PROTOCOL_VERSION => {
RpcHttpStatus::BadRequest
}
RpcError::INVALID_PARAMS if modern_check_invalid_params(msg) => {
RpcHttpStatus::BadRequest
}
RpcError::METHOD_NOT_FOUND => RpcHttpStatus::NotFound,
_ => RpcHttpStatus::Ok,
},
_ => RpcHttpStatus::Ok,
}
}
impl JsonRpcResponse {
#[must_use]
pub fn ok(id: RequestId, result: Value) -> Self {
Self {
jsonrpc: "2.0",
id,
result: Some(result),
error: None,
}
}
#[must_use]
pub fn err(id: RequestId, error: RpcError) -> Self {
Self {
jsonrpc: "2.0",
id,
result: None,
error: Some(error),
}
}
}
#[derive(Debug, Deserialize)]
pub struct InitializeParams {
#[serde(rename = "protocolVersion")]
pub protocol_version: String,
pub capabilities: ClientCapabilities,
#[serde(rename = "clientInfo")]
pub client_info: ClientInfo,
}
#[derive(Debug, Default, Deserialize)]
pub struct ClientCapabilities {
pub roots: Option<Value>,
pub sampling: Option<Value>,
pub elicitation: Option<Value>,
}
impl ClientCapabilities {
#[must_use]
pub fn supports_elicitation(&self) -> bool {
self.elicitation.is_some()
}
#[must_use]
pub fn supports_sampling(&self) -> bool {
self.sampling.is_some()
}
}
#[derive(Debug, Deserialize)]
pub struct ClientInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Serialize)]
pub struct InitializeResult {
#[serde(rename = "protocolVersion")]
pub protocol_version: &'static str,
pub capabilities: ServerCapabilities,
#[serde(rename = "serverInfo")]
pub server_info: ServerInfo,
pub instructions: &'static str,
}
#[derive(Debug, Serialize)]
pub struct ServerCapabilities {
pub tools: ToolsCapability,
pub logging: LoggingCapability,
pub resources: ResourcesCapability,
pub prompts: PromptsCapability,
pub elicitation: ElicitationCapability,
pub tasks: TasksCapability,
pub sampling: SamplingCapability,
#[serde(skip_serializing_if = "Option::is_none")]
pub experimental: Option<Value>,
}
#[derive(Debug, Serialize)]
pub struct SamplingCapability {}
#[derive(Debug, Serialize)]
pub struct ElicitationCapability {}
#[derive(Debug, Serialize)]
pub struct TasksCapability {}
#[derive(Debug, Serialize)]
pub struct ToolsCapability {
#[serde(rename = "listChanged")]
pub list_changed: bool,
}
#[derive(Debug, Serialize)]
pub struct LoggingCapability {}
#[derive(Debug, Serialize)]
pub struct ResourcesCapability {
pub subscribe: bool,
#[serde(rename = "listChanged")]
pub list_changed: bool,
}
#[derive(Debug, Serialize)]
pub struct PromptsCapability {
#[serde(rename = "listChanged")]
pub list_changed: bool,
}
#[derive(Debug, Serialize)]
pub struct ServerInfo {
pub name: &'static str,
pub version: &'static str,
pub title: &'static str,
}
#[derive(Debug, Clone, Serialize)]
pub struct Tool {
pub name: &'static str,
pub title: &'static str,
pub description: &'static str,
#[serde(rename = "inputSchema")]
pub input_schema: Value,
#[serde(rename = "outputSchema")]
pub output_schema: Value,
pub annotations: ToolAnnotations,
}
#[derive(Debug, Clone, Serialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct ToolAnnotations {
#[serde(rename = "readOnlyHint")]
pub read_only: bool,
#[serde(rename = "destructiveHint")]
pub destructive: bool,
#[serde(rename = "idempotentHint")]
pub idempotent: bool,
#[serde(rename = "openWorldHint")]
pub open_world: bool,
}
#[derive(Debug, Serialize)]
pub struct ToolListResult {
pub tools: Vec<Tool>,
}
#[derive(Debug, Deserialize)]
pub struct ToolCallParams {
pub name: String,
#[serde(default)]
pub arguments: Option<Value>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ContentItem {
#[serde(rename = "type")]
pub kind: &'static str,
pub text: String,
}
impl ContentItem {
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self {
kind: "text",
text: text.into(),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ToolCallResult {
pub content: Vec<ContentItem>,
#[serde(rename = "isError")]
pub is_error: bool,
}
impl ToolCallResult {
#[must_use]
pub fn ok(text: impl Into<String>) -> Self {
Self {
content: vec![ContentItem::text(text)],
is_error: false,
}
}
#[must_use]
pub fn error(text: impl Into<String>) -> Self {
Self {
content: vec![ContentItem::text(text)],
is_error: true,
}
}
}
#[derive(Debug, Serialize)]
pub struct PingResult {}
#[derive(Debug, Deserialize)]
pub struct ResourceSubscribeParams {
pub uri: String,
}
#[derive(Debug, Deserialize)]
pub struct ResourceUnsubscribeParams {
pub uri: String,
}
#[derive(Debug, Serialize)]
pub struct ResourceSubscribeResult {}
#[derive(Debug, Clone, Serialize)]
pub struct Resource {
pub uri: &'static str,
pub name: &'static str,
pub title: &'static str,
pub description: &'static str,
#[serde(rename = "mimeType")]
pub mime_type: &'static str,
}
#[derive(Debug, Clone, Serialize)]
pub struct ResourceTemplate {
#[serde(rename = "uriTemplate")]
pub uri_template: &'static str,
pub name: &'static str,
pub title: &'static str,
pub description: &'static str,
#[serde(rename = "mimeType")]
pub mime_type: &'static str,
}
#[derive(Debug, Serialize)]
pub struct ResourceListResult {
pub resources: Vec<Resource>,
}
#[derive(Debug, Serialize)]
pub struct ResourceTemplateListResult {
#[serde(rename = "resourceTemplates")]
pub resource_templates: Vec<ResourceTemplate>,
}
#[derive(Debug, Deserialize)]
pub struct ResourceReadParams {
pub uri: String,
}
#[derive(Debug, Serialize)]
pub struct ResourceContents {
pub uri: String,
#[serde(rename = "mimeType")]
pub mime_type: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub blob: Option<String>,
}
impl ResourceContents {
#[must_use]
pub fn text(uri: impl Into<String>, mime_type: &'static str, text: impl Into<String>) -> Self {
Self {
uri: uri.into(),
mime_type,
text: Some(text.into()),
blob: None,
}
}
#[must_use]
pub fn blob(uri: impl Into<String>, mime_type: &'static str, blob: impl Into<String>) -> Self {
Self {
uri: uri.into(),
mime_type,
text: None,
blob: Some(blob.into()),
}
}
}
#[derive(Debug, Serialize)]
pub struct ResourceReadResult {
pub contents: Vec<ResourceContents>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Prompt {
pub name: &'static str,
pub title: &'static str,
pub description: &'static str,
pub arguments: Vec<PromptArgument>,
}
#[derive(Debug, Clone, Serialize)]
pub struct PromptArgument {
pub name: &'static str,
pub description: &'static str,
pub required: bool,
}
#[derive(Debug, Serialize)]
pub struct PromptListResult {
pub prompts: Vec<Prompt>,
}
#[derive(Debug, Deserialize)]
pub struct PromptGetParams {
pub name: String,
#[serde(default)]
pub arguments: Option<serde_json::Map<String, serde_json::Value>>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum PromptRole {
User,
Assistant,
}
#[derive(Debug, Serialize)]
pub struct PromptMessage {
pub role: PromptRole,
pub content: PromptContent,
}
#[derive(Debug, Serialize)]
pub struct PromptContent {
#[serde(rename = "type")]
pub kind: &'static str,
pub text: String,
}
impl PromptContent {
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self {
kind: "text",
text: text.into(),
}
}
}
#[derive(Debug, Serialize)]
pub struct PromptGetResult {
pub description: String,
pub messages: Vec<PromptMessage>,
}
pub mod task_status {
pub const WORKING: &str = "working";
pub const DONE: &str = "done";
pub const FAILED: &str = "failed";
pub const CANCELLED: &str = "cancelled";
}
#[derive(Debug, Clone, Serialize)]
pub struct TaskInfo {
#[serde(rename = "taskId")]
pub task_id: String,
pub status: &'static str,
#[serde(rename = "statusMessage", skip_serializing_if = "Option::is_none")]
pub status_message: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct TaskResultParams {
#[serde(rename = "taskId")]
pub task_id: String,
}
#[derive(Debug, Deserialize)]
pub struct TaskCancelParams {
#[serde(rename = "taskId")]
pub task_id: String,
}
#[derive(Debug, Serialize)]
pub struct TasksListResult {
pub tasks: Vec<TaskInfo>,
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum TaskResultResponse {
Complete(ToolCallResult),
Pending { task: TaskInfo },
}
#[derive(Debug, Serialize)]
pub struct TaskCancelResult {}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn request_id_round_trips_number() {
let id = RequestId::Number(42);
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, "42");
}
#[test]
fn request_id_round_trips_string() {
let id = RequestId::String("abc".into());
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, r#""abc""#);
}
#[test]
fn rpc_response_ok_omits_error_field() {
let resp = JsonRpcResponse::ok(RequestId::Number(1), json!({"status": "ok"}));
let v: Value = serde_json::to_value(&resp).unwrap();
assert!(v.get("error").is_none());
assert_eq!(v["result"]["status"], "ok");
}
#[test]
fn rpc_response_err_omits_result_field() {
let resp = JsonRpcResponse::err(
RequestId::Number(1),
RpcError::new(RpcError::METHOD_NOT_FOUND, "not found"),
);
let v: Value = serde_json::to_value(&resp).unwrap();
assert!(v.get("result").is_none());
assert_eq!(v["error"]["code"], RpcError::METHOD_NOT_FOUND);
}
#[test]
fn tool_call_result_ok_is_not_error() {
let r = ToolCallResult::ok("done");
assert!(!r.is_error);
assert_eq!(r.content[0].text, "done");
}
#[test]
fn tool_call_result_error_is_error() {
let r = ToolCallResult::error("boom");
assert!(r.is_error);
assert_eq!(r.content[0].text, "boom");
}
#[test]
fn rpc_error_codes_are_correct_jsonrpc_values() {
assert_eq!(RpcError::PARSE_ERROR, -32_700);
assert_eq!(RpcError::METHOD_NOT_FOUND, -32_601);
}
#[test]
fn supports_elicitation_false_by_default() {
let caps = ClientCapabilities::default();
assert!(!caps.supports_elicitation());
}
#[test]
fn supports_elicitation_true_when_set() {
let caps = ClientCapabilities {
elicitation: Some(json!({})),
..Default::default()
};
assert!(caps.supports_elicitation());
}
#[test]
fn supports_sampling_false_by_default() {
let caps = ClientCapabilities::default();
assert!(!caps.supports_sampling());
}
#[test]
fn supports_sampling_true_when_set() {
let caps = ClientCapabilities {
sampling: Some(json!({"createMessage": {}})),
..Default::default()
};
assert!(caps.supports_sampling());
}
}