mirror of
https://github.com/openai/codex.git
synced 2026-11-01 19:33:35 +00:00
## Why Fixed schema budgets can strip parameter descriptions from large MCP tools or render their Code Mode input types as `unknown`. Allow larger budgets to preserve that guidance. ## What changed - Add `mcp_servers.<name>.tool_input_schema_max_bytes` to configure the UTF-8 byte threshold for best-effort input schema compaction, retaining the 5,000-byte default. - Add `features.code_mode.tool_input_schema_max_bytes` for rendered input types. Use the largest of the 16,000-byte default, the Code Mode setting, and the ordinary MCP server's explicit setting. - Apply rendering budgets to prompt declarations and the runtime tool catalog, and avoid reusing cached declarations after the Code Mode budget changes. ## Testing Add coverage for UTF-8 compaction thresholds, budget precedence, configuration round trips, and preservation of parameter guidance in both the Code Mode prompt and `ALL_TOOLS`. GitOrigin-RevId: 0ef2823a614d4794e636f518b09c0b03a5239b45
190 lines
5.6 KiB
Rust
190 lines
5.6 KiB
Rust
use super::mcp_call_tool_result_output_schema;
|
|
use super::parse_agent_plugin_mcp_tool;
|
|
use super::parse_mcp_tool;
|
|
use crate::JsonSchema;
|
|
use crate::ToolDefinition;
|
|
use pretty_assertions::assert_eq;
|
|
use std::collections::BTreeMap;
|
|
|
|
fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> rmcp::model::Tool {
|
|
rmcp::model::Tool::new(
|
|
name.to_string(),
|
|
description.to_string(),
|
|
std::sync::Arc::new(rmcp::model::object(input_schema)),
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn mcp_schema_max_bytes_uses_the_utf8_compaction_threshold() {
|
|
let input_schema = serde_json::json!({
|
|
"type": "object",
|
|
"properties": {"query": {"type": "string", "description": "parameter guidance é ".repeat(400)}},
|
|
"required": ["query"],
|
|
"additionalProperties": false,
|
|
});
|
|
let tool = mcp_tool("search", "Search", input_schema.clone());
|
|
let schema_bytes = serde_json::to_vec(&input_schema)
|
|
.expect("schema JSON")
|
|
.len();
|
|
let compacted = super::parse_mcp_tool_with_schema_max_bytes(&tool, schema_bytes - 1)
|
|
.expect("argument types fit without descriptions");
|
|
let expanded = super::parse_mcp_tool_with_schema_max_bytes(&tool, schema_bytes)
|
|
.expect("parameter description fits");
|
|
let mut expected_compacted = input_schema.clone();
|
|
expected_compacted["properties"]["query"] = serde_json::json!({"type": "string"});
|
|
assert_eq!(
|
|
serde_json::to_value(compacted.input_schema).expect("schema"),
|
|
expected_compacted
|
|
);
|
|
assert_eq!(
|
|
serde_json::to_value(expanded.input_schema).expect("schema"),
|
|
input_schema
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_mcp_tool_inserts_empty_properties() {
|
|
let tool = mcp_tool(
|
|
"no_props",
|
|
"No properties",
|
|
serde_json::json!({
|
|
"type": "object"
|
|
}),
|
|
);
|
|
|
|
assert_eq!(
|
|
parse_mcp_tool(&tool).expect("parse MCP tool"),
|
|
ToolDefinition {
|
|
name: "no_props".to_string(),
|
|
description: "No properties".to_string(),
|
|
input_schema: JsonSchema::object(
|
|
BTreeMap::new(),
|
|
/*required*/ None,
|
|
/*additional_properties*/ None
|
|
),
|
|
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({})).into()),
|
|
defer_loading: false,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn agent_plugin_mcp_tool_bounds_model_visible_description() {
|
|
let description = format!("{}é", "a".repeat(super::MAX_MCP_TOOL_DESCRIPTION_BYTES - 1));
|
|
let tool = mcp_tool(
|
|
"bounded_description",
|
|
&description,
|
|
serde_json::json!({"type": "object"}),
|
|
);
|
|
|
|
let parsed = parse_agent_plugin_mcp_tool(&tool).expect("parse Agent Plugin MCP tool");
|
|
|
|
assert_eq!(
|
|
parsed.description.len(),
|
|
super::MAX_MCP_TOOL_DESCRIPTION_BYTES - 1
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_mcp_tool_preserves_long_description() {
|
|
let description = "a".repeat(super::MAX_MCP_TOOL_DESCRIPTION_BYTES + 100);
|
|
let tool = mcp_tool(
|
|
"legacy_description",
|
|
&description,
|
|
serde_json::json!({"type": "object"}),
|
|
);
|
|
|
|
assert_eq!(
|
|
parse_mcp_tool(&tool)
|
|
.expect("parse legacy MCP tool")
|
|
.description,
|
|
description
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_mcp_tool_preserves_top_level_output_schema() {
|
|
let mut tool = mcp_tool(
|
|
"with_output",
|
|
"Has output schema",
|
|
serde_json::json!({
|
|
"type": "object"
|
|
}),
|
|
);
|
|
tool.output_schema = Some(std::sync::Arc::new(rmcp::model::object(
|
|
serde_json::json!({
|
|
"properties": {
|
|
"result": {
|
|
"properties": {
|
|
"nested": {}
|
|
}
|
|
}
|
|
},
|
|
"required": ["result"]
|
|
}),
|
|
)));
|
|
|
|
assert_eq!(
|
|
parse_mcp_tool(&tool).expect("parse MCP tool"),
|
|
ToolDefinition {
|
|
name: "with_output".to_string(),
|
|
description: "Has output schema".to_string(),
|
|
input_schema: JsonSchema::object(
|
|
BTreeMap::new(),
|
|
/*required*/ None,
|
|
/*additional_properties*/ None
|
|
),
|
|
output_schema: Some(
|
|
mcp_call_tool_result_output_schema(serde_json::json!({
|
|
"properties": {
|
|
"result": {
|
|
"properties": {
|
|
"nested": {}
|
|
}
|
|
}
|
|
},
|
|
"required": ["result"]
|
|
}))
|
|
.into()
|
|
),
|
|
defer_loading: false,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_mcp_tool_preserves_output_schema_without_inferred_type() {
|
|
let mut tool = mcp_tool(
|
|
"with_enum_output",
|
|
"Has enum output schema",
|
|
serde_json::json!({
|
|
"type": "object"
|
|
}),
|
|
);
|
|
tool.output_schema = Some(std::sync::Arc::new(rmcp::model::object(
|
|
serde_json::json!({
|
|
"enum": ["ok", "error"]
|
|
}),
|
|
)));
|
|
|
|
assert_eq!(
|
|
parse_mcp_tool(&tool).expect("parse MCP tool"),
|
|
ToolDefinition {
|
|
name: "with_enum_output".to_string(),
|
|
description: "Has enum output schema".to_string(),
|
|
input_schema: JsonSchema::object(
|
|
BTreeMap::new(),
|
|
/*required*/ None,
|
|
/*additional_properties*/ None
|
|
),
|
|
output_schema: Some(
|
|
mcp_call_tool_result_output_schema(serde_json::json!({
|
|
"enum": ["ok", "error"]
|
|
}))
|
|
.into()
|
|
),
|
|
defer_loading: false,
|
|
}
|
|
);
|
|
}
|