Skip to main content

agent_client_protocol/schema/
proxy_protocol.rs

1//! Protocol types for proxy and MCP-over-ACP communication.
2//!
3//! These types are intended to become part of the ACP protocol specification.
4
5use crate::{JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, UntypedMessage};
6use agent_client_protocol_schema::InitializeResponse;
7use serde::{Deserialize, Serialize};
8
9// =============================================================================
10// Successor forwarding protocol
11// =============================================================================
12
13/// JSON-RPC method name for successor forwarding.
14pub const METHOD_SUCCESSOR_MESSAGE: &str = "_proxy/successor";
15
16/// A message being sent to the successor component.
17///
18/// Used in `_proxy/successor` when the proxy wants to forward a message downstream.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct SuccessorMessage<M: JsonRpcMessage = UntypedMessage> {
21    /// The message to be sent to the successor component.
22    #[serde(flatten)]
23    pub message: M,
24
25    /// Optional metadata
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub meta: Option<serde_json::Value>,
28}
29
30impl<M: JsonRpcMessage> JsonRpcMessage for SuccessorMessage<M> {
31    fn matches_method(method: &str) -> bool {
32        method == METHOD_SUCCESSOR_MESSAGE
33    }
34
35    fn method(&self) -> &str {
36        METHOD_SUCCESSOR_MESSAGE
37    }
38
39    fn to_untyped_message(&self) -> Result<UntypedMessage, crate::Error> {
40        UntypedMessage::new(
41            METHOD_SUCCESSOR_MESSAGE,
42            SuccessorMessage {
43                message: self.message.to_untyped_message()?,
44                meta: self.meta.clone(),
45            },
46        )
47    }
48
49    fn parse_message(method: &str, params: &impl Serialize) -> Result<Self, crate::Error> {
50        if method != METHOD_SUCCESSOR_MESSAGE {
51            return Err(crate::Error::method_not_found());
52        }
53        let outer = crate::util::json_cast_params::<_, SuccessorMessage<UntypedMessage>>(params)?;
54        if !M::matches_method(&outer.message.method) {
55            return Err(crate::Error::method_not_found());
56        }
57        let inner = M::parse_message(&outer.message.method, &outer.message.params)?;
58        Ok(SuccessorMessage {
59            message: inner,
60            meta: outer.meta,
61        })
62    }
63}
64
65impl<Req: JsonRpcRequest> JsonRpcRequest for SuccessorMessage<Req> {
66    type Response = Req::Response;
67}
68
69impl<Notif: JsonRpcNotification> JsonRpcNotification for SuccessorMessage<Notif> {}
70
71// =============================================================================
72// MCP-over-ACP protocol
73// =============================================================================
74
75/// JSON-RPC method name for MCP connect requests
76pub const METHOD_MCP_CONNECT_REQUEST: &str = "_mcp/connect";
77
78/// Creates a new MCP connection. This is equivalent to "running the command".
79#[derive(Debug, Clone, Serialize, Deserialize, crate::JsonRpcRequest)]
80#[request(method = "_mcp/connect", response = McpConnectResponse, crate = crate)]
81pub struct McpConnectRequest {
82    /// The ACP URL to connect to (e.g., "acp:uuid")
83    pub acp_url: String,
84
85    /// Optional metadata
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub meta: Option<serde_json::Value>,
88}
89
90/// Response to an MCP connect request
91#[derive(Debug, Clone, Serialize, Deserialize, crate::JsonRpcResponse)]
92#[response(crate = crate)]
93pub struct McpConnectResponse {
94    /// Unique identifier for the established MCP connection
95    pub connection_id: String,
96
97    /// Optional metadata
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub meta: Option<serde_json::Value>,
100}
101
102/// JSON-RPC method name for MCP disconnect notifications
103pub const METHOD_MCP_DISCONNECT_NOTIFICATION: &str = "_mcp/disconnect";
104
105/// Disconnects the MCP connection.
106#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, crate::JsonRpcNotification)]
107#[notification(method = "_mcp/disconnect", crate = crate)]
108pub struct McpDisconnectNotification {
109    /// The id of the connection to disconnect.
110    pub connection_id: String,
111
112    /// Optional metadata
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub meta: Option<serde_json::Value>,
115}
116
117/// JSON-RPC method name for MCP requests over ACP
118pub const METHOD_MCP_MESSAGE: &str = "_mcp/message";
119
120/// An MCP request sent via ACP. This could be an MCP-server-to-MCP-client request
121/// (in which case it goes from the ACP client to the ACP agent,
122/// note the reversal of roles) or an MCP-client-to-MCP-server request
123/// (in which case it goes from the ACP agent to the ACP client).
124#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct McpOverAcpMessage<M = UntypedMessage> {
127    /// id given in response to `_mcp/connect` request.
128    pub connection_id: String,
129
130    /// Request to be sent to the MCP server or client.
131    #[serde(flatten)]
132    pub message: M,
133
134    /// Optional metadata
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub meta: Option<serde_json::Value>,
137}
138
139impl<M: JsonRpcMessage> JsonRpcMessage for McpOverAcpMessage<M> {
140    fn matches_method(method: &str) -> bool {
141        method == METHOD_MCP_MESSAGE
142    }
143
144    fn method(&self) -> &str {
145        METHOD_MCP_MESSAGE
146    }
147
148    fn to_untyped_message(&self) -> Result<UntypedMessage, crate::Error> {
149        let message = self.message.to_untyped_message()?;
150        UntypedMessage::new(
151            METHOD_MCP_MESSAGE,
152            McpOverAcpMessage {
153                connection_id: self.connection_id.clone(),
154                message,
155                meta: self.meta.clone(),
156            },
157        )
158    }
159
160    fn parse_message(method: &str, params: &impl Serialize) -> Result<Self, crate::Error> {
161        if method != METHOD_MCP_MESSAGE {
162            return Err(crate::Error::method_not_found());
163        }
164        let outer = crate::util::json_cast_params::<_, McpOverAcpMessage<UntypedMessage>>(params)?;
165        if !M::matches_method(&outer.message.method) {
166            return Err(crate::Error::method_not_found());
167        }
168        let inner = M::parse_message(&outer.message.method, &outer.message.params)?;
169        Ok(McpOverAcpMessage {
170            connection_id: outer.connection_id,
171            message: inner,
172            meta: outer.meta,
173        })
174    }
175}
176
177impl<R: JsonRpcRequest> JsonRpcRequest for McpOverAcpMessage<R> {
178    type Response = R::Response;
179}
180
181impl<R: JsonRpcNotification> JsonRpcNotification for McpOverAcpMessage<R> {}
182
183// =============================================================================
184// Proxy initialization protocol
185// =============================================================================
186
187/// JSON-RPC method name for proxy initialization.
188pub const METHOD_INITIALIZE_PROXY: &str = "_proxy/initialize";
189
190/// Initialize request for proxy components.
191///
192/// This is sent to components that have a successor in the chain.
193/// Components that receive this (instead of `InitializeRequest`) know they
194/// are operating as a proxy and should forward messages to their successor.
195#[derive(Debug, Clone, Serialize, Deserialize, crate::JsonRpcRequest)]
196#[request(method = "_proxy/initialize", response = InitializeResponse, crate = crate)]
197pub struct InitializeProxyRequest {
198    /// The underlying initialize request data.
199    #[serde(flatten)]
200    pub initialize: agent_client_protocol_schema::InitializeRequest,
201}
202
203impl From<agent_client_protocol_schema::InitializeRequest> for InitializeProxyRequest {
204    fn from(initialize: agent_client_protocol_schema::InitializeRequest) -> Self {
205        Self { initialize }
206    }
207}