use clap::ValueEnum;
use rmcp::model::{
CallToolResult, GetPromptResult, Prompt, PromptMessageContent, PromptMessageRole,
ReadResourceResult, Resource, ResourceContents, ServerInfo, Tool,
};
use serde_json::{json, Map, Value};
use std::io::IsTerminal;
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
pub enum StructuredOutputFormat {
Json,
JsonPretty,
Toon,
Ndjson,
}
pub fn resolve_structured_format(
format: Option<StructuredOutputFormat>,
pretty: bool,
) -> StructuredOutputFormat {
format.unwrap_or(if pretty {
StructuredOutputFormat::JsonPretty
} else {
StructuredOutputFormat::Json
})
}
pub fn prefer_structured_output(
format: Option<StructuredOutputFormat>,
pretty: bool,
) -> Option<StructuredOutputFormat> {
if format.is_some() || pretty || !std::io::stdout().is_terminal() {
Some(resolve_structured_format(format, pretty))
} else {
None
}
}
pub fn format_structured_value(value: &Value, format: StructuredOutputFormat) -> String {
match format {
StructuredOutputFormat::Json => value.to_string(),
StructuredOutputFormat::JsonPretty => {
serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())
}
StructuredOutputFormat::Toon => encode_toon(value),
StructuredOutputFormat::Ndjson => match value {
Value::Array(items) => items
.iter()
.map(|item| item.to_string())
.collect::<Vec<_>>()
.join("\n"),
_ => value.to_string(),
},
}
}
pub fn format_tool_result(result: &CallToolResult, pretty: bool) -> String {
let texts: Vec<String> = result
.content
.iter()
.filter_map(|c| c.raw.as_text().map(|t| t.text.clone()))
.collect();
let output = texts.join("\n");
if pretty {
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&output) {
if let Ok(pretty_str) = serde_json::to_string_pretty(&val) {
return pretty_str;
}
}
}
output
}
pub fn format_prompt_result(result: &GetPromptResult, pretty: bool) -> String {
if pretty {
return serde_json::to_string_pretty(result)
.unwrap_or_else(|_| serde_json::to_string(result).unwrap_or_default());
}
if result.messages.len() == 1 {
if let Some(message) = result.messages.first() {
if let PromptMessageContent::Text { text } = &message.content {
return text.clone();
}
}
}
let messages: Vec<String> = result
.messages
.iter()
.map(|message| {
let role = match message.role {
PromptMessageRole::User => "user",
PromptMessageRole::Assistant => "assistant",
};
let content = match &message.content {
PromptMessageContent::Text { text } => text.clone(),
_ => serde_json::to_string_pretty(&message.content).unwrap_or_else(|_| {
serde_json::to_string(&message.content).unwrap_or_default()
}),
};
format!("[{}]\n{}", role, content)
})
.collect();
messages.join("\n\n")
}
pub fn format_resource_result(result: &ReadResourceResult, pretty: bool) -> String {
if pretty {
return serde_json::to_string_pretty(result)
.unwrap_or_else(|_| serde_json::to_string(result).unwrap_or_default());
}
let contents: Vec<String> = result
.contents
.iter()
.map(|content| match content {
ResourceContents::TextResourceContents { text, .. } => text.clone(),
ResourceContents::BlobResourceContents { blob, .. } => blob.clone(),
})
.collect();
contents.join("\n\n")
}
pub fn format_tool_list(tools: &[Tool], search: Option<&str>, limit: Option<usize>) -> String {
let filtered: Vec<&Tool> = tools
.iter()
.filter(|tool| {
let name = tool.name.as_ref();
let desc = tool.description.as_deref().unwrap_or("");
if let Some(pattern) = search {
let pattern_lower = pattern.to_lowercase();
name.to_lowercase().contains(&pattern_lower)
|| desc.to_lowercase().contains(&pattern_lower)
} else {
true
}
})
.collect();
if filtered.is_empty() {
if search.is_some() {
return "No matching tools found.".to_string();
}
return "No tools available.".to_string();
}
let total = filtered.len();
let shown = limit.unwrap_or(total).min(total);
let mut lines = Vec::new();
for tool in filtered.into_iter().take(shown) {
let name = tool.name.as_ref();
let desc = tool.description.as_deref().unwrap_or("");
lines.push(format!(" {}", name));
if !desc.is_empty() {
lines.push(format!(" {}", desc));
}
}
let header = if shown < total {
format!("Tools ({} shown of {}):", shown, total)
} else {
format!("Tools ({}):", total)
};
format!("{}\n{}", header, lines.join("\n"))
}
pub fn format_prompt_list(prompts: &[Prompt], limit: Option<usize>) -> String {
if prompts.is_empty() {
return "No prompts available.".to_string();
}
let total = prompts.len();
let shown = limit.unwrap_or(total).min(total);
let mut lines = Vec::new();
for prompt in prompts.iter().take(shown) {
lines.push(format!(" {}", prompt.name));
if let Some(ref desc) = prompt.description {
lines.push(format!(" {}", desc));
}
}
let header = if shown < total {
format!("Prompts ({} shown of {}):", shown, total)
} else {
format!("Prompts ({}):", total)
};
format!("{}\n{}", header, lines.join("\n"))
}
pub fn format_resource_list(resources: &[Resource], limit: Option<usize>) -> String {
if resources.is_empty() {
return "No resources available.".to_string();
}
let total = resources.len();
let shown = limit.unwrap_or(total).min(total);
let mut lines = Vec::new();
for resource in resources.iter().take(shown) {
lines.push(format!(" {} ({})", resource.name, resource.uri));
if let Some(ref desc) = resource.description {
lines.push(format!(" {}", desc));
}
}
let header = if shown < total {
format!("Resources ({} shown of {}):", shown, total)
} else {
format!("Resources ({}):", total)
};
format!("{}\n{}", header, lines.join("\n"))
}
pub fn format_tool_detail(
tool: &Tool,
pretty: bool,
format: Option<StructuredOutputFormat>,
) -> String {
let summary = summarize_tool(tool);
if pretty || format.is_some() {
let format = resolve_structured_format(format, pretty);
return format_structured_value(&summary, format);
}
let mut lines = vec![format!("Tool: {}", tool.name)];
if let Some(title) = &tool.title {
lines.push(format!("Title: {}", title));
}
if let Some(description) = &tool.description {
lines.push(format!("Description: {}", description));
}
let parameters = summary["parameters"]
.as_array()
.cloned()
.unwrap_or_default();
if parameters.is_empty() {
lines.push("Parameters: none".to_string());
} else {
lines.push(format!("Parameters ({}):", parameters.len()));
for parameter in parameters {
let name = parameter["name"].as_str().unwrap_or("unknown");
let ty = parameter["type"].as_str().unwrap_or("any");
let required = parameter["required"].as_bool().unwrap_or(false);
let mut line = format!(
" {}{} ({})",
name,
if required { " [required]" } else { "" },
ty
);
if let Some(description) = parameter["description"].as_str() {
line.push_str(&format!(" - {}", description));
}
if let Some(values) = parameter["enum"].as_array() {
let values = values
.iter()
.filter_map(Value::as_str)
.collect::<Vec<_>>()
.join(", ");
if !values.is_empty() {
line.push_str(&format!(" [enum: {}]", values));
}
}
lines.push(line);
}
}
let capabilities = summary["execution"].clone();
if capabilities != Value::Null {
lines.push(format!("Execution: {}", capabilities));
}
let annotations = summary["annotations"].clone();
if annotations != Value::Null {
lines.push(format!("Hints: {}", annotations));
}
lines.join("\n")
}
pub fn summarize_server_info(server_info: Option<&ServerInfo>) -> Value {
match server_info {
Some(info) => json!({
"protocol_version": info.protocol_version.to_string(),
"server": {
"name": info.server_info.name,
"version": info.server_info.version,
"title": info.server_info.title,
},
"instructions": info.instructions,
"capabilities": {
"tools": info.capabilities.tools.is_some(),
"prompts": info.capabilities.prompts.is_some(),
"resources": info.capabilities.resources.is_some(),
"logging": info.capabilities.logging.is_some(),
"completions": info.capabilities.completions.is_some(),
"tasks": info.capabilities.tasks.is_some(),
"extensions": info.capabilities.extensions.is_some(),
}
}),
None => json!({
"protocol_version": Value::Null,
"server": Value::Null,
"instructions": Value::Null,
"capabilities": {
"tools": Value::Null,
"prompts": Value::Null,
"resources": Value::Null,
"logging": Value::Null,
"completions": Value::Null,
"tasks": Value::Null,
"extensions": Value::Null,
}
}),
}
}
pub fn summarize_tool(tool: &Tool) -> Value {
let required = tool
.input_schema
.get("required")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default()
.into_iter()
.filter_map(|value| value.as_str().map(ToString::to_string))
.collect::<Vec<_>>();
let parameters = tool
.input_schema
.get("properties")
.and_then(Value::as_object)
.map(|properties| {
properties
.iter()
.map(|(name, schema)| summarize_schema_property(name, schema, &required))
.collect::<Vec<_>>()
})
.unwrap_or_default();
json!({
"name": tool.name,
"title": tool.title,
"description": tool.description,
"parameters": parameters,
"annotations": tool.annotations.as_ref().map(|annotations| json!({
"read_only": annotations.read_only_hint,
"destructive": annotations.destructive_hint,
"idempotent": annotations.idempotent_hint,
"open_world": annotations.open_world_hint,
})),
"execution": tool.execution.as_ref().map(|execution| json!({
"task_support": execution.task_support.map(|support| format!("{:?}", support).to_lowercase()),
})),
"input_schema": Value::Object((*tool.input_schema).clone()),
"output_schema": tool
.output_schema
.as_ref()
.map(|schema| Value::Object((**schema).clone())),
})
}
pub fn summarize_tool_brief(tool: &Tool) -> Value {
let required = tool
.input_schema
.get("required")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default()
.into_iter()
.filter_map(|value| value.as_str().map(ToString::to_string))
.collect::<Vec<_>>();
let parameter_names = tool
.input_schema
.get("properties")
.and_then(Value::as_object)
.map(|properties| properties.keys().cloned().collect::<Vec<_>>())
.unwrap_or_default();
json!({
"name": tool.name,
"title": tool.title,
"description": tool.description,
"parameter_count": parameter_names.len(),
"parameter_names": parameter_names,
"required_parameters": required,
"annotations": tool.annotations.as_ref().map(|annotations| json!({
"read_only": annotations.read_only_hint,
"destructive": annotations.destructive_hint,
"idempotent": annotations.idempotent_hint,
"open_world": annotations.open_world_hint,
})),
"execution": tool.execution.as_ref().map(|execution| json!({
"task_support": execution.task_support.map(|support| format!("{:?}", support).to_lowercase()),
})),
})
}
pub fn summarize_prompt(prompt: &Prompt) -> Value {
json!({
"name": prompt.name,
"title": prompt.title,
"description": prompt.description,
"arguments": prompt.arguments.as_ref().map(|arguments| {
arguments.iter().map(|argument| {
json!({
"name": argument.name,
"title": argument.title,
"description": argument.description,
"required": argument.required.unwrap_or(false),
})
}).collect::<Vec<_>>()
}).unwrap_or_default(),
})
}
pub fn summarize_resource(resource: &Resource) -> Value {
json!({
"name": resource.name,
"title": resource.title,
"description": resource.description,
"uri": resource.uri,
"mime_type": resource.mime_type,
"size": resource.size,
})
}
fn summarize_schema_property(name: &str, schema: &Value, required: &[String]) -> Value {
let schema_type = schema
.get("type")
.and_then(Value::as_str)
.unwrap_or("any")
.to_string();
let enum_values = schema
.get("enum")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
json!({
"name": name,
"type": schema_type,
"required": required.iter().any(|value| value == name),
"description": schema.get("description").and_then(Value::as_str),
"enum": enum_values,
})
}
fn encode_toon(value: &Value) -> String {
match value {
Value::Object(map) => render_object(map, 0),
Value::Array(items) => render_array(items, 0, None),
_ => render_scalar(value),
}
}
fn render_object(map: &Map<String, Value>, indent: usize) -> String {
if map.is_empty() {
return "{}".to_string();
}
let mut lines = Vec::new();
for (key, value) in map {
match value {
Value::Object(child) => {
lines.push(format!("{}{}:", indent_str(indent), key));
lines.push(render_object(child, indent + 2));
}
Value::Array(items) => {
if let Some(table) = render_tabular_array(items, indent, Some(key)) {
lines.push(table);
} else if items.iter().all(is_primitive) {
let joined = items
.iter()
.map(render_scalar)
.collect::<Vec<_>>()
.join(", ");
lines.push(format!(
"{}{}[{}]: {}",
indent_str(indent),
key,
items.len(),
joined
));
} else {
lines.push(format!(
"{}{}: {}",
indent_str(indent),
key,
Value::Array(items.clone())
));
}
}
_ => lines.push(format!(
"{}{}: {}",
indent_str(indent),
key,
render_scalar(value)
)),
}
}
lines.join("\n")
}
fn render_array(items: &[Value], indent: usize, name: Option<&str>) -> String {
if let Some(table) = render_tabular_array(items, indent, name) {
return table;
}
if items.is_empty() {
return "[]".to_string();
}
if items.iter().all(is_primitive) {
let joined = items
.iter()
.map(render_scalar)
.collect::<Vec<_>>()
.join(", ");
return match name {
Some(name) => format!(
"{}{}[{}]: {}",
indent_str(indent),
name,
items.len(),
joined
),
None => format!("[{}]: {}", items.len(), joined),
};
}
Value::Array(items.to_vec()).to_string()
}
fn render_tabular_array(items: &[Value], indent: usize, name: Option<&str>) -> Option<String> {
let headers = tabular_headers(items)?;
let header_prefix = match name {
Some(name) => format!(
"{}{}[{}]{{{}}}:",
indent_str(indent),
name,
items.len(),
headers.join(",")
),
None => format!("[{}]{{{}}}:", items.len(), headers.join(",")),
};
let mut lines = vec![header_prefix];
for item in items {
let object = item.as_object()?;
let row = headers
.iter()
.map(|key| {
object
.get(key)
.map(render_scalar)
.unwrap_or_else(|| "null".to_string())
})
.collect::<Vec<_>>()
.join(",");
lines.push(format!("{}{}", indent_str(indent + 2), row));
}
Some(lines.join("\n"))
}
fn tabular_headers(items: &[Value]) -> Option<Vec<String>> {
if items.is_empty() {
return None;
}
let first = items.first()?.as_object()?;
if first.is_empty() {
return None;
}
let headers: Vec<String> = first.keys().cloned().collect();
for item in items {
let object = item.as_object()?;
if object.len() != headers.len() {
return None;
}
if !headers.iter().all(|key| object.contains_key(key)) {
return None;
}
if !object.values().all(is_primitive) {
return None;
}
}
Some(headers)
}
fn render_scalar(value: &Value) -> String {
match value {
Value::Null => "null".to_string(),
Value::Bool(boolean) => boolean.to_string(),
Value::Number(number) => number.to_string(),
Value::String(string) => serde_json::to_string(string).unwrap_or_else(|_| "\"\"".into()),
Value::Array(_) | Value::Object(_) => value.to_string(),
}
}
fn is_primitive(value: &Value) -> bool {
matches!(
value,
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_)
)
}
fn indent_str(indent: usize) -> &'static str {
const SPACES: &str = " ";
&SPACES[..indent.min(SPACES.len())]
}
#[cfg(test)]
mod tests {
use super::*;
use rmcp::model::Tool;
use std::sync::Arc;
fn make_tool(name: &str, desc: &str, properties: &[&str]) -> Tool {
let schema = serde_json::json!({
"type": "object",
"properties": properties
.iter()
.map(|name| {
(
(*name).to_string(),
serde_json::json!({
"type": "string",
"description": format!("{} description", name),
}),
)
})
.collect::<serde_json::Map<String, Value>>(),
"required": properties,
});
Tool::new(
name.to_string(),
desc.to_string(),
Arc::new(schema.as_object().cloned().unwrap_or_default()),
)
}
#[test]
fn test_format_structured_value_json_compact() {
let value = serde_json::json!({"name": "Ada", "count": 2});
let rendered = format_structured_value(&value, StructuredOutputFormat::Json);
let reparsed: Value = serde_json::from_str(&rendered).unwrap();
assert_eq!(reparsed, value);
}
#[test]
fn test_format_structured_value_toon_object() {
let value = serde_json::json!({
"name": "Ada",
"active": true,
"stats": {
"count": 2
}
});
let output = format_structured_value(&value, StructuredOutputFormat::Toon);
assert!(output.contains(r#"name: "Ada""#));
assert!(output.contains("active: true"));
assert!(output.contains("stats:"));
assert!(output.contains(" count: 2"));
}
#[test]
fn test_format_structured_value_toon_tabular_array() {
let value = serde_json::json!({
"pets": [
{"id": 1, "name": "Mochi"},
{"id": 2, "name": "Pixel"}
]
});
let output = format_structured_value(&value, StructuredOutputFormat::Toon);
assert!(output.contains("pets[2]{id,name}:"));
assert!(output.contains(r#" 1,"Mochi""#));
assert!(output.contains(r#" 2,"Pixel""#));
}
#[test]
fn test_format_tool_list_shows_truncation_counts() {
let tools = vec![
make_tool("first", "First tool", &["query"]),
make_tool("second", "Second tool", &["query"]),
];
let output = format_tool_list(&tools, None, Some(1));
assert!(output.contains("Tools (1 shown of 2):"));
assert!(output.contains("first"));
assert!(!output.contains("second"));
}
#[test]
fn test_summarize_tool_brief_omits_full_schema() {
let tool = make_tool("search", "Search things", &["query", "limit"]);
let summary = summarize_tool_brief(&tool);
assert_eq!(summary["parameter_count"], 2);
assert!(summary["parameter_names"].to_string().contains("query"));
assert!(summary.get("input_schema").is_none());
assert!(summary.get("output_schema").is_none());
}
}