Skip to main content

agent_client_protocol_schema/v2/
mcp.rs

1//! MCP-over-ACP transport types.
2
3use std::sync::Arc;
4
5use derive_more::{Display, From};
6use serde::{Deserialize, Serialize};
7use serde_json::{Map, Value};
8use serde_with::{DefaultOnError, serde_as, skip_serializing_none};
9
10use crate::{IntoOption, MaybeUndefined};
11
12use super::{McpServerAcpId, Meta};
13
14/// **UNSTABLE**
15///
16/// An inner MCP error, distinct from an outer ACP binding or runtime error.
17///
18/// `code` and `message` are required and non-null. `data` is optional;
19/// explicit `null` is preserved separately from an omitted key.
20#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
22#[non_exhaustive]
23pub struct McpError {
24    /// Inner MCP error code; never an ACP error code.
25    pub code: i32,
26    /// Inner MCP error message.
27    pub message: String,
28    /// Optional error data; explicit null is retained.
29    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
30    pub data: MaybeUndefined<Value>,
31    /// Additional fields on the inner MCP error object.
32    #[serde(flatten)]
33    pub extra: Map<String, Value>,
34}
35
36impl McpError {
37    /// Construct an inner MCP error without data.
38    #[must_use]
39    pub fn new(code: i32, message: impl Into<String>) -> Self {
40        Self {
41            code,
42            message: message.into(),
43            data: MaybeUndefined::Undefined,
44            extra: Map::new(),
45        }
46    }
47
48    /// Set data, preserving explicit JSON null.
49    #[must_use]
50    pub fn data(mut self, data: Value) -> Self {
51        self.data = if data.is_null() {
52            MaybeUndefined::Null
53        } else {
54            MaybeUndefined::Value(data)
55        };
56        self
57    }
58}
59
60/// **UNSTABLE**
61///
62/// The successful outer ACP `mcp/message` response carries exactly one
63/// inner MCP outcome: an opaque result (including JSON null), or an MCP error.
64/// Outer ACP errors are reserved for binding and runtime failures.
65///
66/// Both branches require their carrier key. An error must be a non-null object.
67/// Carrier `_meta` is optional; null is equivalent to omission.
68#[serde_as]
69#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
71#[serde(untagged, deny_unknown_fields)]
72#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = "mcp/message")))]
73#[non_exhaustive]
74pub enum MessageMcpResponse {
75    /// An opaque inner MCP result.
76    Result {
77        /// Required, even if JSON null.
78        result: Value,
79        /// Optional ACP carrier metadata.
80        #[serde_as(deserialize_as = "DefaultOnError")]
81        #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
82        #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
83        meta: Option<Map<String, Value>>,
84    },
85    /// A structured inner MCP error.
86    Error {
87        /// Required, non-null MCP error object.
88        error: McpError,
89        /// Optional ACP carrier metadata.
90        #[serde_as(deserialize_as = "DefaultOnError")]
91        #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
92        #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
93        meta: Option<Map<String, Value>>,
94    },
95}
96
97impl MessageMcpResponse {
98    /// Wrap any JSON result without interpreting its MCP result type.
99    #[must_use]
100    pub fn success(result: Value) -> Self {
101        Self::Result { result, meta: None }
102    }
103
104    /// Wrap an inner MCP error in a successful outer ACP response.
105    #[must_use]
106    pub fn error(error: McpError) -> Self {
107        Self::Error { error, meta: None }
108    }
109
110    /// Attach optional carrier-level ACP metadata.
111    #[must_use]
112    pub fn meta(mut self, meta: impl IntoOption<Map<String, Value>>) -> Self {
113        match &mut self {
114            Self::Result { meta: field, .. } | Self::Error { meta: field, .. } => {
115                *field = meta.into_option();
116            }
117        }
118        self
119    }
120}
121
122/// **UNSTABLE**
123///
124/// Identifies an inner MCP request active against a server on this ACP connection.
125/// Generated by the caller and preserved unchanged by proxies, independently of
126/// the outer ACP JSON-RPC request ID.
127#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
128#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
129#[serde(transparent)]
130#[from(Arc<str>, String, &'static str)]
131#[non_exhaustive]
132pub struct McpRequestId(pub Arc<str>);
133
134impl McpRequestId {
135    /// Wraps a protocol string as a typed [`McpRequestId`].
136    #[must_use]
137    pub fn new(id: impl Into<Arc<str>>) -> Self {
138        Self(id.into())
139    }
140}
141
142/// **UNSTABLE**
143///
144/// Request parameters for `mcp/message`, sent from consumer to provider.
145#[serde_as]
146#[skip_serializing_none]
147#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
149#[serde(rename_all = "camelCase")]
150#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_MESSAGE_METHOD_NAME)))]
151#[non_exhaustive]
152pub struct MessageMcpRequest {
153    /// The declared ACP MCP server receiving this request.
154    pub server_id: McpServerAcpId,
155    /// The caller-generated identifier for the inner MCP request.
156    pub request_id: McpRequestId,
157    /// The inner MCP method name.
158    pub method: String,
159    /// Optional inner MCP params; null is equivalent to omission.
160    #[serde(default)]
161    pub params: Option<serde_json::Map<String, serde_json::Value>>,
162    /// ACP extension metadata (not inner MCP params._meta); null is equivalent to omission.
163    #[serde_as(deserialize_as = "DefaultOnError")]
164    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
165    #[serde(default)]
166    #[serde(rename = "_meta")]
167    pub meta: Option<Meta>,
168}
169
170impl MessageMcpRequest {
171    /// Builds [`MessageMcpRequest`] with required fields set.
172    #[must_use]
173    pub fn new(
174        server_id: impl Into<McpServerAcpId>,
175        request_id: impl Into<McpRequestId>,
176        method: impl Into<String>,
177    ) -> Self {
178        Self {
179            server_id: server_id.into(),
180            request_id: request_id.into(),
181            method: method.into(),
182            params: None,
183            meta: None,
184        }
185    }
186
187    /// Sets optional inner MCP params.
188    #[must_use]
189    pub fn params(
190        mut self,
191        params: impl IntoOption<serde_json::Map<String, serde_json::Value>>,
192    ) -> Self {
193        self.params = params.into_option();
194        self
195    }
196
197    /// Sets optional ACP extension metadata.
198    #[must_use]
199    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
200        self.meta = meta.into_option();
201        self
202    }
203}
204
205/// **UNSTABLE**
206///
207/// Notification for an active request, sent from provider to consumer.
208/// Includes subscription acknowledgements and updates.
209#[serde_as]
210#[skip_serializing_none]
211#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
212#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
213#[serde(rename_all = "camelCase")]
214#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "agent", "x-method" = MCP_MESSAGE_METHOD_NAME)))]
215#[non_exhaustive]
216pub struct MessageMcpNotification {
217    /// The declared ACP MCP server handling the associated request.
218    pub server_id: McpServerAcpId,
219    /// The identifier of the active inner MCP request.
220    pub request_id: McpRequestId,
221    /// The inner MCP method name.
222    pub method: String,
223    /// Optional inner MCP params; null is equivalent to omission.
224    #[serde(default)]
225    pub params: Option<serde_json::Map<String, serde_json::Value>>,
226    /// ACP extension metadata (not inner MCP params._meta); null is equivalent to omission.
227    #[serde_as(deserialize_as = "DefaultOnError")]
228    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
229    #[serde(default)]
230    #[serde(rename = "_meta")]
231    pub meta: Option<Meta>,
232}
233
234impl MessageMcpNotification {
235    /// Builds [`MessageMcpNotification`] with required fields set.
236    #[must_use]
237    pub fn new(
238        server_id: impl Into<McpServerAcpId>,
239        request_id: impl Into<McpRequestId>,
240        method: impl Into<String>,
241    ) -> Self {
242        Self {
243            server_id: server_id.into(),
244            request_id: request_id.into(),
245            method: method.into(),
246            params: None,
247            meta: None,
248        }
249    }
250
251    /// Sets optional inner MCP params.
252    #[must_use]
253    pub fn params(
254        mut self,
255        params: impl IntoOption<serde_json::Map<String, serde_json::Value>>,
256    ) -> Self {
257        self.params = params.into_option();
258        self
259    }
260
261    /// Sets optional ACP extension metadata.
262    #[must_use]
263    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
264        self.meta = meta.into_option();
265        self
266    }
267}
268
269/// Method name for exchanging MCP-over-ACP messages.
270pub(crate) const MCP_MESSAGE_METHOD_NAME: &str = "mcp/message";
271
272#[cfg(test)]
273mod tests {
274    use serde_json::{Value, json};
275
276    use super::{McpError, MessageMcpResponse};
277    use crate::MaybeUndefined;
278
279    #[test]
280    fn result_is_opaque_and_present_even_when_null() {
281        for result in [
282            Value::Null,
283            json!(false),
284            json!(42),
285            json!("opaque"),
286            json!([null, 1]),
287            json!({"resultType": "future", "unknown": {"value": true}}),
288        ] {
289            let response = MessageMcpResponse::success(result.clone());
290            let wire = json!({"result": result});
291            assert_eq!(serde_json::to_value(&response).unwrap(), wire);
292            assert_eq!(
293                serde_json::from_value::<MessageMcpResponse>(wire).unwrap(),
294                response
295            );
296        }
297    }
298
299    #[test]
300    fn error_round_trips_data_and_extensions_without_acp_translation() {
301        for data in [
302            MaybeUndefined::Undefined,
303            MaybeUndefined::Null,
304            MaybeUndefined::Value(json!({"arbitrary": [1, null]})),
305        ] {
306            let mut error = McpError::new(-32000, "inner error");
307            error.data = data.clone();
308            error.extra.insert("future".into(), json!({"key": 1}));
309            let response = MessageMcpResponse::error(error);
310            let wire = serde_json::to_value(&response).unwrap();
311            assert_eq!(wire["error"]["code"], -32000);
312            assert_eq!(wire["error"].get("data").is_some(), !data.is_undefined());
313            assert_eq!(wire["error"]["future"], json!({"key": 1}));
314            assert_eq!(
315                serde_json::from_value::<MessageMcpResponse>(wire).unwrap(),
316                response
317            );
318        }
319        assert_eq!(
320            McpError::new(1, "x").data(Value::Null).data,
321            MaybeUndefined::Null
322        );
323    }
324
325    #[test]
326    fn only_one_non_null_carrier_key_is_valid() {
327        for wire in [
328            Value::Null,
329            json!({}),
330            json!({"_meta": null}),
331            json!({"result": 1, "error": {"code": 1, "message": "x"}}),
332            json!({"result": 1, "error": null}),
333            json!({"error": null}),
334            json!({"error": 1}),
335            json!({"error": {}}),
336            json!({"error": {"code": null, "message": "x"}}),
337            json!({"error": {"code": 1, "message": null}}),
338            json!({"error": {"code": 1.5, "message": "x"}}),
339            json!({"unexpected": 1, "result": 1}),
340        ] {
341            assert!(
342                serde_json::from_value::<MessageMcpResponse>(wire.clone()).is_err(),
343                "accepted {wire}"
344            );
345        }
346    }
347
348    #[test]
349    fn carrier_metadata_is_optional_and_null_means_absent() {
350        for wire in [
351            json!({"result": null, "_meta": null}),
352            json!({"error": {"code": 1, "message": "x"}, "_meta": null}),
353        ] {
354            let parsed: MessageMcpResponse = serde_json::from_value(wire).unwrap();
355            assert!(serde_json::to_value(parsed).unwrap().get("_meta").is_none());
356        }
357        let meta = json!({"extension": [null, true]})
358            .as_object()
359            .unwrap()
360            .clone();
361        let response =
362            MessageMcpResponse::success(json!({"_meta": {"inner": true}})).meta(meta.clone());
363        assert_eq!(
364            serde_json::to_value(response).unwrap(),
365            json!({"result": {"_meta": {"inner": true}}, "_meta": meta})
366        );
367    }
368}