use serde_json::{Value, json};
use vtcode_config::ToolDocumentationMode;
pub const MCP_TOOL_DESCRIPTION_MAX_LEN: usize = 512;
pub const MINIMAL_DESCRIPTION_MAX_CHARS: usize = 64;
pub const PROGRESSIVE_DESCRIPTION_MAX_CHARS: usize = 1024;
pub const PROGRESSIVE_PARAMETER_DESCRIPTION_MAX_CHARS: usize = 640;
pub fn compact_tool_description(original: &str, mode: ToolDocumentationMode, per_tool_max: Option<usize>) -> String {
let framing = split_mcp_policy_framing(original);
let text = framing.as_ref().map_or(original, |framing| framing.inner.as_str());
let normalized = text.split_whitespace().collect::<Vec<_>>().join(" ");
let limit = |mode_max: usize| per_tool_max.map_or(mode_max, |per_tool| per_tool.min(mode_max));
let summary = match mode {
ToolDocumentationMode::Minimal => {
let first = first_sentence(&normalized);
let first = first.strip_suffix('.').unwrap_or(first);
return truncate_chars_with_ellipsis(first, limit(MINIMAL_DESCRIPTION_MAX_CHARS));
}
ToolDocumentationMode::Progressive => {
trim_to_leading_sentences(&normalized, limit(PROGRESSIVE_DESCRIPTION_MAX_CHARS))
}
ToolDocumentationMode::Full => trim_to_leading_sentences(&normalized, limit(usize::MAX)),
};
match framing {
Some(framing) => format!("{}\n{summary}\n{MCP_FENCE_CLOSE}", framing.open_tag),
None => summary,
}
}
fn sentence_end_offsets(text: &str) -> impl Iterator<Item = usize> + '_ {
text.char_indices().filter_map(move |(index, ch)| {
if !matches!(ch, '.' | '!' | '?') {
return None;
}
let end = index + ch.len_utf8();
let rest = &text[end..];
if rest.is_empty() {
return Some(end);
}
if !rest.starts_with(char::is_whitespace) {
return None;
}
match rest.trim_start().chars().next() {
Some(following) if following.is_lowercase() => None,
_ => Some(end),
}
})
}
fn first_sentence(text: &str) -> &str {
sentence_end_offsets(text).next().map_or(text, |end| &text[..end])
}
fn trim_to_leading_sentences(text: &str, max_chars: usize) -> String {
if text.chars().count() <= max_chars {
return text.to_string();
}
let mut kept = None;
for end in sentence_end_offsets(text) {
if text[..end].chars().count() > max_chars {
break;
}
kept = Some(end);
}
match kept {
Some(end) => text[..end].to_string(),
None => truncate_chars_with_ellipsis(text, max_chars),
}
}
fn truncate_chars_with_ellipsis(text: &str, max_chars: usize) -> String {
if text.chars().count() <= max_chars {
return text.to_string();
}
let keep = max_chars.saturating_sub(1);
let end = text.char_indices().nth(keep).map_or(text.len(), |(index, _)| index);
format!("{}…", text[..end].trim_end())
}
const MCP_FENCE_OPEN_PREFIX: &str = "<untrusted_mcp_description";
const MCP_FENCE_CLOSE: &str = "</untrusted_mcp_description>";
struct McpFraming {
open_tag: String,
inner: String,
}
fn split_mcp_policy_framing(original: &str) -> Option<McpFraming> {
use crate::tools::mcp::{MCP_POLICY_SENTENCE, MCP_UNTRUSTED_NOTE_SENTENCE};
if !original.contains(MCP_POLICY_SENTENCE) {
return None;
}
let without_policy = original
.replace(MCP_POLICY_SENTENCE, "")
.replace(MCP_UNTRUSTED_NOTE_SENTENCE, "");
let mut open_tag = None;
let mut lines: Vec<&str> = Vec::new();
for line in without_policy.lines() {
let trimmed = line.trim();
if trimmed.starts_with('<') {
if open_tag.is_none() && trimmed.starts_with(MCP_FENCE_OPEN_PREFIX) {
open_tag = Some(trimmed.to_string());
}
continue;
}
lines.push(line);
}
Some(McpFraming {
open_tag: open_tag.unwrap_or_else(|| format!("{MCP_FENCE_OPEN_PREFIX}>")),
inner: lines.join("\n"),
})
}
pub fn compact_parameters(parameters: Value, mode: ToolDocumentationMode) -> Value {
let mut compacted = parameters;
match mode {
ToolDocumentationMode::Full => {}
ToolDocumentationMode::Progressive => {
trim_schema_descriptions(&mut compacted, PROGRESSIVE_PARAMETER_DESCRIPTION_MAX_CHARS, false);
}
ToolDocumentationMode::Minimal => remove_schema_descriptions(&mut compacted),
}
compacted
}
fn trim_schema_descriptions(value: &mut Value, max_chars: usize, inside_properties_map: bool) {
match value {
Value::Object(map) => {
if !inside_properties_map
&& let Some(Value::String(description)) = map.get_mut("description")
&& description.chars().count() > max_chars
{
*description = trim_to_leading_sentences(description, max_chars);
}
for (key, nested) in map.iter_mut() {
trim_schema_descriptions(nested, max_chars, key == "properties");
}
}
Value::Array(items) => {
for item in items {
trim_schema_descriptions(item, max_chars, false);
}
}
_ => {}
}
}
pub fn remove_schema_descriptions(value: &mut Value) {
remove_schema_descriptions_impl(value, false);
}
fn remove_schema_descriptions_impl(value: &mut Value, inside_properties_map: bool) {
match value {
Value::Object(map) => {
if !inside_properties_map {
map.remove("description");
}
for (key, nested) in map.iter_mut() {
remove_schema_descriptions_impl(nested, key == "properties");
}
}
Value::Array(items) => {
for item in items {
remove_schema_descriptions_impl(item, false);
}
}
_ => {}
}
}
pub fn default_parameter_schema() -> Value {
json!({
"type": "object",
"properties": {},
"additionalProperties": true
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tools::mcp::{MCP_POLICY_SENTENCE, MCP_UNTRUSTED_NOTE_SENTENCE};
fn wrapped_mcp_description(inner: &str) -> String {
format!(
"{MCP_POLICY_SENTENCE} {MCP_UNTRUSTED_NOTE_SENTENCE}\n<untrusted_mcp_description provider=\"deepwiki\" tool=\"ask_question\">\n<!-- comment -->\n{inner}\n</untrusted_mcp_description>\n{MCP_POLICY_SENTENCE}"
)
}
#[test]
fn compact_mcp_description_summarizes_inner_text_not_framing() {
let wrapped = wrapped_mcp_description("Ask a question about a repository. Returns an answer with citations.");
for mode in [
ToolDocumentationMode::Minimal,
ToolDocumentationMode::Progressive,
ToolDocumentationMode::Full,
] {
let summary = compact_tool_description(&wrapped, mode, None);
let body = summary.lines().find(|line| !line.starts_with('<')).unwrap_or_default();
assert!(
body.starts_with("Ask a question about a repository"),
"summary must describe the tool, got: {summary}"
);
assert!(!summary.contains("remains authoritative"), "{summary}");
assert!(!summary.contains("<!--"), "{summary}");
}
}
#[test]
fn progressive_and_full_mcp_descriptions_stay_fenced() {
let wrapped = wrapped_mcp_description("Ask a question about a repository. Returns an answer with citations.");
for mode in [ToolDocumentationMode::Progressive, ToolDocumentationMode::Full] {
let projected = compact_tool_description(&wrapped, mode, Some(MCP_TOOL_DESCRIPTION_MAX_LEN));
assert_eq!(
projected,
"<untrusted_mcp_description provider=\"deepwiki\" tool=\"ask_question\">\n\
Ask a question about a repository. Returns an answer with citations.\n\
</untrusted_mcp_description>",
"{mode:?}"
);
}
let long = wrapped_mcp_description(&"Server sentence here. ".repeat(60));
let projected =
compact_tool_description(&long, ToolDocumentationMode::Full, Some(MCP_TOOL_DESCRIPTION_MAX_LEN));
let inner = projected
.strip_prefix("<untrusted_mcp_description provider=\"deepwiki\" tool=\"ask_question\">\n")
.and_then(|rest| rest.strip_suffix("\n</untrusted_mcp_description>"))
.expect("fenced projection");
assert!(inner.chars().count() <= MCP_TOOL_DESCRIPTION_MAX_LEN, "{inner}");
assert!(!inner.contains('<'), "{inner}");
}
#[test]
fn compact_plain_description_is_unchanged() {
let description = "Read a file from disk. Returns contents.";
for mode in [ToolDocumentationMode::Progressive, ToolDocumentationMode::Full] {
assert_eq!(compact_tool_description(description, mode, None), description);
}
assert_eq!(
compact_tool_description(description, ToolDocumentationMode::Minimal, None),
"Read a file from disk"
);
}
#[test]
fn progressive_description_keeps_later_sentences_and_trims_only_long_tails() {
let description = "Run a shell command. For file edits, use apply_patch instead. Expanded modes need approval.";
assert_eq!(compact_tool_description(description, ToolDocumentationMode::Progressive, None), description);
let tail = " Extra detail sentence.".repeat(80);
let long = format!("{description}{tail}");
let trimmed = compact_tool_description(&long, ToolDocumentationMode::Progressive, None);
assert!(trimmed.starts_with(description), "{trimmed}");
assert!(trimmed.ends_with('.'), "trim must stop at a sentence boundary: {trimmed}");
assert!(trimmed.chars().count() <= PROGRESSIVE_DESCRIPTION_MAX_CHARS);
}
#[test]
fn sentence_boundaries_ignore_dots_inside_paths_and_abbreviations() {
let description = "Plans live under .vtcode/plans/ and llms.txt, e.g. foo.md or 0.5 files. Second sentence.";
assert_eq!(
first_sentence(description),
"Plans live under .vtcode/plans/ and llms.txt, e.g. foo.md or 0.5 files."
);
assert_eq!(first_sentence("No terminator here"), "No terminator here");
}
#[test]
fn per_tool_max_never_raises_the_mode_limit() {
let description = "A".repeat(200);
let minimal =
compact_tool_description(&description, ToolDocumentationMode::Minimal, Some(MCP_TOOL_DESCRIPTION_MAX_LEN));
assert_eq!(minimal.chars().count(), MINIMAL_DESCRIPTION_MAX_CHARS);
let full =
compact_tool_description(&"B".repeat(900), ToolDocumentationMode::Full, Some(MCP_TOOL_DESCRIPTION_MAX_LEN));
assert_eq!(full.chars().count(), MCP_TOOL_DESCRIPTION_MAX_LEN);
}
#[test]
fn progressive_parameters_keep_descriptions_and_trim_long_tails() {
let long_tail = " More detail.".repeat(80);
let schema = json!({
"type": "object",
"description": "Top-level schema description.",
"properties": {
"cmd": {"type": "string", "description": "Command to run."},
"mode": {"type": "string", "description": format!("Mode to use.{long_tail}")},
"description": {"type": "string", "description": "A property literally named description."}
}
});
let progressive = compact_parameters(schema.clone(), ToolDocumentationMode::Progressive);
assert_eq!(progressive["properties"]["cmd"]["description"], json!("Command to run."));
assert_eq!(
progressive["properties"]["description"]["description"],
json!("A property literally named description.")
);
let mode = progressive["properties"]["mode"]["description"]
.as_str()
.expect("mode description");
assert!(mode.starts_with("Mode to use."));
assert!(mode.chars().count() <= PROGRESSIVE_PARAMETER_DESCRIPTION_MAX_CHARS);
let minimal = compact_parameters(schema.clone(), ToolDocumentationMode::Minimal);
assert!(minimal["properties"]["cmd"].get("description").is_none());
assert!(minimal["properties"]["description"].is_object());
assert_eq!(compact_parameters(schema.clone(), ToolDocumentationMode::Full), schema);
}
#[test]
fn compact_mcp_description_never_leaks_fence_markup() {
let summary = compact_tool_description(
&wrapped_mcp_description("Read the contents of a wiki. Returns markdown."),
ToolDocumentationMode::Minimal,
None,
);
assert!(!summary.contains("<untrusted"), "fence markup must not leak, got: {summary}");
assert!(!summary.contains("<!--"), "fence comment must not leak, got: {summary}");
assert!(summary.starts_with("Read the contents of a wiki"), "summary must describe the tool, got: {summary}");
}
}