Skip to main content

tmcp/
toolset.rs

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
14/// Read-only view used by visibility predicates.
15pub struct ToolSetView<'a> {
16    /// Snapshot of group state keyed by group name.
17    groups: &'a HashMap<String, GroupSnapshot>,
18}
19
20/// Boxed future type returned by tool handlers.
21#[doc(hidden)]
22pub type ToolFuture<'a> = BoxFuture<'a, Result<CallToolResult>>;
23
24impl ToolSetView<'_> {
25    /// Check whether a group is active in the current snapshot.
26    pub fn is_group_active(&self, name: &str) -> bool {
27        is_group_active_snapshot(self.groups, name)
28    }
29}
30
31/// Controls when a tool appears in `list_tools`.
32#[derive(Clone)]
33pub enum Visibility {
34    /// Tool is always visible.
35    Always,
36    /// Tool is visible only when the named group is active.
37    Group(String),
38    /// Tool is visible when the predicate returns true.
39    When(Arc<dyn Fn(&ToolSetView) -> bool + Send + Sync>),
40}
41
42impl Visibility {
43    /// Evaluate visibility for the current snapshot.
44    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
53/// Information about a tool group for introspection.
54pub struct GroupInfo {
55    /// Group identifier.
56    pub name: String,
57    /// Human-readable description of the group's purpose.
58    pub description: String,
59    /// Whether the group is currently active.
60    pub active: bool,
61    /// Parent group name, if this is a child group.
62    pub parent: Option<String>,
63    /// Number of tools registered to this group.
64    pub tool_count: usize,
65}
66
67/// Configuration for group registration, hooks, and deactivator visibility.
68pub struct GroupConfig {
69    /// Group identifier.
70    ///
71    /// When a parent is provided, the name must be fully qualified under that parent.
72    pub name: String,
73    /// Human-readable description for UI/discovery.
74    pub description: String,
75    /// Optional parent group name to form a hierarchy.
76    pub parent: Option<String>,
77    /// Optional setup hook invoked before activation.
78    pub on_activate: Option<ActivationHook>,
79    /// Optional teardown hook invoked before deactivation.
80    pub on_deactivate: Option<ActivationHook>,
81    /// Whether to expose the `group.deactivate` tool.
82    pub show_deactivator: bool,
83}
84
85/// Hook invoked during activation/deactivation.
86pub type ActivationHook = Box<dyn Fn(&ServerCtx) -> BoxFuture<'static, Result<()>> + Send + Sync>;
87
88/// Trait implemented by group structs (usually via `#[derive(Group)]`).
89pub trait Group: GroupDispatch {
90    /// Group configuration for this node.
91    ///
92    /// For derive-based groups, this should return the local path segment; registration
93    /// will qualify it with parent groups.
94    fn config(&self) -> GroupConfig;
95
96    /// Register tools and child groups for this node.
97    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/// Internal dispatch and tool registration hooks for groups.
106#[doc(hidden)]
107pub trait GroupDispatch {
108    /// Register tools and child groups for this node.
109    fn register_tools(&self, toolset: &ToolSet, group_name: &str) -> Result<()>;
110
111    /// Dispatch a tool call relative to this group.
112    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/// Internal helper for overriding group path segments during registration.
121#[doc(hidden)]
122pub trait GroupRegistration: Group + GroupDispatch {
123    /// Register a group, overriding the path segment if provided.
124    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/// Central registry and dispatcher.
159#[derive(Clone)]
160pub struct ToolSet {
161    /// Registered tools and handlers.
162    tools: Arc<RwLock<HashMap<String, ToolEntry>>>,
163    /// Tool group state and configuration.
164    groups: ToolGroups,
165    /// Ensures macro-generated registration runs once per toolset instance.
166    registration: Arc<OnceLock<()>>,
167    /// Serialize activation and deactivation to preserve group invariants.
168    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    /// Register a group definition.
184    ///
185    /// This registers the group configuration and auto activation tools.
186    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    /// Register a mutual exclusion set.
198    pub fn register_exclusion(&self, groups: &[&str]) -> Result<()> {
199        self.groups.register_exclusion(groups)
200    }
201
202    /// Construct a fully-qualified tool name from a group path and base name.
203    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    /// Activate a group and emit `notifications/tools/list_changed` if visibility changed.
212    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    /// Deactivate a group and emit `notifications/tools/list_changed` if visibility changed.
224    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    /// Check if a group is currently active.
236    pub fn is_group_active(&self, name: &str) -> bool {
237        self.groups.is_active(name)
238    }
239
240    /// List all groups with their current state.
241    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    /// Register a tool into the root group (always visible).
249    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    /// Register a tool with explicit visibility.
257    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    /// Remove a tool by name. Returns the tool definition if it existed.
272    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    /// Emit `notifications/tools/list_changed` explicitly.
281    pub fn notify_list_changed(&self, ctx: &ServerCtx) -> Result<()> {
282        ctx.notify(ServerNotification::tool_list_changed())
283    }
284
285    /// List currently visible tools.
286    ///
287    /// Tools are returned in deterministic name-sorted order (ascending); cursors
288    /// are interpreted as offsets into that ordering.
289    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    /// Call a tool by name.
317    ///
318    /// Returns `ToolNotFound` if the tool doesn't exist or is not currently visible.
319    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    /// Call a tool, delegating to a handler method on the server.
334    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    /// Register tool metadata without a handler (macro-only).
355    #[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    /// Ensure macro-generated registration runs once per ToolSet instance.
361    #[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    /// Check visibility by name using the current snapshot.
372    #[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    /// Dispatch dynamic tools registered with handlers (macro fallback).
383    #[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    /// Return the group name for a registered tool, if any.
394    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    /// Count tools per group name.
402    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    /// Insert a tool entry into the registry.
414    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    /// Validate tool names against visibility and group registration.
445    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    /// Retrieve a tool entry clone by name.
466    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    /// Retrieve a cloned handler for a tool, if present.
475    fn tool_handler(&self, name: &str) -> Option<ToolHandler> {
476        self.tool_entry(name).and_then(|entry| entry.handler)
477    }
478
479    /// Register auto-generated activation and deactivation tools.
480    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
547/// Handler function for a tool.
548type ToolHandler =
549    Arc<dyn for<'a> Fn(&'a ServerCtx, Option<Arguments>) -> ToolFuture<'a> + Send + Sync>;
550
551/// Shared activation hook used in group state.
552type SharedActivationHook = Arc<dyn Fn(&ServerCtx) -> BoxFuture<'static, Result<()>> + Send + Sync>;
553
554/// A registered tool with its metadata, visibility rule, and handler.
555#[derive(Clone)]
556struct ToolEntry {
557    /// Tool definition used for listing.
558    tool: Tool,
559    /// Visibility rule for the tool.
560    visibility: Visibility,
561    /// Optional handler for dynamic tools.
562    handler: Option<ToolHandler>,
563    /// Origin tag for conflict resolution.
564    origin: ToolOrigin,
565}
566
567/// Origin metadata for tool registrations.
568#[derive(Clone, Copy, Debug, PartialEq, Eq)]
569enum ToolOrigin {
570    /// Tool generated automatically by a group definition.
571    AutoGroup,
572    /// Tool registered explicitly by the user or macro.
573    Explicit,
574}
575
576/// Auto-generated activation tool settings derived from a group configuration.
577struct AutoGroupConfig {
578    /// Fully-qualified group name.
579    name: String,
580    /// Group description used in auto tool metadata.
581    description: String,
582    /// Whether to expose the auto deactivator tool.
583    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/// Snapshot of group state used for visibility evaluation.
597#[derive(Clone)]
598struct GroupSnapshot {
599    /// Whether the group is active.
600    active: bool,
601    /// Optional parent group name.
602    parent: Option<String>,
603}
604
605/// Shared container for group registry state.
606#[derive(Clone, Default)]
607struct ToolGroups {
608    /// Registry state guarded by a lock.
609    registry: Arc<RwLock<GroupRegistry>>,
610}
611
612impl ToolGroups {
613    /// Register a group configuration.
614    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    /// Remove a group configuration, if present.
645    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    /// Register a mutual exclusion set.
654    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    /// Ensure a group exists.
678    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    /// Check whether a group is active.
688    fn is_active(&self, name: &str) -> bool {
689        let snapshot = self.snapshot();
690        is_group_active_snapshot(&snapshot, name)
691    }
692
693    /// Snapshot group state for visibility evaluation.
694    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    /// List group info using provided tool counts.
712    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    /// Build activation plan for a group.
728    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(&registry);
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 &registry.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(&registry.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    /// Apply activation changes to group state.
783    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    /// Build deactivation plan for a group.
805    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(&registry.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    /// Apply deactivation changes to group state.
842    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/// Group registry state shared across the tool set.
860#[derive(Default)]
861struct GroupRegistry {
862    /// Group states keyed by name.
863    groups: HashMap<String, GroupState>,
864    /// Mutually exclusive group sets.
865    exclusions: Vec<Vec<String>>,
866}
867
868/// Internal group state tracked by the tool set.
869struct GroupState {
870    /// Description of the group.
871    description: String,
872    /// Whether the group is currently active.
873    active: bool,
874    /// Optional parent group name.
875    parent: Option<String>,
876    /// Optional activation hook.
877    on_activate: Option<SharedActivationHook>,
878    /// Optional deactivation hook.
879    on_deactivate: Option<SharedActivationHook>,
880}
881
882/// Planning data for a group activation or deactivation.
883struct GroupChangePlan {
884    /// Target group for the change.
885    target: String,
886    /// Groups to deactivate.
887    deactivate: Vec<String>,
888    /// Whether to activate the target group.
889    activate: bool,
890    /// Hooks to run before applying the change.
891    hooks: Vec<SharedActivationHook>,
892    /// Whether any state will change.
893    changed: bool,
894}
895
896/// Run activation or deactivation hooks in order.
897async fn run_activation_hooks(hooks: &[SharedActivationHook], ctx: &ServerCtx) -> Result<()> {
898    for hook in hooks {
899        hook(ctx).await?;
900    }
901    Ok(())
902}
903
904/// Validate an individual group name segment.
905fn 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
914/// Validate a group path containing one or more segments.
915fn 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
927/// Split a tool name into group path (if any) and base name.
928fn 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
947/// Parse a cursor into a numeric offset.
948fn 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
955/// Determine if a group is active within a snapshot map.
956fn 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
980/// Build a snapshot map from a registry reference.
981fn 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
997/// Collect descendants of a group in child-first order.
998fn 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
1008/// Recursively collect descendants for a group.
1009fn 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
1023/// Build an auto activation or deactivation tool.
1024fn 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}