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