diff --git a/codex-rs/core/src/mcp_tool_exposure.rs b/codex-rs/core/src/mcp_tool_exposure.rs index 3917a1d5e7..f65d68d994 100644 --- a/codex-rs/core/src/mcp_tool_exposure.rs +++ b/codex-rs/core/src/mcp_tool_exposure.rs @@ -9,12 +9,12 @@ use codex_tools::ToolsConfig; use crate::config::Config; use crate::connectors; +use crate::tools::mcp_tool_input::McpToolInput; pub(crate) const DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD: usize = 100; pub(crate) struct McpToolExposure { - pub(crate) direct_tools: HashMap, - pub(crate) deferred_tools: Option>, + pub(crate) tools: HashMap, } pub(crate) fn build_mcp_tool_exposure( @@ -24,9 +24,9 @@ pub(crate) fn build_mcp_tool_exposure( config: &Config, tools_config: &ToolsConfig, ) -> McpToolExposure { - let mut deferred_tools = filter_non_codex_apps_mcp_tools_only(all_mcp_tools); + let mut candidate_tools = filter_non_codex_apps_mcp_tools_only(all_mcp_tools); if let Some(connectors) = connectors { - deferred_tools.extend(filter_codex_apps_mcp_tools( + candidate_tools.extend(filter_codex_apps_mcp_tools( all_mcp_tools, connectors, config, @@ -37,25 +37,49 @@ pub(crate) fn build_mcp_tool_exposure( && (config .features .enabled(Feature::ToolSearchAlwaysDeferMcpTools) - || deferred_tools.len() >= DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD); + || candidate_tools.len() >= DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD); if !should_defer { return McpToolExposure { - direct_tools: deferred_tools, - deferred_tools: None, + tools: candidate_tools + .into_iter() + .map(|(name, tool_info)| { + ( + name, + McpToolInput { + tool_info, + defer_loading: false, + }, + ) + }) + .collect(), }; } let direct_tools = filter_codex_apps_mcp_tools(all_mcp_tools, explicitly_enabled_connectors, config); - for direct_tool_name in direct_tools.keys() { - deferred_tools.remove(direct_tool_name); + let mut tools = HashMap::new(); + for (name, tool_info) in direct_tools { + candidate_tools.remove(&name); + tools.insert( + name, + McpToolInput { + tool_info, + defer_loading: false, + }, + ); + } + for (name, tool_info) in candidate_tools { + tools.insert( + name, + McpToolInput { + tool_info, + defer_loading: true, + }, + ); } - McpToolExposure { - direct_tools, - deferred_tools: (!deferred_tools.is_empty()).then_some(deferred_tools), - } + McpToolExposure { tools } } fn filter_codex_apps_mcp_tools( diff --git a/codex-rs/core/src/mcp_tool_exposure_test.rs b/codex-rs/core/src/mcp_tool_exposure_test.rs index cbd4d3b29c..01f38c1d84 100644 --- a/codex-rs/core/src/mcp_tool_exposure_test.rs +++ b/codex-rs/core/src/mcp_tool_exposure_test.rs @@ -91,6 +91,16 @@ fn numbered_mcp_tools(count: usize) -> HashMap { .collect() } +fn tool_names_with_defer_loading(exposure: &McpToolExposure, defer_loading: bool) -> Vec { + let mut names = exposure + .tools + .iter() + .filter_map(|(name, tool)| (tool.defer_loading == defer_loading).then_some(name.clone())) + .collect::>(); + names.sort(); + names +} + async fn tools_config_for_mcp_tool_exposure(search_tool: bool) -> ToolsConfig { let config = test_config().await; let model_info = @@ -112,7 +122,7 @@ async fn tools_config_for_mcp_tool_exposure(search_tool: bool) -> ToolsConfig { } #[tokio::test] -async fn directly_exposes_small_effective_tool_sets() { +async fn directly_exposes_small_candidate_tool_sets() { let config = test_config().await; let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await; let mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD - 1); @@ -125,16 +135,15 @@ async fn directly_exposes_small_effective_tool_sets() { &tools_config, ); - let mut direct_tool_names: Vec<_> = exposure.direct_tools.keys().cloned().collect(); - direct_tool_names.sort(); + let direct_tool_names = tool_names_with_defer_loading(&exposure, /*defer_loading*/ false); let mut expected_tool_names: Vec<_> = mcp_tools.keys().cloned().collect(); expected_tool_names.sort(); assert_eq!(direct_tool_names, expected_tool_names); - assert!(exposure.deferred_tools.is_none()); + assert!(exposure.tools.values().all(|tool| !tool.defer_loading)); } #[tokio::test] -async fn searches_large_effective_tool_sets() { +async fn searches_large_candidate_tool_sets() { let config = test_config().await; let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await; let mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD); @@ -147,13 +156,7 @@ async fn searches_large_effective_tool_sets() { &tools_config, ); - assert!(exposure.direct_tools.is_empty()); - let deferred_tools = exposure - .deferred_tools - .as_ref() - .expect("large tool sets should be discoverable through tool_search"); - let mut deferred_tool_names: Vec<_> = deferred_tools.keys().cloned().collect(); - deferred_tool_names.sort(); + let deferred_tool_names = tool_names_with_defer_loading(&exposure, /*defer_loading*/ true); let mut expected_tool_names: Vec<_> = mcp_tools.keys().cloned().collect(); expected_tool_names.sort(); assert_eq!(deferred_tool_names, expected_tool_names); @@ -183,28 +186,24 @@ async fn directly_exposes_explicit_apps_without_deferred_overlap() { &tools_config, ); - let mut tool_names: Vec = exposure.direct_tools.into_keys().collect(); - tool_names.sort(); + let tool_names = tool_names_with_defer_loading(&exposure, /*defer_loading*/ false); assert_eq!( tool_names, vec!["mcp__codex_apps__calendar_create_event".to_string()] ); + let deferred_tool_names = tool_names_with_defer_loading(&exposure, /*defer_loading*/ true); assert_eq!( - exposure.deferred_tools.as_ref().map(HashMap::len), - Some(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD - 1) + deferred_tool_names.len(), + DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD - 1 ); - let deferred_tools = exposure - .deferred_tools - .as_ref() - .expect("large tool sets should be discoverable through tool_search"); assert!( tool_names .iter() - .all(|direct_tool_name| !deferred_tools.contains_key(direct_tool_name)), + .all(|direct_tool_name| !deferred_tool_names.contains(direct_tool_name)), "direct tools should not also be deferred: {tool_names:?}" ); - assert!(!deferred_tools.contains_key("mcp__codex_apps__calendar_create_event")); - assert!(deferred_tools.contains_key("mcp__rmcp__tool_0")); + assert!(!deferred_tool_names.contains(&"mcp__codex_apps__calendar_create_event".to_string())); + assert!(deferred_tool_names.contains(&"mcp__rmcp__tool_0".to_string())); } #[tokio::test] @@ -242,16 +241,12 @@ async fn always_defer_feature_preserves_explicit_apps() { &tools_config, ); - let mut direct_tool_names: Vec = exposure.direct_tools.into_keys().collect(); - direct_tool_names.sort(); + let direct_tool_names = tool_names_with_defer_loading(&exposure, /*defer_loading*/ false); assert_eq!( direct_tool_names, vec!["mcp__codex_apps__calendar_create_event".to_string()] ); - let deferred_tools = exposure - .deferred_tools - .as_ref() - .expect("MCP tools should be discoverable through tool_search"); - assert!(deferred_tools.contains_key("mcp__rmcp__tool")); - assert!(!deferred_tools.contains_key("mcp__codex_apps__calendar_create_event")); + let deferred_tool_names = tool_names_with_defer_loading(&exposure, /*defer_loading*/ true); + assert!(deferred_tool_names.contains(&"mcp__rmcp__tool".to_string())); + assert!(!deferred_tool_names.contains(&"mcp__codex_apps__calendar_create_event".to_string())); } diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 283220e8fa..1c8a271348 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -463,7 +463,6 @@ fn test_tool_runtime(session: Arc, turn_context: Arc) -> T &turn_context.tools_config, crate::tools::router::ToolRouterParams { mcp_tools: None, - deferred_mcp_tools: None, unavailable_called_tools: Vec::new(), parallel_mcp_server_names: HashSet::new(), discoverable_tools: None, @@ -7359,11 +7358,21 @@ async fn fatal_tool_error_stops_turn_and_reports_error() { .list_all_tools() .await }; - let deferred_mcp_tools = Some(tools.clone()); + let tools = tools + .into_iter() + .map(|(name, tool_info)| { + ( + name, + crate::tools::mcp_tool_input::McpToolInput { + tool_info, + defer_loading: false, + }, + ) + }) + .collect(); let router = ToolRouter::from_config( &turn_context.tools_config, crate::tools::router::ToolRouterParams { - deferred_mcp_tools, mcp_tools: Some(tools), unavailable_called_tools: Vec::new(), parallel_mcp_server_names: HashSet::new(), diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 41195490b6..18cb6d1d07 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -1203,8 +1203,7 @@ pub(crate) async fn built_tools( &turn_context.config, &turn_context.tools_config, ); - let mcp_tools = has_mcp_servers.then_some(mcp_tool_exposure.direct_tools); - let deferred_mcp_tools = mcp_tool_exposure.deferred_tools; + let mcp_tools = has_mcp_servers.then_some(mcp_tool_exposure.tools); let unavailable_called_tools = if turn_context .config .features @@ -1212,7 +1211,6 @@ pub(crate) async fn built_tools( { let exposed_tool_names = mcp_tools .iter() - .chain(deferred_mcp_tools.iter()) .flat_map(|tools| tools.keys().map(String::as_str)) .collect::>(); collect_unavailable_called_tools(input, &exposed_tool_names) @@ -1236,7 +1234,6 @@ pub(crate) async fn built_tools( &turn_context.tools_config, ToolRouterParams { mcp_tools, - deferred_mcp_tools, unavailable_called_tools, parallel_mcp_server_names, discoverable_tools, diff --git a/codex-rs/core/src/tools/code_mode/mod.rs b/codex-rs/core/src/tools/code_mode/mod.rs index 0bfd080ae0..61205c427f 100644 --- a/codex-rs/core/src/tools/code_mode/mod.rs +++ b/codex-rs/core/src/tools/code_mode/mod.rs @@ -24,6 +24,7 @@ use crate::tools::ToolRouter; use crate::tools::context::FunctionToolOutput; use crate::tools::context::SharedTurnDiffTracker; use crate::tools::context::ToolPayload; +use crate::tools::mcp_tool_input::McpToolInput; use crate::tools::parallel::ToolCallRuntime; use crate::tools::router::ToolCall; use crate::tools::router::ToolCallSource; @@ -262,7 +263,7 @@ pub(super) async fn build_enabled_tools( exec: &ExecContext, ) -> Vec { let router = build_nested_router(exec).await; - let specs = router.specs(); + let specs = router.specs_including_deferred(); collect_code_mode_tool_definitions(&specs) } @@ -280,6 +281,18 @@ async fn build_nested_router(exec: &ExecContext) -> ToolRouter { .await .list_all_tools() .await; + let listed_mcp_tools = listed_mcp_tools + .into_iter() + .map(|(name, tool_info)| { + ( + name, + McpToolInput { + tool_info, + defer_loading: false, + }, + ) + }) + .collect(); let parallel_mcp_server_names = exec .turn .config @@ -296,7 +309,6 @@ async fn build_nested_router(exec: &ExecContext) -> ToolRouter { ToolRouter::from_config( &nested_tools_config, ToolRouterParams { - deferred_mcp_tools: None, mcp_tools: Some(listed_mcp_tools), unavailable_called_tools: Vec::new(), parallel_mcp_server_names, @@ -324,8 +336,18 @@ async fn call_nested_tool( ))); } + let actual_kind = match tool_kind_for_name(tool_runtime.find_spec(&tool_name), &tool_name) { + Ok(kind) => kind, + Err(error) => return Err(FunctionCallError::RespondToModel(error)), + }; + let (tool_call_name, payload) = if let Some(tool_info) = exec.session.resolve_mcp_tool_info(&tool_name).await { + if actual_kind != codex_code_mode::CodeModeToolKind::Function { + return Err(FunctionCallError::RespondToModel(format!( + "MCP tool `{tool_name}` must be invoked with function arguments" + ))); + } let raw_arguments = match serialize_function_tool_arguments(&tool_name, input) { Ok(raw_arguments) => raw_arguments, Err(error) => return Err(FunctionCallError::RespondToModel(error)), @@ -339,7 +361,7 @@ async fn call_nested_tool( }, ) } else { - match build_nested_tool_payload(tool_runtime.find_spec(&tool_name), &tool_name, input) { + match build_nested_tool_payload(actual_kind, &tool_name, input) { Ok(payload) => (tool_name, payload), Err(error) => return Err(FunctionCallError::RespondToModel(error)), } @@ -381,11 +403,10 @@ fn tool_kind_for_name( } fn build_nested_tool_payload( - spec: Option, + actual_kind: codex_code_mode::CodeModeToolKind, tool_name: &ToolName, input: Option, ) -> Result { - let actual_kind = tool_kind_for_name(spec, tool_name)?; match actual_kind { codex_code_mode::CodeModeToolKind::Function => { build_function_tool_payload(tool_name, input) diff --git a/codex-rs/core/src/tools/handlers/tool_search.rs b/codex-rs/core/src/tools/handlers/tool_search.rs index f38b4ee883..0733c2cd1d 100644 --- a/codex-rs/core/src/tools/handlers/tool_search.rs +++ b/codex-rs/core/src/tools/handlers/tool_search.rs @@ -173,6 +173,7 @@ fn default_limit_for_bucket(bucket: &str) -> usize { #[cfg(test)] mod tests { use super::*; + use crate::tools::mcp_tool_input::McpToolInput; use crate::tools::tool_search_entry::build_tool_search_entries; use codex_mcp::ToolInfo; use codex_protocol::dynamic_tools::DynamicToolSpec; @@ -239,7 +240,9 @@ mod tests { /*required*/ None, Some(false.into()), ), - output_schema: None, + output_schema: Some(codex_tools::mcp_call_tool_result_output_schema( + serde_json::json!({}), + )), }), ResponsesApiNamespaceTool::Function(ResponsesApiTool { name: "list_events".to_string(), @@ -251,7 +254,9 @@ mod tests { /*required*/ None, Some(false.into()), ), - output_schema: None, + output_schema: Some(codex_tools::mcp_call_tool_result_output_schema( + serde_json::json!({}), + )), }), ], }), @@ -426,6 +431,23 @@ mod tests { mcp_tools: Option<&std::collections::HashMap>, dynamic_tools: &[DynamicToolSpec], ) -> ToolSearchHandler { - ToolSearchHandler::new(build_tool_search_entries(mcp_tools, dynamic_tools)) + let exposed_mcp_tools = mcp_tools.map(|mcp_tools| { + mcp_tools + .iter() + .map(|(name, tool_info)| { + ( + name.clone(), + McpToolInput { + tool_info: tool_info.clone(), + defer_loading: true, + }, + ) + }) + .collect::>() + }); + ToolSearchHandler::new(build_tool_search_entries( + exposed_mcp_tools.as_ref(), + dynamic_tools, + )) } } diff --git a/codex-rs/core/src/tools/mcp_tool_input.rs b/codex-rs/core/src/tools/mcp_tool_input.rs new file mode 100644 index 0000000000..32eb501a88 --- /dev/null +++ b/codex-rs/core/src/tools/mcp_tool_input.rs @@ -0,0 +1,7 @@ +use codex_mcp::ToolInfo; + +#[derive(Clone, Debug)] +pub(crate) struct McpToolInput { + pub(crate) tool_info: ToolInfo, + pub(crate) defer_loading: bool, +} diff --git a/codex-rs/core/src/tools/mod.rs b/codex-rs/core/src/tools/mod.rs index 659a7d3e54..8d8d862264 100644 --- a/codex-rs/core/src/tools/mod.rs +++ b/codex-rs/core/src/tools/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod context; pub(crate) mod events; pub(crate) mod handlers; pub(crate) mod hook_names; +pub(crate) mod mcp_tool_input; pub(crate) mod network_approval; pub(crate) mod orchestrator; pub(crate) mod parallel; diff --git a/codex-rs/core/src/tools/router.rs b/codex-rs/core/src/tools/router.rs index aeba3b0556..c1aa9c9f4d 100644 --- a/codex-rs/core/src/tools/router.rs +++ b/codex-rs/core/src/tools/router.rs @@ -5,11 +5,11 @@ use crate::session::turn_context::TurnContext; use crate::tools::context::SharedTurnDiffTracker; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; +use crate::tools::mcp_tool_input::McpToolInput; use crate::tools::registry::AnyToolResult; use crate::tools::registry::ToolArgumentDiffConsumer; use crate::tools::registry::ToolRegistry; use crate::tools::spec::build_specs_with_discoverable_tools; -use codex_mcp::ToolInfo; use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::models::LocalShellAction; use codex_protocol::models::ResponseItem; @@ -44,8 +44,7 @@ pub struct ToolRouter { } pub(crate) struct ToolRouterParams<'a> { - pub(crate) mcp_tools: Option>, - pub(crate) deferred_mcp_tools: Option>, + pub(crate) mcp_tools: Option>, pub(crate) unavailable_called_tools: Vec, pub(crate) parallel_mcp_server_names: HashSet, pub(crate) discoverable_tools: Option>, @@ -56,7 +55,6 @@ impl ToolRouter { pub fn from_config(config: &ToolsConfig, params: ToolRouterParams<'_>) -> Self { let ToolRouterParams { mcp_tools, - deferred_mcp_tools, unavailable_called_tools, parallel_mcp_server_names, discoverable_tools, @@ -65,17 +63,11 @@ impl ToolRouter { let builder = build_specs_with_discoverable_tools( config, mcp_tools, - deferred_mcp_tools, unavailable_called_tools, discoverable_tools, dynamic_tools, ); let (specs, registry) = builder.build(); - let deferred_dynamic_tools = dynamic_tools - .iter() - .filter(|tool| tool.defer_loading) - .map(|tool| ToolName::new(tool.namespace.clone(), tool.name.clone())) - .collect::>(); let model_visible_specs = specs .iter() .filter_map(|configured_tool| { @@ -85,10 +77,7 @@ impl ToolRouter { return None; } - filter_deferred_dynamic_tool_spec( - configured_tool.spec.clone(), - &deferred_dynamic_tools, - ) + filter_deferred_tool_spec(configured_tool.spec.clone()) }) .collect(); @@ -100,7 +89,7 @@ impl ToolRouter { } } - pub fn specs(&self) -> Vec { + pub fn specs_including_deferred(&self) -> Vec { self.specs .iter() .map(|config| config.spec.clone()) @@ -164,8 +153,7 @@ impl ToolRouter { pub fn tool_supports_parallel(&self, call: &ToolCall) -> bool { match &call.payload { - // MCP parallel support is configured per server, including for deferred - // tools that may not have a matching spec entry. Use the parsed payload + // MCP parallel support is configured per server. Use the parsed payload // server so similarly named servers/tools cannot collide. ToolPayload::Mcp { server, .. } => self.parallel_mcp_server_names.contains(server), _ => self.configured_tool_supports_parallel(&call.tool_name), @@ -297,28 +285,18 @@ impl ToolRouter { } } -fn filter_deferred_dynamic_tool_spec( - spec: ToolSpec, - deferred_dynamic_tools: &HashSet, -) -> Option { - if deferred_dynamic_tools.is_empty() { - return Some(spec); - } - +fn filter_deferred_tool_spec(spec: ToolSpec) -> Option { match spec { ToolSpec::Function(tool) => { - if deferred_dynamic_tools.contains(&ToolName::plain(tool.name.as_str())) { + if tool.defer_loading == Some(true) { None } else { Some(ToolSpec::Function(tool)) } } ToolSpec::Namespace(mut namespace) => { - let namespace_name = namespace.name.clone(); namespace.tools.retain(|tool| match tool { - ResponsesApiNamespaceTool::Function(tool) => !deferred_dynamic_tools.contains( - &ToolName::namespaced(namespace_name.as_str(), tool.name.as_str()), - ), + ResponsesApiNamespaceTool::Function(tool) => tool.defer_loading != Some(true), }); if namespace.tools.is_empty() { None diff --git a/codex-rs/core/src/tools/router_tests.rs b/codex-rs/core/src/tools/router_tests.rs index ac859d9aa6..05127b980c 100644 --- a/codex-rs/core/src/tools/router_tests.rs +++ b/codex-rs/core/src/tools/router_tests.rs @@ -1,8 +1,11 @@ +use std::collections::HashMap; use std::collections::HashSet; use std::sync::Arc; use crate::session::tests::make_session_and_context; use crate::tools::context::ToolPayload; +use crate::tools::mcp_tool_input::McpToolInput; +use codex_mcp::ToolInfo; use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::models::ResponseItem; use codex_tools::ResponsesApiNamespaceTool; @@ -29,10 +32,10 @@ async fn parallel_support_does_not_match_namespaced_local_tool_names() -> anyhow .await .list_all_tools() .await; + let mcp_tools = expose_mcp_tools(mcp_tools, /*defer_loading*/ false); let router = ToolRouter::from_config( &turn.tools_config, ToolRouterParams { - deferred_mcp_tools: None, mcp_tools: Some(mcp_tools), unavailable_called_tools: Vec::new(), parallel_mcp_server_names: HashSet::new(), @@ -105,7 +108,6 @@ async fn mcp_parallel_support_uses_exact_payload_server() -> anyhow::Result<()> let router = ToolRouter::from_config( &turn.tools_config, ToolRouterParams { - deferred_mcp_tools: None, mcp_tools: None, unavailable_called_tools: Vec::new(), parallel_mcp_server_names: HashSet::from(["echo".to_string()]), @@ -172,7 +174,6 @@ async fn model_visible_specs_filter_deferred_dynamic_tools() -> anyhow::Result<( let router = ToolRouter::from_config( &turn.tools_config, ToolRouterParams { - deferred_mcp_tools: None, mcp_tools: None, unavailable_called_tools: Vec::new(), parallel_mcp_server_names: HashSet::new(), @@ -187,7 +188,7 @@ async fn model_visible_specs_filter_deferred_dynamic_tools() -> anyhow::Result<( .is_some() ); assert_eq!( - namespace_function_names(&router.specs(), "codex_app"), + namespace_function_names(&router.specs_including_deferred(), "codex_app"), vec![hidden_tool.to_string(), visible_tool.to_string()] ); assert_eq!( @@ -198,6 +199,100 @@ async fn model_visible_specs_filter_deferred_dynamic_tools() -> anyhow::Result<( Ok(()) } +#[tokio::test] +async fn model_visible_specs_filter_deferred_mcp_tools() -> anyhow::Result<()> { + let (_, turn) = make_session_and_context().await; + let hidden_tool = "hidden_mcp_tool"; + let visible_tool = "visible_mcp_tool"; + let mcp_tools = HashMap::from([ + ( + format!("mcp__test__{hidden_tool}"), + exposed_mcp_tool(hidden_tool, /*defer_loading*/ true), + ), + ( + format!("mcp__test__{visible_tool}"), + exposed_mcp_tool(visible_tool, /*defer_loading*/ false), + ), + ]); + + let router = ToolRouter::from_config( + &turn.tools_config, + ToolRouterParams { + mcp_tools: Some(mcp_tools), + unavailable_called_tools: Vec::new(), + parallel_mcp_server_names: HashSet::new(), + discoverable_tools: None, + dynamic_tools: &[], + }, + ); + + assert!( + router + .find_spec(&ToolName::namespaced("mcp__test__", hidden_tool)) + .is_some() + ); + assert_eq!( + namespace_function_names(&router.specs_including_deferred(), "mcp__test__"), + vec![hidden_tool.to_string(), visible_tool.to_string()] + ); + assert_eq!( + namespace_function_names(&router.model_visible_specs(), "mcp__test__"), + vec![visible_tool.to_string()] + ); + + Ok(()) +} + +fn expose_mcp_tools( + mcp_tools: HashMap, + defer_loading: bool, +) -> HashMap { + mcp_tools + .into_iter() + .map(|(name, tool_info)| { + ( + name, + McpToolInput { + tool_info, + defer_loading, + }, + ) + }) + .collect() +} + +fn exposed_mcp_tool(tool_name: &str, defer_loading: bool) -> McpToolInput { + let tool = rmcp::model::Tool { + name: tool_name.to_string().into(), + title: None, + description: Some(format!("Test MCP tool {tool_name}").into()), + input_schema: std::sync::Arc::new(rmcp::model::object(json!({ + "type": "object", + "properties": {}, + "additionalProperties": false, + }))), + output_schema: None, + annotations: None, + execution: None, + icons: None, + meta: None, + }; + McpToolInput { + tool_info: ToolInfo { + server_name: "test".to_string(), + callable_name: tool_name.to_string(), + callable_namespace: "mcp__test__".to_string(), + server_instructions: None, + tool, + connector_id: None, + connector_name: None, + plugin_display_names: Vec::new(), + connector_description: None, + }, + defer_loading, + } +} + fn namespace_function_names(specs: &[ToolSpec], namespace_name: &str) -> Vec { specs .iter() diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index ebbc38b8be..fb14b18aaf 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -4,8 +4,8 @@ use crate::tools::handlers::agent_jobs::BatchJobHandler; use crate::tools::handlers::multi_agents_common::DEFAULT_WAIT_TIMEOUT_MS; use crate::tools::handlers::multi_agents_common::MAX_WAIT_TIMEOUT_MS; use crate::tools::handlers::multi_agents_common::MIN_WAIT_TIMEOUT_MS; +use crate::tools::mcp_tool_input::McpToolInput; use crate::tools::registry::ToolRegistryBuilder; -use codex_mcp::ToolInfo; use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_tools::AdditionalProperties; use codex_tools::DiscoverableTool; @@ -14,7 +14,6 @@ use codex_tools::ResponsesApiTool; use codex_tools::ToolHandlerKind; use codex_tools::ToolName; use codex_tools::ToolNamespace; -use codex_tools::ToolRegistryPlanDeferredTool; use codex_tools::ToolRegistryPlanMcpTool; use codex_tools::ToolRegistryPlanParams; use codex_tools::ToolUserShellType; @@ -41,18 +40,26 @@ struct McpToolPlanInputs<'a> { tool_namespaces: HashMap, } -fn map_mcp_tools_for_plan(mcp_tools: &HashMap) -> McpToolPlanInputs<'_> { +fn map_mcp_tools_for_plan(mcp_tools: &HashMap) -> McpToolPlanInputs<'_> { McpToolPlanInputs { mcp_tools: mcp_tools .values() - .map(|tool| ToolRegistryPlanMcpTool { - name: tool.canonical_tool_name(), - tool: &tool.tool, + .map(|exposed_tool| { + let tool = &exposed_tool.tool_info; + ToolRegistryPlanMcpTool { + name: tool.canonical_tool_name(), + tool: &tool.tool, + server_name: tool.server_name.as_str(), + connector_name: tool.connector_name.as_deref(), + connector_description: tool.connector_description.as_deref(), + defer_loading: exposed_tool.defer_loading, + } }) .collect(), tool_namespaces: mcp_tools .values() - .map(|tool| { + .map(|exposed_tool| { + let tool = &exposed_tool.tool_info; ( tool.callable_namespace.clone(), ToolNamespace { @@ -70,8 +77,7 @@ fn map_mcp_tools_for_plan(mcp_tools: &HashMap) -> McpToolPlanI pub(crate) fn build_specs_with_discoverable_tools( config: &ToolsConfig, - mcp_tools: Option>, - deferred_mcp_tools: Option>, + mcp_tools: Option>, unavailable_called_tools: Vec, discoverable_tools: Option>, dynamic_tools: &[DynamicToolSpec], @@ -111,17 +117,6 @@ pub(crate) fn build_specs_with_discoverable_tools( let mut builder = ToolRegistryBuilder::new(); let mcp_tool_plan_inputs = mcp_tools.as_ref().map(map_mcp_tools_for_plan); - let deferred_mcp_tool_sources = deferred_mcp_tools.as_ref().map(|tools| { - tools - .values() - .map(|tool| ToolRegistryPlanDeferredTool { - name: tool.canonical_tool_name(), - server_name: tool.server_name.as_str(), - connector_name: tool.connector_name.as_deref(), - connector_description: tool.connector_description.as_deref(), - }) - .collect::>() - }); let default_agent_type_description = crate::agent::role::spawn_tool_spec::build(&std::collections::BTreeMap::new()); let plan = build_tool_registry_plan( @@ -130,7 +125,6 @@ pub(crate) fn build_specs_with_discoverable_tools( mcp_tools: mcp_tool_plan_inputs .as_ref() .map(|inputs| inputs.mcp_tools.as_slice()), - deferred_mcp_tools: deferred_mcp_tool_sources.as_deref(), tool_namespaces: mcp_tool_plan_inputs .as_ref() .map(|inputs| &inputs.tool_namespaces), @@ -260,10 +254,8 @@ pub(crate) fn build_specs_with_discoverable_tools( } ToolHandlerKind::ToolSearch => { if tool_search_handler.is_none() { - let entries = build_tool_search_entries( - deferred_mcp_tools.as_ref(), - &deferred_dynamic_tools, - ); + let entries = + build_tool_search_entries(mcp_tools.as_ref(), &deferred_dynamic_tools); tool_search_handler = Some(Arc::new(ToolSearchHandler::new(entries))); } if let Some(tool_search_handler) = tool_search_handler.as_ref() { @@ -287,16 +279,6 @@ pub(crate) fn build_specs_with_discoverable_tools( } } } - if let Some(deferred_mcp_tools) = deferred_mcp_tools.as_ref() { - for (name, _) in deferred_mcp_tools.iter().filter(|(name, _)| { - !mcp_tools - .as_ref() - .is_some_and(|tools| tools.contains_key(*name)) - }) { - builder.register_handler(name.clone(), mcp_handler.clone()); - } - } - for unavailable_tool in unavailable_called_tools { let tool_name = unavailable_tool.display(); if existing_spec_names.insert(tool_name.clone()) { diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index f88bb04f3a..8649ca26f0 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -3,11 +3,13 @@ use crate::shell::Shell; use crate::shell::ShellType; use crate::test_support::construct_model_info_offline; use crate::tools::ToolRouter; +use crate::tools::mcp_tool_input::McpToolInput; use crate::tools::router::ToolRouterParams; use codex_app_server_protocol::AppInfo; use codex_features::Feature; use codex_features::Features; use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; +use codex_mcp::ToolInfo; use codex_models_manager::bundled_models_response; use codex_models_manager::model_info::with_config_overrides; use codex_protocol::config_types::WebSearchMode; @@ -32,7 +34,7 @@ use codex_tools::ToolsConfigParams; use codex_tools::UnifiedExecShellMode; use codex_tools::ZshForkConfig; use codex_tools::mcp_call_tool_result_output_schema; -use codex_tools::mcp_tool_to_deferred_responses_api_tool; +use codex_tools::mcp_tool_to_responses_api_tool; use codex_utils_absolute_path::AbsolutePathBuf; use core_test_support::assert_regex_match; use pretty_assertions::assert_eq; @@ -130,9 +132,10 @@ fn deferred_responses_api_tool_serializes_with_defer_loading() { ); let serialized = serde_json::to_value(ToolSpec::Function( - mcp_tool_to_deferred_responses_api_tool( + mcp_tool_to_responses_api_tool( &ToolName::namespaced("mcp__codex_apps__", "lookup_order"), &tool, + /*defer_loading*/ true, ) .expect("convert deferred tool"), )) @@ -237,13 +240,7 @@ async fn multi_agent_v2_tools_config() -> ToolsConfig { } fn multi_agent_v2_spawn_agent_description(tools_config: &ToolsConfig) -> String { - let (tools, _) = build_specs( - tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ) - .build(); + let (tools, _) = build_specs(tools_config, /*mcp_tools*/ None, &[]).build(); let spawn_agent = find_tool(&tools, "spawn_agent"); let ToolSpec::Function(ResponsesApiTool { description, .. }) = &spawn_agent.spec else { panic!("spawn_agent should be a function tool"); @@ -266,36 +263,45 @@ async fn model_info_from_models_json(slug: &str) -> ModelInfo { /// Builds the tool registry builder while collecting tool specs for later serialization. fn build_specs( config: &ToolsConfig, - mcp_tools: Option>, - deferred_mcp_tools: Option>, + mcp_tools: Option>, dynamic_tools: &[DynamicToolSpec], ) -> ToolRegistryBuilder { - build_specs_with_unavailable_tools( - config, - mcp_tools, - deferred_mcp_tools, - Vec::new(), - dynamic_tools, - ) + build_specs_with_unavailable_tools(config, mcp_tools, Vec::new(), dynamic_tools) } fn build_specs_with_unavailable_tools( config: &ToolsConfig, - mcp_tools: Option>, - deferred_mcp_tools: Option>, + mcp_tools: Option>, unavailable_called_tools: Vec, dynamic_tools: &[DynamicToolSpec], ) -> ToolRegistryBuilder { build_specs_with_discoverable_tools( config, mcp_tools, - deferred_mcp_tools, unavailable_called_tools, /*discoverable_tools*/ None, dynamic_tools, ) } +fn mcp_tool_inputs( + mcp_tools: HashMap, + defer_loading: bool, +) -> HashMap { + mcp_tools + .into_iter() + .map(|(name, tool_info)| { + ( + name, + McpToolInput { + tool_info, + defer_loading, + }, + ) + }) + .collect() +} + #[tokio::test] async fn model_provided_unified_exec_is_blocked_for_windows_sandboxed_policies() { let mut model_info = model_info_from_models_json("gpt-5.4").await; @@ -338,13 +344,7 @@ async fn get_memory_requires_feature_flag() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ) - .build(); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]).build(); assert!( !tools.iter().any(|t| t.spec.name() == "get_memory"), "get_memory should be disabled when memory_tool feature is off" @@ -374,7 +374,6 @@ async fn assert_model_tools( &tools_config, ToolRouterParams { mcp_tools: None, - deferred_mcp_tools: None, unavailable_called_tools: Vec::new(), parallel_mcp_server_names: std::collections::HashSet::new(), discoverable_tools: None, @@ -653,13 +652,7 @@ async fn test_build_specs_default_shell_present() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - Some(HashMap::new()), - /*deferred_mcp_tools*/ None, - &[], - ) - .build(); + let (tools, _) = build_specs(&tools_config, Some(HashMap::new()), &[]).build(); // Only check the shell variant and a couple of core tools. let mut subset = vec!["exec_command", "write_stdin", "update_plan"]; @@ -815,7 +808,6 @@ async fn tool_suggest_requires_apps_and_plugins_features() { let (tools, _) = build_specs_with_discoverable_tools( &tools_config, /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, Vec::new(), discoverable_tools.clone(), &[], @@ -832,7 +824,7 @@ async fn tool_suggest_requires_apps_and_plugins_features() { } #[tokio::test] -async fn search_tool_description_handles_no_enabled_mcp_tools() { +async fn search_tool_is_not_registered_without_deferred_tools() { let model_info = search_capable_model_info().await; let mut features = Features::with_defaults(); features.enable(Feature::Apps); @@ -849,20 +841,12 @@ async fn search_tool_description_handles_no_enabled_mcp_tools() { windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - Some(HashMap::new()), - &[], - ) - .build(); - let search_tool = find_tool(&tools, TOOL_SEARCH_TOOL_NAME); - let ToolSpec::ToolSearch { description, .. } = &search_tool.spec else { - panic!("expected tool_search tool"); - }; - - assert!(description.contains("None currently enabled.")); - assert!(!description.contains("{{source_descriptions}}")); + let (tools, _) = build_specs(&tools_config, Some(HashMap::new()), &[]).build(); + assert!( + !tools + .iter() + .any(|tool| tool.name() == TOOL_SEARCH_TOOL_NAME) + ); } #[tokio::test] @@ -885,25 +869,27 @@ async fn search_tool_description_falls_back_to_connector_name_without_descriptio let (tools, _) = build_specs( &tools_config, - /*mcp_tools*/ None, - Some(HashMap::from([( - "mcp__codex_apps__calendar_create_event".to_string(), - ToolInfo { - server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(), - callable_name: "_create_event".to_string(), - callable_namespace: "mcp__codex_apps__calendar".to_string(), - server_instructions: None, - tool: mcp_tool( - "calendar_create_event", - "Create calendar event", - serde_json::json!({"type": "object"}), - ), - connector_id: Some("calendar".to_string()), - connector_name: Some("Calendar".to_string()), - plugin_display_names: Vec::new(), - connector_description: None, - }, - )])), + Some(mcp_tool_inputs( + HashMap::from([( + "mcp__codex_apps__calendar_create_event".to_string(), + ToolInfo { + server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(), + callable_name: "_create_event".to_string(), + callable_namespace: "mcp__codex_apps__calendar".to_string(), + server_instructions: None, + tool: mcp_tool( + "calendar_create_event", + "Create calendar event", + serde_json::json!({"type": "object"}), + ), + connector_id: Some("calendar".to_string()), + connector_name: Some("Calendar".to_string()), + plugin_display_names: Vec::new(), + connector_description: None, + }, + )]), + /*defer_loading*/ true, + )), &[], ) .build(); @@ -936,59 +922,61 @@ async fn search_tool_registers_namespaced_mcp_tool_aliases() { let (_, registry) = build_specs( &tools_config, - /*mcp_tools*/ None, - Some(HashMap::from([ - ( - "mcp__codex_apps__calendar_create_event".to_string(), - ToolInfo { - server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(), - callable_name: "_create_event".to_string(), - callable_namespace: "mcp__codex_apps__calendar".to_string(), - server_instructions: None, - tool: mcp_tool( - "calendar-create-event", - "Create calendar event", - serde_json::json!({"type": "object"}), - ), - connector_id: Some("calendar".to_string()), - connector_name: Some("Calendar".to_string()), - connector_description: None, - plugin_display_names: Vec::new(), - }, - ), - ( - "mcp__codex_apps__calendar_list_events".to_string(), - ToolInfo { - server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(), - callable_name: "_list_events".to_string(), - callable_namespace: "mcp__codex_apps__calendar".to_string(), - server_instructions: None, - tool: mcp_tool( - "calendar-list-events", - "List calendar events", - serde_json::json!({"type": "object"}), - ), - connector_id: Some("calendar".to_string()), - connector_name: Some("Calendar".to_string()), - connector_description: None, - plugin_display_names: Vec::new(), - }, - ), - ( - "mcp__rmcp__echo".to_string(), - ToolInfo { - server_name: "rmcp".to_string(), - callable_name: "echo".to_string(), - callable_namespace: "mcp__rmcp__".to_string(), - server_instructions: None, - tool: mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})), - connector_id: None, - connector_name: None, - connector_description: None, - plugin_display_names: Vec::new(), - }, - ), - ])), + Some(mcp_tool_inputs( + HashMap::from([ + ( + "mcp__codex_apps__calendar_create_event".to_string(), + ToolInfo { + server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(), + callable_name: "_create_event".to_string(), + callable_namespace: "mcp__codex_apps__calendar".to_string(), + server_instructions: None, + tool: mcp_tool( + "calendar-create-event", + "Create calendar event", + serde_json::json!({"type": "object"}), + ), + connector_id: Some("calendar".to_string()), + connector_name: Some("Calendar".to_string()), + connector_description: None, + plugin_display_names: Vec::new(), + }, + ), + ( + "mcp__codex_apps__calendar_list_events".to_string(), + ToolInfo { + server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(), + callable_name: "_list_events".to_string(), + callable_namespace: "mcp__codex_apps__calendar".to_string(), + server_instructions: None, + tool: mcp_tool( + "calendar-list-events", + "List calendar events", + serde_json::json!({"type": "object"}), + ), + connector_id: Some("calendar".to_string()), + connector_name: Some("Calendar".to_string()), + connector_description: None, + plugin_display_names: Vec::new(), + }, + ), + ( + "mcp__rmcp__echo".to_string(), + ToolInfo { + server_name: "rmcp".to_string(), + callable_name: "echo".to_string(), + callable_namespace: "mcp__rmcp__".to_string(), + server_instructions: None, + tool: mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})), + connector_id: None, + connector_name: None, + connector_description: None, + plugin_display_names: Vec::new(), + }, + ), + ]), + /*defer_loading*/ true, + )), &[], ) .build(); @@ -1021,15 +1009,17 @@ async fn direct_mcp_tools_register_namespaced_handlers() { let (_, registry) = build_specs( &tools_config, - Some(HashMap::from([( - "mcp__test_server__echo".to_string(), - mcp_tool_info(mcp_tool( - "echo", - "Echo", - serde_json::json!({"type": "object"}), - )), - )])), - /*deferred_mcp_tools*/ None, + Some(mcp_tool_inputs( + HashMap::from([( + "mcp__test_server__echo".to_string(), + mcp_tool_info(mcp_tool( + "echo", + "Echo", + serde_json::json!({"type": "object"}), + )), + )]), + /*defer_loading*/ false, + )), &[], ) .build(); @@ -1060,7 +1050,6 @@ async fn unavailable_mcp_tools_are_exposed_as_dummy_function_tools() { let (tools, registry) = build_specs_with_unavailable_tools( &tools_config, /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, vec![unavailable_tool], &[], ) @@ -1107,23 +1096,25 @@ async fn test_mcp_tool_property_missing_type_defaults_to_string() { let (tools, _) = build_specs( &tools_config, - Some(HashMap::from([( - "dash/search".to_string(), - mcp_tool_info_with_display_name( - "dash/search", - mcp_tool( - "search", - "Search docs", - serde_json::json!({ - "type": "object", - "properties": { - "query": {"description": "search query"} - } - }), + Some(mcp_tool_inputs( + HashMap::from([( + "dash/search".to_string(), + mcp_tool_info_with_display_name( + "dash/search", + mcp_tool( + "search", + "Search docs", + serde_json::json!({ + "type": "object", + "properties": { + "query": {"description": "search query"} + } + }), + ), ), - ), - )])), - /*deferred_mcp_tools*/ None, + )]), + /*defer_loading*/ false, + )), &[], ) .build(); @@ -1170,21 +1161,23 @@ async fn test_mcp_tool_preserves_integer_schema() { let (tools, _) = build_specs( &tools_config, - Some(HashMap::from([( - "dash/paginate".to_string(), - mcp_tool_info_with_display_name( - "dash/paginate", - mcp_tool( - "paginate", - "Pagination", - serde_json::json!({ - "type": "object", - "properties": {"page": {"type": "integer"}} - }), + Some(mcp_tool_inputs( + HashMap::from([( + "dash/paginate".to_string(), + mcp_tool_info_with_display_name( + "dash/paginate", + mcp_tool( + "paginate", + "Pagination", + serde_json::json!({ + "type": "object", + "properties": {"page": {"type": "integer"}} + }), + ), ), - ), - )])), - /*deferred_mcp_tools*/ None, + )]), + /*defer_loading*/ false, + )), &[], ) .build(); @@ -1232,21 +1225,23 @@ async fn test_mcp_tool_array_without_items_gets_default_string_items() { let (tools, _) = build_specs( &tools_config, - Some(HashMap::from([( - "dash/tags".to_string(), - mcp_tool_info_with_display_name( - "dash/tags", - mcp_tool( - "tags", - "Tags", - serde_json::json!({ - "type": "object", - "properties": {"tags": {"type": "array"}} - }), + Some(mcp_tool_inputs( + HashMap::from([( + "dash/tags".to_string(), + mcp_tool_info_with_display_name( + "dash/tags", + mcp_tool( + "tags", + "Tags", + serde_json::json!({ + "type": "object", + "properties": {"tags": {"type": "array"}} + }), + ), ), - ), - )])), - /*deferred_mcp_tools*/ None, + )]), + /*defer_loading*/ false, + )), &[], ) .build(); @@ -1296,23 +1291,25 @@ async fn test_mcp_tool_anyof_defaults_to_string() { let (tools, _) = build_specs( &tools_config, - Some(HashMap::from([( - "dash/value".to_string(), - mcp_tool_info_with_display_name( - "dash/value", - mcp_tool( - "value", - "AnyOf Value", - serde_json::json!({ - "type": "object", - "properties": { - "value": {"anyOf": [{"type": "string"}, {"type": "number"}]} - } - }), + Some(mcp_tool_inputs( + HashMap::from([( + "dash/value".to_string(), + mcp_tool_info_with_display_name( + "dash/value", + mcp_tool( + "value", + "AnyOf Value", + serde_json::json!({ + "type": "object", + "properties": { + "value": {"anyOf": [{"type": "string"}, {"type": "number"}]} + } + }), + ), ), - ), - )])), - /*deferred_mcp_tools*/ None, + )]), + /*defer_loading*/ false, + )), &[], ) .build(); @@ -1364,40 +1361,42 @@ async fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() { }); let (tools, _) = build_specs( &tools_config, - Some(HashMap::from([( - "test_server/do_something_cool".to_string(), - mcp_tool_info_with_display_name( - "test_server/do_something_cool", - mcp_tool( - "do_something_cool", - "Do something cool", - serde_json::json!({ - "type": "object", - "properties": { - "string_argument": {"type": "string"}, - "number_argument": {"type": "number"}, - "object_argument": { + Some(mcp_tool_inputs( + HashMap::from([( + "test_server/do_something_cool".to_string(), + mcp_tool_info_with_display_name( + "test_server/do_something_cool", + mcp_tool( + "do_something_cool", + "Do something cool", + serde_json::json!({ "type": "object", "properties": { - "string_property": {"type": "string"}, - "number_property": {"type": "number"} - }, - "required": ["string_property", "number_property"], - "additionalProperties": { - "type": "object", - "properties": { - "addtl_prop": {"type": "string"} - }, - "required": ["addtl_prop"], - "additionalProperties": false + "string_argument": {"type": "string"}, + "number_argument": {"type": "number"}, + "object_argument": { + "type": "object", + "properties": { + "string_property": {"type": "string"}, + "number_property": {"type": "number"} + }, + "required": ["string_property", "number_property"], + "additionalProperties": { + "type": "object", + "properties": { + "addtl_prop": {"type": "string"} + }, + "required": ["addtl_prop"], + "additionalProperties": false + } } } - } - }), + }), + ), ), - ), - )])), - /*deferred_mcp_tools*/ None, + )]), + /*defer_loading*/ false, + )), &[], ) .build(); diff --git a/codex-rs/core/src/tools/tool_search_entry.rs b/codex-rs/core/src/tools/tool_search_entry.rs index 23535439f6..2fd3c4c210 100644 --- a/codex-rs/core/src/tools/tool_search_entry.rs +++ b/codex-rs/core/src/tools/tool_search_entry.rs @@ -1,3 +1,4 @@ +use crate::tools::mcp_tool_input::McpToolInput; use codex_mcp::ToolInfo; use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_tools::LoadableToolSpec; @@ -14,13 +15,19 @@ pub(crate) struct ToolSearchEntry { } pub(crate) fn build_tool_search_entries( - mcp_tools: Option<&HashMap>, + mcp_tools: Option<&HashMap>, dynamic_tools: &[DynamicToolSpec], ) -> Vec { let mut entries = Vec::new(); let mut mcp_tools = mcp_tools - .map(|tools| tools.values().collect::>()) + .map(|tools| { + tools + .values() + .filter(|tool| tool.defer_loading) + .map(|tool| &tool.tool_info) + .collect::>() + }) .unwrap_or_default(); mcp_tools.sort_by_key(|info| info.canonical_tool_name().display()); for info in mcp_tools { diff --git a/codex-rs/tools/README.md b/codex-rs/tools/README.md index ac6bba853b..4298f26768 100644 --- a/codex-rs/tools/README.md +++ b/codex-rs/tools/README.md @@ -37,7 +37,6 @@ schema and Responses API tool primitives that no longer need to live in - `dynamic_tool_to_loadable_tool_spec()` - `dynamic_tool_to_responses_api_tool()` - `mcp_tool_to_responses_api_tool()` -- `mcp_tool_to_deferred_responses_api_tool()` - `augment_tool_spec_for_code_mode()` - `tool_spec_to_code_mode_tool_definition()` diff --git a/codex-rs/tools/src/code_mode.rs b/codex-rs/tools/src/code_mode.rs index 459eb7e460..3eb8acd75b 100644 --- a/codex-rs/tools/src/code_mode.rs +++ b/codex-rs/tools/src/code_mode.rs @@ -82,7 +82,7 @@ pub fn collect_code_mode_exec_prompt_tool_definitions<'a>( ) -> Vec { let mut tool_definitions = specs .into_iter() - .flat_map(code_mode_tool_definitions_for_spec) + .flat_map(code_mode_exec_prompt_tool_definitions_for_spec) .filter(|definition| codex_code_mode::is_code_mode_nested_tool(&definition.name)) .collect::>(); tool_definitions.sort_by(|left, right| left.name.cmp(&right.name)); @@ -178,8 +178,22 @@ fn code_mode_tool_definition_for_spec(spec: &ToolSpec) -> Option Vec { + code_mode_tool_definitions_for_spec_with_deferred(spec, /*include_deferred*/ true) +} + +fn code_mode_exec_prompt_tool_definitions_for_spec(spec: &ToolSpec) -> Vec { + code_mode_tool_definitions_for_spec_with_deferred(spec, /*include_deferred*/ false) +} + +fn code_mode_tool_definitions_for_spec_with_deferred( + spec: &ToolSpec, + include_deferred: bool, +) -> Vec { match spec { ToolSpec::Function(tool) => { + if !include_deferred && tool.defer_loading == Some(true) { + return Vec::new(); + } let name = tool.name.clone(); vec![CodeModeToolDefinition { tool_name: ToolName::plain(name.clone()), @@ -204,17 +218,20 @@ fn code_mode_tool_definitions_for_spec(spec: &ToolSpec) -> Vec namespace .tools .iter() - .map(|tool| match tool { + .filter_map(|tool| match tool { ResponsesApiNamespaceTool::Function(tool) => { + if !include_deferred && tool.defer_loading == Some(true) { + return None; + } let tool_name = ToolName::namespaced(namespace.name.clone(), tool.name.clone()); - CodeModeToolDefinition { + Some(CodeModeToolDefinition { name: code_mode_name_for_tool_name(&tool_name), tool_name, description: tool.description.clone(), kind: CodeModeToolKind::Function, input_schema: serde_json::to_value(&tool.parameters).ok(), output_schema: tool.output_schema.clone(), - } + }) } }) .collect(), diff --git a/codex-rs/tools/src/lib.rs b/codex-rs/tools/src/lib.rs index 516bda6859..a697e9ed9f 100644 --- a/codex-rs/tools/src/lib.rs +++ b/codex-rs/tools/src/lib.rs @@ -95,7 +95,6 @@ pub use responses_api::coalesce_loadable_tool_specs; pub(crate) use responses_api::default_namespace_description; pub use responses_api::dynamic_tool_to_loadable_tool_spec; pub use responses_api::dynamic_tool_to_responses_api_tool; -pub use responses_api::mcp_tool_to_deferred_responses_api_tool; pub use responses_api::mcp_tool_to_responses_api_tool; pub use responses_api::tool_definition_to_responses_api_tool; pub use tool_config::ShellCommandBackendConfig; @@ -127,7 +126,6 @@ pub use tool_registry_plan_types::ToolHandlerKind; pub use tool_registry_plan_types::ToolHandlerSpec; pub use tool_registry_plan_types::ToolNamespace; pub use tool_registry_plan_types::ToolRegistryPlan; -pub use tool_registry_plan_types::ToolRegistryPlanDeferredTool; pub use tool_registry_plan_types::ToolRegistryPlanMcpTool; pub use tool_registry_plan_types::ToolRegistryPlanParams; pub use tool_spec::ConfiguredToolSpec; diff --git a/codex-rs/tools/src/responses_api.rs b/codex-rs/tools/src/responses_api.rs index c3643fbba6..cfc8a229b4 100644 --- a/codex-rs/tools/src/responses_api.rs +++ b/codex-rs/tools/src/responses_api.rs @@ -122,21 +122,11 @@ pub fn coalesce_loadable_tool_specs( pub fn mcp_tool_to_responses_api_tool( tool_name: &ToolName, tool: &rmcp::model::Tool, + defer_loading: bool, ) -> Result { - Ok(tool_definition_to_responses_api_tool( - parse_mcp_tool(tool)?.renamed(tool_name.name.clone()), - )) -} - -pub fn mcp_tool_to_deferred_responses_api_tool( - tool_name: &ToolName, - tool: &rmcp::model::Tool, -) -> Result { - Ok(tool_definition_to_responses_api_tool( - parse_mcp_tool(tool)? - .renamed(tool_name.name.clone()) - .into_deferred(), - )) + let mut tool_definition = parse_mcp_tool(tool)?.renamed(tool_name.name.clone()); + tool_definition.defer_loading = defer_loading; + Ok(tool_definition_to_responses_api_tool(tool_definition)) } pub fn tool_definition_to_responses_api_tool(tool_definition: ToolDefinition) -> ResponsesApiTool { diff --git a/codex-rs/tools/src/responses_api_tests.rs b/codex-rs/tools/src/responses_api_tests.rs index 3ce13ebfe8..c7fa8aa531 100644 --- a/codex-rs/tools/src/responses_api_tests.rs +++ b/codex-rs/tools/src/responses_api_tests.rs @@ -3,11 +3,12 @@ use super::ResponsesApiNamespace; use super::ResponsesApiNamespaceTool; use super::ResponsesApiTool; use super::dynamic_tool_to_responses_api_tool; -use super::mcp_tool_to_deferred_responses_api_tool; +use super::mcp_tool_to_responses_api_tool; use super::tool_definition_to_responses_api_tool; use crate::JsonSchema; use crate::ToolDefinition; use crate::ToolName; +use crate::mcp_call_tool_result_output_schema; use codex_protocol::dynamic_tools::DynamicToolSpec; use pretty_assertions::assert_eq; use serde_json::json; @@ -86,7 +87,7 @@ fn dynamic_tool_to_responses_api_tool_preserves_defer_loading() { } #[test] -fn mcp_tool_to_deferred_responses_api_tool_sets_defer_loading() { +fn mcp_tool_to_responses_api_tool_sets_defer_loading_from_argument() { let tool = rmcp::model::Tool { name: "lookup_order".to_string().into(), title: None, @@ -107,9 +108,10 @@ fn mcp_tool_to_deferred_responses_api_tool_sets_defer_loading() { }; assert_eq!( - mcp_tool_to_deferred_responses_api_tool( + mcp_tool_to_responses_api_tool( &ToolName::namespaced("mcp__codex_apps__", "lookup_order"), &tool, + /*defer_loading*/ true, ) .expect("convert deferred tool"), ResponsesApiTool { @@ -125,7 +127,7 @@ fn mcp_tool_to_deferred_responses_api_tool_sets_defer_loading() { Some(vec!["order_id".to_string()]), Some(false.into()) ), - output_schema: None, + output_schema: Some(mcp_call_tool_result_output_schema(json!({}))), } ); } diff --git a/codex-rs/tools/src/tool_definition.rs b/codex-rs/tools/src/tool_definition.rs index d3c5b0209b..3dee8c53d3 100644 --- a/codex-rs/tools/src/tool_definition.rs +++ b/codex-rs/tools/src/tool_definition.rs @@ -17,12 +17,6 @@ impl ToolDefinition { self.name = name; self } - - pub fn into_deferred(mut self) -> Self { - self.output_schema = None; - self.defer_loading = true; - self - } } #[cfg(test)] diff --git a/codex-rs/tools/src/tool_definition_tests.rs b/codex-rs/tools/src/tool_definition_tests.rs index 0d4d97f5ce..358f37f178 100644 --- a/codex-rs/tools/src/tool_definition_tests.rs +++ b/codex-rs/tools/src/tool_definition_tests.rs @@ -29,15 +29,3 @@ fn renamed_overrides_name_only() { } ); } - -#[test] -fn into_deferred_drops_output_schema_and_sets_defer_loading() { - assert_eq!( - tool_definition().into_deferred(), - ToolDefinition { - output_schema: None, - defer_loading: true, - ..tool_definition() - } - ); -} diff --git a/codex-rs/tools/src/tool_discovery.rs b/codex-rs/tools/src/tool_discovery.rs index ddcd2f1ce3..ebce29dc43 100644 --- a/codex-rs/tools/src/tool_discovery.rs +++ b/codex-rs/tools/src/tool_discovery.rs @@ -6,7 +6,7 @@ use crate::ResponsesApiTool; use crate::ToolName; use crate::ToolSpec; use crate::default_namespace_description; -use crate::mcp_tool_to_deferred_responses_api_tool; +use crate::mcp_tool_to_responses_api_tool; use codex_app_server_protocol::AppInfo; use serde::Deserialize; use serde::Serialize; @@ -233,7 +233,7 @@ fn tool_search_result_source_to_namespace_tool( source: ToolSearchResultSource<'_>, ) -> Result { let tool_name = ToolName::namespaced(source.tool_namespace, source.tool_name); - mcp_tool_to_deferred_responses_api_tool(&tool_name, source.tool) + mcp_tool_to_responses_api_tool(&tool_name, source.tool, /*defer_loading*/ true) .map(ResponsesApiNamespaceTool::Function) } diff --git a/codex-rs/tools/src/tool_registry_plan.rs b/codex-rs/tools/src/tool_registry_plan.rs index f7c10dc2c4..237d430144 100644 --- a/codex-rs/tools/src/tool_registry_plan.rs +++ b/codex-rs/tools/src/tool_registry_plan.rs @@ -112,10 +112,7 @@ pub fn build_tool_registry_plan( &enabled_tools, &namespace_descriptions, config.code_mode_only_enabled, - config.search_tool - && params - .deferred_mcp_tools - .is_some_and(|tools| !tools.is_empty()), + config.search_tool && deferred_tools_available(params), ), /*supports_parallel_tool_calls*/ false, config.code_mode_enabled, @@ -260,29 +257,27 @@ pub fn build_tool_registry_plan( plan.register_handler("request_permissions", ToolHandlerKind::RequestPermissions); } - let deferred_dynamic_tools = params - .dynamic_tools - .iter() - .filter(|tool| tool.defer_loading) - .collect::>(); - - if config.search_tool - && (params.deferred_mcp_tools.is_some() || !deferred_dynamic_tools.is_empty()) - { + let deferred_dynamic_tools_available = + params.dynamic_tools.iter().any(|tool| tool.defer_loading); + if config.search_tool && deferred_tools_available(params) { let mut search_source_infos = params - .deferred_mcp_tools - .map(|deferred_mcp_tools| { - collect_tool_search_source_infos(deferred_mcp_tools.iter().map(|tool| { - ToolSearchSource { + .mcp_tools + .map(|mcp_tools| { + collect_tool_search_source_infos(mcp_tools.iter().filter_map(|tool| { + if !tool.defer_loading { + return None; + } + + Some(ToolSearchSource { server_name: tool.server_name, connector_name: tool.connector_name, connector_description: tool.connector_description, - } + }) })) }) .unwrap_or_default(); - if !deferred_dynamic_tools.is_empty() { + if deferred_dynamic_tools_available { search_source_infos.push(ToolSearchSourceInfo { name: "Dynamic tools".to_string(), description: Some("Tools provided by the current Codex thread.".to_string()), @@ -295,12 +290,6 @@ pub fn build_tool_registry_plan( config.code_mode_enabled, ); plan.register_handler(TOOL_SEARCH_TOOL_NAME, ToolHandlerKind::ToolSearch); - - if let Some(deferred_mcp_tools) = params.deferred_mcp_tools { - for tool in deferred_mcp_tools { - plan.register_handler(tool.name.clone(), ToolHandlerKind::Mcp); - } - } } if config.tool_suggest @@ -537,7 +526,11 @@ pub fn build_tool_registry_plan( }); let mut tools = Vec::new(); for tool in entries { - match mcp_tool_to_responses_api_tool(&tool.name, tool.tool) { + match mcp_tool_to_responses_api_tool( + &tool.name, + tool.tool, + /*defer_loading*/ tool.defer_loading, + ) { Ok(converted_tool) => { tools.push(ResponsesApiNamespaceTool::Function(converted_tool)); plan.register_handler(tool.name, ToolHandlerKind::Mcp); @@ -592,6 +585,13 @@ pub fn build_tool_registry_plan( plan } +fn deferred_tools_available(params: ToolRegistryPlanParams<'_>) -> bool { + params + .mcp_tools + .is_some_and(|tools| tools.iter().any(|tool| tool.defer_loading)) + || params.dynamic_tools.iter().any(|tool| tool.defer_loading) +} + fn compare_code_mode_tools( left: &codex_code_mode::ToolDefinition, right: &codex_code_mode::ToolDefinition, diff --git a/codex-rs/tools/src/tool_registry_plan_tests.rs b/codex-rs/tools/src/tool_registry_plan_tests.rs index 8f64349c2f..e87b9e0b26 100644 --- a/codex-rs/tools/src/tool_registry_plan_tests.rs +++ b/codex-rs/tools/src/tool_registry_plan_tests.rs @@ -14,7 +14,6 @@ use crate::ResponsesApiWebSearchUserLocation; use crate::ToolHandlerSpec; use crate::ToolName; use crate::ToolNamespace; -use crate::ToolRegistryPlanDeferredTool; use crate::ToolRegistryPlanMcpTool; use crate::ToolsConfigParams; use crate::WaitAgentTimeoutOptions; @@ -60,12 +59,7 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&config, /*mcp_tools*/ None, &[]); let mut actual = BTreeMap::new(); let mut duplicate_names = Vec::new(); @@ -172,12 +166,7 @@ fn test_build_specs_collab_tools_enabled() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); assert_contains_tool_names( &tools, @@ -210,12 +199,7 @@ fn goal_tools_require_goals_feature() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); assert_lacks_tool_name(&tools, "get_goal"); assert_lacks_tool_name(&tools, "create_goal"); assert_lacks_tool_name(&tools, "update_goal"); @@ -231,12 +215,7 @@ fn goal_tools_require_goals_feature() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); assert_contains_tool_names(&tools, &["get_goal", "create_goal", "update_goal"]); } @@ -257,12 +236,7 @@ fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); assert_contains_tool_names( &tools, @@ -400,12 +374,7 @@ fn test_build_specs_enable_fanout_enables_agent_jobs_and_collab_tools() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); assert_contains_tool_names( &tools, @@ -435,12 +404,7 @@ fn view_image_tool_omits_detail_without_original_detail_support() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); let view_image = find_tool(&tools, VIEW_IMAGE_TOOL_NAME); let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &view_image.spec else { panic!("view_image should be a function tool"); @@ -465,12 +429,7 @@ fn view_image_tool_includes_detail_with_original_detail_support() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); let view_image = find_tool(&tools, VIEW_IMAGE_TOOL_NAME); let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &view_image.spec else { panic!("view_image should be a function tool"); @@ -506,12 +465,7 @@ fn disabled_environment_omits_environment_backed_tools() { tools_config .experimental_supported_tools .push("list_dir".to_string()); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); assert_lacks_tool_name(&tools, "exec_command"); assert_lacks_tool_name(&tools, "write_stdin"); @@ -540,12 +494,7 @@ fn test_build_specs_agent_job_worker_tools_enabled() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); assert_contains_tool_names( &tools, @@ -577,12 +526,7 @@ fn request_user_input_description_reflects_default_mode_feature_flag() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); let request_user_input_tool = find_tool(&tools, REQUEST_USER_INPUT_TOOL_NAME); assert_eq!( request_user_input_tool.spec, @@ -600,12 +544,7 @@ fn request_user_input_description_reflects_default_mode_feature_flag() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); let request_user_input_tool = find_tool(&tools, REQUEST_USER_INPUT_TOOL_NAME); assert_eq!( request_user_input_tool.spec, @@ -628,12 +567,7 @@ fn request_permissions_requires_feature_flag() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); assert_lacks_tool_name(&tools, "request_permissions"); let mut features = Features::with_defaults(); @@ -648,12 +582,7 @@ fn request_permissions_requires_feature_flag() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); let request_permissions_tool = find_tool(&tools, "request_permissions"); assert_eq!( request_permissions_tool.spec, @@ -677,12 +606,7 @@ fn request_permissions_tool_is_independent_from_additional_permissions() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); assert_lacks_tool_name(&tools, "request_permissions"); } @@ -708,12 +632,7 @@ fn image_generation_tools_require_feature_and_supported_model() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (default_tools, _) = build_specs( - &default_tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (default_tools, _) = build_specs(&default_tools_config, /*mcp_tools*/ None, &[]); assert!( !default_tools .iter() @@ -731,12 +650,7 @@ fn image_generation_tools_require_feature_and_supported_model() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (supported_tools, _) = build_specs( - &supported_tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (supported_tools, _) = build_specs(&supported_tools_config, /*mcp_tools*/ None, &[]); assert_contains_tool_names(&supported_tools, &["image_generation"]); let image_generation_tool = find_tool(&supported_tools, "image_generation"); assert_eq!( @@ -757,12 +671,7 @@ fn image_generation_tools_require_feature_and_supported_model() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); assert!( !tools .iter() @@ -787,12 +696,7 @@ fn web_search_mode_cached_sets_external_web_access_false() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); let tool = find_tool(&tools, "web_search"); assert_eq!( @@ -823,12 +727,7 @@ fn web_search_mode_live_sets_external_web_access_true() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); let tool = find_tool(&tools, "web_search"); assert_eq!( @@ -873,12 +772,7 @@ fn web_search_config_is_forwarded_to_tool_spec() { windows_sandbox_level: WindowsSandboxLevel::Disabled, }) .with_web_search_config(Some(web_search_config.clone())); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); let tool = find_tool(&tools, "web_search"); assert_eq!( @@ -914,12 +808,7 @@ fn web_search_tool_type_text_and_image_sets_search_content_types() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); let tool = find_tool(&tools, "web_search"); assert_eq!( @@ -949,12 +838,7 @@ fn mcp_resource_tools_are_hidden_without_mcp_servers() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); assert!( !tools.iter().any(|tool| matches!( @@ -980,12 +864,7 @@ fn mcp_resource_tools_are_included_when_mcp_servers_are_present() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - Some(HashMap::new()), - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, Some(Vec::new()), &[]); assert_contains_tool_names( &tools, @@ -1014,12 +893,7 @@ fn test_parallel_support_flags() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); assert!(find_tool(&tools, "exec_command").supports_parallel_tool_calls); assert!(!find_tool(&tools, "write_stdin").supports_parallel_tool_calls); @@ -1041,12 +915,7 @@ fn test_test_model_info_includes_sync_tool() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); assert!(tools.iter().any(|tool| tool.name() == "test_sync_tool")); } @@ -1069,30 +938,32 @@ fn test_build_specs_mcp_tools_converted() { }); let (tools, _) = build_specs( &tools_config, - Some(HashMap::from([( - ToolName::namespaced("test_server/", "do_something_cool"), - mcp_tool( - "do_something_cool", - "Do something cool", - serde_json::json!({ - "type": "object", - "properties": { - "string_argument": { "type": "string" }, - "number_argument": { "type": "number" }, - "object_argument": { - "type": "object", - "properties": { - "string_property": { "type": "string" }, - "number_property": { "type": "number" }, + Some(mcp_tool_inputs( + HashMap::from([( + ToolName::namespaced("test_server/", "do_something_cool"), + mcp_tool( + "do_something_cool", + "Do something cool", + serde_json::json!({ + "type": "object", + "properties": { + "string_argument": { "type": "string" }, + "number_argument": { "type": "number" }, + "object_argument": { + "type": "object", + "properties": { + "string_property": { "type": "string" }, + "number_property": { "type": "number" }, + }, + "required": ["string_property", "number_property"], + "additionalProperties": false, }, - "required": ["string_property", "number_property"], - "additionalProperties": false, }, - }, - }), - ), - )])), - /*deferred_mcp_tools*/ None, + }), + ), + )]), + /*defer_loading*/ false, + )), &[], ); @@ -1161,15 +1032,17 @@ fn test_build_specs_mcp_namespace_description_falls_back_when_missing() { }); let (tools, _) = build_specs( &tools_config, - Some(HashMap::from([( - ToolName::namespaced("test_server/", "do_something_cool"), - mcp_tool( - "do_something_cool", - "Do something cool", - serde_json::json!({"type": "object"}), - ), - )])), - /*deferred_mcp_tools*/ None, + Some(mcp_tool_inputs( + HashMap::from([( + ToolName::namespaced("test_server/", "do_something_cool"), + mcp_tool( + "do_something_cool", + "Do something cool", + serde_json::json!({"type": "object"}), + ), + )]), + /*defer_loading*/ false, + )), &[], ); @@ -1217,8 +1090,7 @@ fn test_build_specs_mcp_tools_sorted_by_name() { let (tools, _) = build_specs( &tools_config, - Some(tools_map), - /*deferred_mcp_tools*/ None, + Some(mcp_tool_inputs(tools_map, /*defer_loading*/ false)), &[], ); @@ -1250,9 +1122,8 @@ fn search_tool_description_lists_each_mcp_source_once() { windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, handlers) = build_specs( - &tools_config, - Some(HashMap::from([ + let mut mcp_tools = mcp_tool_inputs( + HashMap::from([ ( ToolName::namespaced("mcp__codex_apps__calendar", "_create_event"), mcp_tool( @@ -1265,39 +1136,37 @@ fn search_tool_description_lists_each_mcp_source_once() { ToolName::namespaced("mcp__rmcp__", "echo"), mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})), ), - ])), - Some(vec![ - deferred_mcp_tool( - "_create_event", - "mcp__codex_apps__calendar", - CODEX_APPS_MCP_SERVER_NAME, - Some("Calendar"), - Some("Plan events and manage your calendar."), - ), - deferred_mcp_tool( - "_list_events", - "mcp__codex_apps__calendar", - CODEX_APPS_MCP_SERVER_NAME, - Some("Calendar"), - Some("Plan events and manage your calendar."), - ), - deferred_mcp_tool( - "_search_threads", - "mcp__codex_apps__gmail", - CODEX_APPS_MCP_SERVER_NAME, - Some("Gmail"), - Some("Find and summarize email threads."), - ), - deferred_mcp_tool( - "echo", - "mcp__rmcp__", - "rmcp", - /*connector_name*/ None, - /*connector_description*/ None, - ), ]), - &[], + /*defer_loading*/ false, ); + mcp_tools.extend([ + mcp_tool_input( + "_list_events", + "mcp__codex_apps__calendar", + CODEX_APPS_MCP_SERVER_NAME, + Some("Calendar"), + Some("Plan events and manage your calendar."), + /*defer_loading*/ true, + ), + mcp_tool_input( + "_search_threads", + "mcp__codex_apps__gmail", + CODEX_APPS_MCP_SERVER_NAME, + Some("Gmail"), + Some("Find and summarize email threads."), + /*defer_loading*/ true, + ), + mcp_tool_input( + "deferred_echo", + "mcp__rmcp__", + "rmcp", + /*connector_name*/ None, + /*connector_description*/ None, + /*defer_loading*/ true, + ), + ]); + + let (tools, handlers) = build_specs(&tools_config, Some(mcp_tools), &[]); let search_tool = find_tool(&tools, TOOL_SEARCH_TOOL_NAME); let ToolSpec::ToolSearch { description, .. } = &search_tool.spec else { @@ -1328,13 +1197,14 @@ fn search_tool_description_lists_each_mcp_source_once() { #[test] fn search_tool_requires_model_capability_and_enabled_feature() { let model_info = search_capable_model_info(); - let deferred_mcp_tools = Some(vec![deferred_mcp_tool( + let mcp_tools = vec![mcp_tool_input( "_create_event", "mcp__codex_apps__calendar", CODEX_APPS_MCP_SERVER_NAME, Some("Calendar"), /*connector_description*/ None, - )]); + /*defer_loading*/ true, + )]; let features = Features::with_defaults(); let available_models = Vec::new(); @@ -1351,12 +1221,7 @@ fn search_tool_requires_model_capability_and_enabled_feature() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - deferred_mcp_tools.clone(), - &[], - ); + let (tools, _) = build_specs(&tools_config, Some(mcp_tools.clone()), &[]); assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME); let mut features_without_tool_search = Features::with_defaults(); @@ -1371,12 +1236,7 @@ fn search_tool_requires_model_capability_and_enabled_feature() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - deferred_mcp_tools.clone(), - &[], - ); + let (tools, _) = build_specs(&tools_config, Some(mcp_tools.clone()), &[]); assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME); let tools_config = ToolsConfig::new(&ToolsConfigParams { @@ -1389,12 +1249,7 @@ fn search_tool_requires_model_capability_and_enabled_feature() { permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - deferred_mcp_tools, - &[], - ); + let (tools, _) = build_specs(&tools_config, Some(mcp_tools), &[]); assert_contains_tool_names(&tools, &[TOOL_SEARCH_TOOL_NAME]); } @@ -1439,12 +1294,7 @@ fn search_tool_registers_for_deferred_dynamic_tools() { }, ]; - let (tools, handlers) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &dynamic_tools, - ); + let (tools, handlers) = build_specs(&tools_config, /*mcp_tools*/ None, &dynamic_tools); let search_tool = find_tool(&tools, TOOL_SEARCH_TOOL_NAME); let ToolSpec::ToolSearch { description, .. } = &search_tool.spec else { @@ -1506,7 +1356,6 @@ fn tool_suggest_is_not_registered_without_feature_flag() { let (tools, _) = build_specs_with_discoverable_tools( &tools_config, /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, Some(vec![discoverable_connector( "connector_2128aebfecb84f64a069897515042a44", "Google Calendar", @@ -1546,7 +1395,6 @@ fn tool_suggest_can_be_registered_without_search_tool() { let (tools, _) = build_specs_with_discoverable_tools( &tools_config, /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, Some(vec![discoverable_connector( "connector_2128aebfecb84f64a069897515042a44", "Google Calendar", @@ -1614,7 +1462,6 @@ fn tool_suggest_description_lists_discoverable_tools() { let (tools, _) = build_specs_with_discoverable_tools( &tools_config, /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, Some(discoverable_tools), &[], ); @@ -1694,22 +1541,24 @@ fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() { let (tools, _) = build_specs( &tools_config, - Some(HashMap::from([( - ToolName::namespaced("mcp__sample__", "echo"), - mcp_tool( - "echo", - "Echo text", - serde_json::json!({ - "type": "object", - "properties": { - "message": {"type": "string"} - }, - "required": ["message"], - "additionalProperties": false - }), - ), - )])), - /*deferred_mcp_tools*/ None, + Some(mcp_tool_inputs( + HashMap::from([( + ToolName::namespaced("mcp__sample__", "echo"), + mcp_tool( + "echo", + "Echo text", + serde_json::json!({ + "type": "object", + "properties": { + "message": {"type": "string"} + }, + "required": ["message"], + "additionalProperties": false + }), + ), + )]), + /*defer_loading*/ false, + )), &[], ); @@ -1747,55 +1596,57 @@ fn code_mode_preserves_nullable_and_literal_mcp_input_shapes() { let (tools, _) = build_specs( &tools_config, - Some(HashMap::from([( - ToolName::namespaced("mcp__sample__", "fn"), - mcp_tool( - "fn", - "Sample fn", - serde_json::json!({ - "type": "object", - "properties": { - "open": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "object", - "properties": { - "ref_id": {"type": "string"}, - "lineno": {"anyOf": [{"type": "integer"}, {"type": "null"}]} - }, - "required": ["ref_id"], - "additionalProperties": false - } - }, - {"type": "null"} - ] + Some(mcp_tool_inputs( + HashMap::from([( + ToolName::namespaced("mcp__sample__", "fn"), + mcp_tool( + "fn", + "Sample fn", + serde_json::json!({ + "type": "object", + "properties": { + "open": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "ref_id": {"type": "string"}, + "lineno": {"anyOf": [{"type": "integer"}, {"type": "null"}]} + }, + "required": ["ref_id"], + "additionalProperties": false + } + }, + {"type": "null"} + ] + }, + "tagged_list": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "kind": {"type": "const", "const": "tagged"}, + "variant": {"type": "enum", "enum": ["alpha", "beta"]}, + "scope": {"type": "enum", "enum": ["one", "two"]} + }, + "required": ["kind", "variant", "scope"] + } + }, + {"type": "null"} + ] + }, + "response_length": {"type": "enum", "enum": ["short", "medium", "long"]} }, - "tagged_list": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "object", - "properties": { - "kind": {"type": "const", "const": "tagged"}, - "variant": {"type": "enum", "enum": ["alpha", "beta"]}, - "scope": {"type": "enum", "enum": ["one", "two"]} - }, - "required": ["kind", "variant", "scope"] - } - }, - {"type": "null"} - ] - }, - "response_length": {"type": "enum", "enum": ["short", "medium", "long"]} - }, - "additionalProperties": false - }), - ), - )])), - /*deferred_mcp_tools*/ None, + "additionalProperties": false + }), + ), + )]), + /*defer_loading*/ false, + )), &[], ); @@ -1828,12 +1679,7 @@ fn code_mode_augments_builtin_tool_descriptions_with_typed_sample() { windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); let ToolSpec::Function(ResponsesApiTool { description, .. }) = &find_tool(&tools, VIEW_IMAGE_TOOL_NAME).spec else { @@ -1864,12 +1710,7 @@ fn code_mode_only_exec_description_includes_full_nested_tool_details() { windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); let ToolSpec::Freeform(FreeformTool { description, .. }) = &find_tool(&tools, "exec").spec else { panic!("expected freeform tool"); @@ -1901,12 +1742,7 @@ fn code_mode_exec_description_omits_nested_tool_details_when_not_code_mode_only( windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*deferred_mcp_tools*/ None, - &[], - ); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, &[]); let ToolSpec::Freeform(FreeformTool { description, .. }) = &find_tool(&tools, "exec").spec else { panic!("expected freeform tool"); @@ -1961,60 +1797,51 @@ fn search_capable_model_info() -> ModelInfo { } } -fn build_specs<'a>( +fn build_specs( config: &ToolsConfig, - mcp_tools: Option>, - deferred_mcp_tools: Option>>, + mcp_tools: Option>, dynamic_tools: &[DynamicToolSpec], ) -> (Vec, Vec) { build_specs_with_discoverable_tools( config, mcp_tools, - deferred_mcp_tools, /*discoverable_tools*/ None, dynamic_tools, ) } -fn build_specs_with_discoverable_tools<'a>( +fn build_specs_with_discoverable_tools( config: &ToolsConfig, - mcp_tools: Option>, - deferred_mcp_tools: Option>>, + mcp_tools: Option>, discoverable_tools: Option>, dynamic_tools: &[DynamicToolSpec], ) -> (Vec, Vec) { build_specs_with_optional_tool_namespaces( config, mcp_tools, - deferred_mcp_tools, /*tool_namespaces*/ None, discoverable_tools, dynamic_tools, ) } -fn build_specs_with_optional_tool_namespaces<'a>( +fn build_specs_with_optional_tool_namespaces( config: &ToolsConfig, - mcp_tools: Option>, - deferred_mcp_tools: Option>>, + mcp_tools: Option>, tool_namespaces: Option>, discoverable_tools: Option>, dynamic_tools: &[DynamicToolSpec], ) -> (Vec, Vec) { - let mcp_tool_inputs = mcp_tools.as_ref().map(|mcp_tools| { - mcp_tools - .iter() - .map(|(name, tool)| ToolRegistryPlanMcpTool { - name: name.clone(), - tool, - }) - .collect::>() - }); + let mcp_tool_inputs = mcp_tools + .as_deref() + .unwrap_or_default() + .iter() + .map(TestMcpTool::as_plan_tool) + .collect::>(); let plan = build_tool_registry_plan( config, ToolRegistryPlanParams { - mcp_tools: mcp_tool_inputs.as_deref(), - deferred_mcp_tools: deferred_mcp_tools.as_deref(), + mcp_tools: mcp_tools.as_ref().map(|_| mcp_tool_inputs.as_slice()), tool_namespaces: tool_namespaces.as_ref(), discoverable_tools: discoverable_tools.as_deref(), dynamic_tools, @@ -2025,6 +1852,29 @@ fn build_specs_with_optional_tool_namespaces<'a>( (plan.specs, plan.handlers) } +#[derive(Clone)] +struct TestMcpTool { + name: ToolName, + tool: rmcp::model::Tool, + server_name: String, + connector_name: Option, + connector_description: Option, + defer_loading: bool, +} + +impl TestMcpTool { + fn as_plan_tool(&self) -> ToolRegistryPlanMcpTool<'_> { + ToolRegistryPlanMcpTool { + name: self.name.clone(), + tool: &self.tool, + server_name: self.server_name.as_str(), + connector_name: self.connector_name.as_deref(), + connector_description: self.connector_description.as_deref(), + defer_loading: self.defer_loading, + } + } +} + fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> rmcp::model::Tool { rmcp::model::Tool { name: name.to_string().into(), @@ -2089,11 +1939,10 @@ fn code_mode_augments_mcp_tool_descriptions_with_structured_output_sample() { let (tools, _) = build_specs( &tools_config, - Some(HashMap::from([( - ToolName::namespaced("mcp__sample__", "echo"), - tool, - )])), - /*deferred_mcp_tools*/ None, + Some(mcp_tool_inputs( + HashMap::from([(ToolName::namespaced("mcp__sample__", "echo"), tool)]), + /*defer_loading*/ false, + )), &[], ); @@ -2130,21 +1979,45 @@ fn discoverable_connector(id: &str, name: &str, description: &str) -> Discoverab })) } -fn deferred_mcp_tool<'a>( - tool_name: &'a str, - tool_namespace: &'a str, - server_name: &'a str, - connector_name: Option<&'a str>, - connector_description: Option<&'a str>, -) -> ToolRegistryPlanDeferredTool<'a> { - ToolRegistryPlanDeferredTool { +fn mcp_tool_input( + tool_name: &str, + tool_namespace: &str, + server_name: &str, + connector_name: Option<&str>, + connector_description: Option<&str>, + defer_loading: bool, +) -> TestMcpTool { + TestMcpTool { name: ToolName::namespaced(tool_namespace, tool_name), - server_name, - connector_name, - connector_description, + tool: mcp_tool( + tool_name, + "Deferred MCP test tool", + serde_json::json!({"type": "object"}), + ), + server_name: server_name.to_string(), + connector_name: connector_name.map(str::to_string), + connector_description: connector_description.map(str::to_string), + defer_loading, } } +fn mcp_tool_inputs( + mcp_tools: HashMap, + defer_loading: bool, +) -> Vec { + mcp_tools + .into_iter() + .map(|(name, tool)| TestMcpTool { + server_name: name.namespace.as_deref().unwrap_or("mcp").to_string(), + name, + tool, + connector_name: None, + connector_description: None, + defer_loading, + }) + .collect() +} + fn assert_contains_tool_names(tools: &[ConfiguredToolSpec], expected_subset: &[&str]) { use std::collections::HashSet; diff --git a/codex-rs/tools/src/tool_registry_plan_types.rs b/codex-rs/tools/src/tool_registry_plan_types.rs index d22335b614..627016834e 100644 --- a/codex-rs/tools/src/tool_registry_plan_types.rs +++ b/codex-rs/tools/src/tool_registry_plan_types.rs @@ -57,7 +57,6 @@ pub struct ToolRegistryPlan { #[derive(Debug, Clone, Copy)] pub struct ToolRegistryPlanParams<'a> { pub mcp_tools: Option<&'a [ToolRegistryPlanMcpTool<'a>]>, - pub deferred_mcp_tools: Option<&'a [ToolRegistryPlanDeferredTool<'a>]>, pub tool_namespaces: Option<&'a HashMap>, pub discoverable_tools: Option<&'a [DiscoverableTool]>, pub dynamic_tools: &'a [DynamicToolSpec], @@ -71,21 +70,17 @@ pub struct ToolNamespace { pub description: Option, } -/// Direct MCP tool metadata needed to expose the Responses API namespace tool -/// while registering its runtime handler with the canonical namespace/name -/// identity. +/// MCP tool metadata needed to expose the Responses API namespace tool, mark +/// deferred-loading tools, and register runtime handlers with the canonical +/// namespace/name identity. #[derive(Debug, Clone)] pub struct ToolRegistryPlanMcpTool<'a> { pub name: ToolName, pub tool: &'a rmcp::model::Tool, -} - -#[derive(Debug, Clone)] -pub struct ToolRegistryPlanDeferredTool<'a> { - pub name: ToolName, pub server_name: &'a str, pub connector_name: Option<&'a str>, pub connector_description: Option<&'a str>, + pub defer_loading: bool, } impl ToolRegistryPlan {