Skip to main content

fastmcp_console/
handlers.rs

1//! Unified handler registry renderer.
2//!
3//! Provides a comprehensive display of all server capabilities (tools, resources, prompts)
4//! in a well-organized format, combining the individual table renderers.
5
6use fastmcp_protocol::{Prompt, Resource, Tool};
7
8use crate::console::FastMcpConsole;
9use crate::detection::DisplayContext;
10use crate::tables::{PromptTableRenderer, ResourceTableRenderer, ToolTableRenderer};
11use crate::theme::FastMcpTheme;
12
13/// Server capabilities combining tools, resources, and prompts.
14///
15/// This struct mirrors what a server exposes, making it easy to display
16/// everything at once via the `HandlerRegistryRenderer`.
17#[derive(Debug, Clone, Default)]
18pub struct ServerCapabilities {
19    /// Registered tools that can be called
20    pub tools: Vec<Tool>,
21    /// Registered resources that can be read
22    pub resources: Vec<Resource>,
23    /// Registered prompts that can be requested
24    pub prompts: Vec<Prompt>,
25}
26
27impl ServerCapabilities {
28    /// Create empty capabilities.
29    #[must_use]
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    /// Create capabilities with the given collections.
35    #[must_use]
36    pub fn with(tools: Vec<Tool>, resources: Vec<Resource>, prompts: Vec<Prompt>) -> Self {
37        Self {
38            tools,
39            resources,
40            prompts,
41        }
42    }
43
44    /// Total number of all capabilities.
45    #[must_use]
46    pub fn total(&self) -> usize {
47        self.tools.len() + self.resources.len() + self.prompts.len()
48    }
49
50    /// Check if there are any capabilities.
51    #[must_use]
52    pub fn is_empty(&self) -> bool {
53        self.tools.is_empty() && self.resources.is_empty() && self.prompts.is_empty()
54    }
55}
56
57/// Unified renderer for displaying all server capabilities.
58///
59/// Combines tools, resources, and prompts into a cohesive display with
60/// proper sections, headers, footers, and summaries.
61///
62/// # Example
63///
64/// ```rust,ignore
65/// use fastmcp_console::handlers::{HandlerRegistryRenderer, ServerCapabilities};
66/// use fastmcp_console::detection::DisplayContext;
67///
68/// let renderer = HandlerRegistryRenderer::new(DisplayContext::detect());
69/// let capabilities = ServerCapabilities::new();
70/// renderer.render(&capabilities, console);
71/// ```
72#[derive(Debug, Clone)]
73pub struct HandlerRegistryRenderer {
74    theme: &'static FastMcpTheme,
75    context: DisplayContext,
76    tool_renderer: ToolTableRenderer,
77    resource_renderer: ResourceTableRenderer,
78    prompt_renderer: PromptTableRenderer,
79}
80
81impl HandlerRegistryRenderer {
82    /// Create a new renderer with explicit display context.
83    #[must_use]
84    pub fn new(context: DisplayContext) -> Self {
85        Self {
86            theme: crate::theme::theme(),
87            context: context.clone(),
88            tool_renderer: ToolTableRenderer::new(context.clone()),
89            resource_renderer: ResourceTableRenderer::new(context.clone()),
90            prompt_renderer: PromptTableRenderer::new(context),
91        }
92    }
93
94    /// Create a renderer using auto-detected display context.
95    #[must_use]
96    pub fn detect() -> Self {
97        Self::new(DisplayContext::detect())
98    }
99
100    /// Render complete server capabilities.
101    ///
102    /// Shows all tools, resources, and prompts with section headers and a summary footer.
103    pub fn render(&self, capabilities: &ServerCapabilities, console: &FastMcpConsole) {
104        if capabilities.is_empty() {
105            self.render_empty(console);
106            return;
107        }
108
109        if !self.should_use_rich(console) {
110            self.render_plain(capabilities, console);
111            return;
112        }
113
114        // Header
115        self.render_header(capabilities, console);
116
117        // Tools section
118        if !capabilities.tools.is_empty() {
119            console.print("");
120            self.tool_renderer.render(&capabilities.tools, console);
121        }
122
123        // Resources section
124        if !capabilities.resources.is_empty() {
125            console.print("");
126            self.resource_renderer
127                .render(&capabilities.resources, console);
128        }
129
130        // Prompts section
131        if !capabilities.prompts.is_empty() {
132            console.print("");
133            self.prompt_renderer.render(&capabilities.prompts, console);
134        }
135
136        // Footer
137        self.render_footer(capabilities, console);
138    }
139
140    /// Render a compact summary of capabilities.
141    ///
142    /// Shows counts and status without full tables.
143    pub fn render_summary(&self, capabilities: &ServerCapabilities, console: &FastMcpConsole) {
144        if !self.should_use_rich(console) {
145            self.render_summary_plain(capabilities, console);
146            return;
147        }
148
149        let title_color = self
150            .theme
151            .primary
152            .triplet
153            .map(|t| t.hex())
154            .unwrap_or_else(|| "cyan".to_string());
155
156        console.print(&format!(
157            "\n[{}][bold]Server Capabilities[/][/]",
158            title_color
159        ));
160
161        let dim = self
162            .theme
163            .text_dim
164            .triplet
165            .map(|t| t.hex())
166            .unwrap_or_else(|| "white".to_string());
167
168        // Tools
169        let tools_status = if capabilities.tools.is_empty() {
170            format!("[{}]none[/]", dim)
171        } else {
172            format!("[green]{}[/]", capabilities.tools.len())
173        };
174        console.print(&format!("  Tools:     {}", tools_status));
175
176        // Resources
177        let resources_status = if capabilities.resources.is_empty() {
178            format!("[{}]none[/]", dim)
179        } else {
180            format!("[green]{}[/]", capabilities.resources.len())
181        };
182        console.print(&format!("  Resources: {}", resources_status));
183
184        // Prompts
185        let prompts_status = if capabilities.prompts.is_empty() {
186            format!("[{}]none[/]", dim)
187        } else {
188            format!("[green]{}[/]", capabilities.prompts.len())
189        };
190        console.print(&format!("  Prompts:   {}", prompts_status));
191
192        console.print(&format!(
193            "\n[{}]Total: {} capabilities[/]",
194            dim,
195            capabilities.total()
196        ));
197    }
198
199    /// Render only if there are capabilities.
200    ///
201    /// Returns true if anything was rendered.
202    pub fn render_if_present(
203        &self,
204        capabilities: &ServerCapabilities,
205        console: &FastMcpConsole,
206    ) -> bool {
207        if capabilities.is_empty() {
208            return false;
209        }
210        self.render(capabilities, console);
211        true
212    }
213
214    fn should_use_rich(&self, console: &FastMcpConsole) -> bool {
215        self.context.is_human() && console.is_rich()
216    }
217
218    fn render_empty(&self, console: &FastMcpConsole) {
219        if self.should_use_rich(console) {
220            let dim = self
221                .theme
222                .text_dim
223                .triplet
224                .map(|t| t.hex())
225                .unwrap_or_else(|| "white".to_string());
226            console.print(&format!("[{}]No capabilities registered[/]", dim));
227        } else {
228            console.print("No capabilities registered");
229        }
230    }
231
232    fn render_header(&self, capabilities: &ServerCapabilities, console: &FastMcpConsole) {
233        let primary_color = self
234            .theme
235            .primary
236            .triplet
237            .map(|t| t.hex())
238            .unwrap_or_else(|| "cyan".to_string());
239
240        console.print(&format!(
241            "\n[{}][bold]Server Capabilities ({} total)[/][/]",
242            primary_color,
243            capabilities.total()
244        ));
245    }
246
247    fn render_footer(&self, capabilities: &ServerCapabilities, console: &FastMcpConsole) {
248        let tools = capabilities.tools.len();
249        let resources = capabilities.resources.len();
250        let prompts = capabilities.prompts.len();
251
252        let dim = self
253            .theme
254            .text_dim
255            .triplet
256            .map(|t| t.hex())
257            .unwrap_or_else(|| "white".to_string());
258
259        console.print(&format!(
260            "\n[{}]Summary: {} tool{}, {} resource{}, {} prompt{}[/]",
261            dim,
262            tools,
263            if tools == 1 { "" } else { "s" },
264            resources,
265            if resources == 1 { "" } else { "s" },
266            prompts,
267            if prompts == 1 { "" } else { "s" }
268        ));
269    }
270
271    fn render_plain(&self, capabilities: &ServerCapabilities, console: &FastMcpConsole) {
272        console.print(&format!(
273            "=== Server Capabilities ({} total) ===",
274            capabilities.total()
275        ));
276
277        if !capabilities.tools.is_empty() {
278            console.print("");
279            self.tool_renderer.render(&capabilities.tools, console);
280        }
281
282        if !capabilities.resources.is_empty() {
283            console.print("");
284            self.resource_renderer
285                .render(&capabilities.resources, console);
286        }
287
288        if !capabilities.prompts.is_empty() {
289            console.print("");
290            self.prompt_renderer.render(&capabilities.prompts, console);
291        }
292
293        self.render_summary_footer_plain(capabilities, console);
294    }
295
296    fn render_summary_plain(&self, capabilities: &ServerCapabilities, console: &FastMcpConsole) {
297        console.print("Server Capabilities:");
298        console.print(&format!("  Tools:     {}", capabilities.tools.len()));
299        console.print(&format!("  Resources: {}", capabilities.resources.len()));
300        console.print(&format!("  Prompts:   {}", capabilities.prompts.len()));
301        console.print(&format!("  Total:     {}", capabilities.total()));
302    }
303
304    fn render_summary_footer_plain(
305        &self,
306        capabilities: &ServerCapabilities,
307        console: &FastMcpConsole,
308    ) {
309        let tools = capabilities.tools.len();
310        let resources = capabilities.resources.len();
311        let prompts = capabilities.prompts.len();
312
313        console.print(&format!(
314            "\nSummary: {} tool{}, {} resource{}, {} prompt{}",
315            tools,
316            if tools == 1 { "" } else { "s" },
317            resources,
318            if resources == 1 { "" } else { "s" },
319            prompts,
320            if prompts == 1 { "" } else { "s" }
321        ));
322    }
323}
324
325impl Default for HandlerRegistryRenderer {
326    fn default() -> Self {
327        Self::detect()
328    }
329}
330
331// ============================================================================
332// Tests
333// ============================================================================
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338    use crate::testing::TestConsole;
339    use serde_json::json;
340
341    fn sample_tools() -> Vec<Tool> {
342        vec![
343            Tool {
344                name: "calculate".to_string(),
345                description: Some("Perform calculations".to_string()),
346                input_schema: json!({"type": "object", "properties": {}}),
347                output_schema: None,
348                icon: None,
349                version: None,
350                tags: vec![],
351                annotations: None,
352            },
353            Tool {
354                name: "search".to_string(),
355                description: Some("Search for items".to_string()),
356                input_schema: json!({"type": "object", "properties": {}}),
357                output_schema: None,
358                icon: None,
359                version: None,
360                tags: vec![],
361                annotations: None,
362            },
363        ]
364    }
365
366    fn sample_resources() -> Vec<Resource> {
367        vec![Resource {
368            uri: "file://example.txt".to_string(),
369            name: "example.txt".to_string(),
370            description: Some("Example file".to_string()),
371            mime_type: Some("text/plain".to_string()),
372            icon: None,
373            version: None,
374            tags: vec![],
375        }]
376    }
377
378    fn sample_prompts() -> Vec<Prompt> {
379        vec![Prompt {
380            name: "code_review".to_string(),
381            description: Some("Review code changes".to_string()),
382            arguments: vec![],
383            icon: None,
384            version: None,
385            tags: vec![],
386        }]
387    }
388
389    #[test]
390    fn test_render_empty_capabilities() {
391        let tc = TestConsole::new();
392        let renderer = HandlerRegistryRenderer::new(DisplayContext::new_agent());
393        let caps = ServerCapabilities::new();
394
395        renderer.render(&caps, tc.console());
396
397        tc.assert_contains("No capabilities registered");
398    }
399
400    #[test]
401    fn test_render_tools_only() {
402        let tc = TestConsole::new();
403        let renderer = HandlerRegistryRenderer::new(DisplayContext::new_agent());
404        let caps = ServerCapabilities::with(sample_tools(), vec![], vec![]);
405
406        renderer.render(&caps, tc.console());
407
408        tc.assert_contains("Server Capabilities");
409        tc.assert_contains("2 total");
410        tc.assert_contains("calculate");
411        tc.assert_contains("search");
412        tc.assert_contains("2 tools");
413        tc.assert_contains("0 resources");
414        tc.assert_contains("0 prompts");
415    }
416
417    #[test]
418    fn test_render_all_capabilities() {
419        let tc = TestConsole::new();
420        let renderer = HandlerRegistryRenderer::new(DisplayContext::new_agent());
421        let caps = ServerCapabilities::with(sample_tools(), sample_resources(), sample_prompts());
422
423        renderer.render(&caps, tc.console());
424
425        tc.assert_contains("Server Capabilities");
426        tc.assert_contains("4 total");
427        // Tools
428        tc.assert_contains("calculate");
429        tc.assert_contains("search");
430        // Resources
431        tc.assert_contains("example.txt");
432        // Prompts
433        tc.assert_contains("code_review");
434        // Summary
435        tc.assert_contains("2 tools");
436        tc.assert_contains("1 resource");
437        tc.assert_contains("1 prompt");
438    }
439
440    #[test]
441    fn test_render_summary() {
442        let tc = TestConsole::new();
443        let renderer = HandlerRegistryRenderer::new(DisplayContext::new_agent());
444        let caps = ServerCapabilities::with(sample_tools(), sample_resources(), sample_prompts());
445
446        renderer.render_summary(&caps, tc.console());
447
448        tc.assert_contains("Server Capabilities");
449        tc.assert_contains("Tools:");
450        tc.assert_contains("Resources:");
451        tc.assert_contains("Prompts:");
452        tc.assert_contains("Total:");
453        tc.assert_contains("4");
454    }
455
456    #[test]
457    fn test_render_if_present_empty() {
458        let tc = TestConsole::new();
459        let renderer = HandlerRegistryRenderer::new(DisplayContext::new_agent());
460        let caps = ServerCapabilities::new();
461
462        let rendered = renderer.render_if_present(&caps, tc.console());
463
464        assert!(!rendered);
465        assert!(tc.output().is_empty());
466    }
467
468    #[test]
469    fn test_render_if_present_with_content() {
470        let tc = TestConsole::new();
471        let renderer = HandlerRegistryRenderer::new(DisplayContext::new_agent());
472        let caps = ServerCapabilities::with(sample_tools(), vec![], vec![]);
473
474        let rendered = renderer.render_if_present(&caps, tc.console());
475
476        assert!(rendered);
477        tc.assert_contains("calculate");
478    }
479
480    #[test]
481    fn test_server_capabilities_total() {
482        let caps = ServerCapabilities::with(sample_tools(), sample_resources(), sample_prompts());
483        assert_eq!(caps.total(), 4);
484    }
485
486    #[test]
487    fn test_server_capabilities_is_empty() {
488        let empty = ServerCapabilities::new();
489        assert!(empty.is_empty());
490
491        let non_empty = ServerCapabilities::with(sample_tools(), vec![], vec![]);
492        assert!(!non_empty.is_empty());
493    }
494
495    #[test]
496    fn test_render_resources_only_plain() {
497        let tc = TestConsole::new();
498        let renderer = HandlerRegistryRenderer::new(DisplayContext::new_agent());
499        let caps = ServerCapabilities::with(vec![], sample_resources(), vec![]);
500
501        renderer.render(&caps, tc.console());
502
503        tc.assert_contains("Server Capabilities");
504        tc.assert_contains("1 total");
505        tc.assert_contains("example.txt");
506        tc.assert_contains("0 tools");
507        tc.assert_contains("1 resource");
508        tc.assert_contains("0 prompts");
509    }
510
511    #[test]
512    fn test_render_prompts_only_plain() {
513        let tc = TestConsole::new();
514        let renderer = HandlerRegistryRenderer::new(DisplayContext::new_agent());
515        let caps = ServerCapabilities::with(vec![], vec![], sample_prompts());
516
517        renderer.render(&caps, tc.console());
518
519        tc.assert_contains("Server Capabilities");
520        tc.assert_contains("1 total");
521        tc.assert_contains("code_review");
522        tc.assert_contains("0 tools");
523        tc.assert_contains("0 resources");
524        tc.assert_contains("1 prompt");
525    }
526
527    #[test]
528    fn test_render_empty_capabilities_rich_context() {
529        let tc = TestConsole::new();
530        let renderer = HandlerRegistryRenderer::new(DisplayContext::new_human());
531        let caps = ServerCapabilities::new();
532
533        renderer.render(&caps, tc.console());
534
535        tc.assert_contains("No capabilities registered");
536    }
537
538    #[test]
539    fn test_render_summary_rich_with_none_and_counts() {
540        let tc = TestConsole::new();
541        let renderer = HandlerRegistryRenderer::new(DisplayContext::new_human());
542
543        let empty = ServerCapabilities::new();
544        renderer.render_summary(&empty, tc.console());
545        tc.assert_contains("Tools:");
546        tc.assert_contains("Resources:");
547        tc.assert_contains("Prompts:");
548        tc.assert_contains("none");
549        tc.assert_contains("Total: 0 capabilities");
550
551        tc.clear();
552
553        let caps = ServerCapabilities::with(sample_tools(), sample_resources(), sample_prompts());
554        renderer.render_summary(&caps, tc.console());
555        tc.assert_contains("Total: 4 capabilities");
556    }
557
558    #[test]
559    fn test_render_header_and_footer_pluralization_paths() {
560        let tc = TestConsole::new();
561        let renderer = HandlerRegistryRenderer::new(DisplayContext::new_human());
562
563        let single_each = ServerCapabilities::with(
564            vec![sample_tools()[0].clone()],
565            sample_resources(),
566            sample_prompts(),
567        );
568        renderer.render_header(&single_each, tc.console());
569        renderer.render_footer(&single_each, tc.console());
570        tc.assert_contains("Server Capabilities (3 total)");
571        tc.assert_contains("Summary: 1 tool, 1 resource, 1 prompt");
572
573        tc.clear();
574
575        let mixed = ServerCapabilities::with(sample_tools(), sample_resources(), vec![]);
576        renderer.render_footer(&mixed, tc.console());
577        tc.assert_contains("Summary: 2 tools, 1 resource, 0 prompts");
578    }
579
580    #[test]
581    fn test_render_summary_footer_plain_pluralization_paths() {
582        let tc = TestConsole::new();
583        let renderer = HandlerRegistryRenderer::new(DisplayContext::new_agent());
584
585        let single_each = ServerCapabilities::with(
586            vec![sample_tools()[0].clone()],
587            sample_resources(),
588            sample_prompts(),
589        );
590        renderer.render_summary_footer_plain(&single_each, tc.console());
591        tc.assert_contains("Summary: 1 tool, 1 resource, 1 prompt");
592
593        tc.clear();
594        let mixed = ServerCapabilities::with(sample_tools(), sample_resources(), vec![]);
595        renderer.render_summary_footer_plain(&mixed, tc.console());
596        tc.assert_contains("Summary: 2 tools, 1 resource, 0 prompts");
597    }
598
599    #[test]
600    fn test_should_use_rich_respects_context_and_console_mode() {
601        let human_renderer = HandlerRegistryRenderer::new(DisplayContext::new_human());
602        let agent_renderer = HandlerRegistryRenderer::new(DisplayContext::new_agent());
603        let rich_console = FastMcpConsole::with_enabled(true);
604        let plain_console = FastMcpConsole::with_enabled(false);
605
606        assert!(human_renderer.should_use_rich(&rich_console));
607        assert!(!human_renderer.should_use_rich(&plain_console));
608        assert!(!agent_renderer.should_use_rich(&rich_console));
609    }
610
611    #[test]
612    fn test_default_and_detect_construction() {
613        let _default = HandlerRegistryRenderer::default();
614        let _detect = HandlerRegistryRenderer::detect();
615    }
616}