boxxkite_client/mcp_connections.rs
1//! Outbound-MCP connections (GitHub issues #116/#117,
2//! `docs/OUTBOUND-MCP-DESIGN.md`): `POST/GET/DELETE /v1/mcp-connections*`.
3//! Mirrors `sdk-python`'s `create_mcp_connection`/`list_mcp_connections`/
4//! `delete_mcp_connection`.
5//!
6//! Note: granting a connection only widens a session's per-pod
7//! `NetworkPolicy` egress allowlist to the catalog hostname -- there is no
8//! MCP-proxy transport yet (`docs/OUTBOUND-MCP-DESIGN.md` section 6), so a
9//! granted connection does not yet let an agent actually speak MCP to it.
10
11use reqwest::Method;
12use serde::{Deserialize, Serialize};
13
14use crate::client::Client;
15use crate::error::BoxxkiteError;
16
17/// A curated outbound-MCP catalog entry -- restricted to boxxkite's own
18/// reviewed allowlist, never a caller-supplied hostname.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
20#[serde(rename_all = "lowercase")]
21pub enum McpCatalogId {
22 Slack,
23 Notion,
24 Linear,
25 Github,
26}
27
28/// A granted outbound-MCP connection (`McpConnectionOut`).
29#[derive(Debug, Clone, Deserialize)]
30pub struct McpConnection {
31 pub id: String,
32 /// Unique (per-account) name -- pass it to
33 /// [`crate::CreateSandboxOptions::mcp_connection_names`] to
34 /// grant a session network egress to it.
35 pub label: String,
36 pub catalog_id: String,
37 /// The resolved catalog host, for the caller's own visibility -- never
38 /// treated as caller-supplied input on any other route.
39 pub host: String,
40 pub created_at: String,
41 pub last_used_at: Option<String>,
42}
43
44impl Client {
45 /// `POST /v1/mcp-connections` -- grant this account access to one
46 /// curated outbound-MCP catalog entry.
47 ///
48 /// `label` must be unique per account; `catalog_id` selects the
49 /// catalog entry to grant.
50 pub async fn create_mcp_connection(
51 &self,
52 label: &str,
53 catalog_id: McpCatalogId,
54 ) -> Result<McpConnection, BoxxkiteError> {
55 #[derive(Serialize)]
56 struct Body<'a> {
57 label: &'a str,
58 catalog_id: McpCatalogId,
59 }
60 let body = Body { label, catalog_id };
61 let builder = self
62 .request(Method::POST, "/v1/mcp-connections")
63 .json(&body);
64 self.send(builder).await
65 }
66
67 /// `GET /v1/mcp-connections` -- outbound-MCP connection grants for this account.
68 pub async fn list_mcp_connections(&self) -> Result<Vec<McpConnection>, BoxxkiteError> {
69 let builder = self.request(Method::GET, "/v1/mcp-connections");
70 self.send_or_default(builder).await
71 }
72
73 /// `DELETE /v1/mcp-connections/{id}` -- delete an outbound-MCP
74 /// connection grant owned by this account. 404s if already gone or
75 /// never owned by this account.
76 pub async fn delete_mcp_connection(&self, connection_id: &str) -> Result<(), BoxxkiteError> {
77 let builder = self.request(
78 Method::DELETE,
79 &format!("/v1/mcp-connections/{connection_id}"),
80 );
81 self.send_no_content(builder).await
82 }
83}