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