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