1use std::collections::BTreeSet;
13use std::sync::Mutex;
14
15use anyhow::anyhow;
16
17static 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26pub struct Secret {
27 name: &'static str,
28}
29
30impl Secret {
31 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 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#[derive(Clone, Debug)]
61pub struct McpServer {
62 pub(crate) url: String,
63 pub(crate) auth: Option<Secret>,
64}
65
66impl McpServer {
67 pub fn http(url: impl Into<String>) -> Self {
69 Self {
70 url: url.into(),
71 auth: None,
72 }
73 }
74
75 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 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}