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/// 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/// This capability is not part of the spec yet, and may be removed or changed at any point.
125///
126/// Identifies an inner MCP request active against a server on this ACP connection.
127///
128/// Generated by the caller and preserved unchanged by proxies. This is distinct
129/// from the outer ACP JSON-RPC request ID.
130#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
132#[serde(transparent)]
133#[from(Arc<str>, String, &'static str)]
134#[non_exhaustive]
135pub struct McpRequestId(pub Arc<str>);
136
137impl McpRequestId {
138    /// Wraps a protocol string as a typed [`McpRequestId`].
139    #[must_use]
140    pub fn new(id: impl Into<Arc<str>>) -> Self {
141        Self(id.into())
142    }
143}
144
145/// **UNSTABLE**
146///
147/// This capability is not part of the spec yet, and may be removed or changed at any point.
148///
149/// Request parameters for `mcp/message`.
150#[serde_as]
151#[skip_serializing_none]
152#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
154#[serde(rename_all = "camelCase")]
155#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_MESSAGE_METHOD_NAME)))]
156#[non_exhaustive]
157pub struct MessageMcpRequest {
158    /// The declared ACP MCP server receiving this request.
159    pub server_id: McpServerAcpId,
160    /// The caller-generated identifier for the inner MCP request.
161    pub request_id: McpRequestId,
162    /// The inner MCP method name.
163    pub method: String,
164    /// Optional inner MCP params.
165    ///
166    /// If omitted or set to `null`, the inner MCP message has no params.
167    #[serde(default)]
168    pub params: Option<serde_json::Map<String, serde_json::Value>>,
169    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
170    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
171    /// these keys.
172    ///
173    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
174    #[serde_as(deserialize_as = "DefaultOnError")]
175    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
176    #[serde(default)]
177    #[serde(rename = "_meta")]
178    pub meta: Option<Meta>,
179}
180
181impl MessageMcpRequest {
182    /// Builds [`MessageMcpRequest`] with the required request fields set; optional fields start unset or empty.
183    #[must_use]
184    pub fn new(
185        server_id: impl Into<McpServerAcpId>,
186        request_id: impl Into<McpRequestId>,
187        method: impl Into<String>,
188    ) -> Self {
189        Self {
190            server_id: server_id.into(),
191            request_id: request_id.into(),
192            method: method.into(),
193            params: None,
194            meta: None,
195        }
196    }
197
198    /// Optional inner MCP params.
199    ///
200    /// If omitted or set to `null`, the inner MCP message has no params.
201    #[must_use]
202    pub fn params(
203        mut self,
204        params: impl IntoOption<serde_json::Map<String, serde_json::Value>>,
205    ) -> Self {
206        self.params = params.into_option();
207        self
208    }
209
210    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
211    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
212    /// these keys.
213    ///
214    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
215    #[must_use]
216    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
217        self.meta = meta.into_option();
218        self
219    }
220}
221
222/// **UNSTABLE**
223///
224/// This capability is not part of the spec yet, and may be removed or changed at any point.
225///
226/// Notification parameters for `mcp/message`.
227///
228/// Sent by the provider to the consumer for an active request (including
229/// subscription acknowledgements and updates); the outer envelope has no `id`.
230#[serde_as]
231#[skip_serializing_none]
232#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
233#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
234#[serde(rename_all = "camelCase")]
235#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "agent", "x-method" = MCP_MESSAGE_METHOD_NAME)))]
236#[non_exhaustive]
237pub struct MessageMcpNotification {
238    /// The declared ACP MCP server handling the associated request.
239    pub server_id: McpServerAcpId,
240    /// The identifier of the active inner MCP request.
241    pub request_id: McpRequestId,
242    /// The inner MCP method name.
243    pub method: String,
244    /// Optional inner MCP params.
245    ///
246    /// If omitted or set to `null`, the inner MCP message has no params.
247    #[serde(default)]
248    pub params: Option<serde_json::Map<String, serde_json::Value>>,
249    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
250    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
251    /// these keys.
252    ///
253    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
254    #[serde_as(deserialize_as = "DefaultOnError")]
255    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
256    #[serde(default)]
257    #[serde(rename = "_meta")]
258    pub meta: Option<Meta>,
259}
260
261impl MessageMcpNotification {
262    /// Builds [`MessageMcpNotification`] with the required notification fields set; optional fields start unset or empty.
263    #[must_use]
264    pub fn new(
265        server_id: impl Into<McpServerAcpId>,
266        request_id: impl Into<McpRequestId>,
267        method: impl Into<String>,
268    ) -> Self {
269        Self {
270            server_id: server_id.into(),
271            request_id: request_id.into(),
272            method: method.into(),
273            params: None,
274            meta: None,
275        }
276    }
277
278    /// Optional inner MCP params.
279    ///
280    /// If omitted or set to `null`, the inner MCP message has no params.
281    #[must_use]
282    pub fn params(
283        mut self,
284        params: impl IntoOption<serde_json::Map<String, serde_json::Value>>,
285    ) -> Self {
286        self.params = params.into_option();
287        self
288    }
289
290    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
291    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
292    /// these keys.
293    ///
294    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
295    #[must_use]
296    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
297        self.meta = meta.into_option();
298        self
299    }
300}
301
302/// Method name for exchanging MCP-over-ACP messages.
303pub(crate) const MCP_MESSAGE_METHOD_NAME: &str = "mcp/message";
304
305#[cfg(test)]
306mod tests {
307    use serde_json::{Value, json};
308
309    use super::{McpError, MessageMcpResponse};
310    use crate::MaybeUndefined;
311
312    #[test]
313    fn result_is_opaque_and_present_even_when_null() {
314        for result in [
315            Value::Null,
316            json!(false),
317            json!(42),
318            json!("opaque"),
319            json!([null, 1]),
320            json!({"resultType": "future", "unknown": {"value": true}}),
321        ] {
322            let response = MessageMcpResponse::success(result.clone());
323            let wire = json!({"result": result});
324            assert_eq!(serde_json::to_value(&response).unwrap(), wire);
325            assert_eq!(
326                serde_json::from_value::<MessageMcpResponse>(wire).unwrap(),
327                response
328            );
329        }
330    }
331
332    #[test]
333    fn error_round_trips_data_and_extensions_without_acp_translation() {
334        for data in [
335            MaybeUndefined::Undefined,
336            MaybeUndefined::Null,
337            MaybeUndefined::Value(json!({"arbitrary": [1, null]})),
338        ] {
339            let mut error = McpError::new(-32000, "inner error");
340            error.data = data.clone();
341            error.extra.insert("future".into(), json!({"key": 1}));
342            let response = MessageMcpResponse::error(error);
343            let wire = serde_json::to_value(&response).unwrap();
344            assert_eq!(wire["error"]["code"], -32000);
345            assert_eq!(wire["error"].get("data").is_some(), !data.is_undefined());
346            assert_eq!(wire["error"]["future"], json!({"key": 1}));
347            assert_eq!(
348                serde_json::from_value::<MessageMcpResponse>(wire).unwrap(),
349                response
350            );
351        }
352        assert_eq!(
353            McpError::new(1, "x").data(Value::Null).data,
354            MaybeUndefined::Null
355        );
356    }
357
358    #[test]
359    fn only_one_non_null_carrier_key_is_valid() {
360        for wire in [
361            Value::Null,
362            json!({}),
363            json!({"_meta": null}),
364            json!({"result": 1, "error": {"code": 1, "message": "x"}}),
365            json!({"result": 1, "error": null}),
366            json!({"error": null}),
367            json!({"error": 1}),
368            json!({"error": {}}),
369            json!({"error": {"code": null, "message": "x"}}),
370            json!({"error": {"code": 1, "message": null}}),
371            json!({"error": {"code": 1.5, "message": "x"}}),
372            json!({"unexpected": 1, "result": 1}),
373        ] {
374            assert!(
375                serde_json::from_value::<MessageMcpResponse>(wire.clone()).is_err(),
376                "accepted {wire}"
377            );
378        }
379    }
380
381    #[test]
382    fn carrier_metadata_is_optional_and_null_means_absent() {
383        for wire in [
384            json!({"result": null, "_meta": null}),
385            json!({"error": {"code": 1, "message": "x"}, "_meta": null}),
386        ] {
387            let parsed: MessageMcpResponse = serde_json::from_value(wire).unwrap();
388            assert!(serde_json::to_value(parsed).unwrap().get("_meta").is_none());
389        }
390        let meta = json!({"extension": [null, true]})
391            .as_object()
392            .unwrap()
393            .clone();
394        let response =
395            MessageMcpResponse::success(json!({"_meta": {"inner": true}})).meta(meta.clone());
396        assert_eq!(
397            serde_json::to_value(response).unwrap(),
398            json!({"result": {"_meta": {"inner": true}}, "_meta": meta})
399        );
400    }
401}