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/// 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/// Identifies an inner MCP request active against a server on this ACP connection.
128/// Generated by the caller and preserved unchanged by proxies, independently of
129/// 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/// Request parameters for `mcp/message`, sent from consumer to provider.
148#[serde_as]
149#[skip_serializing_none]
150#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
151#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
152#[serde(rename_all = "camelCase")]
153#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_MESSAGE_METHOD_NAME)))]
154#[non_exhaustive]
155pub struct MessageMcpRequest {
156    /// The declared ACP MCP server receiving this request.
157    pub server_id: McpServerAcpId,
158    /// The caller-generated identifier for the inner MCP request.
159    pub request_id: McpRequestId,
160    /// The inner MCP method name.
161    pub method: String,
162    /// Optional inner MCP params; null is equivalent to omission.
163    #[serde(default)]
164    pub params: Option<serde_json::Map<String, serde_json::Value>>,
165    /// ACP extension metadata (not inner MCP params._meta); null is equivalent to omission.
166    #[serde_as(deserialize_as = "DefaultOnError")]
167    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
168    #[serde(default)]
169    #[serde(rename = "_meta")]
170    pub meta: Option<Meta>,
171}
172
173impl MessageMcpRequest {
174    /// Builds [`MessageMcpRequest`] with required fields set.
175    #[must_use]
176    pub fn new(
177        server_id: impl Into<McpServerAcpId>,
178        request_id: impl Into<McpRequestId>,
179        method: impl Into<String>,
180    ) -> Self {
181        Self {
182            server_id: server_id.into(),
183            request_id: request_id.into(),
184            method: method.into(),
185            params: None,
186            meta: None,
187        }
188    }
189
190    /// Sets optional inner MCP params.
191    #[must_use]
192    pub fn params(
193        mut self,
194        params: impl IntoOption<serde_json::Map<String, serde_json::Value>>,
195    ) -> Self {
196        self.params = params.into_option();
197        self
198    }
199
200    /// Sets optional ACP extension metadata.
201    #[must_use]
202    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
203        self.meta = meta.into_option();
204        self
205    }
206}
207
208/// **UNSTABLE**
209///
210/// Notification for an active request, sent from provider to consumer.
211/// Includes subscription acknowledgements and updates.
212#[serde_as]
213#[skip_serializing_none]
214#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
215#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
216#[serde(rename_all = "camelCase")]
217#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "agent", "x-method" = MCP_MESSAGE_METHOD_NAME)))]
218#[non_exhaustive]
219pub struct MessageMcpNotification {
220    /// The declared ACP MCP server handling the associated request.
221    pub server_id: McpServerAcpId,
222    /// The identifier of the active inner MCP request.
223    pub request_id: McpRequestId,
224    /// The inner MCP method name.
225    pub method: String,
226    /// Optional inner MCP params; null is equivalent to omission.
227    #[serde(default)]
228    pub params: Option<serde_json::Map<String, serde_json::Value>>,
229    /// ACP extension metadata (not inner MCP params._meta); null is equivalent to omission.
230    #[serde_as(deserialize_as = "DefaultOnError")]
231    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
232    #[serde(default)]
233    #[serde(rename = "_meta")]
234    pub meta: Option<Meta>,
235}
236
237impl MessageMcpNotification {
238    /// Builds [`MessageMcpNotification`] with required fields set.
239    #[must_use]
240    pub fn new(
241        server_id: impl Into<McpServerAcpId>,
242        request_id: impl Into<McpRequestId>,
243        method: impl Into<String>,
244    ) -> Self {
245        Self {
246            server_id: server_id.into(),
247            request_id: request_id.into(),
248            method: method.into(),
249            params: None,
250            meta: None,
251        }
252    }
253
254    /// Sets optional inner MCP params.
255    #[must_use]
256    pub fn params(
257        mut self,
258        params: impl IntoOption<serde_json::Map<String, serde_json::Value>>,
259    ) -> Self {
260        self.params = params.into_option();
261        self
262    }
263
264    /// Sets optional ACP extension metadata.
265    #[must_use]
266    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
267        self.meta = meta.into_option();
268        self
269    }
270}
271
272/// Method name for exchanging MCP-over-ACP messages.
273pub(crate) const MCP_MESSAGE_METHOD_NAME: &str = "mcp/message";
274
275#[cfg(test)]
276mod tests {
277    use serde_json::{Value, json};
278
279    use super::{McpError, MessageMcpResponse};
280    use crate::MaybeUndefined;
281
282    #[test]
283    fn result_is_opaque_and_present_even_when_null() {
284        for result in [
285            Value::Null,
286            json!(false),
287            json!(42),
288            json!("opaque"),
289            json!([null, 1]),
290            json!({"resultType": "future", "unknown": {"value": true}}),
291        ] {
292            let response = MessageMcpResponse::success(result.clone());
293            let wire = json!({"result": result});
294            assert_eq!(serde_json::to_value(&response).unwrap(), wire);
295            assert_eq!(
296                serde_json::from_value::<MessageMcpResponse>(wire).unwrap(),
297                response
298            );
299        }
300    }
301
302    #[test]
303    fn error_round_trips_data_and_extensions_without_acp_translation() {
304        for data in [
305            MaybeUndefined::Undefined,
306            MaybeUndefined::Null,
307            MaybeUndefined::Value(json!({"arbitrary": [1, null]})),
308        ] {
309            let mut error = McpError::new(-32000, "inner error");
310            error.data = data.clone();
311            error.extra.insert("future".into(), json!({"key": 1}));
312            let response = MessageMcpResponse::error(error);
313            let wire = serde_json::to_value(&response).unwrap();
314            assert_eq!(wire["error"]["code"], -32000);
315            assert_eq!(wire["error"].get("data").is_some(), !data.is_undefined());
316            assert_eq!(wire["error"]["future"], json!({"key": 1}));
317            assert_eq!(
318                serde_json::from_value::<MessageMcpResponse>(wire).unwrap(),
319                response
320            );
321        }
322        assert_eq!(
323            McpError::new(1, "x").data(Value::Null).data,
324            MaybeUndefined::Null
325        );
326    }
327
328    #[test]
329    fn a_carrier_key_is_required_and_errors_must_be_valid() {
330        for wire in [
331            Value::Null,
332            json!([]),
333            json!({}),
334            json!({"_meta": null}),
335            json!({"unexpected": 1}),
336            json!({"error": null}),
337            json!({"error": 1}),
338            json!({"error": []}),
339            json!({"error": {}}),
340            json!({"error": {"code": 1}}),
341            json!({"error": {"message": "x"}}),
342            json!({"error": {"code": null, "message": "x"}}),
343            json!({"error": {"code": 1, "message": null}}),
344            json!({"error": {"code": 1.5, "message": "x"}}),
345            json!({"error": {"code": "1", "message": "x"}}),
346        ] {
347            assert!(
348                serde_json::from_value::<MessageMcpResponse>(wire.clone()).is_err(),
349                "accepted {wire}"
350            );
351        }
352    }
353
354    #[test]
355    fn result_takes_precedence_when_both_outcome_keys_are_present() {
356        for result in [Value::Null, json!(42), json!({"opaque": [null, true]})] {
357            for error in [
358                Value::Null,
359                json!({"code": 1, "message": "x"}),
360                json!({}),
361                json!({"code": 1, "message": null}),
362            ] {
363                let wire = json!({"result": result, "error": error});
364                assert_eq!(
365                    serde_json::from_value::<MessageMcpResponse>(wire).unwrap(),
366                    MessageMcpResponse::success(result.clone())
367                );
368            }
369        }
370    }
371
372    #[test]
373    fn unknown_outer_fields_are_ignored_and_inner_extensions_are_preserved() {
374        for wire in [
375            json!({"result": null, "unexpected": {"nested": true}}),
376            json!({"result": {"future": [null, {"error": "opaque"}]}, "unexpected": 1}),
377            json!({"error": {"code": 1, "message": "x", "future": [null, true]}, "unexpected": null}),
378        ] {
379            let parsed: MessageMcpResponse = serde_json::from_value(wire.clone()).unwrap();
380            let mut expected = wire;
381            expected.as_object_mut().unwrap().remove("unexpected");
382            assert_eq!(serde_json::to_value(parsed).unwrap(), expected);
383        }
384    }
385
386    #[test]
387    fn carrier_metadata_is_optional_and_invalid_values_are_salvaged() {
388        for outcome in [
389            json!({"result": null}),
390            json!({"error": {"code": 1, "message": "x"}}),
391        ] {
392            let parsed: MessageMcpResponse = serde_json::from_value(outcome.clone()).unwrap();
393            assert_eq!(serde_json::to_value(parsed).unwrap(), outcome);
394            for meta in [
395                Value::Null,
396                json!(true),
397                json!(1),
398                json!("invalid"),
399                json!([]),
400            ] {
401                let mut wire = outcome.clone();
402                wire["_meta"] = meta;
403                let parsed: MessageMcpResponse = serde_json::from_value(wire).unwrap();
404                assert_eq!(serde_json::to_value(parsed).unwrap(), outcome);
405            }
406            let mut wire = outcome;
407            wire["_meta"] = json!({"extension": [null, true]});
408            let parsed: MessageMcpResponse = serde_json::from_value(wire.clone()).unwrap();
409            assert_eq!(serde_json::to_value(parsed).unwrap(), wire);
410        }
411        let meta = json!({"extension": [null, true]})
412            .as_object()
413            .unwrap()
414            .clone();
415        let response =
416            MessageMcpResponse::success(json!({"_meta": {"inner": true}})).meta(meta.clone());
417        assert_eq!(
418            serde_json::to_value(response).unwrap(),
419            json!({"result": {"_meta": {"inner": true}}, "_meta": meta})
420        );
421    }
422
423    #[cfg(feature = "schemars")]
424    #[test]
425    fn response_schema_requires_a_key_without_closing_outer_fields() {
426        let schema = serde_json::to_value(schemars::schema_for!(MessageMcpResponse)).unwrap();
427        assert!(schema.get("not").is_none());
428        let branches = schema["anyOf"].as_array().unwrap();
429        assert_eq!(branches.len(), 2);
430        assert_eq!(branches[0]["required"], json!(["result"]));
431        assert_eq!(branches[1]["required"], json!(["error"]));
432        for branch in branches {
433            assert_ne!(branch.get("additionalProperties"), Some(&json!(false)));
434            assert_eq!(
435                branch["properties"]["_meta"]["x-deserialize-default-on-error"],
436                true
437            );
438        }
439        assert_ne!(schema.get("additionalProperties"), Some(&json!(false)));
440    }
441}