1use std::collections::{BTreeMap, BTreeSet, HashSet};
57use std::sync::Arc;
58
59use parking_lot::RwLock;
60use serde::{Deserialize, Serialize};
61use turbomcp_core::context::RequestContext;
62use turbomcp_core::error::{McpError, McpResult};
63use turbomcp_core::handler::McpHandler;
64use turbomcp_types::{
65 ComponentFilter, ComponentMeta, Prompt, PromptResult, Resource, ResourceResult,
66 ResourceTemplate, Tool, ToolResult,
67};
68
69type SessionVisibilityMap = Arc<dashmap::DashMap<String, HashSet<String>>>;
71
72type SharedComponentRegistry = Arc<RwLock<ComponentRegistryCache>>;
74
75#[derive(Debug, Clone, Default)]
76struct ComponentRegistryCache {
77 tools: Option<BTreeMap<String, Tool>>,
78 resources_by_uri: Option<BTreeMap<String, Resource>>,
79 resource_templates_by_uri_template: Option<BTreeMap<String, ResourceTemplate>>,
80 prompts: Option<BTreeMap<String, Prompt>>,
81}
82
83enum RegistryLookup<T> {
84 Uninitialized,
85 Found(T),
86 Missing,
87}
88
89impl ComponentRegistryCache {
90 fn replace_tools(&mut self, tools: Vec<Tool>) {
91 let mut registry = BTreeMap::new();
92 for tool in tools {
93 registry.entry(tool.name.clone()).or_insert(tool);
94 }
95 self.tools = Some(registry);
96 }
97
98 fn replace_resources(&mut self, resources: Vec<Resource>) {
99 let mut registry = BTreeMap::new();
100 for resource in resources {
101 registry.entry(resource.uri.clone()).or_insert(resource);
102 }
103 self.resources_by_uri = Some(registry);
104 }
105
106 fn replace_resource_templates(&mut self, templates: Vec<ResourceTemplate>) {
107 let mut registry = BTreeMap::new();
108 for template in templates {
109 registry
110 .entry(template.uri_template.clone())
111 .or_insert(template);
112 }
113 self.resource_templates_by_uri_template = Some(registry);
114 }
115
116 fn replace_prompts(&mut self, prompts: Vec<Prompt>) {
117 let mut registry = BTreeMap::new();
118 for prompt in prompts {
119 registry.entry(prompt.name.clone()).or_insert(prompt);
120 }
121 self.prompts = Some(registry);
122 }
123
124 fn tool(&self, name: &str) -> RegistryLookup<Tool> {
125 match &self.tools {
126 Some(tools) => tools
127 .get(name)
128 .cloned()
129 .map_or(RegistryLookup::Missing, RegistryLookup::Found),
130 None => RegistryLookup::Uninitialized,
131 }
132 }
133
134 fn resource_by_uri(&self, uri: &str) -> RegistryLookup<Resource> {
135 match &self.resources_by_uri {
136 Some(resources) => resources
137 .get(uri)
138 .cloned()
139 .map_or(RegistryLookup::Missing, RegistryLookup::Found),
140 None => RegistryLookup::Uninitialized,
141 }
142 }
143
144 fn prompt(&self, name: &str) -> RegistryLookup<Prompt> {
145 match &self.prompts {
146 Some(prompts) => prompts
147 .get(name)
148 .cloned()
149 .map_or(RegistryLookup::Missing, RegistryLookup::Found),
150 None => RegistryLookup::Uninitialized,
151 }
152 }
153
154 fn clear(&mut self) {
155 *self = Self::default();
156 }
157
158 fn is_initialized(&self) -> bool {
159 self.tools.is_some()
160 || self.resources_by_uri.is_some()
161 || self.resource_templates_by_uri_template.is_some()
162 || self.prompts.is_some()
163 }
164}
165
166#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
177#[serde(default)]
178pub struct ComponentVisibilityRules {
179 #[serde(skip_serializing_if = "Option::is_none")]
184 pub allow: Option<BTreeSet<String>>,
185
186 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
188 pub deny: BTreeSet<String>,
189
190 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
192 pub hide: BTreeSet<String>,
193}
194
195impl ComponentVisibilityRules {
196 #[must_use]
198 pub fn new() -> Self {
199 Self::default()
200 }
201
202 #[must_use]
204 pub fn allow<I, S>(names: I) -> Self
205 where
206 I: IntoIterator<Item = S>,
207 S: Into<String>,
208 {
209 Self {
210 allow: Some(collect_names(names)),
211 deny: BTreeSet::new(),
212 hide: BTreeSet::new(),
213 }
214 }
215
216 #[must_use]
218 pub fn deny<I, S>(names: I) -> Self
219 where
220 I: IntoIterator<Item = S>,
221 S: Into<String>,
222 {
223 Self {
224 allow: None,
225 deny: collect_names(names),
226 hide: BTreeSet::new(),
227 }
228 }
229
230 #[must_use]
232 pub fn hide<I, S>(names: I) -> Self
233 where
234 I: IntoIterator<Item = S>,
235 S: Into<String>,
236 {
237 Self {
238 allow: None,
239 deny: BTreeSet::new(),
240 hide: collect_names(names),
241 }
242 }
243
244 #[must_use]
246 pub fn with_allowed<I, S>(mut self, names: I) -> Self
247 where
248 I: IntoIterator<Item = S>,
249 S: Into<String>,
250 {
251 self.allow = Some(collect_names(names));
252 self
253 }
254
255 #[must_use]
257 pub fn with_disabled<I, S>(mut self, names: I) -> Self
258 where
259 I: IntoIterator<Item = S>,
260 S: Into<String>,
261 {
262 self.deny = collect_names(names);
263 self
264 }
265
266 #[must_use]
268 pub fn with_hidden<I, S>(mut self, names: I) -> Self
269 where
270 I: IntoIterator<Item = S>,
271 S: Into<String>,
272 {
273 self.hide = collect_names(names);
274 self
275 }
276
277 #[must_use]
279 pub fn is_enabled(&self, identifier: &str) -> bool {
280 self.is_enabled_any([identifier])
281 }
282
283 #[must_use]
288 pub fn is_enabled_any<'a, I>(&self, identifiers: I) -> bool
289 where
290 I: IntoIterator<Item = &'a str>,
291 {
292 let identifiers = identifiers.into_iter().collect::<Vec<_>>();
293
294 if identifiers
295 .iter()
296 .any(|identifier| self.deny.contains(*identifier))
297 {
298 return false;
299 }
300
301 self.allow.as_ref().is_none_or(|allow| {
302 identifiers
303 .iter()
304 .any(|identifier| allow.contains(*identifier))
305 })
306 }
307
308 #[must_use]
310 pub fn is_listed(&self, identifier: &str) -> bool {
311 self.is_listed_any([identifier])
312 }
313
314 #[must_use]
316 pub fn is_listed_any<'a, I>(&self, identifiers: I) -> bool
317 where
318 I: IntoIterator<Item = &'a str>,
319 {
320 let identifiers = identifiers.into_iter().collect::<Vec<_>>();
321
322 self.is_enabled_any(identifiers.iter().copied())
323 && !identifiers
324 .iter()
325 .any(|identifier| self.hide.contains(*identifier))
326 }
327}
328
329#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
335#[serde(default)]
336pub struct VisibilityConfig {
337 pub tools: ComponentVisibilityRules,
339 pub resources: ComponentVisibilityRules,
341 pub resource_templates: ComponentVisibilityRules,
343 pub prompts: ComponentVisibilityRules,
345 #[serde(skip_serializing_if = "is_false")]
350 pub require_read_only_tools: bool,
351}
352
353impl VisibilityConfig {
354 #[must_use]
356 pub fn new() -> Self {
357 Self::default()
358 }
359
360 #[must_use]
362 pub fn with_allowed_tools<I, S>(mut self, names: I) -> Self
363 where
364 I: IntoIterator<Item = S>,
365 S: Into<String>,
366 {
367 self.tools = self.tools.with_allowed(names);
368 self
369 }
370
371 #[must_use]
373 pub fn with_disabled_tools<I, S>(mut self, names: I) -> Self
374 where
375 I: IntoIterator<Item = S>,
376 S: Into<String>,
377 {
378 self.tools = self.tools.with_disabled(names);
379 self
380 }
381
382 #[must_use]
384 pub fn with_hidden_tools<I, S>(mut self, names: I) -> Self
385 where
386 I: IntoIterator<Item = S>,
387 S: Into<String>,
388 {
389 self.tools = self.tools.with_hidden(names);
390 self
391 }
392
393 #[must_use]
395 pub fn with_allowed_resources<I, S>(mut self, identifiers: I) -> Self
396 where
397 I: IntoIterator<Item = S>,
398 S: Into<String>,
399 {
400 self.resources = self.resources.with_allowed(identifiers);
401 self
402 }
403
404 #[must_use]
406 pub fn with_disabled_resources<I, S>(mut self, identifiers: I) -> Self
407 where
408 I: IntoIterator<Item = S>,
409 S: Into<String>,
410 {
411 self.resources = self.resources.with_disabled(identifiers);
412 self
413 }
414
415 #[must_use]
417 pub fn with_hidden_resources<I, S>(mut self, identifiers: I) -> Self
418 where
419 I: IntoIterator<Item = S>,
420 S: Into<String>,
421 {
422 self.resources = self.resources.with_hidden(identifiers);
423 self
424 }
425
426 #[must_use]
428 pub fn with_allowed_resource_templates<I, S>(mut self, identifiers: I) -> Self
429 where
430 I: IntoIterator<Item = S>,
431 S: Into<String>,
432 {
433 self.resource_templates = self.resource_templates.with_allowed(identifiers);
434 self
435 }
436
437 #[must_use]
439 pub fn with_disabled_resource_templates<I, S>(mut self, identifiers: I) -> Self
440 where
441 I: IntoIterator<Item = S>,
442 S: Into<String>,
443 {
444 self.resource_templates = self.resource_templates.with_disabled(identifiers);
445 self
446 }
447
448 #[must_use]
450 pub fn with_hidden_resource_templates<I, S>(mut self, identifiers: I) -> Self
451 where
452 I: IntoIterator<Item = S>,
453 S: Into<String>,
454 {
455 self.resource_templates = self.resource_templates.with_hidden(identifiers);
456 self
457 }
458
459 #[must_use]
461 pub fn with_allowed_prompts<I, S>(mut self, names: I) -> Self
462 where
463 I: IntoIterator<Item = S>,
464 S: Into<String>,
465 {
466 self.prompts = self.prompts.with_allowed(names);
467 self
468 }
469
470 #[must_use]
472 pub fn with_disabled_prompts<I, S>(mut self, names: I) -> Self
473 where
474 I: IntoIterator<Item = S>,
475 S: Into<String>,
476 {
477 self.prompts = self.prompts.with_disabled(names);
478 self
479 }
480
481 #[must_use]
483 pub fn with_hidden_prompts<I, S>(mut self, names: I) -> Self
484 where
485 I: IntoIterator<Item = S>,
486 S: Into<String>,
487 {
488 self.prompts = self.prompts.with_hidden(names);
489 self
490 }
491
492 #[must_use]
494 pub fn require_read_only_tools(mut self) -> Self {
495 self.require_read_only_tools = true;
496 self
497 }
498}
499
500fn collect_names<I, S>(names: I) -> BTreeSet<String>
501where
502 I: IntoIterator<Item = S>,
503 S: Into<String>,
504{
505 names.into_iter().map(Into::into).collect()
506}
507
508fn is_false(value: &bool) -> bool {
509 !*value
510}
511
512fn is_explicit_read_only_tool(tool: &Tool) -> bool {
513 tool.annotations.as_ref().is_some_and(|annotations| {
514 annotations.read_only_hint == Some(true) && annotations.destructive_hint != Some(true)
515 })
516}
517
518#[derive(Debug)]
539pub struct VisibilitySessionGuard {
540 session_id: String,
541 session_enabled: SessionVisibilityMap,
542 session_disabled: SessionVisibilityMap,
543}
544
545impl VisibilitySessionGuard {
546 pub fn session_id(&self) -> &str {
548 &self.session_id
549 }
550}
551
552impl Drop for VisibilitySessionGuard {
553 fn drop(&mut self) {
554 self.session_enabled.remove(&self.session_id);
555 self.session_disabled.remove(&self.session_id);
556 }
557}
558
559pub struct VisibilityLayer<H> {
568 inner: H,
570 global_disabled: Arc<RwLock<Vec<ComponentFilter>>>,
572 tool_rules: ComponentVisibilityRules,
574 resource_rules: ComponentVisibilityRules,
576 resource_template_rules: ComponentVisibilityRules,
578 prompt_rules: ComponentVisibilityRules,
580 read_only_tools_required: bool,
582 component_registry: SharedComponentRegistry,
584 session_enabled: SessionVisibilityMap,
589 session_disabled: SessionVisibilityMap,
590}
591
592impl<H: Clone> Clone for VisibilityLayer<H> {
593 fn clone(&self) -> Self {
594 Self {
595 inner: self.inner.clone(),
596 global_disabled: Arc::new(RwLock::new(self.global_disabled.read().clone())),
597 tool_rules: self.tool_rules.clone(),
598 resource_rules: self.resource_rules.clone(),
599 resource_template_rules: self.resource_template_rules.clone(),
600 prompt_rules: self.prompt_rules.clone(),
601 read_only_tools_required: self.read_only_tools_required,
602 component_registry: Arc::clone(&self.component_registry),
603 session_enabled: Arc::clone(&self.session_enabled),
604 session_disabled: Arc::clone(&self.session_disabled),
605 }
606 }
607}
608
609impl<H: std::fmt::Debug> std::fmt::Debug for VisibilityLayer<H> {
610 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
611 f.debug_struct("VisibilityLayer")
612 .field("inner", &self.inner)
613 .field("global_disabled_count", &self.global_disabled.read().len())
614 .field(
615 "tool_allow_count",
616 &self.tool_rules.allow.as_ref().map(BTreeSet::len),
617 )
618 .field("tool_deny_count", &self.tool_rules.deny.len())
619 .field("tool_hide_count", &self.tool_rules.hide.len())
620 .field("read_only_tools_required", &self.read_only_tools_required)
621 .field(
622 "component_registry_initialized",
623 &self.component_registry.read().is_initialized(),
624 )
625 .field("session_enabled_count", &self.session_enabled.len())
626 .field("session_disabled_count", &self.session_disabled.len())
627 .finish()
628 }
629}
630
631impl<H: McpHandler> VisibilityLayer<H> {
632 pub fn new(inner: H) -> Self {
634 Self {
635 inner,
636 global_disabled: Arc::new(RwLock::new(Vec::new())),
637 tool_rules: ComponentVisibilityRules::new(),
638 resource_rules: ComponentVisibilityRules::new(),
639 resource_template_rules: ComponentVisibilityRules::new(),
640 prompt_rules: ComponentVisibilityRules::new(),
641 read_only_tools_required: false,
642 component_registry: Arc::new(RwLock::new(ComponentRegistryCache::default())),
643 session_enabled: Arc::new(dashmap::DashMap::new()),
644 session_disabled: Arc::new(dashmap::DashMap::new()),
645 }
646 }
647
648 #[must_use]
652 pub fn with_disabled(self, filter: ComponentFilter) -> Self {
653 self.global_disabled.write().push(filter);
654 self
655 }
656
657 #[must_use]
659 pub fn disable_tags<I, S>(self, tags: I) -> Self
660 where
661 I: IntoIterator<Item = S>,
662 S: Into<String>,
663 {
664 self.with_disabled(ComponentFilter::with_tags(tags))
665 }
666
667 #[must_use]
674 pub fn with_visibility_config(mut self, config: VisibilityConfig) -> Self {
675 self.tool_rules = config.tools;
676 self.resource_rules = config.resources;
677 self.resource_template_rules = config.resource_templates;
678 self.prompt_rules = config.prompts;
679 self.read_only_tools_required = config.require_read_only_tools;
680 self
681 }
682
683 #[must_use]
685 pub fn visibility_config(&self) -> VisibilityConfig {
686 VisibilityConfig {
687 tools: self.tool_rules.clone(),
688 resources: self.resource_rules.clone(),
689 resource_templates: self.resource_template_rules.clone(),
690 prompts: self.prompt_rules.clone(),
691 require_read_only_tools: self.read_only_tools_required,
692 }
693 }
694
695 pub fn refresh_component_registry(&self) {
702 let tools = self.inner.list_tools();
703 let resources = self.inner.list_resources();
704 let resource_templates = self.inner.list_resource_templates();
705 let prompts = self.inner.list_prompts();
706
707 let mut registry = self.component_registry.write();
708 registry.replace_tools(tools);
709 registry.replace_resources(resources);
710 registry.replace_resource_templates(resource_templates);
711 registry.replace_prompts(prompts);
712 }
713
714 pub fn clear_component_registry(&self) {
720 self.component_registry.write().clear();
721 }
722
723 #[must_use]
728 pub fn with_allowed_tools<I, S>(mut self, names: I) -> Self
729 where
730 I: IntoIterator<Item = S>,
731 S: Into<String>,
732 {
733 self.tool_rules = self.tool_rules.with_allowed(names);
734 self
735 }
736
737 #[must_use]
739 pub fn with_disabled_tools<I, S>(mut self, names: I) -> Self
740 where
741 I: IntoIterator<Item = S>,
742 S: Into<String>,
743 {
744 self.tool_rules = self.tool_rules.with_disabled(names);
745 self
746 }
747
748 #[must_use]
750 pub fn with_hidden_tools<I, S>(mut self, names: I) -> Self
751 where
752 I: IntoIterator<Item = S>,
753 S: Into<String>,
754 {
755 self.tool_rules = self.tool_rules.with_hidden(names);
756 self
757 }
758
759 #[must_use]
761 pub fn with_allowed_resources<I, S>(mut self, identifiers: I) -> Self
762 where
763 I: IntoIterator<Item = S>,
764 S: Into<String>,
765 {
766 self.resource_rules = self.resource_rules.with_allowed(identifiers);
767 self
768 }
769
770 #[must_use]
772 pub fn with_disabled_resources<I, S>(mut self, identifiers: I) -> Self
773 where
774 I: IntoIterator<Item = S>,
775 S: Into<String>,
776 {
777 self.resource_rules = self.resource_rules.with_disabled(identifiers);
778 self
779 }
780
781 #[must_use]
783 pub fn with_hidden_resources<I, S>(mut self, identifiers: I) -> Self
784 where
785 I: IntoIterator<Item = S>,
786 S: Into<String>,
787 {
788 self.resource_rules = self.resource_rules.with_hidden(identifiers);
789 self
790 }
791
792 #[must_use]
794 pub fn with_allowed_resource_templates<I, S>(mut self, identifiers: I) -> Self
795 where
796 I: IntoIterator<Item = S>,
797 S: Into<String>,
798 {
799 self.resource_template_rules = self.resource_template_rules.with_allowed(identifiers);
800 self
801 }
802
803 #[must_use]
805 pub fn with_disabled_resource_templates<I, S>(mut self, identifiers: I) -> Self
806 where
807 I: IntoIterator<Item = S>,
808 S: Into<String>,
809 {
810 self.resource_template_rules = self.resource_template_rules.with_disabled(identifiers);
811 self
812 }
813
814 #[must_use]
816 pub fn with_hidden_resource_templates<I, S>(mut self, identifiers: I) -> Self
817 where
818 I: IntoIterator<Item = S>,
819 S: Into<String>,
820 {
821 self.resource_template_rules = self.resource_template_rules.with_hidden(identifiers);
822 self
823 }
824
825 #[must_use]
827 pub fn with_allowed_prompts<I, S>(mut self, names: I) -> Self
828 where
829 I: IntoIterator<Item = S>,
830 S: Into<String>,
831 {
832 self.prompt_rules = self.prompt_rules.with_allowed(names);
833 self
834 }
835
836 #[must_use]
838 pub fn with_disabled_prompts<I, S>(mut self, names: I) -> Self
839 where
840 I: IntoIterator<Item = S>,
841 S: Into<String>,
842 {
843 self.prompt_rules = self.prompt_rules.with_disabled(names);
844 self
845 }
846
847 #[must_use]
849 pub fn with_hidden_prompts<I, S>(mut self, names: I) -> Self
850 where
851 I: IntoIterator<Item = S>,
852 S: Into<String>,
853 {
854 self.prompt_rules = self.prompt_rules.with_hidden(names);
855 self
856 }
857
858 #[must_use]
864 pub fn require_read_only_tools(mut self) -> Self {
865 self.read_only_tools_required = true;
866 self
867 }
868
869 fn register_tools(&self, tools: Vec<Tool>) {
870 self.component_registry.write().replace_tools(tools);
871 }
872
873 fn register_resources(&self, resources: Vec<Resource>) {
874 self.component_registry.write().replace_resources(resources);
875 }
876
877 fn register_resource_templates(&self, templates: Vec<ResourceTemplate>) {
878 self.component_registry
879 .write()
880 .replace_resource_templates(templates);
881 }
882
883 fn register_prompts(&self, prompts: Vec<Prompt>) {
884 self.component_registry.write().replace_prompts(prompts);
885 }
886
887 fn registered_tool(&self, name: &str) -> Option<Tool> {
888 let lookup = { self.component_registry.read().tool(name) };
889 match lookup {
890 RegistryLookup::Found(tool) => return Some(tool),
891 RegistryLookup::Missing => return None,
892 RegistryLookup::Uninitialized => {}
893 }
894
895 let tools = self.inner.list_tools();
896 let tool = tools.iter().find(|tool| tool.name == name).cloned();
897 self.register_tools(tools);
898 tool
899 }
900
901 fn registered_resource(&self, uri: &str) -> Option<Resource> {
902 let lookup = { self.component_registry.read().resource_by_uri(uri) };
903 match lookup {
904 RegistryLookup::Found(resource) => return Some(resource),
905 RegistryLookup::Missing => return None,
906 RegistryLookup::Uninitialized => {}
907 }
908
909 let resources = self.inner.list_resources();
910 let resource = resources
911 .iter()
912 .find(|resource| resource.uri == uri)
913 .cloned();
914 self.register_resources(resources);
915 resource
916 }
917
918 fn registered_prompt(&self, name: &str) -> Option<Prompt> {
919 let lookup = { self.component_registry.read().prompt(name) };
920 match lookup {
921 RegistryLookup::Found(prompt) => return Some(prompt),
922 RegistryLookup::Missing => return None,
923 RegistryLookup::Uninitialized => {}
924 }
925
926 let prompts = self.inner.list_prompts();
927 let prompt = prompts.iter().find(|prompt| prompt.name == name).cloned();
928 self.register_prompts(prompts);
929 prompt
930 }
931
932 fn is_visible(&self, meta: &ComponentMeta, session_id: Option<&str>) -> bool {
934 let global_disabled = self.global_disabled.read();
936 let globally_hidden = global_disabled.iter().any(|filter| filter.matches(meta));
937
938 if !globally_hidden {
939 if let Some(sid) = session_id
941 && let Some(disabled) = self.session_disabled.get(sid)
942 && meta.tags.iter().any(|t| disabled.contains(t))
943 {
944 return false;
945 }
946 return true;
947 }
948
949 if let Some(sid) = session_id
951 && let Some(enabled) = self.session_enabled.get(sid)
952 && meta.tags.iter().any(|t| enabled.contains(t))
953 {
954 return true;
955 }
956
957 false
958 }
959
960 fn is_tool_enabled(&self, tool: &Tool, session_id: Option<&str>) -> bool {
962 if !self.tool_rules.is_enabled(&tool.name) {
963 return false;
964 }
965
966 if self.read_only_tools_required && !is_explicit_read_only_tool(tool) {
967 return false;
968 }
969
970 let meta = ComponentMeta::from_meta_value(tool.meta.as_ref());
971 self.is_visible(&meta, session_id)
972 }
973
974 fn is_tool_listed(&self, tool: &Tool, session_id: Option<&str>) -> bool {
976 self.is_tool_enabled(tool, session_id) && self.tool_rules.is_listed(&tool.name)
977 }
978
979 fn is_unregistered_tool_callable(&self, name: &str) -> bool {
981 self.tool_rules.is_enabled(name) && !self.read_only_tools_required
982 }
983
984 fn is_resource_enabled(&self, resource: &Resource, session_id: Option<&str>) -> bool {
986 if !self
987 .resource_rules
988 .is_enabled_any([resource.name.as_str(), resource.uri.as_str()])
989 {
990 return false;
991 }
992
993 let meta = ComponentMeta::from_meta_value(resource.meta.as_ref());
994 self.is_visible(&meta, session_id)
995 }
996
997 fn is_resource_listed(&self, resource: &Resource, session_id: Option<&str>) -> bool {
999 self.is_resource_enabled(resource, session_id)
1000 && self
1001 .resource_rules
1002 .is_listed_any([resource.name.as_str(), resource.uri.as_str()])
1003 }
1004
1005 fn is_unregistered_resource_readable(&self, uri: &str) -> bool {
1007 self.resource_rules.is_enabled(uri)
1008 }
1009
1010 fn is_resource_template_listed(
1012 &self,
1013 template: &ResourceTemplate,
1014 session_id: Option<&str>,
1015 ) -> bool {
1016 if !self
1017 .resource_template_rules
1018 .is_listed_any([template.name.as_str(), template.uri_template.as_str()])
1019 {
1020 return false;
1021 }
1022
1023 let meta = ComponentMeta::from_meta_value(template.meta.as_ref());
1024 self.is_visible(&meta, session_id)
1025 }
1026
1027 fn is_prompt_enabled(&self, prompt: &Prompt, session_id: Option<&str>) -> bool {
1029 if !self.prompt_rules.is_enabled(&prompt.name) {
1030 return false;
1031 }
1032
1033 let meta = ComponentMeta::from_meta_value(prompt.meta.as_ref());
1034 self.is_visible(&meta, session_id)
1035 }
1036
1037 fn is_prompt_listed(&self, prompt: &Prompt, session_id: Option<&str>) -> bool {
1039 self.is_prompt_enabled(prompt, session_id) && self.prompt_rules.is_listed(&prompt.name)
1040 }
1041
1042 fn is_unregistered_prompt_gettable(&self, name: &str) -> bool {
1044 self.prompt_rules.is_enabled(name)
1045 }
1046
1047 pub fn enable_for_session(&self, session_id: &str, tags: &[String]) {
1049 let mut entry = self
1050 .session_enabled
1051 .entry(session_id.to_string())
1052 .or_default();
1053 entry.extend(tags.iter().cloned());
1054
1055 if let Some(mut disabled) = self.session_disabled.get_mut(session_id) {
1057 for tag in tags {
1058 disabled.remove(tag);
1059 }
1060 }
1061 }
1062
1063 pub fn disable_for_session(&self, session_id: &str, tags: &[String]) {
1065 let mut entry = self
1066 .session_disabled
1067 .entry(session_id.to_string())
1068 .or_default();
1069 entry.extend(tags.iter().cloned());
1070
1071 if let Some(mut enabled) = self.session_enabled.get_mut(session_id) {
1073 for tag in tags {
1074 enabled.remove(tag);
1075 }
1076 }
1077 }
1078
1079 pub fn clear_session(&self, session_id: &str) {
1081 self.session_enabled.remove(session_id);
1082 self.session_disabled.remove(session_id);
1083 }
1084
1085 pub fn session_guard(&self, session_id: impl Into<String>) -> VisibilitySessionGuard {
1102 VisibilitySessionGuard {
1103 session_id: session_id.into(),
1104 session_enabled: Arc::clone(&self.session_enabled),
1105 session_disabled: Arc::clone(&self.session_disabled),
1106 }
1107 }
1108
1109 pub fn active_sessions_count(&self) -> usize {
1113 let mut sessions = HashSet::new();
1115 for entry in self.session_enabled.iter() {
1116 sessions.insert(entry.key().clone());
1117 }
1118 for entry in self.session_disabled.iter() {
1119 sessions.insert(entry.key().clone());
1120 }
1121 sessions.len()
1122 }
1123
1124 pub fn inner(&self) -> &H {
1126 &self.inner
1127 }
1128
1129 pub fn inner_mut(&mut self) -> &mut H {
1131 self.clear_component_registry();
1132 &mut self.inner
1133 }
1134
1135 pub fn into_inner(self) -> H {
1137 self.inner
1138 }
1139}
1140
1141#[allow(clippy::manual_async_fn)]
1142impl<H: McpHandler> McpHandler for VisibilityLayer<H> {
1143 fn server_info(&self) -> turbomcp_types::ServerInfo {
1144 self.inner.server_info()
1145 }
1146
1147 fn server_capabilities(&self) -> turbomcp_types::ServerCapabilities {
1148 self.inner.server_capabilities()
1149 }
1150
1151 fn list_tools(&self) -> Vec<Tool> {
1152 let tools = self.inner.list_tools();
1153 self.register_tools(tools.clone());
1154
1155 tools
1156 .into_iter()
1157 .filter(|tool| self.is_tool_listed(tool, None))
1158 .collect()
1159 }
1160
1161 fn list_resources(&self) -> Vec<Resource> {
1162 let resources = self.inner.list_resources();
1163 self.register_resources(resources.clone());
1164
1165 resources
1166 .into_iter()
1167 .filter(|resource| self.is_resource_listed(resource, None))
1168 .collect()
1169 }
1170
1171 fn list_resource_templates(&self) -> Vec<ResourceTemplate> {
1172 let templates = self.inner.list_resource_templates();
1173 self.register_resource_templates(templates.clone());
1174
1175 templates
1176 .into_iter()
1177 .filter(|template| self.is_resource_template_listed(template, None))
1178 .collect()
1179 }
1180
1181 fn list_prompts(&self) -> Vec<Prompt> {
1182 let prompts = self.inner.list_prompts();
1183 self.register_prompts(prompts.clone());
1184
1185 prompts
1186 .into_iter()
1187 .filter(|prompt| self.is_prompt_listed(prompt, None))
1188 .collect()
1189 }
1190
1191 fn call_tool<'a>(
1192 &'a self,
1193 name: &'a str,
1194 args: serde_json::Value,
1195 ctx: &'a RequestContext,
1196 ) -> impl std::future::Future<Output = McpResult<ToolResult>> + turbomcp_core::marker::MaybeSend + 'a
1197 {
1198 async move {
1199 if let Some(tool) = self.registered_tool(name) {
1202 if !self.is_tool_enabled(&tool, ctx.session_id()) {
1203 return Err(McpError::tool_not_found(name));
1204 }
1205 } else if !self.is_unregistered_tool_callable(name) {
1206 return Err(McpError::tool_not_found(name));
1207 }
1208
1209 self.inner.call_tool(name, args, ctx).await
1210 }
1211 }
1212
1213 fn read_resource<'a>(
1214 &'a self,
1215 uri: &'a str,
1216 ctx: &'a RequestContext,
1217 ) -> impl std::future::Future<Output = McpResult<ResourceResult>>
1218 + turbomcp_core::marker::MaybeSend
1219 + 'a {
1220 async move {
1221 if let Some(resource) = self.registered_resource(uri) {
1224 if !self.is_resource_enabled(&resource, ctx.session_id()) {
1225 return Err(McpError::resource_not_found(uri));
1226 }
1227 } else if !self.is_unregistered_resource_readable(uri) {
1228 return Err(McpError::resource_not_found(uri));
1229 }
1230
1231 self.inner.read_resource(uri, ctx).await
1232 }
1233 }
1234
1235 fn get_prompt<'a>(
1236 &'a self,
1237 name: &'a str,
1238 args: Option<serde_json::Value>,
1239 ctx: &'a RequestContext,
1240 ) -> impl std::future::Future<Output = McpResult<PromptResult>> + turbomcp_core::marker::MaybeSend + 'a
1241 {
1242 async move {
1243 if let Some(prompt) = self.registered_prompt(name) {
1246 if !self.is_prompt_enabled(&prompt, ctx.session_id()) {
1247 return Err(McpError::prompt_not_found(name));
1248 }
1249 } else if !self.is_unregistered_prompt_gettable(name) {
1250 return Err(McpError::prompt_not_found(name));
1251 }
1252
1253 self.inner.get_prompt(name, args, ctx).await
1254 }
1255 }
1256}
1257
1258#[cfg(test)]
1259mod tests {
1260 use super::*;
1261 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1262 use turbomcp_types::ToolAnnotations;
1263
1264 #[derive(Clone, Debug)]
1265 struct MockHandler;
1266
1267 #[allow(clippy::manual_async_fn)]
1268 impl McpHandler for MockHandler {
1269 fn server_info(&self) -> turbomcp_types::ServerInfo {
1270 turbomcp_types::ServerInfo::new("test", "1.0.0")
1271 }
1272
1273 fn list_tools(&self) -> Vec<Tool> {
1274 vec![
1275 Tool {
1276 name: "public_tool".to_string(),
1277 description: Some("Public tool".to_string()),
1278 annotations: Some(ToolAnnotations::default().with_read_only(true)),
1279 meta: Some({
1280 let mut m = std::collections::HashMap::new();
1281 m.insert("tags".to_string(), serde_json::json!(["public"]));
1282 m
1283 }),
1284 ..Default::default()
1285 },
1286 Tool {
1287 name: "admin_tool".to_string(),
1288 description: Some("Admin tool".to_string()),
1289 annotations: Some(ToolAnnotations::default().with_destructive(true)),
1290 meta: Some({
1291 let mut m = std::collections::HashMap::new();
1292 m.insert("tags".to_string(), serde_json::json!(["admin"]));
1293 m
1294 }),
1295 ..Default::default()
1296 },
1297 ]
1298 }
1299
1300 fn list_resources(&self) -> Vec<Resource> {
1301 vec![
1302 Resource {
1303 uri: "vault://public".to_string(),
1304 name: "public_resource".to_string(),
1305 meta: Some({
1306 let mut m = std::collections::HashMap::new();
1307 m.insert("tags".to_string(), serde_json::json!(["public"]));
1308 m
1309 }),
1310 ..Default::default()
1311 },
1312 Resource {
1313 uri: "vault://admin".to_string(),
1314 name: "admin_resource".to_string(),
1315 meta: Some({
1316 let mut m = std::collections::HashMap::new();
1317 m.insert("tags".to_string(), serde_json::json!(["admin"]));
1318 m
1319 }),
1320 ..Default::default()
1321 },
1322 ]
1323 }
1324
1325 fn list_resource_templates(&self) -> Vec<ResourceTemplate> {
1326 vec![ResourceTemplate {
1327 uri_template: "vault://notes/{id}".to_string(),
1328 name: "note_template".to_string(),
1329 meta: Some({
1330 let mut m = std::collections::HashMap::new();
1331 m.insert("tags".to_string(), serde_json::json!(["public"]));
1332 m
1333 }),
1334 ..Default::default()
1335 }]
1336 }
1337
1338 fn list_prompts(&self) -> Vec<Prompt> {
1339 vec![
1340 Prompt {
1341 name: "public_prompt".to_string(),
1342 meta: Some({
1343 let mut m = std::collections::HashMap::new();
1344 m.insert("tags".to_string(), serde_json::json!(["public"]));
1345 m
1346 }),
1347 ..Default::default()
1348 },
1349 Prompt {
1350 name: "admin_prompt".to_string(),
1351 meta: Some({
1352 let mut m = std::collections::HashMap::new();
1353 m.insert("tags".to_string(), serde_json::json!(["admin"]));
1354 m
1355 }),
1356 ..Default::default()
1357 },
1358 ]
1359 }
1360
1361 fn call_tool<'a>(
1362 &'a self,
1363 name: &'a str,
1364 _args: serde_json::Value,
1365 _ctx: &'a RequestContext,
1366 ) -> impl std::future::Future<Output = McpResult<ToolResult>>
1367 + turbomcp_core::marker::MaybeSend
1368 + 'a {
1369 async move { Ok(ToolResult::text(format!("Called {}", name))) }
1370 }
1371
1372 fn read_resource<'a>(
1373 &'a self,
1374 uri: &'a str,
1375 _ctx: &'a RequestContext,
1376 ) -> impl std::future::Future<Output = McpResult<ResourceResult>>
1377 + turbomcp_core::marker::MaybeSend
1378 + 'a {
1379 async move { Ok(ResourceResult::text(uri, format!("Read {}", uri))) }
1380 }
1381
1382 fn get_prompt<'a>(
1383 &'a self,
1384 name: &'a str,
1385 _args: Option<serde_json::Value>,
1386 _ctx: &'a RequestContext,
1387 ) -> impl std::future::Future<Output = McpResult<PromptResult>>
1388 + turbomcp_core::marker::MaybeSend
1389 + 'a {
1390 async move { Ok(PromptResult::user(format!("Prompt {}", name))) }
1391 }
1392 }
1393
1394 #[derive(Clone, Debug)]
1395 struct DynamicHandler;
1396
1397 #[allow(clippy::manual_async_fn)]
1398 impl McpHandler for DynamicHandler {
1399 fn server_info(&self) -> turbomcp_types::ServerInfo {
1400 turbomcp_types::ServerInfo::new("dynamic", "1.0.0")
1401 }
1402
1403 fn list_tools(&self) -> Vec<Tool> {
1404 vec![]
1405 }
1406
1407 fn list_resources(&self) -> Vec<Resource> {
1408 vec![]
1409 }
1410
1411 fn list_prompts(&self) -> Vec<Prompt> {
1412 vec![]
1413 }
1414
1415 fn call_tool<'a>(
1416 &'a self,
1417 name: &'a str,
1418 _args: serde_json::Value,
1419 _ctx: &'a RequestContext,
1420 ) -> impl std::future::Future<Output = McpResult<ToolResult>>
1421 + turbomcp_core::marker::MaybeSend
1422 + 'a {
1423 async move { Ok(ToolResult::text(format!("Dynamic {}", name))) }
1424 }
1425
1426 fn read_resource<'a>(
1427 &'a self,
1428 uri: &'a str,
1429 _ctx: &'a RequestContext,
1430 ) -> impl std::future::Future<Output = McpResult<ResourceResult>>
1431 + turbomcp_core::marker::MaybeSend
1432 + 'a {
1433 async move { Ok(ResourceResult::text(uri, format!("Dynamic {}", uri))) }
1434 }
1435
1436 fn get_prompt<'a>(
1437 &'a self,
1438 name: &'a str,
1439 _args: Option<serde_json::Value>,
1440 _ctx: &'a RequestContext,
1441 ) -> impl std::future::Future<Output = McpResult<PromptResult>>
1442 + turbomcp_core::marker::MaybeSend
1443 + 'a {
1444 async move { Ok(PromptResult::user(format!("Dynamic {}", name))) }
1445 }
1446 }
1447
1448 #[derive(Debug, Default)]
1449 struct CountingState {
1450 tool_lists: AtomicUsize,
1451 resource_lists: AtomicUsize,
1452 prompt_lists: AtomicUsize,
1453 }
1454
1455 #[derive(Clone, Debug, Default)]
1456 struct CountingHandler {
1457 state: Arc<CountingState>,
1458 }
1459
1460 #[allow(clippy::manual_async_fn)]
1461 impl McpHandler for CountingHandler {
1462 fn server_info(&self) -> turbomcp_types::ServerInfo {
1463 turbomcp_types::ServerInfo::new("counting", "1.0.0")
1464 }
1465
1466 fn list_tools(&self) -> Vec<Tool> {
1467 self.state.tool_lists.fetch_add(1, Ordering::SeqCst);
1468 vec![Tool {
1469 name: "counted_tool".to_string(),
1470 annotations: Some(ToolAnnotations::default().with_read_only(true)),
1471 ..Default::default()
1472 }]
1473 }
1474
1475 fn list_resources(&self) -> Vec<Resource> {
1476 self.state.resource_lists.fetch_add(1, Ordering::SeqCst);
1477 vec![Resource::new("counted://resource", "counted_resource")]
1478 }
1479
1480 fn list_prompts(&self) -> Vec<Prompt> {
1481 self.state.prompt_lists.fetch_add(1, Ordering::SeqCst);
1482 vec![Prompt::new("counted_prompt", "Counted prompt")]
1483 }
1484
1485 fn call_tool<'a>(
1486 &'a self,
1487 name: &'a str,
1488 _args: serde_json::Value,
1489 _ctx: &'a RequestContext,
1490 ) -> impl std::future::Future<Output = McpResult<ToolResult>>
1491 + turbomcp_core::marker::MaybeSend
1492 + 'a {
1493 async move { Ok(ToolResult::text(format!("Called {}", name))) }
1494 }
1495
1496 fn read_resource<'a>(
1497 &'a self,
1498 uri: &'a str,
1499 _ctx: &'a RequestContext,
1500 ) -> impl std::future::Future<Output = McpResult<ResourceResult>>
1501 + turbomcp_core::marker::MaybeSend
1502 + 'a {
1503 async move { Ok(ResourceResult::text(uri, format!("Read {}", uri))) }
1504 }
1505
1506 fn get_prompt<'a>(
1507 &'a self,
1508 name: &'a str,
1509 _args: Option<serde_json::Value>,
1510 _ctx: &'a RequestContext,
1511 ) -> impl std::future::Future<Output = McpResult<PromptResult>>
1512 + turbomcp_core::marker::MaybeSend
1513 + 'a {
1514 async move { Ok(PromptResult::user(format!("Prompt {}", name))) }
1515 }
1516 }
1517
1518 #[derive(Clone, Debug)]
1519 struct MutableToolHandler {
1520 read_only: Arc<AtomicBool>,
1521 }
1522
1523 #[allow(clippy::manual_async_fn)]
1524 impl McpHandler for MutableToolHandler {
1525 fn server_info(&self) -> turbomcp_types::ServerInfo {
1526 turbomcp_types::ServerInfo::new("mutable", "1.0.0")
1527 }
1528
1529 fn list_tools(&self) -> Vec<Tool> {
1530 let annotation = if self.read_only.load(Ordering::SeqCst) {
1531 ToolAnnotations::default().with_read_only(true)
1532 } else {
1533 ToolAnnotations::default().with_destructive(true)
1534 };
1535
1536 vec![Tool {
1537 name: "mutable_tool".to_string(),
1538 annotations: Some(annotation),
1539 ..Default::default()
1540 }]
1541 }
1542
1543 fn list_resources(&self) -> Vec<Resource> {
1544 Vec::new()
1545 }
1546
1547 fn list_prompts(&self) -> Vec<Prompt> {
1548 Vec::new()
1549 }
1550
1551 fn call_tool<'a>(
1552 &'a self,
1553 name: &'a str,
1554 _args: serde_json::Value,
1555 _ctx: &'a RequestContext,
1556 ) -> impl std::future::Future<Output = McpResult<ToolResult>>
1557 + turbomcp_core::marker::MaybeSend
1558 + 'a {
1559 async move { Ok(ToolResult::text(format!("Called {}", name))) }
1560 }
1561
1562 fn read_resource<'a>(
1563 &'a self,
1564 uri: &'a str,
1565 _ctx: &'a RequestContext,
1566 ) -> impl std::future::Future<Output = McpResult<ResourceResult>>
1567 + turbomcp_core::marker::MaybeSend
1568 + 'a {
1569 async move { Err(McpError::resource_not_found(uri)) }
1570 }
1571
1572 fn get_prompt<'a>(
1573 &'a self,
1574 name: &'a str,
1575 _args: Option<serde_json::Value>,
1576 _ctx: &'a RequestContext,
1577 ) -> impl std::future::Future<Output = McpResult<PromptResult>>
1578 + turbomcp_core::marker::MaybeSend
1579 + 'a {
1580 async move { Err(McpError::prompt_not_found(name)) }
1581 }
1582 }
1583
1584 #[derive(Clone, Debug)]
1585 struct DuplicateToolMetadataHandler;
1586
1587 #[allow(clippy::manual_async_fn)]
1588 impl McpHandler for DuplicateToolMetadataHandler {
1589 fn server_info(&self) -> turbomcp_types::ServerInfo {
1590 turbomcp_types::ServerInfo::new("duplicates", "1.0.0")
1591 }
1592
1593 fn list_tools(&self) -> Vec<Tool> {
1594 vec![
1595 Tool {
1596 name: "duplicate_tool".to_string(),
1597 annotations: Some(ToolAnnotations::default().with_read_only(true)),
1598 ..Default::default()
1599 },
1600 Tool {
1601 name: "duplicate_tool".to_string(),
1602 annotations: Some(ToolAnnotations::default().with_destructive(true)),
1603 ..Default::default()
1604 },
1605 ]
1606 }
1607
1608 fn list_resources(&self) -> Vec<Resource> {
1609 Vec::new()
1610 }
1611
1612 fn list_prompts(&self) -> Vec<Prompt> {
1613 Vec::new()
1614 }
1615
1616 fn call_tool<'a>(
1617 &'a self,
1618 name: &'a str,
1619 _args: serde_json::Value,
1620 _ctx: &'a RequestContext,
1621 ) -> impl std::future::Future<Output = McpResult<ToolResult>>
1622 + turbomcp_core::marker::MaybeSend
1623 + 'a {
1624 async move { Ok(ToolResult::text(format!("Called {}", name))) }
1625 }
1626
1627 fn read_resource<'a>(
1628 &'a self,
1629 uri: &'a str,
1630 _ctx: &'a RequestContext,
1631 ) -> impl std::future::Future<Output = McpResult<ResourceResult>>
1632 + turbomcp_core::marker::MaybeSend
1633 + 'a {
1634 async move { Err(McpError::resource_not_found(uri)) }
1635 }
1636
1637 fn get_prompt<'a>(
1638 &'a self,
1639 name: &'a str,
1640 _args: Option<serde_json::Value>,
1641 _ctx: &'a RequestContext,
1642 ) -> impl std::future::Future<Output = McpResult<PromptResult>>
1643 + turbomcp_core::marker::MaybeSend
1644 + 'a {
1645 async move { Err(McpError::prompt_not_found(name)) }
1646 }
1647 }
1648
1649 fn tool_names<H: McpHandler>(layer: &VisibilityLayer<H>) -> Vec<String> {
1650 layer
1651 .list_tools()
1652 .into_iter()
1653 .map(|tool| tool.name)
1654 .collect()
1655 }
1656
1657 #[test]
1658 fn test_component_visibility_rules_deny_wins() {
1659 let rules = ComponentVisibilityRules::allow(["search", "delete"]).with_disabled(["delete"]);
1660
1661 assert!(rules.is_enabled("search"));
1662 assert!(rules.is_listed("search"));
1663 assert!(!rules.is_enabled("delete"));
1664 assert!(!rules.is_listed("delete"));
1665 assert!(!rules.is_enabled("unknown"));
1666 }
1667
1668 #[test]
1669 fn test_component_visibility_rules_match_aliases() {
1670 let rules = ComponentVisibilityRules::allow(["vault://public"]);
1671
1672 assert!(rules.is_enabled_any(["public_resource", "vault://public"]));
1673 assert!(!rules.is_enabled_any(["public_resource", "vault://private"]));
1674 }
1675
1676 #[test]
1677 fn test_component_visibility_rules_can_hide_without_disabling() {
1678 let rules = ComponentVisibilityRules::hide(["advanced_tool"]);
1679
1680 assert!(rules.is_enabled("advanced_tool"));
1681 assert!(!rules.is_listed("advanced_tool"));
1682 assert!(rules.is_enabled("public_tool"));
1683 assert!(rules.is_listed("public_tool"));
1684 }
1685
1686 #[test]
1687 fn test_visibility_config_round_trips_serialization() {
1688 let config = VisibilityConfig::new()
1689 .with_allowed_tools(["search", "read_note"])
1690 .with_disabled_tools(["delete_note"])
1691 .with_hidden_tools(["advanced_graph"])
1692 .with_allowed_resources(["vault://public"])
1693 .with_allowed_prompts(["summarize"])
1694 .require_read_only_tools();
1695
1696 let json = serde_json::to_string(&config).expect("visibility config serializes");
1697 let decoded: VisibilityConfig =
1698 serde_json::from_str(&json).expect("visibility config deserializes");
1699
1700 assert_eq!(decoded, config);
1701 }
1702
1703 #[test]
1704 fn test_empty_tool_allowlist_hides_all_tools() {
1705 let layer =
1706 VisibilityLayer::new(MockHandler).with_allowed_tools(std::iter::empty::<&str>());
1707
1708 assert!(layer.list_tools().is_empty());
1709 }
1710
1711 #[test]
1712 fn test_conflicting_read_only_and_destructive_hints_fail_closed() {
1713 let tool = Tool {
1714 name: "conflicting_tool".to_string(),
1715 annotations: Some(
1716 ToolAnnotations::default()
1717 .with_read_only(true)
1718 .with_destructive(true),
1719 ),
1720 ..Default::default()
1721 };
1722
1723 assert!(!is_explicit_read_only_tool(&tool));
1724 }
1725
1726 #[test]
1727 fn test_visibility_layer_hides_admin() {
1728 let layer = VisibilityLayer::new(MockHandler).disable_tags(["admin"]);
1729
1730 let tools = layer.list_tools();
1731 assert_eq!(tools.len(), 1);
1732 assert_eq!(tools[0].name, "public_tool");
1733 }
1734
1735 #[test]
1736 fn test_visibility_layer_shows_all_by_default() {
1737 let layer = VisibilityLayer::new(MockHandler);
1738
1739 let tools = layer.list_tools();
1740 assert_eq!(tools.len(), 2);
1741 }
1742
1743 #[test]
1744 fn test_exact_tool_allowlist_reduces_list_surface() {
1745 let layer = VisibilityLayer::new(MockHandler).with_allowed_tools(["public_tool"]);
1746
1747 assert_eq!(tool_names(&layer), vec!["public_tool"]);
1748 }
1749
1750 #[test]
1751 fn test_exact_tool_denylist_wins_over_allowlist() {
1752 let layer = VisibilityLayer::new(MockHandler)
1753 .with_allowed_tools(["public_tool", "admin_tool"])
1754 .with_disabled_tools(["public_tool"]);
1755
1756 assert_eq!(tool_names(&layer), vec!["admin_tool"]);
1757 }
1758
1759 #[test]
1760 fn test_layer_clone_has_independent_exact_rules() {
1761 let base = VisibilityLayer::new(MockHandler);
1762 let narrowed = base.clone().with_allowed_tools(["public_tool"]);
1763
1764 assert_eq!(base.list_tools().len(), 2);
1765 assert_eq!(tool_names(&narrowed), vec!["public_tool"]);
1766 }
1767
1768 #[test]
1769 fn test_layer_clone_has_independent_tag_filters() {
1770 let base = VisibilityLayer::new(MockHandler);
1771 let narrowed = base.clone().disable_tags(["admin"]);
1772
1773 assert_eq!(base.list_tools().len(), 2);
1774 assert_eq!(tool_names(&narrowed), vec!["public_tool"]);
1775 }
1776
1777 #[test]
1778 fn test_with_disabled_tools_replaces_previous_denylist() {
1779 let layer = VisibilityLayer::new(MockHandler)
1780 .with_disabled_tools(["public_tool"])
1781 .with_disabled_tools(["admin_tool"]);
1782
1783 assert_eq!(tool_names(&layer), vec!["public_tool"]);
1784 }
1785
1786 #[tokio::test]
1787 async fn test_hidden_tool_is_not_listed_but_remains_callable() {
1788 let layer = VisibilityLayer::new(MockHandler).with_hidden_tools(["public_tool"]);
1789 let ctx = RequestContext::default();
1790
1791 assert_eq!(tool_names(&layer), vec!["admin_tool"]);
1792
1793 let result = layer
1794 .call_tool("public_tool", serde_json::json!({}), &ctx)
1795 .await
1796 .expect("hidden but enabled tool should remain callable");
1797 assert_eq!(result.first_text(), Some("Called public_tool"));
1798 }
1799
1800 #[tokio::test]
1801 async fn test_hidden_resource_and_prompt_are_not_listed_but_remain_callable() {
1802 let layer = VisibilityLayer::new(MockHandler)
1803 .with_hidden_resources(["vault://public"])
1804 .with_hidden_prompts(["public_prompt"]);
1805 let ctx = RequestContext::default();
1806
1807 assert_eq!(layer.list_resources().len(), 1);
1808 assert_eq!(layer.list_prompts().len(), 1);
1809
1810 let resource = layer
1811 .read_resource("vault://public", &ctx)
1812 .await
1813 .expect("hidden but enabled resource should remain readable");
1814 assert_eq!(resource.first_text(), Some("Read vault://public"));
1815
1816 let prompt = layer
1817 .get_prompt("public_prompt", None, &ctx)
1818 .await
1819 .expect("hidden but enabled prompt should remain gettable");
1820 assert_eq!(
1821 prompt.messages[0].content.as_text(),
1822 Some("Prompt public_prompt")
1823 );
1824 }
1825
1826 #[test]
1827 fn test_hidden_only_profile_still_advertises_operation_capabilities() {
1828 let layer = VisibilityLayer::new(MockHandler)
1829 .with_hidden_tools(["public_tool", "admin_tool"])
1830 .with_hidden_resources(["vault://public", "vault://admin"])
1831 .with_hidden_resource_templates(["vault://notes/{id}"])
1832 .with_hidden_prompts(["public_prompt", "admin_prompt"]);
1833
1834 assert!(layer.list_tools().is_empty());
1835 assert!(layer.list_resources().is_empty());
1836 assert!(layer.list_resource_templates().is_empty());
1837 assert!(layer.list_prompts().is_empty());
1838
1839 let capabilities = layer.server_capabilities();
1840 assert!(
1841 capabilities.tools.is_some(),
1842 "hidden-but-callable tools still require the tools capability"
1843 );
1844 assert!(
1845 capabilities.resources.is_some(),
1846 "hidden-but-readable resources still require the resources capability"
1847 );
1848 assert!(
1849 capabilities.prompts.is_some(),
1850 "hidden-but-gettable prompts still require the prompts capability"
1851 );
1852 }
1853
1854 #[tokio::test]
1855 async fn test_disabled_tool_wins_over_hidden_tool() {
1856 let layer = VisibilityLayer::new(MockHandler)
1857 .with_hidden_tools(["public_tool"])
1858 .with_disabled_tools(["public_tool"]);
1859 let ctx = RequestContext::default();
1860
1861 assert!(!tool_names(&layer).contains(&"public_tool".to_string()));
1862
1863 let err = layer
1864 .call_tool("public_tool", serde_json::json!({}), &ctx)
1865 .await
1866 .expect_err("disabled tool should not be callable even if hidden");
1867 assert_eq!(err.kind, turbomcp_core::error::ErrorKind::ToolNotFound);
1868 }
1869
1870 #[test]
1871 fn test_tag_disabled_tool_stays_hidden_despite_name_allowlist() {
1872 let layer = VisibilityLayer::new(MockHandler)
1873 .with_allowed_tools(["admin_tool"])
1874 .disable_tags(["admin"]);
1875
1876 assert!(layer.list_tools().is_empty());
1877 }
1878
1879 #[test]
1880 fn test_visibility_config_getter_reflects_builder_methods() {
1881 let layer = VisibilityLayer::new(MockHandler)
1882 .with_allowed_tools(["public_tool"])
1883 .with_disabled_resources(["vault://admin"])
1884 .with_hidden_prompts(["admin_prompt"])
1885 .require_read_only_tools();
1886
1887 let config = layer.visibility_config();
1888
1889 assert!(config.tools.allow.unwrap().contains("public_tool"));
1890 assert!(config.resources.deny.contains("vault://admin"));
1891 assert!(config.prompts.hide.contains("admin_prompt"));
1892 assert!(config.require_read_only_tools);
1893 }
1894
1895 #[tokio::test]
1896 async fn test_disabled_tool_call_returns_not_found() {
1897 let layer = VisibilityLayer::new(MockHandler).with_disabled_tools(["public_tool"]);
1898 let ctx = RequestContext::default();
1899
1900 let err = layer
1901 .call_tool("public_tool", serde_json::json!({}), &ctx)
1902 .await
1903 .expect_err("hidden tool calls should be rejected");
1904
1905 assert_eq!(err.kind, turbomcp_core::error::ErrorKind::ToolNotFound);
1906 }
1907
1908 #[tokio::test]
1909 async fn test_session_enable_allows_hidden_tagged_tool_call() {
1910 let layer = VisibilityLayer::new(MockHandler).disable_tags(["admin"]);
1911 let ctx = RequestContext::default().with_session_id("session1");
1912
1913 let err = layer
1914 .call_tool("admin_tool", serde_json::json!({}), &ctx)
1915 .await
1916 .expect_err("globally hidden tagged tool should be rejected");
1917 assert_eq!(err.kind, turbomcp_core::error::ErrorKind::ToolNotFound);
1918
1919 layer.enable_for_session("session1", &["admin".to_string()]);
1920
1921 let result = layer
1922 .call_tool("admin_tool", serde_json::json!({}), &ctx)
1923 .await
1924 .expect("session-enabled tagged tool should pass through");
1925 assert_eq!(result.first_text(), Some("Called admin_tool"));
1926 }
1927
1928 #[tokio::test]
1929 async fn test_session_disable_blocks_visible_tagged_tool_call() {
1930 let layer = VisibilityLayer::new(MockHandler);
1931 let ctx = RequestContext::default().with_session_id("session1");
1932
1933 let result = layer
1934 .call_tool("public_tool", serde_json::json!({}), &ctx)
1935 .await
1936 .expect("public tool should initially pass through");
1937 assert_eq!(result.first_text(), Some("Called public_tool"));
1938
1939 layer.disable_for_session("session1", &["public".to_string()]);
1940
1941 let err = layer
1942 .call_tool("public_tool", serde_json::json!({}), &ctx)
1943 .await
1944 .expect_err("session-disabled tagged tool should be rejected");
1945 assert_eq!(err.kind, turbomcp_core::error::ErrorKind::ToolNotFound);
1946 }
1947
1948 #[tokio::test]
1949 async fn test_dispatch_uses_registered_tool_without_relisting() {
1950 let handler = CountingHandler::default();
1951 let state = Arc::clone(&handler.state);
1952 let layer = VisibilityLayer::new(handler);
1953 let ctx = RequestContext::default();
1954
1955 assert_eq!(tool_names(&layer), vec!["counted_tool"]);
1956 assert_eq!(state.tool_lists.load(Ordering::SeqCst), 1);
1957
1958 layer
1959 .call_tool("counted_tool", serde_json::json!({}), &ctx)
1960 .await
1961 .expect("registered tool should dispatch");
1962 layer
1963 .call_tool("counted_tool", serde_json::json!({}), &ctx)
1964 .await
1965 .expect("registered tool should dispatch again");
1966
1967 assert_eq!(
1968 state.tool_lists.load(Ordering::SeqCst),
1969 1,
1970 "dispatch should use the registry populated by tools/list"
1971 );
1972 }
1973
1974 #[tokio::test]
1975 async fn test_dispatch_lazily_builds_registry_once_per_component_family() {
1976 let handler = CountingHandler::default();
1977 let state = Arc::clone(&handler.state);
1978 let layer = VisibilityLayer::new(handler);
1979 let ctx = RequestContext::default();
1980
1981 layer
1982 .call_tool("counted_tool", serde_json::json!({}), &ctx)
1983 .await
1984 .expect("registered tool should dispatch");
1985 layer
1986 .call_tool("counted_tool", serde_json::json!({}), &ctx)
1987 .await
1988 .expect("registered tool should dispatch again");
1989 assert_eq!(state.tool_lists.load(Ordering::SeqCst), 1);
1990
1991 layer
1992 .read_resource("counted://resource", &ctx)
1993 .await
1994 .expect("registered resource should dispatch");
1995 layer
1996 .read_resource("counted://resource", &ctx)
1997 .await
1998 .expect("registered resource should dispatch again");
1999 assert_eq!(state.resource_lists.load(Ordering::SeqCst), 1);
2000
2001 layer
2002 .get_prompt("counted_prompt", None, &ctx)
2003 .await
2004 .expect("registered prompt should dispatch");
2005 layer
2006 .get_prompt("counted_prompt", None, &ctx)
2007 .await
2008 .expect("registered prompt should dispatch again");
2009 assert_eq!(state.prompt_lists.load(Ordering::SeqCst), 1);
2010 }
2011
2012 #[tokio::test]
2013 async fn test_refresh_component_registry_updates_dispatch_metadata() {
2014 let read_only = Arc::new(AtomicBool::new(false));
2015 let layer = VisibilityLayer::new(MutableToolHandler {
2016 read_only: Arc::clone(&read_only),
2017 })
2018 .require_read_only_tools();
2019 let ctx = RequestContext::default();
2020
2021 let err = layer
2022 .call_tool("mutable_tool", serde_json::json!({}), &ctx)
2023 .await
2024 .expect_err("initial destructive metadata should be rejected");
2025 assert_eq!(err.kind, turbomcp_core::error::ErrorKind::ToolNotFound);
2026
2027 read_only.store(true, Ordering::SeqCst);
2028 let err = layer
2029 .call_tool("mutable_tool", serde_json::json!({}), &ctx)
2030 .await
2031 .expect_err("cached destructive metadata should remain fail-closed");
2032 assert_eq!(err.kind, turbomcp_core::error::ErrorKind::ToolNotFound);
2033
2034 layer.refresh_component_registry();
2035
2036 let result = layer
2037 .call_tool("mutable_tool", serde_json::json!({}), &ctx)
2038 .await
2039 .expect("refreshed read-only metadata should permit dispatch");
2040 assert_eq!(result.first_text(), Some("Called mutable_tool"));
2041 }
2042
2043 #[tokio::test]
2044 async fn test_registry_preserves_first_duplicate_tool_metadata() {
2045 let layer = VisibilityLayer::new(DuplicateToolMetadataHandler).require_read_only_tools();
2046 let ctx = RequestContext::default();
2047
2048 let result = layer
2049 .call_tool("duplicate_tool", serde_json::json!({}), &ctx)
2050 .await
2051 .expect("first listed read-only metadata should govern dispatch");
2052
2053 assert_eq!(result.first_text(), Some("Called duplicate_tool"));
2054 }
2055
2056 #[tokio::test]
2057 async fn test_exact_tool_policy_blocks_unlisted_dynamic_call() {
2058 let layer = VisibilityLayer::new(DynamicHandler).with_disabled_tools(["dynamic_tool"]);
2059 let ctx = RequestContext::default();
2060
2061 let err = layer
2062 .call_tool("dynamic_tool", serde_json::json!({}), &ctx)
2063 .await
2064 .expect_err("denylisted dynamic tool calls should be rejected");
2065
2066 assert_eq!(err.kind, turbomcp_core::error::ErrorKind::ToolNotFound);
2067 }
2068
2069 #[tokio::test]
2070 async fn test_exact_tool_allowlist_can_permit_unlisted_dynamic_call() {
2071 let layer = VisibilityLayer::new(DynamicHandler).with_allowed_tools(["dynamic_tool"]);
2072 let ctx = RequestContext::default();
2073
2074 let result = layer
2075 .call_tool("dynamic_tool", serde_json::json!({}), &ctx)
2076 .await
2077 .expect("allowlisted dynamic tool should pass through");
2078
2079 assert_eq!(result.first_text(), Some("Dynamic dynamic_tool"));
2080 }
2081
2082 #[tokio::test]
2083 async fn test_read_only_policy_blocks_unlisted_dynamic_tool() {
2084 let layer = VisibilityLayer::new(DynamicHandler)
2085 .with_allowed_tools(["dynamic_tool"])
2086 .require_read_only_tools();
2087 let ctx = RequestContext::default();
2088
2089 let err = layer
2090 .call_tool("dynamic_tool", serde_json::json!({}), &ctx)
2091 .await
2092 .expect_err("read-only policy should fail closed without annotations");
2093
2094 assert_eq!(err.kind, turbomcp_core::error::ErrorKind::ToolNotFound);
2095 }
2096
2097 #[test]
2098 fn test_require_read_only_tools_hides_mutating_tools() {
2099 let layer = VisibilityLayer::new(MockHandler).require_read_only_tools();
2100
2101 assert_eq!(tool_names(&layer), vec!["public_tool"]);
2102 }
2103
2104 #[tokio::test]
2105 async fn test_disabled_resource_read_returns_not_found() {
2106 let layer = VisibilityLayer::new(MockHandler).with_disabled_resources(["vault://public"]);
2107 let ctx = RequestContext::default();
2108
2109 let err = layer
2110 .read_resource("vault://public", &ctx)
2111 .await
2112 .expect_err("hidden resource reads should be rejected");
2113
2114 assert_eq!(err.kind, turbomcp_core::error::ErrorKind::ResourceNotFound);
2115 }
2116
2117 #[tokio::test]
2118 async fn test_resource_allowlist_by_name_allows_uri_read() {
2119 let layer = VisibilityLayer::new(MockHandler).with_allowed_resources(["public_resource"]);
2120 let ctx = RequestContext::default();
2121
2122 let result = layer
2123 .read_resource("vault://public", &ctx)
2124 .await
2125 .expect("allowlisted resource name should permit URI read");
2126
2127 assert_eq!(result.first_text(), Some("Read vault://public"));
2128 }
2129
2130 #[tokio::test]
2131 async fn test_exact_resource_policy_blocks_unlisted_dynamic_read() {
2132 let layer =
2133 VisibilityLayer::new(DynamicHandler).with_disabled_resources(["vault://dynamic"]);
2134 let ctx = RequestContext::default();
2135
2136 let err = layer
2137 .read_resource("vault://dynamic", &ctx)
2138 .await
2139 .expect_err("denylisted dynamic resources should be rejected");
2140
2141 assert_eq!(err.kind, turbomcp_core::error::ErrorKind::ResourceNotFound);
2142 }
2143
2144 #[tokio::test]
2145 async fn test_disabled_prompt_get_returns_not_found() {
2146 let layer = VisibilityLayer::new(MockHandler).with_disabled_prompts(["public_prompt"]);
2147 let ctx = RequestContext::default();
2148
2149 let err = layer
2150 .get_prompt("public_prompt", None, &ctx)
2151 .await
2152 .expect_err("hidden prompts should be rejected");
2153
2154 assert_eq!(err.kind, turbomcp_core::error::ErrorKind::PromptNotFound);
2155 }
2156
2157 #[tokio::test]
2158 async fn test_exact_prompt_policy_blocks_unlisted_dynamic_get() {
2159 let layer = VisibilityLayer::new(DynamicHandler).with_disabled_prompts(["dynamic_prompt"]);
2160 let ctx = RequestContext::default();
2161
2162 let err = layer
2163 .get_prompt("dynamic_prompt", None, &ctx)
2164 .await
2165 .expect_err("denylisted dynamic prompts should be rejected");
2166
2167 assert_eq!(err.kind, turbomcp_core::error::ErrorKind::PromptNotFound);
2168 }
2169
2170 #[test]
2171 fn test_visibility_config_applies_component_rules() {
2172 let config = VisibilityConfig::new()
2173 .with_allowed_tools(["public_tool"])
2174 .with_disabled_resources(["vault://admin"])
2175 .with_allowed_prompts(["public_prompt"])
2176 .with_allowed_resource_templates(["vault://notes/{id}"]);
2177
2178 let layer = VisibilityLayer::new(MockHandler).with_visibility_config(config);
2179
2180 assert_eq!(tool_names(&layer), vec!["public_tool"]);
2181
2182 let resources = layer.list_resources();
2183 assert_eq!(resources.len(), 1);
2184 assert_eq!(resources[0].name, "public_resource");
2185
2186 let prompts = layer.list_prompts();
2187 assert_eq!(prompts.len(), 1);
2188 assert_eq!(prompts[0].name, "public_prompt");
2189
2190 let templates = layer.list_resource_templates();
2191 assert_eq!(templates.len(), 1);
2192 assert_eq!(templates[0].name, "note_template");
2193 }
2194
2195 #[test]
2196 fn test_session_enable_override() {
2197 let layer = VisibilityLayer::new(MockHandler).disable_tags(["admin"]);
2198
2199 assert_eq!(layer.list_tools().len(), 1);
2201
2202 layer.enable_for_session("session1", &["admin".to_string()]);
2204
2205 assert_eq!(layer.list_tools().len(), 1);
2208
2209 layer.clear_session("session1");
2211 }
2212
2213 #[test]
2214 fn test_session_guard_cleanup() {
2215 let layer = VisibilityLayer::new(MockHandler).disable_tags(["admin"]);
2216
2217 {
2218 let _guard = layer.session_guard("guard-session");
2219
2220 layer.enable_for_session("guard-session", &["admin".to_string()]);
2222 layer.disable_for_session("guard-session", &["public".to_string()]);
2223
2224 assert!(layer.active_sessions_count() > 0);
2226 }
2227
2228 assert_eq!(layer.active_sessions_count(), 0);
2230 }
2231
2232 #[test]
2233 fn test_active_sessions_count() {
2234 let layer = VisibilityLayer::new(MockHandler);
2235
2236 assert_eq!(layer.active_sessions_count(), 0);
2237
2238 layer.enable_for_session("session1", &["tag1".to_string()]);
2239 assert_eq!(layer.active_sessions_count(), 1);
2240
2241 layer.disable_for_session("session2", &["tag2".to_string()]);
2242 assert_eq!(layer.active_sessions_count(), 2);
2243
2244 layer.enable_for_session("session1", &["tag2".to_string()]);
2246 assert_eq!(layer.active_sessions_count(), 2);
2247
2248 layer.clear_session("session1");
2249 assert_eq!(layer.active_sessions_count(), 1);
2250
2251 layer.clear_session("session2");
2252 assert_eq!(layer.active_sessions_count(), 0);
2253 }
2254}