Skip to main content

serve/
connection.rs

1//! Connections and secrets.
2//!
3//! A `#[connection]` returns any value; tools reach it with
4//! `cx.connection::<T>()`, so credentials live in the connection and never in
5//! the model context. [`McpServer`] is the one connection serve understands
6//! itself: it is attached to every agent as an MCP server.
7//!
8//! Secrets are named, not read, at declaration time. The manifest lists every
9//! [`Secret::named`] a connection or channel mentions, so the host can ask for
10//! missing ones before the first deploy.
11
12use std::collections::BTreeSet;
13use std::sync::Mutex;
14
15use anyhow::anyhow;
16
17/// Every secret name mentioned while the app was being discovered.
18static DECLARED: Mutex<BTreeSet<&'static str>> = Mutex::new(BTreeSet::new());
19
20pub(crate) fn declared_secrets() -> BTreeSet<&'static str> {
21    DECLARED.lock().map(|set| set.clone()).unwrap_or_default()
22}
23
24/// A secret provided by the host as an environment variable.
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26pub struct Secret {
27    name: &'static str,
28}
29
30impl Secret {
31    /// Declare a secret. Declaring records it for the manifest; nothing is
32    /// read until [`value`](Self::value).
33    pub fn named(name: &'static str) -> Self {
34        if let Ok(mut set) = DECLARED.lock() {
35            set.insert(name);
36        }
37        Self { name }
38    }
39
40    pub fn name(&self) -> &'static str {
41        self.name
42    }
43
44    /// The value, from the environment the host prepared.
45    pub fn value(&self) -> crate::Result<String> {
46        std::env::var(self.name).map_err(|_| {
47            anyhow!(
48                "secret `{}` is not set; the host provides it as an environment variable",
49                self.name
50            )
51        })
52    }
53
54    pub fn is_present(&self) -> bool {
55        std::env::var_os(self.name).is_some_and(|value| !value.is_empty())
56    }
57}
58
59/// An MCP server connection, attached to every agent.
60#[derive(Clone, Debug)]
61pub struct McpServer {
62    pub(crate) url: String,
63    pub(crate) auth: Option<Secret>,
64}
65
66impl McpServer {
67    /// A streamable-HTTP (or SSE) MCP endpoint.
68    pub fn http(url: impl Into<String>) -> Self {
69        Self {
70            url: url.into(),
71            auth: None,
72        }
73    }
74
75    /// Send `Authorization: Bearer <secret>`.
76    pub fn auth(mut self, secret: Secret) -> Self {
77        self.auth = Some(secret);
78        self
79    }
80
81    pub fn url(&self) -> &str {
82        &self.url
83    }
84
85    /// The everruns server config, or why it cannot be attached right now.
86    pub(crate) fn to_everruns(&self, name: &str) -> crate::Result<everruns::McpServer> {
87        let mut server = everruns::McpServer::http(name, &self.url);
88        if let Some(secret) = self.auth {
89            server = server.header("Authorization", format!("Bearer {}", secret.value()?));
90        }
91        Ok(server)
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn declaring_a_secret_records_it_without_reading_it() {
101        let secret = Secret::named("SERVE_TEST_DECLARED_ONLY");
102        assert!(declared_secrets().contains("SERVE_TEST_DECLARED_ONLY"));
103        assert!(!secret.is_present());
104        let err = secret.value().unwrap_err().to_string();
105        assert!(err.contains("SERVE_TEST_DECLARED_ONLY"), "{err}");
106    }
107
108    #[test]
109    fn mcp_without_its_secret_is_not_attachable() {
110        let server = McpServer::http("https://example.test/mcp")
111            .auth(Secret::named("SERVE_TEST_MISSING_TOKEN"));
112        assert!(server.to_everruns("linear").is_err());
113        assert!(
114            McpServer::http("https://example.test/mcp")
115                .to_everruns("x")
116                .is_ok()
117        );
118    }
119}