mcp_methods/server/
env.rs1use std::fs;
18use std::path::{Path, PathBuf};
19
20pub fn load_env_walk(start: &Path) -> Option<PathBuf> {
25 let mut cursor: PathBuf = if start.is_absolute() {
26 start.to_path_buf()
27 } else {
28 start.canonicalize().unwrap_or_else(|_| start.to_path_buf())
29 };
30 loop {
31 let candidate = cursor.join(".env");
32 if candidate.is_file() {
33 apply_env_file(&candidate);
34 return Some(candidate);
35 }
36 if !cursor.pop() {
37 return None;
38 }
39 }
40}
41
42pub fn load_env_explicit(path: &Path) -> Result<(), String> {
45 if !path.is_file() {
46 return Err(format!("env_file does not exist: {}", path.display()));
47 }
48 apply_env_file(path);
49 Ok(())
50}
51
52fn apply_env_file(path: &Path) {
53 let Ok(text) = fs::read_to_string(path) else {
54 return;
55 };
56 for line in text.lines() {
57 let trimmed = line.trim();
58 if trimmed.is_empty() || trimmed.starts_with('#') {
59 continue;
60 }
61 let Some(eq) = trimmed.find('=') else {
62 continue;
63 };
64 let key = trimmed[..eq].trim();
65 let val = trimmed[eq + 1..].trim();
66 let val = strip_outer_quotes(val);
67 if key.is_empty() {
68 continue;
69 }
70 if std::env::var_os(key).is_some() {
71 continue;
72 }
73 unsafe { std::env::set_var(key, val) };
79 }
80}
81
82fn strip_outer_quotes(s: &str) -> &str {
83 let bytes = s.as_bytes();
84 if bytes.len() >= 2 {
85 let first = bytes[0];
86 let last = bytes[bytes.len() - 1];
87 if (first == b'"' && last == b'"') || (first == b'\'' && last == b'\'') {
88 return &s[1..s.len() - 1];
89 }
90 }
91 s
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use std::io::Write;
98
99 fn env_lock() -> std::sync::MutexGuard<'static, ()> {
101 use std::sync::{Mutex, OnceLock};
102 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
103 LOCK.get_or_init(|| Mutex::new(()))
104 .lock()
105 .unwrap_or_else(|p| p.into_inner())
106 }
107
108 fn write_env(dir: &Path, body: &str) -> PathBuf {
109 let p = dir.join(".env");
110 let mut f = fs::File::create(&p).unwrap();
111 f.write_all(body.as_bytes()).unwrap();
112 p
113 }
114
115 #[test]
116 fn finds_env_in_start_dir() {
117 let _g = env_lock();
118 let dir = tempfile::tempdir().unwrap();
119 let key = "MCP_TEST_DIRECT_HIT";
120 unsafe { std::env::remove_var(key) };
121 write_env(dir.path(), &format!("{key}=ok\n"));
122 let found = load_env_walk(dir.path()).expect("found env");
123 assert!(found.ends_with(".env"));
124 assert_eq!(std::env::var(key).ok().as_deref(), Some("ok"));
125 unsafe { std::env::remove_var(key) };
126 }
127
128 #[test]
129 fn walks_up_to_parent_for_env() {
130 let _g = env_lock();
131 let dir = tempfile::tempdir().unwrap();
132 let key = "MCP_TEST_WALK_UP";
133 unsafe { std::env::remove_var(key) };
134 write_env(dir.path(), &format!("{key}=parent\n"));
135 let sub = dir.path().join("a").join("b").join("c");
136 fs::create_dir_all(&sub).unwrap();
137 let found = load_env_walk(&sub).expect("found env via walk-up");
138 let expected = dir.path().canonicalize().unwrap().join(".env");
139 assert_eq!(found.canonicalize().unwrap(), expected);
140 assert_eq!(std::env::var(key).ok().as_deref(), Some("parent"));
141 unsafe { std::env::remove_var(key) };
142 }
143
144 #[test]
145 fn does_not_overwrite_existing_env() {
146 let _g = env_lock();
147 let dir = tempfile::tempdir().unwrap();
148 let key = "MCP_TEST_NO_OVERWRITE";
149 unsafe { std::env::set_var(key, "preset") };
150 write_env(dir.path(), &format!("{key}=fromfile\n"));
151 load_env_walk(dir.path());
152 assert_eq!(std::env::var(key).ok().as_deref(), Some("preset"));
153 unsafe { std::env::remove_var(key) };
154 }
155
156 #[test]
157 fn strips_quotes_skips_comments() {
158 let _g = env_lock();
159 let dir = tempfile::tempdir().unwrap();
160 let k1 = "MCP_TEST_DQUOTED";
161 let k2 = "MCP_TEST_SQUOTED";
162 let k3 = "MCP_TEST_COMMENT";
163 for k in [k1, k2, k3] {
164 unsafe { std::env::remove_var(k) };
165 }
166 write_env(
167 dir.path(),
168 &format!("# comment\n\n{k1}=\"hello\"\n{k2}='world'\n# {k3}=skipped\n"),
169 );
170 load_env_walk(dir.path()).unwrap();
171 assert_eq!(std::env::var(k1).ok().as_deref(), Some("hello"));
172 assert_eq!(std::env::var(k2).ok().as_deref(), Some("world"));
173 assert!(std::env::var(k3).is_err());
174 for k in [k1, k2, k3] {
175 unsafe { std::env::remove_var(k) };
176 }
177 }
178
179 #[test]
180 fn explicit_missing_path_errors() {
181 let dir = tempfile::tempdir().unwrap();
182 let missing = dir.path().join("nope.env");
183 assert!(load_env_explicit(&missing).is_err());
184 }
185
186 #[test]
187 fn returns_none_when_no_env_anywhere() {
188 let _g = env_lock();
189 let dir = tempfile::tempdir().unwrap();
190 let _ = load_env_walk(dir.path());
194 }
195}