Compare commits

...

2 Commits

Author SHA1 Message Date
Sayan Sisodiya
f53e6061a4 Add full MCP tool definitions 2026-04-20 15:51:26 -07:00
Sayan Sisodiya
104a3f71a6 Introduce canonical tool definitions 2026-04-20 15:23:35 -07:00
21 changed files with 652 additions and 405 deletions

View File

@@ -202,6 +202,8 @@ mod tests {
use codex_tools::ResponsesApiNamespace;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ResponsesApiTool;
use codex_tools::ToolDefinition;
use codex_tools::ToolLoadingPolicy;
use pretty_assertions::assert_eq;
use rmcp::model::Tool;
use std::sync::Arc;
@@ -444,6 +446,21 @@ mod tests {
mcp_tools: Option<&std::collections::HashMap<String, ToolInfo>>,
dynamic_tools: &[DynamicToolSpec],
) -> ToolSearchHandler {
ToolSearchHandler::new(build_tool_search_entries(mcp_tools, dynamic_tools))
let mcp_tools = mcp_tools.map(|tools| {
tools
.values()
.map(|tool| {
crate::tools::mcp_tool_definition::mcp_tool_info_to_tool_definition(
tool,
ToolLoadingPolicy::Deferred,
)
.expect("convert deferred MCP test tool")
})
.collect::<Vec<ToolDefinition>>()
});
ToolSearchHandler::new(build_tool_search_entries(
mcp_tools.as_deref(),
dynamic_tools,
))
}
}

View File

@@ -0,0 +1,84 @@
use codex_mcp::ToolInfo;
use codex_tools::ToolDefinition;
use codex_tools::ToolLoadingPolicy;
use codex_tools::ToolPresentation;
use codex_tools::ToolSearchMetadata;
use codex_tools::mcp_tool_to_tool_definition;
pub(crate) fn mcp_tool_info_to_tool_definition(
info: &ToolInfo,
loading: ToolLoadingPolicy,
) -> Result<ToolDefinition, serde_json::Error> {
let mut definition = mcp_tool_to_tool_definition(&info.canonical_tool_name(), &info.tool)?;
if loading.is_deferred() {
definition = definition.into_deferred();
definition.search = Some(mcp_tool_search_metadata(info));
}
definition.presentation = mcp_tool_presentation(info, loading);
Ok(definition)
}
fn mcp_tool_presentation(info: &ToolInfo, loading: ToolLoadingPolicy) -> Option<ToolPresentation> {
let namespace_description = match loading {
ToolLoadingPolicy::Eager => non_empty(info.connector_description.as_deref())
.or_else(|| non_empty(info.server_instructions.as_deref())),
ToolLoadingPolicy::Deferred => {
non_empty(info.connector_description.as_deref()).or_else(|| {
non_empty(info.connector_name.as_deref())
.map(|name| format!("Tools for working with {name}."))
})
}
};
namespace_description.map(|namespace_description| ToolPresentation {
namespace_display_name: None,
namespace_description: Some(namespace_description),
})
}
fn mcp_tool_search_metadata(info: &ToolInfo) -> ToolSearchMetadata {
let source_name = non_empty(info.connector_name.as_deref())
.unwrap_or_else(|| info.server_name.trim().to_string());
let source_description = non_empty(info.connector_description.as_deref());
let mut extra_terms = Vec::new();
push_non_empty(&mut extra_terms, &info.callable_name);
push_non_empty(&mut extra_terms, info.tool.name.as_ref());
push_non_empty(&mut extra_terms, &info.server_name);
if let Some(title) = info.tool.title.as_deref() {
push_non_empty(&mut extra_terms, title);
}
if let Some(connector_name) = info.connector_name.as_deref() {
push_non_empty(&mut extra_terms, connector_name);
}
if let Some(connector_description) = info.connector_description.as_deref() {
push_non_empty(&mut extra_terms, connector_description);
}
for plugin_display_name in &info.plugin_display_names {
push_non_empty(&mut extra_terms, plugin_display_name);
}
ToolSearchMetadata {
source_name,
source_description,
extra_terms,
limit_bucket: Some(info.server_name.clone()),
}
}
fn push_non_empty(parts: &mut Vec<String>, value: &str) {
if let Some(value) = non_empty(Some(value)) {
parts.push(value);
}
}
fn non_empty(value: Option<&str>) -> Option<String> {
value
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
}
#[cfg(test)]
#[path = "mcp_tool_definition_tests.rs"]
mod tests;

View File

@@ -0,0 +1,108 @@
use super::*;
use codex_tools::JsonSchema;
use codex_tools::ToolExecution;
use codex_tools::ToolName;
use codex_tools::mcp_call_tool_result_output_schema;
use pretty_assertions::assert_eq;
use rmcp::model::Tool;
use std::collections::BTreeMap;
use std::sync::Arc;
fn tool_info() -> ToolInfo {
ToolInfo {
server_name: "codex_apps".to_string(),
callable_name: "create_event".to_string(),
callable_namespace: "mcp__calendar__".to_string(),
server_instructions: Some("Use the calendar carefully.".to_string()),
tool: Tool {
name: "calendar-create-event".to_string().into(),
title: None,
description: Some("Create events".to_string().into()),
input_schema: Arc::new(rmcp::model::object(serde_json::json!({
"type": "object",
"properties": {
"title": {"type": "string"}
},
"additionalProperties": false
}))),
output_schema: None,
annotations: None,
execution: None,
icons: None,
meta: None,
},
connector_id: Some("calendar".to_string()),
connector_name: Some("Calendar".to_string()),
plugin_display_names: vec!["calendar-plugin".to_string()],
connector_description: None,
}
}
#[test]
fn eager_mcp_tool_info_to_tool_definition_uses_server_instructions_for_namespace() {
assert_eq!(
mcp_tool_info_to_tool_definition(&tool_info(), ToolLoadingPolicy::Eager)
.expect("convert MCP tool info"),
ToolDefinition {
name: ToolName::namespaced("mcp__calendar__", "create_event"),
description: "Create events".to_string(),
input_schema: JsonSchema::object(
BTreeMap::from([(
"title".to_string(),
JsonSchema::string(/*description*/ None),
)]),
/*required*/ None,
Some(false.into())
),
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({}))),
loading: ToolLoadingPolicy::Eager,
execution: ToolExecution::Mcp,
presentation: Some(ToolPresentation {
namespace_display_name: None,
namespace_description: Some("Use the calendar carefully.".to_string()),
}),
search: None,
supports_parallel_tool_calls: false,
}
);
}
#[test]
fn deferred_mcp_tool_info_to_tool_definition_populates_search_metadata() {
assert_eq!(
mcp_tool_info_to_tool_definition(&tool_info(), ToolLoadingPolicy::Deferred)
.expect("convert MCP tool info"),
ToolDefinition {
name: ToolName::namespaced("mcp__calendar__", "create_event"),
description: "Create events".to_string(),
input_schema: JsonSchema::object(
BTreeMap::from([(
"title".to_string(),
JsonSchema::string(/*description*/ None),
)]),
/*required*/ None,
Some(false.into())
),
output_schema: None,
loading: ToolLoadingPolicy::Deferred,
execution: ToolExecution::Mcp,
presentation: Some(ToolPresentation {
namespace_display_name: None,
namespace_description: Some("Tools for working with Calendar.".to_string()),
}),
search: Some(ToolSearchMetadata {
source_name: "Calendar".to_string(),
source_description: None,
extra_terms: vec![
"create_event".to_string(),
"calendar-create-event".to_string(),
"codex_apps".to_string(),
"Calendar".to_string(),
"calendar-plugin".to_string(),
],
limit_bucket: Some("codex_apps".to_string()),
}),
supports_parallel_tool_calls: false,
}
);
}

View File

@@ -3,6 +3,7 @@ pub(crate) mod context;
pub(crate) mod events;
pub(crate) mod handlers;
pub(crate) mod js_repl;
pub(crate) mod mcp_tool_definition;
pub(crate) mod network_approval;
pub(crate) mod orchestrator;
pub(crate) mod parallel;

View File

@@ -11,11 +11,11 @@ use codex_tools::AdditionalProperties;
use codex_tools::DiscoverableTool;
use codex_tools::JsonSchema;
use codex_tools::ResponsesApiTool;
use codex_tools::ToolDefinition;
use codex_tools::ToolHandlerKind;
use codex_tools::ToolLoadingPolicy;
use codex_tools::ToolName;
use codex_tools::ToolNamespace;
use codex_tools::ToolRegistryPlanDeferredTool;
use codex_tools::ToolRegistryPlanMcpTool;
use codex_tools::ToolRegistryPlanParams;
use codex_tools::ToolUserShellType;
use codex_tools::ToolsConfig;
@@ -36,38 +36,59 @@ pub(crate) fn tool_user_shell_type(user_shell: &Shell) -> ToolUserShellType {
}
}
struct McpToolPlanInputs<'a> {
mcp_tools: Vec<ToolRegistryPlanMcpTool<'a>>,
struct McpToolPlanInputs {
mcp_tools: Vec<ToolDefinition>,
tool_namespaces: HashMap<String, ToolNamespace>,
}
fn map_mcp_tools_for_plan(mcp_tools: &HashMap<String, ToolInfo>) -> McpToolPlanInputs<'_> {
fn map_mcp_tools_for_plan(mcp_tools: &HashMap<String, ToolInfo>) -> McpToolPlanInputs {
let mcp_tools = mcp_tool_definitions_for_plan(mcp_tools, ToolLoadingPolicy::Eager);
let tool_namespaces = mcp_tools
.iter()
.filter_map(|tool| {
let namespace = tool.name.namespace.clone()?;
Some((
namespace.clone(),
ToolNamespace {
name: namespace,
description: tool
.presentation
.as_ref()
.and_then(|presentation| presentation.namespace_description.clone()),
},
))
})
.collect();
McpToolPlanInputs {
mcp_tools: mcp_tools
.values()
.map(|tool| ToolRegistryPlanMcpTool {
name: tool.canonical_tool_name(),
tool: &tool.tool,
})
.collect(),
tool_namespaces: mcp_tools
.values()
.map(|tool| {
(
tool.callable_namespace.clone(),
ToolNamespace {
name: tool.callable_namespace.clone(),
description: tool
.connector_description
.clone()
.or_else(|| tool.server_instructions.clone()),
},
)
})
.collect(),
mcp_tools,
tool_namespaces,
}
}
fn mcp_tool_definitions_for_plan(
mcp_tools: &HashMap<String, ToolInfo>,
loading: ToolLoadingPolicy,
) -> Vec<ToolDefinition> {
use crate::tools::mcp_tool_definition::mcp_tool_info_to_tool_definition;
mcp_tools
.values()
.filter_map(
|tool| match mcp_tool_info_to_tool_definition(tool, loading) {
Ok(tool_definition) => Some(tool_definition),
Err(error) => {
let tool_name = tool.canonical_tool_name();
tracing::error!(
"Failed to convert MCP tool `{tool_name}` to tool definition: {error:?}"
);
None
}
},
)
.collect()
}
pub(crate) fn build_specs_with_discoverable_tools(
config: &ToolsConfig,
mcp_tools: Option<HashMap<String, ToolInfo>>,
@@ -112,17 +133,9 @@ 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::<Vec<_>>()
});
let deferred_mcp_tool_definitions = deferred_mcp_tools
.as_ref()
.map(|tools| mcp_tool_definitions_for_plan(tools, ToolLoadingPolicy::Deferred));
let default_agent_type_description =
crate::agent::role::spawn_tool_spec::build(&std::collections::BTreeMap::new());
let plan = build_tool_registry_plan(
@@ -131,7 +144,7 @@ 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(),
deferred_mcp_tools: deferred_mcp_tool_definitions.as_deref(),
tool_namespaces: mcp_tool_plan_inputs
.as_ref()
.map(|inputs| &inputs.tool_namespaces),
@@ -266,7 +279,7 @@ 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_mcp_tool_definitions.as_deref(),
&deferred_dynamic_tools,
);
tool_search_handler = Some(Arc::new(ToolSearchHandler::new(entries)));

View File

@@ -32,7 +32,8 @@ 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_tool_definition;
use codex_tools::tool_definition_to_responses_api_tool;
use codex_utils_absolute_path::AbsolutePathBuf;
use core_test_support::assert_regex_match;
use pretty_assertions::assert_eq;
@@ -129,12 +130,14 @@ fn deferred_responses_api_tool_serializes_with_defer_loading() {
}),
);
let tool_definition = mcp_tool_to_tool_definition(
&ToolName::namespaced("mcp__codex_apps__", "lookup_order"),
&tool,
)
.expect("convert deferred tool")
.into_deferred();
let serialized = serde_json::to_value(ToolSpec::Function(
mcp_tool_to_deferred_responses_api_tool(
&ToolName::namespaced("mcp__codex_apps__", "lookup_order"),
&tool,
)
.expect("convert deferred tool"),
tool_definition_to_responses_api_tool(&tool_definition),
))
.expect("serialize deferred tool");

View File

@@ -1,10 +1,8 @@
use codex_mcp::ToolInfo;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_tools::ToolDefinition;
use codex_tools::ToolSearchOutputTool;
use codex_tools::ToolSearchResultSource;
use codex_tools::dynamic_tool_to_responses_api_tool;
use codex_tools::tool_search_result_source_to_output_tool;
use std::collections::HashMap;
use codex_tools::tool_definition_to_tool_search_output_tool;
#[derive(Clone)]
pub(crate) struct ToolSearchEntry {
@@ -14,25 +12,17 @@ pub(crate) struct ToolSearchEntry {
}
pub(crate) fn build_tool_search_entries(
mcp_tools: Option<&HashMap<String, ToolInfo>>,
mcp_tools: Option<&[ToolDefinition]>,
dynamic_tools: &[DynamicToolSpec],
) -> Vec<ToolSearchEntry> {
let mut entries = Vec::new();
let mut mcp_tools = mcp_tools
.map(|tools| tools.values().collect::<Vec<_>>())
.map(|tools| tools.iter().collect::<Vec<_>>())
.unwrap_or_default();
mcp_tools.sort_by_key(|info| info.canonical_tool_name().display());
for info in mcp_tools {
match mcp_tool_search_entry(info) {
Ok(entry) => entries.push(entry),
Err(error) => {
let tool_name = info.canonical_tool_name();
tracing::error!(
"Failed to convert deferred MCP tool `{tool_name}` to OpenAI tool: {error:?}"
);
}
}
mcp_tools.sort_by_key(|tool| tool.name.display());
for tool in mcp_tools {
entries.push(tool_definition_search_entry(tool));
}
let mut dynamic_tools = dynamic_tools.iter().collect::<Vec<_>>();
@@ -52,19 +42,15 @@ pub(crate) fn build_tool_search_entries(
entries
}
fn mcp_tool_search_entry(info: &ToolInfo) -> Result<ToolSearchEntry, serde_json::Error> {
Ok(ToolSearchEntry {
search_text: build_mcp_search_text(info),
output: tool_search_result_source_to_output_tool(ToolSearchResultSource {
server_name: info.server_name.as_str(),
tool_namespace: info.callable_namespace.as_str(),
tool_name: info.callable_name.as_str(),
tool: &info.tool,
connector_name: info.connector_name.as_deref(),
connector_description: info.connector_description.as_deref(),
})?,
limit_bucket: Some(info.server_name.clone()),
})
fn tool_definition_search_entry(tool: &ToolDefinition) -> ToolSearchEntry {
ToolSearchEntry {
search_text: build_tool_definition_search_text(tool),
output: tool_definition_to_tool_search_output_tool(tool),
limit_bucket: tool
.search
.as_ref()
.and_then(|metadata| metadata.limit_bucket.clone()),
}
}
fn dynamic_tool_search_entry(tool: &DynamicToolSpec) -> Result<ToolSearchEntry, serde_json::Error> {
@@ -75,52 +61,25 @@ fn dynamic_tool_search_entry(tool: &DynamicToolSpec) -> Result<ToolSearchEntry,
})
}
fn build_mcp_search_text(info: &ToolInfo) -> String {
fn build_tool_definition_search_text(tool: &ToolDefinition) -> String {
let mut parts = vec![
info.canonical_tool_name().display(),
info.callable_name.clone(),
info.tool.name.to_string(),
info.server_name.clone(),
tool.name.display(),
tool.name.name.clone(),
tool.description.clone(),
];
if let Some(title) = info.tool.title.as_deref()
&& !title.trim().is_empty()
{
parts.push(title.to_string());
}
if let Some(description) = info.tool.description.as_deref()
&& !description.trim().is_empty()
{
parts.push(description.to_string());
}
if let Some(connector_name) = info.connector_name.as_deref()
&& !connector_name.trim().is_empty()
{
parts.push(connector_name.to_string());
}
if let Some(connector_description) = info.connector_description.as_deref()
&& !connector_description.trim().is_empty()
{
parts.push(connector_description.to_string());
if let Some(search) = tool.search.as_ref() {
parts.push(search.source_name.clone());
if let Some(source_description) = search.source_description.as_ref() {
parts.push(source_description.clone());
}
parts.extend(search.extra_terms.clone());
}
parts.extend(
info.plugin_display_names
.iter()
.map(String::as_str)
.map(str::trim)
.filter(|name| !name.is_empty())
.map(str::to_string),
);
parts.extend(
info.tool
.input_schema
.get("properties")
.and_then(serde_json::Value::as_object)
tool.input_schema
.properties
.as_ref()
.map(|map| map.keys().cloned().collect::<Vec<_>>())
.unwrap_or_default(),
);

View File

@@ -11,6 +11,10 @@ schema and Responses API tool primitives that no longer need to live in
- `JsonSchema`
- `AdditionalProperties`
- `ToolDefinition`
- `ToolLoadingPolicy`
- `ToolExecution`
- `ToolPresentation`
- `ToolSearchMetadata`
- `ToolSpec`
- `ConfiguredToolSpec`
- `ResponsesApiTool`
@@ -32,12 +36,13 @@ schema and Responses API tool primitives that no longer need to live in
- `parse_tool_input_schema()`
- `parse_dynamic_tool()`
- `parse_mcp_tool()`
- `dynamic_tool_to_tool_definition()`
- `mcp_tool_to_tool_definition()`
- `create_tools_json_for_responses_api()`
- `mcp_call_tool_result_output_schema()`
- `tool_definition_to_responses_api_tool()`
- `tool_definition_to_tool_search_output_tool()`
- `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()`

View File

@@ -1,8 +1,13 @@
use crate::ToolDefinition;
use crate::ToolExecution;
use crate::ToolLoadingPolicy;
use crate::ToolName;
use crate::parse_tool_input_schema;
use codex_protocol::dynamic_tools::DynamicToolSpec;
pub fn parse_dynamic_tool(tool: &DynamicToolSpec) -> Result<ToolDefinition, serde_json::Error> {
pub fn dynamic_tool_to_tool_definition(
tool: &DynamicToolSpec,
) -> Result<ToolDefinition, serde_json::Error> {
let DynamicToolSpec {
name,
description,
@@ -10,14 +15,28 @@ pub fn parse_dynamic_tool(tool: &DynamicToolSpec) -> Result<ToolDefinition, serd
defer_loading,
} = tool;
Ok(ToolDefinition {
name: name.clone(),
name: ToolName::plain(name.clone()),
description: description.clone(),
input_schema: parse_tool_input_schema(input_schema)?,
output_schema: None,
defer_loading: *defer_loading,
loading: if *defer_loading {
ToolLoadingPolicy::Deferred
} else {
ToolLoadingPolicy::Eager
},
execution: ToolExecution::Dynamic,
presentation: None,
search: None,
supports_parallel_tool_calls: false,
})
}
// TODO(tool-definition-unification): migrate remaining callers to
// `dynamic_tool_to_tool_definition` and remove this compatibility wrapper.
pub fn parse_dynamic_tool(tool: &DynamicToolSpec) -> Result<ToolDefinition, serde_json::Error> {
dynamic_tool_to_tool_definition(tool)
}
#[cfg(test)]
#[path = "dynamic_tool_tests.rs"]
mod tests;

View File

@@ -1,6 +1,9 @@
use super::parse_dynamic_tool;
use crate::JsonSchema;
use crate::ToolDefinition;
use crate::ToolExecution;
use crate::ToolLoadingPolicy;
use crate::ToolName;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
@@ -23,7 +26,7 @@ fn parse_dynamic_tool_sanitizes_input_schema() {
assert_eq!(
parse_dynamic_tool(&tool).expect("parse dynamic tool"),
ToolDefinition {
name: "lookup_ticket".to_string(),
name: ToolName::plain("lookup_ticket"),
description: "Fetch a ticket".to_string(),
input_schema: JsonSchema::object(
BTreeMap::from([(
@@ -34,7 +37,11 @@ fn parse_dynamic_tool_sanitizes_input_schema() {
/*additional_properties*/ None
),
output_schema: None,
defer_loading: false,
loading: ToolLoadingPolicy::Eager,
execution: ToolExecution::Dynamic,
presentation: None,
search: None,
supports_parallel_tool_calls: false,
}
);
}
@@ -54,7 +61,7 @@ fn parse_dynamic_tool_preserves_defer_loading() {
assert_eq!(
parse_dynamic_tool(&tool).expect("parse dynamic tool"),
ToolDefinition {
name: "lookup_ticket".to_string(),
name: ToolName::plain("lookup_ticket"),
description: "Fetch a ticket".to_string(),
input_schema: JsonSchema::object(
BTreeMap::new(),
@@ -62,7 +69,11 @@ fn parse_dynamic_tool_preserves_defer_loading() {
/*additional_properties*/ None
),
output_schema: None,
defer_loading: true,
loading: ToolLoadingPolicy::Deferred,
execution: ToolExecution::Dynamic,
presentation: None,
search: None,
supports_parallel_tool_calls: false,
}
);
}

View File

@@ -50,6 +50,7 @@ pub use code_mode::create_code_mode_tool;
pub use code_mode::create_wait_tool;
pub use code_mode::tool_spec_to_code_mode_tool_definition;
pub use codex_protocol::ToolName;
pub use dynamic_tool::dynamic_tool_to_tool_definition;
pub use dynamic_tool::parse_dynamic_tool;
pub use image_detail::can_request_original_image_detail;
pub use image_detail::normalize_output_image_detail;
@@ -73,6 +74,7 @@ pub use mcp_resource_tool::create_list_mcp_resource_templates_tool;
pub use mcp_resource_tool::create_list_mcp_resources_tool;
pub use mcp_resource_tool::create_read_mcp_resource_tool;
pub use mcp_tool::mcp_call_tool_result_output_schema;
pub use mcp_tool::mcp_tool_to_tool_definition;
pub use mcp_tool::parse_mcp_tool;
pub use plan_tool::create_update_plan_tool;
pub use request_user_input_tool::REQUEST_USER_INPUT_TOOL_NAME;
@@ -88,9 +90,8 @@ pub use responses_api::ResponsesApiTool;
pub use responses_api::ToolSearchOutputTool;
pub(crate) use responses_api::default_namespace_description;
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 responses_api::tool_definition_to_tool_search_output_tool;
pub use tool_config::ShellCommandBackendConfig;
pub use tool_config::ToolUserShellType;
pub use tool_config::ToolsConfig;
@@ -98,6 +99,10 @@ pub use tool_config::ToolsConfigParams;
pub use tool_config::UnifiedExecShellMode;
pub use tool_config::ZshForkConfig;
pub use tool_definition::ToolDefinition;
pub use tool_definition::ToolExecution;
pub use tool_definition::ToolLoadingPolicy;
pub use tool_definition::ToolPresentation;
pub use tool_definition::ToolSearchMetadata;
pub use tool_discovery::DiscoverablePluginInfo;
pub use tool_discovery::DiscoverableTool;
pub use tool_discovery::DiscoverableToolAction;
@@ -105,23 +110,17 @@ 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::ToolSearchResultSource;
pub use tool_discovery::ToolSearchSource;
pub use tool_discovery::ToolSearchSourceInfo;
pub use tool_discovery::ToolSuggestEntry;
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;
pub use tool_discovery::filter_tool_suggest_discoverable_tools_for_client;
pub use tool_discovery::tool_search_result_source_to_output_tool;
pub use tool_registry_plan::build_tool_registry_plan;
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;
pub use tool_spec::ResponsesApiWebSearchFilters;

View File

@@ -1,9 +1,18 @@
use crate::ToolDefinition;
use crate::ToolExecution;
use crate::ToolLoadingPolicy;
use crate::ToolName;
use crate::parse_tool_input_schema;
use serde_json::Value as JsonValue;
use serde_json::json;
pub fn parse_mcp_tool(tool: &rmcp::model::Tool) -> Result<ToolDefinition, serde_json::Error> {
// TODO(tool-definition-unification): remove this incomplete raw MCP adapter
// once callers can use a full `ToolInfo -> ToolDefinition` adapter that also
// populates MCP presentation/search metadata.
pub fn mcp_tool_to_tool_definition(
tool_name: &ToolName,
tool: &rmcp::model::Tool,
) -> Result<ToolDefinition, serde_json::Error> {
let mut serialized_input_schema = serde_json::Value::Object(tool.input_schema.as_ref().clone());
// OpenAI models mandate the "properties" field in the schema. Some MCP
@@ -26,16 +35,26 @@ pub fn parse_mcp_tool(tool: &rmcp::model::Tool) -> Result<ToolDefinition, serde_
.unwrap_or_else(|| JsonValue::Object(serde_json::Map::new()));
Ok(ToolDefinition {
name: tool.name.to_string(),
name: tool_name.clone(),
description: tool.description.clone().map(Into::into).unwrap_or_default(),
input_schema,
output_schema: Some(mcp_call_tool_result_output_schema(
structured_content_schema,
)),
defer_loading: false,
loading: ToolLoadingPolicy::Eager,
execution: ToolExecution::Mcp,
presentation: None,
search: None,
supports_parallel_tool_calls: false,
})
}
// TODO(tool-definition-unification): remove this compatibility wrapper once
// callers can use the full `ToolInfo -> ToolDefinition` adapter.
pub fn parse_mcp_tool(tool: &rmcp::model::Tool) -> Result<ToolDefinition, serde_json::Error> {
mcp_tool_to_tool_definition(&ToolName::plain(tool.name.to_string()), tool)
}
pub fn mcp_call_tool_result_output_schema(structured_content_schema: JsonValue) -> JsonValue {
json!({
"type": "object",

View File

@@ -1,7 +1,11 @@
use super::mcp_call_tool_result_output_schema;
use super::mcp_tool_to_tool_definition;
use super::parse_mcp_tool;
use crate::JsonSchema;
use crate::ToolDefinition;
use crate::ToolExecution;
use crate::ToolLoadingPolicy;
use crate::ToolName;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
@@ -19,6 +23,40 @@ fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> r
}
}
#[test]
fn mcp_tool_to_tool_definition_uses_canonical_tool_name() {
let tool = mcp_tool(
"raw_lookup",
"Look up an order",
serde_json::json!({
"type": "object"
}),
);
assert_eq!(
mcp_tool_to_tool_definition(
&ToolName::namespaced("mcp__orders__", "lookup_order"),
&tool,
)
.expect("convert MCP tool"),
ToolDefinition {
name: ToolName::namespaced("mcp__orders__", "lookup_order"),
description: "Look up an order".to_string(),
input_schema: JsonSchema::object(
BTreeMap::new(),
/*required*/ None,
/*additional_properties*/ None
),
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({}))),
loading: ToolLoadingPolicy::Eager,
execution: ToolExecution::Mcp,
presentation: None,
search: None,
supports_parallel_tool_calls: false,
}
);
}
#[test]
fn parse_mcp_tool_inserts_empty_properties() {
let tool = mcp_tool(
@@ -32,7 +70,7 @@ fn parse_mcp_tool_inserts_empty_properties() {
assert_eq!(
parse_mcp_tool(&tool).expect("parse MCP tool"),
ToolDefinition {
name: "no_props".to_string(),
name: ToolName::plain("no_props"),
description: "No properties".to_string(),
input_schema: JsonSchema::object(
BTreeMap::new(),
@@ -40,7 +78,11 @@ fn parse_mcp_tool_inserts_empty_properties() {
/*additional_properties*/ None
),
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({}))),
defer_loading: false,
loading: ToolLoadingPolicy::Eager,
execution: ToolExecution::Mcp,
presentation: None,
search: None,
supports_parallel_tool_calls: false,
}
);
}
@@ -70,7 +112,7 @@ fn parse_mcp_tool_preserves_top_level_output_schema() {
assert_eq!(
parse_mcp_tool(&tool).expect("parse MCP tool"),
ToolDefinition {
name: "with_output".to_string(),
name: ToolName::plain("with_output"),
description: "Has output schema".to_string(),
input_schema: JsonSchema::object(
BTreeMap::new(),
@@ -87,7 +129,11 @@ fn parse_mcp_tool_preserves_top_level_output_schema() {
},
"required": ["result"]
}))),
defer_loading: false,
loading: ToolLoadingPolicy::Eager,
execution: ToolExecution::Mcp,
presentation: None,
search: None,
supports_parallel_tool_calls: false,
}
);
}
@@ -110,7 +156,7 @@ fn parse_mcp_tool_preserves_output_schema_without_inferred_type() {
assert_eq!(
parse_mcp_tool(&tool).expect("parse MCP tool"),
ToolDefinition {
name: "with_enum_output".to_string(),
name: ToolName::plain("with_enum_output"),
description: "Has enum output schema".to_string(),
input_schema: JsonSchema::object(
BTreeMap::new(),
@@ -120,7 +166,11 @@ fn parse_mcp_tool_preserves_output_schema_without_inferred_type() {
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({
"enum": ["ok", "error"]
}))),
defer_loading: false,
loading: ToolLoadingPolicy::Eager,
execution: ToolExecution::Mcp,
presentation: None,
search: None,
supports_parallel_tool_calls: false,
}
);
}

View File

@@ -1,8 +1,6 @@
use crate::JsonSchema;
use crate::ToolDefinition;
use crate::ToolName;
use crate::parse_dynamic_tool;
use crate::parse_mcp_tool;
use crate::dynamic_tool_to_tool_definition;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use serde::Deserialize;
use serde::Serialize;
@@ -68,43 +66,51 @@ pub enum ResponsesApiNamespaceTool {
pub fn dynamic_tool_to_responses_api_tool(
tool: &DynamicToolSpec,
) -> Result<ResponsesApiTool, serde_json::Error> {
Ok(tool_definition_to_responses_api_tool(parse_dynamic_tool(
tool,
)?))
}
pub fn mcp_tool_to_responses_api_tool(
tool_name: &ToolName,
tool: &rmcp::model::Tool,
) -> Result<ResponsesApiTool, serde_json::Error> {
Ok(tool_definition_to_responses_api_tool(
parse_mcp_tool(tool)?.renamed(tool_name.name.clone()),
&dynamic_tool_to_tool_definition(tool)?,
))
}
pub fn mcp_tool_to_deferred_responses_api_tool(
tool_name: &ToolName,
tool: &rmcp::model::Tool,
) -> Result<ResponsesApiTool, serde_json::Error> {
Ok(tool_definition_to_responses_api_tool(
parse_mcp_tool(tool)?
.renamed(tool_name.name.clone())
.into_deferred(),
))
}
pub fn tool_definition_to_responses_api_tool(tool_definition: ToolDefinition) -> ResponsesApiTool {
/// Converts the leaf function shape of a canonical tool definition.
///
/// If the tool is namespaced, callers are still responsible for wrapping the
/// returned function in a Responses API namespace tool.
pub fn tool_definition_to_responses_api_tool(tool_definition: &ToolDefinition) -> ResponsesApiTool {
ResponsesApiTool {
name: tool_definition.name,
description: tool_definition.description,
name: tool_definition.name.name.clone(),
description: tool_definition.description.clone(),
strict: false,
defer_loading: tool_definition.defer_loading.then_some(true),
parameters: tool_definition.input_schema,
output_schema: tool_definition.output_schema,
defer_loading: tool_definition.defer_loading().then_some(true),
parameters: tool_definition.input_schema.clone(),
output_schema: tool_definition.output_schema.clone(),
}
}
pub fn tool_definition_to_tool_search_output_tool(
tool_definition: &ToolDefinition,
) -> ToolSearchOutputTool {
let function_tool = tool_definition_to_responses_api_tool(tool_definition);
let Some(namespace) = tool_definition.name.namespace.as_ref() else {
return ToolSearchOutputTool::Function(function_tool);
};
let description = tool_definition
.presentation
.as_ref()
.and_then(|presentation| presentation.namespace_description.as_deref())
.map(str::trim)
.filter(|description| !description.is_empty())
.map(str::to_string)
.unwrap_or_else(|| default_namespace_description(namespace));
ToolSearchOutputTool::Namespace(ResponsesApiNamespace {
name: namespace.clone(),
description,
tools: vec![ResponsesApiNamespaceTool::Function(function_tool)],
})
}
#[cfg(test)]
#[path = "responses_api_tests.rs"]
mod tests;

View File

@@ -3,11 +3,14 @@ use super::ResponsesApiNamespaceTool;
use super::ResponsesApiTool;
use super::ToolSearchOutputTool;
use super::dynamic_tool_to_responses_api_tool;
use super::mcp_tool_to_deferred_responses_api_tool;
use super::tool_definition_to_responses_api_tool;
use super::tool_definition_to_tool_search_output_tool;
use crate::JsonSchema;
use crate::ToolDefinition;
use crate::ToolExecution;
use crate::ToolLoadingPolicy;
use crate::ToolName;
use crate::ToolPresentation;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use pretty_assertions::assert_eq;
use serde_json::json;
@@ -16,8 +19,8 @@ use std::collections::BTreeMap;
#[test]
fn tool_definition_to_responses_api_tool_omits_false_defer_loading() {
assert_eq!(
tool_definition_to_responses_api_tool(ToolDefinition {
name: "lookup_order".to_string(),
tool_definition_to_responses_api_tool(&ToolDefinition {
name: ToolName::plain("lookup_order"),
description: "Look up an order".to_string(),
input_schema: JsonSchema::object(
BTreeMap::from([(
@@ -28,7 +31,11 @@ fn tool_definition_to_responses_api_tool_omits_false_defer_loading() {
Some(false.into())
),
output_schema: Some(json!({"type": "object"})),
defer_loading: false,
loading: ToolLoadingPolicy::Eager,
execution: ToolExecution::Dynamic,
presentation: None,
search: None,
supports_parallel_tool_calls: false,
}),
ResponsesApiTool {
name: "lookup_order".to_string(),
@@ -84,51 +91,6 @@ fn dynamic_tool_to_responses_api_tool_preserves_defer_loading() {
);
}
#[test]
fn mcp_tool_to_deferred_responses_api_tool_sets_defer_loading() {
let tool = rmcp::model::Tool {
name: "lookup_order".to_string().into(),
title: None,
description: Some("Look up an order".to_string().into()),
input_schema: std::sync::Arc::new(rmcp::model::object(json!({
"type": "object",
"properties": {
"order_id": {"type": "string"}
},
"required": ["order_id"],
"additionalProperties": false,
}))),
output_schema: None,
annotations: None,
execution: None,
icons: None,
meta: None,
};
assert_eq!(
mcp_tool_to_deferred_responses_api_tool(
&ToolName::namespaced("mcp__codex_apps__", "lookup_order"),
&tool,
)
.expect("convert deferred tool"),
ResponsesApiTool {
name: "lookup_order".to_string(),
description: "Look up an order".to_string(),
strict: false,
defer_loading: Some(true),
parameters: JsonSchema::object(
BTreeMap::from([(
"order_id".to_string(),
JsonSchema::string(/*description*/ None),
)]),
Some(vec!["order_id".to_string()]),
Some(false.into())
),
output_schema: None,
}
);
}
#[test]
fn tool_search_output_namespace_serializes_with_deferred_child_tools() {
let namespace = ToolSearchOutputTool::Namespace(ResponsesApiNamespace {
@@ -172,3 +134,43 @@ fn tool_search_output_namespace_serializes_with_deferred_child_tools() {
})
);
}
#[test]
fn tool_definition_to_tool_search_output_tool_wraps_namespaced_tools() {
assert_eq!(
tool_definition_to_tool_search_output_tool(&ToolDefinition {
name: ToolName::namespaced("mcp__calendar__", "create_event"),
description: "Create a calendar event.".to_string(),
input_schema: JsonSchema::object(
Default::default(),
/*required*/ None,
/*additional_properties*/ None,
),
output_schema: None,
loading: ToolLoadingPolicy::Deferred,
execution: ToolExecution::Mcp,
presentation: Some(ToolPresentation {
namespace_display_name: None,
namespace_description: Some("Calendar tools.".to_string()),
}),
search: None,
supports_parallel_tool_calls: false,
}),
ToolSearchOutputTool::Namespace(ResponsesApiNamespace {
name: "mcp__calendar__".to_string(),
description: "Calendar tools.".to_string(),
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
name: "create_event".to_string(),
description: "Create a calendar event.".to_string(),
strict: false,
defer_loading: Some(true),
parameters: JsonSchema::object(
Default::default(),
/*required*/ None,
/*additional_properties*/ None,
),
output_schema: None,
})],
})
);
}

View File

@@ -1,28 +1,76 @@
use crate::JsonSchema;
use crate::ToolName;
use serde_json::Value as JsonValue;
/// Tool metadata and schemas that downstream crates can adapt into higher-level
/// tool specs.
#[derive(Debug, PartialEq)]
/// Canonical metadata for JSON-schema function tools.
///
/// This intentionally models function-like tools only. If freeform tools need
/// the same registry/search/code-mode lifecycle later, this can grow a
/// function-vs-freeform input enum without changing the conversion boundary.
#[derive(Debug, Clone, PartialEq)]
pub struct ToolDefinition {
pub name: String,
pub name: ToolName,
pub description: String,
pub input_schema: JsonSchema,
pub output_schema: Option<JsonValue>,
pub defer_loading: bool,
pub loading: ToolLoadingPolicy,
pub execution: ToolExecution,
pub presentation: Option<ToolPresentation>,
pub search: Option<ToolSearchMetadata>,
pub supports_parallel_tool_calls: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolLoadingPolicy {
Eager,
Deferred,
}
impl ToolLoadingPolicy {
pub fn is_deferred(self) -> bool {
matches!(self, Self::Deferred)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolExecution {
/// Tool execution handled by an in-process Codex handler.
Builtin,
/// Tool registered dynamically by the caller for the current thread.
Dynamic,
/// Tool routed through the MCP connection manager.
Mcp,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolPresentation {
pub namespace_display_name: Option<String>,
pub namespace_description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolSearchMetadata {
pub source_name: String,
pub source_description: Option<String>,
pub extra_terms: Vec<String>,
pub limit_bucket: Option<String>,
}
impl ToolDefinition {
pub fn renamed(mut self, name: String) -> Self {
self.name = name;
pub fn renamed(mut self, name: impl Into<ToolName>) -> Self {
self.name = name.into();
self
}
pub fn into_deferred(mut self) -> Self {
self.output_schema = None;
self.defer_loading = true;
self.loading = ToolLoadingPolicy::Deferred;
self
}
pub fn defer_loading(&self) -> bool {
self.loading.is_deferred()
}
}
#[cfg(test)]

View File

@@ -1,11 +1,14 @@
use super::ToolDefinition;
use crate::JsonSchema;
use crate::ToolExecution;
use crate::ToolLoadingPolicy;
use crate::ToolName;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
fn tool_definition() -> ToolDefinition {
ToolDefinition {
name: "lookup_order".to_string(),
name: ToolName::plain("lookup_order"),
description: "Look up an order".to_string(),
input_schema: JsonSchema::object(
BTreeMap::new(),
@@ -15,16 +18,20 @@ fn tool_definition() -> ToolDefinition {
output_schema: Some(serde_json::json!({
"type": "object",
})),
defer_loading: false,
loading: ToolLoadingPolicy::Eager,
execution: ToolExecution::Dynamic,
presentation: None,
search: None,
supports_parallel_tool_calls: false,
}
}
#[test]
fn renamed_overrides_name_only() {
assert_eq!(
tool_definition().renamed("mcp__orders__lookup_order".to_string()),
tool_definition().renamed(ToolName::namespaced("mcp__orders__", "lookup_order")),
ToolDefinition {
name: "mcp__orders__lookup_order".to_string(),
name: ToolName::namespaced("mcp__orders__", "lookup_order"),
..tool_definition()
}
);
@@ -36,7 +43,7 @@ fn into_deferred_drops_output_schema_and_sets_defer_loading() {
tool_definition().into_deferred(),
ToolDefinition {
output_schema: None,
defer_loading: true,
loading: ToolLoadingPolicy::Deferred,
..tool_definition()
}
);

View File

@@ -1,12 +1,6 @@
use crate::JsonSchema;
use crate::ResponsesApiNamespace;
use crate::ResponsesApiNamespaceTool;
use crate::ResponsesApiTool;
use crate::ToolName;
use crate::ToolSearchOutputTool;
use crate::ToolSpec;
use crate::default_namespace_description;
use crate::mcp_tool_to_deferred_responses_api_tool;
use codex_app_server_protocol::AppInfo;
use serde::Deserialize;
use serde::Serialize;
@@ -23,23 +17,6 @@ pub struct ToolSearchSourceInfo {
pub description: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ToolSearchSource<'a> {
pub server_name: &'a str,
pub connector_name: Option<&'a str>,
pub connector_description: Option<&'a str>,
}
#[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,
pub connector_name: Option<&'a str>,
pub connector_description: Option<&'a str>,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DiscoverableToolType {
@@ -203,74 +180,6 @@ pub fn create_tool_search_tool(
}
}
pub fn tool_search_result_source_to_output_tool(
source: ToolSearchResultSource<'_>,
) -> Result<ToolSearchOutputTool, serde_json::Error> {
Ok(ToolSearchOutputTool::Namespace(ResponsesApiNamespace {
name: source.tool_namespace.to_string(),
description: tool_search_result_source_namespace_description(source),
tools: vec![tool_search_result_source_to_namespace_tool(source)?],
}))
}
fn tool_search_result_source_namespace_description(source: ToolSearchResultSource<'_>) -> String {
source
.connector_description
.map(str::trim)
.filter(|description| !description.is_empty())
.map(str::to_string)
.or_else(|| {
source
.connector_name
.map(str::trim)
.filter(|connector_name| !connector_name.is_empty())
.map(|connector_name| format!("Tools for working with {connector_name}."))
})
.unwrap_or_else(|| default_namespace_description(source.tool_namespace))
}
fn tool_search_result_source_to_namespace_tool(
source: ToolSearchResultSource<'_>,
) -> Result<ResponsesApiNamespaceTool, serde_json::Error> {
let tool_name = ToolName::namespaced(source.tool_namespace, source.tool_name);
mcp_tool_to_deferred_responses_api_tool(&tool_name, source.tool)
.map(ResponsesApiNamespaceTool::Function)
}
pub fn collect_tool_search_source_infos<'a>(
searchable_tools: impl IntoIterator<Item = ToolSearchSource<'a>>,
) -> Vec<ToolSearchSourceInfo> {
searchable_tools
.into_iter()
.filter_map(|tool| {
if let Some(name) = tool
.connector_name
.map(str::trim)
.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()
}
pub fn create_tool_suggest_tool(discoverable_tools: &[ToolSuggestEntry]) -> ToolSpec {
let discoverable_tool_ids = discoverable_tools
.iter()

View File

@@ -10,14 +10,12 @@ use crate::TOOL_SUGGEST_TOOL_NAME;
use crate::ToolHandlerKind;
use crate::ToolRegistryPlan;
use crate::ToolRegistryPlanParams;
use crate::ToolSearchSource;
use crate::ToolSearchSourceInfo;
use crate::ToolSpec;
use crate::ToolsConfig;
use crate::ViewImageToolOptions;
use crate::WebSearchToolOptions;
use crate::collect_code_mode_exec_prompt_tool_definitions;
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;
@@ -58,9 +56,9 @@ use crate::create_web_search_tool;
use crate::create_write_stdin_tool;
use crate::default_namespace_description;
use crate::dynamic_tool_to_responses_api_tool;
use crate::mcp_tool_to_responses_api_tool;
use crate::request_permissions_tool_description;
use crate::request_user_input_tool_description;
use crate::tool_definition_to_responses_api_tool;
use crate::tool_registry_plan_types::agent_type_description;
use codex_protocol::openai_models::ApplyPatchToolType;
use codex_protocol::openai_models::ConfigShellToolType;
@@ -261,16 +259,28 @@ pub fn build_tool_registry_plan(
if config.search_tool
&& (params.deferred_mcp_tools.is_some() || !deferred_dynamic_tools.is_empty())
{
let mut search_source_infos = params
let mut search_source_infos: Vec<ToolSearchSourceInfo> = params
.deferred_mcp_tools
.map(|deferred_mcp_tools| {
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,
}
}))
deferred_mcp_tools
.iter()
.filter_map(|tool| {
let search = tool.search.as_ref()?;
let name = search.source_name.trim();
if name.is_empty() {
return None;
}
Some(ToolSearchSourceInfo {
name: name.to_string(),
description: search
.source_description
.as_deref()
.map(str::trim)
.filter(|description| !description.is_empty())
.map(str::to_string),
})
})
.collect()
})
.unwrap_or_default();
@@ -493,7 +503,7 @@ pub fn build_tool_registry_plan(
}
if let Some(mcp_tools) = params.mcp_tools {
let mut entries = mcp_tools.to_vec();
let mut entries = mcp_tools.iter().collect::<Vec<_>>();
entries.sort_by_key(|tool| tool.name.display());
let mut namespace_entries = BTreeMap::new();
@@ -511,34 +521,20 @@ pub fn build_tool_registry_plan(
for (namespace, mut entries) in namespace_entries {
entries.sort_by_key(|tool| tool.name.name.clone());
let tool_namespace = params
.tool_namespaces
.and_then(|namespaces| namespaces.get(&namespace));
let description = tool_namespace
.and_then(|namespace| namespace.description.as_deref())
let description = entries
.iter()
.filter_map(|tool| tool.presentation.as_ref())
.filter_map(|presentation| presentation.namespace_description.as_deref())
.map(str::trim)
.filter(|description| !description.is_empty())
.find(|description| !description.is_empty())
.map(str::to_string)
.unwrap_or_else(|| {
let namespace_name = tool_namespace
.map(|namespace| namespace.name.as_str())
.unwrap_or(namespace.as_str());
default_namespace_description(namespace_name)
});
.unwrap_or_else(|| default_namespace_description(&namespace));
let mut tools = Vec::new();
for tool in entries {
match mcp_tool_to_responses_api_tool(&tool.name, tool.tool) {
Ok(converted_tool) => {
tools.push(ResponsesApiNamespaceTool::Function(converted_tool));
plan.register_handler(tool.name, ToolHandlerKind::Mcp);
}
Err(error) => {
let tool_name = &tool.name;
tracing::error!(
"Failed to convert `{tool_name}` MCP tool to OpenAI tool: {error:?}"
);
}
}
tools.push(ResponsesApiNamespaceTool::Function(
tool_definition_to_responses_api_tool(tool),
));
plan.register_handler(tool.name.clone(), ToolHandlerKind::Mcp);
}
if !tools.is_empty() {

View File

@@ -11,14 +11,15 @@ use crate::ResponsesApiNamespaceTool;
use crate::ResponsesApiTool;
use crate::ResponsesApiWebSearchFilters;
use crate::ResponsesApiWebSearchUserLocation;
use crate::ToolDefinition;
use crate::ToolHandlerSpec;
use crate::ToolName;
use crate::ToolNamespace;
use crate::ToolRegistryPlanDeferredTool;
use crate::ToolRegistryPlanMcpTool;
use crate::ToolSearchMetadata;
use crate::ToolsConfigParams;
use crate::WaitAgentTimeoutOptions;
use crate::mcp_call_tool_result_output_schema;
use crate::mcp_tool_to_tool_definition;
use codex_app_server_protocol::AppInfo;
use codex_features::Feature;
use codex_features::Features;
@@ -1935,10 +1936,10 @@ fn search_capable_model_info() -> ModelInfo {
}
}
fn build_specs<'a>(
fn build_specs(
config: &ToolsConfig,
mcp_tools: Option<HashMap<ToolName, rmcp::model::Tool>>,
deferred_mcp_tools: Option<Vec<ToolRegistryPlanDeferredTool<'a>>>,
deferred_mcp_tools: Option<Vec<ToolDefinition>>,
dynamic_tools: &[DynamicToolSpec],
) -> (Vec<ConfiguredToolSpec>, Vec<ToolHandlerSpec>) {
build_specs_with_discoverable_tools(
@@ -1950,10 +1951,10 @@ fn build_specs<'a>(
)
}
fn build_specs_with_discoverable_tools<'a>(
fn build_specs_with_discoverable_tools(
config: &ToolsConfig,
mcp_tools: Option<HashMap<ToolName, rmcp::model::Tool>>,
deferred_mcp_tools: Option<Vec<ToolRegistryPlanDeferredTool<'a>>>,
deferred_mcp_tools: Option<Vec<ToolDefinition>>,
discoverable_tools: Option<Vec<DiscoverableTool>>,
dynamic_tools: &[DynamicToolSpec],
) -> (Vec<ConfiguredToolSpec>, Vec<ToolHandlerSpec>) {
@@ -1967,10 +1968,10 @@ fn build_specs_with_discoverable_tools<'a>(
)
}
fn build_specs_with_optional_tool_namespaces<'a>(
fn build_specs_with_optional_tool_namespaces(
config: &ToolsConfig,
mcp_tools: Option<HashMap<ToolName, rmcp::model::Tool>>,
deferred_mcp_tools: Option<Vec<ToolRegistryPlanDeferredTool<'a>>>,
deferred_mcp_tools: Option<Vec<ToolDefinition>>,
tool_namespaces: Option<HashMap<String, ToolNamespace>>,
discoverable_tools: Option<Vec<DiscoverableTool>>,
dynamic_tools: &[DynamicToolSpec],
@@ -1978,9 +1979,8 @@ fn build_specs_with_optional_tool_namespaces<'a>(
let mcp_tool_inputs = mcp_tools.as_ref().map(|mcp_tools| {
mcp_tools
.iter()
.map(|(name, tool)| ToolRegistryPlanMcpTool {
name: name.clone(),
tool,
.map(|(name, tool)| {
mcp_tool_to_tool_definition(name, tool).expect("convert MCP test tool")
})
.collect::<Vec<_>>()
});
@@ -2104,19 +2104,26 @@ 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 {
name: ToolName::namespaced(tool_namespace, tool_name),
server_name,
connector_name,
connector_description,
}
fn deferred_mcp_tool(
tool_name: &str,
tool_namespace: &str,
server_name: &str,
connector_name: Option<&str>,
connector_description: Option<&str>,
) -> ToolDefinition {
let mut definition = mcp_tool_to_tool_definition(
&ToolName::namespaced(tool_namespace, tool_name),
&mcp_tool(tool_name, "", serde_json::json!({"type": "object"})),
)
.expect("convert deferred MCP test tool")
.into_deferred();
definition.search = Some(ToolSearchMetadata {
source_name: connector_name.unwrap_or(server_name).to_string(),
source_description: connector_description.map(str::to_string),
extra_terms: Vec::new(),
limit_bucket: Some(server_name.to_string()),
});
definition
}
fn assert_contains_tool_names(tools: &[ConfiguredToolSpec], expected_subset: &[&str]) {

View File

@@ -1,5 +1,6 @@
use crate::ConfiguredToolSpec;
use crate::DiscoverableTool;
use crate::ToolDefinition;
use crate::ToolName;
use crate::ToolSpec;
use crate::ToolsConfig;
@@ -57,8 +58,8 @@ 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 mcp_tools: Option<&'a [ToolDefinition]>,
pub deferred_mcp_tools: Option<&'a [ToolDefinition]>,
pub tool_namespaces: Option<&'a HashMap<String, ToolNamespace>>,
pub discoverable_tools: Option<&'a [DiscoverableTool]>,
pub dynamic_tools: &'a [DynamicToolSpec],
@@ -72,23 +73,6 @@ pub struct ToolNamespace {
pub description: Option<String>,
}
/// Direct MCP tool metadata needed to expose the Responses API namespace tool
/// while registering its runtime handler 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>,
}
impl ToolRegistryPlan {
pub(crate) fn new() -> Self {
Self {