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, Eq)]
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/// Unknown outer fields are ignored; inner result and error fields are preserved.
68/// Carrier `_meta` is optional; null or invalid values are treated as absent.
69/// Senders must include exactly one outcome; receivers prefer `result` if both are present.
70#[serde_as]
71#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
72#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
73#[serde(untagged)]
74#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = "mcp/message")))]
75#[non_exhaustive]
76pub enum MessageMcpResponse {
77    /// An opaque inner MCP result.
78    Result {
79        /// Required, even if JSON null.
80        #[serde(deserialize_with = "Deserialize::deserialize")]
81        result: Value,
82        /// The _meta property is reserved by ACP to allow clients and agents to attach additional
83        /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
84        /// Implementations MUST NOT make assumptions about values at these keys.
85        /// This is ACP metadata, not the inner MCP message's `_meta`.
86        ///
87        /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
88        #[serde_as(deserialize_as = "DefaultOnError")]
89        #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
90        #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
91        meta: Option<Map<String, Value>>,
92    },
93    /// A structured inner MCP error.
94    Error {
95        /// Required, non-null MCP error object.
96        error: McpError,
97        /// The _meta property is reserved by ACP to allow clients and agents to attach additional
98        /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
99        /// Implementations MUST NOT make assumptions about values at these keys.
100        /// This is ACP metadata, not the inner MCP message's `_meta`.
101        ///
102        /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
103        #[serde_as(deserialize_as = "DefaultOnError")]
104        #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
105        #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
106        meta: Option<Map<String, Value>>,
107    },
108}
109
110impl MessageMcpResponse {
111    /// Wrap any JSON result without interpreting its MCP result type.
112    #[must_use]
113    pub fn success(result: Value) -> Self {
114        Self::Result { result, meta: None }
115    }
116
117    /// Wrap an inner MCP error in a successful outer ACP response.
118    #[must_use]
119    pub fn error(error: McpError) -> Self {
120        Self::Error { error, meta: None }
121    }
122
123    /// Attach optional carrier-level ACP metadata.
124    #[must_use]
125    pub fn meta(mut self, meta: impl IntoOption<Map<String, Value>>) -> Self {
126        match &mut self {
127            Self::Result { meta: field, .. } | Self::Error { meta: field, .. } => {
128                *field = meta.into_option();
129            }
130        }
131        self
132    }
133}
134
135/// **UNSTABLE**
136///
137/// Identifies an inner MCP request active against a server on this ACP connection.
138/// Generated by the caller and preserved unchanged by proxies, independently of
139/// the outer ACP JSON-RPC request ID.
140#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
141#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
142#[serde(transparent)]
143#[from(Arc<str>, String, &'static str)]
144#[non_exhaustive]
145pub struct McpRequestId(pub Arc<str>);
146
147impl McpRequestId {
148    /// Wraps a protocol string as a typed [`McpRequestId`].
149    #[must_use]
150    pub fn new(id: impl Into<Arc<str>>) -> Self {
151        Self(id.into())
152    }
153}
154
155/// **UNSTABLE**
156///
157/// Request parameters for `mcp/message`, sent from consumer to provider.
158#[serde_as]
159#[skip_serializing_none]
160#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
161#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
162#[serde(rename_all = "camelCase")]
163#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_MESSAGE_METHOD_NAME)))]
164#[non_exhaustive]
165pub struct MessageMcpRequest {
166    /// The declared ACP MCP server receiving this request.
167    pub server_id: McpServerAcpId,
168    /// The caller-generated identifier for the inner MCP request.
169    pub request_id: McpRequestId,
170    /// The inner MCP method name.
171    pub method: String,
172    /// Optional inner MCP params; null is equivalent to omission.
173    #[serde(default)]
174    pub params: Option<serde_json::Map<String, serde_json::Value>>,
175    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
176    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
177    /// Implementations MUST NOT make assumptions about values at these keys.
178    /// This is ACP metadata, not the inner MCP message's `_meta`.
179    ///
180    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
181    #[serde_as(deserialize_as = "DefaultOnError")]
182    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
183    #[serde(default)]
184    #[serde(rename = "_meta")]
185    pub meta: Option<Meta>,
186}
187
188impl MessageMcpRequest {
189    /// Builds [`MessageMcpRequest`] with required fields set.
190    #[must_use]
191    pub fn new(
192        server_id: impl Into<McpServerAcpId>,
193        request_id: impl Into<McpRequestId>,
194        method: impl Into<String>,
195    ) -> Self {
196        Self {
197            server_id: server_id.into(),
198            request_id: request_id.into(),
199            method: method.into(),
200            params: None,
201            meta: None,
202        }
203    }
204
205    /// Sets optional inner MCP params.
206    #[must_use]
207    pub fn params(
208        mut self,
209        params: impl IntoOption<serde_json::Map<String, serde_json::Value>>,
210    ) -> Self {
211        self.params = params.into_option();
212        self
213    }
214
215    /// Sets optional ACP extension metadata.
216    #[must_use]
217    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
218        self.meta = meta.into_option();
219        self
220    }
221}
222
223/// **UNSTABLE**
224///
225/// Notification for an active request, sent from provider to consumer.
226/// Includes subscription acknowledgements and updates.
227#[serde_as]
228#[skip_serializing_none]
229#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
230#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
231#[serde(rename_all = "camelCase")]
232#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "agent", "x-method" = MCP_MESSAGE_METHOD_NAME)))]
233#[non_exhaustive]
234pub struct MessageMcpNotification {
235    /// The declared ACP MCP server handling the associated request.
236    pub server_id: McpServerAcpId,
237    /// The identifier of the active inner MCP request.
238    pub request_id: McpRequestId,
239    /// The inner MCP method name.
240    pub method: String,
241    /// Optional inner MCP params; null is equivalent to omission.
242    #[serde(default)]
243    pub params: Option<serde_json::Map<String, serde_json::Value>>,
244    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
245    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
246    /// Implementations MUST NOT make assumptions about values at these keys.
247    /// This is ACP metadata, not the inner MCP message's `_meta`.
248    ///
249    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
250    #[serde_as(deserialize_as = "DefaultOnError")]
251    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
252    #[serde(default)]
253    #[serde(rename = "_meta")]
254    pub meta: Option<Meta>,
255}
256
257impl MessageMcpNotification {
258    /// Builds [`MessageMcpNotification`] with required fields set.
259    #[must_use]
260    pub fn new(
261        server_id: impl Into<McpServerAcpId>,
262        request_id: impl Into<McpRequestId>,
263        method: impl Into<String>,
264    ) -> Self {
265        Self {
266            server_id: server_id.into(),
267            request_id: request_id.into(),
268            method: method.into(),
269            params: None,
270            meta: None,
271        }
272    }
273
274    /// Sets optional inner MCP params.
275    #[must_use]
276    pub fn params(
277        mut self,
278        params: impl IntoOption<serde_json::Map<String, serde_json::Value>>,
279    ) -> Self {
280        self.params = params.into_option();
281        self
282    }
283
284    /// Sets optional ACP extension metadata.
285    #[must_use]
286    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
287        self.meta = meta.into_option();
288        self
289    }
290}
291
292/// Method name for exchanging MCP-over-ACP messages.
293pub(crate) const MCP_MESSAGE_METHOD_NAME: &str = "mcp/message";
294
295#[cfg(test)]
296mod tests {
297    use serde_json::{Value, json};
298
299    use super::{McpError, MessageMcpResponse};
300    use crate::MaybeUndefined;
301
302    #[test]
303    fn result_is_opaque_and_present_even_when_null() {
304        for result in [
305            Value::Null,
306            json!(false),
307            json!(42),
308            json!("opaque"),
309            json!([null, 1]),
310            json!({"resultType": "future", "unknown": {"value": true}}),
311        ] {
312            let response = MessageMcpResponse::success(result.clone());
313            let wire = json!({"result": result});
314            assert_eq!(serde_json::to_value(&response).unwrap(), wire);
315            assert_eq!(
316                serde_json::from_value::<MessageMcpResponse>(wire).unwrap(),
317                response
318            );
319        }
320    }
321
322    #[test]
323    fn error_round_trips_data_and_extensions_without_acp_translation() {
324        for data in [
325            MaybeUndefined::Undefined,
326            MaybeUndefined::Null,
327            MaybeUndefined::Value(json!({"arbitrary": [1, null]})),
328        ] {
329            let mut error = McpError::new(-32000, "inner error");
330            error.data = data.clone();
331            error.extra.insert("future".into(), json!({"key": 1}));
332            let response = MessageMcpResponse::error(error);
333            let wire = serde_json::to_value(&response).unwrap();
334            assert_eq!(wire["error"]["code"], -32000);
335            assert_eq!(wire["error"].get("data").is_some(), !data.is_undefined());
336            assert_eq!(wire["error"]["future"], json!({"key": 1}));
337            assert_eq!(
338                serde_json::from_value::<MessageMcpResponse>(wire).unwrap(),
339                response
340            );
341        }
342        assert_eq!(
343            McpError::new(1, "x").data(Value::Null).data,
344            MaybeUndefined::Null
345        );
346    }
347
348    #[test]
349    fn a_carrier_key_is_required_and_errors_must_be_valid() {
350        for wire in [
351            Value::Null,
352            json!([]),
353            json!({}),
354            json!({"_meta": null}),
355            json!({"unexpected": 1}),
356            json!({"error": null}),
357            json!({"error": 1}),
358            json!({"error": []}),
359            json!({"error": {}}),
360            json!({"error": {"code": 1}}),
361            json!({"error": {"message": "x"}}),
362            json!({"error": {"code": null, "message": "x"}}),
363            json!({"error": {"code": 1, "message": null}}),
364            json!({"error": {"code": 1.5, "message": "x"}}),
365            json!({"error": {"code": "1", "message": "x"}}),
366        ] {
367            assert!(
368                serde_json::from_value::<MessageMcpResponse>(wire.clone()).is_err(),
369                "accepted {wire}"
370            );
371        }
372    }
373
374    #[test]
375    fn result_takes_precedence_when_both_outcome_keys_are_present() {
376        for result in [Value::Null, json!(42), json!({"opaque": [null, true]})] {
377            for error in [
378                Value::Null,
379                json!({"code": 1, "message": "x"}),
380                json!({}),
381                json!({"code": 1, "message": null}),
382            ] {
383                let wire = json!({"result": result, "error": error});
384                assert_eq!(
385                    serde_json::from_value::<MessageMcpResponse>(wire).unwrap(),
386                    MessageMcpResponse::success(result.clone())
387                );
388            }
389        }
390    }
391
392    #[test]
393    fn unknown_outer_fields_are_ignored_and_inner_extensions_are_preserved() {
394        for wire in [
395            json!({"result": null, "unexpected": {"nested": true}}),
396            json!({"result": {"future": [null, {"error": "opaque"}]}, "unexpected": 1}),
397            json!({"error": {"code": 1, "message": "x", "future": [null, true]}, "unexpected": null}),
398        ] {
399            let parsed: MessageMcpResponse = serde_json::from_value(wire.clone()).unwrap();
400            let mut expected = wire;
401            expected.as_object_mut().unwrap().remove("unexpected");
402            assert_eq!(serde_json::to_value(parsed).unwrap(), expected);
403        }
404    }
405
406    #[test]
407    fn carrier_metadata_is_optional_and_invalid_values_are_salvaged() {
408        for outcome in [
409            json!({"result": null}),
410            json!({"error": {"code": 1, "message": "x"}}),
411        ] {
412            let parsed: MessageMcpResponse = serde_json::from_value(outcome.clone()).unwrap();
413            assert_eq!(serde_json::to_value(parsed).unwrap(), outcome);
414            for meta in [
415                Value::Null,
416                json!(true),
417                json!(1),
418                json!("invalid"),
419                json!([]),
420            ] {
421                let mut wire = outcome.clone();
422                wire["_meta"] = meta;
423                let parsed: MessageMcpResponse = serde_json::from_value(wire).unwrap();
424                assert_eq!(serde_json::to_value(parsed).unwrap(), outcome);
425            }
426            let mut wire = outcome;
427            wire["_meta"] = json!({"extension": [null, true]});
428            let parsed: MessageMcpResponse = serde_json::from_value(wire.clone()).unwrap();
429            assert_eq!(serde_json::to_value(parsed).unwrap(), wire);
430        }
431        let meta = json!({"extension": [null, true]})
432            .as_object()
433            .unwrap()
434            .clone();
435        let response =
436            MessageMcpResponse::success(json!({"_meta": {"inner": true}})).meta(meta.clone());
437        assert_eq!(
438            serde_json::to_value(response).unwrap(),
439            json!({"result": {"_meta": {"inner": true}}, "_meta": meta})
440        );
441    }
442
443    #[cfg(feature = "schemars")]
444    #[test]
445    fn response_schema_requires_a_key_without_closing_outer_fields() {
446        let schema = serde_json::to_value(schemars::schema_for!(MessageMcpResponse)).unwrap();
447        assert!(schema.get("not").is_none());
448        let branches = schema["anyOf"].as_array().unwrap();
449        assert_eq!(branches.len(), 2);
450        assert_eq!(branches[0]["required"], json!(["result"]));
451        assert_eq!(branches[1]["required"], json!(["error"]));
452        for branch in branches {
453            assert_ne!(branch.get("additionalProperties"), Some(&json!(false)));
454            assert_eq!(
455                branch["properties"]["_meta"]["x-deserialize-default-on-error"],
456                true
457            );
458        }
459        assert_ne!(schema.get("additionalProperties"), Some(&json!(false)));
460    }
461}