mcp_host/server/
profile.rs

1//! Session profiles for role-based capability configuration
2//!
3//! Profiles allow pre-defining sets of tools, resources, and prompts
4//! that can be applied to sessions based on user roles (admin, vip, guest, etc.)
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! // Create an admin profile with extra tools
10//! let mut admin = SessionProfile::new("admin");
11//! admin.add_tool(Arc::new(DeleteDatabaseTool));
12//! admin.add_tool(Arc::new(SystemRebootTool));
13//!
14//! // Create a VIP profile with upgraded implementations
15//! let mut vip = SessionProfile::new("vip");
16//! vip.override_tool("export", Arc::new(UnlimitedExportTool));
17//!
18//! // Create a guest profile with restricted access
19//! let mut guest = SessionProfile::new("guest");
20//! guest.hide_tool("delete");
21//! guest.hide_tool("admin_panel");
22//!
23//! // Apply to session
24//! session.apply_profile(&admin);
25//! ```
26
27use std::collections::{HashMap, HashSet};
28use std::sync::Arc;
29
30use crate::registry::prompts::Prompt;
31use crate::registry::resources::Resource;
32use crate::registry::tools::Tool;
33
34/// Session profile for role-based capability configuration
35///
36/// A profile defines a set of modifications to apply to a session:
37/// - Extra tools/resources/prompts to add
38/// - Overrides for existing tools (same name, different implementation)
39/// - Tools/resources/prompts to hide
40/// - Aliases for ergonomic name mappings
41#[derive(Default)]
42pub struct SessionProfile {
43    /// Profile name (e.g., "admin", "vip", "guest")
44    pub name: String,
45
46    // Tool configuration
47    /// Extra tools to add to the session
48    pub tool_extras: Vec<Arc<dyn Tool>>,
49    /// Tool overrides (same name, different implementation)
50    pub tool_overrides: HashMap<String, Arc<dyn Tool>>,
51    /// Tools to hide from the session
52    pub tool_hidden: HashSet<String>,
53    /// Tool aliases (alias -> actual name)
54    pub tool_aliases: HashMap<String, String>,
55
56    // Resource configuration
57    /// Extra resources to add to the session
58    pub resource_extras: Vec<Arc<dyn Resource>>,
59    /// Resource overrides (same URI, different implementation)
60    pub resource_overrides: HashMap<String, Arc<dyn Resource>>,
61    /// Resources to hide from the session
62    pub resource_hidden: HashSet<String>,
63
64    // Prompt configuration
65    /// Extra prompts to add to the session
66    pub prompt_extras: Vec<Arc<dyn Prompt>>,
67    /// Prompt overrides (same name, different implementation)
68    pub prompt_overrides: HashMap<String, Arc<dyn Prompt>>,
69    /// Prompts to hide from the session
70    pub prompt_hidden: HashSet<String>,
71}
72
73impl SessionProfile {
74    /// Create a new empty profile with the given name
75    pub fn new(name: impl Into<String>) -> Self {
76        Self {
77            name: name.into(),
78            ..Default::default()
79        }
80    }
81
82    /// Create an admin profile (typically has extra management tools)
83    pub fn admin() -> Self {
84        Self::new("admin")
85    }
86
87    /// Create a VIP profile (typically has upgraded implementations)
88    pub fn vip() -> Self {
89        Self::new("vip")
90    }
91
92    /// Create a readonly profile (hides write operations)
93    pub fn readonly() -> Self {
94        Self::new("readonly")
95    }
96
97    /// Create a guest profile (minimal access)
98    pub fn guest() -> Self {
99        Self::new("guest")
100    }
101
102    // Tool methods
103
104    /// Add an extra tool to the profile
105    pub fn add_tool(&mut self, tool: Arc<dyn Tool>) -> &mut Self {
106        self.tool_extras.push(tool);
107        self
108    }
109
110    /// Override a tool (same name, different implementation)
111    pub fn override_tool(&mut self, name: impl Into<String>, tool: Arc<dyn Tool>) -> &mut Self {
112        self.tool_overrides.insert(name.into(), tool);
113        self
114    }
115
116    /// Hide a tool from sessions with this profile
117    pub fn hide_tool(&mut self, name: impl Into<String>) -> &mut Self {
118        self.tool_hidden.insert(name.into());
119        self
120    }
121
122    /// Hide multiple tools
123    pub fn hide_tools(&mut self, names: impl IntoIterator<Item = impl Into<String>>) -> &mut Self {
124        for name in names {
125            self.tool_hidden.insert(name.into());
126        }
127        self
128    }
129
130    /// Add a tool alias (alias -> target)
131    pub fn alias_tool(&mut self, alias: impl Into<String>, target: impl Into<String>) -> &mut Self {
132        self.tool_aliases.insert(alias.into(), target.into());
133        self
134    }
135
136    // Resource methods
137
138    /// Add an extra resource to the profile
139    pub fn add_resource(&mut self, resource: Arc<dyn Resource>) -> &mut Self {
140        self.resource_extras.push(resource);
141        self
142    }
143
144    /// Override a resource (same URI, different implementation)
145    pub fn override_resource(
146        &mut self,
147        uri: impl Into<String>,
148        resource: Arc<dyn Resource>,
149    ) -> &mut Self {
150        self.resource_overrides.insert(uri.into(), resource);
151        self
152    }
153
154    /// Hide a resource from sessions with this profile
155    pub fn hide_resource(&mut self, uri: impl Into<String>) -> &mut Self {
156        self.resource_hidden.insert(uri.into());
157        self
158    }
159
160    /// Hide multiple resources
161    pub fn hide_resources(
162        &mut self,
163        uris: impl IntoIterator<Item = impl Into<String>>,
164    ) -> &mut Self {
165        for uri in uris {
166            self.resource_hidden.insert(uri.into());
167        }
168        self
169    }
170
171    // Prompt methods
172
173    /// Add an extra prompt to the profile
174    pub fn add_prompt(&mut self, prompt: Arc<dyn Prompt>) -> &mut Self {
175        self.prompt_extras.push(prompt);
176        self
177    }
178
179    /// Override a prompt (same name, different implementation)
180    pub fn override_prompt(
181        &mut self,
182        name: impl Into<String>,
183        prompt: Arc<dyn Prompt>,
184    ) -> &mut Self {
185        self.prompt_overrides.insert(name.into(), prompt);
186        self
187    }
188
189    /// Hide a prompt from sessions with this profile
190    pub fn hide_prompt(&mut self, name: impl Into<String>) -> &mut Self {
191        self.prompt_hidden.insert(name.into());
192        self
193    }
194
195    /// Hide multiple prompts
196    pub fn hide_prompts(
197        &mut self,
198        names: impl IntoIterator<Item = impl Into<String>>,
199    ) -> &mut Self {
200        for name in names {
201            self.prompt_hidden.insert(name.into());
202        }
203        self
204    }
205
206    /// Merge another profile into this one
207    ///
208    /// Later profiles take precedence for overrides and hidden items
209    pub fn merge(&mut self, other: &SessionProfile) -> &mut Self {
210        // Tools
211        self.tool_extras.extend(other.tool_extras.iter().cloned());
212        self.tool_overrides.extend(
213            other
214                .tool_overrides
215                .iter()
216                .map(|(k, v)| (k.clone(), v.clone())),
217        );
218        self.tool_hidden.extend(other.tool_hidden.iter().cloned());
219        self.tool_aliases.extend(
220            other
221                .tool_aliases
222                .iter()
223                .map(|(k, v)| (k.clone(), v.clone())),
224        );
225
226        // Resources
227        self.resource_extras
228            .extend(other.resource_extras.iter().cloned());
229        self.resource_overrides.extend(
230            other
231                .resource_overrides
232                .iter()
233                .map(|(k, v)| (k.clone(), v.clone())),
234        );
235        self.resource_hidden
236            .extend(other.resource_hidden.iter().cloned());
237
238        // Prompts
239        self.prompt_extras
240            .extend(other.prompt_extras.iter().cloned());
241        self.prompt_overrides.extend(
242            other
243                .prompt_overrides
244                .iter()
245                .map(|(k, v)| (k.clone(), v.clone())),
246        );
247        self.prompt_hidden
248            .extend(other.prompt_hidden.iter().cloned());
249
250        self
251    }
252}
253
254impl std::fmt::Debug for SessionProfile {
255    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256        f.debug_struct("SessionProfile")
257            .field("name", &self.name)
258            .field("tool_extras_count", &self.tool_extras.len())
259            .field(
260                "tool_overrides",
261                &self.tool_overrides.keys().collect::<Vec<_>>(),
262            )
263            .field("tool_hidden", &self.tool_hidden)
264            .field("tool_aliases", &self.tool_aliases)
265            .field("resource_extras_count", &self.resource_extras.len())
266            .field(
267                "resource_overrides",
268                &self.resource_overrides.keys().collect::<Vec<_>>(),
269            )
270            .field("resource_hidden", &self.resource_hidden)
271            .field("prompt_extras_count", &self.prompt_extras.len())
272            .field(
273                "prompt_overrides",
274                &self.prompt_overrides.keys().collect::<Vec<_>>(),
275            )
276            .field("prompt_hidden", &self.prompt_hidden)
277            .finish()
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[test]
286    fn test_profile_creation() {
287        let profile = SessionProfile::new("test");
288        assert_eq!(profile.name, "test");
289        assert!(profile.tool_extras.is_empty());
290        assert!(profile.tool_overrides.is_empty());
291        assert!(profile.tool_hidden.is_empty());
292    }
293
294    #[test]
295    fn test_profile_presets() {
296        let admin = SessionProfile::admin();
297        assert_eq!(admin.name, "admin");
298
299        let vip = SessionProfile::vip();
300        assert_eq!(vip.name, "vip");
301
302        let readonly = SessionProfile::readonly();
303        assert_eq!(readonly.name, "readonly");
304
305        let guest = SessionProfile::guest();
306        assert_eq!(guest.name, "guest");
307    }
308
309    #[test]
310    fn test_hide_tools() {
311        let mut profile = SessionProfile::new("test");
312        profile.hide_tool("delete");
313        profile.hide_tools(["admin_panel", "system_reboot"]);
314
315        assert!(profile.tool_hidden.contains("delete"));
316        assert!(profile.tool_hidden.contains("admin_panel"));
317        assert!(profile.tool_hidden.contains("system_reboot"));
318        assert_eq!(profile.tool_hidden.len(), 3);
319    }
320
321    #[test]
322    fn test_tool_aliases() {
323        let mut profile = SessionProfile::new("test");
324        profile.alias_tool("save", "git_commit");
325        profile.alias_tool("deploy", "kubernetes_apply");
326
327        assert_eq!(
328            profile.tool_aliases.get("save"),
329            Some(&"git_commit".to_string())
330        );
331        assert_eq!(
332            profile.tool_aliases.get("deploy"),
333            Some(&"kubernetes_apply".to_string())
334        );
335    }
336
337    #[test]
338    fn test_profile_merge() {
339        let mut base = SessionProfile::new("base");
340        base.hide_tool("tool1");
341        base.alias_tool("a", "b");
342
343        let mut extra = SessionProfile::new("extra");
344        extra.hide_tool("tool2");
345        extra.alias_tool("c", "d");
346
347        base.merge(&extra);
348
349        assert!(base.tool_hidden.contains("tool1"));
350        assert!(base.tool_hidden.contains("tool2"));
351        assert_eq!(base.tool_aliases.get("a"), Some(&"b".to_string()));
352        assert_eq!(base.tool_aliases.get("c"), Some(&"d".to_string()));
353    }
354
355    #[test]
356    fn test_builder_pattern() {
357        let mut profile = SessionProfile::new("test");
358        profile
359            .hide_tool("dangerous")
360            .alias_tool("save", "commit")
361            .hide_resource("secret://data");
362
363        assert!(profile.tool_hidden.contains("dangerous"));
364        assert_eq!(
365            profile.tool_aliases.get("save"),
366            Some(&"commit".to_string())
367        );
368        assert!(profile.resource_hidden.contains("secret://data"));
369    }
370}