1use 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#[derive(Debug, Clone, Default)]
18pub struct ServerCapabilities {
19 pub tools: Vec<Tool>,
21 pub resources: Vec<Resource>,
23 pub prompts: Vec<Prompt>,
25}
26
27impl ServerCapabilities {
28 #[must_use]
30 pub fn new() -> Self {
31 Self::default()
32 }
33
34 #[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 #[must_use]
46 pub fn total(&self) -> usize {
47 self.tools.len() + self.resources.len() + self.prompts.len()
48 }
49
50 #[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#[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 #[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 #[must_use]
96 pub fn detect() -> Self {
97 Self::new(DisplayContext::detect())
98 }
99
100 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 self.render_header(capabilities, console);
116
117 if !capabilities.tools.is_empty() {
119 console.print("");
120 self.tool_renderer.render(&capabilities.tools, console);
121 }
122
123 if !capabilities.resources.is_empty() {
125 console.print("");
126 self.resource_renderer
127 .render(&capabilities.resources, console);
128 }
129
130 if !capabilities.prompts.is_empty() {
132 console.print("");
133 self.prompt_renderer.render(&capabilities.prompts, console);
134 }
135
136 self.render_footer(capabilities, console);
138 }
139
140 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 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 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 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 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#[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 tc.assert_contains("calculate");
429 tc.assert_contains("search");
430 tc.assert_contains("example.txt");
432 tc.assert_contains("code_review");
434 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}