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