register all mcp tools with namespace (#17404)

stacked on #17402.

MCP tools returned by `tool_search` (deferred tools) get registered in
our `ToolRegistry` with a different format than directly available
tools. this leads to two different ways of accessing MCP tools from our
tool catalog, only one of which works for each. fix this by registering
all MCP tools with the namespace format, since this info is already
available.

also, direct MCP tools are registered to responsesapi without a
namespace, while deferred MCP tools have a namespace. this means we can
receive MCP `FunctionCall`s in both formats from namespaces. fix this by
always registering MCP tools with namespace, regardless of deferral
status.

make code mode track `ToolName` provenance of tools so it can map the
literal JS function name string to the correct `ToolName` for
invocation, rather than supporting both in core.

this lets us unify to a single canonical `ToolName` representation for
each MCP tool and force everywhere to use that one, without supporting
fallbacks.
This commit is contained in:
sayan-oai
2026-04-15 21:02:59 +08:00
committed by GitHub
parent 9402347f34
commit 0df7e9a820
41 changed files with 1170 additions and 432 deletions
+87 -24
View File
@@ -1,7 +1,9 @@
use crate::FreeformTool;
use crate::FreeformToolFormat;
use crate::JsonSchema;
use crate::ResponsesApiNamespaceTool;
use crate::ResponsesApiTool;
use crate::ToolName;
use crate::ToolSpec;
use codex_code_mode::CodeModeToolKind;
use codex_code_mode::ToolDefinition as CodeModeToolDefinition;
@@ -9,22 +11,46 @@ use std::collections::BTreeMap;
/// Augment tool descriptions with code-mode-specific exec samples.
pub fn augment_tool_spec_for_code_mode(spec: ToolSpec) -> ToolSpec {
let Some(description) = code_mode_tool_definition_for_spec(&spec)
.map(codex_code_mode::augment_tool_definition)
.map(|definition| definition.description)
else {
return spec;
};
match spec {
ToolSpec::Function(mut tool) => {
let Some(description) =
augmented_description_for_spec(&ToolSpec::Function(tool.clone()))
else {
return ToolSpec::Function(tool);
};
tool.description = description;
ToolSpec::Function(tool)
}
ToolSpec::Freeform(mut tool) => {
let Some(description) =
augmented_description_for_spec(&ToolSpec::Freeform(tool.clone()))
else {
return ToolSpec::Freeform(tool);
};
tool.description = description;
ToolSpec::Freeform(tool)
}
ToolSpec::Namespace(mut namespace) => {
for tool in &mut namespace.tools {
match tool {
ResponsesApiNamespaceTool::Function(tool) => {
let tool_name =
ToolName::namespaced(namespace.name.clone(), tool.name.clone());
let definition = CodeModeToolDefinition {
name: tool_name.display(),
tool_name,
description: tool.description.clone(),
kind: CodeModeToolKind::Function,
input_schema: serde_json::to_value(&tool.parameters).ok(),
output_schema: tool.output_schema.clone(),
};
tool.description =
codex_code_mode::augment_tool_definition(definition).description;
}
}
}
ToolSpec::Namespace(namespace)
}
other => other,
}
}
@@ -42,7 +68,9 @@ pub fn collect_code_mode_tool_definitions<'a>(
) -> Vec<CodeModeToolDefinition> {
let mut tool_definitions = specs
.into_iter()
.filter_map(tool_spec_to_code_mode_tool_definition)
.flat_map(code_mode_tool_definitions_for_spec)
.filter(|definition| codex_code_mode::is_code_mode_nested_tool(&definition.name))
.map(codex_code_mode::augment_tool_definition)
.collect::<Vec<_>>();
tool_definitions.sort_by(|left, right| left.name.cmp(&right.name));
tool_definitions.dedup_by(|left, right| left.name == right.name);
@@ -54,7 +82,7 @@ pub fn collect_code_mode_exec_prompt_tool_definitions<'a>(
) -> Vec<CodeModeToolDefinition> {
let mut tool_definitions = specs
.into_iter()
.filter_map(code_mode_tool_definition_for_spec)
.flat_map(code_mode_tool_definitions_for_spec)
.filter(|definition| codex_code_mode::is_code_mode_nested_tool(&definition.name))
.collect::<Vec<_>>();
tool_definitions.sort_by(|left, right| left.name.cmp(&right.name));
@@ -137,26 +165,61 @@ SOURCE: /[\s\S]+/
})
}
fn augmented_description_for_spec(spec: &ToolSpec) -> Option<String> {
code_mode_tool_definition_for_spec(spec)
.map(codex_code_mode::augment_tool_definition)
.map(|definition| definition.description)
}
fn code_mode_tool_definition_for_spec(spec: &ToolSpec) -> Option<CodeModeToolDefinition> {
code_mode_tool_definitions_for_spec(spec).into_iter().next()
}
fn code_mode_tool_definitions_for_spec(spec: &ToolSpec) -> Vec<CodeModeToolDefinition> {
match spec {
ToolSpec::Function(tool) => Some(CodeModeToolDefinition {
name: tool.name.clone(),
description: tool.description.clone(),
kind: CodeModeToolKind::Function,
input_schema: serde_json::to_value(&tool.parameters).ok(),
output_schema: tool.output_schema.clone(),
}),
ToolSpec::Freeform(tool) => Some(CodeModeToolDefinition {
name: tool.name.clone(),
description: tool.description.clone(),
kind: CodeModeToolKind::Freeform,
input_schema: None,
output_schema: None,
}),
ToolSpec::Function(tool) => {
let name = tool.name.clone();
vec![CodeModeToolDefinition {
tool_name: ToolName::plain(name.clone()),
name,
description: tool.description.clone(),
kind: CodeModeToolKind::Function,
input_schema: serde_json::to_value(&tool.parameters).ok(),
output_schema: tool.output_schema.clone(),
}]
}
ToolSpec::Freeform(tool) => {
let name = tool.name.clone();
vec![CodeModeToolDefinition {
tool_name: ToolName::plain(name.clone()),
name,
description: tool.description.clone(),
kind: CodeModeToolKind::Freeform,
input_schema: None,
output_schema: None,
}]
}
ToolSpec::Namespace(namespace) => namespace
.tools
.iter()
.map(|tool| match tool {
ResponsesApiNamespaceTool::Function(tool) => {
let tool_name = ToolName::namespaced(namespace.name.clone(), tool.name.clone());
CodeModeToolDefinition {
name: tool_name.display(),
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(),
ToolSpec::LocalShell {}
| ToolSpec::ImageGeneration { .. }
| ToolSpec::ToolSearch { .. }
| ToolSpec::WebSearch { .. } => None,
| ToolSpec::WebSearch { .. } => Vec::new(),
}
}
+3
View File
@@ -7,6 +7,7 @@ use crate::FreeformTool;
use crate::FreeformToolFormat;
use crate::JsonSchema;
use crate::ResponsesApiTool;
use crate::ToolName;
use crate::ToolSpec;
use pretty_assertions::assert_eq;
use serde_json::json;
@@ -106,6 +107,7 @@ fn tool_spec_to_code_mode_tool_definition_returns_augmented_nested_tools() {
tool_spec_to_code_mode_tool_definition(&spec),
Some(codex_code_mode::ToolDefinition {
name: "apply_patch".to_string(),
tool_name: ToolName::plain("apply_patch"),
description: r#"Apply a patch
exec tool declaration:
@@ -184,6 +186,7 @@ fn create_wait_tool_matches_expected_spec() {
fn create_code_mode_tool_matches_expected_spec() {
let enabled_tools = vec![codex_code_mode::ToolDefinition {
name: "update_plan".to_string(),
tool_name: ToolName::plain("update_plan"),
description: "Update the plan".to_string(),
kind: codex_code_mode::CodeModeToolKind::Function,
input_schema: None,
+2 -2
View File
@@ -18,7 +18,6 @@ mod responses_api;
mod tool_config;
mod tool_definition;
mod tool_discovery;
mod tool_name;
mod tool_registry_plan;
mod tool_registry_plan_types;
mod tool_spec;
@@ -50,6 +49,7 @@ pub use code_mode::collect_code_mode_tool_definitions;
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::parse_dynamic_tool;
pub use image_detail::can_request_original_image_detail;
pub use image_detail::normalize_output_image_detail;
@@ -113,13 +113,13 @@ 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_name::ToolName;
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;
+7 -4
View File
@@ -1,5 +1,6 @@
use crate::JsonSchema;
use crate::ToolDefinition;
use crate::ToolName;
use crate::parse_dynamic_tool;
use crate::parse_mcp_tool;
use codex_protocol::dynamic_tools::DynamicToolSpec;
@@ -70,20 +71,22 @@ pub fn dynamic_tool_to_responses_api_tool(
}
pub fn mcp_tool_to_responses_api_tool(
name: String,
tool_name: &ToolName,
tool: &rmcp::model::Tool,
) -> Result<ResponsesApiTool, serde_json::Error> {
Ok(tool_definition_to_responses_api_tool(
parse_mcp_tool(tool)?.renamed(name),
parse_mcp_tool(tool)?.renamed(tool_name.name.clone()),
))
}
pub fn mcp_tool_to_deferred_responses_api_tool(
name: String,
tool_name: &ToolName,
tool: &rmcp::model::Tool,
) -> Result<ResponsesApiTool, serde_json::Error> {
Ok(tool_definition_to_responses_api_tool(
parse_mcp_tool(tool)?.renamed(name).into_deferred(),
parse_mcp_tool(tool)?
.renamed(tool_name.name.clone())
.into_deferred(),
))
}
+9 -4
View File
@@ -7,6 +7,7 @@ use super::mcp_tool_to_deferred_responses_api_tool;
use super::tool_definition_to_responses_api_tool;
use crate::JsonSchema;
use crate::ToolDefinition;
use crate::ToolName;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use pretty_assertions::assert_eq;
use serde_json::json;
@@ -106,19 +107,23 @@ fn mcp_tool_to_deferred_responses_api_tool_sets_defer_loading() {
assert_eq!(
mcp_tool_to_deferred_responses_api_tool(
"mcp__codex_apps__lookup_order".to_string(),
&ToolName::namespaced("mcp__codex_apps__", "lookup_order"),
&tool,
)
.expect("convert deferred tool"),
ResponsesApiTool {
name: "mcp__codex_apps__lookup_order".to_string(),
name: "lookup_order".to_string(),
description: "Look up an order".to_string(),
strict: false,
defer_loading: Some(true),
parameters: JsonSchema::object(BTreeMap::from([(
parameters: JsonSchema::object(
BTreeMap::from([(
"order_id".to_string(),
JsonSchema::string(/*description*/ None),
)]), Some(vec!["order_id".to_string()]), Some(false.into())),
)]),
Some(vec!["order_id".to_string()]),
Some(false.into())
),
output_schema: None,
}
);
+3 -1
View File
@@ -2,6 +2,7 @@ use crate::JsonSchema;
use crate::ResponsesApiNamespace;
use crate::ResponsesApiNamespaceTool;
use crate::ResponsesApiTool;
use crate::ToolName;
use crate::ToolSearchOutputTool;
use crate::ToolSpec;
use crate::mcp_tool_to_deferred_responses_api_tool;
@@ -242,7 +243,8 @@ pub fn collect_tool_search_output_tools<'a>(
let tools = tools
.iter()
.map(|tool| {
mcp_tool_to_deferred_responses_api_tool(tool.tool_name.to_string(), tool.tool)
let tool_name = ToolName::namespaced(tool.tool_namespace, tool.tool_name);
mcp_tool_to_deferred_responses_api_tool(&tool_name, tool.tool)
.map(ResponsesApiNamespaceTool::Function)
})
.collect::<Result<Vec<_>, _>>()?;
-42
View File
@@ -1,42 +0,0 @@
/// Identifies a callable tool, preserving the namespace split when the model
/// provides one.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ToolName {
pub name: String,
pub namespace: Option<String>,
}
impl ToolName {
pub fn plain(name: impl Into<String>) -> Self {
Self {
name: name.into(),
namespace: None,
}
}
pub fn namespaced(namespace: impl Into<String>, name: impl Into<String>) -> Self {
Self {
name: name.into(),
namespace: Some(namespace.into()),
}
}
pub fn display(&self) -> String {
match &self.namespace {
Some(namespace) => format!("{namespace}{}", self.name),
None => self.name.clone(),
}
}
}
impl From<String> for ToolName {
fn from(name: String) -> Self {
Self::plain(name)
}
}
impl From<&str> for ToolName {
fn from(name: &str) -> Self {
Self::plain(name)
}
}
+66 -52
View File
@@ -1,12 +1,13 @@
use crate::CommandToolOptions;
use crate::REQUEST_USER_INPUT_TOOL_NAME;
use crate::ResponsesApiNamespace;
use crate::ResponsesApiNamespaceTool;
use crate::ShellToolOptions;
use crate::SpawnAgentToolOptions;
use crate::TOOL_SEARCH_DEFAULT_LIMIT;
use crate::TOOL_SEARCH_TOOL_NAME;
use crate::TOOL_SUGGEST_TOOL_NAME;
use crate::ToolHandlerKind;
use crate::ToolName;
use crate::ToolRegistryPlan;
use crate::ToolRegistryPlanParams;
use crate::ToolSearchSource;
@@ -61,7 +62,6 @@ use crate::request_user_input_tool_description;
use crate::tool_registry_plan_types::agent_type_description;
use codex_protocol::openai_models::ApplyPatchToolType;
use codex_protocol::openai_models::ConfigShellToolType;
use rmcp::model::Tool as McpTool;
use std::collections::BTreeMap;
pub fn build_tool_registry_plan(
@@ -76,9 +76,9 @@ pub fn build_tool_registry_plan(
.tool_namespaces
.into_iter()
.flatten()
.map(|(name, detail)| {
.map(|(namespace, detail)| {
(
name.clone(),
namespace.clone(),
codex_code_mode::ToolNamespaceDescription {
name: detail.name.clone(),
description: detail.description.clone().unwrap_or_default(),
@@ -100,9 +100,8 @@ pub fn build_tool_registry_plan(
.iter()
.map(|configured_tool| &configured_tool.spec),
);
enabled_tools.sort_by(|left, right| {
compare_code_mode_tool_names(&left.name, &right.name, &namespace_descriptions)
});
enabled_tools
.sort_by(|left, right| compare_code_mode_tools(left, right, &namespace_descriptions));
plan.push_spec(
create_code_mode_tool(
&enabled_tools,
@@ -266,10 +265,7 @@ pub fn build_tool_registry_plan(
plan.register_handler(TOOL_SEARCH_TOOL_NAME, ToolHandlerKind::ToolSearch);
for tool in deferred_mcp_tools {
plan.register_handler(
ToolName::namespaced(tool.tool_namespace, tool.tool_name),
ToolHandlerKind::Mcp,
);
plan.register_handler(tool.name.clone(), ToolHandlerKind::Mcp);
}
}
@@ -471,28 +467,56 @@ pub fn build_tool_registry_plan(
}
if let Some(mcp_tools) = params.mcp_tools {
let mut entries: Vec<(String, &McpTool)> = mcp_tools
.iter()
.map(|(name, tool)| (name.clone(), tool))
.collect();
entries.sort_by(|left, right| left.0.cmp(&right.0));
let mut entries = mcp_tools.to_vec();
entries.sort_by_key(|tool| tool.name.display());
let mut namespace_entries = BTreeMap::new();
for (name, tool) in entries {
match mcp_tool_to_responses_api_tool(name.clone(), tool) {
Ok(converted_tool) => {
plan.push_spec(
ToolSpec::Function(converted_tool),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.register_handler(name, ToolHandlerKind::Mcp);
}
Err(error) => {
tracing::error!(
"Failed to convert {name:?} MCP tool to OpenAI tool: {error:?}"
);
for tool in entries {
let Some(namespace) = tool.name.namespace.as_ref() else {
let tool_name = &tool.name;
tracing::error!("Skipping MCP tool `{tool_name}`: MCP tools must be namespaced");
continue;
};
namespace_entries
.entry(namespace.clone())
.or_insert_with(Vec::new)
.push(tool);
}
for (namespace, mut entries) in namespace_entries {
entries.sort_by_key(|tool| tool.name.name.clone());
let description = params
.tool_namespaces
.and_then(|namespaces| namespaces.get(&namespace))
.and_then(|namespace| namespace.description.clone())
.unwrap_or_default();
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:?}"
);
}
}
}
if !tools.is_empty() {
plan.push_spec(
ToolSpec::Namespace(ResponsesApiNamespace {
name: namespace,
description,
tools,
}),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
}
}
}
@@ -518,41 +542,31 @@ pub fn build_tool_registry_plan(
plan
}
fn compare_code_mode_tool_names(
left_name: &str,
right_name: &str,
fn compare_code_mode_tools(
left: &codex_code_mode::ToolDefinition,
right: &codex_code_mode::ToolDefinition,
namespace_descriptions: &BTreeMap<String, codex_code_mode::ToolNamespaceDescription>,
) -> std::cmp::Ordering {
let left_namespace = code_mode_namespace_name(left_name, namespace_descriptions);
let right_namespace = code_mode_namespace_name(right_name, namespace_descriptions);
let left_namespace = code_mode_namespace_name(left, namespace_descriptions);
let right_namespace = code_mode_namespace_name(right, namespace_descriptions);
left_namespace
.cmp(&right_namespace)
.then_with(|| {
code_mode_function_name(left_name, left_namespace)
.cmp(code_mode_function_name(right_name, right_namespace))
})
.then_with(|| left_name.cmp(right_name))
.then_with(|| left.tool_name.name.cmp(&right.tool_name.name))
.then_with(|| left.name.cmp(&right.name))
}
fn code_mode_namespace_name<'a>(
name: &str,
tool: &codex_code_mode::ToolDefinition,
namespace_descriptions: &'a BTreeMap<String, codex_code_mode::ToolNamespaceDescription>,
) -> Option<&'a str> {
namespace_descriptions
.get(name)
tool.tool_name
.namespace
.as_ref()
.and_then(|namespace| namespace_descriptions.get(namespace))
.map(|namespace_description| namespace_description.name.as_str())
}
fn code_mode_function_name<'a>(name: &'a str, namespace: Option<&str>) -> &'a str {
namespace
.and_then(|namespace| {
name.strip_prefix(namespace)
.and_then(|suffix| suffix.strip_prefix("__"))
})
.unwrap_or(name)
}
#[cfg(test)]
#[path = "tool_registry_plan_tests.rs"]
mod tests;
+95 -49
View File
@@ -7,12 +7,15 @@ use crate::FreeformTool;
use crate::JsonSchema;
use crate::JsonSchemaPrimitiveType;
use crate::JsonSchemaType;
use crate::ResponsesApiNamespaceTool;
use crate::ResponsesApiTool;
use crate::ResponsesApiWebSearchFilters;
use crate::ResponsesApiWebSearchUserLocation;
use crate::ToolHandlerSpec;
use crate::ToolName;
use crate::ToolNamespace;
use crate::ToolRegistryPlanDeferredTool;
use crate::ToolRegistryPlanMcpTool;
use crate::ToolsConfigParams;
use crate::WaitAgentTimeoutOptions;
use crate::mcp_call_tool_result_output_schema;
@@ -1075,7 +1078,7 @@ fn test_build_specs_mcp_tools_converted() {
let (tools, _) = build_specs(
&tools_config,
Some(HashMap::from([(
"test_server/do_something_cool".to_string(),
ToolName::namespaced("test_server/", "do_something_cool"),
mcp_tool(
"do_something_cool",
"Do something cool",
@@ -1101,11 +1104,11 @@ fn test_build_specs_mcp_tools_converted() {
&[],
);
let tool = find_tool(&tools, "test_server/do_something_cool");
let tool = find_namespace_function_tool(&tools, "test_server/", "do_something_cool");
assert_eq!(
&tool.spec,
&ToolSpec::Function(ResponsesApiTool {
name: "test_server/do_something_cool".to_string(),
tool,
&ResponsesApiTool {
name: "do_something_cool".to_string(),
parameters: JsonSchema::object(
BTreeMap::from([
(
@@ -1144,7 +1147,7 @@ fn test_build_specs_mcp_tools_converted() {
strict: false,
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({}))),
defer_loading: None,
})
}
);
}
@@ -1167,16 +1170,16 @@ fn test_build_specs_mcp_tools_sorted_by_name() {
let tools_map = HashMap::from([
(
"test_server/do".to_string(),
mcp_tool("a", "a", serde_json::json!({"type": "object"})),
ToolName::namespaced("test_server/", "do"),
mcp_tool("do", "a", serde_json::json!({"type": "object"})),
),
(
"test_server/something".to_string(),
mcp_tool("b", "b", serde_json::json!({"type": "object"})),
ToolName::namespaced("test_server/", "something"),
mcp_tool("something", "b", serde_json::json!({"type": "object"})),
),
(
"test_server/cool".to_string(),
mcp_tool("c", "c", serde_json::json!({"type": "object"})),
ToolName::namespaced("test_server/", "cool"),
mcp_tool("cool", "c", serde_json::json!({"type": "object"})),
),
]);
@@ -1187,17 +1190,14 @@ fn test_build_specs_mcp_tools_sorted_by_name() {
&[],
);
let mcp_names: Vec<_> = tools
.iter()
.map(|tool| tool.name().to_string())
.filter(|name| name.starts_with("test_server/"))
.collect();
let expected = vec![
"test_server/cool".to_string(),
"test_server/do".to_string(),
"test_server/something".to_string(),
];
assert_eq!(mcp_names, expected);
assert_eq!(
namespace_function_names(&tools, "test_server/"),
vec![
"cool".to_string(),
"do".to_string(),
"something".to_string(),
]
);
}
#[test]
@@ -1222,7 +1222,7 @@ fn search_tool_description_lists_each_mcp_source_once() {
&tools_config,
Some(HashMap::from([
(
"mcp__codex_apps__calendar_create_event".to_string(),
ToolName::namespaced("mcp__codex_apps__calendar", "_create_event"),
mcp_tool(
"calendar_create_event",
"Create calendar event",
@@ -1230,7 +1230,7 @@ fn search_tool_description_lists_each_mcp_source_once() {
),
),
(
"mcp__rmcp__echo".to_string(),
ToolName::namespaced("mcp__rmcp__", "echo"),
mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})),
),
])),
@@ -1577,7 +1577,7 @@ fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() {
let (tools, _) = build_specs(
&tools_config,
Some(HashMap::from([(
"mcp__sample__echo".to_string(),
ToolName::namespaced("mcp__sample__", "echo"),
mcp_tool(
"echo",
"Echo text",
@@ -1595,11 +1595,8 @@ fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() {
&[],
);
let ToolSpec::Function(ResponsesApiTool { description, .. }) =
&find_tool(&tools, "mcp__sample__echo").spec
else {
panic!("expected function tool");
};
let ResponsesApiTool { description, .. } =
find_namespace_function_tool(&tools, "mcp__sample__", "echo");
assert_eq!(
description,
@@ -1633,7 +1630,7 @@ fn code_mode_preserves_nullable_and_literal_mcp_input_shapes() {
let (tools, _) = build_specs(
&tools_config,
Some(HashMap::from([(
"mcp__sample__fn".to_string(),
ToolName::namespaced("mcp__sample__", "fn"),
mcp_tool(
"fn",
"Sample fn",
@@ -1684,11 +1681,8 @@ fn code_mode_preserves_nullable_and_literal_mcp_input_shapes() {
&[],
);
let ToolSpec::Function(ResponsesApiTool { description, .. }) =
&find_tool(&tools, "mcp__sample__fn").spec
else {
panic!("expected function tool");
};
let ResponsesApiTool { description, .. } =
find_namespace_function_tool(&tools, "mcp__sample__", "fn");
assert!(description.contains(
r#"exec tool declaration:
@@ -1851,7 +1845,7 @@ fn search_capable_model_info() -> ModelInfo {
fn build_specs<'a>(
config: &ToolsConfig,
mcp_tools: Option<HashMap<String, rmcp::model::Tool>>,
mcp_tools: Option<HashMap<ToolName, rmcp::model::Tool>>,
deferred_mcp_tools: Option<Vec<ToolRegistryPlanDeferredTool<'a>>>,
dynamic_tools: &[DynamicToolSpec],
) -> (Vec<ConfiguredToolSpec>, Vec<ToolHandlerSpec>) {
@@ -1866,7 +1860,7 @@ fn build_specs<'a>(
fn build_specs_with_discoverable_tools<'a>(
config: &ToolsConfig,
mcp_tools: Option<HashMap<String, rmcp::model::Tool>>,
mcp_tools: Option<HashMap<ToolName, rmcp::model::Tool>>,
deferred_mcp_tools: Option<Vec<ToolRegistryPlanDeferredTool<'a>>>,
discoverable_tools: Option<Vec<DiscoverableTool>>,
dynamic_tools: &[DynamicToolSpec],
@@ -1883,16 +1877,25 @@ 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>>,
mcp_tools: Option<HashMap<ToolName, rmcp::model::Tool>>,
deferred_mcp_tools: Option<Vec<ToolRegistryPlanDeferredTool<'a>>>,
tool_namespaces: Option<HashMap<String, ToolNamespace>>,
discoverable_tools: Option<Vec<DiscoverableTool>>,
dynamic_tools: &[DynamicToolSpec],
) -> (Vec<ConfiguredToolSpec>, Vec<ToolHandlerSpec>) {
let mcp_tool_inputs = mcp_tools.as_ref().map(|mcp_tools| {
mcp_tools
.iter()
.map(|(name, tool)| ToolRegistryPlanMcpTool {
name: name.clone(),
tool,
})
.collect::<Vec<_>>()
});
let plan = build_tool_registry_plan(
config,
ToolRegistryPlanParams {
mcp_tools: mcp_tools.as_ref(),
mcp_tools: mcp_tool_inputs.as_deref(),
deferred_mcp_tools: deferred_mcp_tools.as_deref(),
tool_namespaces: tool_namespaces.as_ref(),
discoverable_tools: discoverable_tools.as_deref(),
@@ -1968,16 +1971,16 @@ fn code_mode_augments_mcp_tool_descriptions_with_structured_output_sample() {
let (tools, _) = build_specs(
&tools_config,
Some(HashMap::from([("mcp__sample__echo".to_string(), tool)])),
Some(HashMap::from([(
ToolName::namespaced("mcp__sample__", "echo"),
tool,
)])),
/*deferred_mcp_tools*/ None,
&[],
);
let ToolSpec::Function(ResponsesApiTool { description, .. }) =
&find_tool(&tools, "mcp__sample__echo").spec
else {
panic!("expected function tool");
};
let ResponsesApiTool { description, .. } =
find_namespace_function_tool(&tools, "mcp__sample__", "echo");
assert_eq!(
description,
@@ -2017,8 +2020,7 @@ fn deferred_mcp_tool<'a>(
connector_description: Option<&'a str>,
) -> ToolRegistryPlanDeferredTool<'a> {
ToolRegistryPlanDeferredTool {
tool_name,
tool_namespace,
name: ToolName::namespaced(tool_namespace, tool_name),
server_name,
connector_name,
connector_description,
@@ -2089,6 +2091,39 @@ fn find_tool<'a>(tools: &'a [ConfiguredToolSpec], expected_name: &str) -> &'a Co
.unwrap_or_else(|| panic!("expected tool {expected_name}"))
}
fn find_namespace_function_tool<'a>(
tools: &'a [ConfiguredToolSpec],
expected_namespace: &str,
expected_name: &str,
) -> &'a ResponsesApiTool {
let namespace_tool = find_tool(tools, expected_namespace);
let ToolSpec::Namespace(namespace) = &namespace_tool.spec else {
panic!("expected namespace tool {expected_namespace}");
};
namespace
.tools
.iter()
.find_map(|tool| match tool {
ResponsesApiNamespaceTool::Function(tool) if tool.name == expected_name => Some(tool),
_ => None,
})
.unwrap_or_else(|| panic!("expected tool {expected_namespace}{expected_name} in namespace"))
}
fn namespace_function_names(tools: &[ConfiguredToolSpec], expected_namespace: &str) -> Vec<String> {
let namespace_tool = find_tool(tools, expected_namespace);
let ToolSpec::Namespace(namespace) = &namespace_tool.spec else {
panic!("expected namespace tool {expected_namespace}");
};
namespace
.tools
.iter()
.map(|tool| match tool {
ResponsesApiNamespaceTool::Function(tool) => tool.name.clone(),
})
.collect()
}
fn expect_object_schema(
schema: &JsonSchema,
) -> (&BTreeMap<String, JsonSchema>, Option<&Vec<String>>) {
@@ -2137,6 +2172,17 @@ fn strip_descriptions_tool(spec: &mut ToolSpec) {
ToolSpec::Function(ResponsesApiTool { parameters, .. }) => {
strip_descriptions_schema(parameters);
}
ToolSpec::Namespace(namespace) => {
for tool in &mut namespace.tools {
match tool {
ResponsesApiNamespaceTool::Function(ResponsesApiTool {
parameters, ..
}) => {
strip_descriptions_schema(parameters);
}
}
}
}
ToolSpec::Freeform(FreeformTool { .. })
| ToolSpec::LocalShell {}
| ToolSpec::ImageGeneration { .. }
+12 -5
View File
@@ -6,7 +6,6 @@ use crate::ToolsConfig;
use crate::WaitAgentTimeoutOptions;
use crate::augment_tool_spec_for_code_mode;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use rmcp::model::Tool as McpTool;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -58,7 +57,7 @@ pub struct ToolRegistryPlan {
#[derive(Debug, Clone, Copy)]
pub struct ToolRegistryPlanParams<'a> {
pub mcp_tools: Option<&'a HashMap<String, McpTool>>,
pub mcp_tools: Option<&'a [ToolRegistryPlanMcpTool<'a>]>,
pub deferred_mcp_tools: Option<&'a [ToolRegistryPlanDeferredTool<'a>]>,
pub tool_namespaces: Option<&'a HashMap<String, ToolNamespace>>,
pub discoverable_tools: Option<&'a [DiscoverableTool]>,
@@ -73,10 +72,18 @@ pub struct ToolNamespace {
pub description: Option<String>,
}
#[derive(Debug, Clone, Copy)]
/// 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 tool_name: &'a str,
pub tool_namespace: &'a str,
pub name: ToolName,
pub server_name: &'a str,
pub connector_name: Option<&'a str>,
pub connector_description: Option<&'a str>,
+4
View File
@@ -1,5 +1,6 @@
use crate::FreeformTool;
use crate::JsonSchema;
use crate::ResponsesApiNamespace;
use crate::ResponsesApiTool;
use codex_protocol::config_types::WebSearchConfig;
use codex_protocol::config_types::WebSearchContextSize;
@@ -20,6 +21,8 @@ const WEB_SEARCH_TEXT_AND_IMAGE_CONTENT_TYPES: [&str; 2] = ["text", "image"];
pub enum ToolSpec {
#[serde(rename = "function")]
Function(ResponsesApiTool),
#[serde(rename = "namespace")]
Namespace(ResponsesApiNamespace),
#[serde(rename = "tool_search")]
ToolSearch {
execution: String,
@@ -57,6 +60,7 @@ impl ToolSpec {
pub fn name(&self) -> &str {
match self {
ToolSpec::Function(tool) => tool.name.as_str(),
ToolSpec::Namespace(namespace) => namespace.name.as_str(),
ToolSpec::ToolSearch { .. } => "tool_search",
ToolSpec::LocalShell {} => "local_shell",
ToolSpec::ImageGeneration { .. } => "image_generation",
+56
View File
@@ -1,4 +1,5 @@
use super::ConfiguredToolSpec;
use super::ResponsesApiNamespace;
use super::ResponsesApiWebSearchFilters;
use super::ResponsesApiWebSearchUserLocation;
use super::ToolSpec;
@@ -6,6 +7,7 @@ use crate::AdditionalProperties;
use crate::FreeformTool;
use crate::FreeformToolFormat;
use crate::JsonSchema;
use crate::ResponsesApiNamespaceTool;
use crate::ResponsesApiTool;
use crate::create_tools_json_for_responses_api;
use codex_protocol::config_types::WebSearchContextSize;
@@ -34,6 +36,15 @@ fn tool_spec_name_covers_all_variants() {
.name(),
"lookup_order"
);
assert_eq!(
ToolSpec::Namespace(ResponsesApiNamespace {
name: "mcp__demo__".to_string(),
description: "Demo tools".to_string(),
tools: Vec::new(),
})
.name(),
"mcp__demo__"
);
assert_eq!(
ToolSpec::ToolSearch {
execution: "sync".to_string(),
@@ -163,6 +174,51 @@ fn create_tools_json_for_responses_api_includes_top_level_name() {
);
}
#[test]
fn namespace_tool_spec_serializes_expected_wire_shape() {
assert_eq!(
serde_json::to_value(ToolSpec::Namespace(ResponsesApiNamespace {
name: "mcp__demo__".to_string(),
description: "Demo tools".to_string(),
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
name: "lookup_order".to_string(),
description: "Look up an order".to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::object(
BTreeMap::from([(
"order_id".to_string(),
JsonSchema::string(/*description*/ None),
)]),
/*required*/ None,
/*additional_properties*/ None,
),
output_schema: None,
})],
}))
.expect("serialize namespace tool"),
json!({
"type": "namespace",
"name": "mcp__demo__",
"description": "Demo tools",
"tools": [
{
"type": "function",
"name": "lookup_order",
"description": "Look up an order",
"strict": false,
"parameters": {
"type": "object",
"properties": {
"order_id": { "type": "string" },
},
},
},
],
})
);
}
#[test]
fn web_search_tool_spec_serializes_expected_wire_shape() {
assert_eq!(