use rust_mcp_sdk::schema::{CallToolResult, TextContent};
use std::fmt::Write;
pub struct Icons;
impl Icons {
pub const SUCCESS: &'static str = "\u{2713}"; pub const ERROR: &'static str = "\u{2717}"; pub const WARNING: &'static str = "\u{26A0}"; pub const INFO: &'static str = "\u{2139}"; pub const PENDING: &'static str = "\u{25CB}"; pub const ACTIVE: &'static str = "\u{25CF}"; }
#[derive(Default)]
pub struct ToolOutput {
sections: Vec<String>,
}
impl ToolOutput {
pub fn new() -> Self {
Self::default()
}
pub fn header(mut self, text: &str) -> Self {
self.sections.push(format!("## {}", text));
self
}
pub fn subheader(mut self, text: &str) -> Self {
self.sections.push(format!("### {}", text));
self
}
pub fn text(mut self, text: &str) -> Self {
self.sections.push(text.to_string());
self
}
pub fn blank(mut self) -> Self {
self.sections.push(String::new());
self
}
pub fn rule(mut self) -> Self {
self.sections.push("---".to_string());
self
}
pub fn success(mut self, msg: &str) -> Self {
self.sections.push(format!("{} {}", Icons::SUCCESS, msg));
self
}
pub fn error(mut self, msg: &str) -> Self {
self.sections.push(format!("{} {}", Icons::ERROR, msg));
self
}
pub fn warning(mut self, msg: &str) -> Self {
self.sections.push(format!("{} {}", Icons::WARNING, msg));
self
}
pub fn info(mut self, msg: &str) -> Self {
self.sections.push(format!("{} {}", Icons::INFO, msg));
self
}
pub fn field(mut self, key: &str, value: &str) -> Self {
self.sections.push(format!("**{}**: {}", key, value));
self
}
pub fn code_inline(mut self, code: &str) -> Self {
self.sections.push(format!("`{}`", code));
self
}
pub fn code_block(mut self, code: &str, lang: Option<&str>) -> Self {
let fence = format!("```{}", lang.unwrap_or(""));
self.sections.push(format!("{}\n{}\n```", fence, code));
self
}
pub fn diff(mut self, old: &str, new: &str) -> Self {
let mut diff_content = String::new();
for line in old.lines() {
writeln!(diff_content, "- {}", line).unwrap();
}
for line in new.lines() {
writeln!(diff_content, "+ {}", line).unwrap();
}
if diff_content.ends_with('\n') {
diff_content.pop();
}
self.sections.push(format!("```diff\n{}\n```", diff_content));
self
}
pub fn table(mut self, headers: &[&str], rows: Vec<Vec<String>>) -> Self {
if headers.is_empty() {
return self;
}
let num_cols = headers.len();
let mut col_widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
for row in &rows {
for (i, cell) in row.iter().enumerate() {
if i < num_cols && cell.len() > col_widths[i] {
col_widths[i] = cell.len();
}
}
}
let mut table = String::new();
let header_cells: Vec<String> = headers
.iter()
.enumerate()
.map(|(i, h)| format!("{:width$}", h, width = col_widths[i]))
.collect();
writeln!(table, "| {} |", header_cells.join(" | ")).unwrap();
let separators: Vec<String> = col_widths
.iter()
.map(|w| "-".repeat(*w))
.collect();
writeln!(table, "| {} |", separators.join(" | ")).unwrap();
for row in rows {
let cells: Vec<String> = (0..num_cols)
.map(|i| {
let cell = row.get(i).map(|s| s.as_str()).unwrap_or("");
format!("{:width$}", cell, width = col_widths[i])
})
.collect();
writeln!(table, "| {} |", cells.join(" | ")).unwrap();
}
if table.ends_with('\n') {
table.pop();
}
self.sections.push(table);
self
}
pub fn status_list(mut self, items: Vec<(&str, bool)>) -> Self {
let mut list = String::new();
for (item, completed) in items {
let icon = if completed { Icons::ACTIVE } else { Icons::PENDING };
writeln!(list, "{} {}", icon, item).unwrap();
}
if list.ends_with('\n') {
list.pop();
}
self.sections.push(list);
self
}
pub fn bullet_list(mut self, items: &[&str]) -> Self {
let list: Vec<String> = items.iter().map(|item| format!("- {}", item)).collect();
self.sections.push(list.join("\n"));
self
}
pub fn indented(mut self, items: Vec<(bool, &str)>) -> Self {
let mut content = String::new();
for (success, item) in items {
let icon = if success { Icons::SUCCESS } else { Icons::ERROR };
writeln!(content, " {} {}", icon, item).unwrap();
}
if content.ends_with('\n') {
content.pop();
}
self.sections.push(content);
self
}
pub fn phase_progress(mut self, phases: &[&str], current_index: usize) -> Self {
let indicators: Vec<String> = phases
.iter()
.enumerate()
.map(|(i, phase)| {
let icon = if i <= current_index {
Icons::ACTIVE
} else {
Icons::PENDING
};
format!("{} {}", icon, phase)
})
.collect();
self.sections.push(indicators.join(" -> "));
self
}
pub fn hint(mut self, msg: &str) -> Self {
self.sections.push(format!("**Hint**: {}", msg));
self
}
pub fn build(self) -> String {
self.sections.join("\n\n")
}
pub fn build_result(self) -> CallToolResult {
let text = self.build();
CallToolResult {
content: vec![TextContent::new(text, None, None).into()],
is_error: None,
meta: None,
structured_content: None,
}
}
}
pub fn format_error(title: &str, message: &str, hint: Option<&str>) -> String {
let mut output = ToolOutput::new()
.header("Error")
.error(title)
.blank()
.text(message);
if let Some(h) = hint {
output = output.blank().hint(h);
}
output.build()
}
pub fn error_result(title: &str, message: &str, hint: Option<&str>) -> CallToolResult {
let mut output = ToolOutput::new()
.header("Error")
.error(title)
.blank()
.text(message);
if let Some(h) = hint {
output = output.blank().hint(h);
}
let text = output.build();
CallToolResult {
content: vec![TextContent::new(text, None, None).into()],
is_error: Some(true),
meta: None,
structured_content: None,
}
}
pub fn format_not_found(resource_type: &str, identifier: &str, hint: Option<&str>) -> String {
format_error(
&format!("{} not found: {}", resource_type, identifier),
&format!(
"No {} with identifier \"{}\" exists in this project.",
resource_type.to_lowercase(),
identifier
),
hint,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_output() {
let output = ToolOutput::new()
.header("Document Created")
.success("METIS-T-0001 created successfully")
.build();
assert!(output.contains("## Document Created"));
assert!(output.contains(Icons::SUCCESS));
assert!(output.contains("METIS-T-0001"));
}
#[test]
fn test_table_output() {
let output = ToolOutput::new()
.header("Test")
.table(
&["Code", "Title"],
vec![
vec!["METIS-T-0001".to_string(), "Test Task".to_string()],
],
)
.build();
assert!(output.contains("| Code"));
assert!(output.contains("| Title"));
assert!(output.contains("METIS-T-0001"));
assert!(output.contains("----"));
}
#[test]
fn test_diff_output() {
let output = ToolOutput::new()
.header("Change")
.diff("old text", "new text")
.build();
assert!(output.contains("```diff"));
assert!(output.contains("- old text"));
assert!(output.contains("+ new text"));
}
#[test]
fn test_phase_progress() {
let output = ToolOutput::new()
.phase_progress(&["todo", "active", "completed"], 1)
.build();
assert!(output.contains(Icons::ACTIVE));
assert!(output.contains(Icons::PENDING));
assert!(output.contains("todo"));
assert!(output.contains("active"));
assert!(output.contains("completed"));
}
#[test]
fn test_error_formatting() {
let output = format_error(
"Something went wrong",
"Detailed explanation here.",
Some("Try doing X instead."),
);
assert!(output.contains("## Error"));
assert!(output.contains(Icons::ERROR));
assert!(output.contains("**Hint**"));
}
}