diff --git a/agent-client-protocol-schema/src/serde_util.rs b/agent-client-protocol-schema/src/serde_util.rs index c8cb74fd7..62aa92866 100644 --- a/agent-client-protocol-schema/src/serde_util.rs +++ b/agent-client-protocol-schema/src/serde_util.rs @@ -135,10 +135,6 @@ mod default_on_null_tests { $check::(); $check::(); } - #[cfg(feature = "unstable_mcp_over_acp")] - { - $check::(); - } #[cfg(feature = "unstable_protocol_v2")] { @@ -163,10 +159,6 @@ mod default_on_null_tests { $check::(); $check::(); } - #[cfg(feature = "unstable_mcp_over_acp")] - { - $check::(); - } } }; } @@ -355,8 +347,9 @@ mod default_on_null_tests { #[cfg(feature = "unstable_mcp_over_acp")] { - let mcp: v1::MessageMcpResponse = serde_json::from_value(Value::Null).unwrap(); - assert_eq!(serde_json::to_value(mcp).unwrap(), Value::Null); + let mcp: v1::MessageMcpResponse = + serde_json::from_value(json!({"result": null})).unwrap(); + assert_eq!(serde_json::to_value(mcp).unwrap(), json!({"result": null})); } #[cfg(feature = "unstable_protocol_v2")] @@ -367,8 +360,8 @@ mod default_on_null_tests { #[cfg(feature = "unstable_mcp_over_acp")] { let mcp: crate::v2::MessageMcpResponse = - serde_json::from_value(Value::Null).unwrap(); - assert_eq!(serde_json::to_value(mcp).unwrap(), Value::Null); + serde_json::from_value(json!({"result": null})).unwrap(); + assert_eq!(serde_json::to_value(mcp).unwrap(), json!({"result": null})); } } } diff --git a/agent-client-protocol-schema/src/v1/agent.rs b/agent-client-protocol-schema/src/v1/agent.rs index ccc984b6e..a311b3d77 100644 --- a/agent-client-protocol-schema/src/v1/agent.rs +++ b/agent-client-protocol-schema/src/v1/agent.rs @@ -18,9 +18,7 @@ use super::{ }; #[cfg(feature = "unstable_mcp_over_acp")] -use super::mcp::{ - MCP_MESSAGE_METHOD_NAME, MessageMcpNotification, MessageMcpRequest, MessageMcpResponse, -}; +use super::mcp::{MCP_MESSAGE_METHOD_NAME, MessageMcpNotification}; #[cfg(feature = "unstable_nes")] use super::{ @@ -2796,8 +2794,7 @@ impl McpServerSse { /// Unique identifier for an MCP server using the ACP transport. /// /// The value is opaque and generated by the ACP component providing the MCP server. It is -/// used by `mcp/connect` to route connection requests back to the component that declared the -/// server. +/// used by `mcp/message` to route requests to the component that declared the server. #[cfg(feature = "unstable_mcp_over_acp")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)] @@ -2822,7 +2819,7 @@ impl McpServerAcpId { /// ACP transport configuration for MCP. /// /// The MCP server is provided by an ACP component and communicates over the ACP channel -/// using `mcp/connect`, `mcp/message`, and `mcp/disconnect`. +/// using `mcp/message`. #[serde_as] #[skip_serializing_none] #[cfg(feature = "unstable_mcp_over_acp")] @@ -4962,13 +4959,6 @@ pub enum ClientRequest { /// The agent must cancel any ongoing work and then free up any resources /// associated with the NES session. CloseNesRequest(CloseNesRequest), - /// **UNSTABLE** - /// - /// This capability is not part of the spec yet, and may be removed or changed at any point. - /// - /// Exchanges an MCP-over-ACP message. - #[cfg(feature = "unstable_mcp_over_acp")] - MessageMcpRequest(MessageMcpRequest), /// Handles extension method requests from the client. /// /// Extension methods provide a way to add custom functionality while maintaining @@ -5009,8 +4999,6 @@ impl ClientRequest { Self::SuggestNesRequest(_) => AGENT_METHOD_NAMES.nes_suggest, #[cfg(feature = "unstable_nes")] Self::CloseNesRequest(_) => AGENT_METHOD_NAMES.nes_close, - #[cfg(feature = "unstable_mcp_over_acp")] - Self::MessageMcpRequest(_) => AGENT_METHOD_NAMES.mcp_message, Self::ExtMethodRequest(ext_request) => &ext_request.method, } } @@ -5076,9 +5064,6 @@ pub enum AgentResponse { CloseNesResponse(#[serde(default)] CloseNesResponse), /// Successful result returned by an extension method outside the core ACP method set. ExtMethodResponse(ExtResponse), - /// Successful result returned by an MCP-over-ACP `mcp/message` request. - #[cfg(feature = "unstable_mcp_over_acp")] - MessageMcpResponse(MessageMcpResponse), } /// All possible notifications that a client can send to an agent. @@ -5420,21 +5405,36 @@ mod test_serialization { #[cfg(feature = "unstable_mcp_over_acp")] #[test] fn test_client_mcp_message_method_names() { + use serde_json::json; + assert_eq!(AGENT_METHOD_NAMES.mcp_message, "mcp/message"); + let notification = + MessageMcpNotification::new("server-1", "req-1", "notifications/progress"); assert_eq!( - ClientRequest::MessageMcpRequest(MessageMcpRequest::new("conn-1", "tools/list")) - .method(), + ClientNotification::MessageMcpNotification(notification.clone()).method(), "mcp/message" ); assert_eq!( - ClientNotification::MessageMcpNotification(MessageMcpNotification::new( - "conn-1", - "notifications/progress" - )) - .method(), - "mcp/message" + serde_json::to_value(notification).unwrap(), + json!({ + "serverId": "server-1", + "requestId": "req-1", + "method": "notifications/progress" + }) ); + let notification: MessageMcpNotification = serde_json::from_value(json!({ + "serverId": "server-1", "requestId": "req-1", "method": "notifications/progress", + "params": null, "_meta": null + })) + .unwrap(); + assert_eq!(notification.params, None); + assert_eq!(notification.meta, None); + for key in ["serverId", "requestId", "method"] { + let mut value = json!({"serverId":"server-1", "requestId":"req-1", "method":"notifications/progress"}); + value.as_object_mut().unwrap().remove(key); + assert!(serde_json::from_value::(value).is_err()); + } } #[cfg(all(feature = "unstable_mcp_over_acp", feature = "schemars"))] diff --git a/agent-client-protocol-schema/src/v1/client.rs b/agent-client-protocol-schema/src/v1/client.rs index d06808b30..7b116bb9d 100644 --- a/agent-client-protocol-schema/src/v1/client.rs +++ b/agent-client-protocol-schema/src/v1/client.rs @@ -23,11 +23,7 @@ use super::{ use super::{PlanCapabilities, PlanRemoved, PlanUpdate}; #[cfg(feature = "unstable_mcp_over_acp")] -use super::mcp::{ - ConnectMcpRequest, ConnectMcpResponse, DisconnectMcpRequest, DisconnectMcpResponse, - MCP_CONNECT_METHOD_NAME, MCP_DISCONNECT_METHOD_NAME, MCP_MESSAGE_METHOD_NAME, - MessageMcpNotification, MessageMcpRequest, MessageMcpResponse, -}; +use super::mcp::{MCP_MESSAGE_METHOD_NAME, MessageMcpRequest, MessageMcpResponse}; #[cfg(feature = "unstable_nes")] use super::{ClientNesCapabilities, PositionEncodingKind}; @@ -2649,15 +2645,9 @@ pub struct ClientMethodNames { pub terminal_wait_for_exit: &'static str, /// Method for killing a terminal. pub terminal_kill: &'static str, - /// Method for opening an MCP-over-ACP connection. - #[cfg(feature = "unstable_mcp_over_acp")] - pub mcp_connect: &'static str, /// Method for exchanging MCP-over-ACP messages. #[cfg(feature = "unstable_mcp_over_acp")] pub mcp_message: &'static str, - /// Method for closing an MCP-over-ACP connection. - #[cfg(feature = "unstable_mcp_over_acp")] - pub mcp_disconnect: &'static str, /// Method for elicitation. pub elicitation_create: &'static str, /// Notification for elicitation completion. @@ -2676,11 +2666,7 @@ pub const CLIENT_METHOD_NAMES: ClientMethodNames = ClientMethodNames { terminal_wait_for_exit: TERMINAL_WAIT_FOR_EXIT_METHOD_NAME, terminal_kill: TERMINAL_KILL_METHOD_NAME, #[cfg(feature = "unstable_mcp_over_acp")] - mcp_connect: MCP_CONNECT_METHOD_NAME, - #[cfg(feature = "unstable_mcp_over_acp")] mcp_message: MCP_MESSAGE_METHOD_NAME, - #[cfg(feature = "unstable_mcp_over_acp")] - mcp_disconnect: MCP_DISCONNECT_METHOD_NAME, elicitation_create: ELICITATION_CREATE_METHOD_NAME, elicitation_complete: ELICITATION_COMPLETE_NOTIFICATION, }; @@ -2806,23 +2792,9 @@ pub enum AgentRequest { /// /// This capability is not part of the spec yet, and may be removed or changed at any point. /// - /// Opens an MCP-over-ACP connection. - #[cfg(feature = "unstable_mcp_over_acp")] - ConnectMcpRequest(ConnectMcpRequest), - /// **UNSTABLE** - /// - /// This capability is not part of the spec yet, and may be removed or changed at any point. - /// /// Exchanges an MCP-over-ACP message. #[cfg(feature = "unstable_mcp_over_acp")] MessageMcpRequest(MessageMcpRequest), - /// **UNSTABLE** - /// - /// This capability is not part of the spec yet, and may be removed or changed at any point. - /// - /// Closes an MCP-over-ACP connection. - #[cfg(feature = "unstable_mcp_over_acp")] - DisconnectMcpRequest(DisconnectMcpRequest), /// Handles extension method requests from the agent. /// /// Allows the Agent to send an arbitrary request that is not part of the ACP spec. @@ -2848,11 +2820,7 @@ impl AgentRequest { Self::KillTerminalRequest(_) => CLIENT_METHOD_NAMES.terminal_kill, Self::CreateElicitationRequest(_) => CLIENT_METHOD_NAMES.elicitation_create, #[cfg(feature = "unstable_mcp_over_acp")] - Self::ConnectMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_connect, - #[cfg(feature = "unstable_mcp_over_acp")] Self::MessageMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_message, - #[cfg(feature = "unstable_mcp_over_acp")] - Self::DisconnectMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_disconnect, Self::ExtMethodRequest(ext_request) => &ext_request.method, } } @@ -2888,12 +2856,6 @@ pub enum ClientResponse { KillTerminalResponse(#[serde(default)] KillTerminalResponse), /// Successful result returned for a `elicitation/create` request. CreateElicitationResponse(CreateElicitationResponse), - /// Successful result returned for a `mcp/connect` request. - #[cfg(feature = "unstable_mcp_over_acp")] - ConnectMcpResponse(ConnectMcpResponse), - /// Successful result returned for a `mcp/disconnect` request. - #[cfg(feature = "unstable_mcp_over_acp")] - DisconnectMcpResponse(#[serde(default)] DisconnectMcpResponse), /// Successful result returned by an MCP-over-ACP `mcp/message` request. #[cfg(feature = "unstable_mcp_over_acp")] MessageMcpResponse(MessageMcpResponse), @@ -2930,13 +2892,6 @@ pub enum AgentNotification { /// /// See protocol docs: [Elicitation](https://agentclientprotocol.com/protocol/elicitation#url-completion) CompleteElicitationNotification(CompleteElicitationNotification), - /// **UNSTABLE** - /// - /// This capability is not part of the spec yet, and may be removed or changed at any point. - /// - /// Receives an MCP-over-ACP notification. - #[cfg(feature = "unstable_mcp_over_acp")] - MessageMcpNotification(MessageMcpNotification), /// Handles extension notifications from the agent. /// /// Allows the Agent to send an arbitrary notification that is not part of the ACP spec. @@ -2954,8 +2909,6 @@ impl AgentNotification { match self { Self::SessionNotification(_) => CLIENT_METHOD_NAMES.session_update, Self::CompleteElicitationNotification(_) => CLIENT_METHOD_NAMES.elicitation_complete, - #[cfg(feature = "unstable_mcp_over_acp")] - Self::MessageMcpNotification(_) => CLIENT_METHOD_NAMES.mcp_message, Self::ExtNotification(ext_notification) => &ext_notification.method, } } @@ -3613,72 +3566,51 @@ mod tests { let params: serde_json::Map = [("cursor".to_string(), json!("abc"))].into_iter().collect(); - assert_eq!(CLIENT_METHOD_NAMES.mcp_connect, "mcp/connect"); assert_eq!(CLIENT_METHOD_NAMES.mcp_message, "mcp/message"); - assert_eq!(CLIENT_METHOD_NAMES.mcp_disconnect, "mcp/disconnect"); - assert_eq!( - AgentRequest::ConnectMcpRequest(ConnectMcpRequest::new("server-1")).method(), - "mcp/connect" - ); - assert_eq!( - AgentRequest::MessageMcpRequest(MessageMcpRequest::new("conn-1", "tools/list")) - .method(), - "mcp/message" - ); - assert_eq!( - AgentRequest::DisconnectMcpRequest(DisconnectMcpRequest::new("conn-1")).method(), - "mcp/disconnect" - ); - assert_eq!( - AgentNotification::MessageMcpNotification(MessageMcpNotification::new( - "conn-1", - "notifications/progress" + AgentRequest::MessageMcpRequest(MessageMcpRequest::new( + "server-1", + "req-1", + "tools/list" )) .method(), "mcp/message" ); - assert_eq!( - serde_json::to_value(ConnectMcpRequest::new("server-1")).unwrap(), - json!({ "serverId": "server-1" }) - ); - assert_eq!( - serde_json::to_value(ConnectMcpResponse::new("conn-1")).unwrap(), - json!({ "connectionId": "conn-1" }) - ); - assert_eq!( - serde_json::to_value(MessageMcpRequest::new("conn-1", "tools/list").params(params)) - .unwrap(), + serde_json::to_value( + MessageMcpRequest::new("server-1", "req-1", "tools/list").params(params) + ) + .unwrap(), json!({ - "connectionId": "conn-1", + "serverId": "server-1", + "requestId": "req-1", "method": "tools/list", "params": { "cursor": "abc" } }) ); - assert_eq!( - serde_json::to_value(DisconnectMcpRequest::new("conn-1")).unwrap(), - json!({ "connectionId": "conn-1" }) - ); - assert_eq!( - serde_json::to_value(MessageMcpNotification::new( - "conn-1", - "notifications/progress" - )) - .unwrap(), - json!({ - "connectionId": "conn-1", - "method": "notifications/progress" - }) - ); let request_with_null_params: MessageMcpRequest = serde_json::from_value(json!({ - "connectionId": "conn-1", + "serverId": "server-1", + "requestId": "req-1", "method": "tools/list", - "params": null + "params": null, + "_meta": null })) .unwrap(); assert_eq!(request_with_null_params.params, None); + assert_eq!(request_with_null_params.meta, None); + for key in ["serverId", "requestId", "method"] { + let mut value = + json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"}); + value.as_object_mut().unwrap().remove(key); + assert!(serde_json::from_value::(value).is_err()); + } + for key in ["serverId", "requestId", "method"] { + let mut value = + json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"}); + value[key] = serde_json::Value::Null; + assert!(serde_json::from_value::(value).is_err()); + } } #[test] diff --git a/agent-client-protocol-schema/src/v1/mcp.rs b/agent-client-protocol-schema/src/v1/mcp.rs index 0430748aa..0cbcba67f 100644 --- a/agent-client-protocol-schema/src/v1/mcp.rs +++ b/agent-client-protocol-schema/src/v1/mcp.rs @@ -4,78 +4,117 @@ use std::sync::Arc; use derive_more::{Display, From}; use serde::{Deserialize, Serialize}; -use serde_json::value::RawValue; +use serde_json::{Map, Value}; use serde_with::{DefaultOnError, serde_as, skip_serializing_none}; -use crate::IntoOption; +use crate::{IntoOption, MaybeUndefined}; use super::{McpServerAcpId, Meta}; /// **UNSTABLE** /// -/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// An inner MCP error, distinct from an outer ACP binding or runtime error. /// -/// A unique identifier for an active MCP-over-ACP connection. +/// `code` and `message` are required and non-null. `data` is optional; +/// explicit `null` is preserved separately from an omitted key. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)] -#[serde(transparent)] -#[from(Arc, String, &'static str)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[non_exhaustive] -pub struct McpConnectionId(pub Arc); +pub struct McpError { + /// Inner MCP error code; never an ACP error code. + pub code: i32, + /// Inner MCP error message. + pub message: String, + /// Optional error data; explicit null is retained. + #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")] + pub data: MaybeUndefined, + /// Additional fields on the inner MCP error object. + #[serde(flatten)] + pub extra: Map, +} -impl McpConnectionId { - /// Wraps a protocol string as a typed [`McpConnectionId`]. +impl McpError { + /// Construct an inner MCP error without data. #[must_use] - pub fn new(id: impl Into>) -> Self { - Self(id.into()) + pub fn new(code: i32, message: impl Into) -> Self { + Self { + code, + message: message.into(), + data: MaybeUndefined::Undefined, + extra: Map::new(), + } + } + + /// Set data, preserving explicit JSON null. + #[must_use] + pub fn data(mut self, data: Value) -> Self { + self.data = if data.is_null() { + MaybeUndefined::Null + } else { + MaybeUndefined::Value(data) + }; + self } } /// **UNSTABLE** /// -/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// The successful outer ACP `mcp/message` response carries exactly one +/// inner MCP outcome: an opaque result (including JSON null), or an MCP error. +/// Outer ACP errors are reserved for binding and runtime failures. /// -/// Request parameters for `mcp/connect`. +/// Both branches require their carrier key. An error must be a non-null object. +/// Carrier `_meta` is optional; null is equivalent to omission. #[serde_as] -#[skip_serializing_none] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_CONNECT_METHOD_NAME)))] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(untagged, deny_unknown_fields)] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = "mcp/message")))] #[non_exhaustive] -pub struct ConnectMcpRequest { - /// The ACP MCP server ID that was provided by the component declaring the MCP server. - pub server_id: McpServerAcpId, - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - #[serde(rename = "_meta")] - pub meta: Option, +pub enum MessageMcpResponse { + /// An opaque inner MCP result. + Result { + /// Required, even if JSON null. + result: Value, + /// Optional ACP carrier metadata. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + meta: Option>, + }, + /// A structured inner MCP error. + Error { + /// Required, non-null MCP error object. + error: McpError, + /// Optional ACP carrier metadata. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + meta: Option>, + }, } -impl ConnectMcpRequest { - /// Builds [`ConnectMcpRequest`] with the required request fields set; optional fields start unset or empty. +impl MessageMcpResponse { + /// Wrap any JSON result without interpreting its MCP result type. #[must_use] - pub fn new(server_id: impl Into) -> Self { - Self { - server_id: server_id.into(), - meta: None, - } + pub fn success(result: Value) -> Self { + Self::Result { result, meta: None } } - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + /// Wrap an inner MCP error in a successful outer ACP response. #[must_use] - pub fn meta(mut self, meta: impl IntoOption) -> Self { - self.meta = meta.into_option(); + pub fn error(error: McpError) -> Self { + Self::Error { error, meta: None } + } + + /// Attach optional carrier-level ACP metadata. + #[must_use] + pub fn meta(mut self, meta: impl IntoOption>) -> Self { + match &mut self { + Self::Result { meta: field, .. } | Self::Error { meta: field, .. } => { + *field = meta.into_option(); + } + } self } } @@ -84,48 +123,22 @@ impl ConnectMcpRequest { /// /// This capability is not part of the spec yet, and may be removed or changed at any point. /// -/// Response to `mcp/connect`. -#[serde_as] -#[skip_serializing_none] +/// Identifies an inner MCP request active against a server on this ACP connection. +/// +/// Generated by the caller and preserved unchanged by proxies. This is distinct +/// from the outer ACP JSON-RPC request ID. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_CONNECT_METHOD_NAME)))] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)] +#[serde(transparent)] +#[from(Arc, String, &'static str)] #[non_exhaustive] -pub struct ConnectMcpResponse { - /// The unique identifier for this MCP-over-ACP connection. - pub connection_id: McpConnectionId, - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - #[serde(rename = "_meta")] - pub meta: Option, -} - -impl ConnectMcpResponse { - /// Builds [`ConnectMcpResponse`] with the required response fields set; optional fields start unset or empty. - #[must_use] - pub fn new(connection_id: impl Into) -> Self { - Self { - connection_id: connection_id.into(), - meta: None, - } - } +pub struct McpRequestId(pub Arc); - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) +impl McpRequestId { + /// Wraps a protocol string as a typed [`McpRequestId`]. #[must_use] - pub fn meta(mut self, meta: impl IntoOption) -> Self { - self.meta = meta.into_option(); - self + pub fn new(id: impl Into>) -> Self { + Self(id.into()) } } @@ -139,11 +152,13 @@ impl ConnectMcpResponse { #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "both", "x-method" = MCP_MESSAGE_METHOD_NAME)))] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_MESSAGE_METHOD_NAME)))] #[non_exhaustive] pub struct MessageMcpRequest { - /// The MCP-over-ACP connection this message is sent on. - pub connection_id: McpConnectionId, + /// The declared ACP MCP server receiving this request. + pub server_id: McpServerAcpId, + /// The caller-generated identifier for the inner MCP request. + pub request_id: McpRequestId, /// The inner MCP method name. pub method: String, /// Optional inner MCP params. @@ -166,9 +181,14 @@ pub struct MessageMcpRequest { impl MessageMcpRequest { /// Builds [`MessageMcpRequest`] with the required request fields set; optional fields start unset or empty. #[must_use] - pub fn new(connection_id: impl Into, method: impl Into) -> Self { + pub fn new( + server_id: impl Into, + request_id: impl Into, + method: impl Into, + ) -> Self { Self { - connection_id: connection_id.into(), + server_id: server_id.into(), + request_id: request_id.into(), method: method.into(), params: None, meta: None, @@ -205,25 +225,25 @@ impl MessageMcpRequest { /// /// Notification parameters for `mcp/message`. /// -/// This is used when the wrapped MCP message is a notification and the outer JSON-RPC -/// envelope has no `id`. +/// Sent by the provider to the consumer for an active request (including +/// subscription acknowledgements and updates); the outer envelope has no `id`. #[serde_as] #[skip_serializing_none] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "both", "x-method" = MCP_MESSAGE_METHOD_NAME)))] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "agent", "x-method" = MCP_MESSAGE_METHOD_NAME)))] #[non_exhaustive] pub struct MessageMcpNotification { - /// The MCP-over-ACP connection this message is sent on. - pub connection_id: McpConnectionId, + /// The declared ACP MCP server handling the associated request. + pub server_id: McpServerAcpId, + /// The identifier of the active inner MCP request. + pub request_id: McpRequestId, /// The inner MCP method name. pub method: String, /// Optional inner MCP params. /// /// If omitted or set to `null`, the inner MCP message has no params. - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] #[serde(default)] pub params: Option>, /// The _meta property is reserved by ACP to allow clients and agents to attach additional @@ -241,9 +261,14 @@ pub struct MessageMcpNotification { impl MessageMcpNotification { /// Builds [`MessageMcpNotification`] with the required notification fields set; optional fields start unset or empty. #[must_use] - pub fn new(connection_id: impl Into, method: impl Into) -> Self { + pub fn new( + server_id: impl Into, + request_id: impl Into, + method: impl Into, + ) -> Self { Self { - connection_id: connection_id.into(), + server_id: server_id.into(), + request_id: request_id.into(), method: method.into(), params: None, meta: None, @@ -274,128 +299,103 @@ impl MessageMcpNotification { } } -/// **UNSTABLE** -/// -/// This capability is not part of the spec yet, and may be removed or changed at any point. -/// -/// Response to `mcp/message`. -/// -/// This is the inner MCP response result payload. Any JSON value is valid. -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize, From)] -#[serde(transparent)] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "both", "x-method" = MCP_MESSAGE_METHOD_NAME)))] -#[non_exhaustive] -pub struct MessageMcpResponse( - #[cfg_attr(feature = "schemars", schemars(with = "serde_json::Value"))] pub Arc, -); +/// Method name for exchanging MCP-over-ACP messages. +pub(crate) const MCP_MESSAGE_METHOD_NAME: &str = "mcp/message"; -impl MessageMcpResponse { - /// Builds [`MessageMcpResponse`] with the required response fields set; optional fields start unset or empty. - #[must_use] - pub fn new(result: Arc) -> Self { - Self(result) - } -} +#[cfg(test)] +mod tests { + use serde_json::{Value, json}; -/// **UNSTABLE** -/// -/// This capability is not part of the spec yet, and may be removed or changed at any point. -/// -/// Request parameters for `mcp/disconnect`. -#[serde_as] -#[skip_serializing_none] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_DISCONNECT_METHOD_NAME)))] -#[non_exhaustive] -pub struct DisconnectMcpRequest { - /// The MCP-over-ACP connection to close. - pub connection_id: McpConnectionId, - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - #[serde(rename = "_meta")] - pub meta: Option, -} + use super::{McpError, MessageMcpResponse}; + use crate::MaybeUndefined; -impl DisconnectMcpRequest { - /// Builds [`DisconnectMcpRequest`] with the required request fields set; optional fields start unset or empty. - #[must_use] - pub fn new(connection_id: impl Into) -> Self { - Self { - connection_id: connection_id.into(), - meta: None, + #[test] + fn result_is_opaque_and_present_even_when_null() { + for result in [ + Value::Null, + json!(false), + json!(42), + json!("opaque"), + json!([null, 1]), + json!({"resultType": "future", "unknown": {"value": true}}), + ] { + let response = MessageMcpResponse::success(result.clone()); + let wire = json!({"result": result}); + assert_eq!(serde_json::to_value(&response).unwrap(), wire); + assert_eq!( + serde_json::from_value::(wire).unwrap(), + response + ); } } - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[must_use] - pub fn meta(mut self, meta: impl IntoOption) -> Self { - self.meta = meta.into_option(); - self - } -} - -crate::serde_util::default_on_null! { - /// **UNSTABLE** - /// - /// This capability is not part of the spec yet, and may be removed or changed at any point. - /// - /// Response to `mcp/disconnect`. - #[serde_as] - #[skip_serializing_none] - #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] - #[derive(Default, Debug, Clone, Serialize, PartialEq, Eq)] - #[serde(rename_all = "camelCase")] - #[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_DISCONNECT_METHOD_NAME)))] - #[non_exhaustive] - pub struct DisconnectMcpResponse { - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - #[serde(rename = "_meta")] - pub meta: Option, + #[test] + fn error_round_trips_data_and_extensions_without_acp_translation() { + for data in [ + MaybeUndefined::Undefined, + MaybeUndefined::Null, + MaybeUndefined::Value(json!({"arbitrary": [1, null]})), + ] { + let mut error = McpError::new(-32000, "inner error"); + error.data = data.clone(); + error.extra.insert("future".into(), json!({"key": 1})); + let response = MessageMcpResponse::error(error); + let wire = serde_json::to_value(&response).unwrap(); + assert_eq!(wire["error"]["code"], -32000); + assert_eq!(wire["error"].get("data").is_some(), !data.is_undefined()); + assert_eq!(wire["error"]["future"], json!({"key": 1})); + assert_eq!( + serde_json::from_value::(wire).unwrap(), + response + ); + } + assert_eq!( + McpError::new(1, "x").data(Value::Null).data, + MaybeUndefined::Null + ); } -} -impl DisconnectMcpResponse { - /// Builds [`DisconnectMcpResponse`] with the required response fields set; optional fields start unset or empty. - #[must_use] - pub fn new() -> Self { - Self::default() + #[test] + fn only_one_non_null_carrier_key_is_valid() { + for wire in [ + Value::Null, + json!({}), + json!({"_meta": null}), + json!({"result": 1, "error": {"code": 1, "message": "x"}}), + json!({"result": 1, "error": null}), + json!({"error": null}), + json!({"error": 1}), + json!({"error": {}}), + json!({"error": {"code": null, "message": "x"}}), + json!({"error": {"code": 1, "message": null}}), + json!({"error": {"code": 1.5, "message": "x"}}), + json!({"unexpected": 1, "result": 1}), + ] { + assert!( + serde_json::from_value::(wire.clone()).is_err(), + "accepted {wire}" + ); + } } - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[must_use] - pub fn meta(mut self, meta: impl IntoOption) -> Self { - self.meta = meta.into_option(); - self + #[test] + fn carrier_metadata_is_optional_and_null_means_absent() { + for wire in [ + json!({"result": null, "_meta": null}), + json!({"error": {"code": 1, "message": "x"}, "_meta": null}), + ] { + let parsed: MessageMcpResponse = serde_json::from_value(wire).unwrap(); + assert!(serde_json::to_value(parsed).unwrap().get("_meta").is_none()); + } + let meta = json!({"extension": [null, true]}) + .as_object() + .unwrap() + .clone(); + let response = + MessageMcpResponse::success(json!({"_meta": {"inner": true}})).meta(meta.clone()); + assert_eq!( + serde_json::to_value(response).unwrap(), + json!({"result": {"_meta": {"inner": true}}, "_meta": meta}) + ); } } - -/// Method name for opening an MCP-over-ACP connection. -pub(crate) const MCP_CONNECT_METHOD_NAME: &str = "mcp/connect"; -/// Method name for exchanging MCP-over-ACP messages. -pub(crate) const MCP_MESSAGE_METHOD_NAME: &str = "mcp/message"; -/// Method name for closing an MCP-over-ACP connection. -pub(crate) const MCP_DISCONNECT_METHOD_NAME: &str = "mcp/disconnect"; diff --git a/agent-client-protocol-schema/src/v2/agent.rs b/agent-client-protocol-schema/src/v2/agent.rs index 1e6b9688b..25a4534ae 100644 --- a/agent-client-protocol-schema/src/v2/agent.rs +++ b/agent-client-protocol-schema/src/v2/agent.rs @@ -21,9 +21,7 @@ use super::{ use crate::{IntoOption, ProtocolVersion, SkipListener}; #[cfg(feature = "unstable_mcp_over_acp")] -use super::mcp::{ - MCP_MESSAGE_METHOD_NAME, MessageMcpNotification, MessageMcpRequest, MessageMcpResponse, -}; +use super::mcp::{MCP_MESSAGE_METHOD_NAME, MessageMcpNotification}; #[cfg(feature = "unstable_nes")] use super::{ @@ -2929,8 +2927,7 @@ impl McpServerHttp { /// Unique identifier for an MCP server using the ACP transport. /// /// The value is opaque and generated by the ACP component providing the MCP server. It is -/// used by `mcp/connect` to route connection requests back to the component that declared the -/// server. +/// used by `mcp/message` to route requests to the component that declared the server. #[cfg(feature = "unstable_mcp_over_acp")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)] @@ -2955,7 +2952,7 @@ impl McpServerAcpId { /// ACP transport configuration for MCP. /// /// The MCP server is provided by an ACP component and communicates over the ACP channel -/// using `mcp/connect`, `mcp/message`, and `mcp/disconnect`. +/// using `mcp/message`. #[serde_as] #[skip_serializing_none] #[cfg(feature = "unstable_mcp_over_acp")] @@ -5240,13 +5237,6 @@ pub enum ClientRequest { /// The agent must cancel any ongoing work and then free up any resources /// associated with the NES session. CloseNesRequest(Box), - /// **UNSTABLE** - /// - /// This capability is not part of the spec yet, and may be removed or changed at any point. - /// - /// Exchanges an MCP-over-ACP message. - #[cfg(feature = "unstable_mcp_over_acp")] - MessageMcpRequest(Box), /// Handles extension method requests from the client. /// /// Extension methods provide a way to add custom functionality while maintaining @@ -5285,8 +5275,6 @@ impl ClientRequest { Self::SuggestNesRequest(_) => AGENT_METHOD_NAMES.nes_suggest, #[cfg(feature = "unstable_nes")] Self::CloseNesRequest(_) => AGENT_METHOD_NAMES.nes_close, - #[cfg(feature = "unstable_mcp_over_acp")] - Self::MessageMcpRequest(_) => AGENT_METHOD_NAMES.mcp_message, Self::ExtMethodRequest(ext_request) => &ext_request.method, } } @@ -5347,9 +5335,6 @@ pub enum AgentResponse { CloseNesResponse(#[serde(default)] Box), /// Successful result returned by an extension method outside the core ACP method set. ExtMethodResponse(Box), - /// Successful result returned by an MCP-over-ACP `mcp/message` request. - #[cfg(feature = "unstable_mcp_over_acp")] - MessageMcpResponse(Box), } /// All possible notifications that a client can send to an agent. @@ -5780,24 +5765,36 @@ mod test_serialization { #[cfg(feature = "unstable_mcp_over_acp")] #[test] fn test_client_mcp_message_method_names() { + use serde_json::json; + assert_eq!(AGENT_METHOD_NAMES.mcp_message, "mcp/message"); + let notification = + MessageMcpNotification::new("server-1", "req-1", "notifications/progress"); assert_eq!( - ClientRequest::MessageMcpRequest(Box::new(MessageMcpRequest::new( - "conn-1", - "tools/list" - ))) - .method(), + ClientNotification::MessageMcpNotification(Box::new(notification.clone())).method(), "mcp/message" ); assert_eq!( - ClientNotification::MessageMcpNotification(Box::new(MessageMcpNotification::new( - "conn-1", - "notifications/progress" - ))) - .method(), - "mcp/message" + serde_json::to_value(notification).unwrap(), + json!({ + "serverId": "server-1", + "requestId": "req-1", + "method": "notifications/progress" + }) ); + let notification: MessageMcpNotification = serde_json::from_value(json!({ + "serverId": "server-1", "requestId": "req-1", "method": "notifications/progress", + "params": null, "_meta": null + })) + .unwrap(); + assert_eq!(notification.params, None); + assert_eq!(notification.meta, None); + for key in ["serverId", "requestId", "method"] { + let mut value = json!({"serverId":"server-1", "requestId":"req-1", "method":"notifications/progress"}); + value.as_object_mut().unwrap().remove(key); + assert!(serde_json::from_value::(value).is_err()); + } } #[test] diff --git a/agent-client-protocol-schema/src/v2/client.rs b/agent-client-protocol-schema/src/v2/client.rs index b30006e1e..5943e62fb 100644 --- a/agent-client-protocol-schema/src/v2/client.rs +++ b/agent-client-protocol-schema/src/v2/client.rs @@ -27,11 +27,7 @@ use super::{ use crate::{IntoMaybeUndefined, IntoOption, MaybeUndefined, SkipListener}; #[cfg(feature = "unstable_mcp_over_acp")] -use super::mcp::{ - ConnectMcpRequest, ConnectMcpResponse, DisconnectMcpRequest, DisconnectMcpResponse, - MCP_CONNECT_METHOD_NAME, MCP_DISCONNECT_METHOD_NAME, MCP_MESSAGE_METHOD_NAME, - MessageMcpNotification, MessageMcpRequest, MessageMcpResponse, -}; +use super::mcp::{MCP_MESSAGE_METHOD_NAME, MessageMcpRequest, MessageMcpResponse}; #[cfg(feature = "unstable_nes")] use super::{ClientNesCapabilities, PositionEncodingKind}; @@ -2424,15 +2420,9 @@ pub struct ClientMethodNames { pub session_request_permission: &'static str, /// Notification for session updates. pub session_update: &'static str, - /// Method for opening an MCP-over-ACP connection. - #[cfg(feature = "unstable_mcp_over_acp")] - pub mcp_connect: &'static str, /// Method for exchanging MCP-over-ACP messages. #[cfg(feature = "unstable_mcp_over_acp")] pub mcp_message: &'static str, - /// Method for closing an MCP-over-ACP connection. - #[cfg(feature = "unstable_mcp_over_acp")] - pub mcp_disconnect: &'static str, /// Method for elicitation. pub elicitation_create: &'static str, /// Notification for elicitation completion. @@ -2444,11 +2434,7 @@ pub const CLIENT_METHOD_NAMES: ClientMethodNames = ClientMethodNames { session_update: SESSION_UPDATE_NOTIFICATION, session_request_permission: SESSION_REQUEST_PERMISSION_METHOD_NAME, #[cfg(feature = "unstable_mcp_over_acp")] - mcp_connect: MCP_CONNECT_METHOD_NAME, - #[cfg(feature = "unstable_mcp_over_acp")] mcp_message: MCP_MESSAGE_METHOD_NAME, - #[cfg(feature = "unstable_mcp_over_acp")] - mcp_disconnect: MCP_DISCONNECT_METHOD_NAME, elicitation_create: ELICITATION_CREATE_METHOD_NAME, elicitation_complete: ELICITATION_COMPLETE_NOTIFICATION, }; @@ -2493,23 +2479,9 @@ pub enum AgentRequest { /// /// This capability is not part of the spec yet, and may be removed or changed at any point. /// - /// Opens an MCP-over-ACP connection. - #[cfg(feature = "unstable_mcp_over_acp")] - ConnectMcpRequest(Box), - /// **UNSTABLE** - /// - /// This capability is not part of the spec yet, and may be removed or changed at any point. - /// /// Exchanges an MCP-over-ACP message. #[cfg(feature = "unstable_mcp_over_acp")] MessageMcpRequest(Box), - /// **UNSTABLE** - /// - /// This capability is not part of the spec yet, and may be removed or changed at any point. - /// - /// Closes an MCP-over-ACP connection. - #[cfg(feature = "unstable_mcp_over_acp")] - DisconnectMcpRequest(Box), /// Handles extension method requests from the agent. /// /// Allows the Agent to send an arbitrary request that is not part of the ACP spec. @@ -2528,11 +2500,7 @@ impl AgentRequest { Self::RequestPermissionRequest(_) => CLIENT_METHOD_NAMES.session_request_permission, Self::CreateElicitationRequest(_) => CLIENT_METHOD_NAMES.elicitation_create, #[cfg(feature = "unstable_mcp_over_acp")] - Self::ConnectMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_connect, - #[cfg(feature = "unstable_mcp_over_acp")] Self::MessageMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_message, - #[cfg(feature = "unstable_mcp_over_acp")] - Self::DisconnectMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_disconnect, Self::ExtMethodRequest(ext_request) => &ext_request.method, } } @@ -2554,12 +2522,6 @@ pub enum ClientResponse { RequestPermissionResponse(Box), /// Successful result returned for a `elicitation/create` request. CreateElicitationResponse(Box), - /// Successful result returned for a `mcp/connect` request. - #[cfg(feature = "unstable_mcp_over_acp")] - ConnectMcpResponse(Box), - /// Successful result returned for a `mcp/disconnect` request. - #[cfg(feature = "unstable_mcp_over_acp")] - DisconnectMcpResponse(#[serde(default)] Box), /// Successful result returned by an MCP-over-ACP `mcp/message` request. #[cfg(feature = "unstable_mcp_over_acp")] MessageMcpResponse(Box), @@ -2596,13 +2558,6 @@ pub enum AgentNotification { /// /// See protocol docs: [Elicitation](https://agentclientprotocol.com/protocol/elicitation#url-completion) CompleteElicitationNotification(Box), - /// **UNSTABLE** - /// - /// This capability is not part of the spec yet, and may be removed or changed at any point. - /// - /// Receives an MCP-over-ACP notification. - #[cfg(feature = "unstable_mcp_over_acp")] - MessageMcpNotification(Box), /// Handles extension notifications from the agent. /// /// Allows the Agent to send an arbitrary notification that is not part of the ACP spec. @@ -2620,8 +2575,6 @@ impl AgentNotification { match self { Self::UpdateSessionNotification(_) => CLIENT_METHOD_NAMES.session_update, Self::CompleteElicitationNotification(_) => CLIENT_METHOD_NAMES.elicitation_complete, - #[cfg(feature = "unstable_mcp_over_acp")] - Self::MessageMcpNotification(_) => CLIENT_METHOD_NAMES.mcp_message, Self::ExtNotification(ext_notification) => &ext_notification.method, } } @@ -3822,76 +3775,51 @@ mod tests { let params: serde_json::Map = [("cursor".to_string(), json!("abc"))].into_iter().collect(); - assert_eq!(CLIENT_METHOD_NAMES.mcp_connect, "mcp/connect"); assert_eq!(CLIENT_METHOD_NAMES.mcp_message, "mcp/message"); - assert_eq!(CLIENT_METHOD_NAMES.mcp_disconnect, "mcp/disconnect"); - - assert_eq!( - AgentRequest::ConnectMcpRequest(Box::new(ConnectMcpRequest::new("server-1"))).method(), - "mcp/connect" - ); assert_eq!( AgentRequest::MessageMcpRequest(Box::new(MessageMcpRequest::new( - "conn-1", + "server-1", + "req-1", "tools/list" ))) .method(), "mcp/message" ); assert_eq!( - AgentRequest::DisconnectMcpRequest(Box::new(DisconnectMcpRequest::new("conn-1"))) - .method(), - "mcp/disconnect" - ); - assert_eq!( - AgentNotification::MessageMcpNotification(Box::new(MessageMcpNotification::new( - "conn-1", - "notifications/progress" - ))) - .method(), - "mcp/message" - ); - - assert_eq!( - serde_json::to_value(ConnectMcpRequest::new("server-1")).unwrap(), - json!({ "serverId": "server-1" }) - ); - assert_eq!( - serde_json::to_value(ConnectMcpResponse::new("conn-1")).unwrap(), - json!({ "connectionId": "conn-1" }) - ); - assert_eq!( - serde_json::to_value(MessageMcpRequest::new("conn-1", "tools/list").params(params)) - .unwrap(), + serde_json::to_value( + MessageMcpRequest::new("server-1", "req-1", "tools/list").params(params) + ) + .unwrap(), json!({ - "connectionId": "conn-1", + "serverId": "server-1", + "requestId": "req-1", "method": "tools/list", "params": { "cursor": "abc" } }) ); - assert_eq!( - serde_json::to_value(DisconnectMcpRequest::new("conn-1")).unwrap(), - json!({ "connectionId": "conn-1" }) - ); - assert_eq!( - serde_json::to_value(MessageMcpNotification::new( - "conn-1", - "notifications/progress" - )) - .unwrap(), - json!({ - "connectionId": "conn-1", - "method": "notifications/progress" - }) - ); let request_with_null_params: MessageMcpRequest = serde_json::from_value(json!({ - "connectionId": "conn-1", + "serverId": "server-1", + "requestId": "req-1", "method": "tools/list", - "params": null + "params": null, + "_meta": null })) .unwrap(); assert_eq!(request_with_null_params.params, None); + assert_eq!(request_with_null_params.meta, None); + for key in ["serverId", "requestId", "method"] { + let mut value = + json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"}); + value.as_object_mut().unwrap().remove(key); + assert!(serde_json::from_value::(value).is_err()); + } + for key in ["serverId", "requestId", "method"] { + let mut value = + json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"}); + value[key] = serde_json::Value::Null; + assert!(serde_json::from_value::(value).is_err()); + } } #[test] diff --git a/agent-client-protocol-schema/src/v2/mcp.rs b/agent-client-protocol-schema/src/v2/mcp.rs index 7386af965..15987a6a6 100644 --- a/agent-client-protocol-schema/src/v2/mcp.rs +++ b/agent-client-protocol-schema/src/v2/mcp.rs @@ -4,157 +4,162 @@ use std::sync::Arc; use derive_more::{Display, From}; use serde::{Deserialize, Serialize}; -use serde_json::value::RawValue; +use serde_json::{Map, Value}; use serde_with::{DefaultOnError, serde_as, skip_serializing_none}; +use crate::{IntoOption, MaybeUndefined}; + use super::{McpServerAcpId, Meta}; -use crate::IntoOption; /// **UNSTABLE** /// -/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// An inner MCP error, distinct from an outer ACP binding or runtime error. /// -/// A unique identifier for an active MCP-over-ACP connection. +/// `code` and `message` are required and non-null. `data` is optional; +/// explicit `null` is preserved separately from an omitted key. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)] -#[serde(transparent)] -#[from(forward)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[non_exhaustive] -pub struct McpConnectionId(pub Arc); +pub struct McpError { + /// Inner MCP error code; never an ACP error code. + pub code: i32, + /// Inner MCP error message. + pub message: String, + /// Optional error data; explicit null is retained. + #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")] + pub data: MaybeUndefined, + /// Additional fields on the inner MCP error object. + #[serde(flatten)] + pub extra: Map, +} -impl McpConnectionId { - /// Wraps a protocol string as a typed [`McpConnectionId`]. +impl McpError { + /// Construct an inner MCP error without data. #[must_use] - pub fn new(id: impl Into) -> Self { - id.into() + pub fn new(code: i32, message: impl Into) -> Self { + Self { + code, + message: message.into(), + data: MaybeUndefined::Undefined, + extra: Map::new(), + } + } + + /// Set data, preserving explicit JSON null. + #[must_use] + pub fn data(mut self, data: Value) -> Self { + self.data = if data.is_null() { + MaybeUndefined::Null + } else { + MaybeUndefined::Value(data) + }; + self } } /// **UNSTABLE** /// -/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// The successful outer ACP `mcp/message` response carries exactly one +/// inner MCP outcome: an opaque result (including JSON null), or an MCP error. +/// Outer ACP errors are reserved for binding and runtime failures. /// -/// Request parameters for `mcp/connect`. +/// Both branches require their carrier key. An error must be a non-null object. +/// Carrier `_meta` is optional; null is equivalent to omission. #[serde_as] -#[skip_serializing_none] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_CONNECT_METHOD_NAME)))] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(untagged, deny_unknown_fields)] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = "mcp/message")))] #[non_exhaustive] -pub struct ConnectMcpRequest { - /// The ACP MCP server ID that was provided by the component declaring the MCP server. - pub server_id: McpServerAcpId, - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - #[serde(rename = "_meta")] - pub meta: Option, +pub enum MessageMcpResponse { + /// An opaque inner MCP result. + Result { + /// Required, even if JSON null. + result: Value, + /// Optional ACP carrier metadata. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + meta: Option>, + }, + /// A structured inner MCP error. + Error { + /// Required, non-null MCP error object. + error: McpError, + /// Optional ACP carrier metadata. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + meta: Option>, + }, } -impl ConnectMcpRequest { - /// Builds [`ConnectMcpRequest`] with the required request fields set; optional fields start unset or empty. +impl MessageMcpResponse { + /// Wrap any JSON result without interpreting its MCP result type. #[must_use] - pub fn new(server_id: impl Into) -> Self { - Self { - server_id: server_id.into(), - meta: None, - } + pub fn success(result: Value) -> Self { + Self::Result { result, meta: None } } - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + /// Wrap an inner MCP error in a successful outer ACP response. #[must_use] - pub fn meta(mut self, meta: impl IntoOption) -> Self { - self.meta = meta.into_option(); + pub fn error(error: McpError) -> Self { + Self::Error { error, meta: None } + } + + /// Attach optional carrier-level ACP metadata. + #[must_use] + pub fn meta(mut self, meta: impl IntoOption>) -> Self { + match &mut self { + Self::Result { meta: field, .. } | Self::Error { meta: field, .. } => { + *field = meta.into_option(); + } + } self } } /// **UNSTABLE** /// -/// This capability is not part of the spec yet, and may be removed or changed at any point. -/// -/// Response to `mcp/connect`. -#[serde_as] -#[skip_serializing_none] +/// Identifies an inner MCP request active against a server on this ACP connection. +/// Generated by the caller and preserved unchanged by proxies, independently of +/// the outer ACP JSON-RPC request ID. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_CONNECT_METHOD_NAME)))] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)] +#[serde(transparent)] +#[from(Arc, String, &'static str)] #[non_exhaustive] -pub struct ConnectMcpResponse { - /// The unique identifier for this MCP-over-ACP connection. - pub connection_id: McpConnectionId, - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - #[serde(rename = "_meta")] - pub meta: Option, -} - -impl ConnectMcpResponse { - /// Builds [`ConnectMcpResponse`] with the required response fields set; optional fields start unset or empty. - #[must_use] - pub fn new(connection_id: impl Into) -> Self { - Self { - connection_id: connection_id.into(), - meta: None, - } - } +pub struct McpRequestId(pub Arc); - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) +impl McpRequestId { + /// Wraps a protocol string as a typed [`McpRequestId`]. #[must_use] - pub fn meta(mut self, meta: impl IntoOption) -> Self { - self.meta = meta.into_option(); - self + pub fn new(id: impl Into>) -> Self { + Self(id.into()) } } /// **UNSTABLE** /// -/// This capability is not part of the spec yet, and may be removed or changed at any point. -/// -/// Request parameters for `mcp/message`. +/// Request parameters for `mcp/message`, sent from consumer to provider. #[serde_as] #[skip_serializing_none] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "both", "x-method" = MCP_MESSAGE_METHOD_NAME)))] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_MESSAGE_METHOD_NAME)))] #[non_exhaustive] pub struct MessageMcpRequest { - /// The MCP-over-ACP connection this message is sent on. - pub connection_id: McpConnectionId, + /// The declared ACP MCP server receiving this request. + pub server_id: McpServerAcpId, + /// The caller-generated identifier for the inner MCP request. + pub request_id: McpRequestId, /// The inner MCP method name. pub method: String, - /// Optional inner MCP params. - /// - /// If omitted or set to `null`, the inner MCP message has no params. + /// Optional inner MCP params; null is equivalent to omission. #[serde(default)] pub params: Option>, - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + /// ACP extension metadata (not inner MCP params._meta); null is equivalent to omission. #[serde_as(deserialize_as = "DefaultOnError")] #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] #[serde(default)] @@ -163,20 +168,23 @@ pub struct MessageMcpRequest { } impl MessageMcpRequest { - /// Builds [`MessageMcpRequest`] with the required request fields set; optional fields start unset or empty. + /// Builds [`MessageMcpRequest`] with required fields set. #[must_use] - pub fn new(connection_id: impl Into, method: impl Into) -> Self { + pub fn new( + server_id: impl Into, + request_id: impl Into, + method: impl Into, + ) -> Self { Self { - connection_id: connection_id.into(), + server_id: server_id.into(), + request_id: request_id.into(), method: method.into(), params: None, meta: None, } } - /// Optional inner MCP params. - /// - /// If omitted or set to `null`, the inner MCP message has no params. + /// Sets optional inner MCP params. #[must_use] pub fn params( mut self, @@ -186,11 +194,7 @@ impl MessageMcpRequest { self } - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + /// Sets optional ACP extension metadata. #[must_use] pub fn meta(mut self, meta: impl IntoOption) -> Self { self.meta = meta.into_option(); @@ -200,36 +204,26 @@ impl MessageMcpRequest { /// **UNSTABLE** /// -/// This capability is not part of the spec yet, and may be removed or changed at any point. -/// -/// Notification parameters for `mcp/message`. -/// -/// This is used when the wrapped MCP message is a notification and the outer JSON-RPC -/// envelope has no `id`. +/// Notification for an active request, sent from provider to consumer. +/// Includes subscription acknowledgements and updates. #[serde_as] #[skip_serializing_none] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "both", "x-method" = MCP_MESSAGE_METHOD_NAME)))] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "agent", "x-method" = MCP_MESSAGE_METHOD_NAME)))] #[non_exhaustive] pub struct MessageMcpNotification { - /// The MCP-over-ACP connection this message is sent on. - pub connection_id: McpConnectionId, + /// The declared ACP MCP server handling the associated request. + pub server_id: McpServerAcpId, + /// The identifier of the active inner MCP request. + pub request_id: McpRequestId, /// The inner MCP method name. pub method: String, - /// Optional inner MCP params. - /// - /// If omitted or set to `null`, the inner MCP message has no params. - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + /// Optional inner MCP params; null is equivalent to omission. #[serde(default)] pub params: Option>, - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + /// ACP extension metadata (not inner MCP params._meta); null is equivalent to omission. #[serde_as(deserialize_as = "DefaultOnError")] #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] #[serde(default)] @@ -238,20 +232,23 @@ pub struct MessageMcpNotification { } impl MessageMcpNotification { - /// Builds [`MessageMcpNotification`] with the required notification fields set; optional fields start unset or empty. + /// Builds [`MessageMcpNotification`] with required fields set. #[must_use] - pub fn new(connection_id: impl Into, method: impl Into) -> Self { + pub fn new( + server_id: impl Into, + request_id: impl Into, + method: impl Into, + ) -> Self { Self { - connection_id: connection_id.into(), + server_id: server_id.into(), + request_id: request_id.into(), method: method.into(), params: None, meta: None, } } - /// Optional inner MCP params. - /// - /// If omitted or set to `null`, the inner MCP message has no params. + /// Sets optional inner MCP params. #[must_use] pub fn params( mut self, @@ -261,11 +258,7 @@ impl MessageMcpNotification { self } - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + /// Sets optional ACP extension metadata. #[must_use] pub fn meta(mut self, meta: impl IntoOption) -> Self { self.meta = meta.into_option(); @@ -273,128 +266,103 @@ impl MessageMcpNotification { } } -/// **UNSTABLE** -/// -/// This capability is not part of the spec yet, and may be removed or changed at any point. -/// -/// Response to `mcp/message`. -/// -/// This is the inner MCP response result payload. Any JSON value is valid. -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize, From)] -#[serde(transparent)] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "both", "x-method" = MCP_MESSAGE_METHOD_NAME)))] -#[non_exhaustive] -pub struct MessageMcpResponse( - #[cfg_attr(feature = "schemars", schemars(with = "serde_json::Value"))] pub Arc, -); +/// Method name for exchanging MCP-over-ACP messages. +pub(crate) const MCP_MESSAGE_METHOD_NAME: &str = "mcp/message"; -impl MessageMcpResponse { - /// Builds [`MessageMcpResponse`] with the required response fields set; optional fields start unset or empty. - #[must_use] - pub fn new(result: Arc) -> Self { - Self(result) - } -} +#[cfg(test)] +mod tests { + use serde_json::{Value, json}; -/// **UNSTABLE** -/// -/// This capability is not part of the spec yet, and may be removed or changed at any point. -/// -/// Request parameters for `mcp/disconnect`. -#[serde_as] -#[skip_serializing_none] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_DISCONNECT_METHOD_NAME)))] -#[non_exhaustive] -pub struct DisconnectMcpRequest { - /// The MCP-over-ACP connection to close. - pub connection_id: McpConnectionId, - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - #[serde(rename = "_meta")] - pub meta: Option, -} + use super::{McpError, MessageMcpResponse}; + use crate::MaybeUndefined; -impl DisconnectMcpRequest { - /// Builds [`DisconnectMcpRequest`] with the required request fields set; optional fields start unset or empty. - #[must_use] - pub fn new(connection_id: impl Into) -> Self { - Self { - connection_id: connection_id.into(), - meta: None, + #[test] + fn result_is_opaque_and_present_even_when_null() { + for result in [ + Value::Null, + json!(false), + json!(42), + json!("opaque"), + json!([null, 1]), + json!({"resultType": "future", "unknown": {"value": true}}), + ] { + let response = MessageMcpResponse::success(result.clone()); + let wire = json!({"result": result}); + assert_eq!(serde_json::to_value(&response).unwrap(), wire); + assert_eq!( + serde_json::from_value::(wire).unwrap(), + response + ); } } - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[must_use] - pub fn meta(mut self, meta: impl IntoOption) -> Self { - self.meta = meta.into_option(); - self - } -} - -crate::serde_util::default_on_null! { - /// **UNSTABLE** - /// - /// This capability is not part of the spec yet, and may be removed or changed at any point. - /// - /// Response to `mcp/disconnect`. - #[serde_as] - #[skip_serializing_none] - #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] - #[derive(Default, Debug, Clone, Serialize, PartialEq, Eq)] - #[serde(rename_all = "camelCase")] - #[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_DISCONNECT_METHOD_NAME)))] - #[non_exhaustive] - pub struct DisconnectMcpResponse { - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - #[serde(rename = "_meta")] - pub meta: Option, + #[test] + fn error_round_trips_data_and_extensions_without_acp_translation() { + for data in [ + MaybeUndefined::Undefined, + MaybeUndefined::Null, + MaybeUndefined::Value(json!({"arbitrary": [1, null]})), + ] { + let mut error = McpError::new(-32000, "inner error"); + error.data = data.clone(); + error.extra.insert("future".into(), json!({"key": 1})); + let response = MessageMcpResponse::error(error); + let wire = serde_json::to_value(&response).unwrap(); + assert_eq!(wire["error"]["code"], -32000); + assert_eq!(wire["error"].get("data").is_some(), !data.is_undefined()); + assert_eq!(wire["error"]["future"], json!({"key": 1})); + assert_eq!( + serde_json::from_value::(wire).unwrap(), + response + ); + } + assert_eq!( + McpError::new(1, "x").data(Value::Null).data, + MaybeUndefined::Null + ); } -} -impl DisconnectMcpResponse { - /// Builds [`DisconnectMcpResponse`] with the required response fields set; optional fields start unset or empty. - #[must_use] - pub fn new() -> Self { - Self::default() + #[test] + fn only_one_non_null_carrier_key_is_valid() { + for wire in [ + Value::Null, + json!({}), + json!({"_meta": null}), + json!({"result": 1, "error": {"code": 1, "message": "x"}}), + json!({"result": 1, "error": null}), + json!({"error": null}), + json!({"error": 1}), + json!({"error": {}}), + json!({"error": {"code": null, "message": "x"}}), + json!({"error": {"code": 1, "message": null}}), + json!({"error": {"code": 1.5, "message": "x"}}), + json!({"unexpected": 1, "result": 1}), + ] { + assert!( + serde_json::from_value::(wire.clone()).is_err(), + "accepted {wire}" + ); + } } - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[must_use] - pub fn meta(mut self, meta: impl IntoOption) -> Self { - self.meta = meta.into_option(); - self + #[test] + fn carrier_metadata_is_optional_and_null_means_absent() { + for wire in [ + json!({"result": null, "_meta": null}), + json!({"error": {"code": 1, "message": "x"}, "_meta": null}), + ] { + let parsed: MessageMcpResponse = serde_json::from_value(wire).unwrap(); + assert!(serde_json::to_value(parsed).unwrap().get("_meta").is_none()); + } + let meta = json!({"extension": [null, true]}) + .as_object() + .unwrap() + .clone(); + let response = + MessageMcpResponse::success(json!({"_meta": {"inner": true}})).meta(meta.clone()); + assert_eq!( + serde_json::to_value(response).unwrap(), + json!({"result": {"_meta": {"inner": true}}, "_meta": meta}) + ); } } - -/// Method name for opening an MCP-over-ACP connection. -pub(crate) const MCP_CONNECT_METHOD_NAME: &str = "mcp/connect"; -/// Method name for exchanging MCP-over-ACP messages. -pub(crate) const MCP_MESSAGE_METHOD_NAME: &str = "mcp/message"; -/// Method name for closing an MCP-over-ACP connection. -pub(crate) const MCP_DISCONNECT_METHOD_NAME: &str = "mcp/disconnect"; diff --git a/docs/protocol/v1/draft/schema.mdx b/docs/protocol/v1/draft/schema.mdx index 007b66cd7..6af33a595 100644 --- a/docs/protocol/v1/draft/schema.mdx +++ b/docs/protocol/v1/draft/schema.mdx @@ -388,7 +388,7 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/d This capability is not part of the spec yet, and may be removed or changed at any point. -Exchanges an MCP-over-ACP message. +Sends an MCP-over-ACP notification. #### MessageMcpNotification @@ -398,8 +398,8 @@ This capability is not part of the spec yet, and may be removed or changed at an Notification parameters for `mcp/message`. -This is used when the wrapped MCP message is a notification and the outer JSON-RPC -envelope has no `id`. +Sent by the provider to the consumer for an active request (including +subscription acknowledgements and updates); the outer envelope has no `id`. **Type:** Object @@ -412,9 +412,6 @@ these keys. See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/draft/extensibility) - -McpConnectionId} required> - The MCP-over-ACP connection this message is sent on. The inner MCP method name. @@ -425,50 +422,13 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/d If omitted or set to `null`, the inner MCP message has no params. - -#### MessageMcpRequest - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Request parameters for `mcp/message`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/draft/extensibility) - - -McpConnectionId} required> - The MCP-over-ACP connection this message is sent on. - - - The inner MCP method name. +McpRequestId} required> + The identifier of the active inner MCP request. - - Optional inner MCP params. - -If omitted or set to `null`, the inner MCP message has no params. - +McpServerAcpId} required> + The declared ACP MCP server handling the associated request. -#### MessageMcpResponse - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Response to `mcp/message`. - -This is the inner MCP response result payload. Any JSON value is valid. - ### nes/accept @@ -2095,117 +2055,6 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/d - -### mcp/connect - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Opens an MCP-over-ACP connection. - -#### ConnectMcpRequest - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Request parameters for `mcp/connect`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/draft/extensibility) - - -McpServerAcpId} required> - The ACP MCP server ID that was provided by the component declaring the MCP server. - - -#### ConnectMcpResponse - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Response to `mcp/connect`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/draft/extensibility) - - -McpConnectionId} required> - The unique identifier for this MCP-over-ACP connection. - - - -### mcp/disconnect - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Closes an MCP-over-ACP connection. - -#### DisconnectMcpRequest - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Request parameters for `mcp/disconnect`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/draft/extensibility) - - -McpConnectionId} required> - The MCP-over-ACP connection to close. - - -#### DisconnectMcpResponse - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Response to `mcp/disconnect`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/draft/extensibility) - - - ### mcp/message @@ -2215,16 +2064,13 @@ This capability is not part of the spec yet, and may be removed or changed at an Exchanges an MCP-over-ACP message. -#### MessageMcpNotification +#### MessageMcpRequest **UNSTABLE** This capability is not part of the spec yet, and may be removed or changed at any point. -Notification parameters for `mcp/message`. - -This is used when the wrapped MCP message is a notification and the outer JSON-RPC -envelope has no `id`. +Request parameters for `mcp/message`. **Type:** Object @@ -2237,9 +2083,6 @@ these keys. See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/draft/extensibility) - -McpConnectionId} required> - The MCP-over-ACP connection this message is sent on. The inner MCP method name. @@ -2250,49 +2093,55 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/d If omitted or set to `null`, the inner MCP message has no params. +McpRequestId} required> + The caller-generated identifier for the inner MCP request. + +McpServerAcpId} required> + The declared ACP MCP server receiving this request. + -#### MessageMcpRequest +#### MessageMcpResponse **UNSTABLE** -This capability is not part of the spec yet, and may be removed or changed at any point. - -Request parameters for `mcp/message`. +The successful outer ACP `mcp/message` response carries exactly one +inner MCP outcome: an opaque result (including JSON null), or an MCP error. +Outer ACP errors are reserved for binding and runtime failures. -**Type:** Object +Both branches require their carrier key. An error must be a non-null object. +Carrier `_meta` is optional; null is equivalent to omission. -**Properties:** +**Type:** Union - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. + +An opaque inner MCP result. -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/draft/extensibility) + + + Optional ACP carrier metadata. -McpConnectionId} required> - The MCP-over-ACP connection this message is sent on. - - - The inner MCP method name. + + Required, even if JSON null. - - Optional inner MCP params. - -If omitted or set to `null`, the inner MCP message has no params. + -#### MessageMcpResponse + +A structured inner MCP error. -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. + -Response to `mcp/message`. + + Optional ACP carrier metadata. + +McpError} required> + Required, non-null MCP error object. + -This is the inner MCP response result payload. Any JSON value is valid. + + ### session/request_permission @@ -5021,13 +4870,39 @@ Agent supports `McpServer::Acp`. -## McpConnectionId +## McpError + +**UNSTABLE** + +An inner MCP error, distinct from an outer ACP binding or runtime error. + +`code` and `message` are required and non-null. `data` is optional; +explicit `null` is preserved separately from an omitted key. + +**Type:** Object + +**Properties:** + + + Inner MCP error code; never an ACP error code. + + + Optional error data; explicit null is retained. + + + Inner MCP error message. + + +## McpRequestId **UNSTABLE** This capability is not part of the spec yet, and may be removed or changed at any point. -A unique identifier for an active MCP-over-ACP connection. +Identifies an inner MCP request active against a server on this ACP connection. + +Generated by the caller and preserved unchanged by proxies. This is distinct +from the outer ACP JSON-RPC request ID. **Type:** `string` @@ -5181,7 +5056,7 @@ This capability is not part of the spec yet, and may be removed or changed at an ACP transport configuration for MCP. The MCP server is provided by an ACP component and communicates over the ACP channel -using `mcp/connect`, `mcp/message`, and `mcp/disconnect`. +using `mcp/message`. **Type:** Object @@ -5215,8 +5090,7 @@ This capability is not part of the spec yet, and may be removed or changed at an Unique identifier for an MCP server using the ACP transport. The value is opaque and generated by the ACP component providing the MCP server. It is -used by `mcp/connect` to route connection requests back to the component that declared the -server. +used by `mcp/message` to route requests to the component that declared the server. **Type:** `string` diff --git a/docs/protocol/v2/draft/schema.mdx b/docs/protocol/v2/draft/schema.mdx index 22a21b0f2..97793fd25 100644 --- a/docs/protocol/v2/draft/schema.mdx +++ b/docs/protocol/v2/draft/schema.mdx @@ -419,87 +419,44 @@ The client should disconnect, if it doesn't support this version. This capability is not part of the spec yet, and may be removed or changed at any point. -Exchanges an MCP-over-ACP message. +Sends an MCP-over-ACP notification. #### MessageMcpNotification **UNSTABLE** -This capability is not part of the spec yet, and may be removed or changed at any point. - -Notification parameters for `mcp/message`. - -This is used when the wrapped MCP message is a notification and the outer JSON-RPC -envelope has no `id`. +Notification for an active request, sent from provider to consumer. +Includes subscription acknowledgements and updates. **Type:** Object **Properties:** - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) - - -McpConnectionId} required> - The MCP-over-ACP connection this message is sent on. + + ACP extension metadata (not inner MCP params._meta); null is equivalent to + omission. The inner MCP method name. - - Optional inner MCP params. - -If omitted or set to `null`, the inner MCP message has no params. - + + Optional inner MCP params; null is equivalent to omission. - -#### MessageMcpRequest - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Request parameters for `mcp/message`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) - - -McpConnectionId} required> - The MCP-over-ACP connection this message is sent on. - - - The inner MCP method name. +McpRequestId} + required +> + The identifier of the active inner MCP request. - - Optional inner MCP params. - -If omitted or set to `null`, the inner MCP message has no params. - +McpServerAcpId} + required +> + The declared ACP MCP server handling the associated request. -#### MessageMcpResponse - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Response to `mcp/message`. - -This is the inner MCP response result payload. Any JSON value is valid. - ### nes/accept @@ -1519,7 +1476,7 @@ extensions. Unknown values that do not begin with `_` are reserved for future ACP variants. - + Raw value payload for the custom or future value type. @@ -1885,117 +1842,6 @@ future ACP variants. - -### mcp/connect - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Opens an MCP-over-ACP connection. - -#### ConnectMcpRequest - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Request parameters for `mcp/connect`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) - - -McpServerAcpId} required> - The ACP MCP server ID that was provided by the component declaring the MCP server. - - -#### ConnectMcpResponse - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Response to `mcp/connect`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) - - -McpConnectionId} required> - The unique identifier for this MCP-over-ACP connection. - - - -### mcp/disconnect - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Closes an MCP-over-ACP connection. - -#### DisconnectMcpRequest - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Request parameters for `mcp/disconnect`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) - - -McpConnectionId} required> - The MCP-over-ACP connection to close. - - -#### DisconnectMcpResponse - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Response to `mcp/disconnect`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) - - - ### mcp/message @@ -2005,84 +1851,83 @@ This capability is not part of the spec yet, and may be removed or changed at an Exchanges an MCP-over-ACP message. -#### MessageMcpNotification +#### MessageMcpRequest **UNSTABLE** -This capability is not part of the spec yet, and may be removed or changed at any point. - -Notification parameters for `mcp/message`. - -This is used when the wrapped MCP message is a notification and the outer JSON-RPC -envelope has no `id`. +Request parameters for `mcp/message`, sent from consumer to provider. **Type:** Object **Properties:** - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) - - -McpConnectionId} required> - The MCP-over-ACP connection this message is sent on. + + ACP extension metadata (not inner MCP params._meta); null is equivalent to + omission. The inner MCP method name. - - Optional inner MCP params. - -If omitted or set to `null`, the inner MCP message has no params. - + + Optional inner MCP params; null is equivalent to omission. + +McpRequestId} + required +> + The caller-generated identifier for the inner MCP request. + +McpServerAcpId} + required +> + The declared ACP MCP server receiving this request. -#### MessageMcpRequest +#### MessageMcpResponse **UNSTABLE** -This capability is not part of the spec yet, and may be removed or changed at any point. - -Request parameters for `mcp/message`. +The successful outer ACP `mcp/message` response carries exactly one +inner MCP outcome: an opaque result (including JSON null), or an MCP error. +Outer ACP errors are reserved for binding and runtime failures. -**Type:** Object +Both branches require their carrier key. An error must be a non-null object. +Carrier `_meta` is optional; null is equivalent to omission. -**Properties:** +**Type:** Union - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. + +An opaque inner MCP result. -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) + + + Optional ACP carrier metadata. -McpConnectionId} required> - The MCP-over-ACP connection this message is sent on. - - - The inner MCP method name. + + Required, even if JSON null. - - Optional inner MCP params. - -If omitted or set to `null`, the inner MCP message has no params. + -#### MessageMcpResponse - -**UNSTABLE** + +A structured inner MCP error. -This capability is not part of the spec yet, and may be removed or changed at any point. + -Response to `mcp/message`. + + Optional ACP carrier metadata. + +McpError} required> + Required, non-null MCP error object. + -This is the inner MCP response result payload. Any JSON value is valid. + + ### session/request_permission @@ -4995,15 +4840,28 @@ Supplying `\{\}` means the agent supports stdio MCP server transports. -## McpConnectionId +## McpError **UNSTABLE** -This capability is not part of the spec yet, and may be removed or changed at any point. +An inner MCP error, distinct from an outer ACP binding or runtime error. -A unique identifier for an active MCP-over-ACP connection. +`code` and `message` are required and non-null. `data` is optional; +explicit `null` is preserved separately from an omitted key. -**Type:** `string` +**Type:** Object + +**Properties:** + + + Inner MCP error code; never an ACP error code. + + + Optional error data; explicit null is retained. + + + Inner MCP error message. + ## McpHttpCapabilities @@ -5024,6 +4882,16 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/d +## McpRequestId + +**UNSTABLE** + +Identifies an inner MCP request active against a server on this ACP connection. +Generated by the caller and preserved unchanged by proxies, independently of +the outer ACP JSON-RPC request ID. + +**Type:** `string` + ## McpServer Configuration for connecting to an MCP (Model Context Protocol) server. @@ -5174,7 +5042,7 @@ This capability is not part of the spec yet, and may be removed or changed at an ACP transport configuration for MCP. The MCP server is provided by an ACP component and communicates over the ACP channel -using `mcp/connect`, `mcp/message`, and `mcp/disconnect`. +using `mcp/message`. **Type:** Object @@ -5208,8 +5076,7 @@ This capability is not part of the spec yet, and may be removed or changed at an Unique identifier for an MCP server using the ACP transport. The value is opaque and generated by the ACP component providing the MCP server. It is -used by `mcp/connect` to route connection requests back to the component that declared the -server. +used by `mcp/message` to route requests to the component that declared the server. **Type:** `string` diff --git a/docs/protocol/v2/schema.mdx b/docs/protocol/v2/schema.mdx index 0823e0f1a..3460afe40 100644 --- a/docs/protocol/v2/schema.mdx +++ b/docs/protocol/v2/schema.mdx @@ -737,7 +737,7 @@ extensions. Unknown values that do not begin with `_` are reserved for future ACP variants. - + Raw value payload for the custom or future value type. diff --git a/docs/rfds/mcp-over-acp.mdx b/docs/rfds/mcp-over-acp.mdx index 4242c5b6b..f525cbb26 100644 --- a/docs/rfds/mcp-over-acp.mdx +++ b/docs/rfds/mcp-over-acp.mdx @@ -1,306 +1,336 @@ --- -title: "MCP-over-ACP: MCP Transport via ACP Channels" +title: "MCP-over-ACP: Stateless MCP Requests over ACP" --- Author(s): [nikomatsakis](https://github.com/nikomatsakis) ## Elevator pitch -> What are you proposing to change? +Let an ACP client or proxy supply tools to an agent using the ACP connection it already has. Declare an MCP server with `"type": "acp"`, then send independent MCP requests to its `serverId`. Results, request-scoped notifications, and cancellation travel over ACP without a second process or network endpoint. -Add support for MCP servers that communicate over ACP channels instead of stdio or HTTP. This enables any ACP component to provide MCP tools and handle callbacks through the existing ACP connection, without spawning separate processes or managing additional transports. +This draft targets **MCP 2026-07-28 only**. It does not emulate older MCP revisions. ACP initialization and sessions remain unchanged, but there is no MCP initialization handshake, MCP connection object, or connect/disconnect exchange. -## Status quo +## Motivation -> How do things work today and what problems does this cause? Why would we change things? +ACP manages an agent's conversation while MCP provides tools behind it. A client often needs to do both: provide project-aware tools, ask the agent to use them, and execute them inside the client's process or sandbox. -ACP and MCP each solve different halves of the problem of interacting with an agent. ACP stands in "front" of the agent, managing sessions, sending prompts, and receiving responses. MCP stands "behind" the agent, providing tools that the agent can use to do its work. +Requiring a separate HTTP listener or subprocess for those tools creates an extra communication path and makes isolation harder. Native MCP-over-ACP keeps the interaction on the authorized ACP channel. It works directly between a client and an agent; proxy chains are useful but not required. -Many applications would benefit from being able to be both "in front" of the agent and "behind" it. This would allow a client, for example, to create custom MCP tools that are tailored to a specific request and which live in the client's address space. +## Protocol model -The only way to combine ACP and MCP today is to use some sort of "backdoor", such as opening an HTTP port for the agent to connect to or providing a binary that communicates with IPC. This is inconvenient to implement but also means that clients cannot be properly abstracted and sandboxed, as some of the communication with the agent is going through side channels. Imagine trying to host an ACP component (client, agent, or [agent extension](./proxy-chains.mdx)) that runs in a WASM sandbox or even on another machine: for that to work, the ACP protocol has to encompass all of the relevant interactions so that messages can be transmitted properly. +There are three identities, with different purposes: -## What we propose to do about it +- **`serverId`** names a server declared by its provider. It selects a tool/resource/prompt offering, not an MCP session. +- **`requestId`** is a caller-generated opaque string identifying one logical MCP request. It is used as the inner MCP JSON-RPC ID and is preserved across ACP proxies. +- **Outer JSON-RPC `id`** identifies the ACP request on one hop. A proxy may renumber it when forwarding. -> What are you proposing to improve the situation? +Every operation carries its own MCP version, client capabilities, and other request context. Nothing about a previous request establishes that context for the next one. A subscription may keep one request alive; that is not a session for unrelated calls. -We propose adding `"acp"` as a new MCP transport type. When an ACP component (client or proxy) adds an MCP server with ACP transport to a session, tool invocations for that server are routed back through the ACP channel to the component that provided it. +Stateless MCP does not require stateless application services. Providers may reuse tool implementations, database pools, caches, and authorization services. Execution and notification permissions belong to each operation, while application state may outlive it. Discovery, listing, and execution must use that operation's explicit caller context, not the identity or capabilities of an earlier request. -This enables patterns like: +The binding uses one method name, `mcp/message`, in two directions: -- A **client** that injects project-aware tools into every session and handles callbacks directly -- An **[agent extension](./proxy-chains.mdx)** that adds context-aware tools based on the conversation state -- A **bridge** that translates ACP-transport MCP servers to stdio for agents that don't support native ACP transport +- An **agent-to-provider request** invokes one MCP operation and eventually receives one result or error. +- A **provider-to-agent notification** carries an MCP notification belonging to that active operation. -### How it works +`mcp/message` is this binding's ACP transport envelope, not an MCP-defined method. JSON-RPC distinguishes the two forms by the outer `id`: requests have one, notifications do not. The inner MCP method is preserved, such as `tools/call` for a request or `notifications/progress` for a notification. Implementations dispatch by JSON-RPC message kind and direction, not the outer method name alone. -When the client connects, the agent advertises MCP-over-ACP support via `mcpCapabilities.acp` in its `InitializeResponse`. If supported, the client can add MCP servers to a `session/new` request with `"type": "acp"` and an `id` that identifies the server: +There are no provider-originated MCP requests. Interactive tools use MCP's multi round-trip request pattern (MRTR). + +## Declaring a server + +The client checks the agent's ACP MCP capability, then includes a declaration in a session setup request. For example, `session/new` parameters can include: ```json { - "tools": { - "mcpServers": [ - { - "type": "acp", - "name": "project-tools", - "id": "550e8400-e29b-41d4-a716-446655440000" - } - ] - } + "cwd": "/workspace/project", + "mcpServers": [ + { + "type": "acp", + "name": "project-tools", + "serverId": "project-tools:7a72" + } + ] } ``` -The `id` is generated by the component providing the MCP server. - -When the agent connects to the MCP server, an `mcp/connect` message is sent with the MCP server's `id`. This returns a fresh `connectionId`. MCP messages are then sent back and forth using `mcp/message` requests and notifications. Finally, `mcp/disconnect` signals that the connection is closing. - -`mcp/connect` and `mcp/disconnect` are initiated by the side connecting to the ACP-transport MCP server. In the direct client-provided server case, that means the agent sends them to the client. Once connected, `mcp/message` is bidirectional: the agent can send MCP client-originated requests to the server, and the server can send MCP server-originated requests or notifications back to the agent. - -### Bridging and compatibility - -Existing agents don't support ACP transport for MCP servers. To bridge this gap, a wrapper component can translate between ACP-transport MCP servers and the stdio/HTTP transports that agents already support. The wrapper spawns shim processes or HTTP servers that the agent connects to normally, then relays messages to/from the ACP channel. - -We've implemented this bridging as part of the conductor described in the [Proxy Chains RFD](./proxy-chains). The conductor always advertises `mcpCapabilities.acp: true` to its clients, handling the translation transparently regardless of whether the downstream agent supports native ACP transport. - -### Message flow example +The provider generates the opaque `serverId`. A server ID identifies one registration for the lifetime of the ACP connection and must not be rebound to a different registration, including after removal. The same registration may be offered to multiple ACP sessions; distinct offerings use distinct server IDs rather than hidden per-MCP-connection catalogs. -```mermaid -sequenceDiagram - participant Client - participant Agent +The provider must be ready to serve requests when it publishes the declaration. An agent may invoke tools or discover the server before returning the ACP session ID. - Client->>Agent: session/new (with ACP-transport MCP server) - Agent-->>Client: session created +Only the owning provider claims requests for its registered IDs. Intermediaries without a matching registration forward the request. If no provider can resolve the ID, the final recipient returns the binding's server-unavailable error. - Client->>Agent: prompt ("analyze this codebase") +A declaration is a reference to a registration, not a remote allocation request. This RFD adds no server-update or unadvertisement method. The provider owns the registration's local lifetime: releasing it rejects future requests and cancels outstanding work. Closing the ACP connection releases all its registrations. Merely omitting a previously declared server from a later setup request does not revoke a registration used by another session. - Note over Agent: Agent decides to use the tool - Agent->>Client: mcp/connect (acpId: "") - Client-->>Agent: connectionId: "conn-1" - - Agent->>Client: mcp/message (list_files tool call) - Client-->>Agent: file listing results - - Client->>Agent: mcp/message (server callback or notification) - Agent-->>Client: callback result, if request +### Capability advertising - Agent-->>Client: response using tool results +In **ACP v1**, the relevant `InitializeResponse` fragment is: - Agent->>Client: mcp/disconnect (connectionId: "conn-1") +```json +{ + "agentCapabilities": { + "mcpCapabilities": { + "acp": true + } + } +} ``` -## Shiny future +Omission is equivalent to `false`. -> How will things play out once this feature exists? - -### Seamless tool injection - -Components can provide tools without any process management. A Rust development environment could inject cargo-aware tools, a cloud IDE could inject deployment tools, and a security scanner could inject vulnerability checking - all through the same ACP connection they're already using. - -### WebAssembly-based tooling - -Components running in sandboxed environments (like WASM) can provide MCP tools without needing filesystem or process spawning capabilities. The ACP channel is their only interface, and that's sufficient. - -### Transparent bridging - -For agents that don't natively support ACP transport, intermediaries can transparently bridge: accepting MCP-over-ACP from clients and spawning stdio- or HTTP-based MCP servers that the agent can use normally. This provides backwards compatibility while allowing the ecosystem to adopt ACP transport incrementally. - -## Implementation details and plan - -> Tell me more about your implementation. What is your detailed implementation plan? - -### Capability advertising - -Agents advertise MCP-over-ACP support via the [`mcpCapabilities`](/protocol/v1/schema#mcpcapabilities) field in their `InitializeResponse`. We propose adding an `acp` field to this existing structure: +In **draft ACP v2**, the fragment is: ```json { "capabilities": { - "mcpCapabilities": { - "http": false, - "sse": false, - "acp": true + "session": { + "mcp": { + "acp": {} + } } } } ``` -When `mcpCapabilities.acp` is `true`, the agent can handle MCP servers declared with `"type": "acp"` natively. It will initiate `mcp/connect` and `mcp/disconnect` through the ACP channel, and both sides can exchange MCP payloads with `mcp/message`. +The v2 field is an optional object: omission or `null` means support is not advertised, and `{}` advertises support. These are ACP capabilities, separate from the MCP client capabilities carried on each request. -Clients don't need to advertise anything - they simply check the agent's capabilities to determine whether bridging is needed. +An intermediary only advertises support when its downstream chain can consume this transport. The conductor does not unconditionally add the capability. A proxy that provides no adaptation preserves its successor's capabilities. -**Bridging intermediaries**: An intermediary that provides bridging can present `mcpCapabilities.acp: true` to its clients regardless of whether the downstream agent supports it, handling bridging transparently (see [Bridging](#bridging-for-agents-without-native-support) below). +This capability advertises the MCP transport, not every optional MCP feature or a guarantee that every operation can be cancelled. MCP discovery and per-request capabilities describe the supported features. -### MCP transport schema extension +## Requests and results -We extend the MCP server JSON schema to include ACP as a transport option: +The agent sends an ACP request with a server ID, a fresh logical MCP request ID, and flattened MCP method/parameters: ```json { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "acp" - }, - "name": { - "type": "string" - }, - "id": { - "type": "string" - }, - "_meta": { - "type": ["object", "null"] + "jsonrpc": "2.0", + "id": 20, + "method": "mcp/message", + "params": { + "serverId": "project-tools:7a72", + "requestId": "mcp-request:a11f", + "method": "tools/call", + "params": { + "name": "echo", + "arguments": { + "message": "hello" + }, + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {}, + "io.modelcontextprotocol/clientInfo": { + "name": "example-agent", + "version": "1" + }, + "progressToken": "progress-1" + } } - }, - "required": ["type", "name", "id"] + } } ``` -### Message reference +The provider executes the inner request using `"mcp-request:a11f"` as its MCP JSON-RPC ID. It does not use the outer ACP ID `20`, which may differ on another hop. -**Connection lifecycle:** +The successful ACP response carries exactly one inner MCP outcome. A result is nested under `result`: ```json -// Establish MCP connection { - "method": "mcp/connect", - "params": { - "acpId": "550e8400-e29b-41d4-a716-446655440000", - "_meta": { ... } + "jsonrpc": "2.0", + "id": 20, + "result": { + "result": { + "resultType": "complete", + "content": [ + { + "type": "text", + "text": "hello" + } + ], + "isError": false + } } } -// Response result: -{ - "connectionId": "conn-123", - "_meta": { ... } -} +``` -// Close MCP connection +An inner MCP protocol error is also a successful **outer ACP response**, using the carrier's `error` branch: + +```json { - "method": "mcp/disconnect", - "params": { - "connectionId": "conn-123", - "_meta": { ... } + "jsonrpc": "2.0", + "id": 20, + "result": { + "error": { + "code": -32602, + "message": "Unknown tool", + "data": { "name": "echo" } + } } } -// Response result: -{ - "_meta": { ... } -} ``` -**MCP message exchange:** +This preserves error-domain provenance. An inner error code is an MCP code; it must not be interpreted as an ACP error, even if the numerical codes collide. For example, an inner `-32000` must not trigger ACP authentication. An MCP tool-execution error remains a result with `isError`, not either kind of protocol error. + +The carrier requires exactly one of `result` or `error`. `result` accepts any JSON value, including explicit `null`. `error` is a non-null object with required integer `code` and string `message`; optional `data` distinguishes omission from explicit `null`. Unknown inner result and error fields are preserved. An optional carrier `_meta` contains ACP metadata and is separate from the inner outcome's metadata; omission and `null` are equivalent. + +### Binding failures + +Outer ACP errors are reserved for failures to admit, route, or execute the binding itself: + +| Code | Meaning | +| -------- | ------------------------------------------------------------------ | +| `-32602` | Malformed ACP envelope or duplicate active `(serverId, requestId)` | +| `-32800` | ACP operation cancelled | +| `-33000` | Binding resource limit exceeded | +| `-33001` | Server registration unavailable | +| `-33002` | Backend or transport failed without a valid MCP outcome | + +These binding-specific codes do not allocate new meanings in MCP's reserved `-32000` through `-32019` range. No implementation may report overload as ACP's authentication-required error. A malformed inner MCP request or an MCP error returned by a backend belongs in the outcome carrier instead. + +`server/discover` is an ordinary inner MCP request. It is supported but is not a prerequisite for calling a tool. The transport does not silently perform an MCP handshake or substitute ACP capability discovery for MCP server discovery. + +Discovery's `supportedVersions` describes the revisions available through this binding. The SDK restricts it to 2026-07-28, even when the hosted backend supports additional revisions on other transports; it does not invent support that the backend lacks. Other discovery capabilities and metadata are preserved. + +### Fields and metadata + +`serverId`, `requestId`, and `method` are required non-null strings on both requests and notifications. A caller uses a fresh `requestId` for each operation, including an MRTR retry, and must not reuse an active ID for the same server on the same ACP connection. + +The inner `params` field accepts an object or `null` and is optional at the envelope level. Omission and `null` both mean no inner parameters; positional arrays are invalid. This does not relax MCP's requirements: a valid 2026-07-28 request must include its required metadata in `params._meta`. + +An optional outer `_meta` object alongside `serverId` is ACP envelope metadata. Omission and `null` are equivalent. It is distinct from MCP metadata inside the inner parameters or result, which must be preserved. + +Providers validate the per-request MCP version and capabilities. Missing or malformed required inner metadata is an MCP invalid-parameters outcome. An unsupported version is MCP's `UnsupportedProtocolVersion` error (`-32022`), with the supported and requested versions, inside the outcome carrier. This binding's supported set contains only 2026-07-28: a caller without a mutual version surfaces that error rather than falling back to `initialize` or an earlier revision. -`mcp/message` is bidirectional. Either side can send the following request or notification shape on an established `connectionId`. +## Request-scoped notifications + +While a request is active, its provider can send a notification with the same server and logical request IDs: ```json -// Send MCP request { - "id": 123, + "jsonrpc": "2.0", "method": "mcp/message", "params": { - "connectionId": "conn-123", - "method": "", - "params": { ... }, - "_meta": { ... } + "serverId": "project-tools:7a72", + "requestId": "mcp-request:a11f", + "method": "notifications/progress", + "params": { + "progressToken": "progress-1", + "progress": 1, + "total": 2 + } } } -// Response result: -{ - ... inner MCP result payload ... -} +``` + +The outer notification has no JSON-RPC ID and receives no response. It belongs only to the identified request; a consumer must not deliver unknown or late notifications to a different operation. + +Progress tokens and other MCP metadata are preserved. Progress and any supported logging notifications belong to their original request, not an unrelated subscription. A provider stops forwarding notifications for a request once its final result or error has been sent. + +### Subscriptions + +`subscriptions/listen` is a long-lived `mcp/message` request, not a new ACP connection. Its first associated MCP notification is `notifications/subscriptions/acknowledged`. Subsequent notifications obey the acknowledged filter. + +Each subscription notification carries `io.modelcontextprotocol/subscriptionId` in its inner `_meta`. That value is the logical `requestId` of the listen request, not the outer ACP JSON-RPC ID. A graceful completion result carries the same subscription metadata. Multiple subscriptions and ordinary requests may be active concurrently. -// Send MCP notification +## Cancellation and lifetime + +Cancellation uses ACP's existing `$/cancel_request` for the **outer ACP request**; this binding adds no separate cancellation method or support requirement. A caller can request cancellation when it no longer needs an operation's result. For the example request above: + +```json { - "method": "mcp/message", + "jsonrpc": "2.0", + "method": "$/cancel_request", "params": { - "connectionId": "conn-123", - "method": "", - "params": { ... }, - "_meta": { ... } + "requestId": 20 } } ``` -The inner MCP message fields (`method`, `params`) are flattened into the params object. The `params` field is optional; if omitted or set to `null`, the inner MCP message has no params. Whether the wrapped message is a request or notification is determined by the presence of an `id` field in the outer JSON-RPC envelope, following JSON-RPC conventions. For requests, the ACP response result is the inner MCP result payload, and inner MCP errors are represented with the outer JSON-RPC error response. +This cancellation ID is hop-local. When a proxy forwards cancellation, it uses its downstream ACP request ID; it does not rewrite the logical MCP `requestId` or tunnel an unrelated hop's cancellation ID. -### Routing by ID +Cancellation is best effort. A provider may complete an operation normally if it cannot cancel it or completion wins the race. When it honors cancellation, it stops producing new notifications for that operation and answers the original ACP request with a cancellation error after cleanup. Cancelling one operation does not cancel sibling requests, subscriptions, the server registration, or the containing ACP connection. -The `acpId` in `mcp/connect` matches the `id` that was provided by the component when it declared the MCP server in `session/new`. The receiving side uses this `id` to route messages to the correct handler. +For long-lived operations such as `subscriptions/listen`, per-request cancellation is useful because it avoids closing the whole ACP connection to stop one stream. An HTTP adapter translates response-stream closure into a cancellation request upstream, following [MCP's transport-specific cancellation rules](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation). -When a component provides multiple MCP servers in a single session, each gets a unique `id`, enabling proper message routing. +Honoring cancellation is not merely deletion of a routing entry. Execution and cleanup remain supervised; admission permits and the active logical ID stay owned until cleanup finishes. A reusable service remains available for sibling operations. Work reported as cancelled before execution does not start later. Cancellation cannot roll back already-performed external side effects. -### Connection multiplexing +Releasing a provider registration or closing its ACP connection cancels all work owned by it. Transport EOF initiates cancellation; it must not wait indefinitely for an application future that is itself awaiting the disconnected peer. Unknown servers and invalid or duplicate active request IDs receive errors; malformed notifications do not receive synthetic replies. -Multiple connections to the same MCP server are supported - each `mcp/connect` returns a unique `connectionId`. This allows scenarios where an agent opens multiple concurrent connections to the same tool server. +Bound notification buffering, frame sizes, and outstanding work. Retain resource accounting while messages are queued, deferred, serialized, or held in an unread response body, not only while backend execution is active. Keep shutdown and any supported cancellation responsive when data capacity is exhausted. If an implementation cannot continue a stream safely, fail or cancel that request explicitly instead of silently dropping subscription events or growing an unbounded queue. Do not block unrelated dispatch while waiting for a slow consumer. -### Bridging for agents without native support +## Interactive tools -Not all agents will support MCP-over-ACP natively. To maintain compatibility, it is possible to write a bridge that translates ACP-transport MCP servers to transports the agent does support. +MCP 2026-07-28 replaces reverse JSON-RPC calls with MRTR. For `tools/call`, `resources/read`, or `prompts/get`, a provider may return `resultType: "input_required"` with input requests and/or opaque `requestState`. That completes the current ACP request. -**Bridging approaches:** +The agent obtains the requested input, then issues a fresh `mcp/message` request for the original operation with `inputResponses` and the exact opaque state. Each retry supplies its own MCP metadata and uses a fresh logical request ID. The transport must not parse retry state, automatically approve an elicitation, or replay side-effecting calls without the agent's policy. -- **Stdio shim**: Spawn a small shim process that the agent connects to via stdio. The shim relays MCP messages to/from the ACP channel. This is the most compatible approach since all MCP-capable agents support stdio. +This flow needs neither a persistent MCP session nor a provider-originated ACP request. -- **HTTP bridge**: Run a local HTTP server that the agent connects to. MCP messages are relayed to/from the ACP channel. This works for agents that prefer HTTP transport. +## Proxying and HTTP adaptation -**How bridging works:** +Proxies preserve `serverId`, logical `requestId`, inner payloads, and metadata. Normal ACP forwarding handles outer responses and, where supported, hop-local cancellation. Providers claim requests for their declared servers; other components forward them normally. There is no conductor MCP connection table or special connect/disconnect routing. -When a client provides an MCP server with `"type": "acp"`, and the agent doesn't advertise `mcpCapabilities.acp: true`, a bridge can: +An optional adapter may expose a native server to a **modern MCP HTTP client**. HTTP capability alone does not prove that an agent supports this MCP revision. The adapter must not add a legacy fallback. -1. Rewrite the MCP server declaration in `session/new` to use stdio or HTTP transport -2. Spawn the appropriate shim process or HTTP server -3. Relay messages between the shim and the ACP channel +The HTTP endpoint can be reused, but every POST represents its own request: -From the agent's perspective, it's talking to a normal stdio/HTTP MCP server. From the client's perspective, it's handling MCP-over-ACP messages. The bridge handles the translation transparently. +- Accept a single JSON-RPC request per POST and return JSON or request-scoped SSE. Reject batches and client-sent responses. +- Support subscriptions as long-lived POST response streams. Closing one response stream requests cancellation of only its mapped ACP request and stops delivery on that stream. +- Return 405 for GET and DELETE. Do not issue session headers or implement SSE resumption. +- Validate protocol-version, method, name, and applicable mirrored tool-parameter headers against the body, including required value decoding. +- Validate supplied Origin headers, bind local listeners to loopback, and enforce access control. A random port is not authentication. +- Allocate independent logical MCP IDs for overlapping HTTP request IDs. Translate MCP ID references at this transport boundary, including subscription IDs, while preserving progress tokens and opaque state. ACP proxies do not perform that translation. -```mermaid -sequenceDiagram - participant Client - participant Bridge - participant Shim as Stdio Shim - participant Agent +### Native-tool re-export - Note over Bridge: Agent doesn't support mcpCapabilities.acp - Client->>Bridge: session/new (MCP server with acp transport) - Bridge->>Agent: session/new (MCP server with stdio transport) - Note over Bridge: Spawns shim for bridging +The Rust adapter exposes a **new local HTTP endpoint for native tools**, not a transparent tunnel for another HTTP endpoint's routing or authorization policy. It uses one loopback listener per ACP connection. A non-secret encoding of `serverId` identifies the route, and a connection-specific bearer credential is bound to that server. Credentials never appear in URLs. The provider still decides whether the registration exists and the caller is authorized; retaining an old URL does not resurrect a removed registration. - Agent->>Shim: MCP tool call (stdio) - Shim->>Bridge: relay - Bridge->>Client: mcp/message - Client-->>Bridge: tool result - Bridge-->>Shim: relay - Shim-->>Agent: MCP response (stdio) -``` +This endpoint does not advertise tool-parameter header mirroring. It removes `x-mcp-header` only from actual schema annotation positions in returned tool descriptors, preserving validation keywords, argument names, and example/default data. It rejects supplied `Mcp-Param-*` headers rather than assigning them authority. Standard method/name/version header validation still applies. + +Each `tools/call` goes directly to its native provider without hidden `tools/list` requests. This avoids a descriptor lookup/execution race and does not introduce a discovery prerequisite. Native ACP passthrough preserves descriptors unchanged. A deployment requiring an existing HTTP gateway's parameter-header policy must implement that policy at the new endpoint or decline this re-export; native execution cannot inherit HTTP headers that were never transported. + +HTTP is an adapter, not a prerequisite for the native protocol. Its validation and security surface must be tested separately; working native tool calls do not establish HTTP conformance. + +## Security + +Server IDs and request IDs are routing identifiers, not credentials. Providers bind server ownership and visibility to the supplying ACP component and authorized callers. Self-reported MCP `clientInfo` is not an authentication identity. + +Keep outer ACP metadata separate from inner MCP metadata. Do not persist runtime credentials from rewritten HTTP declarations in traces. -A first implementation of this bridging exists in the `sacp-conductor` crate, part of the proposed new version of the [ACP Rust SDK](https://github.com/anthropics/rust-sdk). +Servers treat MRTR state as attacker-controlled input. Where it influences authorization or business logic, protect integrity and address principal binding, expiry, replay, and single-use requirements. Intermediaries keep it opaque. -## Frequently asked questions +Tool lists vary by explicit server identity and authorization scope, not hidden connection state. Preserve required cache metadata and do not share private catalogs across callers. -> What questions have arisen over the course of authoring this document or during subsequent discussions? +## Implementation and validation -### Why use a separate `id` instead of server names? +The Rust implementation uses each ACP version's unstable `MessageMcpRequest`, `MessageMcpResponse`, `MessageMcpNotification`, `McpError`, and `McpRequestId` schema types. The v1 and v2 response and error types are defined independently so either version can evolve without changing the other. Their JSON representation is currently the same. -Server names in `mcpServers` are chosen by whoever adds them to the session, and could potentially collide if multiple components add servers. A component-generated `id` provides guaranteed uniqueness and allows the providing component to correlate incoming messages back to the correct session context. +The SDK's target API separates a reusable `McpService` from each owned operation. A `McpRequestContext` supplies logical/server identity, validated metadata and capabilities, cancellation, and request-scoped notifications. A backend factory is an explicit adapter for implementations that need per-operation construction, not a requirement imposed by stateless MCP. Integration adapters must supervise any tasks spawned by their underlying library, not assume dropping a wrapper joins detached handlers. -This also avoids a potential deadlock: some agents don't return the session ID until after MCP servers have been initialized. Using a component-generated `id` avoids any dependency on agent-provided identifiers. +Reference implementation tests cover: -### How does this relate to proxy chains? +- Real discovery and tool calls without MCP initialization, directly and through a proxy. +- Stable logical IDs when ACP outer IDs are renumbered. +- Independent concurrent requests, request-specific metadata/errors, and rejection of duplicate active IDs. +- Separation of inner MCP errors from outer ACP errors, including colliding codes, explicit null data, and unknown extension fields. +- MRTR with nonempty input responses, opaque state, and fresh retry IDs. +- Subscription acknowledgement ordering, filtered notifications, correlation, and graceful completion. +- Cancellation of queued and running tools and subscriptions, including noncooperative user futures, registration removal, transport EOF, late-message rejection, and joined resource cleanup. +- Slow readers, bounded pending work, control-plane liveness under saturation, and admission recovery after response-body release. +- HTTP request isolation, header/Origin/access checks, direct annotated native-tool calls without preliminary requests, request-close cancellation, and rejection of removed transport behavior. -MCP-over-ACP is a transport mechanism that works independently of proxy chains. However, proxy chains are a natural use case: a proxy can inject MCP servers into sessions it forwards, handle the tool callbacks, and use the results to enhance its transformations. +These tests verify the binding, not every optional MCP feature. Tasks, Apps, and other extensions remain opt-in. The new design should not depend on deprecated Roots, Sampling, or Logging features. -See the [Proxy Chains RFD](./proxy-chains) for details on how MCP-over-ACP enables context-aware tooling. +### Stabilization gates -### What if the agent doesn't support ACP transport? +The Rust reference implementation exercises the versioned response carriers, reusable services, bounded transport queues, and native-tool HTTP re-export together. Its cleanup regression deliberately pauses a tool runner while ACP continues dispatching: cancellation cannot settle or release the logical request ID until the runner drops the tool future. Both mutable and concurrent tools use this rule. Independent item-count limits are not evidence of complete memory bounds, and dropping a task handle is not evidence that its work stopped. -See the [Bridging for agents without native support](#bridging-for-agents-without-native-support) section above. A bridge can transparently translate ACP-transport MCP servers to stdio or HTTP for agents that don't advertise `mcpCapabilities.acp` support. +Concrete buffer sizes and concurrency quotas are implementation policies, not wire-protocol constants. Their behavior must be configurable or documented, and quota failures must preserve the error-domain distinction above. Both ACP v1 and draft v2 must exercise the same binding semantics; this proposal does not otherwise stabilize ACP v2 or optional MCP extensions. -### What about security? +The Rust reference implementation supervises cancellation and joins owned backend work before reporting it complete; it cannot forcibly terminate detached application work. These are implementation safeguards, not additional cancellation requirements for advertising the transport. The reference tests establish the covered binding behavior, not full conformance for every MCP feature or readiness to publish packages. The draft schema must be released and dependent SDK major versions coordinated before publication. -MCP-over-ACP has the same trust model as regular MCP: you're allowing a component to handle tool invocations. The difference is transport, not trust. Components should only add MCP servers from sources they trust, same as with stdio or HTTP transport. +## References and revision history -## Revision history +Normative MCP references: [2026-07-28 specification](https://modelcontextprotocol.io/specification/2026-07-28), [versioning](https://modelcontextprotocol.io/specification/2026-07-28/basic/lifecycle), [MRTR](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr), [subscriptions](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions), [cancellation](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation), and [Streamable HTTP](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http). -Split from proxy-chains RFD to enable independent use of MCP-over-ACP transport by any ACP component, not just proxies. +This proposal was split from the [proxy-chain RFD](./proxy-chains). Earlier drafts used server `id`/`acpId`, an MCP connection ID, connect/disconnect methods, and reverse requests. Those drafts and the stateful implementation checkpoint are not compatibility commitments. This revision replaces them with server-addressed requests and request-scoped notifications targeting MCP 2026-07-28 only. diff --git a/docs/rfds/proxy-chains.mdx b/docs/rfds/proxy-chains.mdx index eb89a05c1..0efa07fc5 100644 --- a/docs/rfds/proxy-chains.mdx +++ b/docs/rfds/proxy-chains.mdx @@ -253,9 +253,11 @@ Note: A conductor can be configured to run in either terminal mode (expecting `i ### MCP-over-ACP support -Proxies that provide MCP servers use the [MCP-over-ACP transport](./mcp-over-acp) mechanism. The conductor always advertises `mcpCapabilities.acp: true` to proxies and handles bridging for agents that don't support native ACP transport. +The [MCP-over-ACP transport](./mcp-over-acp) targets stateless MCP 2026-07-28 only. Requests target a declared `serverId`; a separate logical MCP `requestId` correlates request-scoped notifications and remains unchanged when proxies renumber the outer ACP JSON-RPC ID. There is no MCP connect/disconnect lifecycle. -All proxies MUST respond to `proxy/initialize` with the MCP-over-ACP capability enabled. When the conductor sends `proxy/initialize`, proxies should be prepared to handle `mcp/connect`, `mcp/message`, and `mcp/disconnect` messages for any MCP servers they provide. +Proxies that provide MCP servers use the [MCP-over-ACP transport](./mcp-over-acp) mechanism. Capability advertising reflects what the downstream chain can consume; the conductor does not unconditionally add MCP-over-ACP support. In the Rust SDK, an explicit `McpOverAcpPolyfill` proxy can be placed immediately before an HTTP-capable agent that lacks native ACP MCP support. + +A forwarding proxy preserves downstream MCP capabilities. A bridging proxy may advertise ACP MCP support only when its successor can consume the target MCP revision over the adapted transport. Proxies that publish MCP servers handle `mcp/message` requests as soon as their declarations are forwarded, including while session setup is still in progress. Ordinary ACP response forwarding and hop-local cancellation apply; logical MCP IDs and payloads are preserved. See the transport RFD for the v1 and draft-v2 capability shapes. ### Message reference @@ -409,9 +411,9 @@ The key advantage is that proxy-based extensions work with any ACP-compatible ag Proxies can provide MCP servers via [MCP-over-ACP transport](./mcp-over-acp), enabling a single proxy to add context, provide tools, and handle callbacks with full awareness of the conversation state. -The conductor always advertises `mcpCapabilities.acp: true` to proxies, regardless of whether the downstream agent supports it natively. When the agent doesn't support ACP transport, the conductor handles bridging transparently - spawning stdio shims or HTTP servers that the agent connects to normally, then relaying messages to/from the proxy's ACP channel. +When the agent supports native ACP MCP transport, no adapter is needed. Otherwise, the chain can include an explicit adapter for a modern MCP HTTP client. The Rust SDK's HTTP polyfill rewrites MCP declarations to local HTTP endpoints and relays requests and request-scoped notifications to and from the providing proxy's ACP channel. Requests may share a listening endpoint without sharing MCP session state. -This means proxy authors don't need to worry about agent compatibility - they implement MCP-over-ACP, and the conductor handles the rest. +Tool-providing proxies implement MCP-over-ACP without managing those alternative transports themselves. The chain's owner chooses an appropriate adapter, and the resulting advertised capability tells providers whether ACP MCP servers can be consumed. ```mermaid sequenceDiagram diff --git a/schema-generator/src/main.rs b/schema-generator/src/main.rs index b5690d4e0..b15ca1c50 100644 --- a/schema-generator/src/main.rs +++ b/schema-generator/src/main.rs @@ -1648,6 +1648,15 @@ starting with '$/' it is free to ignore the notification." } fn get_type_string(schema: &Value) -> String { + // An unconstrained JSON Schema (possibly with only a description) + // accepts every JSON value, not only objects. + if schema + .as_object() + .is_some_and(|fields| fields.keys().all(|key| key == "description")) + { + return "\"any\"".to_string(); + } + // Check for $ref if let Some(ref_val) = schema.get("$ref").and_then(|v| v.as_str()) { let type_name = ref_val.strip_prefix("#/$defs/").unwrap_or(ref_val); @@ -1931,7 +1940,7 @@ starting with '$/' it is free to ignore the notification." "document/didClose" => self.agent.get("DidCloseDocumentNotification").unwrap(), "document/didSave" => self.agent.get("DidSaveDocumentNotification").unwrap(), "document/didFocus" => self.agent.get("DidFocusDocumentNotification").unwrap(), - "mcp/message" => self.agent.get("MessageMcpRequest").unwrap(), + "mcp/message" => self.agent.get("MessageMcpNotification").unwrap(), _ => panic!("Introduced a method? Add it here :)"), } } @@ -1957,9 +1966,7 @@ starting with '$/' it is free to ignore the notification." "elicitation/complete" => { self.client.get("CompleteElicitationNotification").unwrap() } - "mcp/connect" => self.client.get("ConnectMcpRequest").unwrap(), "mcp/message" => self.client.get("MessageMcpRequest").unwrap(), - "mcp/disconnect" => self.client.get("DisconnectMcpRequest").unwrap(), _ => panic!("Introduced a method? Add it here :)"), } } @@ -2112,6 +2119,19 @@ starting with '$/' it is free to ignore the notification." use super::MarkdownGenerator; use serde_json::json; + #[test] + fn unconstrained_json_schema_renders_any_value() { + assert_eq!(MarkdownGenerator::get_type_string(&json!({})), "\"any\""); + assert_eq!( + MarkdownGenerator::get_type_string(&json!({"description": "Opaque MCP result"})), + "\"any\"" + ); + assert_eq!( + MarkdownGenerator::get_type_string(&json!({"type": "object"})), + "\"object\"" + ); + } + #[test] fn document_union_includes_shared_properties() { let mut generator = MarkdownGenerator::new("schema.json"); diff --git a/schema/v1/meta.unstable.json b/schema/v1/meta.unstable.json index d8937f384..956449fc0 100644 --- a/schema/v1/meta.unstable.json +++ b/schema/v1/meta.unstable.json @@ -40,9 +40,7 @@ "terminal_release": "terminal/release", "terminal_wait_for_exit": "terminal/wait_for_exit", "terminal_kill": "terminal/kill", - "mcp_connect": "mcp/connect", "mcp_message": "mcp/message", - "mcp_disconnect": "mcp/disconnect", "elicitation_create": "elicitation/create", "elicitation_complete": "elicitation/complete" }, diff --git a/schema/v1/schema.unstable.json b/schema/v1/schema.unstable.json index dafc52359..3561b2f96 100644 --- a/schema/v1/schema.unstable.json +++ b/schema/v1/schema.unstable.json @@ -222,15 +222,6 @@ } ] }, - { - "title": "ConnectMcpRequest", - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nOpens an MCP-over-ACP connection.", - "allOf": [ - { - "$ref": "#/$defs/ConnectMcpRequest" - } - ] - }, { "title": "MessageMcpRequest", "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nExchanges an MCP-over-ACP message.", @@ -240,15 +231,6 @@ } ] }, - { - "title": "DisconnectMcpRequest", - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nCloses an MCP-over-ACP connection.", - "allOf": [ - { - "$ref": "#/$defs/DisconnectMcpRequest" - } - ] - }, { "title": "ExtMethodRequest", "description": "Handles extension method requests from the agent.\n\nAllows the Agent to send an arbitrary request that is not part of the ACP spec.\nExtension methods provide a way to add custom functionality while maintaining\nprotocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", @@ -2193,42 +2175,23 @@ ], "required": ["elicitationId", "url"] }, - "ConnectMcpRequest": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequest parameters for `mcp/connect`.", + "MessageMcpRequest": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequest parameters for `mcp/message`.", "type": "object", "properties": { "serverId": { - "description": "The ACP MCP server ID that was provided by the component declaring the MCP server.", + "description": "The declared ACP MCP server receiving this request.", "allOf": [ { "$ref": "#/$defs/McpServerAcpId" } ] }, - "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true - } - }, - "required": ["serverId"], - "x-side": "client", - "x-method": "mcp/connect" - }, - "McpServerAcpId": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nUnique identifier for an MCP server using the ACP transport.\n\nThe value is opaque and generated by the ACP component providing the MCP server. It is\nused by `mcp/connect` to route connection requests back to the component that declared the\nserver.", - "type": "string" - }, - "MessageMcpRequest": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequest parameters for `mcp/message`.", - "type": "object", - "properties": { - "connectionId": { - "description": "The MCP-over-ACP connection this message is sent on.", + "requestId": { + "description": "The caller-generated identifier for the inner MCP request.", "allOf": [ { - "$ref": "#/$defs/McpConnectionId" + "$ref": "#/$defs/McpRequestId" } ] }, @@ -2248,36 +2211,17 @@ "additionalProperties": true } }, - "required": ["connectionId", "method"], - "x-side": "both", + "required": ["serverId", "requestId", "method"], + "x-side": "client", "x-method": "mcp/message" }, - "McpConnectionId": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nA unique identifier for an active MCP-over-ACP connection.", + "McpServerAcpId": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nUnique identifier for an MCP server using the ACP transport.\n\nThe value is opaque and generated by the ACP component providing the MCP server. It is\nused by `mcp/message` to route requests to the component that declared the server.", "type": "string" }, - "DisconnectMcpRequest": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequest parameters for `mcp/disconnect`.", - "type": "object", - "properties": { - "connectionId": { - "description": "The MCP-over-ACP connection to close.", - "allOf": [ - { - "$ref": "#/$defs/McpConnectionId" - } - ] - }, - "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true - } - }, - "required": ["connectionId"], - "x-side": "client", - "x-method": "mcp/disconnect" + "McpRequestId": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nIdentifies an inner MCP request active against a server on this ACP connection.\n\nGenerated by the caller and preserved unchanged by proxies. This is distinct\nfrom the outer ACP JSON-RPC request ID.", + "type": "string" }, "ExtRequest": { "description": "Allows for sending an arbitrary request that is not part of the ACP spec.\nExtension methods provide a way to add custom functionality while maintaining\nprotocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)" @@ -2480,15 +2424,6 @@ "$ref": "#/$defs/ExtResponse" } ] - }, - { - "title": "MessageMcpResponse", - "description": "Successful result returned by an MCP-over-ACP `mcp/message` request.", - "allOf": [ - { - "$ref": "#/$defs/MessageMcpResponse" - } - ] } ] } @@ -4787,11 +4722,6 @@ "ExtResponse": { "description": "Allows for sending an arbitrary response to an [`ExtRequest`] that is not part of the ACP spec.\nExtension methods provide a way to add custom functionality while maintaining\nprotocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)" }, - "MessageMcpResponse": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nResponse to `mcp/message`.\n\nThis is the inner MCP response result payload. Any JSON value is valid.", - "x-side": "both", - "x-method": "mcp/message" - }, "Error": { "description": "JSON-RPC error object.\n\nRepresents an error that occurred during method execution, following the\nJSON-RPC 2.0 error object specification with optional additional data.\n\nSee protocol docs: [JSON-RPC Error Object](https://www.jsonrpc.org/specification#error_object)", "type": "object", @@ -4914,15 +4844,6 @@ } ] }, - { - "title": "MessageMcpNotification", - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nReceives an MCP-over-ACP notification.", - "allOf": [ - { - "$ref": "#/$defs/MessageMcpNotification" - } - ] - }, { "title": "ExtNotification", "description": "Handles extension notifications from the agent.\n\nAllows the Agent to send an arbitrary notification that is not part of the ACP spec.\nExtension notifications provide a way to send one-way messages for custom functionality\nwhile maintaining protocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", @@ -6021,39 +5942,6 @@ "x-side": "client", "x-method": "elicitation/complete" }, - "MessageMcpNotification": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nNotification parameters for `mcp/message`.\n\nThis is used when the wrapped MCP message is a notification and the outer JSON-RPC\nenvelope has no `id`.", - "type": "object", - "properties": { - "connectionId": { - "description": "The MCP-over-ACP connection this message is sent on.", - "allOf": [ - { - "$ref": "#/$defs/McpConnectionId" - } - ] - }, - "method": { - "description": "The inner MCP method name.", - "type": "string" - }, - "params": { - "description": "Optional inner MCP params.\n\nIf omitted or set to `null`, the inner MCP message has no params.", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true - }, - "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true - } - }, - "required": ["connectionId", "method"], - "x-side": "both", - "x-method": "mcp/message" - }, "ExtNotification": { "description": "Allows the Agent to send an arbitrary notification that is not part of the ACP spec.\nExtension notifications provide a way to send one-way messages for custom functionality\nwhile maintaining protocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)" }, @@ -6250,15 +6138,6 @@ } ] }, - { - "title": "MessageMcpRequest", - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nExchanges an MCP-over-ACP message.", - "allOf": [ - { - "$ref": "#/$defs/MessageMcpRequest" - } - ] - }, { "title": "ExtMethodRequest", "description": "Handles extension method requests from the client.\n\nExtension methods provide a way to add custom functionality while maintaining\nprotocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", @@ -7016,7 +6895,7 @@ "required": ["name", "url", "headers"] }, "McpServerAcp": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nACP transport configuration for MCP.\n\nThe MCP server is provided by an ACP component and communicates over the ACP channel\nusing `mcp/connect`, `mcp/message`, and `mcp/disconnect`.", + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nACP transport configuration for MCP.\n\nThe MCP server is provided by an ACP component and communicates over the ACP channel\nusing `mcp/message`.", "type": "object", "properties": { "name": { @@ -7990,24 +7869,6 @@ } ] }, - { - "title": "ConnectMcpResponse", - "description": "Successful result returned for a `mcp/connect` request.", - "allOf": [ - { - "$ref": "#/$defs/ConnectMcpResponse" - } - ] - }, - { - "title": "DisconnectMcpResponse", - "description": "Successful result returned for a `mcp/disconnect` request.", - "allOf": [ - { - "$ref": "#/$defs/DisconnectMcpResponse" - } - ] - }, { "title": "MessageMcpResponse", "description": "Successful result returned by an MCP-over-ACP `mcp/message` request.", @@ -8455,42 +8316,73 @@ } } }, - "ConnectMcpResponse": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nResponse to `mcp/connect`.", - "type": "object", - "properties": { - "connectionId": { - "description": "The unique identifier for this MCP-over-ACP connection.", - "allOf": [ - { - "$ref": "#/$defs/McpConnectionId" + "MessageMcpResponse": { + "description": "**UNSTABLE**\n\nThe successful outer ACP `mcp/message` response carries exactly one\ninner MCP outcome: an opaque result (including JSON null), or an MCP error.\nOuter ACP errors are reserved for binding and runtime failures.\n\nBoth branches require their carrier key. An error must be a non-null object.\nCarrier `_meta` is optional; null is equivalent to omission.", + "anyOf": [ + { + "title": "Result", + "description": "An opaque inner MCP result.", + "type": "object", + "properties": { + "result": { + "description": "Required, even if JSON null." + }, + "_meta": { + "description": "Optional ACP carrier metadata.", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true } - ] + }, + "required": ["result"], + "additionalProperties": false }, - "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true + { + "title": "Error", + "description": "A structured inner MCP error.", + "type": "object", + "properties": { + "error": { + "description": "Required, non-null MCP error object.", + "allOf": [ + { + "$ref": "#/$defs/McpError" + } + ] + }, + "_meta": { + "description": "Optional ACP carrier metadata.", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": ["error"], + "additionalProperties": false } - }, - "required": ["connectionId"], + ], "x-side": "client", - "x-method": "mcp/connect" + "x-method": "mcp/message" }, - "DisconnectMcpResponse": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nResponse to `mcp/disconnect`.", + "McpError": { + "description": "**UNSTABLE**\n\nAn inner MCP error, distinct from an outer ACP binding or runtime error.\n\n`code` and `message` are required and non-null. `data` is optional;\nexplicit `null` is preserved separately from an omitted key.", "type": "object", "properties": { - "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true + "code": { + "description": "Inner MCP error code; never an ACP error code.", + "type": "integer", + "format": "int32" + }, + "message": { + "description": "Inner MCP error message.", + "type": "string" + }, + "data": { + "description": "Optional error data; explicit null is retained." } }, - "x-side": "client", - "x-method": "mcp/disconnect" + "required": ["code", "message"], + "additionalProperties": true }, "ClientNotification": { "description": "A JSON-RPC notification object.", @@ -8940,6 +8832,46 @@ } ] }, + "MessageMcpNotification": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nNotification parameters for `mcp/message`.\n\nSent by the provider to the consumer for an active request (including\nsubscription acknowledgements and updates); the outer envelope has no `id`.", + "type": "object", + "properties": { + "serverId": { + "description": "The declared ACP MCP server handling the associated request.", + "allOf": [ + { + "$ref": "#/$defs/McpServerAcpId" + } + ] + }, + "requestId": { + "description": "The identifier of the active inner MCP request.", + "allOf": [ + { + "$ref": "#/$defs/McpRequestId" + } + ] + }, + "method": { + "description": "The inner MCP method name.", + "type": "string" + }, + "params": { + "description": "Optional inner MCP params.\n\nIf omitted or set to `null`, the inner MCP message has no params.", + "type": ["object", "null"], + "additionalProperties": true + }, + "_meta": { + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": ["serverId", "requestId", "method"], + "x-side": "agent", + "x-method": "mcp/message" + }, "CancelRequestNotification": { "description": "Notification to cancel an ongoing request.\n\nSee protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/cancellation)", "type": "object", diff --git a/schema/v2/meta.unstable.json b/schema/v2/meta.unstable.json index 47d7973cb..1912c7914 100644 --- a/schema/v2/meta.unstable.json +++ b/schema/v2/meta.unstable.json @@ -31,9 +31,7 @@ "clientMethods": { "session_request_permission": "session/request_permission", "session_update": "session/update", - "mcp_connect": "mcp/connect", "mcp_message": "mcp/message", - "mcp_disconnect": "mcp/disconnect", "elicitation_create": "elicitation/create", "elicitation_complete": "elicitation/complete" }, diff --git a/schema/v2/schema.unstable.json b/schema/v2/schema.unstable.json index 03c0e762e..3f3afa744 100644 --- a/schema/v2/schema.unstable.json +++ b/schema/v2/schema.unstable.json @@ -313,15 +313,6 @@ "$ref": "#/$defs/ExtResponse" } ] - }, - { - "title": "MessageMcpResponse", - "description": "Successful result returned by an MCP-over-ACP `mcp/message` request.", - "allOf": [ - { - "$ref": "#/$defs/MessageMcpResponse" - } - ] } ] } @@ -449,24 +440,6 @@ } ] }, - { - "title": "ConnectMcpResponse", - "description": "Successful result returned for a `mcp/connect` request.", - "allOf": [ - { - "$ref": "#/$defs/ConnectMcpResponse" - } - ] - }, - { - "title": "DisconnectMcpResponse", - "description": "Successful result returned for a `mcp/disconnect` request.", - "allOf": [ - { - "$ref": "#/$defs/DisconnectMcpResponse" - } - ] - }, { "title": "MessageMcpResponse", "description": "Successful result returned by an MCP-over-ACP `mcp/message` request.", @@ -600,15 +573,6 @@ } ] }, - { - "title": "ConnectMcpRequest", - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nOpens an MCP-over-ACP connection.", - "allOf": [ - { - "$ref": "#/$defs/ConnectMcpRequest" - } - ] - }, { "title": "MessageMcpRequest", "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nExchanges an MCP-over-ACP message.", @@ -618,15 +582,6 @@ } ] }, - { - "title": "DisconnectMcpRequest", - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nCloses an MCP-over-ACP connection.", - "allOf": [ - { - "$ref": "#/$defs/DisconnectMcpRequest" - } - ] - }, { "title": "ExtMethodRequest", "description": "Handles extension method requests from the agent.\n\nAllows the Agent to send an arbitrary request that is not part of the ACP spec.\nExtension methods provide a way to add custom functionality while maintaining\nprotocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)", @@ -3010,42 +2965,23 @@ ], "required": ["elicitationId", "url"] }, - "ConnectMcpRequest": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequest parameters for `mcp/connect`.", + "MessageMcpRequest": { + "description": "**UNSTABLE**\n\nRequest parameters for `mcp/message`, sent from consumer to provider.", "type": "object", "properties": { "serverId": { - "description": "The ACP MCP server ID that was provided by the component declaring the MCP server.", + "description": "The declared ACP MCP server receiving this request.", "allOf": [ { "$ref": "#/$defs/McpServerAcpId" } ] }, - "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true - } - }, - "required": ["serverId"], - "x-side": "client", - "x-method": "mcp/connect" - }, - "McpServerAcpId": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nUnique identifier for an MCP server using the ACP transport.\n\nThe value is opaque and generated by the ACP component providing the MCP server. It is\nused by `mcp/connect` to route connection requests back to the component that declared the\nserver.", - "type": "string" - }, - "MessageMcpRequest": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequest parameters for `mcp/message`.", - "type": "object", - "properties": { - "connectionId": { - "description": "The MCP-over-ACP connection this message is sent on.", + "requestId": { + "description": "The caller-generated identifier for the inner MCP request.", "allOf": [ { - "$ref": "#/$defs/McpConnectionId" + "$ref": "#/$defs/McpRequestId" } ] }, @@ -3054,47 +2990,28 @@ "type": "string" }, "params": { - "description": "Optional inner MCP params.\n\nIf omitted or set to `null`, the inner MCP message has no params.", + "description": "Optional inner MCP params; null is equivalent to omission.", "type": ["object", "null"], "additionalProperties": true }, "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)", + "description": "ACP extension metadata (not inner MCP params._meta); null is equivalent to omission.", "type": ["object", "null"], "x-deserialize-default-on-error": true, "additionalProperties": true } }, - "required": ["connectionId", "method"], - "x-side": "both", + "required": ["serverId", "requestId", "method"], + "x-side": "client", "x-method": "mcp/message" }, - "McpConnectionId": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nA unique identifier for an active MCP-over-ACP connection.", + "McpServerAcpId": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nUnique identifier for an MCP server using the ACP transport.\n\nThe value is opaque and generated by the ACP component providing the MCP server. It is\nused by `mcp/message` to route requests to the component that declared the server.", "type": "string" }, - "DisconnectMcpRequest": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequest parameters for `mcp/disconnect`.", - "type": "object", - "properties": { - "connectionId": { - "description": "The MCP-over-ACP connection to close.", - "allOf": [ - { - "$ref": "#/$defs/McpConnectionId" - } - ] - }, - "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true - } - }, - "required": ["connectionId"], - "x-side": "client", - "x-method": "mcp/disconnect" + "McpRequestId": { + "description": "**UNSTABLE**\n\nIdentifies an inner MCP request active against a server on this ACP connection.\nGenerated by the caller and preserved unchanged by proxies, independently of\nthe outer ACP JSON-RPC request ID.", + "type": "string" }, "ExtRequest": { "description": "Allows for sending an arbitrary request that is not part of the ACP spec.\nExtension methods provide a way to add custom functionality while maintaining\nprotocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)" @@ -3279,15 +3196,6 @@ "$ref": "#/$defs/ExtResponse" } ] - }, - { - "title": "MessageMcpResponse", - "description": "Successful result returned by an MCP-over-ACP `mcp/message` request.", - "allOf": [ - { - "$ref": "#/$defs/MessageMcpResponse" - } - ] } ] } @@ -5541,11 +5449,6 @@ "ExtResponse": { "description": "Allows for sending an arbitrary response to an [`ExtRequest`] that is not part of the ACP spec.\nExtension methods provide a way to add custom functionality while maintaining\nprotocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)" }, - "MessageMcpResponse": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nResponse to `mcp/message`.\n\nThis is the inner MCP response result payload. Any JSON value is valid.", - "x-side": "both", - "x-method": "mcp/message" - }, "Error": { "description": "JSON-RPC error object.\n\nRepresents an error that occurred during method execution, following the\nJSON-RPC 2.0 error object specification with optional additional data.\n\nSee protocol docs: [JSON-RPC Error Object](https://www.jsonrpc.org/specification#error_object)", "type": "object", @@ -5668,15 +5571,6 @@ } ] }, - { - "title": "MessageMcpNotification", - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nReceives an MCP-over-ACP notification.", - "allOf": [ - { - "$ref": "#/$defs/MessageMcpNotification" - } - ] - }, { "title": "ExtNotification", "description": "Handles extension notifications from the agent.\n\nAllows the Agent to send an arbitrary notification that is not part of the ACP spec.\nExtension notifications provide a way to send one-way messages for custom functionality\nwhile maintaining protocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)", @@ -7528,39 +7422,6 @@ "x-side": "client", "x-method": "elicitation/complete" }, - "MessageMcpNotification": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nNotification parameters for `mcp/message`.\n\nThis is used when the wrapped MCP message is a notification and the outer JSON-RPC\nenvelope has no `id`.", - "type": "object", - "properties": { - "connectionId": { - "description": "The MCP-over-ACP connection this message is sent on.", - "allOf": [ - { - "$ref": "#/$defs/McpConnectionId" - } - ] - }, - "method": { - "description": "The inner MCP method name.", - "type": "string" - }, - "params": { - "description": "Optional inner MCP params.\n\nIf omitted or set to `null`, the inner MCP message has no params.", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true - }, - "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true - } - }, - "required": ["connectionId", "method"], - "x-side": "both", - "x-method": "mcp/message" - }, "ExtNotification": { "description": "Allows the Agent to send an arbitrary notification that is not part of the ACP spec.\nExtension notifications provide a way to send one-way messages for custom functionality\nwhile maintaining protocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)" }, @@ -7739,15 +7600,6 @@ } ] }, - { - "title": "MessageMcpRequest", - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nExchanges an MCP-over-ACP message.", - "allOf": [ - { - "$ref": "#/$defs/MessageMcpRequest" - } - ] - }, { "title": "ExtMethodRequest", "description": "Handles extension method requests from the client.\n\nExtension methods provide a way to add custom functionality while maintaining\nprotocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)", @@ -8355,7 +8207,7 @@ "required": ["name", "url"] }, "McpServerAcp": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nACP transport configuration for MCP.\n\nThe MCP server is provided by an ACP component and communicates over the ACP channel\nusing `mcp/connect`, `mcp/message`, and `mcp/disconnect`.", + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nACP transport configuration for MCP.\n\nThe MCP server is provided by an ACP component and communicates over the ACP channel\nusing `mcp/message`.", "type": "object", "properties": { "name": { @@ -9356,24 +9208,6 @@ } ] }, - { - "title": "ConnectMcpResponse", - "description": "Successful result returned for a `mcp/connect` request.", - "allOf": [ - { - "$ref": "#/$defs/ConnectMcpResponse" - } - ] - }, - { - "title": "DisconnectMcpResponse", - "description": "Successful result returned for a `mcp/disconnect` request.", - "allOf": [ - { - "$ref": "#/$defs/DisconnectMcpResponse" - } - ] - }, { "title": "MessageMcpResponse", "description": "Successful result returned by an MCP-over-ACP `mcp/message` request.", @@ -9686,42 +9520,73 @@ } } }, - "ConnectMcpResponse": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nResponse to `mcp/connect`.", - "type": "object", - "properties": { - "connectionId": { - "description": "The unique identifier for this MCP-over-ACP connection.", - "allOf": [ - { - "$ref": "#/$defs/McpConnectionId" + "MessageMcpResponse": { + "description": "**UNSTABLE**\n\nThe successful outer ACP `mcp/message` response carries exactly one\ninner MCP outcome: an opaque result (including JSON null), or an MCP error.\nOuter ACP errors are reserved for binding and runtime failures.\n\nBoth branches require their carrier key. An error must be a non-null object.\nCarrier `_meta` is optional; null is equivalent to omission.", + "anyOf": [ + { + "title": "Result", + "description": "An opaque inner MCP result.", + "type": "object", + "properties": { + "result": { + "description": "Required, even if JSON null." + }, + "_meta": { + "description": "Optional ACP carrier metadata.", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true } - ] + }, + "required": ["result"], + "additionalProperties": false }, - "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true + { + "title": "Error", + "description": "A structured inner MCP error.", + "type": "object", + "properties": { + "error": { + "description": "Required, non-null MCP error object.", + "allOf": [ + { + "$ref": "#/$defs/McpError" + } + ] + }, + "_meta": { + "description": "Optional ACP carrier metadata.", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": ["error"], + "additionalProperties": false } - }, - "required": ["connectionId"], + ], "x-side": "client", - "x-method": "mcp/connect" + "x-method": "mcp/message" }, - "DisconnectMcpResponse": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nResponse to `mcp/disconnect`.", + "McpError": { + "description": "**UNSTABLE**\n\nAn inner MCP error, distinct from an outer ACP binding or runtime error.\n\n`code` and `message` are required and non-null. `data` is optional;\nexplicit `null` is preserved separately from an omitted key.", "type": "object", "properties": { - "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true + "code": { + "description": "Inner MCP error code; never an ACP error code.", + "type": "integer", + "format": "int32" + }, + "message": { + "description": "Inner MCP error message.", + "type": "string" + }, + "data": { + "description": "Optional error data; explicit null is retained." } }, - "x-side": "client", - "x-method": "mcp/disconnect" + "required": ["code", "message"], + "additionalProperties": true }, "ClientNotification": { "description": "A JSON-RPC notification object.", @@ -10181,6 +10046,46 @@ } ] }, + "MessageMcpNotification": { + "description": "**UNSTABLE**\n\nNotification for an active request, sent from provider to consumer.\nIncludes subscription acknowledgements and updates.", + "type": "object", + "properties": { + "serverId": { + "description": "The declared ACP MCP server handling the associated request.", + "allOf": [ + { + "$ref": "#/$defs/McpServerAcpId" + } + ] + }, + "requestId": { + "description": "The identifier of the active inner MCP request.", + "allOf": [ + { + "$ref": "#/$defs/McpRequestId" + } + ] + }, + "method": { + "description": "The inner MCP method name.", + "type": "string" + }, + "params": { + "description": "Optional inner MCP params; null is equivalent to omission.", + "type": ["object", "null"], + "additionalProperties": true + }, + "_meta": { + "description": "ACP extension metadata (not inner MCP params._meta); null is equivalent to omission.", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": ["serverId", "requestId", "method"], + "x-side": "agent", + "x-method": "mcp/message" + }, "ProtocolLevelNotification": { "description": "A JSON-RPC notification object.", "type": "object",