Skip to main content

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}