1use std::{
2 collections::{HashMap, HashSet},
3 sync::{Arc, OnceLock, RwLock},
4};
5
6use futures::future::BoxFuture;
7use tokio::sync::Mutex;
8
9use crate::{
10 Arguments, Error, Result, ServerCtx,
11 schema::{CallToolResult, Cursor, ListToolsResult, ServerNotification, Tool, ToolSchema},
12};
13
14pub struct ToolSetView<'a> {
16 groups: &'a HashMap<String, GroupSnapshot>,
18}
19
20#[doc(hidden)]
22pub type ToolFuture<'a> = BoxFuture<'a, Result<CallToolResult>>;
23
24impl ToolSetView<'_> {
25 pub fn is_group_active(&self, name: &str) -> bool {
27 is_group_active_snapshot(self.groups, name)
28 }
29}
30
31#[derive(Clone)]
33pub enum Visibility {
34 Always,
36 Group(String),
38 When(Arc<dyn Fn(&ToolSetView) -> bool + Send + Sync>),
40}
41
42impl Visibility {
43 fn is_visible(&self, view: &ToolSetView) -> bool {
45 match self {
46 Self::Always => true,
47 Self::Group(name) => view.is_group_active(name),
48 Self::When(predicate) => predicate(view),
49 }
50 }
51}
52
53pub struct GroupInfo {
55 pub name: String,
57 pub description: String,
59 pub active: bool,
61 pub parent: Option<String>,
63 pub tool_count: usize,
65}
66
67pub struct GroupConfig {
69 pub name: String,
73 pub description: String,
75 pub parent: Option<String>,
77 pub on_activate: Option<ActivationHook>,
79 pub on_deactivate: Option<ActivationHook>,
81 pub show_deactivator: bool,
83}
84
85pub type ActivationHook = Box<dyn Fn(&ServerCtx) -> BoxFuture<'static, Result<()>> + Send + Sync>;
87
88pub trait Group: GroupDispatch {
90 fn config(&self) -> GroupConfig;
95
96 fn register(&self, toolset: &ToolSet, parent: Option<&str>) -> Result<()>
98 where
99 Self: Sized,
100 {
101 GroupRegistration::register_with_override(self, toolset, parent, None)
102 }
103}
104
105#[doc(hidden)]
107pub trait GroupDispatch {
108 fn register_tools(&self, toolset: &ToolSet, group_name: &str) -> Result<()>;
110
111 fn call_tool<'a>(
113 &'a self,
114 ctx: &'a ServerCtx,
115 name: &'a str,
116 arguments: Option<Arguments>,
117 ) -> ToolFuture<'a>;
118}
119
120#[doc(hidden)]
122pub trait GroupRegistration: Group + GroupDispatch {
123 fn register_with_override(
125 &self,
126 toolset: &ToolSet,
127 parent: Option<&str>,
128 segment_override: Option<&str>,
129 ) -> Result<()>;
130}
131
132impl<T> GroupRegistration for T
133where
134 T: Group + GroupDispatch,
135{
136 fn register_with_override(
137 &self,
138 toolset: &ToolSet,
139 parent: Option<&str>,
140 segment_override: Option<&str>,
141 ) -> Result<()> {
142 let mut config = self.config();
143 let segment = segment_override.unwrap_or(config.name.as_str());
144 validate_group_segment(segment)?;
145 let group_name = if let Some(parent) = parent {
146 format!("{parent}.{segment}")
147 } else {
148 segment.to_string()
149 };
150 config.parent = parent.map(|parent| parent.to_string());
151 config.name = group_name.clone();
152 toolset.register_group(config)?;
153 self.register_tools(toolset, &group_name)?;
154 Ok(())
155 }
156}
157
158#[derive(Clone)]
160pub struct ToolSet {
161 tools: Arc<RwLock<HashMap<String, ToolEntry>>>,
163 groups: ToolGroups,
165 registration: Arc<OnceLock<()>>,
167 activation_lock: Arc<Mutex<()>>,
169}
170
171impl Default for ToolSet {
172 fn default() -> Self {
173 Self {
174 tools: Arc::new(RwLock::new(HashMap::new())),
175 groups: ToolGroups::default(),
176 registration: Arc::new(OnceLock::new()),
177 activation_lock: Arc::new(Mutex::new(())),
178 }
179 }
180}
181
182impl ToolSet {
183 pub fn register_group(&self, config: GroupConfig) -> Result<()> {
187 validate_group_path(&config.name)?;
188 let auto_config = AutoGroupConfig::from(&config);
189 self.groups.register_group(config)?;
190 if let Err(error) = self.register_auto_tools(&auto_config) {
191 self.groups.unregister_group(&auto_config.name);
192 return Err(error);
193 }
194 Ok(())
195 }
196
197 pub fn register_exclusion(&self, groups: &[&str]) -> Result<()> {
199 self.groups.register_exclusion(groups)
200 }
201
202 pub fn qualified_name(group: &str, base: &str) -> String {
204 if group.is_empty() {
205 base.to_string()
206 } else {
207 format!("{group}.{base}")
208 }
209 }
210
211 pub async fn activate_group(&self, name: &str, ctx: &ServerCtx) -> Result<bool> {
213 let _guard = self.activation_lock.lock().await;
214 let plan = self.groups.plan_activation(name)?;
215 run_activation_hooks(&plan.hooks, ctx).await?;
216 let changed = self.groups.apply_activation(plan)?;
217 if changed {
218 self.notify_list_changed(ctx)?;
219 }
220 Ok(changed)
221 }
222
223 pub async fn deactivate_group(&self, name: &str, ctx: &ServerCtx) -> Result<bool> {
225 let _guard = self.activation_lock.lock().await;
226 let plan = self.groups.plan_deactivation(name)?;
227 run_activation_hooks(&plan.hooks, ctx).await?;
228 let changed = self.groups.apply_deactivation(plan)?;
229 if changed {
230 self.notify_list_changed(ctx)?;
231 }
232 Ok(changed)
233 }
234
235 pub fn is_group_active(&self, name: &str) -> bool {
237 self.groups.is_active(name)
238 }
239
240 pub fn list_groups(&self) -> Vec<GroupInfo> {
242 let tool_counts = self.group_tool_counts();
243 let mut groups = self.groups.list_groups(&tool_counts);
244 groups.sort_by(|a, b| a.name.cmp(&b.name));
245 groups
246 }
247
248 pub fn register<F>(&self, name: &str, tool: Tool, handler: F) -> Result<()>
250 where
251 F: for<'a> Fn(&'a ServerCtx, Option<Arguments>) -> ToolFuture<'a> + Send + Sync + 'static,
252 {
253 self.register_with_visibility(name, tool, Visibility::Always, handler)
254 }
255
256 pub fn register_with_visibility<F>(
258 &self,
259 name: &str,
260 tool: Tool,
261 visibility: Visibility,
262 handler: F,
263 ) -> Result<()>
264 where
265 F: for<'a> Fn(&'a ServerCtx, Option<Arguments>) -> ToolFuture<'a> + Send + Sync + 'static,
266 {
267 let handler: ToolHandler = Arc::new(handler);
268 self.register_entry(name, tool, visibility, Some(handler), ToolOrigin::Explicit)
269 }
270
271 pub fn unregister(&self, name: &str) -> Option<Tool> {
273 self.tools
274 .write()
275 .unwrap_or_else(|err| err.into_inner())
276 .remove(name)
277 .map(|entry| entry.tool)
278 }
279
280 pub fn notify_list_changed(&self, ctx: &ServerCtx) -> Result<()> {
282 ctx.notify(ServerNotification::tool_list_changed())
283 }
284
285 pub fn list_tools(&self, cursor: Option<Cursor>) -> Result<ListToolsResult> {
290 let snapshot = self.groups.snapshot();
291 let view = ToolSetView { groups: &snapshot };
292 let entries = self
293 .tools
294 .read()
295 .unwrap_or_else(|err| err.into_inner())
296 .values()
297 .cloned()
298 .collect::<Vec<_>>();
299 let mut tools = entries
300 .into_iter()
301 .filter(|entry| entry.visibility.is_visible(&view))
302 .map(|entry| entry.tool)
303 .collect::<Vec<_>>();
304 tools.sort_by(|a, b| a.name.cmp(&b.name));
305 let offset = cursor.map(parse_cursor_offset).transpose()?.unwrap_or(0);
306 if offset > tools.len() {
307 return Err(Error::InvalidParams("cursor out of range".to_string()));
308 }
309 let tools = tools.into_iter().skip(offset).collect();
310 Ok(ListToolsResult {
311 tools,
312 next_cursor: None,
313 })
314 }
315
316 pub async fn call_tool(
320 &self,
321 ctx: &ServerCtx,
322 name: &str,
323 arguments: Option<Arguments>,
324 ) -> Result<CallToolResult> {
325 if !self.is_tool_visible(name) {
326 return Err(Error::ToolNotFound(name.to_string()));
327 }
328 let handler = self.tool_handler(name);
329 let handler = handler.ok_or_else(|| Error::ToolNotFound(name.to_string()))?;
330 handler(ctx, arguments).await
331 }
332
333 pub async fn call_tool_with<H, F>(
335 &self,
336 handler: &H,
337 ctx: &ServerCtx,
338 name: &str,
339 arguments: Option<Arguments>,
340 dispatch: F,
341 ) -> Result<CallToolResult>
342 where
343 F: for<'a> Fn(&'a H, &'a ServerCtx, &'a str, Option<Arguments>) -> ToolFuture<'a>,
344 {
345 if !self.is_tool_visible(name) {
346 return Err(Error::ToolNotFound(name.to_string()));
347 }
348 if let Some(tool_handler) = self.tool_handler(name) {
349 return tool_handler(ctx, arguments).await;
350 }
351 dispatch(handler, ctx, name, arguments).await
352 }
353
354 #[doc(hidden)]
356 pub fn register_schema(&self, name: &str, tool: Tool, visibility: Visibility) -> Result<()> {
357 self.register_entry(name, tool, visibility, None, ToolOrigin::Explicit)
358 }
359
360 #[doc(hidden)]
362 pub fn ensure_registered<F>(&self, register: F)
363 where
364 F: FnOnce(),
365 {
366 let _ = self.registration.get_or_init(|| {
367 register();
368 });
369 }
370
371 #[doc(hidden)]
373 pub fn is_tool_visible(&self, name: &str) -> bool {
374 let Some(entry) = self.tool_entry(name) else {
375 return false;
376 };
377 let snapshot = self.groups.snapshot();
378 let view = ToolSetView { groups: &snapshot };
379 entry.visibility.is_visible(&view)
380 }
381
382 #[doc(hidden)]
384 pub async fn call_dynamic_tool(
385 &self,
386 ctx: &ServerCtx,
387 name: &str,
388 arguments: Option<Arguments>,
389 ) -> Result<CallToolResult> {
390 self.call_tool(ctx, name, arguments).await
391 }
392
393 fn tool_group_name(tool_name: &str) -> Option<String> {
395 tool_name
396 .rsplit_once('.')
397 .map(|x| x.0)
398 .map(|group| group.to_string())
399 }
400
401 fn group_tool_counts(&self) -> HashMap<String, usize> {
403 let tools = self.tools.read().unwrap_or_else(|err| err.into_inner());
404 let mut counts = HashMap::new();
405 for name in tools.keys() {
406 if let Some(group) = Self::tool_group_name(name) {
407 *counts.entry(group).or_insert(0) += 1;
408 }
409 }
410 counts
411 }
412
413 fn register_entry(
415 &self,
416 name: &str,
417 mut tool: Tool,
418 visibility: Visibility,
419 handler: Option<ToolHandler>,
420 origin: ToolOrigin,
421 ) -> Result<()> {
422 self.validate_tool_registration(name, &visibility)?;
423 tool.name = name.to_string();
424 let mut tools = self.tools.write().unwrap_or_else(|err| err.into_inner());
425 if let Some(existing) = tools.get(name)
426 && (existing.origin != ToolOrigin::AutoGroup || origin != ToolOrigin::Explicit)
427 {
428 return Err(Error::InvalidConfiguration(format!(
429 "tool already registered: {name}"
430 )));
431 }
432 tools.insert(
433 name.to_string(),
434 ToolEntry {
435 tool,
436 visibility,
437 handler,
438 origin,
439 },
440 );
441 Ok(())
442 }
443
444 fn validate_tool_registration(&self, name: &str, visibility: &Visibility) -> Result<()> {
446 let (group_path, _base) = split_tool_name(name)?;
447 match visibility {
448 Visibility::Group(group_name) => {
449 if group_path != Some(group_name.as_str()) {
450 return Err(Error::InvalidConfiguration(format!(
451 "tool '{name}' must be prefixed with group '{group_name}'"
452 )));
453 }
454 self.groups.ensure_group_exists(group_name)?;
455 }
456 _ => {
457 if let Some(group) = group_path {
458 self.groups.ensure_group_exists(group)?;
459 }
460 }
461 }
462 Ok(())
463 }
464
465 fn tool_entry(&self, name: &str) -> Option<ToolEntry> {
467 self.tools
468 .read()
469 .unwrap_or_else(|err| err.into_inner())
470 .get(name)
471 .cloned()
472 }
473
474 fn tool_handler(&self, name: &str) -> Option<ToolHandler> {
476 self.tool_entry(name).and_then(|entry| entry.handler)
477 }
478
479 fn register_auto_tools(&self, config: &AutoGroupConfig) -> Result<()> {
481 let group = config.name.clone();
482 let activate_name = Self::qualified_name(&group, "activate");
483 let activate_tool = activation_tool(&activate_name, &config.description, true);
484 let toolset = self.clone();
485 let handler_group = group.clone();
486 let activate_handler: ToolHandler =
487 Arc::new(move |ctx: &ServerCtx, _args: Option<Arguments>| {
488 let toolset = toolset.clone();
489 let handler_group = handler_group.clone();
490 let ctx = ctx.clone();
491 Box::pin(async move {
492 let _ = toolset.activate_group(&handler_group, &ctx).await?;
493 Ok(CallToolResult::new())
494 })
495 });
496 let mut entries = vec![(activate_name, activate_tool, Some(activate_handler))];
497
498 if config.show_deactivator {
499 let deactivate_name = Self::qualified_name(&group, "deactivate");
500 let deactivate_tool = activation_tool(&deactivate_name, &config.description, false);
501 let toolset = self.clone();
502 let handler_group = group;
503 let deactivate_handler: ToolHandler =
504 Arc::new(move |ctx: &ServerCtx, _args: Option<Arguments>| {
505 let toolset = toolset.clone();
506 let handler_group = handler_group.clone();
507 let ctx = ctx.clone();
508 Box::pin(async move {
509 let _ = toolset.deactivate_group(&handler_group, &ctx).await?;
510 Ok(CallToolResult::new())
511 })
512 });
513 entries.push((deactivate_name, deactivate_tool, Some(deactivate_handler)));
514 }
515
516 let visibility = Visibility::Always;
517 for (name, tool, _handler) in &mut entries {
518 self.validate_tool_registration(name, &visibility)?;
519 tool.name = name.clone();
520 }
521
522 let mut tools = self.tools.write().unwrap_or_else(|err| err.into_inner());
523 for (name, _, _) in &entries {
524 if tools.contains_key(name) {
525 return Err(Error::InvalidConfiguration(format!(
526 "tool already registered: {name}"
527 )));
528 }
529 }
530
531 for (name, tool, handler) in entries {
532 tools.insert(
533 name,
534 ToolEntry {
535 tool,
536 visibility: visibility.clone(),
537 handler,
538 origin: ToolOrigin::AutoGroup,
539 },
540 );
541 }
542
543 Ok(())
544 }
545}
546
547type ToolHandler =
549 Arc<dyn for<'a> Fn(&'a ServerCtx, Option<Arguments>) -> ToolFuture<'a> + Send + Sync>;
550
551type SharedActivationHook = Arc<dyn Fn(&ServerCtx) -> BoxFuture<'static, Result<()>> + Send + Sync>;
553
554#[derive(Clone)]
556struct ToolEntry {
557 tool: Tool,
559 visibility: Visibility,
561 handler: Option<ToolHandler>,
563 origin: ToolOrigin,
565}
566
567#[derive(Clone, Copy, Debug, PartialEq, Eq)]
569enum ToolOrigin {
570 AutoGroup,
572 Explicit,
574}
575
576struct AutoGroupConfig {
578 name: String,
580 description: String,
582 show_deactivator: bool,
584}
585
586impl From<&GroupConfig> for AutoGroupConfig {
587 fn from(config: &GroupConfig) -> Self {
588 Self {
589 name: config.name.clone(),
590 description: config.description.clone(),
591 show_deactivator: config.show_deactivator,
592 }
593 }
594}
595
596#[derive(Clone)]
598struct GroupSnapshot {
599 active: bool,
601 parent: Option<String>,
603}
604
605#[derive(Clone, Default)]
607struct ToolGroups {
608 registry: Arc<RwLock<GroupRegistry>>,
610}
611
612impl ToolGroups {
613 fn register_group(&self, config: GroupConfig) -> Result<()> {
615 let mut registry = self.registry.write().unwrap_or_else(|err| err.into_inner());
616 if registry.groups.contains_key(&config.name) {
617 return Err(Error::InvalidConfiguration(format!(
618 "group already registered: {}",
619 config.name
620 )));
621 }
622 if let Some(parent) = &config.parent {
623 if !registry.groups.contains_key(parent) {
624 return Err(Error::GroupNotFound(parent.clone()));
625 }
626 if !config.name.starts_with(&format!("{parent}.")) {
627 return Err(Error::InvalidConfiguration(format!(
628 "group '{}' must be nested under parent '{}'",
629 config.name, parent
630 )));
631 }
632 }
633 let state = GroupState {
634 description: config.description.clone(),
635 active: false,
636 parent: config.parent.clone(),
637 on_activate: config.on_activate.map(Arc::from),
638 on_deactivate: config.on_deactivate.map(Arc::from),
639 };
640 registry.groups.insert(config.name, state);
641 Ok(())
642 }
643
644 fn unregister_group(&self, name: &str) {
646 let mut registry = self.registry.write().unwrap_or_else(|err| err.into_inner());
647 registry.groups.remove(name);
648 registry
649 .exclusions
650 .retain(|exclusion| !exclusion.iter().any(|group| group == name));
651 }
652
653 fn register_exclusion(&self, groups: &[&str]) -> Result<()> {
655 if groups.len() < 2 {
656 return Err(Error::InvalidConfiguration(
657 "exclusion sets require at least two groups".to_string(),
658 ));
659 }
660 let mut unique = HashSet::new();
661 let mut entries = Vec::new();
662 let registry = self.registry.read().unwrap_or_else(|err| err.into_inner());
663 for group in groups {
664 if !registry.groups.contains_key(*group) {
665 return Err(Error::GroupNotFound((*group).to_string()));
666 }
667 if unique.insert(*group) {
668 entries.push((*group).to_string());
669 }
670 }
671 drop(registry);
672 let mut registry = self.registry.write().unwrap_or_else(|err| err.into_inner());
673 registry.exclusions.push(entries);
674 Ok(())
675 }
676
677 fn ensure_group_exists(&self, name: &str) -> Result<()> {
679 let registry = self.registry.read().unwrap_or_else(|err| err.into_inner());
680 if registry.groups.contains_key(name) {
681 Ok(())
682 } else {
683 Err(Error::GroupNotFound(name.to_string()))
684 }
685 }
686
687 fn is_active(&self, name: &str) -> bool {
689 let snapshot = self.snapshot();
690 is_group_active_snapshot(&snapshot, name)
691 }
692
693 fn snapshot(&self) -> HashMap<String, GroupSnapshot> {
695 let registry = self.registry.read().unwrap_or_else(|err| err.into_inner());
696 registry
697 .groups
698 .iter()
699 .map(|(name, state)| {
700 (
701 name.clone(),
702 GroupSnapshot {
703 active: state.active,
704 parent: state.parent.clone(),
705 },
706 )
707 })
708 .collect()
709 }
710
711 fn list_groups(&self, tool_counts: &HashMap<String, usize>) -> Vec<GroupInfo> {
713 let registry = self.registry.read().unwrap_or_else(|err| err.into_inner());
714 registry
715 .groups
716 .iter()
717 .map(|(name, state)| GroupInfo {
718 name: name.clone(),
719 description: state.description.clone(),
720 active: state.active,
721 parent: state.parent.clone(),
722 tool_count: *tool_counts.get(name).unwrap_or(&0),
723 })
724 .collect()
725 }
726
727 fn plan_activation(&self, name: &str) -> Result<GroupChangePlan> {
729 let registry = self.registry.read().unwrap_or_else(|err| err.into_inner());
730 let snapshot = snapshot_from_registry(®istry);
731 let target = registry
732 .groups
733 .get(name)
734 .ok_or_else(|| Error::GroupNotFound(name.to_string()))?;
735 if let Some(parent) = &target.parent
736 && !is_group_active_snapshot(&snapshot, parent)
737 {
738 return Err(Error::GroupInactive {
739 group: name.to_string(),
740 parent: parent.clone(),
741 });
742 }
743 let mut to_deactivate = Vec::new();
744 let mut deactivation_set = HashSet::new();
745 for exclusion in ®istry.exclusions {
746 if exclusion.iter().any(|group| group == name) {
747 for other in exclusion {
748 if other != name && is_group_active_snapshot(&snapshot, other) {
749 for group in collect_descendants_including_self(®istry.groups, other) {
750 if deactivation_set.insert(group.clone()) {
751 to_deactivate.push(group);
752 }
753 }
754 }
755 }
756 }
757 }
758 let mut hooks = Vec::new();
759 for group in &to_deactivate {
760 if let Some(state) = registry.groups.get(group)
761 && state.active
762 && let Some(hook) = &state.on_deactivate
763 {
764 hooks.push(hook.clone());
765 }
766 }
767 if !target.active
768 && let Some(hook) = &target.on_activate
769 {
770 hooks.push(hook.clone());
771 }
772 let changed = !to_deactivate.is_empty() || !target.active;
773 Ok(GroupChangePlan {
774 target: name.to_string(),
775 deactivate: to_deactivate,
776 activate: !target.active,
777 hooks,
778 changed,
779 })
780 }
781
782 fn apply_activation(&self, plan: GroupChangePlan) -> Result<bool> {
784 if !plan.changed {
785 return Ok(false);
786 }
787 let mut registry = self.registry.write().unwrap_or_else(|err| err.into_inner());
788 if !registry.groups.contains_key(&plan.target) {
789 return Err(Error::GroupNotFound(plan.target));
790 }
791 for group in plan.deactivate {
792 if let Some(state) = registry.groups.get_mut(&group) {
793 state.active = false;
794 }
795 }
796 if plan.activate
797 && let Some(state) = registry.groups.get_mut(&plan.target)
798 {
799 state.active = true;
800 }
801 Ok(true)
802 }
803
804 fn plan_deactivation(&self, name: &str) -> Result<GroupChangePlan> {
806 let registry = self.registry.read().unwrap_or_else(|err| err.into_inner());
807 let target = registry
808 .groups
809 .get(name)
810 .ok_or_else(|| Error::GroupNotFound(name.to_string()))?;
811 let mut to_deactivate = Vec::new();
812 for group in collect_descendants_including_self(®istry.groups, name) {
813 to_deactivate.push(group);
814 }
815 let mut hooks = Vec::new();
816 for group in &to_deactivate {
817 if let Some(state) = registry.groups.get(group)
818 && state.active
819 && let Some(hook) = &state.on_deactivate
820 {
821 hooks.push(hook.clone());
822 }
823 }
824 let changed = target.active
825 || to_deactivate.iter().any(|group| {
826 registry
827 .groups
828 .get(group)
829 .map(|state| state.active)
830 .unwrap_or(false)
831 });
832 Ok(GroupChangePlan {
833 target: name.to_string(),
834 deactivate: to_deactivate,
835 activate: false,
836 hooks,
837 changed,
838 })
839 }
840
841 fn apply_deactivation(&self, plan: GroupChangePlan) -> Result<bool> {
843 if !plan.changed {
844 return Ok(false);
845 }
846 let mut registry = self.registry.write().unwrap_or_else(|err| err.into_inner());
847 if !registry.groups.contains_key(&plan.target) {
848 return Err(Error::GroupNotFound(plan.target));
849 }
850 for group in plan.deactivate {
851 if let Some(state) = registry.groups.get_mut(&group) {
852 state.active = false;
853 }
854 }
855 Ok(true)
856 }
857}
858
859#[derive(Default)]
861struct GroupRegistry {
862 groups: HashMap<String, GroupState>,
864 exclusions: Vec<Vec<String>>,
866}
867
868struct GroupState {
870 description: String,
872 active: bool,
874 parent: Option<String>,
876 on_activate: Option<SharedActivationHook>,
878 on_deactivate: Option<SharedActivationHook>,
880}
881
882struct GroupChangePlan {
884 target: String,
886 deactivate: Vec<String>,
888 activate: bool,
890 hooks: Vec<SharedActivationHook>,
892 changed: bool,
894}
895
896async fn run_activation_hooks(hooks: &[SharedActivationHook], ctx: &ServerCtx) -> Result<()> {
898 for hook in hooks {
899 hook(ctx).await?;
900 }
901 Ok(())
902}
903
904fn validate_group_segment(segment: &str) -> Result<()> {
906 if segment.is_empty() || segment.contains('.') {
907 return Err(Error::InvalidConfiguration(format!(
908 "invalid group segment: {segment}"
909 )));
910 }
911 Ok(())
912}
913
914fn validate_group_path(group: &str) -> Result<()> {
916 if group.is_empty() {
917 return Err(Error::InvalidConfiguration(
918 "group name is empty".to_string(),
919 ));
920 }
921 for segment in group.split('.') {
922 validate_group_segment(segment)?;
923 }
924 Ok(())
925}
926
927fn split_tool_name(name: &str) -> Result<(Option<&str>, &str)> {
929 let mut parts = name.rsplitn(2, '.');
930 let base = parts
931 .next()
932 .ok_or_else(|| Error::InvalidConfiguration("tool name is empty".to_string()))?;
933 if base.is_empty() {
934 return Err(Error::InvalidConfiguration(
935 "tool name is empty".to_string(),
936 ));
937 }
938 let group = parts.next();
939 if let Some(group) = group {
940 validate_group_path(group)?;
941 Ok((Some(group), base))
942 } else {
943 Ok((None, base))
944 }
945}
946
947fn parse_cursor_offset(cursor: Cursor) -> Result<usize> {
949 let Cursor(value) = cursor;
950 value
951 .parse::<usize>()
952 .map_err(|_| Error::InvalidParams("invalid cursor".to_string()))
953}
954
955fn is_group_active_snapshot(groups: &HashMap<String, GroupSnapshot>, name: &str) -> bool {
957 let mut current = match groups.get(name) {
958 Some(state) => state,
959 None => return false,
960 };
961 if !current.active {
962 return false;
963 }
964 let mut guard = HashSet::new();
965 while let Some(parent) = current.parent.as_deref() {
966 if !guard.insert(parent) {
967 return false;
968 }
969 current = match groups.get(parent) {
970 Some(state) => state,
971 None => return false,
972 };
973 if !current.active {
974 return false;
975 }
976 }
977 true
978}
979
980fn snapshot_from_registry(registry: &GroupRegistry) -> HashMap<String, GroupSnapshot> {
982 registry
983 .groups
984 .iter()
985 .map(|(name, state)| {
986 (
987 name.clone(),
988 GroupSnapshot {
989 active: state.active,
990 parent: state.parent.clone(),
991 },
992 )
993 })
994 .collect()
995}
996
997fn collect_descendants_including_self(
999 groups: &HashMap<String, GroupState>,
1000 root: &str,
1001) -> Vec<String> {
1002 let mut collected = Vec::new();
1003 collect_descendants(groups, root, &mut collected);
1004 collected.push(root.to_string());
1005 collected
1006}
1007
1008fn collect_descendants(
1010 groups: &HashMap<String, GroupState>,
1011 root: &str,
1012 collected: &mut Vec<String>,
1013) {
1014 for (name, _) in groups
1015 .iter()
1016 .filter(|(_, state)| state.parent.as_deref() == Some(root))
1017 {
1018 collect_descendants(groups, name, collected);
1019 collected.push(name.clone());
1020 }
1021}
1022
1023fn activation_tool(name: &str, description: &str, activate: bool) -> Tool {
1025 let mut tool = Tool::new(name.to_string(), ToolSchema::empty());
1026 if !description.is_empty() {
1027 let label = if activate { "Activate" } else { "Deactivate" };
1028 tool = tool.with_description(format!("{label} {description}"));
1029 }
1030 tool
1031}