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, deny_unknown_fields)]
#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = "mcp/message")))]
#[non_exhaustive]
pub enum MessageMcpResponse {
Result {
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 only_one_non_null_carrier_key_is_valid() {
for wire in [
Value::Null,
json!({}),
json!({"_meta": null}),
json!({"result": 1, "error": {"code": 1, "message": "x"}}),
json!({"result": 1, "error": null}),
json!({"error": null}),
json!({"error": 1}),
json!({"error": {}}),
json!({"error": {"code": null, "message": "x"}}),
json!({"error": {"code": 1, "message": null}}),
json!({"error": {"code": 1.5, "message": "x"}}),
json!({"unexpected": 1, "result": 1}),
] {
assert!(
serde_json::from_value::<MessageMcpResponse>(wire.clone()).is_err(),
"accepted {wire}"
);
}
}
#[test]
fn carrier_metadata_is_optional_and_null_means_absent() {
for wire in [
json!({"result": null, "_meta": null}),
json!({"error": {"code": 1, "message": "x"}, "_meta": null}),
] {
let parsed: MessageMcpResponse = serde_json::from_value(wire).unwrap();
assert!(serde_json::to_value(parsed).unwrap().get("_meta").is_none());
}
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})
);
}
}