Skip to main content

fastmcp_console/
tables.rs

1//! Server info table renderers
2//!
3//! Provides beautiful table displays for tools, resources, and prompts
4//! using rich_rust, with plain-text fallback for agent contexts.
5
6use std::collections::HashMap;
7
8use fastmcp_protocol::{Prompt, PromptArgument, Resource, Tool};
9use rich_rust::r#box::ROUNDED;
10use rich_rust::prelude::*;
11use rich_rust::text::OverflowMethod;
12use serde_json::Value;
13
14use crate::console::FastMcpConsole;
15use crate::detection::DisplayContext;
16use crate::theme::FastMcpTheme;
17
18// ============================================================================
19// Tool Table Renderer
20// ============================================================================
21
22/// Renders tool registry as beautiful tables.
23///
24/// Supports both summary table view and detailed single-tool view,
25/// with configuration options for what information to display.
26#[derive(Debug, Clone)]
27pub struct ToolTableRenderer {
28    theme: &'static FastMcpTheme,
29    context: DisplayContext,
30    /// Whether to show parameter counts in the table
31    pub show_parameters: bool,
32    /// Maximum width for description column before truncation
33    pub max_description_width: usize,
34}
35
36impl ToolTableRenderer {
37    /// Create a new renderer with explicit display context.
38    #[must_use]
39    pub fn new(context: DisplayContext) -> Self {
40        Self {
41            theme: crate::theme::theme(),
42            context,
43            show_parameters: true,
44            max_description_width: 50,
45        }
46    }
47
48    /// Create a renderer using auto-detected display context.
49    #[must_use]
50    pub fn detect() -> Self {
51        Self::new(DisplayContext::detect())
52    }
53
54    /// Render a collection of tools as a table.
55    pub fn render(&self, tools: &[Tool], console: &FastMcpConsole) {
56        if tools.is_empty() {
57            if self.should_use_rich(console) {
58                console.print("[dim]No tools registered[/]");
59            } else {
60                console.print("No tools registered");
61            }
62            return;
63        }
64
65        if !self.should_use_rich(console) {
66            self.render_plain(tools, console);
67            return;
68        }
69
70        let mut table = Table::new()
71            .title(format!("Registered Tools ({})", tools.len()))
72            .title_style(self.theme.header_style.clone())
73            .box_style(&ROUNDED)
74            .border_style(self.theme.border_style.clone())
75            .show_header(true);
76
77        table.add_column(Column::new("Name").style(self.theme.key_style.clone()));
78        table.add_column(
79            Column::new("Description")
80                .max_width(self.max_description_width)
81                .overflow(OverflowMethod::Ellipsis),
82        );
83
84        if self.show_parameters {
85            table.add_column(Column::new("Parameters").justify(JustifyMethod::Center));
86        }
87
88        for tool in tools {
89            let desc = tool.description.as_deref().unwrap_or("-");
90            let truncated_desc = self.truncate_description(desc);
91
92            if self.show_parameters {
93                let params = self.format_parameters(&tool.input_schema);
94                table.add_row_cells([tool.name.as_str(), truncated_desc.as_str(), params.as_str()]);
95            } else {
96                table.add_row_cells([tool.name.as_str(), truncated_desc.as_str()]);
97            }
98        }
99
100        console.render(&table);
101    }
102
103    /// Render a single tool in detail.
104    pub fn render_detail(&self, tool: &Tool, console: &FastMcpConsole) {
105        if !self.should_use_rich(console) {
106            self.render_detail_plain(tool, console);
107            return;
108        }
109
110        // Tool name header
111        console.print(&format!("\n[bold cyan]{}[/]", tool.name));
112        console.print(&format!(
113            "[dim]{}[/]\n",
114            tool.description.as_deref().unwrap_or("No description")
115        ));
116
117        // Parameters table (extracted from JSON Schema)
118        let params = self.extract_parameters(&tool.input_schema);
119        if !params.is_empty() {
120            let mut param_table = Table::new()
121                .title("Parameters")
122                .title_style(self.theme.subheader_style.clone())
123                .box_style(&ROUNDED)
124                .border_style(self.theme.border_style.clone())
125                .show_header(true);
126
127            param_table.add_column(Column::new("Name").style(self.theme.key_style.clone()));
128            param_table.add_column(Column::new("Type"));
129            param_table.add_column(Column::new("Required").justify(JustifyMethod::Center));
130            param_table.add_column(Column::new("Description").max_width(40));
131
132            for param in &params {
133                let required_mark = if param.required { "✓" } else { "" };
134                param_table.add_row_cells([
135                    param.name.as_str(),
136                    &param.type_name,
137                    required_mark,
138                    param.description.as_deref().unwrap_or("-"),
139                ]);
140            }
141
142            console.render(&param_table);
143        } else {
144            console.print("[dim]No parameters[/]");
145        }
146    }
147
148    fn should_use_rich(&self, console: &FastMcpConsole) -> bool {
149        self.context.is_human() && console.is_rich()
150    }
151
152    fn format_parameters(&self, schema: &Value) -> String {
153        let params = self.extract_parameters(schema);
154        if params.is_empty() {
155            "none".to_string()
156        } else {
157            let required = params.iter().filter(|p| p.required).count();
158            let optional = params.len() - required;
159
160            match (required, optional) {
161                (r, 0) => format!("{r} required"),
162                (0, o) => format!("{o} optional"),
163                (r, o) => format!("{r} req, {o} opt"),
164            }
165        }
166    }
167
168    fn extract_parameters(&self, schema: &Value) -> Vec<ParameterInfo> {
169        let mut params = Vec::new();
170
171        let properties = schema.get("properties").and_then(Value::as_object);
172        let required = schema
173            .get("required")
174            .and_then(Value::as_array)
175            .map(|arr| {
176                arr.iter()
177                    .filter_map(Value::as_str)
178                    .map(String::from)
179                    .collect::<Vec<_>>()
180            })
181            .unwrap_or_default();
182
183        if let Some(props) = properties {
184            for (name, prop) in props {
185                let type_name = prop
186                    .get("type")
187                    .and_then(Value::as_str)
188                    .unwrap_or("any")
189                    .to_string();
190
191                let description = prop
192                    .get("description")
193                    .and_then(Value::as_str)
194                    .map(String::from);
195
196                params.push(ParameterInfo {
197                    name: name.clone(),
198                    type_name,
199                    required: required.contains(name),
200                    description,
201                });
202            }
203        }
204
205        // Sort: required first, then alphabetically
206        params.sort_by(|a, b| match (a.required, b.required) {
207            (true, false) => std::cmp::Ordering::Less,
208            (false, true) => std::cmp::Ordering::Greater,
209            _ => a.name.cmp(&b.name),
210        });
211
212        params
213    }
214
215    fn truncate_description(&self, desc: &str) -> String {
216        let char_count = desc.chars().count();
217        if char_count <= self.max_description_width {
218            desc.to_string()
219        } else {
220            let truncated: String = desc
221                .chars()
222                .take(self.max_description_width.saturating_sub(3))
223                .collect();
224            format!("{truncated}...")
225        }
226    }
227
228    fn render_plain(&self, tools: &[Tool], console: &FastMcpConsole) {
229        console.print_plain(&format!("Registered Tools ({})", tools.len()));
230        console.print_plain(&"=".repeat(40));
231        for tool in tools {
232            let desc = tool.description.as_deref().unwrap_or("-");
233            if self.show_parameters {
234                let params = self.format_parameters(&tool.input_schema);
235                console.print_plain(&format!("  {} - {} [{}]", tool.name, desc, params));
236            } else {
237                console.print_plain(&format!("  {} - {}", tool.name, desc));
238            }
239        }
240    }
241
242    fn render_detail_plain(&self, tool: &Tool, console: &FastMcpConsole) {
243        console.print_plain(&format!("Tool: {}", tool.name));
244        console.print_plain(&format!(
245            "Description: {}",
246            tool.description.as_deref().unwrap_or("No description")
247        ));
248
249        let params = self.extract_parameters(&tool.input_schema);
250        if params.is_empty() {
251            console.print_plain("Parameters: none");
252        } else {
253            console.print_plain("Parameters:");
254            for param in &params {
255                let req = if param.required {
256                    "required"
257                } else {
258                    "optional"
259                };
260                console.print_plain(&format!(
261                    "  - {}: {} ({}) - {}",
262                    param.name,
263                    param.type_name,
264                    req,
265                    param.description.as_deref().unwrap_or("-")
266                ));
267            }
268        }
269    }
270}
271
272impl Default for ToolTableRenderer {
273    fn default() -> Self {
274        Self::detect()
275    }
276}
277
278/// Parameter information extracted from JSON Schema.
279#[derive(Debug, Clone)]
280struct ParameterInfo {
281    name: String,
282    type_name: String,
283    required: bool,
284    description: Option<String>,
285}
286
287// ============================================================================
288// Resource Table Renderer
289// ============================================================================
290
291/// Renders resource registry as beautiful tables.
292#[derive(Debug, Clone)]
293pub struct ResourceTableRenderer {
294    theme: &'static FastMcpTheme,
295    context: DisplayContext,
296    /// Maximum width for description column
297    pub max_description_width: usize,
298    /// Whether to show MIME type column
299    pub show_mime_type: bool,
300}
301
302impl ResourceTableRenderer {
303    /// Create a new renderer with explicit display context.
304    #[must_use]
305    pub fn new(context: DisplayContext) -> Self {
306        Self {
307            theme: crate::theme::theme(),
308            context,
309            max_description_width: 40,
310            show_mime_type: true,
311        }
312    }
313
314    /// Create a renderer using auto-detected display context.
315    #[must_use]
316    pub fn detect() -> Self {
317        Self::new(DisplayContext::detect())
318    }
319
320    /// Render a collection of resources as a table.
321    pub fn render(&self, resources: &[Resource], console: &FastMcpConsole) {
322        if resources.is_empty() {
323            if self.should_use_rich(console) {
324                console.print("[dim]No resources registered[/]");
325            } else {
326                console.print_plain("No resources registered");
327            }
328            return;
329        }
330
331        if !self.should_use_rich(console) {
332            self.render_plain(resources, console);
333            return;
334        }
335
336        let mut table = Table::new()
337            .title(format!("Registered Resources ({})", resources.len()))
338            .title_style(self.theme.header_style.clone())
339            .box_style(&ROUNDED)
340            .border_style(self.theme.border_style.clone())
341            .show_header(true);
342
343        table.add_column(Column::new("Name").style(self.theme.key_style.clone()));
344        table.add_column(Column::new("URI").style(self.theme.muted_style.clone()));
345        table.add_column(
346            Column::new("Description")
347                .max_width(self.max_description_width)
348                .overflow(OverflowMethod::Ellipsis),
349        );
350
351        if self.show_mime_type {
352            table.add_column(Column::new("Type"));
353        }
354
355        for resource in resources {
356            let desc = resource.description.as_deref().unwrap_or("-");
357            let truncated_desc = self.truncate_description(desc);
358            let formatted_uri = self.format_uri(&resource.uri);
359
360            if self.show_mime_type {
361                let mime = resource.mime_type.as_deref().unwrap_or("-");
362                table.add_row_cells([
363                    resource.name.as_str(),
364                    formatted_uri.as_str(),
365                    truncated_desc.as_str(),
366                    mime,
367                ]);
368            } else {
369                table.add_row_cells([
370                    resource.name.as_str(),
371                    formatted_uri.as_str(),
372                    truncated_desc.as_str(),
373                ]);
374            }
375        }
376
377        console.render(&table);
378    }
379
380    /// Render a single resource in detail.
381    pub fn render_detail(&self, resource: &Resource, console: &FastMcpConsole) {
382        if !self.should_use_rich(console) {
383            self.render_detail_plain(resource, console);
384            return;
385        }
386
387        console.print(&format!("\n[bold cyan]{}[/]", resource.name));
388        console.print(&format!("[dim]URI:[/] {}", self.format_uri(&resource.uri)));
389        console.print(&format!(
390            "[dim]Description:[/] {}",
391            resource.description.as_deref().unwrap_or("No description")
392        ));
393        if let Some(mime) = &resource.mime_type {
394            console.print(&format!("[dim]MIME Type:[/] {}", mime));
395        }
396    }
397
398    /// Render resources as a tree grouped by URI prefix/scheme.
399    ///
400    /// Resources are grouped by their URI scheme (file://, config://, db://, etc.)
401    /// and displayed in a hierarchical tree structure.
402    pub fn render_tree(&self, resources: &[Resource], console: &FastMcpConsole) {
403        if resources.is_empty() {
404            if self.should_use_rich(console) {
405                console.print("[dim]No resources registered[/]");
406            } else {
407                console.print_plain("No resources registered");
408            }
409            return;
410        }
411
412        if !self.should_use_rich(console) {
413            self.render_plain(resources, console);
414            return;
415        }
416
417        // Group resources by URI scheme/prefix
418        let mut groups: HashMap<String, Vec<&Resource>> = HashMap::new();
419        for resource in resources {
420            let prefix = self.extract_uri_prefix(&resource.uri);
421            groups.entry(prefix).or_default().push(resource);
422        }
423
424        // Build tree
425        let root = TreeNode::with_icon("📄", format!("[bold]Resources[/] ({})", resources.len()));
426
427        // Sort group keys for consistent ordering
428        let mut sorted_keys: Vec<_> = groups.keys().cloned().collect();
429        sorted_keys.sort();
430
431        let root = sorted_keys.into_iter().fold(root, |root, prefix| {
432            let group_resources = groups.get(&prefix).unwrap();
433            let group_node =
434                TreeNode::new(format!("[cyan]{}[/] ({})", prefix, group_resources.len()));
435
436            // Add each resource as a child
437            let group_node = group_resources.iter().fold(group_node, |node, resource| {
438                let name_part = self.extract_uri_path(&resource.uri);
439                let desc = self.truncate_description(resource.description.as_deref().unwrap_or(""));
440                let leaf_label = if desc.is_empty() {
441                    self.format_uri(&name_part)
442                } else {
443                    format!("{} [dim]- {}[/]", self.format_uri(&name_part), desc)
444                };
445                node.child(TreeNode::new(leaf_label))
446            });
447
448            root.child(group_node)
449        });
450
451        let tree = Tree::new(root).guides(TreeGuides::Rounded);
452        console.render(&tree);
453    }
454
455    /// Format a URI with template parts highlighted in yellow.
456    ///
457    /// Template parts like `{path}` or `{id}` are highlighted to make
458    /// them visually distinct from static URI parts.
459    fn format_uri(&self, uri: &str) -> String {
460        if !uri.contains('{') {
461            return uri.to_string();
462        }
463
464        let mut result = String::with_capacity(uri.len() + 20); // Pre-alloc some extra for markup
465        let mut buffer = String::new();
466        let mut in_template = false;
467
468        for c in uri.chars() {
469            if in_template {
470                buffer.push(c);
471                if c == '}' {
472                    // Valid template part end
473                    result.push_str("[yellow]");
474                    result.push_str(&buffer);
475                    result.push_str("[/]");
476                    buffer.clear();
477                    in_template = false;
478                } else if c == '{' {
479                    // Nested '{' found, treat previous buffer as literal text
480                    // (URI templates don't support nesting, so assume previous '{' was literal)
481                    // But wait, '{' inside a template is invalid unless escaped.
482                    // Let's just flush the buffer up to the new '{' as literal.
483                    // Actually, simpler: if we hit another '{', assume the previous one was just a char.
484                    // But we already consumed it.
485                    // Let's flush everything except the new char.
486                    result.push_str(&buffer[..buffer.len() - 1]); // Flush previous part
487                    // Keep the new '{' in buffer (buffer now contains just "{")
488                    buffer.clear();
489                    buffer.push('{');
490                }
491            } else if c == '{' {
492                in_template = true;
493                buffer.push(c);
494            } else {
495                result.push(c);
496            }
497        }
498
499        // Flush any remaining buffer (unclosed template)
500        if in_template {
501            result.push_str(&buffer);
502        }
503
504        result
505    }
506
507    /// Extract the URI scheme/prefix (e.g., "file", "config", "db").
508    fn extract_uri_prefix(&self, uri: &str) -> String {
509        if let Some(idx) = uri.find("://") {
510            uri[..idx].to_string()
511        } else if let Some(idx) = uri.find(':') {
512            uri[..idx].to_string()
513        } else {
514            "other".to_string()
515        }
516    }
517
518    /// Extract the path portion of a URI after the scheme.
519    fn extract_uri_path(&self, uri: &str) -> String {
520        if let Some(idx) = uri.find("://") {
521            uri[idx + 3..].to_string()
522        } else if let Some(idx) = uri.find(':') {
523            uri[idx + 1..].to_string()
524        } else {
525            uri.to_string()
526        }
527    }
528
529    fn should_use_rich(&self, console: &FastMcpConsole) -> bool {
530        self.context.is_human() && console.is_rich()
531    }
532
533    fn truncate_description(&self, desc: &str) -> String {
534        let char_count = desc.chars().count();
535        if char_count <= self.max_description_width {
536            desc.to_string()
537        } else {
538            let truncated: String = desc
539                .chars()
540                .take(self.max_description_width.saturating_sub(3))
541                .collect();
542            format!("{truncated}...")
543        }
544    }
545
546    fn render_plain(&self, resources: &[Resource], console: &FastMcpConsole) {
547        console.print_plain(&format!("Registered Resources ({})", resources.len()));
548        console.print_plain(&"=".repeat(40));
549        for resource in resources {
550            let desc = resource.description.as_deref().unwrap_or("-");
551            console.print_plain(&format!(
552                "  {} ({}) - {}",
553                resource.name, resource.uri, desc
554            ));
555        }
556    }
557
558    fn render_detail_plain(&self, resource: &Resource, console: &FastMcpConsole) {
559        console.print_plain(&format!("Resource: {}", resource.name));
560        console.print_plain(&format!("URI: {}", resource.uri));
561        console.print_plain(&format!(
562            "Description: {}",
563            resource.description.as_deref().unwrap_or("No description")
564        ));
565        if let Some(mime) = &resource.mime_type {
566            console.print_plain(&format!("MIME Type: {}", mime));
567        }
568    }
569}
570
571impl Default for ResourceTableRenderer {
572    fn default() -> Self {
573        Self::detect()
574    }
575}
576
577// ============================================================================
578// Prompt Table Renderer
579// ============================================================================
580
581/// Renders prompt registry as beautiful tables.
582#[derive(Debug, Clone)]
583pub struct PromptTableRenderer {
584    theme: &'static FastMcpTheme,
585    context: DisplayContext,
586    /// Maximum width for description column
587    pub max_description_width: usize,
588    /// Whether to show argument counts
589    pub show_arguments: bool,
590}
591
592impl PromptTableRenderer {
593    /// Create a new renderer with explicit display context.
594    #[must_use]
595    pub fn new(context: DisplayContext) -> Self {
596        Self {
597            theme: crate::theme::theme(),
598            context,
599            max_description_width: 50,
600            show_arguments: true,
601        }
602    }
603
604    /// Create a renderer using auto-detected display context.
605    #[must_use]
606    pub fn detect() -> Self {
607        Self::new(DisplayContext::detect())
608    }
609
610    /// Render a collection of prompts as a table.
611    pub fn render(&self, prompts: &[Prompt], console: &FastMcpConsole) {
612        if prompts.is_empty() {
613            if self.should_use_rich(console) {
614                console.print("[dim]No prompts registered[/]");
615            } else {
616                console.print_plain("No prompts registered");
617            }
618            return;
619        }
620
621        if !self.should_use_rich(console) {
622            self.render_plain(prompts, console);
623            return;
624        }
625
626        let mut table = Table::new()
627            .title(format!("Registered Prompts ({})", prompts.len()))
628            .title_style(self.theme.header_style.clone())
629            .box_style(&ROUNDED)
630            .border_style(self.theme.border_style.clone())
631            .show_header(true);
632
633        table.add_column(Column::new("Name").style(self.theme.key_style.clone()));
634        table.add_column(
635            Column::new("Description")
636                .max_width(self.max_description_width)
637                .overflow(OverflowMethod::Ellipsis),
638        );
639
640        if self.show_arguments {
641            table.add_column(Column::new("Arguments").justify(JustifyMethod::Center));
642        }
643
644        for prompt in prompts {
645            let desc = prompt.description.as_deref().unwrap_or("-");
646            let truncated_desc = self.truncate_description(desc);
647
648            if self.show_arguments {
649                let args = self.format_arguments(&prompt.arguments);
650                table.add_row_cells([prompt.name.as_str(), truncated_desc.as_str(), args.as_str()]);
651            } else {
652                table.add_row_cells([prompt.name.as_str(), truncated_desc.as_str()]);
653            }
654        }
655
656        console.render(&table);
657    }
658
659    /// Render a single prompt in detail.
660    pub fn render_detail(&self, prompt: &Prompt, console: &FastMcpConsole) {
661        if !self.should_use_rich(console) {
662            self.render_detail_plain(prompt, console);
663            return;
664        }
665
666        console.print(&format!("\n[bold cyan]{}[/]", prompt.name));
667        console.print(&format!(
668            "[dim]{}[/]\n",
669            prompt.description.as_deref().unwrap_or("No description")
670        ));
671
672        // Arguments table
673        if !prompt.arguments.is_empty() {
674            let mut arg_table = Table::new()
675                .title("Arguments")
676                .title_style(self.theme.subheader_style.clone())
677                .box_style(&ROUNDED)
678                .border_style(self.theme.border_style.clone())
679                .show_header(true);
680
681            arg_table.add_column(Column::new("Name").style(self.theme.key_style.clone()));
682            arg_table.add_column(Column::new("Required").justify(JustifyMethod::Center));
683            arg_table.add_column(Column::new("Description").max_width(40));
684
685            for arg in &prompt.arguments {
686                let required_mark = if arg.required { "✓" } else { "" };
687                arg_table.add_row_cells([
688                    arg.name.as_str(),
689                    required_mark,
690                    arg.description.as_deref().unwrap_or("-"),
691                ]);
692            }
693
694            console.render(&arg_table);
695        } else {
696            console.print("[dim]No arguments[/]");
697        }
698    }
699
700    fn should_use_rich(&self, console: &FastMcpConsole) -> bool {
701        self.context.is_human() && console.is_rich()
702    }
703
704    fn format_arguments(&self, args: &[PromptArgument]) -> String {
705        if args.is_empty() {
706            return "none".to_string();
707        }
708        let required = args.iter().filter(|a| a.required).count();
709        let optional = args.len() - required;
710
711        match (required, optional) {
712            (r, 0) => format!("{r} required"),
713            (0, o) => format!("{o} optional"),
714            (r, o) => format!("{r} req, {o} opt"),
715        }
716    }
717
718    fn truncate_description(&self, desc: &str) -> String {
719        let char_count = desc.chars().count();
720        if char_count <= self.max_description_width {
721            desc.to_string()
722        } else {
723            let truncated: String = desc
724                .chars()
725                .take(self.max_description_width.saturating_sub(3))
726                .collect();
727            format!("{truncated}...")
728        }
729    }
730
731    fn render_plain(&self, prompts: &[Prompt], console: &FastMcpConsole) {
732        console.print_plain(&format!("Registered Prompts ({})", prompts.len()));
733        console.print_plain(&"=".repeat(40));
734        for prompt in prompts {
735            let desc = prompt.description.as_deref().unwrap_or("-");
736            if self.show_arguments {
737                let args = self.format_arguments(&prompt.arguments);
738                console.print_plain(&format!("  {} - {} [{}]", prompt.name, desc, args));
739            } else {
740                console.print_plain(&format!("  {} - {}", prompt.name, desc));
741            }
742        }
743    }
744
745    fn render_detail_plain(&self, prompt: &Prompt, console: &FastMcpConsole) {
746        console.print_plain(&format!("Prompt: {}", prompt.name));
747        console.print_plain(&format!(
748            "Description: {}",
749            prompt.description.as_deref().unwrap_or("No description")
750        ));
751
752        if prompt.arguments.is_empty() {
753            console.print_plain("Arguments: none");
754        } else {
755            console.print_plain("Arguments:");
756            for arg in &prompt.arguments {
757                let req = if arg.required { "required" } else { "optional" };
758                console.print_plain(&format!(
759                    "  - {} ({}) - {}",
760                    arg.name,
761                    req,
762                    arg.description.as_deref().unwrap_or("-")
763                ));
764            }
765        }
766    }
767}
768
769impl Default for PromptTableRenderer {
770    fn default() -> Self {
771        Self::detect()
772    }
773}
774
775// ============================================================================
776// Legacy Functions (for backwards compatibility)
777// ============================================================================
778
779/// Display registered tools in a table (legacy function).
780///
781/// Use `ToolTableRenderer` for more control.
782pub fn render_tools_table(tools: &[Tool], console: &FastMcpConsole) {
783    ToolTableRenderer::detect().render(tools, console);
784}
785
786/// Display registered resources in a table (legacy function).
787///
788/// Use `ResourceTableRenderer` for more control.
789pub fn render_resources_table(resources: &[Resource], console: &FastMcpConsole) {
790    ResourceTableRenderer::detect().render(resources, console);
791}
792
793/// Display registered prompts in a table (legacy function).
794///
795/// Use `PromptTableRenderer` for more control.
796pub fn render_prompts_table(prompts: &[Prompt], console: &FastMcpConsole) {
797    PromptTableRenderer::detect().render(prompts, console);
798}
799
800// ============================================================================
801// Tests
802// ============================================================================
803
804#[cfg(test)]
805mod tests {
806    use super::*;
807    use crate::testing::TestConsole;
808    use serde_json::json;
809
810    fn sample_tools() -> Vec<Tool> {
811        vec![
812            Tool {
813                name: "calculate".to_string(),
814                description: Some("Perform mathematical calculations".to_string()),
815                input_schema: json!({
816                    "type": "object",
817                    "properties": {
818                        "expression": {
819                            "type": "string",
820                            "description": "Mathematical expression to evaluate"
821                        },
822                        "precision": {
823                            "type": "integer",
824                            "description": "Number of decimal places"
825                        }
826                    },
827                    "required": ["expression"]
828                }),
829                output_schema: None,
830                icon: None,
831                version: None,
832                tags: vec![],
833                annotations: None,
834            },
835            Tool {
836                name: "search".to_string(),
837                description: Some("Search for files matching a pattern".to_string()),
838                input_schema: json!({
839                    "type": "object",
840                    "properties": {
841                        "pattern": {
842                            "type": "string",
843                            "description": "Search pattern"
844                        }
845                    },
846                    "required": ["pattern"]
847                }),
848                output_schema: None,
849                icon: None,
850                version: None,
851                tags: vec![],
852                annotations: None,
853            },
854        ]
855    }
856
857    fn sample_resources() -> Vec<Resource> {
858        vec![
859            Resource {
860                uri: "file://config.json".to_string(),
861                name: "config".to_string(),
862                description: Some("Application configuration".to_string()),
863                mime_type: Some("application/json".to_string()),
864                icon: None,
865                version: None,
866                tags: vec![],
867            },
868            Resource {
869                uri: "file://data.csv".to_string(),
870                name: "data".to_string(),
871                description: Some("Data file".to_string()),
872                mime_type: Some("text/csv".to_string()),
873                icon: None,
874                version: None,
875                tags: vec![],
876            },
877        ]
878    }
879
880    fn sample_prompts() -> Vec<Prompt> {
881        vec![
882            Prompt {
883                name: "greeting".to_string(),
884                description: Some("Generate a greeting message".to_string()),
885                arguments: vec![PromptArgument {
886                    name: "name".to_string(),
887                    description: Some("Person's name".to_string()),
888                    required: true,
889                }],
890                icon: None,
891                version: None,
892                tags: vec![],
893            },
894            Prompt {
895                name: "summarize".to_string(),
896                description: Some("Summarize the given text".to_string()),
897                arguments: vec![
898                    PromptArgument {
899                        name: "text".to_string(),
900                        description: Some("Text to summarize".to_string()),
901                        required: true,
902                    },
903                    PromptArgument {
904                        name: "length".to_string(),
905                        description: Some("Target length".to_string()),
906                        required: false,
907                    },
908                ],
909                icon: None,
910                version: None,
911                tags: vec![],
912            },
913        ]
914    }
915
916    #[test]
917    fn test_tool_table_render_plain() {
918        let tools = sample_tools();
919        let console = TestConsole::new();
920        let renderer = ToolTableRenderer::new(DisplayContext::new_agent());
921        renderer.render(&tools, console.console());
922        console.assert_contains("Registered Tools (2)");
923        console.assert_contains("calculate");
924        console.assert_contains("search");
925    }
926
927    #[test]
928    fn test_tool_table_render_rich() {
929        let tools = sample_tools();
930        let console = TestConsole::new_rich();
931        let renderer = ToolTableRenderer::new(DisplayContext::new_human());
932        renderer.render(&tools, console.console());
933        console.assert_contains("Registered Tools");
934        console.assert_contains("calculate");
935    }
936
937    #[test]
938    fn test_tool_table_empty() {
939        let console = TestConsole::new();
940        let renderer = ToolTableRenderer::new(DisplayContext::new_agent());
941        renderer.render(&[], console.console());
942        console.assert_contains("No tools registered");
943    }
944
945    #[test]
946    fn test_tool_table_empty_rich() {
947        let console = TestConsole::new_rich();
948        let renderer = ToolTableRenderer::new(DisplayContext::new_human());
949        renderer.render(&[], console.console());
950        console.assert_contains("No tools registered");
951    }
952
953    #[test]
954    fn test_tool_parameter_extraction() {
955        let tools = sample_tools();
956        let renderer = ToolTableRenderer::new(DisplayContext::new_agent());
957        let params = renderer.extract_parameters(&tools[0].input_schema);
958        assert_eq!(params.len(), 2);
959        // First should be required (expression)
960        assert!(params[0].required);
961        assert_eq!(params[0].name, "expression");
962    }
963
964    #[test]
965    fn test_resource_table_render_plain() {
966        let resources = sample_resources();
967        let console = TestConsole::new();
968        let renderer = ResourceTableRenderer::new(DisplayContext::new_agent());
969        renderer.render(&resources, console.console());
970        console.assert_contains("Registered Resources (2)");
971        console.assert_contains("config");
972    }
973
974    #[test]
975    fn test_resource_table_empty() {
976        let console = TestConsole::new();
977        let renderer = ResourceTableRenderer::new(DisplayContext::new_agent());
978        renderer.render(&[], console.console());
979        console.assert_contains("No resources registered");
980    }
981
982    #[test]
983    fn test_resource_table_empty_rich() {
984        let console = TestConsole::new_rich();
985        let renderer = ResourceTableRenderer::new(DisplayContext::new_human());
986        renderer.render(&[], console.console());
987        console.assert_contains("No resources registered");
988    }
989
990    #[test]
991    fn test_prompt_table_render_plain() {
992        let prompts = sample_prompts();
993        let console = TestConsole::new();
994        let renderer = PromptTableRenderer::new(DisplayContext::new_agent());
995        renderer.render(&prompts, console.console());
996        console.assert_contains("Registered Prompts (2)");
997        console.assert_contains("greeting");
998        console.assert_contains("summarize");
999    }
1000
1001    #[test]
1002    fn test_prompt_table_empty() {
1003        let console = TestConsole::new();
1004        let renderer = PromptTableRenderer::new(DisplayContext::new_agent());
1005        renderer.render(&[], console.console());
1006        console.assert_contains("No prompts registered");
1007    }
1008
1009    #[test]
1010    fn test_prompt_table_empty_rich() {
1011        let console = TestConsole::new_rich();
1012        let renderer = PromptTableRenderer::new(DisplayContext::new_human());
1013        renderer.render(&[], console.console());
1014        console.assert_contains("No prompts registered");
1015    }
1016
1017    #[test]
1018    fn test_prompt_arguments_formatting() {
1019        let renderer = PromptTableRenderer::new(DisplayContext::new_agent());
1020
1021        // Empty
1022        assert_eq!(renderer.format_arguments(&[]), "none");
1023
1024        // Mixed
1025        let args = vec![
1026            PromptArgument {
1027                name: "a".to_string(),
1028                description: None,
1029                required: true,
1030            },
1031            PromptArgument {
1032                name: "b".to_string(),
1033                description: None,
1034                required: false,
1035            },
1036        ];
1037        assert_eq!(renderer.format_arguments(&args), "1 req, 1 opt");
1038    }
1039
1040    #[test]
1041    fn test_description_truncation() {
1042        let renderer = ToolTableRenderer {
1043            theme: crate::theme::theme(),
1044            context: DisplayContext::new_agent(),
1045            show_parameters: true,
1046            max_description_width: 20,
1047        };
1048
1049        assert_eq!(renderer.truncate_description("Short"), "Short");
1050        assert_eq!(
1051            renderer
1052                .truncate_description("This is a very long description that should be truncated"),
1053            "This is a very lo..."
1054        );
1055    }
1056
1057    #[test]
1058    fn test_uri_template_highlighting() {
1059        let renderer = ResourceTableRenderer::new(DisplayContext::new_human());
1060
1061        // No template - unchanged
1062        assert_eq!(
1063            renderer.format_uri("file://config.json"),
1064            "file://config.json"
1065        );
1066
1067        // Simple template
1068        assert_eq!(
1069            renderer.format_uri("file://{path}"),
1070            "file://[yellow]{path}[/]"
1071        );
1072
1073        // Multiple templates
1074        assert_eq!(
1075            renderer.format_uri("db://{table}/{id}"),
1076            "db://[yellow]{table}[/]/[yellow]{id}[/]"
1077        );
1078
1079        // Template in the middle
1080        assert_eq!(
1081            renderer.format_uri("api://users/{id}/profile"),
1082            "api://users/[yellow]{id}[/]/profile"
1083        );
1084    }
1085
1086    #[test]
1087    fn test_uri_prefix_extraction() {
1088        let renderer = ResourceTableRenderer::new(DisplayContext::new_agent());
1089
1090        assert_eq!(renderer.extract_uri_prefix("file://path"), "file");
1091        assert_eq!(renderer.extract_uri_prefix("db://table"), "db");
1092        assert_eq!(renderer.extract_uri_prefix("config:settings"), "config");
1093        assert_eq!(renderer.extract_uri_prefix("no-scheme"), "other");
1094    }
1095
1096    #[test]
1097    fn test_uri_path_extraction() {
1098        let renderer = ResourceTableRenderer::new(DisplayContext::new_agent());
1099
1100        assert_eq!(
1101            renderer.extract_uri_path("file://config.json"),
1102            "config.json"
1103        );
1104        assert_eq!(renderer.extract_uri_path("db://users/{id}"), "users/{id}");
1105        assert_eq!(renderer.extract_uri_path("config:settings"), "settings");
1106    }
1107
1108    fn sample_resources_with_templates() -> Vec<Resource> {
1109        vec![
1110            Resource {
1111                uri: "file://{path}".to_string(),
1112                name: "file".to_string(),
1113                description: Some("Read file contents".to_string()),
1114                mime_type: None,
1115                icon: None,
1116                version: None,
1117                tags: vec![],
1118            },
1119            Resource {
1120                uri: "file://config.json".to_string(),
1121                name: "config".to_string(),
1122                description: Some("Application config".to_string()),
1123                mime_type: Some("application/json".to_string()),
1124                icon: None,
1125                version: None,
1126                tags: vec![],
1127            },
1128            Resource {
1129                uri: "db://users/{id}".to_string(),
1130                name: "user".to_string(),
1131                description: Some("User record by ID".to_string()),
1132                mime_type: None,
1133                icon: None,
1134                version: None,
1135                tags: vec![],
1136            },
1137            Resource {
1138                uri: "cache://stats".to_string(),
1139                name: "stats".to_string(),
1140                description: Some("Cached statistics".to_string()),
1141                mime_type: None,
1142                icon: None,
1143                version: None,
1144                tags: vec![],
1145            },
1146        ]
1147    }
1148
1149    #[test]
1150    fn test_resource_tree_render_plain() {
1151        let resources = sample_resources_with_templates();
1152        let console = TestConsole::new();
1153        let renderer = ResourceTableRenderer::new(DisplayContext::new_agent());
1154        // Tree falls back to plain render in agent mode
1155        renderer.render_tree(&resources, console.console());
1156        console.assert_contains("Registered Resources (4)");
1157    }
1158
1159    #[test]
1160    fn test_resource_tree_empty() {
1161        let console = TestConsole::new();
1162        let renderer = ResourceTableRenderer::new(DisplayContext::new_agent());
1163        renderer.render_tree(&[], console.console());
1164        console.assert_contains("No resources registered");
1165    }
1166
1167    #[test]
1168    fn test_tool_table_plain_without_parameter_summary() {
1169        let tools = sample_tools();
1170        let console = TestConsole::new();
1171        let mut renderer = ToolTableRenderer::new(DisplayContext::new_agent());
1172        renderer.show_parameters = false;
1173        renderer.render(&tools, console.console());
1174        console.assert_contains("Registered Tools (2)");
1175        console.assert_not_contains("[");
1176    }
1177
1178    #[test]
1179    fn test_tool_table_rich_without_parameter_summary() {
1180        let tools = sample_tools();
1181        let console = TestConsole::new_rich();
1182        let mut renderer = ToolTableRenderer::new(DisplayContext::new_human());
1183        renderer.show_parameters = false;
1184        renderer.render(&tools, console.console());
1185        console.assert_contains("Registered Tools");
1186        console.assert_contains("calculate");
1187    }
1188
1189    #[test]
1190    fn test_tool_detail_plain_and_rich() {
1191        let tools = sample_tools();
1192
1193        let plain = TestConsole::new();
1194        let renderer_plain = ToolTableRenderer::new(DisplayContext::new_agent());
1195        renderer_plain.render_detail(&tools[0], plain.console());
1196        plain.assert_contains("Tool: calculate");
1197        plain.assert_contains("Parameters:");
1198        plain.assert_contains("expression: string (required)");
1199
1200        let rich = TestConsole::new_rich();
1201        let renderer_rich = ToolTableRenderer::new(DisplayContext::new_human());
1202        renderer_rich.render_detail(&tools[0], rich.console());
1203        rich.assert_contains("calculate");
1204        rich.assert_contains("Parameters");
1205    }
1206
1207    #[test]
1208    fn test_tool_detail_plain_no_parameters() {
1209        let tool = Tool {
1210            name: "ping".to_string(),
1211            description: Some("No args".to_string()),
1212            input_schema: json!({"type": "object"}),
1213            output_schema: None,
1214            icon: None,
1215            version: None,
1216            tags: vec![],
1217            annotations: None,
1218        };
1219        let console = TestConsole::new();
1220        let renderer = ToolTableRenderer::new(DisplayContext::new_agent());
1221        renderer.render_detail(&tool, console.console());
1222        console.assert_contains("Parameters: none");
1223    }
1224
1225    #[test]
1226    fn test_tool_detail_rich_no_parameters() {
1227        let tool = Tool {
1228            name: "ping".to_string(),
1229            description: Some("No args".to_string()),
1230            input_schema: json!({"type": "object"}),
1231            output_schema: None,
1232            icon: None,
1233            version: None,
1234            tags: vec![],
1235            annotations: None,
1236        };
1237        let console = TestConsole::new_rich();
1238        let renderer = ToolTableRenderer::new(DisplayContext::new_human());
1239        renderer.render_detail(&tool, console.console());
1240        console.assert_contains("ping");
1241        console.assert_contains("No parameters");
1242    }
1243
1244    #[test]
1245    fn test_tool_format_parameters_variants() {
1246        let renderer = ToolTableRenderer::new(DisplayContext::new_agent());
1247        assert_eq!(
1248            renderer.format_parameters(&json!({"type": "object"})),
1249            "none"
1250        );
1251        assert_eq!(
1252            renderer.format_parameters(&json!({
1253                "type": "object",
1254                "properties": {"a": {"type": "string"}},
1255                "required": ["a"]
1256            })),
1257            "1 required"
1258        );
1259        assert_eq!(
1260            renderer.format_parameters(&json!({
1261                "type": "object",
1262                "properties": {"a": {"type": "string"}}
1263            })),
1264            "1 optional"
1265        );
1266        assert_eq!(
1267            renderer.format_parameters(&json!({
1268                "type": "object",
1269                "properties": {"a": {"type": "string"}, "b": {"type": "number"}},
1270                "required": ["a"]
1271            })),
1272            "1 req, 1 opt"
1273        );
1274    }
1275
1276    #[test]
1277    fn test_tool_extract_parameters_defaults_to_any() {
1278        let renderer = ToolTableRenderer::new(DisplayContext::new_agent());
1279        let params = renderer.extract_parameters(&json!({
1280            "type": "object",
1281            "properties": {
1282                "raw": {}
1283            }
1284        }));
1285        assert_eq!(params.len(), 1);
1286        assert_eq!(params[0].name, "raw");
1287        assert_eq!(params[0].type_name, "any");
1288        assert!(!params[0].required);
1289        assert!(params[0].description.is_none());
1290    }
1291
1292    #[test]
1293    fn test_tool_extract_parameters_sorts_required_before_optional() {
1294        let renderer = ToolTableRenderer::new(DisplayContext::new_agent());
1295        let params = renderer.extract_parameters(&json!({
1296            "type": "object",
1297            "properties": {
1298                "a": {"type": "string"},
1299                "z": {"type": "number"}
1300            },
1301            "required": ["z"]
1302        }));
1303        assert_eq!(params.len(), 2);
1304        assert_eq!(params[0].name, "z");
1305        assert!(params[0].required);
1306        assert_eq!(params[1].name, "a");
1307        assert!(!params[1].required);
1308    }
1309
1310    #[test]
1311    fn test_resource_table_without_mime_column() {
1312        let resources = sample_resources();
1313        let console = TestConsole::new();
1314        let mut renderer = ResourceTableRenderer::new(DisplayContext::new_agent());
1315        renderer.show_mime_type = false;
1316        renderer.render(&resources, console.console());
1317        console.assert_contains("Registered Resources (2)");
1318        console.assert_contains("config (file://config.json) - Application configuration");
1319    }
1320
1321    #[test]
1322    fn test_resource_table_rich_with_and_without_mime_column() {
1323        let resources = sample_resources();
1324        let console = TestConsole::new_rich();
1325        let mut renderer = ResourceTableRenderer::new(DisplayContext::new_human());
1326
1327        renderer.render(&resources, console.console());
1328        console.assert_contains("Registered Resources");
1329        console.assert_contains("application/json");
1330
1331        console.clear();
1332        renderer.show_mime_type = false;
1333        renderer.render(&resources, console.console());
1334        console.assert_contains("Registered Resources");
1335        console.assert_not_contains("application/json");
1336    }
1337
1338    #[test]
1339    fn test_resource_detail_plain_and_rich() {
1340        let resources = sample_resources();
1341
1342        let plain = TestConsole::new();
1343        let renderer_plain = ResourceTableRenderer::new(DisplayContext::new_agent());
1344        renderer_plain.render_detail(&resources[0], plain.console());
1345        plain.assert_contains("Resource: config");
1346        plain.assert_contains("URI: file://config.json");
1347        plain.assert_contains("MIME Type: application/json");
1348
1349        let rich = TestConsole::new_rich();
1350        let renderer_rich = ResourceTableRenderer::new(DisplayContext::new_human());
1351        renderer_rich.render_detail(&resources[0], rich.console());
1352        rich.assert_contains("config");
1353        rich.assert_contains("URI:");
1354    }
1355
1356    #[test]
1357    fn test_resource_detail_plain_without_mime() {
1358        let resource = Resource {
1359            uri: "cache://hits".to_string(),
1360            name: "hits".to_string(),
1361            description: Some("Cache hits".to_string()),
1362            mime_type: None,
1363            icon: None,
1364            version: None,
1365            tags: vec![],
1366        };
1367        let console = TestConsole::new();
1368        let renderer = ResourceTableRenderer::new(DisplayContext::new_agent());
1369        renderer.render_detail(&resource, console.console());
1370        console.assert_contains("Resource: hits");
1371        console.assert_not_contains("MIME Type:");
1372    }
1373
1374    #[test]
1375    fn test_resource_tree_render_rich_groups_by_prefix() {
1376        let resources = sample_resources_with_templates();
1377        let console = TestConsole::new_rich();
1378        let renderer = ResourceTableRenderer::new(DisplayContext::new_human());
1379        renderer.render_tree(&resources, console.console());
1380        console.assert_contains("Resources");
1381        console.assert_contains("(4)");
1382        console.assert_contains("file");
1383        console.assert_contains("db");
1384        console.assert_contains("cache");
1385    }
1386
1387    #[test]
1388    fn test_resource_tree_render_rich_with_empty_description_leaf() {
1389        let mut resources = sample_resources_with_templates();
1390        resources.push(Resource {
1391            uri: "file://no-desc".to_string(),
1392            name: "no_desc".to_string(),
1393            description: None,
1394            mime_type: None,
1395            icon: None,
1396            version: None,
1397            tags: vec![],
1398        });
1399
1400        let console = TestConsole::new_rich();
1401        let renderer = ResourceTableRenderer::new(DisplayContext::new_human());
1402        renderer.render_tree(&resources, console.console());
1403        console.assert_contains("no-desc");
1404        console.assert_not_contains("no-desc -");
1405    }
1406
1407    #[test]
1408    fn test_resource_format_uri_unclosed_and_nested_braces() {
1409        let renderer = ResourceTableRenderer::new(DisplayContext::new_agent());
1410        assert_eq!(renderer.format_uri("file://{path"), "file://{path");
1411        assert_eq!(
1412            renderer.format_uri("weird://{a{b}"),
1413            "weird://{a[yellow]{b}[/]"
1414        );
1415    }
1416
1417    #[test]
1418    fn test_resource_extract_uri_path_no_scheme() {
1419        let renderer = ResourceTableRenderer::new(DisplayContext::new_agent());
1420        assert_eq!(renderer.extract_uri_path("just-a-path"), "just-a-path");
1421    }
1422
1423    #[test]
1424    fn test_prompt_table_plain_without_argument_summary() {
1425        let prompts = sample_prompts();
1426        let console = TestConsole::new();
1427        let mut renderer = PromptTableRenderer::new(DisplayContext::new_agent());
1428        renderer.show_arguments = false;
1429        renderer.render(&prompts, console.console());
1430        console.assert_contains("Registered Prompts (2)");
1431        console.assert_contains("greeting - Generate a greeting message");
1432    }
1433
1434    #[test]
1435    fn test_prompt_table_rich_without_argument_summary() {
1436        let prompts = sample_prompts();
1437        let console = TestConsole::new_rich();
1438        let mut renderer = PromptTableRenderer::new(DisplayContext::new_human());
1439        renderer.show_arguments = false;
1440        renderer.render(&prompts, console.console());
1441        console.assert_contains("Registered Prompts");
1442        console.assert_contains("greeting");
1443    }
1444
1445    #[test]
1446    fn test_prompt_detail_plain_and_rich() {
1447        let prompts = sample_prompts();
1448
1449        let plain = TestConsole::new();
1450        let renderer_plain = PromptTableRenderer::new(DisplayContext::new_agent());
1451        renderer_plain.render_detail(&prompts[1], plain.console());
1452        plain.assert_contains("Prompt: summarize");
1453        plain.assert_contains("Arguments:");
1454        plain.assert_contains("text (required)");
1455        plain.assert_contains("length (optional)");
1456
1457        let rich = TestConsole::new_rich();
1458        let renderer_rich = PromptTableRenderer::new(DisplayContext::new_human());
1459        renderer_rich.render_detail(&prompts[1], rich.console());
1460        rich.assert_contains("summarize");
1461        rich.assert_contains("Arguments");
1462    }
1463
1464    #[test]
1465    fn test_prompt_detail_plain_no_arguments() {
1466        let prompt = Prompt {
1467            name: "ping".to_string(),
1468            description: Some("No args".to_string()),
1469            arguments: vec![],
1470            icon: None,
1471            version: None,
1472            tags: vec![],
1473        };
1474        let console = TestConsole::new();
1475        let renderer = PromptTableRenderer::new(DisplayContext::new_agent());
1476        renderer.render_detail(&prompt, console.console());
1477        console.assert_contains("Arguments: none");
1478    }
1479
1480    #[test]
1481    fn test_prompt_detail_rich_no_arguments() {
1482        let prompt = Prompt {
1483            name: "ping".to_string(),
1484            description: Some("No args".to_string()),
1485            arguments: vec![],
1486            icon: None,
1487            version: None,
1488            tags: vec![],
1489        };
1490        let console = TestConsole::new_rich();
1491        let renderer = PromptTableRenderer::new(DisplayContext::new_human());
1492        renderer.render_detail(&prompt, console.console());
1493        console.assert_contains("ping");
1494        console.assert_contains("No arguments");
1495    }
1496
1497    #[test]
1498    fn test_resource_and_prompt_description_truncation_helpers() {
1499        let resource_renderer = ResourceTableRenderer {
1500            theme: crate::theme::theme(),
1501            context: DisplayContext::new_human(),
1502            max_description_width: 12,
1503            show_mime_type: true,
1504        };
1505        assert_eq!(resource_renderer.truncate_description("short"), "short");
1506        assert_eq!(
1507            resource_renderer.truncate_description("this description is too long"),
1508            "this desc..."
1509        );
1510
1511        let prompt_renderer = PromptTableRenderer {
1512            theme: crate::theme::theme(),
1513            context: DisplayContext::new_human(),
1514            max_description_width: 12,
1515            show_arguments: true,
1516        };
1517        assert_eq!(prompt_renderer.truncate_description("short"), "short");
1518        assert_eq!(
1519            prompt_renderer.truncate_description("this description is too long"),
1520            "this desc..."
1521        );
1522    }
1523
1524    #[test]
1525    fn test_prompt_format_arguments_variants() {
1526        let renderer = PromptTableRenderer::new(DisplayContext::new_agent());
1527        assert_eq!(renderer.format_arguments(&[]), "none");
1528        assert_eq!(
1529            renderer.format_arguments(&[PromptArgument {
1530                name: "a".to_string(),
1531                description: None,
1532                required: true,
1533            }]),
1534            "1 required"
1535        );
1536        assert_eq!(
1537            renderer.format_arguments(&[PromptArgument {
1538                name: "b".to_string(),
1539                description: None,
1540                required: false,
1541            }]),
1542            "1 optional"
1543        );
1544    }
1545
1546    #[test]
1547    fn test_legacy_render_functions() {
1548        let tools = sample_tools();
1549        let resources = sample_resources();
1550        let prompts = sample_prompts();
1551        let console = TestConsole::new();
1552
1553        render_tools_table(&tools, console.console());
1554        render_resources_table(&resources, console.console());
1555        render_prompts_table(&prompts, console.console());
1556
1557        console.assert_contains("Registered Tools (2)");
1558        console.assert_contains("Registered Resources (2)");
1559        console.assert_contains("Registered Prompts (2)");
1560    }
1561
1562    #[test]
1563    fn test_renderer_defaults_detect() {
1564        let _tool = ToolTableRenderer::default();
1565        let _resource = ResourceTableRenderer::default();
1566        let _prompt = PromptTableRenderer::default();
1567    }
1568}