sacp_proxy/
mcp_over_acp.rs

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