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 environment: Option<&'a dyn Environment>,
127
128 client_requester: Option<ClientRequester>,
130}
131
132impl<'a> ExecutionContext<'a> {
133 pub fn new(params: Value, session: &'a Session) -> Self {
135 Self {
136 params,
137 uri_params: std::collections::HashMap::new(),
138 session,
139 environment: None,
140 client_requester: None,
141 }
142 }
143
144 pub fn with_environment(
146 params: Value,
147 session: &'a Session,
148 environment: &'a dyn Environment,
149 ) -> Self {
150 Self {
151 params,
152 uri_params: std::collections::HashMap::new(),
153 session,
154 environment: Some(environment),
155 client_requester: None,
156 }
157 }
158
159 pub fn for_resource(
161 uri_params: std::collections::HashMap<String, String>,
162 session: &'a Session,
163 ) -> Self {
164 Self {
165 params: Value::Null,
166 uri_params,
167 session,
168 environment: None,
169 client_requester: None,
170 }
171 }
172
173 pub fn for_resource_with_environment(
175 uri_params: std::collections::HashMap<String, String>,
176 session: &'a Session,
177 environment: &'a dyn Environment,
178 ) -> Self {
179 Self {
180 params: Value::Null,
181 uri_params,
182 session,
183 environment: Some(environment),
184 client_requester: None,
185 }
186 }
187
188 pub fn with_client_requester(mut self, requester: ClientRequester) -> Self {
190 self.client_requester = Some(requester);
191 self
192 }
193
194 pub fn client_requester(&self) -> Option<&ClientRequester> {
198 self.client_requester.as_ref()
199 }
200
201 pub fn get_uri_param(&self, name: &str) -> Option<&str> {
203 self.uri_params.get(name).map(|s| s.as_str())
204 }
205
206 pub fn get_state<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
208 self.session
209 .get_state(key)
210 .and_then(|v| serde_json::from_value(v).ok())
211 }
212
213 pub fn has_role(&self, role: &str) -> bool {
215 self.session
216 .get_state("roles")
217 .and_then(|v| v.as_array().cloned())
218 .map(|roles| roles.iter().any(|r| r.as_str() == Some(role)))
219 .unwrap_or(false)
220 }
221
222 pub fn is_admin(&self) -> bool {
224 self.has_role("admin")
225 }
226
227 pub fn is_vip(&self) -> bool {
229 self.has_role("vip")
230 }
231
232 pub fn session_id(&self) -> &str {
234 &self.session.id
235 }
236
237 pub fn client_name(&self) -> Option<&str> {
239 self.session.client_info.as_ref().map(|c| c.name.as_str())
240 }
241
242 pub fn client_version(&self) -> Option<&str> {
244 self.session
245 .client_info
246 .as_ref()
247 .map(|c| c.version.as_str())
248 }
249
250 pub fn protocol_version(&self) -> Option<&str> {
252 self.session.protocol_version()
253 }
254
255 pub fn client_capabilities(
257 &self,
258 ) -> Option<&crate::protocol::capabilities::ClientCapabilities> {
259 self.session.capabilities.as_ref()
260 }
261
262 pub fn as_visibility_context(&self) -> VisibilityContext<'a> {
264 match self.environment {
265 Some(env) => VisibilityContext::with_environment(self.session, env),
266 None => VisibilityContext::new(self.session),
267 }
268 }
269}
270
271pub trait Environment: Send + Sync {
309 fn has_git_repo(&self) -> bool {
311 false
312 }
313
314 fn git_is_clean(&self) -> bool {
316 true
317 }
318
319 fn git_commits_ahead(&self) -> usize {
321 0
322 }
323
324 fn git_has_staged(&self) -> bool {
326 false
327 }
328
329 fn git_has_stash(&self) -> bool {
331 false
332 }
333
334 fn cwd(&self) -> &Path;
336
337 fn file_exists(&self, path: &str) -> bool {
339 self.cwd().join(path).exists()
340 }
341
342 fn dir_exists(&self, path: &str) -> bool {
344 let full_path = self.cwd().join(path);
345 full_path.exists() && full_path.is_dir()
346 }
347
348 fn get_custom(&self, _key: &str) -> Option<String> {
350 None
351 }
352}
353
354pub struct SimpleEnvironment {
356 cwd: std::path::PathBuf,
357}
358
359impl SimpleEnvironment {
360 pub fn new(cwd: impl Into<std::path::PathBuf>) -> Self {
362 Self { cwd: cwd.into() }
363 }
364
365 pub fn current_dir() -> std::io::Result<Self> {
367 Ok(Self {
368 cwd: std::env::current_dir()?,
369 })
370 }
371}
372
373impl Environment for SimpleEnvironment {
374 fn has_git_repo(&self) -> bool {
375 self.cwd.join(".git").exists()
376 }
377
378 fn cwd(&self) -> &Path {
379 &self.cwd
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386 use crate::server::session::Session;
387
388 #[test]
389 fn test_visibility_context_creation() {
390 let session = Session::new();
391 let ctx = VisibilityContext::new(&session);
392
393 assert!(ctx.environment.is_none());
394 }
395
396 #[test]
397 fn test_visibility_context_with_environment() {
398 let session = Session::new();
399 let env = SimpleEnvironment::new("/tmp");
400 let ctx = VisibilityContext::with_environment(&session, &env);
401
402 assert!(ctx.environment.is_some());
403 assert_eq!(ctx.environment.unwrap().cwd(), Path::new("/tmp"));
404 }
405
406 #[test]
407 fn test_role_checks() {
408 let session = Session::new();
409 session.set_state("roles", serde_json::json!(["admin", "vip"]));
410
411 let ctx = VisibilityContext::new(&session);
412
413 assert!(ctx.has_role("admin"));
414 assert!(ctx.has_role("vip"));
415 assert!(!ctx.has_role("guest"));
416 assert!(ctx.is_admin());
417 assert!(ctx.is_vip());
418 }
419
420 #[test]
421 fn test_role_checks_empty() {
422 let session = Session::new();
423 let ctx = VisibilityContext::new(&session);
424
425 assert!(!ctx.has_role("admin"));
426 assert!(!ctx.is_admin());
427 assert!(!ctx.is_vip());
428 }
429
430 #[test]
431 fn test_simple_environment() {
432 let env = SimpleEnvironment::new("/tmp");
433
434 assert_eq!(env.cwd(), Path::new("/tmp"));
435 assert!(env.git_is_clean()); assert!(!env.has_git_repo()); }
438
439 #[test]
440 fn test_environment_defaults() {
441 struct MinimalEnv;
442 impl Environment for MinimalEnv {
443 fn cwd(&self) -> &Path {
444 Path::new("/")
445 }
446 }
447
448 let env = MinimalEnv;
449 assert!(!env.has_git_repo());
450 assert!(env.git_is_clean());
451 assert_eq!(env.git_commits_ahead(), 0);
452 assert!(!env.git_has_staged());
453 assert!(!env.git_has_stash());
454 assert!(env.get_custom("anything").is_none());
455 }
456}