1use crate::cli::OutputFormat;
4use crate::error::{CliError, CliResult};
5use comfy_table::{Table, TableStyle, presets::UTF8_FULL};
6use owo_colors::OwoColorize;
7use serde::Serialize;
8use turbomcp_protocol::types::*;
9
10const TABLE_STYLE: TableStyle = UTF8_FULL.with_rounded_corners();
12
13pub struct Formatter {
15 format: OutputFormat,
16 colored: bool,
17}
18
19impl Formatter {
20 #[must_use]
21 pub fn new(format: OutputFormat, colored: bool) -> Self {
22 Self { format, colored }
23 }
24
25 pub fn display<T: Serialize + ?Sized>(&self, value: &T) -> CliResult<()> {
27 match self.format {
28 OutputFormat::Human => self.display_human(value),
29 OutputFormat::Json => self.display_json(value, true),
30 OutputFormat::Compact => self.display_json(value, false),
31 OutputFormat::Yaml => self.display_yaml(value),
32 OutputFormat::Table => self.display_human(value), }
34 }
35
36 pub fn display_tools(&self, tools: &[Tool]) -> CliResult<()> {
38 match self.format {
39 OutputFormat::Human => {
40 if tools.is_empty() {
41 self.print_info("No tools available");
42 return Ok(());
43 }
44
45 self.print_header("Available Tools");
46 for tool in tools {
47 self.print_tool(tool);
48 }
49 self.print_footer(&format!("Total: {} tools", tools.len()));
50 Ok(())
51 }
52 OutputFormat::Table => {
53 let mut table = Table::new();
54 table.load_style(TABLE_STYLE).set_header(vec![
55 "Name",
56 "Description",
57 "Input Schema",
58 ]);
59
60 for tool in tools {
61 let schema_summary = format_schema_summary(&tool.input_schema);
62
63 table.add_row(vec![
64 &tool.name,
65 tool.description.as_deref().unwrap_or("-"),
66 &schema_summary,
67 ]);
68 }
69
70 println!("{table}");
71 Ok(())
72 }
73 _ => self.display(tools),
74 }
75 }
76
77 pub fn display_resources(&self, resources: &[Resource]) -> CliResult<()> {
79 match self.format {
80 OutputFormat::Human => {
81 if resources.is_empty() {
82 self.print_info("No resources available");
83 return Ok(());
84 }
85
86 self.print_header("Available Resources");
87 for resource in resources {
88 self.print_resource(resource);
89 }
90 self.print_footer(&format!("Total: {} resources", resources.len()));
91 Ok(())
92 }
93 OutputFormat::Table => {
94 let mut table = Table::new();
95 table.load_style(TABLE_STYLE).set_header(vec![
96 "URI",
97 "Name",
98 "Description",
99 "MIME Type",
100 ]);
101
102 for resource in resources {
103 let mime_str = resource
104 .mime_type
105 .as_ref()
106 .map(|m| m.as_str())
107 .unwrap_or("-");
108
109 table.add_row(vec![
110 resource.uri.as_str(),
111 &resource.name,
112 resource.description.as_deref().unwrap_or("-"),
113 mime_str,
114 ]);
115 }
116
117 println!("{table}");
118 Ok(())
119 }
120 _ => self.display(resources),
121 }
122 }
123
124 pub fn display_prompts(&self, prompts: &[Prompt]) -> CliResult<()> {
126 match self.format {
127 OutputFormat::Human => {
128 if prompts.is_empty() {
129 self.print_info("No prompts available");
130 return Ok(());
131 }
132
133 self.print_header("Available Prompts");
134 for prompt in prompts {
135 self.print_prompt(prompt);
136 }
137 self.print_footer(&format!("Total: {} prompts", prompts.len()));
138 Ok(())
139 }
140 OutputFormat::Table => {
141 let mut table = Table::new();
142 table
143 .load_style(TABLE_STYLE)
144 .set_header(vec!["Name", "Description", "Arguments"]);
145
146 for prompt in prompts {
147 let args = prompt
148 .arguments
149 .as_ref()
150 .map(|a| {
151 a.iter()
152 .map(|arg| arg.name.as_str())
153 .collect::<Vec<_>>()
154 .join(", ")
155 })
156 .unwrap_or_else(|| "None".to_string());
157
158 table.add_row(vec![
159 &prompt.name,
160 prompt.description.as_deref().unwrap_or("-"),
161 &args,
162 ]);
163 }
164
165 println!("{table}");
166 Ok(())
167 }
168 _ => self.display(prompts),
169 }
170 }
171
172 pub fn display_server_info(&self, info: &Implementation) -> CliResult<()> {
174 match self.format {
175 OutputFormat::Human => {
176 self.print_header("Server Information");
177 self.print_kv("Name", &info.name);
178 self.print_kv("Version", &info.version);
179 Ok(())
180 }
181 _ => self.display(info),
182 }
183 }
184
185 pub fn display_error(&self, error: &CliError) {
187 if self.colored {
188 eprintln!("{}: {}", "Error".bright_red().bold(), error);
189
190 let suggestions = error.suggestions();
191 if !suggestions.is_empty() {
192 eprintln!("\n{}", "Suggestions:".bright_yellow().bold());
193 for suggestion in suggestions {
194 eprintln!(" {} {}", "•".bright_blue(), suggestion);
195 }
196 }
197 } else {
198 eprintln!("Error: {error}");
199
200 let suggestions = error.suggestions();
201 if !suggestions.is_empty() {
202 eprintln!("\nSuggestions:");
203 for suggestion in suggestions {
204 eprintln!(" • {suggestion}");
205 }
206 }
207 }
208 }
209
210 fn display_json<T: Serialize + ?Sized>(&self, value: &T, pretty: bool) -> CliResult<()> {
213 let json = if pretty {
214 serde_json::to_string_pretty(value)?
215 } else {
216 serde_json::to_string(value)?
217 };
218 println!("{json}");
219 Ok(())
220 }
221
222 fn display_yaml<T: Serialize + ?Sized>(&self, value: &T) -> CliResult<()> {
223 let yaml = serde_norway::to_string(value)?;
224 println!("{yaml}");
225 Ok(())
226 }
227
228 fn display_human<T: Serialize + ?Sized>(&self, value: &T) -> CliResult<()> {
229 self.display_json(value, true)
231 }
232
233 fn print_header(&self, text: &str) {
234 if self.colored {
235 println!("\n{}", text.bright_cyan().bold());
236 println!("{}", "=".repeat(text.len()).bright_cyan());
237 } else {
238 println!("\n{text}");
239 println!("{}", "=".repeat(text.len()));
240 }
241 }
242
243 fn print_footer(&self, text: &str) {
244 if self.colored {
245 println!("\n{}", text.bright_black());
246 } else {
247 println!("\n{text}");
248 }
249 }
250
251 fn print_info(&self, text: &str) {
252 if self.colored {
253 println!("{}", text.bright_blue());
254 } else {
255 println!("{text}");
256 }
257 }
258
259 fn print_kv(&self, key: &str, value: &str) {
260 if self.colored {
261 println!(" {}: {}", key.bright_green().bold(), value);
262 } else {
263 println!(" {key}: {value}");
264 }
265 }
266
267 fn print_tool(&self, tool: &Tool) {
268 if self.colored {
269 println!(
270 " {} {}",
271 "•".bright_blue(),
272 tool.name.bright_green().bold()
273 );
274 if let Some(desc) = &tool.description {
275 println!(" {desc}");
276 }
277 } else {
278 println!(" • {}", tool.name);
279 if let Some(desc) = &tool.description {
280 println!(" {desc}");
281 }
282 }
283 }
284
285 fn print_resource(&self, resource: &Resource) {
286 if self.colored {
287 println!(
288 " {} {}",
289 "•".bright_blue(),
290 resource.uri.as_str().bright_green().bold()
291 );
292 println!(" Name: {}", resource.name);
293 if let Some(desc) = &resource.description {
294 println!(" {desc}");
295 }
296 } else {
297 println!(" • {}", resource.uri.as_str());
298 println!(" Name: {}", resource.name);
299 if let Some(desc) = &resource.description {
300 println!(" {desc}");
301 }
302 }
303 }
304
305 fn print_prompt(&self, prompt: &Prompt) {
306 if self.colored {
307 println!(
308 " {} {}",
309 "•".bright_blue(),
310 prompt.name.bright_green().bold()
311 );
312 if let Some(desc) = &prompt.description {
313 println!(" {desc}");
314 }
315 if let Some(args) = &prompt.arguments {
316 if !args.is_empty() {
317 let arg_names: Vec<_> = args.iter().map(|a| a.name.as_str()).collect();
318 println!(" Arguments: {}", arg_names.join(", ").bright_yellow());
319 }
320 }
321 } else {
322 println!(" • {}", prompt.name);
323 if let Some(desc) = &prompt.description {
324 println!(" {desc}");
325 }
326 if let Some(args) = &prompt.arguments {
327 if !args.is_empty() {
328 let arg_names: Vec<_> = args.iter().map(|a| a.name.as_str()).collect();
329 println!(" Arguments: {}", arg_names.join(", "));
330 }
331 }
332 }
333 }
334}
335
336fn format_schema_summary(schema: &ToolInputSchema) -> String {
338 if let Some(props) = schema.properties_as_object()
339 && !props.is_empty()
340 {
341 let prop_names: Vec<_> = props.keys().map(|k| k.as_str()).collect();
342 return prop_names.join(", ");
343 }
344 "No properties".to_string()
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350
351 #[test]
352 fn test_formatter_creation() {
353 let formatter = Formatter::new(OutputFormat::Human, true);
354 assert!(formatter.colored);
355 }
356}