sacp_proxy/
mcp_over_acp.rs

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