[mcp] Expand tool search to custom MCPs. (#16944)

- [x] Expand tool search to custom MCPs.
- [x] Rename several variables/fields to be more generic.

Updated tool & server name lifecycles:

**Raw Identity**

ToolInfo.server_name is raw MCP server name.
ToolInfo.tool.name is raw MCP tool name.
MCP calls route back to raw via parse_tool_name() returning
(tool.server_name, tool.tool.name).
mcpServerStatus/list now groups by raw server and keys tools by
Tool.name: mod.rs:599
App-server just forwards that grouped raw snapshot:
codex_message_processor.rs:5245

**Callable Names**

On list-tools, we create provisional callable_namespace / callable_name:
mcp_connection_manager.rs:1556
For non-app MCP, provisional callable name starts as raw tool name.
For codex-apps, provisional callable name is sanitized and strips
connector name/id prefix; namespace includes connector name.
Then qualify_tools() sanitizes callable namespace + name to ASCII alnum
/ _ only: mcp_tool_names.rs:128
Note: this is stricter than Responses API. Hyphen is currently replaced
with _ for code-mode compatibility.

**Collision Handling**

We do initially collapse example-server and example_server to the same
base.
Then qualify_tools() detects distinct raw namespace identities behind
the same sanitized namespace and appends a hash to the callable
namespace: mcp_tool_names.rs:137
Same idea for tool-name collisions: hash suffix goes on callable tool
name.
Final list_all_tools() map key is callable_namespace + callable_name:
mcp_connection_manager.rs:769

**Direct Model Tools**

Direct MCP tool declarations use the full qualified sanitized key as the
Responses function name.
The raw rmcp Tool is converted but renamed for model exposure.

**Tool Search / Deferred**

Tool search result namespace = final ToolInfo.callable_namespace:
tool_search.rs:85
Tool search result nested name = final ToolInfo.callable_name:
tool_search.rs:86
Deferred tool handler is registered as "{namespace}:{name}":
tool_registry_plan.rs:248
When a function call comes back, core recombines namespace + name, looks
up the full qualified key, and gets the raw server/tool for MCP
execution: codex.rs:4353

**Separate Legacy Snapshot**

collect_mcp_snapshot_from_manager_with_detail() still returns a map
keyed by qualified callable name.
mcpServerStatus/list no longer uses that; it uses
McpServerStatusSnapshot, which is raw-inventory shaped.
This commit is contained in:
Matthew Zeng
2026-04-09 13:34:52 -07:00
committed by GitHub
parent 545f3daba0
commit d7f99b0fa6
26 changed files with 1297 additions and 737 deletions

View File

@@ -101,12 +101,12 @@ pub use tool_discovery::DiscoverableToolType;
pub use tool_discovery::TOOL_SEARCH_DEFAULT_LIMIT;
pub use tool_discovery::TOOL_SEARCH_TOOL_NAME;
pub use tool_discovery::TOOL_SUGGEST_TOOL_NAME;
pub use tool_discovery::ToolSearchAppInfo;
pub use tool_discovery::ToolSearchAppSource;
pub use tool_discovery::ToolSearchResultSource;
pub use tool_discovery::ToolSearchSource;
pub use tool_discovery::ToolSearchSourceInfo;
pub use tool_discovery::ToolSuggestEntry;
pub use tool_discovery::collect_tool_search_app_infos;
pub use tool_discovery::collect_tool_search_output_tools;
pub use tool_discovery::collect_tool_search_source_infos;
pub use tool_discovery::collect_tool_suggest_entries;
pub use tool_discovery::create_tool_search_tool;
pub use tool_discovery::create_tool_suggest_tool;
@@ -116,7 +116,7 @@ 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::ToolRegistryPlanAppTool;
pub use tool_registry_plan_types::ToolRegistryPlanDeferredTool;
pub use tool_registry_plan_types::ToolRegistryPlanParams;
pub use tool_spec::ConfiguredToolSpec;
pub use tool_spec::ResponsesApiWebSearchFilters;

View File

@@ -16,13 +16,13 @@ pub const TOOL_SEARCH_DEFAULT_LIMIT: usize = 8;
pub const TOOL_SUGGEST_TOOL_NAME: &str = "tool_suggest";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ToolSearchAppInfo {
pub struct ToolSearchSourceInfo {
pub name: String,
pub description: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ToolSearchAppSource<'a> {
pub struct ToolSearchSource<'a> {
pub server_name: &'a str,
pub connector_name: Option<&'a str>,
pub connector_description: Option<&'a str>,
@@ -30,6 +30,7 @@ pub struct ToolSearchAppSource<'a> {
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ToolSearchResultSource<'a> {
pub server_name: &'a str,
pub tool_namespace: &'a str,
pub tool_name: &'a str,
pub tool: &'a rmcp::model::Tool,
@@ -143,11 +144,14 @@ pub struct ToolSuggestEntry {
pub app_connector_ids: Vec<String>,
}
pub fn create_tool_search_tool(app_tools: &[ToolSearchAppInfo], default_limit: usize) -> ToolSpec {
pub fn create_tool_search_tool(
searchable_sources: &[ToolSearchSourceInfo],
default_limit: usize,
) -> ToolSpec {
let properties = BTreeMap::from([
(
"query".to_string(),
JsonSchema::string(Some("Search query for apps tools.".to_string())),
JsonSchema::string(Some("Search query for MCP tools.".to_string())),
),
(
"limit".to_string(),
@@ -157,22 +161,22 @@ pub fn create_tool_search_tool(app_tools: &[ToolSearchAppInfo], default_limit: u
),
]);
let mut app_descriptions = BTreeMap::new();
for app_tool in app_tools {
app_descriptions
.entry(app_tool.name.clone())
let mut source_descriptions = BTreeMap::new();
for source in searchable_sources {
source_descriptions
.entry(source.name.clone())
.and_modify(|existing: &mut Option<String>| {
if existing.is_none() {
*existing = app_tool.description.clone();
*existing = source.description.clone();
}
})
.or_insert(app_tool.description.clone());
.or_insert(source.description.clone());
}
let app_descriptions = if app_descriptions.is_empty() {
let source_descriptions = if source_descriptions.is_empty() {
"None currently enabled.".to_string()
} else {
app_descriptions
source_descriptions
.into_iter()
.map(|(name, description)| match description {
Some(description) => format!("- {name}: {description}"),
@@ -183,7 +187,7 @@ pub fn create_tool_search_tool(app_tools: &[ToolSearchAppInfo], default_limit: u
};
let description = format!(
"# Apps (Connectors) tool discovery\n\nSearches over apps/connectors tool metadata with BM25 and exposes matching tools for the next model call.\n\nYou have access to all the tools of the following apps/connectors:\n{app_descriptions}\nSome of the tools may not have been provided to you upfront, and you should use this tool (`{TOOL_SEARCH_TOOL_NAME}`) to search for the required tools and load them for the apps mentioned above. For the apps mentioned above, always use `{TOOL_SEARCH_TOOL_NAME}` instead of `list_mcp_resources` or `list_mcp_resource_templates` for tool discovery."
"# MCP tool discovery\n\nSearches over MCP tool metadata with BM25 and exposes matching tools for the next model call.\n\nYou have access to tools from the following MCP servers/connectors:\n{source_descriptions}\nSome of the tools may not have been provided to you upfront, and you should use this tool (`{TOOL_SEARCH_TOOL_NAME}`) to search for the required MCP tools. For MCP tool discovery, always use `{TOOL_SEARCH_TOOL_NAME}` instead of `list_mcp_resources` or `list_mcp_resource_templates`."
);
ToolSpec::ToolSearch {
@@ -201,9 +205,12 @@ pub fn collect_tool_search_output_tools<'a>(
tool_sources: impl IntoIterator<Item = ToolSearchResultSource<'a>>,
) -> Result<Vec<ToolSearchOutputTool>, serde_json::Error> {
let grouped = tool_sources.into_iter().fold(
BTreeMap::<&'a str, Vec<ToolSearchResultSource<'a>>>::new(),
BTreeMap::<String, Vec<(String, ToolSearchResultSource<'a>)>>::new(),
|mut grouped, tool| {
grouped.entry(tool.tool_namespace).or_default().push(tool);
grouped
.entry(tool.tool_namespace.to_string())
.or_default()
.push((tool.tool_name.to_string(), tool));
grouped
},
);
@@ -215,20 +222,28 @@ pub fn collect_tool_search_output_tools<'a>(
};
let description = first_tool
.1
.connector_description
.map(str::to_string)
.or_else(|| {
first_tool
.1
.connector_name
.map(str::trim)
.filter(|connector_name| !connector_name.is_empty())
.map(|connector_name| format!("Tools for working with {connector_name}."))
})
.or_else(|| {
Some(format!(
"Tools from the {} MCP server.",
first_tool.1.server_name
))
});
let tools = tools
.iter()
.map(|tool| {
mcp_tool_to_deferred_responses_api_tool(tool.tool_name.to_string(), tool.tool)
mcp_tool_to_deferred_responses_api_tool(tool.0.clone(), tool.1.tool)
.map(ResponsesApiNamespaceTool::Function)
})
.collect::<Result<Vec<_>, _>>()?;
@@ -243,25 +258,36 @@ pub fn collect_tool_search_output_tools<'a>(
Ok(results)
}
pub fn collect_tool_search_app_infos<'a>(
app_tools: impl IntoIterator<Item = ToolSearchAppSource<'a>>,
codex_apps_server_name: &str,
) -> Vec<ToolSearchAppInfo> {
app_tools
pub fn collect_tool_search_source_infos<'a>(
searchable_tools: impl IntoIterator<Item = ToolSearchSource<'a>>,
) -> Vec<ToolSearchSourceInfo> {
searchable_tools
.into_iter()
.filter(|tool| tool.server_name == codex_apps_server_name)
.filter_map(|tool| {
let name = tool
if let Some(name) = tool
.connector_name
.map(str::trim)
.filter(|connector_name| !connector_name.is_empty())?
.to_string();
let description = tool
.connector_description
.map(str::trim)
.filter(|connector_description| !connector_description.is_empty())
.map(str::to_string);
Some(ToolSearchAppInfo { name, description })
.filter(|connector_name| !connector_name.is_empty())
{
return Some(ToolSearchSourceInfo {
name: name.to_string(),
description: tool
.connector_description
.map(str::trim)
.filter(|description| !description.is_empty())
.map(str::to_string),
});
}
let name = tool.server_name.trim();
if name.is_empty() {
return None;
}
Some(ToolSearchSourceInfo {
name: name.to_string(),
description: None,
})
})
.collect()
}

View File

@@ -26,23 +26,23 @@ fn mcp_tool(name: &str, description: &str) -> Tool {
}
#[test]
fn create_tool_search_tool_deduplicates_and_renders_enabled_apps() {
fn create_tool_search_tool_deduplicates_and_renders_enabled_sources() {
assert_eq!(
create_tool_search_tool(
&[
ToolSearchAppInfo {
ToolSearchSourceInfo {
name: "Google Drive".to_string(),
description: Some(
"Use Google Drive as the single entrypoint for Drive, Docs, Sheets, and Slides work."
.to_string(),
),
},
ToolSearchAppInfo {
ToolSearchSourceInfo {
name: "Google Drive".to_string(),
description: None,
},
ToolSearchAppInfo {
name: "Slack".to_string(),
ToolSearchSourceInfo {
name: "docs".to_string(),
description: None,
},
],
@@ -50,7 +50,7 @@ fn create_tool_search_tool_deduplicates_and_renders_enabled_apps() {
),
ToolSpec::ToolSearch {
execution: "client".to_string(),
description: "# Apps (Connectors) tool discovery\n\nSearches over apps/connectors tool metadata with BM25 and exposes matching tools for the next model call.\n\nYou have access to all the tools of the following apps/connectors:\n- Google Drive: Use Google Drive as the single entrypoint for Drive, Docs, Sheets, and Slides work.\n- Slack\nSome of the tools may not have been provided to you upfront, and you should use this tool (`tool_search`) to search for the required tools and load them for the apps mentioned above. For the apps mentioned above, always use `tool_search` instead of `list_mcp_resources` or `list_mcp_resource_templates` for tool discovery.".to_string(),
description: "# MCP tool discovery\n\nSearches over MCP tool metadata with BM25 and exposes matching tools for the next model call.\n\nYou have access to tools from the following MCP servers/connectors:\n- Google Drive: Use Google Drive as the single entrypoint for Drive, Docs, Sheets, and Slides work.\n- docs\nSome of the tools may not have been provided to you upfront, and you should use this tool (`tool_search`) to search for the required MCP tools. For MCP tool discovery, always use `tool_search` instead of `list_mcp_resources` or `list_mcp_resource_templates`.".to_string(),
parameters: JsonSchema::object(BTreeMap::from([
(
"limit".to_string(),
@@ -61,7 +61,7 @@ fn create_tool_search_tool_deduplicates_and_renders_enabled_apps() {
),
(
"query".to_string(),
JsonSchema::string(Some("Search query for apps tools.".to_string()),),
JsonSchema::string(Some("Search query for MCP tools.".to_string()),),
),
]), Some(vec!["query".to_string()]), Some(false.into())),
}
@@ -141,9 +141,11 @@ fn collect_tool_search_output_tools_groups_results_by_namespace() {
let calendar_create_event = mcp_tool("calendar-create-event", "Create a calendar event.");
let gmail_read_email = mcp_tool("gmail-read-email", "Read an email.");
let calendar_list_events = mcp_tool("calendar-list-events", "List calendar events.");
let docs_search = mcp_tool("search", "Search docs.");
let tools = collect_tool_search_output_tools([
ToolSearchResultSource {
server_name: "codex_apps",
tool_namespace: "mcp__codex_apps__calendar",
tool_name: "_create_event",
tool: &calendar_create_event,
@@ -151,6 +153,7 @@ fn collect_tool_search_output_tools_groups_results_by_namespace() {
connector_description: Some("Plan events"),
},
ToolSearchResultSource {
server_name: "codex_apps",
tool_namespace: "mcp__codex_apps__gmail",
tool_name: "_read_email",
tool: &gmail_read_email,
@@ -158,12 +161,21 @@ fn collect_tool_search_output_tools_groups_results_by_namespace() {
connector_description: Some("Read mail"),
},
ToolSearchResultSource {
server_name: "codex_apps",
tool_namespace: "mcp__codex_apps__calendar",
tool_name: "_list_events",
tool: &calendar_list_events,
connector_name: Some("Calendar"),
connector_description: Some("Plan events"),
},
ToolSearchResultSource {
server_name: "docs",
tool_namespace: "mcp__docs__",
tool_name: "search",
tool: &docs_search,
connector_name: None,
connector_description: None,
},
])
.expect("collect tool search output tools");
@@ -216,6 +228,22 @@ fn collect_tool_search_output_tools_groups_results_by_namespace() {
output_schema: None,
})],
}),
ToolSearchOutputTool::Namespace(ResponsesApiNamespace {
name: "mcp__docs__".to_string(),
description: "Tools from the docs MCP server.".to_string(),
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
name: "search".to_string(),
description: "Search docs.".to_string(),
strict: false,
defer_loading: Some(true),
parameters: JsonSchema::object(
Default::default(),
/*required*/ None,
/*additional_properties*/ None
),
output_schema: None,
})],
}),
],
);
}
@@ -225,6 +253,7 @@ fn collect_tool_search_output_tools_falls_back_to_connector_name_description() {
let gmail_batch_read_email = mcp_tool("gmail-batch-read-email", "Read multiple emails.");
let tools = collect_tool_search_output_tools([ToolSearchResultSource {
server_name: "codex_apps",
tool_namespace: "mcp__codex_apps__gmail",
tool_name: "_batch_read_email",
tool: &gmail_batch_read_email,

View File

@@ -8,13 +8,13 @@ use crate::TOOL_SUGGEST_TOOL_NAME;
use crate::ToolHandlerKind;
use crate::ToolRegistryPlan;
use crate::ToolRegistryPlanParams;
use crate::ToolSearchAppSource;
use crate::ToolSearchSource;
use crate::ToolSpec;
use crate::ToolsConfig;
use crate::ViewImageToolOptions;
use crate::WebSearchToolOptions;
use crate::collect_code_mode_tool_definitions;
use crate::collect_tool_search_app_infos;
use crate::collect_tool_search_source_infos;
use crate::collect_tool_suggest_entries;
use crate::create_apply_patch_freeform_tool;
use crate::create_apply_patch_json_tool;
@@ -250,24 +250,24 @@ pub fn build_tool_registry_plan(
}
if config.search_tool
&& let Some(app_tools) = params.app_tools
&& let Some(deferred_mcp_tools) = params.deferred_mcp_tools
{
let search_app_infos = collect_tool_search_app_infos(
app_tools.iter().map(|tool| ToolSearchAppSource {
server_name: tool.server_name,
connector_name: tool.connector_name,
connector_description: tool.connector_description,
}),
params.codex_apps_mcp_server_name,
);
let search_source_infos =
collect_tool_search_source_infos(deferred_mcp_tools.iter().map(|tool| {
ToolSearchSource {
server_name: tool.server_name,
connector_name: tool.connector_name,
connector_description: tool.connector_description,
}
}));
plan.push_spec(
create_tool_search_tool(&search_app_infos, TOOL_SEARCH_DEFAULT_LIMIT),
create_tool_search_tool(&search_source_infos, TOOL_SEARCH_DEFAULT_LIMIT),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
plan.register_handler(TOOL_SEARCH_TOOL_NAME, ToolHandlerKind::ToolSearch);
for tool in app_tools {
for tool in deferred_mcp_tools {
plan.register_handler(
format!("{}:{}", tool.tool_namespace, tool.tool_name),
ToolHandlerKind::Mcp,

View File

@@ -12,7 +12,7 @@ use crate::ResponsesApiWebSearchFilters;
use crate::ResponsesApiWebSearchUserLocation;
use crate::ToolHandlerSpec;
use crate::ToolNamespace;
use crate::ToolRegistryPlanAppTool;
use crate::ToolRegistryPlanDeferredTool;
use crate::ToolsConfigParams;
use crate::WaitAgentTimeoutOptions;
use crate::mcp_call_tool_result_output_schema;
@@ -60,7 +60,7 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
let (tools, _) = build_specs(
&config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
@@ -162,7 +162,7 @@ fn test_build_specs_collab_tools_enabled() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
@@ -202,7 +202,7 @@ fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
@@ -345,7 +345,7 @@ fn test_build_specs_enable_fanout_enables_agent_jobs_and_collab_tools() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
@@ -380,7 +380,7 @@ fn view_image_tool_omits_detail_without_original_detail_feature() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
let view_image = find_tool(&tools, VIEW_IMAGE_TOOL_NAME);
@@ -411,7 +411,7 @@ fn view_image_tool_includes_detail_with_original_detail_feature() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
let view_image = find_tool(&tools, VIEW_IMAGE_TOOL_NAME);
@@ -453,7 +453,7 @@ fn disabled_environment_omits_environment_backed_tools() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
@@ -489,7 +489,7 @@ fn test_build_specs_agent_job_worker_tools_enabled() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
@@ -526,7 +526,7 @@ fn request_user_input_description_reflects_default_mode_feature_flag() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
let request_user_input_tool = find_tool(&tools, REQUEST_USER_INPUT_TOOL_NAME);
@@ -549,7 +549,7 @@ fn request_user_input_description_reflects_default_mode_feature_flag() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
let request_user_input_tool = find_tool(&tools, REQUEST_USER_INPUT_TOOL_NAME);
@@ -577,7 +577,7 @@ fn request_permissions_requires_feature_flag() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
assert_lacks_tool_name(&tools, "request_permissions");
@@ -597,7 +597,7 @@ fn request_permissions_requires_feature_flag() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
let request_permissions_tool = find_tool(&tools, "request_permissions");
@@ -626,7 +626,7 @@ fn request_permissions_tool_is_independent_from_additional_permissions() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
@@ -652,7 +652,7 @@ fn js_repl_requires_feature_flag() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
@@ -686,7 +686,7 @@ fn js_repl_enabled_adds_tools() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
@@ -717,7 +717,7 @@ fn image_generation_tools_require_feature_and_supported_model() {
let (default_tools, _) = build_specs(
&default_tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
assert!(
@@ -740,7 +740,7 @@ fn image_generation_tools_require_feature_and_supported_model() {
let (supported_tools, _) = build_specs(
&supported_tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
assert_contains_tool_names(&supported_tools, &["image_generation"]);
@@ -766,7 +766,7 @@ fn image_generation_tools_require_feature_and_supported_model() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
assert!(
@@ -796,7 +796,7 @@ fn web_search_mode_cached_sets_external_web_access_false() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
@@ -832,7 +832,7 @@ fn web_search_mode_live_sets_external_web_access_true() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
@@ -882,7 +882,7 @@ fn web_search_config_is_forwarded_to_tool_spec() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
@@ -923,7 +923,7 @@ fn web_search_tool_type_text_and_image_sets_search_content_types() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
@@ -958,7 +958,7 @@ fn mcp_resource_tools_are_hidden_without_mcp_servers() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
@@ -989,7 +989,7 @@ fn mcp_resource_tools_are_included_when_mcp_servers_are_present() {
let (tools, _) = build_specs(
&tools_config,
Some(HashMap::new()),
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
@@ -1023,7 +1023,7 @@ fn test_parallel_support_flags() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
@@ -1050,7 +1050,7 @@ fn test_test_model_info_includes_sync_tool() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
@@ -1098,7 +1098,7 @@ fn test_build_specs_mcp_tools_converted() {
}),
),
)])),
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
@@ -1181,7 +1181,12 @@ fn test_build_specs_mcp_tools_sorted_by_name() {
),
]);
let (tools, _) = build_specs(&tools_config, Some(tools_map), /*app_tools*/ None, &[]);
let (tools, _) = build_specs(
&tools_config,
Some(tools_map),
/*deferred_mcp_tools*/ None,
&[],
);
let mcp_names: Vec<_> = tools
.iter()
@@ -1197,7 +1202,7 @@ fn test_build_specs_mcp_tools_sorted_by_name() {
}
#[test]
fn search_tool_description_lists_each_codex_apps_connector_once() {
fn search_tool_description_lists_each_mcp_source_once() {
let model_info = search_capable_model_info();
let mut features = Features::with_defaults();
features.enable(Feature::Apps);
@@ -1214,7 +1219,7 @@ fn search_tool_description_lists_each_codex_apps_connector_once() {
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
let (tools, _) = build_specs(
let (tools, handlers) = build_specs(
&tools_config,
Some(HashMap::from([
(
@@ -1231,29 +1236,32 @@ fn search_tool_description_lists_each_codex_apps_connector_once() {
),
])),
Some(vec![
app_tool(
deferred_mcp_tool(
"_create_event",
"mcp__codex_apps__calendar",
CODEX_APPS_MCP_SERVER_NAME,
Some("Calendar"),
Some("Plan events and manage your calendar."),
),
app_tool(
deferred_mcp_tool(
"_list_events",
"mcp__codex_apps__calendar",
CODEX_APPS_MCP_SERVER_NAME,
Some("Calendar"),
Some("Plan events and manage your calendar."),
),
app_tool(
deferred_mcp_tool(
"_search_threads",
"mcp__codex_apps__gmail",
CODEX_APPS_MCP_SERVER_NAME,
Some("Gmail"),
Some("Find and summarize email threads."),
),
app_tool(
"echo", "rmcp", "rmcp", /*connector_name*/ None,
deferred_mcp_tool(
"echo",
"mcp__rmcp__",
"rmcp",
/*connector_name*/ None,
/*connector_description*/ None,
),
]),
@@ -1273,14 +1281,24 @@ fn search_tool_description_lists_each_codex_apps_connector_once() {
.count(),
1
);
assert!(description.contains("- rmcp"));
assert!(!description.contains("mcp__rmcp__echo"));
assert!(handlers.contains(&ToolHandlerSpec {
name: "mcp__codex_apps__calendar:_create_event".to_string(),
kind: ToolHandlerKind::Mcp,
}));
assert!(handlers.contains(&ToolHandlerSpec {
name: "mcp__rmcp__:echo".to_string(),
kind: ToolHandlerKind::Mcp,
}));
}
#[test]
fn search_tool_requires_model_capability_and_feature_flag() {
let model_info = search_capable_model_info();
let app_tools = Some(vec![app_tool(
"calendar_create_event",
let deferred_mcp_tools = Some(vec![deferred_mcp_tool(
"_create_event",
"mcp__codex_apps__calendar",
CODEX_APPS_MCP_SERVER_NAME,
Some("Calendar"),
@@ -1305,7 +1323,7 @@ fn search_tool_requires_model_capability_and_feature_flag() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
app_tools.clone(),
deferred_mcp_tools.clone(),
&[],
);
assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME);
@@ -1323,7 +1341,7 @@ fn search_tool_requires_model_capability_and_feature_flag() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
app_tools.clone(),
deferred_mcp_tools.clone(),
&[],
);
assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME);
@@ -1340,7 +1358,12 @@ fn search_tool_requires_model_capability_and_feature_flag() {
sandbox_policy: &SandboxPolicy::DangerFullAccess,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, app_tools, &[]);
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
deferred_mcp_tools,
&[],
);
assert_contains_tool_names(&tools, &[TOOL_SEARCH_TOOL_NAME]);
}
@@ -1366,8 +1389,7 @@ fn tool_suggest_is_not_registered_without_feature_flag() {
let (tools, _) = build_specs_with_discoverable_tools(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*tool_namespaces*/ None,
/*deferred_mcp_tools*/ None,
Some(vec![discoverable_connector(
"connector_2128aebfecb84f64a069897515042a44",
"Google Calendar",
@@ -1407,8 +1429,7 @@ fn tool_suggest_can_be_registered_without_search_tool() {
let (tools, _) = build_specs_with_discoverable_tools(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*tool_namespaces*/ None,
/*deferred_mcp_tools*/ None,
Some(vec![discoverable_connector(
"connector_2128aebfecb84f64a069897515042a44",
"Google Calendar",
@@ -1476,8 +1497,7 @@ fn tool_suggest_description_lists_discoverable_tools() {
let (tools, _) = build_specs_with_discoverable_tools(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*tool_namespaces*/ None,
/*deferred_mcp_tools*/ None,
Some(discoverable_tools),
&[],
);
@@ -1572,7 +1592,7 @@ fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() {
}),
),
)])),
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
@@ -1661,7 +1681,7 @@ fn code_mode_preserves_nullable_and_literal_mcp_input_shapes() {
}),
),
)])),
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
@@ -1700,7 +1720,7 @@ fn code_mode_augments_builtin_tool_descriptions_with_typed_sample() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
let ToolSpec::Function(ResponsesApiTool { description, .. }) =
@@ -1736,7 +1756,7 @@ fn code_mode_only_exec_description_includes_full_nested_tool_details() {
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
let ToolSpec::Freeform(FreeformTool { description, .. }) = &find_tool(&tools, "exec").spec
@@ -1773,7 +1793,7 @@ fn code_mode_exec_description_omits_nested_tool_details_when_not_code_mode_only(
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
let ToolSpec::Freeform(FreeformTool { description, .. }) = &find_tool(&tools, "exec").spec
@@ -1833,14 +1853,13 @@ fn search_capable_model_info() -> ModelInfo {
fn build_specs<'a>(
config: &ToolsConfig,
mcp_tools: Option<HashMap<String, rmcp::model::Tool>>,
app_tools: Option<Vec<ToolRegistryPlanAppTool<'a>>>,
deferred_mcp_tools: Option<Vec<ToolRegistryPlanDeferredTool<'a>>>,
dynamic_tools: &[DynamicToolSpec],
) -> (Vec<ConfiguredToolSpec>, Vec<ToolHandlerSpec>) {
build_specs_with_discoverable_tools(
config,
mcp_tools,
app_tools,
/*tool_namespaces*/ None,
deferred_mcp_tools,
/*discoverable_tools*/ None,
dynamic_tools,
)
@@ -1849,16 +1868,15 @@ fn build_specs<'a>(
fn build_specs_with_discoverable_tools<'a>(
config: &ToolsConfig,
mcp_tools: Option<HashMap<String, rmcp::model::Tool>>,
app_tools: Option<Vec<ToolRegistryPlanAppTool<'a>>>,
tool_namespaces: Option<HashMap<String, ToolNamespace>>,
deferred_mcp_tools: Option<Vec<ToolRegistryPlanDeferredTool<'a>>>,
discoverable_tools: Option<Vec<DiscoverableTool>>,
dynamic_tools: &[DynamicToolSpec],
) -> (Vec<ConfiguredToolSpec>, Vec<ToolHandlerSpec>) {
build_specs_with_optional_tool_namespaces(
config,
mcp_tools,
tool_namespaces,
app_tools,
deferred_mcp_tools,
/*tool_namespaces*/ None,
discoverable_tools,
dynamic_tools,
)
@@ -1867,8 +1885,8 @@ fn build_specs_with_discoverable_tools<'a>(
fn build_specs_with_optional_tool_namespaces<'a>(
config: &ToolsConfig,
mcp_tools: Option<HashMap<String, rmcp::model::Tool>>,
deferred_mcp_tools: Option<Vec<ToolRegistryPlanDeferredTool<'a>>>,
tool_namespaces: Option<HashMap<String, ToolNamespace>>,
app_tools: Option<Vec<ToolRegistryPlanAppTool<'a>>>,
discoverable_tools: Option<Vec<DiscoverableTool>>,
dynamic_tools: &[DynamicToolSpec],
) -> (Vec<ConfiguredToolSpec>, Vec<ToolHandlerSpec>) {
@@ -1876,13 +1894,12 @@ fn build_specs_with_optional_tool_namespaces<'a>(
config,
ToolRegistryPlanParams {
mcp_tools: mcp_tools.as_ref(),
deferred_mcp_tools: deferred_mcp_tools.as_deref(),
tool_namespaces: tool_namespaces.as_ref(),
app_tools: app_tools.as_deref(),
discoverable_tools: discoverable_tools.as_deref(),
dynamic_tools,
default_agent_type_description: DEFAULT_AGENT_TYPE_DESCRIPTION,
wait_agent_timeouts: wait_agent_timeout_options(),
codex_apps_mcp_server_name: CODEX_APPS_MCP_SERVER_NAME,
},
);
(plan.specs, plan.handlers)
@@ -1921,14 +1938,14 @@ fn discoverable_connector(id: &str, name: &str, description: &str) -> Discoverab
}))
}
fn app_tool<'a>(
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>,
) -> ToolRegistryPlanAppTool<'a> {
ToolRegistryPlanAppTool {
) -> ToolRegistryPlanDeferredTool<'a> {
ToolRegistryPlanDeferredTool {
tool_name,
tool_namespace,
server_name,

View File

@@ -58,13 +58,12 @@ pub struct ToolRegistryPlan {
#[derive(Debug, Clone, Copy)]
pub struct ToolRegistryPlanParams<'a> {
pub mcp_tools: Option<&'a HashMap<String, McpTool>>,
pub deferred_mcp_tools: Option<&'a [ToolRegistryPlanDeferredTool<'a>]>,
pub tool_namespaces: Option<&'a HashMap<String, ToolNamespace>>,
pub app_tools: Option<&'a [ToolRegistryPlanAppTool<'a>]>,
pub discoverable_tools: Option<&'a [DiscoverableTool]>,
pub dynamic_tools: &'a [DynamicToolSpec],
pub default_agent_type_description: &'a str,
pub wait_agent_timeouts: WaitAgentTimeoutOptions,
pub codex_apps_mcp_server_name: &'a str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -74,7 +73,7 @@ pub struct ToolNamespace {
}
#[derive(Debug, Clone, Copy)]
pub struct ToolRegistryPlanAppTool<'a> {
pub struct ToolRegistryPlanDeferredTool<'a> {
pub tool_name: &'a str,
pub tool_namespace: &'a str,
pub server_name: &'a str,