use std::sync::Arc;
use derive_more::{Display, From};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use serde_with::{DefaultOnError, serde_as, skip_serializing_none};
use crate::{IntoOption, MaybeUndefined};
use super::{McpServerAcpId, Meta};
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[non_exhaustive]
pub struct McpError {
pub code: i32,
pub message: String,
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub data: MaybeUndefined<Value>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
impl McpError {
#[must_use]
pub fn new(code: i32, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
data: MaybeUndefined::Undefined,
extra: Map::new(),
}
}
#[must_use]
pub fn data(mut self, data: Value) -> Self {
self.data = if data.is_null() {
MaybeUndefined::Null
} else {
MaybeUndefined::Value(data)
};
self
}
}
#[serde_as]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = "mcp/message")))]
#[non_exhaustive]
pub enum MessageMcpResponse {
Result {
#[serde(deserialize_with = "Deserialize::deserialize")]
result: Value,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
meta: Option<Map<String, Value>>,
},
Error {
error: McpError,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
meta: Option<Map<String, Value>>,
},
}
impl MessageMcpResponse {
#[must_use]
pub fn success(result: Value) -> Self {
Self::Result { result, meta: None }
}
#[must_use]
pub fn error(error: McpError) -> Self {
Self::Error { error, meta: None }
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Map<String, Value>>) -> Self {
match &mut self {
Self::Result { meta: field, .. } | Self::Error { meta: field, .. } => {
*field = meta.into_option();
}
}
self
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
#[serde(transparent)]
#[from(Arc<str>, String, &'static str)]
#[non_exhaustive]
pub struct McpRequestId(pub Arc<str>);
impl McpRequestId {
#[must_use]
pub fn new(id: impl Into<Arc<str>>) -> Self {
Self(id.into())
}
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_MESSAGE_METHOD_NAME)))]
#[non_exhaustive]
pub struct MessageMcpRequest {
pub server_id: McpServerAcpId,
pub request_id: McpRequestId,
pub method: String,
#[serde(default)]
pub params: Option<serde_json::Map<String, serde_json::Value>>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl MessageMcpRequest {
#[must_use]
pub fn new(
server_id: impl Into<McpServerAcpId>,
request_id: impl Into<McpRequestId>,
method: impl Into<String>,
) -> Self {
Self {
server_id: server_id.into(),
request_id: request_id.into(),
method: method.into(),
params: None,
meta: None,
}
}
#[must_use]
pub fn params(
mut self,
params: impl IntoOption<serde_json::Map<String, serde_json::Value>>,
) -> Self {
self.params = params.into_option();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "agent", "x-method" = MCP_MESSAGE_METHOD_NAME)))]
#[non_exhaustive]
pub struct MessageMcpNotification {
pub server_id: McpServerAcpId,
pub request_id: McpRequestId,
pub method: String,
#[serde(default)]
pub params: Option<serde_json::Map<String, serde_json::Value>>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl MessageMcpNotification {
#[must_use]
pub fn new(
server_id: impl Into<McpServerAcpId>,
request_id: impl Into<McpRequestId>,
method: impl Into<String>,
) -> Self {
Self {
server_id: server_id.into(),
request_id: request_id.into(),
method: method.into(),
params: None,
meta: None,
}
}
#[must_use]
pub fn params(
mut self,
params: impl IntoOption<serde_json::Map<String, serde_json::Value>>,
) -> Self {
self.params = params.into_option();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
pub(crate) const MCP_MESSAGE_METHOD_NAME: &str = "mcp/message";
#[cfg(test)]
mod tests {
use serde_json::{Value, json};
use super::{McpError, MessageMcpResponse};
use crate::MaybeUndefined;
#[test]
fn result_is_opaque_and_present_even_when_null() {
for result in [
Value::Null,
json!(false),
json!(42),
json!("opaque"),
json!([null, 1]),
json!({"resultType": "future", "unknown": {"value": true}}),
] {
let response = MessageMcpResponse::success(result.clone());
let wire = json!({"result": result});
assert_eq!(serde_json::to_value(&response).unwrap(), wire);
assert_eq!(
serde_json::from_value::<MessageMcpResponse>(wire).unwrap(),
response
);
}
}
#[test]
fn error_round_trips_data_and_extensions_without_acp_translation() {
for data in [
MaybeUndefined::Undefined,
MaybeUndefined::Null,
MaybeUndefined::Value(json!({"arbitrary": [1, null]})),
] {
let mut error = McpError::new(-32000, "inner error");
error.data = data.clone();
error.extra.insert("future".into(), json!({"key": 1}));
let response = MessageMcpResponse::error(error);
let wire = serde_json::to_value(&response).unwrap();
assert_eq!(wire["error"]["code"], -32000);
assert_eq!(wire["error"].get("data").is_some(), !data.is_undefined());
assert_eq!(wire["error"]["future"], json!({"key": 1}));
assert_eq!(
serde_json::from_value::<MessageMcpResponse>(wire).unwrap(),
response
);
}
assert_eq!(
McpError::new(1, "x").data(Value::Null).data,
MaybeUndefined::Null
);
}
#[test]
fn a_carrier_key_is_required_and_errors_must_be_valid() {
for wire in [
Value::Null,
json!([]),
json!({}),
json!({"_meta": null}),
json!({"unexpected": 1}),
json!({"error": null}),
json!({"error": 1}),
json!({"error": []}),
json!({"error": {}}),
json!({"error": {"code": 1}}),
json!({"error": {"message": "x"}}),
json!({"error": {"code": null, "message": "x"}}),
json!({"error": {"code": 1, "message": null}}),
json!({"error": {"code": 1.5, "message": "x"}}),
json!({"error": {"code": "1", "message": "x"}}),
] {
assert!(
serde_json::from_value::<MessageMcpResponse>(wire.clone()).is_err(),
"accepted {wire}"
);
}
}
#[test]
fn result_takes_precedence_when_both_outcome_keys_are_present() {
for result in [Value::Null, json!(42), json!({"opaque": [null, true]})] {
for error in [
Value::Null,
json!({"code": 1, "message": "x"}),
json!({}),
json!({"code": 1, "message": null}),
] {
let wire = json!({"result": result, "error": error});
assert_eq!(
serde_json::from_value::<MessageMcpResponse>(wire).unwrap(),
MessageMcpResponse::success(result.clone())
);
}
}
}
#[test]
fn unknown_outer_fields_are_ignored_and_inner_extensions_are_preserved() {
for wire in [
json!({"result": null, "unexpected": {"nested": true}}),
json!({"result": {"future": [null, {"error": "opaque"}]}, "unexpected": 1}),
json!({"error": {"code": 1, "message": "x", "future": [null, true]}, "unexpected": null}),
] {
let parsed: MessageMcpResponse = serde_json::from_value(wire.clone()).unwrap();
let mut expected = wire;
expected.as_object_mut().unwrap().remove("unexpected");
assert_eq!(serde_json::to_value(parsed).unwrap(), expected);
}
}
#[test]
fn carrier_metadata_is_optional_and_invalid_values_are_salvaged() {
for outcome in [
json!({"result": null}),
json!({"error": {"code": 1, "message": "x"}}),
] {
let parsed: MessageMcpResponse = serde_json::from_value(outcome.clone()).unwrap();
assert_eq!(serde_json::to_value(parsed).unwrap(), outcome);
for meta in [
Value::Null,
json!(true),
json!(1),
json!("invalid"),
json!([]),
] {
let mut wire = outcome.clone();
wire["_meta"] = meta;
let parsed: MessageMcpResponse = serde_json::from_value(wire).unwrap();
assert_eq!(serde_json::to_value(parsed).unwrap(), outcome);
}
let mut wire = outcome;
wire["_meta"] = json!({"extension": [null, true]});
let parsed: MessageMcpResponse = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(serde_json::to_value(parsed).unwrap(), wire);
}
let meta = json!({"extension": [null, true]})
.as_object()
.unwrap()
.clone();
let response =
MessageMcpResponse::success(json!({"_meta": {"inner": true}})).meta(meta.clone());
assert_eq!(
serde_json::to_value(response).unwrap(),
json!({"result": {"_meta": {"inner": true}}, "_meta": meta})
);
}
#[cfg(feature = "schemars")]
#[test]
fn response_schema_requires_a_key_without_closing_outer_fields() {
let schema = serde_json::to_value(schemars::schema_for!(MessageMcpResponse)).unwrap();
assert!(schema.get("not").is_none());
let branches = schema["anyOf"].as_array().unwrap();
assert_eq!(branches.len(), 2);
assert_eq!(branches[0]["required"], json!(["result"]));
assert_eq!(branches[1]["required"], json!(["error"]));
for branch in branches {
assert_ne!(branch.get("additionalProperties"), Some(&json!(false)));
assert_eq!(
branch["properties"]["_meta"]["x-deserialize-default-on-error"],
true
);
}
assert_ne!(schema.get("additionalProperties"), Some(&json!(false)));
}
}