From 84e898d9ba699451d5d13fe384e7bbe220564bc1 Mon Sep 17 00:00:00 2001 From: main Date: Thu, 19 Mar 2026 17:15:25 -0400 Subject: Add orthogonal detail controls to libmcp --- crates/libmcp/src/lib.rs | 4 +-- crates/libmcp/src/render.rs | 86 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 83 insertions(+), 7 deletions(-) (limited to 'crates') diff --git a/crates/libmcp/src/lib.rs b/crates/libmcp/src/lib.rs index e769125..2f4e3b1 100644 --- a/crates/libmcp/src/lib.rs +++ b/crates/libmcp/src/lib.rs @@ -31,8 +31,8 @@ pub use normalize::{ parse_human_unsigned_u64, saturating_u64_to_usize, }; pub use render::{ - JsonPorcelainConfig, PathStyle, RenderConfig, RenderMode, TruncatedText, - collapse_inline_whitespace, render_json_porcelain, + DetailLevel, JsonPorcelainConfig, PathStyle, RenderConfig, RenderMode, TruncatedText, + collapse_inline_whitespace, render_json_porcelain, with_presentation_properties, }; pub use replay::ReplayContract; pub use telemetry::{TelemetryLog, ToolErrorDetail, ToolOutcome}; diff --git a/crates/libmcp/src/render.rs b/crates/libmcp/src/render.rs index dd884b4..cb309a6 100644 --- a/crates/libmcp/src/render.rs +++ b/crates/libmcp/src/render.rs @@ -18,6 +18,19 @@ pub enum RenderMode { Json, } +/// Output detail level. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)] +#[serde(rename_all = "snake_case")] +pub enum DetailLevel { + /// Model-optimized concise output. + #[default] + #[serde(alias = "summary", alias = "compact")] + Concise, + /// Verbose output that retains additional structure and fields. + #[serde(alias = "verbose", alias = "detailed")] + Full, +} + /// Path rendering style. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)] #[serde(rename_all = "snake_case")] @@ -35,6 +48,8 @@ pub enum PathStyle { pub struct RenderConfig { /// Chosen render mode. pub render: RenderMode, + /// Chosen detail level. + pub detail: DetailLevel, /// Chosen path rendering style. pub path_style: PathStyle, } @@ -43,7 +58,11 @@ impl RenderConfig { /// Builds a render configuration from user input, applying the default /// path style implied by the render mode. #[must_use] - pub fn from_user_input(render: Option, path_style: Option) -> Self { + pub fn from_user_input( + render: Option, + path_style: Option, + detail: Option, + ) -> Self { let render = render.unwrap_or(RenderMode::Porcelain); let default_path_style = match render { RenderMode::Porcelain => PathStyle::Relative, @@ -51,6 +70,7 @@ impl RenderConfig { }; Self { render, + detail: detail.unwrap_or(DetailLevel::Concise), path_style: path_style.unwrap_or(default_path_style), } } @@ -111,6 +131,41 @@ pub fn render_path(path: &Path, style: PathStyle, workspace_root: Option<&Path>) } } +/// Injects the common presentation controls into an object input schema. +#[must_use] +pub fn with_presentation_properties(schema: Value) -> Value { + let Value::Object(mut object) = schema else { + return schema; + }; + let properties = object + .entry("properties".to_owned()) + .or_insert_with(|| Value::Object(serde_json::Map::new())); + if let Value::Object(properties) = properties { + let _ = properties.insert("render".to_owned(), render_property_schema()); + let _ = properties.insert("detail".to_owned(), detail_property_schema()); + } + let _ = object + .entry("additionalProperties".to_owned()) + .or_insert(Value::Bool(false)); + Value::Object(object) +} + +fn render_property_schema() -> Value { + serde_json::json!({ + "type": "string", + "enum": ["porcelain", "json"], + "description": "Output rendering. Defaults to porcelain for model-friendly summaries." + }) +} + +fn detail_property_schema() -> Value { + serde_json::json!({ + "type": "string", + "enum": ["concise", "full"], + "description": "Output detail level. Concise is the default model-facing summary; full retains more structure." + }) +} + /// Generic JSON-to-porcelain rendering configuration. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct JsonPorcelainConfig { @@ -225,19 +280,21 @@ fn quote_string(text: &str) -> String { #[cfg(test)] mod tests { use super::{ - JsonPorcelainConfig, PathStyle, RenderConfig, RenderMode, collapse_inline_whitespace, - render_json_porcelain, render_path, + DetailLevel, JsonPorcelainConfig, PathStyle, RenderConfig, RenderMode, + collapse_inline_whitespace, render_json_porcelain, render_path, + with_presentation_properties, }; use serde_json::json; use std::path::Path; #[test] fn render_config_uses_mode_specific_defaults() { - let porcelain = RenderConfig::from_user_input(None, None); + let porcelain = RenderConfig::from_user_input(None, None, None); assert_eq!(porcelain.render, RenderMode::Porcelain); + assert_eq!(porcelain.detail, DetailLevel::Concise); assert_eq!(porcelain.path_style, PathStyle::Relative); - let json = RenderConfig::from_user_input(Some(RenderMode::Json), None); + let json = RenderConfig::from_user_input(Some(RenderMode::Json), None, None); assert_eq!(json.path_style, PathStyle::Absolute); } @@ -274,4 +331,23 @@ mod tests { "2 item(s)\n[1] {id=1, title=\"first\"}\n[2] {id=2, title=\"second\"}" ); } + + #[test] + fn injects_render_and_detail_schema_properties() { + let schema = with_presentation_properties(json!({ + "type": "object", + "properties": { + "path": { "type": "string" } + } + })); + assert_eq!( + schema["properties"]["render"]["enum"], + json!(["porcelain", "json"]) + ); + assert_eq!( + schema["properties"]["detail"]["enum"], + json!(["concise", "full"]) + ); + assert_eq!(schema["additionalProperties"], json!(false)); + } } -- cgit v1.2.3