diff options
| author | main <main@swarm.moe> | 2026-03-19 17:15:25 -0400 |
|---|---|---|
| committer | main <main@swarm.moe> | 2026-03-19 17:15:25 -0400 |
| commit | 84e898d9ba699451d5d13fe384e7bbe220564bc1 (patch) | |
| tree | bcb6140c8dbbf34a67f7c007440c4fc5a3d351ce | |
| parent | 478b0bc47fade5864f4f397de7ea519beddab749 (diff) | |
| download | libmcp-84e898d9ba699451d5d13fe384e7bbe220564bc1.zip | |
Add orthogonal detail controls to libmcp
| -rw-r--r-- | assets/codex-skills/mcp-bootstrap/SKILL.md | 2 | ||||
| -rw-r--r-- | assets/codex-skills/mcp-bootstrap/references/bootstrap-fresh.md | 8 | ||||
| -rw-r--r-- | assets/codex-skills/mcp-bootstrap/references/bootstrap-retrofit.md | 1 | ||||
| -rw-r--r-- | assets/codex-skills/mcp-bootstrap/references/checklist.md | 2 | ||||
| -rw-r--r-- | crates/libmcp/src/lib.rs | 4 | ||||
| -rw-r--r-- | crates/libmcp/src/render.rs | 86 | ||||
| -rw-r--r-- | docs/spec.md | 17 |
7 files changed, 111 insertions, 9 deletions
diff --git a/assets/codex-skills/mcp-bootstrap/SKILL.md b/assets/codex-skills/mcp-bootstrap/SKILL.md index 19d59d9..84a9244 100644 --- a/assets/codex-skills/mcp-bootstrap/SKILL.md +++ b/assets/codex-skills/mcp-bootstrap/SKILL.md @@ -33,6 +33,7 @@ Default posture: - make replay legality explicit per request surface - ship health, telemetry, and recovery tests before feature sprawl - default nontrivial tool output to porcelain +- treat `render` and `detail` as orthogonal controls - keep structured JSON output available where exact consumers need it ## Retrofit @@ -47,6 +48,7 @@ Retrofit in order: - separate durable transport ownership from fragile execution - define typed faults and replay contracts before adding retries - adopt the model UX doctrine, especially porcelain-by-default output +- make `detail=concise|full` real before inventing consumer-local verbosity knobs - add rollout, telemetry, and recovery tests before claiming stability ## Guardrails diff --git a/assets/codex-skills/mcp-bootstrap/references/bootstrap-fresh.md b/assets/codex-skills/mcp-bootstrap/references/bootstrap-fresh.md index aefe25d..4eea2b3 100644 --- a/assets/codex-skills/mcp-bootstrap/references/bootstrap-fresh.md +++ b/assets/codex-skills/mcp-bootstrap/references/bootstrap-fresh.md @@ -46,6 +46,11 @@ Faults should flow through health, telemetry, and user-facing shaping. Nontrivial tools should default to `render=porcelain`. +`render` and detail are separate axes. + +- `render=porcelain|json` +- `detail=concise|full` + Porcelain should be: - line-oriented @@ -55,6 +60,9 @@ Porcelain should be: Structured `render=json` should remain available. +`json + concise` should be a structured summary, not merely the full payload in +different clothes. + Use library rendering helpers where possible. Do not default to pretty-printed JSON dumps and call that porcelain. diff --git a/assets/codex-skills/mcp-bootstrap/references/bootstrap-retrofit.md b/assets/codex-skills/mcp-bootstrap/references/bootstrap-retrofit.md index 146733c..faccc4c 100644 --- a/assets/codex-skills/mcp-bootstrap/references/bootstrap-retrofit.md +++ b/assets/codex-skills/mcp-bootstrap/references/bootstrap-retrofit.md @@ -8,6 +8,7 @@ scratch. 1. Separate session ownership from fragile execution. 2. Define typed replay contracts and typed faults. 3. Replace ad hoc backend dumps with porcelain-by-default output. + Make `render` and `detail` orthogonal before you start bikeshedding prose. 4. Add health, telemetry, and recovery tests. 5. Only then promise hot rollout or stronger operational guarantees. diff --git a/assets/codex-skills/mcp-bootstrap/references/checklist.md b/assets/codex-skills/mcp-bootstrap/references/checklist.md index f2eeffd..babb157 100644 --- a/assets/codex-skills/mcp-bootstrap/references/checklist.md +++ b/assets/codex-skills/mcp-bootstrap/references/checklist.md @@ -9,6 +9,8 @@ Use this checklist when reviewing a `libmcp` consumer. - Are replay contracts typed and local to the request surface? - Are faults typed and connected to recovery semantics? - Do nontrivial tools default to porcelain output? +- Are `render` and `detail` treated as orthogonal controls? +- Does `detail=concise` return an actual summary rather than the full payload? - Are library render helpers used where bespoke porcelain has not yet been justified? - Is structured JSON still available where exact consumers need it? 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<RenderMode>, path_style: Option<PathStyle>) -> Self { + pub fn from_user_input( + render: Option<RenderMode>, + path_style: Option<PathStyle>, + detail: Option<DetailLevel>, + ) -> 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)); + } } diff --git a/docs/spec.md b/docs/spec.md index f8a72d4..80e464b 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -163,6 +163,11 @@ Nontrivial tools should default to `render=porcelain`. Structured `render=json` must remain available, but it is opt-in unless a tool is intrinsically structured and tiny. +`render` and detail are orthogonal concerns. + +- `render=porcelain|json` selects text versus structured output. +- `detail=concise|full` selects summary versus expanded output. + Porcelain output should be: - line-oriented @@ -174,6 +179,7 @@ Porcelain output should be: The library should therefore provide reusable primitives for: - render mode selection +- detail selection - bounded/truncated text shaping - stable note emission - path rendering @@ -181,10 +187,17 @@ The library should therefore provide reusable primitives for: - generic JSON-to-porcelain projection for consumers that have not yet earned bespoke renderers -`libmcp` does not require a universal detail taxonomy like -`summary|compact|full`. Consumers may add extra detail controls when their tool +`libmcp` standardizes only the minimal shared detail axis +`concise|full`. Consumers may add richer local taxonomies when their tool surface actually needs them. +When a tool supports both detail levels: + +- `porcelain + concise` should be the hot-path default +- `json + concise` should return a structured summary rather than the full + backing object +- `json + full` should remain the authoritative structured payload + ### Normalization The library should reduce trivial model-facing friction where the semantics are |