swarm repositories / source
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormain <main@swarm.moe>2026-03-19 17:15:25 -0400
committermain <main@swarm.moe>2026-03-19 17:15:25 -0400
commit84e898d9ba699451d5d13fe384e7bbe220564bc1 (patch)
treebcb6140c8dbbf34a67f7c007440c4fc5a3d351ce
parent478b0bc47fade5864f4f397de7ea519beddab749 (diff)
downloadlibmcp-84e898d9ba699451d5d13fe384e7bbe220564bc1.zip
Add orthogonal detail controls to libmcp
-rw-r--r--assets/codex-skills/mcp-bootstrap/SKILL.md2
-rw-r--r--assets/codex-skills/mcp-bootstrap/references/bootstrap-fresh.md8
-rw-r--r--assets/codex-skills/mcp-bootstrap/references/bootstrap-retrofit.md1
-rw-r--r--assets/codex-skills/mcp-bootstrap/references/checklist.md2
-rw-r--r--crates/libmcp/src/lib.rs4
-rw-r--r--crates/libmcp/src/render.rs86
-rw-r--r--docs/spec.md17
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