mirror of
https://github.com/openai/codex.git
synced 2026-11-10 08:43:33 +00:00
Support per-tool MCP output limits (#41421)
## What changed - Add a positive `output_token_limit` setting to each entry under an MCP server's `tools` configuration. - Apply the most restrictive limit when plugin and user policies overlap, while keeping approval policy independent. - Carry the effective MCP output budget in conversation history so tool output, post-tool hook responses, and resumed sessions use the same truncation limit. ## Testing - Cover configuration parsing, serialization, schema validation, and plugin policy merging. - Cover MCP output below and above the configured limit, post-tool hook responses, and session resume. GitOrigin-RevId: d0beb4fca9ba6055d9e1d31c137373b465d50d61
This commit is contained in:
1 parent
03147407e3
commit
f742dabc6f
38 files changed
+487
-224
No files matched your search
@@ -6773,6 +6773,7 @@ approval_mode = "approve"
|
||||
server.tools.get("search"),
|
||||
Some(&McpServerToolConfig {
|
||||
approval_mode: Some(AppToolApproval::Approve),
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -6816,6 +6817,7 @@ approval_mode = "approve"
|
||||
tool,
|
||||
&McpServerToolConfig {
|
||||
approval_mode: Some(AppToolApproval::Approve),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -314,7 +314,7 @@ impl ConfigDocument {
|
||||
&[NOTICE_TABLE_KEY, "model_migrations", from.as_str()],
|
||||
value(to.clone()),
|
||||
)),
|
||||
ConfigEdit::ReplaceMcpServers(servers) => Ok(self.replace_mcp_servers(servers)),
|
||||
ConfigEdit::ReplaceMcpServers(servers) => self.replace_mcp_servers(servers),
|
||||
ConfigEdit::AddToolSuggestDisabledTool(disabled_tool) => {
|
||||
Ok(self.add_tool_suggest_disabled_tool(disabled_tool))
|
||||
}
|
||||
@@ -432,9 +432,12 @@ impl ConfigDocument {
|
||||
self.remove(segments)
|
||||
}
|
||||
|
||||
fn replace_mcp_servers(&mut self, servers: &BTreeMap<String, McpServerConfig>) -> bool {
|
||||
fn replace_mcp_servers(
|
||||
&mut self,
|
||||
servers: &BTreeMap<String, McpServerConfig>,
|
||||
) -> anyhow::Result<bool> {
|
||||
if servers.is_empty() {
|
||||
return self.clear(&["mcp_servers"]);
|
||||
return Ok(self.clear(&["mcp_servers"]));
|
||||
}
|
||||
|
||||
let root = self.doc.as_table_mut();
|
||||
@@ -446,7 +449,7 @@ impl ConfigDocument {
|
||||
}
|
||||
|
||||
let Some(item) = root.get_mut("mcp_servers") else {
|
||||
return false;
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
if document_helpers::ensure_table_for_write(item).is_none() {
|
||||
@@ -454,7 +457,7 @@ impl ConfigDocument {
|
||||
}
|
||||
|
||||
let Some(table) = item.as_table_mut() else {
|
||||
return false;
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let keys_to_remove: Vec<String> = table
|
||||
@@ -472,17 +475,17 @@ impl ConfigDocument {
|
||||
if let TomlItem::Value(value) = existing
|
||||
&& let Some(inline) = value.as_inline_table_mut()
|
||||
{
|
||||
let replacement = document_helpers::serialize_mcp_server_inline(config);
|
||||
let replacement = document_helpers::serialize_mcp_server_inline(config)?;
|
||||
document_helpers::merge_inline_table(inline, replacement);
|
||||
} else {
|
||||
*existing = document_helpers::serialize_mcp_server(config);
|
||||
*existing = document_helpers::serialize_mcp_server(config)?;
|
||||
}
|
||||
} else {
|
||||
table.insert(name, document_helpers::serialize_mcp_server(config));
|
||||
table.insert(name, document_helpers::serialize_mcp_server(config)?);
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn set_skill_config(&mut self, selector: SkillConfigSelector, enabled: bool) -> bool {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use anyhow::Context;
|
||||
use codex_config::types::AppToolApproval;
|
||||
use codex_config::types::McpServerAuth;
|
||||
use codex_config::types::McpServerConfig;
|
||||
@@ -45,7 +46,7 @@ pub(super) fn ensure_table_for_read(item: &mut TomlItem) -> Option<&mut TomlTabl
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_mcp_server_table(config: &McpServerConfig) -> TomlTable {
|
||||
fn serialize_mcp_server_table(config: &McpServerConfig) -> anyhow::Result<TomlTable> {
|
||||
let mut entry = TomlTable::new();
|
||||
entry.set_implicit(false);
|
||||
|
||||
@@ -175,15 +176,15 @@ fn serialize_mcp_server_table(config: &McpServerConfig) -> TomlTable {
|
||||
let mut tool_entries: Vec<_> = config.tools.iter().collect();
|
||||
tool_entries.sort_by_key(|(name, _)| *name);
|
||||
for (name, tool_config) in tool_entries {
|
||||
tools.insert(name, serialize_mcp_server_tool(tool_config));
|
||||
tools.insert(name, serialize_mcp_server_tool(tool_config)?);
|
||||
}
|
||||
entry.insert("tools", TomlItem::Table(tools));
|
||||
}
|
||||
|
||||
entry
|
||||
Ok(entry)
|
||||
}
|
||||
|
||||
fn serialize_mcp_server_tool(config: &McpServerToolConfig) -> TomlItem {
|
||||
fn serialize_mcp_server_tool(config: &McpServerToolConfig) -> anyhow::Result<TomlItem> {
|
||||
let mut entry = TomlTable::new();
|
||||
entry.set_implicit(false);
|
||||
if let Some(approval_mode) = config.approval_mode {
|
||||
@@ -194,15 +195,21 @@ fn serialize_mcp_server_tool(config: &McpServerToolConfig) -> TomlItem {
|
||||
AppToolApproval::Approve => "approve",
|
||||
});
|
||||
}
|
||||
TomlItem::Table(entry)
|
||||
if let Some(output_token_limit) = config.output_token_limit {
|
||||
entry["output_token_limit"] = value(
|
||||
i64::try_from(output_token_limit.get())
|
||||
.context("output_token_limit exceeds the TOML integer range")?,
|
||||
);
|
||||
}
|
||||
Ok(TomlItem::Table(entry))
|
||||
}
|
||||
|
||||
pub(super) fn serialize_mcp_server(config: &McpServerConfig) -> TomlItem {
|
||||
TomlItem::Table(serialize_mcp_server_table(config))
|
||||
pub(super) fn serialize_mcp_server(config: &McpServerConfig) -> anyhow::Result<TomlItem> {
|
||||
Ok(TomlItem::Table(serialize_mcp_server_table(config)?))
|
||||
}
|
||||
|
||||
pub(super) fn serialize_mcp_server_inline(config: &McpServerConfig) -> InlineTable {
|
||||
serialize_mcp_server_table(config).into_inline_table()
|
||||
pub(super) fn serialize_mcp_server_inline(config: &McpServerConfig) -> anyhow::Result<InlineTable> {
|
||||
Ok(serialize_mcp_server_table(config)?.into_inline_table())
|
||||
}
|
||||
|
||||
pub(super) fn merge_inline_table(existing: &mut InlineTable, replacement: InlineTable) {
|
||||
|
||||
@@ -1161,8 +1161,9 @@ B = \"2\"
|
||||
assert_eq!(raw, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocking_replace_mcp_servers_serializes_tool_approval_overrides() {
|
||||
#[test_case::test_case(30_000; "positive limit")]
|
||||
#[test_case::test_case(usize::MAX; "largest native limit")]
|
||||
fn blocking_replace_mcp_servers_serializes_tool_approval_overrides(output_token_limit: usize) {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
let codex_home = tmp.path();
|
||||
|
||||
@@ -1196,22 +1197,36 @@ fn blocking_replace_mcp_servers_serializes_tool_approval_overrides() {
|
||||
"search".to_string(),
|
||||
McpServerToolConfig {
|
||||
approval_mode: Some(AppToolApproval::Approve),
|
||||
output_token_limit: std::num::NonZeroUsize::new(output_token_limit),
|
||||
},
|
||||
)]),
|
||||
},
|
||||
);
|
||||
|
||||
apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist");
|
||||
let result = apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]);
|
||||
if i64::try_from(output_token_limit).is_err() {
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("output_token_limit exceeds the TOML integer range")
|
||||
);
|
||||
return;
|
||||
}
|
||||
result.expect("persist");
|
||||
|
||||
let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let expected = "\
|
||||
let expected = format!(
|
||||
"\
|
||||
[mcp_servers.docs]
|
||||
command = \"docs-server\"
|
||||
default_tools_approval_mode = \"prompt\"
|
||||
|
||||
[mcp_servers.docs.tools.search]
|
||||
approval_mode = \"approve\"
|
||||
";
|
||||
output_token_limit = {output_token_limit}
|
||||
"
|
||||
);
|
||||
assert_eq!(raw, expected);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user