mcp_host/server/
visibility.rs1use std::path::Path;
22
23use serde_json::Value;
24
25use crate::server::multiplexer::ClientRequester;
26use crate::server::session::Session;
27
28pub struct VisibilityContext<'a> {
33 pub session: &'a Session,
35
36 pub environment: Option<&'a dyn Environment>,
38}
39
40impl<'a> VisibilityContext<'a> {
41 pub fn new(session: &'a Session) -> Self {
43 Self {
44 session,
45 environment: None,
46 }
47 }
48
49 pub fn with_environment(session: &'a Session, environment: &'a dyn Environment) -> Self {
51 Self {
52 session,
53 environment: Some(environment),
54 }
55 }
56
57 pub fn get_state<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
59 self.session
60 .get_state(key)
61 .and_then(|v| serde_json::from_value(v).ok())
62 }
63
64 pub fn has_role(&self, role: &str) -> bool {
66 self.session
67 .get_state("roles")
68 .and_then(|v| v.as_array().cloned())
69 .map(|roles| roles.iter().any(|r| r.as_str() == Some(role)))
70 .unwrap_or(false)
71 }
72
73 pub fn is_admin(&self) -> bool {
75 self.has_role("admin")
76 }
77
78 pub fn is_vip(&self) -> bool {
80 self.has_role("vip")
81 }
82}
83
84pub struct ExecutionContext<'a> {
116 pub params: Value,
118
119 pub uri_params: std::collections::HashMap<String, String>,
121
122 pub session: &'a Session,
124
125 pub logger: &'a crate::logging::McpLogger,
127
128 pub environment: Option<&'a dyn Environment>,
130
131 client_requester: Option<ClientRequester>,
133}
134
135impl<'a> ExecutionContext<'a> {
136 pub fn new(params: Value, session: &'a Session, logger: &'a crate::logging::McpLogger) -> Self {
138 Self {
139 params,
140 uri_params: std::collections::HashMap::new(),
141 session,
142 logger,
143 environment: None,
144 client_requester: None,
145 }
146 }
147
148 pub fn with_environment(
150 params: Value,
151 session: &'a Session,
152 logger: &'a crate::logging::McpLogger,
153 environment: &'a dyn Environment,
154 ) -> Self {
155 Self {
156 params,
157 uri_params: std::collections::HashMap::new(),
158 session,
159 logger,
160 environment: Some(environment),
161 client_requester: None,
162 }
163 }
164
165 pub fn for_resource(
167 uri_params: std::collections::HashMap<String, String>,
168 session: &'a Session,
169 logger: &'a crate::logging::McpLogger,
170 ) -> Self {
171 Self {
172 params: Value::Null,
173 uri_params,
174 session,
175 logger,
176 environment: None,
177 client_requester: None,
178 }
179 }
180
181 pub fn for_resource_with_environment(
183 uri_params: std::collections::HashMap<String, String>,
184 session: &'a Session,
185 logger: &'a crate::logging::McpLogger,
186 environment: &'a dyn Environment,
187 ) -> Self {
188 Self {
189 params: Value::Null,
190 uri_params,
191 session,
192 logger,
193 environment: Some(environment),
194 client_requester: None,
195 }
196 }
197
198 pub fn with_client_requester(mut self, requester: ClientRequester) -> Self {
200 self.client_requester = Some(requester);
201 self
202 }
203
204 pub fn client_requester(&self) -> Option<&ClientRequester> {
208 self.client_requester.as_ref()
209 }
210
211 pub fn get_uri_param(&self, name: &str) -> Option<&str> {
213 self.uri_params.get(name).map(|s| s.as_str())
214 }
215
216 pub fn get_state<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
218 self.session
219 .get_state(key)
220 .and_then(|v| serde_json::from_value(v).ok())
221 }
222
223 pub fn has_role(&self, role: &str) -> bool {
225 self.session
226 .get_state("roles")
227 .and_then(|v| v.as_array().cloned())
228 .map(|roles| roles.iter().any(|r| r.as_str() == Some(role)))
229 .unwrap_or(false)
230 }
231
232 pub fn is_admin(&self) -> bool {
234 self.has_role("admin")
235 }
236
237 pub fn is_vip(&self) -> bool {
239 self.has_role("vip")
240 }
241
242 pub fn session_id(&self) -> &str {
244 &self.session.id
245 }
246
247 pub fn client_name(&self) -> Option<&str> {
249 self.session.client_info.as_ref().map(|c| c.name.as_str())
250 }
251
252 pub fn client_version(&self) -> Option<&str> {
254 self.session
255 .client_info
256 .as_ref()
257 .map(|c| c.version.as_str())
258 }
259
260 pub fn protocol_version(&self) -> Option<&str> {
262 self.session.protocol_version()
263 }
264
265 pub fn client_capabilities(
267 &self,
268 ) -> Option<&crate::protocol::capabilities::ClientCapabilities> {
269 self.session.capabilities.as_ref()
270 }
271
272 pub fn as_visibility_context(&self) -> VisibilityContext<'a> {
274 match self.environment {
275 Some(env) => VisibilityContext::with_environment(self.session, env),
276 None => VisibilityContext::new(self.session),
277 }
278 }
279}
280
281pub trait Environment: Send + Sync {
319 fn has_git_repo(&self) -> bool {
321 false
322 }
323
324 fn git_is_clean(&self) -> bool {
326 true
327 }
328
329 fn git_commits_ahead(&self) -> usize {
331 0
332 }
333
334 fn git_has_staged(&self) -> bool {
336 false
337 }
338
339 fn git_has_stash(&self) -> bool {
341 false
342 }
343
344 fn cwd(&self) -> &Path;
346
347 fn file_exists(&self, path: &str) -> bool {
349 self.cwd().join(path).exists()
350 }
351
352 fn dir_exists(&self, path: &str) -> bool {
354 let full_path = self.cwd().join(path);
355 full_path.exists() && full_path.is_dir()
356 }
357
358 fn get_custom(&self, _key: &str) -> Option<String> {
360 None
361 }
362}
363
364pub struct SimpleEnvironment {
366 cwd: std::path::PathBuf,
367}
368
369impl SimpleEnvironment {
370 pub fn new(cwd: impl Into<std::path::PathBuf>) -> Self {
372 Self { cwd: cwd.into() }
373 }
374
375 pub fn current_dir() -> std::io::Result<Self> {
377 Ok(Self {
378 cwd: std::env::current_dir()?,
379 })
380 }
381}
382
383impl Environment for SimpleEnvironment {
384 fn has_git_repo(&self) -> bool {
385 self.cwd.join(".git").exists()
386 }
387
388 fn cwd(&self) -> &Path {
389 &self.cwd
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use crate::server::session::Session;
397
398 #[test]
399 fn test_visibility_context_creation() {
400 let session = Session::new();
401 let ctx = VisibilityContext::new(&session);
402
403 assert!(ctx.environment.is_none());
404 }
405
406 #[test]
407 fn test_visibility_context_with_environment() {
408 let session = Session::new();
409 let env = SimpleEnvironment::new("/tmp");
410 let ctx = VisibilityContext::with_environment(&session, &env);
411
412 assert!(ctx.environment.is_some());
413 assert_eq!(ctx.environment.unwrap().cwd(), Path::new("/tmp"));
414 }
415
416 #[test]
417 fn test_role_checks() {
418 let session = Session::new();
419 session.set_state("roles", serde_json::json!(["admin", "vip"]));
420
421 let ctx = VisibilityContext::new(&session);
422
423 assert!(ctx.has_role("admin"));
424 assert!(ctx.has_role("vip"));
425 assert!(!ctx.has_role("guest"));
426 assert!(ctx.is_admin());
427 assert!(ctx.is_vip());
428 }
429
430 #[test]
431 fn test_role_checks_empty() {
432 let session = Session::new();
433 let ctx = VisibilityContext::new(&session);
434
435 assert!(!ctx.has_role("admin"));
436 assert!(!ctx.is_admin());
437 assert!(!ctx.is_vip());
438 }
439
440 #[test]
441 fn test_simple_environment() {
442 let env = SimpleEnvironment::new("/tmp");
443
444 assert_eq!(env.cwd(), Path::new("/tmp"));
445 assert!(env.git_is_clean()); assert!(!env.has_git_repo()); }
448
449 #[test]
450 fn test_environment_defaults() {
451 struct MinimalEnv;
452 impl Environment for MinimalEnv {
453 fn cwd(&self) -> &Path {
454 Path::new("/")
455 }
456 }
457
458 let env = MinimalEnv;
459 assert!(!env.has_git_repo());
460 assert!(env.git_is_clean());
461 assert_eq!(env.git_commits_ahead(), 0);
462 assert!(!env.git_has_staged());
463 assert!(!env.git_has_stash());
464 assert!(env.get_custom("anything").is_none());
465 }
466}