Preserve complete MCP namespace descriptions (#36882)

## What changed

- Keep complete MCP namespace descriptions in tool-search source metadata.
- Raise the namespace tool-spec description limit from 1,000 bytes to 512 KiB,
  truncating at a UTF-8 character boundary only when the new limit is exceeded.

## Testing

- Cover descriptions beyond the former limit and multibyte truncation at 512 KiB.
- Update SSE and stdio MCP tests to verify that complete server instructions are
  preserved without hiding tools.

GitOrigin-RevId: 000bfcafb3df348065ae451685bfbf978a0e3248
This commit is contained in:
jif
2026-08-04 11:14:06 +00:00
committed by copyberry
parent 77ce1d10aa
commit fd1e4d7a6d
3 changed files with 39 additions and 22 deletions
+3 -7
View File
@@ -31,7 +31,7 @@ use serde_json::Value;
const LEGACY_MCP_TOOL_NAME_PREFIX: &str = "mcp__";
const MCP_TOOL_NAME_DELIMITER: &str = "__";
const MAX_MCP_NAMESPACE_DESCRIPTION_BYTES: usize = 1_000;
const MAX_MCP_NAMESPACE_DESCRIPTION_BYTES: usize = 512 * 1024;
pub struct McpHandler {
tool_info: ToolInfo,
@@ -106,7 +106,6 @@ impl ToolExecutor<ToolInvocation> for McpHandler {
.as_deref()
.map(str::trim)
.filter(|description| !description.is_empty())
.map(bounded_mcp_namespace_description)
.map(str::to_string),
});
@@ -258,15 +257,12 @@ fn create_tool_spec(tool_info: &ToolInfo) -> Result<ToolSpec, serde_json::Error>
Ok(ToolSpec::Namespace(ResponsesApiNamespace {
name: tool_info.callable_namespace.clone(),
description: bounded_mcp_namespace_description(&description).to_string(),
description: take_bytes_at_char_boundary(&description, MAX_MCP_NAMESPACE_DESCRIPTION_BYTES)
.to_string(),
tools: vec![ResponsesApiNamespaceTool::Function(tool)],
}))
}
fn bounded_mcp_namespace_description(description: &str) -> &str {
take_bytes_at_char_boundary(description, MAX_MCP_NAMESPACE_DESCRIPTION_BYTES)
}
fn mcp_hook_tool_input(raw_arguments: &str) -> Value {
if raw_arguments.trim().is_empty() {
return Value::Object(Map::new());
@@ -43,9 +43,8 @@ fn search_info_uses_connector_name_for_output_namespace_description() {
}
#[test]
fn mcp_namespace_descriptions_are_bounded_without_mutating_metadata() {
let expected_description = "é".repeat(499);
let full_description = format!("{expected_description}🦀keep the complete app metadata");
fn mcp_namespace_descriptions_preserve_complete_metadata() {
let full_description = format!("{}🦀keep the complete app metadata", "é".repeat(499));
let mut info = tool_info();
info.namespace_description = Some(full_description.clone());
let handler = McpHandler::new(info).expect("MCP tool spec should build");
@@ -55,17 +54,39 @@ fn mcp_namespace_descriptions_are_bounded_without_mutating_metadata() {
search_info.source_info,
Some(ToolSearchSourceInfo {
name: "Calendar".to_string(),
description: Some(expected_description.clone()),
description: Some(full_description.clone()),
})
);
let LoadableToolSpec::Namespace(namespace) = search_info.entry.output else {
panic!("expected namespace search output");
};
assert_eq!(namespace.description, full_description);
assert_eq!(
handler.tool_info.namespace_description,
Some(full_description)
);
}
#[test]
fn mcp_namespace_descriptions_are_bounded_at_512_kib() {
let expected_description = "é".repeat(MAX_MCP_NAMESPACE_DESCRIPTION_BYTES / 2 - 1);
let full_description = format!("{expected_description}🦀overflow");
let mut info = tool_info();
info.namespace_description = Some(full_description.clone());
let handler = McpHandler::new(info).expect("MCP tool spec should build");
let search_info = handler.search_info().expect("MCP search info");
assert_eq!(
search_info.source_info,
Some(ToolSearchSourceInfo {
name: "Calendar".to_string(),
description: Some(full_description),
})
);
let LoadableToolSpec::Namespace(namespace) = search_info.entry.output else {
panic!("expected namespace search output");
};
assert_eq!(namespace.description, expected_description);
assert_eq!(
handler.tool_info.namespace_description,
Some(full_description)
);
}
fn tool_info() -> ToolInfo {