use rmcp::ErrorData;
use rmcp::model::Tool;
use serde_json::{Map, Value, json};
use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput};
use lean_ctx::core::output_shapers::OutputShapers;
use lean_ctx::tool_defs::tool_def;
pub struct CtxShapeTool;
const MIN_SHAPE_BYTES: usize = 2_000;
const MAX_SHAPE_BYTES: usize = 8 * 1024 * 1024;
impl McpTool for CtxShapeTool {
fn name(&self) -> &'static str {
"ctx_shape"
}
fn produces_machine_readable(&self, _args: Option<&Map<String, Value>>) -> bool {
true
}
fn tool_def(&self) -> Tool {
tool_def(
"ctx_shape",
"Internal host hook: shape a native tool's output after it ran. Not for agents.",
json!({
"type": "object",
"properties": {
"tool": { "type": "string", "description": "Native tool name, e.g. Bash" },
"command": { "type": "string", "description": "Exact command line (Bash)" },
"exit_code": { "type": "integer", "description": "The command's exit code, when known" },
"output": { "type": "string", "description": "The native tool's output as the model would read it" }
},
"required": ["tool", "output"]
}),
)
}
fn handle(
&self,
args: &Map<String, Value>,
_ctx: &ToolContext,
) -> Result<ToolOutput, ErrorData> {
let tool = args
.get("tool")
.and_then(Value::as_str)
.ok_or_else(|| ErrorData::invalid_params("tool is required", None))?;
let output = args
.get("output")
.and_then(Value::as_str)
.ok_or_else(|| ErrorData::invalid_params("output is required", None))?;
let command = args.get("command").and_then(Value::as_str);
let exit_code = args
.get("exit_code")
.and_then(Value::as_i64)
.and_then(|c| i32::try_from(c).ok());
let shaped = shape(tool, command, exit_code, output);
let original_tokens = lean_ctx::core::tokens::count_tokens(output);
let shaped_tokens = lean_ctx::core::tokens::count_tokens(&shaped);
Ok(ToolOutput {
text: shaped,
original_tokens,
saved_tokens: original_tokens.saturating_sub(shaped_tokens),
mode: Some("shape".to_string()),
path: None,
changed: false,
shell_outcome: None,
content_blocks: None,
})
}
}
pub fn shape(tool: &str, command: Option<&str>, exit_code: Option<i32>, output: &str) -> String {
shape_with(
lean_ctx::core::output_shapers::output_shapers(),
tool,
command,
exit_code,
output,
)
}
fn shape_with(
shapers: Option<&OutputShapers>,
tool: &str,
command: Option<&str>,
exit_code: Option<i32>,
output: &str,
) -> String {
if output.len() < MIN_SHAPE_BYTES || output.len() > MAX_SHAPE_BYTES {
return output.to_string();
}
let redacted = super::ctx_shell_background::redact_shell_output_secrets(output);
let Some(shapers) = shapers else {
return redacted;
};
let shaped = match (tool, command) {
("Bash", Some(cmd)) if !cmd.trim().is_empty() => {
(shapers.command)(cmd, &redacted, exit_code)
}
_ => (shapers.tool)(&redacted, Some(tool)),
};
if shaped.len() >= redacted.len() {
redacted
} else {
shaped
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn outside_the_size_window_output_is_returned_verbatim() {
assert_eq!(shape("Bash", Some("ls"), Some(0), "a\nb\n"), "a\nb\n");
let huge = "x\n".repeat(MAX_SHAPE_BYTES / 2 + 1);
assert_eq!(shape("Bash", Some("cat big"), Some(0), &huge), huge);
}
#[test]
fn without_registered_shapers_the_redacted_output_is_returned() {
use std::fmt::Write as _;
let mut noisy = String::new();
for i in 0..400 {
let _ = writeln!(noisy, " Compiling crate-{i} v0.1.{i} (/tmp/x)");
}
let expected = super::super::ctx_shell_background::redact_shell_output_secrets(&noisy);
assert_eq!(
shape_with(None, "Bash", Some("cargo build"), Some(0), &noisy),
expected
);
assert_eq!(shape_with(None, "Grep", None, None, &noisy), expected);
}
}