swarm repositories / source
aboutsummaryrefslogtreecommitdiff
path: root/crates/fidget-spinner-cli/src/mcp/output.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/fidget-spinner-cli/src/mcp/output.rs')
-rw-r--r--crates/fidget-spinner-cli/src/mcp/output.rs171
1 files changed, 136 insertions, 35 deletions
diff --git a/crates/fidget-spinner-cli/src/mcp/output.rs b/crates/fidget-spinner-cli/src/mcp/output.rs
index 58f7eb4..01b7bc1 100644
--- a/crates/fidget-spinner-cli/src/mcp/output.rs
+++ b/crates/fidget-spinner-cli/src/mcp/output.rs
@@ -1,16 +1,72 @@
-use libmcp::{JsonPorcelainConfig, RenderMode, render_json_porcelain};
+use libmcp::{
+ DetailLevel, JsonPorcelainConfig, RenderMode, render_json_porcelain,
+ with_presentation_properties,
+};
use serde::Serialize;
-use serde_json::{Map, Value, json};
+use serde_json::{Value, json};
use crate::mcp::fault::{FaultKind, FaultRecord, FaultStage};
-pub(crate) fn split_render_mode(
+const CONCISE_PORCELAIN_MAX_LINES: usize = 12;
+const CONCISE_PORCELAIN_MAX_INLINE_CHARS: usize = 160;
+const FULL_PORCELAIN_MAX_LINES: usize = 40;
+const FULL_PORCELAIN_MAX_INLINE_CHARS: usize = 512;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) struct Presentation {
+ pub render: RenderMode,
+ pub detail: DetailLevel,
+}
+
+#[derive(Debug, Clone)]
+pub(crate) struct ToolOutput {
+ concise: Value,
+ full: Value,
+ concise_text: String,
+ full_text: Option<String>,
+}
+
+impl ToolOutput {
+ #[must_use]
+ pub(crate) fn from_values(
+ concise: Value,
+ full: Value,
+ concise_text: impl Into<String>,
+ full_text: Option<String>,
+ ) -> Self {
+ Self {
+ concise,
+ full,
+ concise_text: concise_text.into(),
+ full_text,
+ }
+ }
+
+ fn structured(&self, detail: DetailLevel) -> &Value {
+ match detail {
+ DetailLevel::Concise => &self.concise,
+ DetailLevel::Full => &self.full,
+ }
+ }
+
+ fn porcelain_text(&self, detail: DetailLevel) -> String {
+ match detail {
+ DetailLevel::Concise => self.concise_text.clone(),
+ DetailLevel::Full => self
+ .full_text
+ .clone()
+ .unwrap_or_else(|| render_json_porcelain(&self.full, full_porcelain_config())),
+ }
+ }
+}
+
+pub(crate) fn split_presentation(
arguments: Value,
operation: &str,
stage: FaultStage,
-) -> Result<(RenderMode, Value), FaultRecord> {
+) -> Result<(Presentation, Value), FaultRecord> {
let Value::Object(mut object) = arguments else {
- return Ok((RenderMode::Porcelain, arguments));
+ return Ok((Presentation::default(), arguments));
};
let render = object
.remove("render")
@@ -26,29 +82,71 @@ pub(crate) fn split_render_mode(
})
.transpose()?
.unwrap_or(RenderMode::Porcelain);
- Ok((render, Value::Object(object)))
+ let detail = object
+ .remove("detail")
+ .map(|value| {
+ serde_json::from_value::<DetailLevel>(value).map_err(|error| {
+ FaultRecord::new(
+ FaultKind::InvalidInput,
+ stage,
+ operation,
+ format!("invalid detail level: {error}"),
+ )
+ })
+ })
+ .transpose()?
+ .unwrap_or(DetailLevel::Concise);
+ Ok((Presentation { render, detail }, Value::Object(object)))
}
-pub(crate) fn tool_success(
+pub(crate) fn tool_output(
value: &impl Serialize,
- render: RenderMode,
stage: FaultStage,
operation: &str,
-) -> Result<Value, FaultRecord> {
+) -> Result<ToolOutput, FaultRecord> {
let structured = serde_json::to_value(value).map_err(|error| {
FaultRecord::new(FaultKind::Internal, stage, operation, error.to_string())
})?;
- tool_success_from_value(structured, render, stage, operation)
+ let concise_text = render_json_porcelain(&structured, concise_porcelain_config());
+ Ok(ToolOutput::from_values(
+ structured.clone(),
+ structured,
+ concise_text,
+ None,
+ ))
+}
+
+pub(crate) fn detailed_tool_output(
+ concise: &impl Serialize,
+ full: &impl Serialize,
+ concise_text: impl Into<String>,
+ full_text: Option<String>,
+ stage: FaultStage,
+ operation: &str,
+) -> Result<ToolOutput, FaultRecord> {
+ let concise = serde_json::to_value(concise).map_err(|error| {
+ FaultRecord::new(FaultKind::Internal, stage, operation, error.to_string())
+ })?;
+ let full = serde_json::to_value(full).map_err(|error| {
+ FaultRecord::new(FaultKind::Internal, stage, operation, error.to_string())
+ })?;
+ Ok(ToolOutput::from_values(
+ concise,
+ full,
+ concise_text,
+ full_text,
+ ))
}
-pub(crate) fn tool_success_from_value(
- structured: Value,
- render: RenderMode,
+pub(crate) fn tool_success(
+ output: ToolOutput,
+ presentation: Presentation,
stage: FaultStage,
operation: &str,
) -> Result<Value, FaultRecord> {
- let text = match render {
- RenderMode::Porcelain => render_json_porcelain(&structured, JsonPorcelainConfig::default()),
+ let structured = output.structured(presentation.detail).clone();
+ let text = match presentation.render {
+ RenderMode::Porcelain => output.porcelain_text(presentation.detail),
RenderMode::Json => crate::to_pretty_json(&structured).map_err(|error| {
FaultRecord::new(FaultKind::Internal, stage, operation, error.to_string())
})?,
@@ -63,26 +161,29 @@ pub(crate) fn tool_success_from_value(
}))
}
-pub(crate) fn with_render_property(schema: Value) -> Value {
- let Value::Object(mut object) = schema else {
- return schema;
- };
+pub(crate) fn with_common_presentation(schema: Value) -> Value {
+ with_presentation_properties(schema)
+}
+
+const fn concise_porcelain_config() -> JsonPorcelainConfig {
+ JsonPorcelainConfig {
+ max_lines: CONCISE_PORCELAIN_MAX_LINES,
+ max_inline_chars: CONCISE_PORCELAIN_MAX_INLINE_CHARS,
+ }
+}
+
+const fn full_porcelain_config() -> JsonPorcelainConfig {
+ JsonPorcelainConfig {
+ max_lines: FULL_PORCELAIN_MAX_LINES,
+ max_inline_chars: FULL_PORCELAIN_MAX_INLINE_CHARS,
+ }
+}
- let properties = object
- .entry("properties".to_owned())
- .or_insert_with(|| Value::Object(Map::new()));
- if let Value::Object(properties) = properties {
- let _ = properties.insert(
- "render".to_owned(),
- json!({
- "type": "string",
- "enum": ["porcelain", "json"],
- "description": "Output mode. Defaults to porcelain for model-friendly summaries."
- }),
- );
+impl Default for Presentation {
+ fn default() -> Self {
+ Self {
+ render: RenderMode::Porcelain,
+ detail: DetailLevel::Concise,
+ }
}
- let _ = object
- .entry("additionalProperties".to_owned())
- .or_insert(Value::Bool(false));
- Value::Object(object)
}