sacp_proxy/
mcp_over_acp.rs

1use agent_client_protocol as acp;
2use sacp::{
3    JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponsePayload, UntypedMessage,
4    util::json_cast,
5};
6use serde::{Deserialize, Serialize};
7
8pub const METHOD_MCP_CONNECT_REQUEST: &str = "_mcp/connect";
9
10/// Creates a new MCP connection. This is equivalent to "running the command".
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct McpConnectRequest {
13    pub acp_url: String,
14}
15
16impl JsonRpcMessage for McpConnectRequest {
17    fn into_untyped_message(self) -> Result<UntypedMessage, acp::Error> {
18        UntypedMessage::new(METHOD_MCP_CONNECT_REQUEST, self)
19    }
20
21    fn method(&self) -> &str {
22        METHOD_MCP_CONNECT_REQUEST
23    }
24
25    fn parse_request(method: &str, params: &impl Serialize) -> Option<Result<Self, acp::Error>> {
26        if method != METHOD_MCP_CONNECT_REQUEST {
27            return None;
28        }
29        Some(sacp::util::json_cast(params))
30    }
31
32    fn parse_notification(
33        _method: &str,
34        _params: &impl Serialize,
35    ) -> Option<Result<Self, acp::Error>> {
36        // This is a request, not a notification
37        None
38    }
39}
40
41impl JsonRpcRequest for McpConnectRequest {
42    type Response = McpConnectResponse;
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct McpConnectResponse {
47    pub connection_id: String,
48}
49
50impl JsonRpcResponsePayload for McpConnectResponse {
51    fn into_json(self, _method: &str) -> Result<serde_json::Value, agent_client_protocol::Error> {
52        serde_json::to_value(self).map_err(acp::Error::into_internal_error)
53    }
54
55    fn from_value(
56        _method: &str,
57        value: serde_json::Value,
58    ) -> Result<Self, agent_client_protocol::Error> {
59        serde_json::from_value(value).map_err(|_| acp::Error::invalid_params())
60    }
61}
62
63pub const METHOD_MCP_DISCONNECT_NOTIFICATION: &str = "_mcp/disconnect";
64
65/// Disconnects the MCP connection.
66#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
67pub struct McpDisconnectNotification {
68    /// The id of the connection to disconnect.
69    pub connection_id: String,
70}
71
72impl JsonRpcMessage for McpDisconnectNotification {
73    fn into_untyped_message(self) -> Result<UntypedMessage, acp::Error> {
74        UntypedMessage::new(METHOD_MCP_DISCONNECT_NOTIFICATION, self)
75    }
76
77    fn method(&self) -> &str {
78        METHOD_MCP_DISCONNECT_NOTIFICATION
79    }
80
81    fn parse_request(_method: &str, _params: &impl Serialize) -> Option<Result<Self, acp::Error>> {
82        // This is a notification, not a request
83        None
84    }
85
86    fn parse_notification(
87        method: &str,
88        params: &impl Serialize,
89    ) -> Option<Result<Self, acp::Error>> {
90        if method != METHOD_MCP_DISCONNECT_NOTIFICATION {
91            return None;
92        }
93        Some(sacp::util::json_cast(params))
94    }
95}
96
97impl JsonRpcNotification for McpDisconnectNotification {}
98
99pub const METHOD_MCP_REQUEST: &str = "_mcp/request";
100
101/// An MCP request sent via ACP. This could be an MCP-server-to-MCP-client request
102/// (in which case it goes from the ACP client to the ACP agent,
103/// note the reversal of roles) or an MCP-client-to-MCP-server request
104/// (in which case it goes from the ACP agent to the ACP client).
105#[derive(Debug, Clone, Serialize, Deserialize)]
106#[serde(rename_all = "camelCase")]
107pub struct McpOverAcpRequest<R> {
108    /// id given in response to `_mcp/connect` request.
109    pub connection_id: String,
110
111    /// Request to be sent to the MCP server or client.
112    #[serde(flatten)]
113    pub request: R,
114}
115
116impl<R: JsonRpcRequest> JsonRpcMessage for McpOverAcpRequest<R> {
117    fn into_untyped_message(self) -> Result<UntypedMessage, acp::Error> {
118        let message = self.request.into_untyped_message()?;
119        UntypedMessage::new(
120            METHOD_MCP_REQUEST,
121            McpOverAcpRequest {
122                connection_id: self.connection_id,
123                request: message,
124            },
125        )
126    }
127
128    fn method(&self) -> &str {
129        METHOD_MCP_REQUEST
130    }
131
132    fn parse_request(method: &str, params: &impl Serialize) -> Option<Result<Self, acp::Error>> {
133        if method == METHOD_MCP_REQUEST {
134            match json_cast::<_, McpOverAcpRequest<UntypedMessage>>(params) {
135                Ok(outer) => match R::parse_request(&outer.request.method, &outer.request.params) {
136                    Some(Ok(request)) => Some(Ok(McpOverAcpRequest {
137                        connection_id: outer.connection_id,
138                        request,
139                    })),
140                    Some(Err(err)) => Some(Err(err)),
141                    None => None,
142                },
143                Err(err) => Some(Err(err)),
144            }
145        } else {
146            None
147        }
148    }
149
150    fn parse_notification(
151        _method: &str,
152        _params: &impl Serialize,
153    ) -> Option<Result<Self, acp::Error>> {
154        None // Request, not notification
155    }
156}
157
158impl<R: JsonRpcRequest> JsonRpcRequest for McpOverAcpRequest<R> {
159    type Response = R::Response;
160}
161
162pub const METHOD_MCP_NOTIFICATION: &str = "_mcp/notification";
163
164/// An MCP notification sent via ACP, either from the MCP client (the ACP agent)
165/// or the MCP server (the ACP client).
166///
167/// Delivered via `_mcp/notification` when the MCP client (the ACP agent)
168/// sends a notification to the MCP server (the ACP client).
169#[derive(Debug, Clone, Serialize, Deserialize)]
170#[serde(rename_all = "camelCase")]
171pub struct McpOverAcpNotification<R> {
172    /// id given in response to `_mcp/connect` request.
173    pub connection_id: String,
174
175    /// Notification to be sent to the MCP server or client.
176    #[serde(flatten)]
177    pub notification: R,
178}
179
180impl<R: JsonRpcMessage> JsonRpcMessage for McpOverAcpNotification<R> {
181    fn into_untyped_message(self) -> Result<UntypedMessage, acp::Error> {
182        let params = self.notification.into_untyped_message()?;
183        UntypedMessage::new(
184            METHOD_MCP_NOTIFICATION,
185            McpOverAcpNotification {
186                connection_id: self.connection_id,
187                notification: params,
188            },
189        )
190    }
191
192    fn method(&self) -> &str {
193        METHOD_MCP_NOTIFICATION
194    }
195
196    fn parse_request(_method: &str, _params: &impl Serialize) -> Option<Result<Self, acp::Error>> {
197        None // Notification, not request
198    }
199
200    fn parse_notification(
201        method: &str,
202        params: &impl Serialize,
203    ) -> Option<Result<Self, acp::Error>> {
204        if method == METHOD_MCP_NOTIFICATION {
205            match json_cast::<_, McpOverAcpNotification<UntypedMessage>>(params) {
206                Ok(outer) => match R::parse_notification(
207                    &outer.notification.method,
208                    &outer.notification.params,
209                ) {
210                    Some(Ok(notification)) => Some(Ok(McpOverAcpNotification {
211                        connection_id: outer.connection_id,
212                        notification,
213                    })),
214                    Some(Err(err)) => Some(Err(err)),
215                    None => None,
216                },
217                Err(err) => Some(Err(err)),
218            }
219        } else {
220            None
221        }
222    }
223}
224
225impl<R: JsonRpcMessage> JsonRpcNotification for McpOverAcpNotification<R> {}