use std::collections::HashSet;
use std::sync::{Mutex, OnceLock};
pub(crate) fn build_tool_description(
base: &str,
output_schema: Option<&serde_json::Value>,
forward: bool,
hint_bytes: usize,
max_combined_bytes: usize,
tool_name: &str,
) -> String {
if !forward {
return base.to_owned();
}
let Some(schema) = output_schema else {
return base.to_owned();
};
let compact = serde_json::to_string(schema).unwrap_or_default();
let hint = if compact.len() > hint_bytes {
static WARNED: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
let guard = WARNED.get_or_init(|| Mutex::new(HashSet::new()));
let mut warned = guard
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if warned.insert(tool_name.to_owned()) {
tracing::warn!(
tool = tool_name,
schema_bytes = compact.len(),
cap = hint_bytes,
event = "mcp.output_schema.stub_used",
"MCP output_schema hint exceeds budget — using stub"
);
}
format!(
"Output schema too large ({} bytes); details omitted.",
compact.len()
)
} else {
tracing::debug!(
tool = tool_name,
event = "mcp.output_schema.forwarded_to_llm",
"MCP tool output schema forwarded to LLM description"
);
compact
};
let combined = format!("{base}\n\nExpected output schema (JSON):\n{hint}");
if max_combined_bytes < usize::MAX && combined.len() > max_combined_bytes {
zeph_common::text::truncate_to_bytes(&combined, max_combined_bytes).clone()
} else {
combined
}
}