1use anyhow::{Context, Result};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12use std::sync::Mutex;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum TrustLevel {
18 Trusted,
20 Verified,
22 Untrusted,
24}
25
26impl std::fmt::Display for TrustLevel {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 match self {
29 TrustLevel::Trusted => write!(f, "trusted"),
30 TrustLevel::Verified => write!(f, "verified"),
31 TrustLevel::Untrusted => write!(f, "untrusted"),
32 }
33 }
34}
35
36impl std::str::FromStr for TrustLevel {
37 type Err = anyhow::Error;
38
39 fn from_str(s: &str) -> Result<Self> {
40 match s.to_lowercase().as_str() {
41 "trusted" => Ok(TrustLevel::Trusted),
42 "verified" => Ok(TrustLevel::Verified),
43 "untrusted" => Ok(TrustLevel::Untrusted),
44 _ => anyhow::bail!("Unknown trust level: '{}' (expected: trusted, verified, untrusted)", s),
45 }
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum McpCapability {
55 ReadFile,
56 ListTools,
57 Search,
58 WriteFile,
59 ExecuteSafe,
60 NetworkHttp,
61}
62
63impl std::fmt::Display for McpCapability {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 match self {
66 McpCapability::ReadFile => write!(f, "read_file"),
67 McpCapability::ListTools => write!(f, "list_tools"),
68 McpCapability::Search => write!(f, "search"),
69 McpCapability::WriteFile => write!(f, "write_file"),
70 McpCapability::ExecuteSafe => write!(f, "execute_safe"),
71 McpCapability::NetworkHttp => write!(f, "network_http"),
72 }
73 }
74}
75
76impl std::str::FromStr for McpCapability {
77 type Err = anyhow::Error;
78
79 fn from_str(s: &str) -> Result<Self> {
80 match s {
81 "read_file" => Ok(McpCapability::ReadFile),
82 "list_tools" => Ok(McpCapability::ListTools),
83 "search" => Ok(McpCapability::Search),
84 "write_file" => Ok(McpCapability::WriteFile),
85 "execute_safe" => Ok(McpCapability::ExecuteSafe),
86 "network_http" => Ok(McpCapability::NetworkHttp),
87 _ => anyhow::bail!(
88 "Unknown MCP capability: '{}' (known: read_file, list_tools, search, write_file, execute_safe, network_http)",
89 s
90 ),
91 }
92 }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct TrustEntry {
98 pub server_name: String,
100 pub level: TrustLevel,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub checksum: Option<String>,
105 #[serde(default)]
107 pub set_by: String,
108 #[serde(default)]
110 pub updated_at: String,
111}
112
113pub struct McpTrustStore {
118 store_dir: PathBuf,
119 cache: Mutex<Option<HashMap<String, TrustEntry>>>,
121}
122
123impl McpTrustStore {
124 pub fn new(store_dir: &Path) -> Self {
126 Self {
127 store_dir: store_dir.to_path_buf(),
128 cache: Mutex::new(None),
129 }
130 }
131
132 pub fn get_trust(&self, server_name: &str) -> Result<TrustLevel> {
134 let entries = self.load_entries()?;
135 Ok(entries
136 .get(server_name)
137 .map(|e| e.level)
138 .unwrap_or(TrustLevel::Untrusted))
139 }
140
141 pub fn set_trust(
143 &self,
144 server_name: &str,
145 level: TrustLevel,
146 checksum: Option<String>,
147 ) -> Result<()> {
148 let mut entries = self.load_entries()?;
149
150 entries.insert(
151 server_name.to_string(),
152 TrustEntry {
153 server_name: server_name.to_string(),
154 level,
155 checksum,
156 set_by: whoami(),
157 updated_at: chrono::Utc::now().to_rfc3339(),
158 },
159 );
160
161 self.save_entries(&entries)?;
162 self.invalidate_cache();
163 Ok(())
164 }
165
166 pub fn revoke_trust(&self, server_name: &str) -> Result<()> {
168 let mut entries = self.load_entries()?;
169 entries.remove(server_name);
170 self.save_entries(&entries)?;
171 self.invalidate_cache();
172 Ok(())
173 }
174
175 pub fn list(&self) -> Result<Vec<TrustEntry>> {
177 let entries = self.load_entries()?;
178 Ok(entries.into_values().collect())
179 }
180
181 pub fn verify_checksum(&self, server_name: &str, command_path: &std::path::Path) -> Result<bool> {
184 let entries = self.load_entries()?;
185 let entry = match entries.get(server_name) {
186 Some(e) => e,
187 None => return Ok(false),
188 };
189 let stored = match &entry.checksum {
190 Some(c) => c,
191 None => return Ok(false),
192 };
193 let bytes = std::fs::read(command_path)
195 .with_context(|| format!("Reading server binary: {:?}", command_path))?;
196 use sha2::{Sha256, Digest};
197 let hash = format!("{:x}", Sha256::digest(&bytes));
198 if hash != *stored {
199 anyhow::bail!(
200 "Checksum mismatch for '{}': expected {}, got {}. Server binary may have been tampered with.",
201 server_name, stored, hash
202 );
203 }
204 Ok(true)
205 }
206
207 pub fn compute_checksum(path: &std::path::Path) -> Result<String> {
209 let bytes = std::fs::read(path)?;
210 use sha2::{Sha256, Digest};
211 Ok(format!("{:x}", Sha256::digest(&bytes)))
212 }
213
214 pub fn check_permission(&self, server_name: &str, capability: McpCapability) -> Result<bool> {
216 let level = self.get_trust(server_name)?;
217 Ok(match level {
218 TrustLevel::Trusted => true,
219 TrustLevel::Verified => matches!(
220 capability,
221 McpCapability::ReadFile
222 | McpCapability::ListTools
223 | McpCapability::Search
224 | McpCapability::WriteFile
225 | McpCapability::ExecuteSafe
226 | McpCapability::NetworkHttp
227 ),
228 TrustLevel::Untrusted => matches!(
229 capability,
230 McpCapability::ReadFile | McpCapability::ListTools | McpCapability::Search
231 ),
232 })
233 }
234
235 pub fn check_permission_str(&self, server_name: &str, capability: &str) -> Result<bool> {
241 match capability.parse::<McpCapability>() {
242 Ok(cap) => self.check_permission(server_name, cap),
243 Err(_) => Ok(false), }
245 }
246
247 fn store_path(&self) -> PathBuf {
248 self.store_dir.join("trust.json")
249 }
250
251 fn load_entries(&self) -> Result<HashMap<String, TrustEntry>> {
253 let mut cache = self.cache.lock().unwrap_or_else(|e| e.into_inner());
254 if let Some(ref entries) = *cache {
255 return Ok(entries.clone());
256 }
257 let entries = self.load_entries_from_disk()?;
258 *cache = Some(entries.clone());
259 Ok(entries)
260 }
261
262 fn load_entries_from_disk(&self) -> Result<HashMap<String, TrustEntry>> {
264 let path = self.store_path();
265 if !path.exists() {
266 return Ok(HashMap::new());
267 }
268 let content =
269 std::fs::read_to_string(&path).context("Reading trust store")?;
270 let entries: HashMap<String, TrustEntry> =
271 serde_json::from_str(&content).context("Parsing trust store")?;
272 Ok(entries)
273 }
274
275 fn invalidate_cache(&self) {
277 let mut cache = self.cache.lock().unwrap_or_else(|e| e.into_inner());
278 *cache = None;
279 }
280
281 fn save_entries(&self, entries: &HashMap<String, TrustEntry>) -> Result<()> {
282 std::fs::create_dir_all(&self.store_dir)?;
283 let json = serde_json::to_string_pretty(entries)?;
284 let tmp_path = self.store_path().with_extension("json.tmp");
285 std::fs::write(&tmp_path, &json)?;
286 std::fs::rename(&tmp_path, self.store_path())?;
287 #[cfg(unix)]
288 {
289 use std::os::unix::fs::PermissionsExt;
290 let _ = std::fs::set_permissions(
291 self.store_path(),
292 std::fs::Permissions::from_mode(0o600),
293 );
294 }
295 Ok(())
296 }
297}
298
299fn whoami() -> String {
300 std::env::var("USER").unwrap_or_else(|_| "unknown".into())
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
308 fn test_trust_level_display() {
309 assert_eq!(TrustLevel::Trusted.to_string(), "trusted");
310 assert_eq!(TrustLevel::Verified.to_string(), "verified");
311 assert_eq!(TrustLevel::Untrusted.to_string(), "untrusted");
312 }
313
314 #[test]
315 fn test_trust_level_parse() {
316 assert_eq!("trusted".parse::<TrustLevel>().unwrap(), TrustLevel::Trusted);
317 assert_eq!("Verified".parse::<TrustLevel>().unwrap(), TrustLevel::Verified);
318 assert_eq!("UNTRUSTED".parse::<TrustLevel>().unwrap(), TrustLevel::Untrusted);
319 assert!("invalid".parse::<TrustLevel>().is_err());
320 }
321
322 #[test]
323 fn test_mcp_capability_parse() {
324 assert_eq!("read_file".parse::<McpCapability>().unwrap(), McpCapability::ReadFile);
325 assert_eq!("list_tools".parse::<McpCapability>().unwrap(), McpCapability::ListTools);
326 assert_eq!("search".parse::<McpCapability>().unwrap(), McpCapability::Search);
327 assert_eq!("write_file".parse::<McpCapability>().unwrap(), McpCapability::WriteFile);
328 assert_eq!("execute_safe".parse::<McpCapability>().unwrap(), McpCapability::ExecuteSafe);
329 assert_eq!("network_http".parse::<McpCapability>().unwrap(), McpCapability::NetworkHttp);
330 assert!("unknown_cap".parse::<McpCapability>().is_err());
331 }
332
333 #[test]
334 fn test_mcp_capability_display() {
335 assert_eq!(McpCapability::ReadFile.to_string(), "read_file");
336 assert_eq!(McpCapability::ListTools.to_string(), "list_tools");
337 assert_eq!(McpCapability::Search.to_string(), "search");
338 assert_eq!(McpCapability::WriteFile.to_string(), "write_file");
339 assert_eq!(McpCapability::ExecuteSafe.to_string(), "execute_safe");
340 assert_eq!(McpCapability::NetworkHttp.to_string(), "network_http");
341 }
342
343 #[test]
344 fn test_default_untrusted() {
345 let dir = tempfile::TempDir::new().unwrap();
346 let store = McpTrustStore::new(dir.path());
347 assert_eq!(store.get_trust("unknown-server").unwrap(), TrustLevel::Untrusted);
348 }
349
350 #[test]
351 fn test_set_and_get_trust() {
352 let dir = tempfile::TempDir::new().unwrap();
353 let store = McpTrustStore::new(dir.path());
354
355 store.set_trust("my-server", TrustLevel::Trusted, None).unwrap();
356 assert_eq!(store.get_trust("my-server").unwrap(), TrustLevel::Trusted);
357
358 store.set_trust("my-server", TrustLevel::Verified, Some("abc123".into())).unwrap();
359 assert_eq!(store.get_trust("my-server").unwrap(), TrustLevel::Verified);
360 }
361
362 #[test]
363 fn test_revoke_trust() {
364 let dir = tempfile::TempDir::new().unwrap();
365 let store = McpTrustStore::new(dir.path());
366
367 store.set_trust("temp", TrustLevel::Trusted, None).unwrap();
368 assert_eq!(store.get_trust("temp").unwrap(), TrustLevel::Trusted);
369
370 store.revoke_trust("temp").unwrap();
371 assert_eq!(store.get_trust("temp").unwrap(), TrustLevel::Untrusted);
372 }
373
374 #[test]
375 fn test_list_entries() {
376 let dir = tempfile::TempDir::new().unwrap();
377 let store = McpTrustStore::new(dir.path());
378
379 store.set_trust("a", TrustLevel::Trusted, None).unwrap();
380 store.set_trust("b", TrustLevel::Verified, None).unwrap();
381
382 let entries = store.list().unwrap();
383 assert_eq!(entries.len(), 2);
384 }
385
386 #[test]
387 fn test_check_permission_trusted() {
388 let dir = tempfile::TempDir::new().unwrap();
389 let store = McpTrustStore::new(dir.path());
390 store.set_trust("full", TrustLevel::Trusted, None).unwrap();
391
392 assert!(store.check_permission("full", McpCapability::ReadFile).unwrap());
394 assert!(store.check_permission("full", McpCapability::ExecuteSafe).unwrap());
395 assert!(store.check_permission("full", McpCapability::NetworkHttp).unwrap());
396
397 assert!(store.check_permission_str("full", "read_file").unwrap());
399 }
402
403 #[test]
404 fn test_check_permission_str_unknown_denied() {
405 let dir = tempfile::TempDir::new().unwrap();
406 let store = McpTrustStore::new(dir.path());
407 store.set_trust("full", TrustLevel::Trusted, None).unwrap();
408
409 assert!(!store.check_permission_str("full", "execute_arbitrary").unwrap());
411 assert!(!store.check_permission_str("full", "system_config").unwrap());
412 }
413
414 #[test]
415 fn test_check_permission_verified() {
416 let dir = tempfile::TempDir::new().unwrap();
417 let store = McpTrustStore::new(dir.path());
418 store.set_trust("community", TrustLevel::Verified, None).unwrap();
419
420 assert!(store.check_permission("community", McpCapability::ReadFile).unwrap());
421 assert!(store.check_permission("community", McpCapability::WriteFile).unwrap());
422 assert!(store.check_permission("community", McpCapability::ExecuteSafe).unwrap());
423
424 assert!(!store.check_permission_str("community", "execute_arbitrary").unwrap());
426 assert!(!store.check_permission_str("community", "system_config").unwrap());
427 }
428
429 #[test]
430 fn test_check_permission_untrusted() {
431 let dir = tempfile::TempDir::new().unwrap();
432 let store = McpTrustStore::new(dir.path());
433
434 assert!(store.check_permission("unknown", McpCapability::ReadFile).unwrap());
435 assert!(store.check_permission("unknown", McpCapability::Search).unwrap());
436 assert!(!store.check_permission("unknown", McpCapability::WriteFile).unwrap());
437 assert!(!store.check_permission("unknown", McpCapability::ExecuteSafe).unwrap());
438
439 assert!(!store.check_permission_str("unknown", "execute_arbitrary").unwrap());
441 assert!(!store.check_permission_str("unknown", "network_unrestricted").unwrap());
442 }
443
444 #[test]
445 fn test_cache_invalidation_on_set() {
446 let dir = tempfile::TempDir::new().unwrap();
447 let store = McpTrustStore::new(dir.path());
448
449 assert_eq!(store.get_trust("srv").unwrap(), TrustLevel::Untrusted);
451
452 store.set_trust("srv", TrustLevel::Trusted, None).unwrap();
454
455 assert_eq!(store.get_trust("srv").unwrap(), TrustLevel::Trusted);
457 }
458
459 #[test]
460 fn test_cache_invalidation_on_revoke() {
461 let dir = tempfile::TempDir::new().unwrap();
462 let store = McpTrustStore::new(dir.path());
463
464 store.set_trust("srv", TrustLevel::Trusted, None).unwrap();
465 assert_eq!(store.get_trust("srv").unwrap(), TrustLevel::Trusted);
466
467 store.revoke_trust("srv").unwrap();
469 assert_eq!(store.get_trust("srv").unwrap(), TrustLevel::Untrusted);
470 }
471
472 #[test]
473 fn test_trust_entry_serialization() {
474 let entry = TrustEntry {
475 server_name: "test".into(),
476 level: TrustLevel::Verified,
477 checksum: Some("sha256:abc".into()),
478 set_by: "user".into(),
479 updated_at: "2025-01-01T00:00:00Z".into(),
480 };
481 let json = serde_json::to_string(&entry).unwrap();
482 let parsed: TrustEntry = serde_json::from_str(&json).unwrap();
483 assert_eq!(parsed.level, TrustLevel::Verified);
484 assert_eq!(parsed.checksum.as_deref(), Some("sha256:abc"));
485 }
486}