Skip to main content

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