mirror of
https://github.com/openai/codex.git
synced 2026-05-05 05:42:33 +03:00
[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:
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user