Append MCP instructions to deferred code-mode tools

This commit is contained in:
Sayan Sisodiya
2026-06-12 12:38:20 -07:00
parent b6baa77eec
commit 28cb5b6bb1
2 changed files with 116 additions and 3 deletions
+32 -2
View File
@@ -85,6 +85,7 @@ use codex_tools::request_user_input_available_modes;
use codex_tools::shell_command_backend_for_features;
use codex_tools::shell_type_for_model_and_features;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Arc;
use tracing::instrument;
@@ -426,6 +427,7 @@ fn is_excluded_from_code_mode(turn_context: &TurnContext, tool_name: &ToolName)
fn build_code_mode_executors(
turn_context: &TurnContext,
executors: &[Arc<dyn CoreToolRuntime>],
deferred_mcp_tools: Option<&[ToolInfo]>,
) -> Vec<Arc<dyn CoreToolRuntime>> {
if !matches!(
turn_context.tool_mode,
@@ -438,6 +440,17 @@ fn build_code_mode_executors(
let mut exec_prompt_tool_specs = Vec::new();
let mut deferred_tools_available = false;
let deferred_tools_guidance_enabled = search_tool_enabled(turn_context);
let deferred_mcp_instructions = deferred_mcp_tools
.unwrap_or_default()
.iter()
.filter_map(|tool| {
tool.namespace_description
.as_deref()
.map(str::trim)
.filter(|instructions| !instructions.is_empty())
.map(|instructions| (tool.canonical_tool_name(), instructions))
})
.collect::<HashMap<_, _>>();
for executor in executors {
let exposure = executor.exposure();
if exposure == ToolExposure::DirectModelOnly {
@@ -452,9 +465,22 @@ fn build_code_mode_executors(
continue;
}
let spec = executor.spec();
let mut spec = executor.spec();
if exposure == ToolExposure::Deferred {
if let Some(instructions) = deferred_mcp_instructions.get(&executor.tool_name())
&& let ToolSpec::Namespace(namespace) = &mut spec
{
for tool in &mut namespace.tools {
let ResponsesApiNamespaceTool::Function(tool) = tool;
if tool.description.trim().is_empty() {
tool.description = (*instructions).to_string();
} else {
tool.description =
format!("{}\n\n{instructions}", tool.description.trim_end());
}
}
}
// Only show deferred-tool guidance when supported and an included spec is usable by code mode.
deferred_tools_available |= deferred_tools_guidance_enabled
&& !collect_code_mode_exec_prompt_tool_definitions(std::iter::once(&spec))
@@ -864,7 +890,11 @@ fn prepend_code_mode_executors(
planned_tools: &mut PlannedTools,
) {
let turn_context = context.turn_context;
let code_mode_executors = build_code_mode_executors(turn_context, planned_tools.runtimes());
let code_mode_executors = build_code_mode_executors(
turn_context,
planned_tools.runtimes(),
context.deferred_mcp_tools,
);
planned_tools.runtimes.splice(0..0, code_mode_executors);
}
+84 -1
View File
@@ -330,8 +330,13 @@ async fn run_code_mode_turn_with_rmcp_model(
model: &'static str,
) -> Result<(TestCodex, ResponseMock)> {
run_code_mode_turn_with_rmcp_config(
server, prompt, code, model, /*code_mode_only*/ false,
server,
prompt,
code,
model,
/*code_mode_only*/ false,
/*non_prefixed_mcp_tool_names*/ false,
McpToolLoading::Direct,
)
.await
}
@@ -349,10 +354,17 @@ async fn run_code_mode_turn_with_rmcp_mode(
"test-gpt-5.1-codex",
code_mode_only,
/*non_prefixed_mcp_tool_names*/ false,
McpToolLoading::Direct,
)
.await
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum McpToolLoading {
Direct,
Deferred,
}
async fn run_code_mode_turn_with_rmcp_config(
server: &MockServer,
prompt: &str,
@@ -360,6 +372,7 @@ async fn run_code_mode_turn_with_rmcp_config(
model: &'static str,
code_mode_only: bool,
non_prefixed_mcp_tool_names: bool,
tool_loading: McpToolLoading,
) -> Result<(TestCodex, ResponseMock)> {
let rmcp_test_server_bin = stdio_server_bin()?;
let mut builder = test_codex().with_model(model).with_config(move |config| {
@@ -371,6 +384,21 @@ async fn run_code_mode_turn_with_rmcp_config(
if non_prefixed_mcp_tool_names {
let _ = config.features.enable(Feature::NonPrefixedMcpToolNames);
}
if tool_loading == McpToolLoading::Deferred {
config
.features
.enable(Feature::ToolSearchAlwaysDeferMcpTools)
.expect("test config should allow feature update");
let mut model_catalog = bundled_models_response()
.unwrap_or_else(|err| panic!("bundled models.json should parse: {err}"));
let model = model_catalog
.models
.iter_mut()
.find(|model_info| model_info.slug == model)
.unwrap_or_else(|| panic!("{model} exists in bundled models.json"));
model.supports_search_tool = true;
config.model_catalog = Some(model_catalog);
}
let mut servers = config.mcp_servers.get().clone();
servers.insert(
@@ -3019,6 +3047,7 @@ text(JSON.stringify({
"test-gpt-5.1-codex",
/*code_mode_only*/ false,
/*non_prefixed_mcp_tool_names*/ true,
McpToolLoading::Direct,
)
.await?;
@@ -3299,6 +3328,60 @@ text(JSON.stringify(tool));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn code_mode_appends_server_instructions_to_deferred_mcp_tool_descriptions() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let code = r#"
const tool = ALL_TOOLS.find(
({ name }) => name === "mcp__rmcp__echo"
);
text(JSON.stringify(tool));
"#;
let (_test, second_mock) = run_code_mode_turn_with_rmcp_config(
&server,
"use exec to inspect ALL_TOOLS",
code,
"gpt-5.4",
/*code_mode_only*/ true,
/*non_prefixed_mcp_tool_names*/ false,
McpToolLoading::Deferred,
)
.await?;
let req = second_mock.single_request();
let (output, success) = custom_tool_output_body_and_success(&req, "call-1");
assert_ne!(
success,
Some(false),
"exec ALL_TOOLS MCP lookup failed unexpectedly: {output}"
);
let parsed: Value = serde_json::from_str(
&custom_tool_output_last_non_empty_text(&req, "call-1")
.expect("exec ALL_TOOLS lookup should emit JSON"),
)?;
assert_eq!(
parsed,
serde_json::json!({
"name": "mcp__rmcp__echo",
"description": concat!(
"Echo back the provided message and include environment data.\n\n",
"Use these tools to exercise the rmcp test server.\n\n",
"exec tool declaration:\n",
"```ts\n",
"declare const tools: { mcp__rmcp__echo(args: { env_var?: string; message: string; }): ",
"Promise<CallToolResult<{ echo: string; env: string | null; }>>; };\n",
"```",
),
})
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn code_mode_can_call_hidden_dynamic_tools() -> Result<()> {
skip_if_no_network!(Ok(()));