mirror of
https://github.com/openai/codex.git
synced 2026-04-20 04:34:47 +00:00
Compare commits
6 Commits
pr17694
...
dev/sayan/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0732c913fb | ||
|
|
933411dadf | ||
|
|
85729e250f | ||
|
|
64815da52c | ||
|
|
dd29229c01 | ||
|
|
b4e66fb8ec |
@@ -610,7 +610,7 @@ async fn collect_mcp_server_status_snapshot_from_manager(
|
||||
);
|
||||
|
||||
let mut tools_by_server = HashMap::<String, HashMap<String, Tool>>::new();
|
||||
for (_qualified_name, tool_info) in tools {
|
||||
for (_tool_name, tool_info) in tools {
|
||||
let raw_tool_name = tool_info.tool.name.to_string();
|
||||
let Some(tool) = protocol_tool_from_rmcp_tool(&raw_tool_name, &tool_info.tool) else {
|
||||
continue;
|
||||
@@ -668,7 +668,8 @@ pub async fn collect_mcp_snapshot_from_manager_with_detail(
|
||||
let tools = tools
|
||||
.into_iter()
|
||||
.filter_map(|(name, tool)| {
|
||||
protocol_tool_from_rmcp_tool(&name, &tool.tool).map(|tool| (name, tool))
|
||||
let display_name = name.display();
|
||||
protocol_tool_from_rmcp_tool(&display_name, &tool.tool).map(|tool| (display_name, tool))
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
//! The [`McpConnectionManager`] owns one [`codex_rmcp_client::RmcpClient`] per
|
||||
//! configured server (keyed by the *server name*). It offers convenience
|
||||
//! helpers to query the available tools across *all* servers and returns them
|
||||
//! in a single aggregated map using the model-visible fully-qualified tool name
|
||||
//! as the key.
|
||||
//! in a single aggregated map using the model-visible callable tool name as the
|
||||
//! key.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
@@ -36,6 +36,7 @@ use codex_async_utils::CancelErr;
|
||||
use codex_async_utils::OrCancelExt;
|
||||
use codex_config::Constrained;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_protocol::ToolName;
|
||||
use codex_protocol::approvals::ElicitationRequest;
|
||||
use codex_protocol::approvals::ElicitationRequestEvent;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
@@ -155,6 +156,12 @@ pub struct ToolInfo {
|
||||
pub connector_description: Option<String>,
|
||||
}
|
||||
|
||||
impl ToolInfo {
|
||||
pub fn callable_tool_name(&self) -> ToolName {
|
||||
ToolName::namespaced(self.callable_namespace.clone(), self.callable_name.clone())
|
||||
}
|
||||
}
|
||||
|
||||
const META_OPENAI_FILE_PARAMS: &str = "openai/fileParams";
|
||||
|
||||
pub fn declared_openai_file_input_param_names(
|
||||
@@ -903,10 +910,11 @@ impl McpConnectionManager {
|
||||
failures
|
||||
}
|
||||
|
||||
/// Returns a single map that contains all tools. Each key is the
|
||||
/// fully-qualified name for the tool.
|
||||
/// Returns a single map that contains all tools keyed by model-visible
|
||||
/// callable name. Each key's `display()` is unique and <= 64 bytes so it
|
||||
/// can be used at flat protocol/display boundaries.
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
pub async fn list_all_tools(&self) -> HashMap<String, ToolInfo> {
|
||||
pub async fn list_all_tools(&self) -> HashMap<ToolName, ToolInfo> {
|
||||
let mut tools = Vec::new();
|
||||
for managed_client in self.clients.values() {
|
||||
let Some(server_tools) = managed_client.listed_tools().await else {
|
||||
@@ -922,7 +930,7 @@ impl McpConnectionManager {
|
||||
/// On success, the refreshed tools replace the cache contents and the
|
||||
/// latest filtered tool map is returned directly to the caller. On
|
||||
/// failure, the existing cache remains unchanged.
|
||||
pub async fn hard_refresh_codex_apps_tools_cache(&self) -> Result<HashMap<String, ToolInfo>> {
|
||||
pub async fn hard_refresh_codex_apps_tools_cache(&self) -> Result<HashMap<ToolName, ToolInfo>> {
|
||||
let managed_client = self
|
||||
.clients
|
||||
.get(CODEX_APPS_MCP_SERVER_NAME)
|
||||
@@ -1191,14 +1199,15 @@ impl McpConnectionManager {
|
||||
.with_context(|| format!("resources/read failed for `{server}` ({uri})"))
|
||||
}
|
||||
|
||||
pub async fn resolve_tool_info(&self, name: &str, namespace: Option<&str>) -> Option<ToolInfo> {
|
||||
let qualified_name = match namespace {
|
||||
Some(namespace) if name.starts_with(namespace) => name.to_string(),
|
||||
Some(namespace) => format!("{namespace}{name}"),
|
||||
None => name.to_string(),
|
||||
};
|
||||
pub async fn resolve_tool_info(&self, tool_name: &ToolName) -> Option<ToolInfo> {
|
||||
let all_tools = self.list_all_tools().await;
|
||||
if let Some(tool) = all_tools.get(tool_name) {
|
||||
return Some(tool.clone());
|
||||
}
|
||||
|
||||
self.list_all_tools().await.get(&qualified_name).cloned()
|
||||
all_tools
|
||||
.into_iter()
|
||||
.find_map(|(name, tool)| (name.display() == tool_name.name).then_some(tool))
|
||||
}
|
||||
|
||||
pub async fn notify_sandbox_state_change(&self, sandbox_state: &SandboxState) -> Result<()> {
|
||||
@@ -1286,8 +1295,8 @@ fn filter_tools(tools: Vec<ToolInfo>, filter: &ToolFilter) -> Vec<ToolInfo> {
|
||||
}
|
||||
|
||||
pub fn filter_non_codex_apps_mcp_tools_only(
|
||||
mcp_tools: &HashMap<String, ToolInfo>,
|
||||
) -> HashMap<String, ToolInfo> {
|
||||
mcp_tools: &HashMap<ToolName, ToolInfo>,
|
||||
) -> HashMap<ToolName, ToolInfo> {
|
||||
mcp_tools
|
||||
.iter()
|
||||
.filter(|(_, tool)| tool.server_name != CODEX_APPS_MCP_SERVER_NAME)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use codex_protocol::ToolName;
|
||||
use codex_protocol::protocol::GranularApprovalConfig;
|
||||
use codex_protocol::protocol::McpAuthStatus;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -251,8 +252,8 @@ fn test_qualify_tools_short_non_duplicated_names() {
|
||||
let qualified_tools = qualify_tools(tools);
|
||||
|
||||
assert_eq!(qualified_tools.len(), 2);
|
||||
assert!(qualified_tools.contains_key("mcp__server1__tool1"));
|
||||
assert!(qualified_tools.contains_key("mcp__server1__tool2"));
|
||||
assert!(qualified_tools.contains_key(&ToolName::namespaced("mcp__server1__", "tool1")));
|
||||
assert!(qualified_tools.contains_key(&ToolName::namespaced("mcp__server1__", "tool2")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -266,7 +267,9 @@ fn test_qualify_tools_duplicated_names_skipped() {
|
||||
|
||||
// Only the first tool should remain, the second is skipped
|
||||
assert_eq!(qualified_tools.len(), 1);
|
||||
assert!(qualified_tools.contains_key("mcp__server1__duplicate_tool"));
|
||||
assert!(
|
||||
qualified_tools.contains_key(&ToolName::namespaced("mcp__server1__", "duplicate_tool"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -288,7 +291,7 @@ fn test_qualify_tools_long_names_same_server() {
|
||||
|
||||
assert_eq!(qualified_tools.len(), 2);
|
||||
|
||||
let mut keys: Vec<_> = qualified_tools.keys().cloned().collect();
|
||||
let mut keys: Vec<_> = qualified_tools.keys().map(ToolName::display).collect();
|
||||
keys.sort();
|
||||
|
||||
assert!(keys.iter().all(|key| key.len() == 64));
|
||||
@@ -308,10 +311,13 @@ fn test_qualify_tools_sanitizes_invalid_characters() {
|
||||
|
||||
assert_eq!(qualified_tools.len(), 1);
|
||||
let (qualified_name, tool) = qualified_tools.into_iter().next().expect("one tool");
|
||||
assert_eq!(qualified_name, "mcp__server_one__tool_two_three");
|
||||
assert_eq!(
|
||||
qualified_name,
|
||||
ToolName::namespaced("mcp__server_one__", "tool_two_three")
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}{}", tool.callable_namespace, tool.callable_name),
|
||||
qualified_name
|
||||
qualified_name.display()
|
||||
);
|
||||
|
||||
// The key and callable parts are sanitized for model-visible tool calls, but
|
||||
@@ -323,6 +329,7 @@ fn test_qualify_tools_sanitizes_invalid_characters() {
|
||||
|
||||
assert!(
|
||||
qualified_name
|
||||
.display()
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_'),
|
||||
"qualified name must be code-mode compatible: {qualified_name:?}"
|
||||
@@ -337,7 +344,10 @@ fn test_qualify_tools_keeps_hyphenated_mcp_tools_callable() {
|
||||
|
||||
assert_eq!(qualified_tools.len(), 1);
|
||||
let (qualified_name, tool) = qualified_tools.into_iter().next().expect("one tool");
|
||||
assert_eq!(qualified_name, "mcp__music_studio__get_strudel_guide");
|
||||
assert_eq!(
|
||||
qualified_name,
|
||||
ToolName::namespaced("mcp__music_studio__", "get_strudel_guide")
|
||||
);
|
||||
assert_eq!(tool.callable_namespace, "mcp__music_studio__");
|
||||
assert_eq!(tool.callable_name, "get_strudel_guide");
|
||||
assert_eq!(tool.tool.name, "get-strudel-guide");
|
||||
@@ -367,9 +377,10 @@ fn test_qualify_tools_disambiguates_sanitized_namespace_collisions() {
|
||||
.collect::<HashSet<_>>();
|
||||
assert_eq!(raw_servers, HashSet::from(["basic-server", "basic_server"]));
|
||||
assert!(
|
||||
qualified_tools
|
||||
.keys()
|
||||
.all(|key| key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')),
|
||||
qualified_tools.keys().all(|key| key
|
||||
.display()
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_')),
|
||||
"qualified names must be code-mode compatible: {qualified_tools:?}"
|
||||
);
|
||||
}
|
||||
@@ -640,12 +651,77 @@ async fn list_all_tools_uses_startup_snapshot_while_client_is_pending() {
|
||||
|
||||
let tools = manager.list_all_tools().await;
|
||||
let tool = tools
|
||||
.get("mcp__codex_apps__calendar_create_event")
|
||||
.get(&ToolName::namespaced(
|
||||
"mcp__codex_apps__",
|
||||
"calendar_create_event",
|
||||
))
|
||||
.expect("tool from startup cache");
|
||||
assert_eq!(tool.server_name, CODEX_APPS_MCP_SERVER_NAME);
|
||||
assert_eq!(tool.callable_name, "calendar_create_event");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_tool_info_accepts_plain_and_namespaced_tool_names() {
|
||||
let startup_tools = vec![create_test_tool("rmcp", "echo")];
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy);
|
||||
manager.clients.insert(
|
||||
"rmcp".to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
startup_snapshot: Some(startup_tools),
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
},
|
||||
);
|
||||
|
||||
let flat = manager
|
||||
.resolve_tool_info(&ToolName::plain("mcp__rmcp__echo"))
|
||||
.await
|
||||
.expect("flat qualified MCP tool name should resolve");
|
||||
let split = manager
|
||||
.resolve_tool_info(&ToolName::namespaced("mcp__rmcp__", "echo"))
|
||||
.await
|
||||
.expect("split MCP tool namespace and name should resolve");
|
||||
let split_with_flat_name = manager
|
||||
.resolve_tool_info(&ToolName::namespaced("mcp__rmcp__", "mcp__rmcp__echo"))
|
||||
.await
|
||||
.expect("split namespace with flat qualified MCP tool name should resolve");
|
||||
|
||||
let expected = ("rmcp", "mcp__rmcp__", "echo", "echo");
|
||||
assert_eq!(
|
||||
(
|
||||
flat.server_name.as_str(),
|
||||
flat.callable_namespace.as_str(),
|
||||
flat.callable_name.as_str(),
|
||||
flat.tool.name.as_ref(),
|
||||
),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
split.server_name.as_str(),
|
||||
split.callable_namespace.as_str(),
|
||||
split.callable_name.as_str(),
|
||||
split.tool.name.as_ref(),
|
||||
),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
split_with_flat_name.server_name.as_str(),
|
||||
split_with_flat_name.callable_namespace.as_str(),
|
||||
split_with_flat_name.callable_name.as_str(),
|
||||
split_with_flat_name.tool.name.as_ref(),
|
||||
),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_all_tools_blocks_while_client_is_pending_without_startup_snapshot() {
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
@@ -722,7 +798,10 @@ async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() {
|
||||
|
||||
let tools = manager.list_all_tools().await;
|
||||
let tool = tools
|
||||
.get("mcp__codex_apps__calendar_create_event")
|
||||
.get(&ToolName::namespaced(
|
||||
"mcp__codex_apps__",
|
||||
"calendar_create_event",
|
||||
))
|
||||
.expect("tool from startup cache");
|
||||
assert_eq!(tool.server_name, CODEX_APPS_MCP_SERVER_NAME);
|
||||
assert_eq!(tool.callable_name, "calendar_create_event");
|
||||
|
||||
@@ -9,6 +9,7 @@ use tracing::warn;
|
||||
|
||||
use crate::mcp::sanitize_responses_api_tool_name;
|
||||
use crate::mcp_connection_manager::ToolInfo;
|
||||
use codex_protocol::ToolName;
|
||||
|
||||
const MCP_TOOL_NAME_DELIMITER: &str = "__";
|
||||
const MAX_TOOL_NAME_LENGTH: usize = 64;
|
||||
@@ -71,10 +72,10 @@ fn unique_callable_parts(
|
||||
tool_name: &str,
|
||||
raw_identity: &str,
|
||||
used_names: &mut HashSet<String>,
|
||||
) -> (String, String, String) {
|
||||
let qualified_name = format!("{namespace}{tool_name}");
|
||||
if qualified_name.len() <= MAX_TOOL_NAME_LENGTH && used_names.insert(qualified_name.clone()) {
|
||||
return (namespace.to_string(), tool_name.to_string(), qualified_name);
|
||||
) -> (String, String) {
|
||||
let display_name = ToolName::namespaced(namespace, tool_name).display();
|
||||
if display_name.len() <= MAX_TOOL_NAME_LENGTH && used_names.insert(display_name) {
|
||||
return (namespace.to_string(), tool_name.to_string());
|
||||
}
|
||||
|
||||
let mut attempt = 0_u32;
|
||||
@@ -86,9 +87,9 @@ fn unique_callable_parts(
|
||||
};
|
||||
let (namespace, tool_name) =
|
||||
fit_callable_parts_with_hash(namespace, tool_name, &hash_input);
|
||||
let qualified_name = format!("{namespace}{tool_name}");
|
||||
if used_names.insert(qualified_name.clone()) {
|
||||
return (namespace, tool_name, qualified_name);
|
||||
let display_name = ToolName::namespaced(&namespace, &tool_name).display();
|
||||
if used_names.insert(display_name) {
|
||||
return (namespace, tool_name);
|
||||
}
|
||||
attempt = attempt.saturating_add(1);
|
||||
}
|
||||
@@ -103,12 +104,12 @@ struct CallableToolCandidate {
|
||||
callable_name: String,
|
||||
}
|
||||
|
||||
/// Returns a qualified-name lookup for MCP tools.
|
||||
/// Returns a callable-name lookup for MCP tools.
|
||||
///
|
||||
/// Raw MCP server/tool names are kept on each [`ToolInfo`] for protocol calls, while
|
||||
/// `callable_namespace` / `callable_name` are sanitized and, when necessary, hashed so
|
||||
/// every model-visible `mcp__namespace__tool` name is unique and <= 64 bytes.
|
||||
pub(crate) fn qualify_tools<I>(tools: I) -> HashMap<String, ToolInfo>
|
||||
pub(crate) fn qualify_tools<I>(tools: I) -> HashMap<ToolName, ToolInfo>
|
||||
where
|
||||
I: IntoIterator<Item = ToolInfo>,
|
||||
{
|
||||
@@ -188,7 +189,7 @@ where
|
||||
let mut used_names = HashSet::new();
|
||||
let mut qualified_tools = HashMap::new();
|
||||
for mut candidate in candidates {
|
||||
let (callable_namespace, callable_name, qualified_name) = unique_callable_parts(
|
||||
let (callable_namespace, callable_name) = unique_callable_parts(
|
||||
&candidate.callable_namespace,
|
||||
&candidate.callable_name,
|
||||
&candidate.raw_tool_identity,
|
||||
@@ -196,7 +197,7 @@ where
|
||||
);
|
||||
candidate.tool.callable_namespace = callable_namespace;
|
||||
candidate.tool.callable_name = callable_name;
|
||||
qualified_tools.insert(qualified_name, candidate.tool);
|
||||
qualified_tools.insert(candidate.tool.callable_tool_name(), candidate.tool);
|
||||
}
|
||||
qualified_tools
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ use codex_otel::current_span_trace_id;
|
||||
use codex_otel::current_span_w3c_trace_context;
|
||||
use codex_otel::set_parent_from_w3c_trace_context;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::ToolName;
|
||||
use codex_protocol::approvals::ElicitationRequestEvent;
|
||||
use codex_protocol::approvals::ExecPolicyAmendment;
|
||||
use codex_protocol::approvals::NetworkPolicyAmendment;
|
||||
@@ -4431,16 +4432,12 @@ impl Session {
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_mcp_tool_info(
|
||||
&self,
|
||||
name: &str,
|
||||
namespace: Option<&str>,
|
||||
) -> Option<ToolInfo> {
|
||||
pub(crate) async fn resolve_mcp_tool_info(&self, tool_name: &ToolName) -> Option<ToolInfo> {
|
||||
self.services
|
||||
.mcp_connection_manager
|
||||
.read()
|
||||
.await
|
||||
.resolve_tool_info(name, namespace)
|
||||
.resolve_tool_info(tool_name)
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ use codex_protocol::protocol::ReadOnlyAccess;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::request_permissions::PermissionGrantScope;
|
||||
use codex_protocol::request_permissions::RequestPermissionProfile;
|
||||
use codex_tools::ToolName;
|
||||
use tracing::Span;
|
||||
|
||||
use crate::RolloutRecorderParams;
|
||||
@@ -414,10 +415,18 @@ fn make_mcp_tool(
|
||||
} else {
|
||||
format!("mcp__{server_name}__")
|
||||
};
|
||||
let callable_name = if server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
connector_id
|
||||
.and_then(|connector_id| tool_name.strip_prefix(connector_id))
|
||||
.unwrap_or(tool_name)
|
||||
.to_string()
|
||||
} else {
|
||||
tool_name.to_string()
|
||||
};
|
||||
|
||||
ToolInfo {
|
||||
server_name: server_name.to_string(),
|
||||
callable_name: tool_name.to_string(),
|
||||
callable_name,
|
||||
callable_namespace: tool_namespace,
|
||||
server_instructions: None,
|
||||
tool: Tool {
|
||||
@@ -438,16 +447,14 @@ fn make_mcp_tool(
|
||||
}
|
||||
}
|
||||
|
||||
fn numbered_mcp_tools(count: usize) -> HashMap<String, ToolInfo> {
|
||||
fn numbered_mcp_tools(count: usize) -> HashMap<ToolName, ToolInfo> {
|
||||
(0..count)
|
||||
.map(|index| {
|
||||
let tool_name = format!("tool_{index}");
|
||||
(
|
||||
format!("mcp__rmcp__{tool_name}"),
|
||||
make_mcp_tool(
|
||||
"rmcp", &tool_name, /*connector_id*/ None, /*connector_name*/ None,
|
||||
),
|
||||
)
|
||||
let tool = make_mcp_tool(
|
||||
"rmcp", &tool_name, /*connector_id*/ None, /*connector_name*/ None,
|
||||
);
|
||||
(tool.callable_tool_name(), tool)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -934,9 +941,13 @@ fn mcp_tool_exposure_directly_exposes_small_effective_tool_sets() {
|
||||
&tools_config,
|
||||
);
|
||||
|
||||
let mut direct_tool_names: Vec<_> = exposure.direct_tools.keys().cloned().collect();
|
||||
let mut direct_tool_names: Vec<_> = exposure
|
||||
.direct_tools
|
||||
.keys()
|
||||
.map(ToolName::display)
|
||||
.collect();
|
||||
direct_tool_names.sort();
|
||||
let mut expected_tool_names: Vec<_> = mcp_tools.keys().cloned().collect();
|
||||
let mut expected_tool_names: Vec<_> = mcp_tools.keys().map(ToolName::display).collect();
|
||||
expected_tool_names.sort();
|
||||
assert_eq!(direct_tool_names, expected_tool_names);
|
||||
assert!(exposure.deferred_tools.is_none());
|
||||
@@ -961,9 +972,9 @@ fn mcp_tool_exposure_searches_large_effective_tool_sets() {
|
||||
.deferred_tools
|
||||
.as_ref()
|
||||
.expect("large tool sets should be discoverable through tool_search");
|
||||
let mut deferred_tool_names: Vec<_> = deferred_tools.keys().cloned().collect();
|
||||
let mut deferred_tool_names: Vec<_> = deferred_tools.keys().map(ToolName::display).collect();
|
||||
deferred_tool_names.sort();
|
||||
let mut expected_tool_names: Vec<_> = mcp_tools.keys().cloned().collect();
|
||||
let mut expected_tool_names: Vec<_> = mcp_tools.keys().map(ToolName::display).collect();
|
||||
expected_tool_names.sort();
|
||||
assert_eq!(deferred_tool_names, expected_tool_names);
|
||||
}
|
||||
@@ -973,15 +984,13 @@ fn mcp_tool_exposure_directly_exposes_explicit_apps_in_large_search_sets() {
|
||||
let config = test_config();
|
||||
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true);
|
||||
let mut mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD - 1);
|
||||
mcp_tools.extend([(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
make_mcp_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_create_event",
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
),
|
||||
)]);
|
||||
let calendar_tool = make_mcp_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_create_event",
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
);
|
||||
mcp_tools.insert(calendar_tool.callable_tool_name(), calendar_tool);
|
||||
let connectors = vec![make_connector("calendar", "Calendar")];
|
||||
|
||||
let exposure = build_mcp_tool_exposure(
|
||||
@@ -992,7 +1001,11 @@ fn mcp_tool_exposure_directly_exposes_explicit_apps_in_large_search_sets() {
|
||||
&tools_config,
|
||||
);
|
||||
|
||||
let mut tool_names: Vec<String> = exposure.direct_tools.into_keys().collect();
|
||||
let mut tool_names: Vec<String> = exposure
|
||||
.direct_tools
|
||||
.into_keys()
|
||||
.map(|name| name.display())
|
||||
.collect();
|
||||
tool_names.sort();
|
||||
assert_eq!(
|
||||
tool_names,
|
||||
@@ -1006,8 +1019,11 @@ fn mcp_tool_exposure_directly_exposes_explicit_apps_in_large_search_sets() {
|
||||
.deferred_tools
|
||||
.as_ref()
|
||||
.expect("large tool sets should be discoverable through tool_search");
|
||||
assert!(deferred_tools.contains_key("mcp__codex_apps__calendar_create_event"));
|
||||
assert!(deferred_tools.contains_key("mcp__rmcp__tool_0"));
|
||||
assert!(deferred_tools.contains_key(&ToolName::namespaced(
|
||||
"mcp__codex_apps__calendar",
|
||||
"_create_event"
|
||||
)));
|
||||
assert!(deferred_tools.contains_key(&ToolName::namespaced("mcp__rmcp__", "tool_0")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -19,6 +19,7 @@ use codex_connectors::DirectoryListResponse;
|
||||
use codex_login::token_data::TokenData;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_tools::DiscoverableTool;
|
||||
use codex_tools::ToolName;
|
||||
use rmcp::model::ToolAnnotations;
|
||||
use serde::Deserialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
@@ -159,7 +160,7 @@ pub async fn list_cached_accessible_connectors_from_mcp_tools(
|
||||
pub(crate) fn refresh_accessible_connectors_cache_from_mcp_tools(
|
||||
config: &Config,
|
||||
auth: Option<&CodexAuth>,
|
||||
mcp_tools: &HashMap<String, ToolInfo>,
|
||||
mcp_tools: &HashMap<ToolName, ToolInfo>,
|
||||
) {
|
||||
if !config.features.enabled(Feature::Apps) {
|
||||
return;
|
||||
@@ -510,7 +511,7 @@ pub fn connector_mention_slug(connector: &AppInfo) -> String {
|
||||
}
|
||||
|
||||
pub(crate) fn accessible_connectors_from_mcp_tools(
|
||||
mcp_tools: &HashMap<String, ToolInfo>,
|
||||
mcp_tools: &HashMap<ToolName, ToolInfo>,
|
||||
) -> Vec<AppInfo> {
|
||||
// ToolInfo already carries plugin provenance, so app-level plugin sources
|
||||
// can be derived here instead of requiring a separate enrichment pass.
|
||||
|
||||
@@ -14,6 +14,7 @@ use codex_config::types::AppsDefaultConfig;
|
||||
use codex_features::Feature;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::ToolInfo;
|
||||
use codex_tools::ToolName;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::model::JsonObject;
|
||||
@@ -121,6 +122,13 @@ fn codex_app_tool(
|
||||
}
|
||||
}
|
||||
|
||||
fn mcp_tool_map<const N: usize>(tools: [ToolInfo; N]) -> HashMap<ToolName, ToolInfo> {
|
||||
tools
|
||||
.into_iter()
|
||||
.map(|tool| (tool.callable_tool_name(), tool))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn with_accessible_connectors_cache_cleared<R>(f: impl FnOnce() -> R) -> R {
|
||||
let previous = {
|
||||
let mut cache_guard = ACCESSIBLE_CONNECTORS_CACHE
|
||||
@@ -166,39 +174,30 @@ fn merge_connectors_replaces_plugin_placeholder_name_with_accessible_name() {
|
||||
|
||||
#[test]
|
||||
fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() {
|
||||
let tools = HashMap::from([
|
||||
(
|
||||
"mcp__codex_apps__calendar_list_events".to_string(),
|
||||
codex_app_tool(
|
||||
"calendar_list_events",
|
||||
"calendar",
|
||||
/*connector_name*/ None,
|
||||
&["sample", "sample"],
|
||||
),
|
||||
let tools = mcp_tool_map([
|
||||
codex_app_tool(
|
||||
"calendar_list_events",
|
||||
"calendar",
|
||||
/*connector_name*/ None,
|
||||
&["sample", "sample"],
|
||||
),
|
||||
(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
codex_app_tool(
|
||||
"calendar_create_event",
|
||||
"calendar",
|
||||
Some("Google Calendar"),
|
||||
&["beta", "sample"],
|
||||
),
|
||||
),
|
||||
(
|
||||
"mcp__sample__echo".to_string(),
|
||||
ToolInfo {
|
||||
server_name: "sample".to_string(),
|
||||
callable_name: "echo".to_string(),
|
||||
callable_namespace: "sample".to_string(),
|
||||
server_instructions: None,
|
||||
tool: test_tool_definition("echo"),
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
connector_description: None,
|
||||
plugin_display_names: plugin_names(&["ignored"]),
|
||||
},
|
||||
codex_app_tool(
|
||||
"calendar_create_event",
|
||||
"calendar",
|
||||
Some("Google Calendar"),
|
||||
&["beta", "sample"],
|
||||
),
|
||||
ToolInfo {
|
||||
server_name: "sample".to_string(),
|
||||
callable_name: "echo".to_string(),
|
||||
callable_namespace: "sample".to_string(),
|
||||
server_instructions: None,
|
||||
tool: test_tool_definition("echo"),
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
connector_description: None,
|
||||
plugin_display_names: plugin_names(&["ignored"]),
|
||||
},
|
||||
]);
|
||||
|
||||
let connectors = accessible_connectors_from_mcp_tools(&tools);
|
||||
@@ -233,24 +232,18 @@ async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_instal
|
||||
.expect("config should load");
|
||||
let _ = config.features.set_enabled(Feature::Apps, /*enabled*/ true);
|
||||
let cache_key = accessible_connectors_cache_key(&config, /*auth*/ None);
|
||||
let tools = HashMap::from([
|
||||
(
|
||||
"mcp__codex_apps__calendar_list_events".to_string(),
|
||||
codex_app_tool(
|
||||
"calendar_list_events",
|
||||
"calendar",
|
||||
Some("Google Calendar"),
|
||||
&["calendar-plugin"],
|
||||
),
|
||||
let tools = mcp_tool_map([
|
||||
codex_app_tool(
|
||||
"calendar_list_events",
|
||||
"calendar",
|
||||
Some("Google Calendar"),
|
||||
&["calendar-plugin"],
|
||||
),
|
||||
(
|
||||
"mcp__codex_apps__openai_hidden".to_string(),
|
||||
codex_app_tool(
|
||||
"openai_hidden",
|
||||
"connector_openai_hidden",
|
||||
Some("Hidden"),
|
||||
&[],
|
||||
),
|
||||
codex_app_tool(
|
||||
"openai_hidden",
|
||||
"connector_openai_hidden",
|
||||
Some("Hidden"),
|
||||
&[],
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -310,30 +303,27 @@ fn merge_connectors_unions_and_dedupes_plugin_display_names() {
|
||||
|
||||
#[test]
|
||||
fn accessible_connectors_from_mcp_tools_preserves_description() {
|
||||
let mcp_tools = HashMap::from([(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
callable_name: "calendar_create_event".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
server_instructions: None,
|
||||
tool: Tool {
|
||||
name: "calendar_create_event".to_string().into(),
|
||||
title: None,
|
||||
description: Some("Create a calendar event".into()),
|
||||
input_schema: Arc::new(JsonObject::default()),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
connector_id: Some("calendar".to_string()),
|
||||
connector_name: Some("Calendar".to_string()),
|
||||
connector_description: Some("Plan events".to_string()),
|
||||
plugin_display_names: Vec::new(),
|
||||
let mcp_tools = mcp_tool_map([ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
callable_name: "calendar_create_event".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
server_instructions: None,
|
||||
tool: Tool {
|
||||
name: "calendar_create_event".to_string().into(),
|
||||
title: None,
|
||||
description: Some("Create a calendar event".into()),
|
||||
input_schema: Arc::new(JsonObject::default()),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
)]);
|
||||
connector_id: Some("calendar".to_string()),
|
||||
connector_name: Some("Calendar".to_string()),
|
||||
connector_description: Some("Plan events".to_string()),
|
||||
plugin_display_names: Vec::new(),
|
||||
}]);
|
||||
|
||||
assert_eq!(
|
||||
accessible_connectors_from_mcp_tools(&mcp_tools),
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::collections::HashSet;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::ToolInfo as McpToolInfo;
|
||||
use codex_mcp::filter_non_codex_apps_mcp_tools_only;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolsConfig;
|
||||
|
||||
use crate::config::Config;
|
||||
@@ -12,12 +13,12 @@ use crate::connectors;
|
||||
pub(crate) const DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD: usize = 100;
|
||||
|
||||
pub(crate) struct McpToolExposure {
|
||||
pub(crate) direct_tools: HashMap<String, McpToolInfo>,
|
||||
pub(crate) deferred_tools: Option<HashMap<String, McpToolInfo>>,
|
||||
pub(crate) direct_tools: HashMap<ToolName, McpToolInfo>,
|
||||
pub(crate) deferred_tools: Option<HashMap<ToolName, McpToolInfo>>,
|
||||
}
|
||||
|
||||
pub(crate) fn build_mcp_tool_exposure(
|
||||
all_mcp_tools: &HashMap<String, McpToolInfo>,
|
||||
all_mcp_tools: &HashMap<ToolName, McpToolInfo>,
|
||||
connectors: Option<&[connectors::AppInfo]>,
|
||||
explicitly_enabled_connectors: &[connectors::AppInfo],
|
||||
config: &Config,
|
||||
@@ -48,10 +49,10 @@ pub(crate) fn build_mcp_tool_exposure(
|
||||
}
|
||||
|
||||
fn filter_codex_apps_mcp_tools(
|
||||
mcp_tools: &HashMap<String, McpToolInfo>,
|
||||
mcp_tools: &HashMap<ToolName, McpToolInfo>,
|
||||
connectors: &[connectors::AppInfo],
|
||||
config: &Config,
|
||||
) -> HashMap<String, McpToolInfo> {
|
||||
) -> HashMap<ToolName, McpToolInfo> {
|
||||
let allowed: HashSet<&str> = connectors
|
||||
.iter()
|
||||
.map(|connector| connector.id.as_str())
|
||||
|
||||
@@ -9,10 +9,11 @@ use crate::plugins::PluginCapabilitySummary;
|
||||
use crate::plugins::render_explicit_plugin_instructions;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::ToolInfo;
|
||||
use codex_tools::ToolName;
|
||||
|
||||
pub(crate) fn build_plugin_injections(
|
||||
mentioned_plugins: &[PluginCapabilitySummary],
|
||||
mcp_tools: &HashMap<String, ToolInfo>,
|
||||
mcp_tools: &HashMap<ToolName, ToolInfo>,
|
||||
available_connectors: &[connectors::AppInfo],
|
||||
) -> Vec<ResponseItem> {
|
||||
if mentioned_plugins.is_empty() {
|
||||
|
||||
@@ -122,7 +122,7 @@ impl CodeModeTurnHost for CoreTurnHost {
|
||||
call_nested_tool(
|
||||
self.exec.clone(),
|
||||
self.tool_runtime.clone(),
|
||||
tool_name,
|
||||
ToolName::plain(tool_name),
|
||||
input,
|
||||
cancellation_token,
|
||||
)
|
||||
@@ -274,38 +274,40 @@ async fn build_nested_router(exec: &ExecContext) -> ToolRouter {
|
||||
async fn call_nested_tool(
|
||||
exec: ExecContext,
|
||||
tool_runtime: ToolCallRuntime,
|
||||
tool_name: String,
|
||||
tool_name: ToolName,
|
||||
input: Option<JsonValue>,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Result<JsonValue, FunctionCallError> {
|
||||
if tool_name == PUBLIC_TOOL_NAME {
|
||||
if tool_name.namespace.is_none() && tool_name.name == PUBLIC_TOOL_NAME {
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"{PUBLIC_TOOL_NAME} cannot invoke itself"
|
||||
)));
|
||||
}
|
||||
|
||||
let payload = if let Some(tool_info) = exec
|
||||
.session
|
||||
.resolve_mcp_tool_info(&tool_name, /*namespace*/ None)
|
||||
.await
|
||||
{
|
||||
match serialize_function_tool_arguments(&tool_name, input) {
|
||||
Ok(raw_arguments) => ToolPayload::Mcp {
|
||||
server: tool_info.server_name,
|
||||
tool: tool_info.tool.name.to_string(),
|
||||
raw_arguments,
|
||||
},
|
||||
Err(error) => return Err(FunctionCallError::RespondToModel(error)),
|
||||
}
|
||||
} else {
|
||||
match build_nested_tool_payload(tool_runtime.find_spec(&tool_name), &tool_name, input) {
|
||||
Ok(payload) => payload,
|
||||
Err(error) => return Err(FunctionCallError::RespondToModel(error)),
|
||||
}
|
||||
};
|
||||
let display_name = tool_name.display();
|
||||
let (tool_call_name, payload) =
|
||||
if let Some(tool_info) = exec.session.resolve_mcp_tool_info(&tool_name).await {
|
||||
let raw_arguments = match serialize_function_tool_arguments(&display_name, input) {
|
||||
Ok(raw_arguments) => raw_arguments,
|
||||
Err(error) => return Err(FunctionCallError::RespondToModel(error)),
|
||||
};
|
||||
(
|
||||
tool_info.callable_tool_name(),
|
||||
ToolPayload::Mcp {
|
||||
server: tool_info.server_name,
|
||||
tool: tool_info.tool.name.to_string(),
|
||||
raw_arguments,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
match build_nested_tool_payload(tool_runtime.find_spec(&tool_name), &tool_name, input) {
|
||||
Ok(payload) => (tool_name, payload),
|
||||
Err(error) => return Err(FunctionCallError::RespondToModel(error)),
|
||||
}
|
||||
};
|
||||
|
||||
let call = ToolCall {
|
||||
tool_name: ToolName::plain(tool_name.clone()),
|
||||
tool_name: tool_call_name,
|
||||
call_id: format!("{PUBLIC_TOOL_NAME}-{}", uuid::Uuid::new_v4()),
|
||||
payload,
|
||||
};
|
||||
@@ -325,16 +327,17 @@ fn tool_kind_for_spec(spec: &ToolSpec) -> codex_code_mode::CodeModeToolKind {
|
||||
|
||||
fn tool_kind_for_name(
|
||||
spec: Option<ToolSpec>,
|
||||
tool_name: &str,
|
||||
tool_name: &ToolName,
|
||||
) -> Result<codex_code_mode::CodeModeToolKind, String> {
|
||||
let display_name = tool_name.display();
|
||||
spec.as_ref()
|
||||
.map(tool_kind_for_spec)
|
||||
.ok_or_else(|| format!("tool `{tool_name}` is not enabled in {PUBLIC_TOOL_NAME}"))
|
||||
.ok_or_else(|| format!("tool `{display_name}` is not enabled in {PUBLIC_TOOL_NAME}"))
|
||||
}
|
||||
|
||||
fn build_nested_tool_payload(
|
||||
spec: Option<ToolSpec>,
|
||||
tool_name: &str,
|
||||
tool_name: &ToolName,
|
||||
input: Option<JsonValue>,
|
||||
) -> Result<ToolPayload, String> {
|
||||
let actual_kind = tool_kind_for_name(spec, tool_name)?;
|
||||
@@ -349,10 +352,11 @@ fn build_nested_tool_payload(
|
||||
}
|
||||
|
||||
fn build_function_tool_payload(
|
||||
tool_name: &str,
|
||||
tool_name: &ToolName,
|
||||
input: Option<JsonValue>,
|
||||
) -> Result<ToolPayload, String> {
|
||||
let arguments = serialize_function_tool_arguments(tool_name, input)?;
|
||||
let display_name = tool_name.display();
|
||||
let arguments = serialize_function_tool_arguments(&display_name, input)?;
|
||||
Ok(ToolPayload::Function { arguments })
|
||||
}
|
||||
|
||||
@@ -371,11 +375,12 @@ fn serialize_function_tool_arguments(
|
||||
}
|
||||
|
||||
fn build_freeform_tool_payload(
|
||||
tool_name: &str,
|
||||
tool_name: &ToolName,
|
||||
input: Option<JsonValue>,
|
||||
) -> Result<ToolPayload, String> {
|
||||
let display_name = tool_name.display();
|
||||
match input {
|
||||
Some(JsonValue::String(input)) => Ok(ToolPayload::Custom { input }),
|
||||
_ => Err(format!("tool `{tool_name}` expects a string input")),
|
||||
_ => Err(format!("tool `{display_name}` expects a string input")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,18 +11,19 @@ use bm25::SearchEngineBuilder;
|
||||
use codex_mcp::ToolInfo;
|
||||
use codex_tools::TOOL_SEARCH_DEFAULT_LIMIT;
|
||||
use codex_tools::TOOL_SEARCH_TOOL_NAME;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSearchResultSource;
|
||||
use codex_tools::collect_tool_search_output_tools;
|
||||
|
||||
pub struct ToolSearchHandler {
|
||||
entries: Vec<(String, ToolInfo)>,
|
||||
entries: Vec<(ToolName, ToolInfo)>,
|
||||
search_engine: SearchEngine<usize>,
|
||||
}
|
||||
|
||||
impl ToolSearchHandler {
|
||||
pub fn new(tools: std::collections::HashMap<String, ToolInfo>) -> Self {
|
||||
let mut entries: Vec<(String, ToolInfo)> = tools.into_iter().collect();
|
||||
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
pub fn new(tools: std::collections::HashMap<ToolName, ToolInfo>) -> Self {
|
||||
let mut entries: Vec<(ToolName, ToolInfo)> = tools.into_iter().collect();
|
||||
entries.sort_by_key(|entry| entry.0.display());
|
||||
|
||||
let documents: Vec<Document<usize>> = entries
|
||||
.iter()
|
||||
@@ -104,9 +105,9 @@ impl ToolHandler for ToolSearchHandler {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_search_text(name: &str, info: &ToolInfo) -> String {
|
||||
fn build_search_text(name: &ToolName, info: &ToolInfo) -> String {
|
||||
let mut parts = vec![
|
||||
name.to_string(),
|
||||
name.display(),
|
||||
info.callable_name.clone(),
|
||||
info.tool.name.to_string(),
|
||||
info.server_name.clone(),
|
||||
|
||||
@@ -1572,29 +1572,38 @@ impl JsReplManager {
|
||||
},
|
||||
);
|
||||
|
||||
let payload = if let Some(tool_info) = exec
|
||||
let requested_tool_name = codex_tools::ToolName::plain(req.tool_name.clone());
|
||||
let (tool_call_name, payload) = if let Some(tool_info) = exec
|
||||
.session
|
||||
.resolve_mcp_tool_info(&req.tool_name, /*namespace*/ None)
|
||||
.resolve_mcp_tool_info(&requested_tool_name)
|
||||
.await
|
||||
{
|
||||
crate::tools::context::ToolPayload::Mcp {
|
||||
server: tool_info.server_name,
|
||||
tool: tool_info.tool.name.to_string(),
|
||||
raw_arguments: req.arguments.clone(),
|
||||
}
|
||||
(
|
||||
tool_info.callable_tool_name(),
|
||||
crate::tools::context::ToolPayload::Mcp {
|
||||
server: tool_info.server_name,
|
||||
tool: tool_info.tool.name.to_string(),
|
||||
raw_arguments: req.arguments.clone(),
|
||||
},
|
||||
)
|
||||
} else if is_freeform_tool(&router.specs(), &req.tool_name) {
|
||||
crate::tools::context::ToolPayload::Custom {
|
||||
input: req.arguments.clone(),
|
||||
}
|
||||
(
|
||||
requested_tool_name,
|
||||
crate::tools::context::ToolPayload::Custom {
|
||||
input: req.arguments.clone(),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
crate::tools::context::ToolPayload::Function {
|
||||
arguments: req.arguments.clone(),
|
||||
}
|
||||
(
|
||||
requested_tool_name,
|
||||
crate::tools::context::ToolPayload::Function {
|
||||
arguments: req.arguments.clone(),
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
let tool_name = req.tool_name.clone();
|
||||
let call = crate::tools::router::ToolCall {
|
||||
tool_name: codex_tools::ToolName::plain(tool_name.clone()),
|
||||
tool_name: tool_call_name,
|
||||
call_id: req.id.clone(),
|
||||
payload,
|
||||
};
|
||||
|
||||
@@ -48,7 +48,7 @@ impl ToolCallRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn find_spec(&self, tool_name: &str) -> Option<ToolSpec> {
|
||||
pub(crate) fn find_spec(&self, tool_name: &codex_tools::ToolName) -> Option<ToolSpec> {
|
||||
self.router.find_spec(tool_name)
|
||||
}
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@ pub struct ToolRouter {
|
||||
}
|
||||
|
||||
pub(crate) struct ToolRouterParams<'a> {
|
||||
pub(crate) mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
pub(crate) deferred_mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
pub(crate) mcp_tools: Option<HashMap<ToolName, ToolInfo>>,
|
||||
pub(crate) deferred_mcp_tools: Option<HashMap<ToolName, ToolInfo>>,
|
||||
pub(crate) discoverable_tools: Option<Vec<DiscoverableTool>>,
|
||||
pub(crate) dynamic_tools: &'a [DynamicToolSpec],
|
||||
}
|
||||
@@ -97,10 +97,11 @@ impl ToolRouter {
|
||||
self.model_visible_specs.clone()
|
||||
}
|
||||
|
||||
pub fn find_spec(&self, tool_name: &str) -> Option<ToolSpec> {
|
||||
pub fn find_spec(&self, tool_name: &ToolName) -> Option<ToolSpec> {
|
||||
let display_name = tool_name.display();
|
||||
self.specs
|
||||
.iter()
|
||||
.find(|config| config.name() == tool_name)
|
||||
.find(|config| config.name() == display_name.as_str())
|
||||
.map(|config| config.spec.clone())
|
||||
}
|
||||
|
||||
@@ -126,16 +127,10 @@ impl ToolRouter {
|
||||
call_id,
|
||||
..
|
||||
} => {
|
||||
let mcp_tool = session
|
||||
.resolve_mcp_tool_info(&name, namespace.as_deref())
|
||||
.await;
|
||||
let tool_name = match namespace {
|
||||
Some(namespace) => ToolName::namespaced(namespace, name),
|
||||
None => ToolName::plain(name),
|
||||
};
|
||||
if let Some(tool_info) = mcp_tool {
|
||||
let tool_name = ToolName::new(namespace, name);
|
||||
if let Some(tool_info) = session.resolve_mcp_tool_info(&tool_name).await {
|
||||
Ok(Some(ToolCall {
|
||||
tool_name,
|
||||
tool_name: tool_info.callable_tool_name(),
|
||||
call_id,
|
||||
payload: ToolPayload::Mcp {
|
||||
server: tool_info.server_name,
|
||||
|
||||
@@ -9,8 +9,10 @@ use codex_mcp::ToolInfo;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_tools::DiscoverableTool;
|
||||
use codex_tools::ToolHandlerKind;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolNamespace;
|
||||
use codex_tools::ToolRegistryPlanDeferredTool;
|
||||
use codex_tools::ToolRegistryPlanMcpTool;
|
||||
use codex_tools::ToolRegistryPlanParams;
|
||||
use codex_tools::ToolUserShellType;
|
||||
use codex_tools::ToolsConfig;
|
||||
@@ -29,16 +31,19 @@ pub(crate) fn tool_user_shell_type(user_shell: &Shell) -> ToolUserShellType {
|
||||
}
|
||||
}
|
||||
|
||||
struct McpToolPlanInputs {
|
||||
mcp_tools: HashMap<String, rmcp::model::Tool>,
|
||||
tool_namespaces: HashMap<String, ToolNamespace>,
|
||||
struct McpToolPlanInputs<'a> {
|
||||
mcp_tools: Vec<ToolRegistryPlanMcpTool<'a>>,
|
||||
tool_namespaces: HashMap<ToolName, ToolNamespace>,
|
||||
}
|
||||
|
||||
fn map_mcp_tools_for_plan(mcp_tools: &HashMap<String, ToolInfo>) -> McpToolPlanInputs {
|
||||
fn map_mcp_tools_for_plan(mcp_tools: &HashMap<ToolName, ToolInfo>) -> McpToolPlanInputs<'_> {
|
||||
McpToolPlanInputs {
|
||||
mcp_tools: mcp_tools
|
||||
.iter()
|
||||
.map(|(name, tool)| (name.clone(), tool.tool.clone()))
|
||||
.map(|(name, tool)| ToolRegistryPlanMcpTool {
|
||||
name: name.clone(),
|
||||
tool: &tool.tool,
|
||||
})
|
||||
.collect(),
|
||||
tool_namespaces: mcp_tools
|
||||
.iter()
|
||||
@@ -57,8 +62,8 @@ fn map_mcp_tools_for_plan(mcp_tools: &HashMap<String, ToolInfo>) -> McpToolPlanI
|
||||
|
||||
pub(crate) fn build_specs_with_discoverable_tools(
|
||||
config: &ToolsConfig,
|
||||
mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
deferred_mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
mcp_tools: Option<HashMap<ToolName, ToolInfo>>,
|
||||
deferred_mcp_tools: Option<HashMap<ToolName, ToolInfo>>,
|
||||
discoverable_tools: Option<Vec<DiscoverableTool>>,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> ToolRegistryBuilder {
|
||||
@@ -97,10 +102,9 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
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 {
|
||||
tool_name: tool.callable_name.as_str(),
|
||||
tool_namespace: tool.callable_namespace.as_str(),
|
||||
.iter()
|
||||
.map(|(name, tool)| ToolRegistryPlanDeferredTool {
|
||||
name: name.clone(),
|
||||
server_name: tool.server_name.as_str(),
|
||||
connector_name: tool.connector_name.as_deref(),
|
||||
connector_description: tool.connector_description.as_deref(),
|
||||
@@ -114,7 +118,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
ToolRegistryPlanParams {
|
||||
mcp_tools: mcp_tool_plan_inputs
|
||||
.as_ref()
|
||||
.map(|inputs| &inputs.mcp_tools),
|
||||
.map(|inputs| inputs.mcp_tools.as_slice()),
|
||||
deferred_mcp_tools: deferred_mcp_tool_sources.as_deref(),
|
||||
tool_namespaces: mcp_tool_plan_inputs
|
||||
.as_ref()
|
||||
|
||||
@@ -67,6 +67,32 @@ fn mcp_tool_info(tool: rmcp::model::Tool) -> ToolInfo {
|
||||
}
|
||||
}
|
||||
|
||||
fn mcp_tool_info_with_display_name(display_name: &str, tool: rmcp::model::Tool) -> ToolInfo {
|
||||
let (callable_namespace, callable_name) = display_name
|
||||
.rsplit_once('/')
|
||||
.map(|(namespace, callable_name)| (format!("{namespace}/"), callable_name.to_string()))
|
||||
.unwrap_or_else(|| ("".to_string(), display_name.to_string()));
|
||||
|
||||
ToolInfo {
|
||||
server_name: "test_server".to_string(),
|
||||
callable_name,
|
||||
callable_namespace,
|
||||
server_instructions: None,
|
||||
tool,
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
plugin_display_names: Vec::new(),
|
||||
connector_description: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn mcp_tool_map<const N: usize>(tools: [ToolInfo; N]) -> HashMap<ToolName, ToolInfo> {
|
||||
tools
|
||||
.into_iter()
|
||||
.map(|tool| (tool.callable_tool_name(), tool))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn discoverable_connector(id: &str, name: &str, description: &str) -> DiscoverableTool {
|
||||
let slug = name.replace(' ', "-").to_lowercase();
|
||||
DiscoverableTool::Connector(Box::new(AppInfo {
|
||||
@@ -222,8 +248,8 @@ fn model_info_from_models_json(slug: &str) -> ModelInfo {
|
||||
/// Builds the tool registry builder while collecting tool specs for later serialization.
|
||||
fn build_specs(
|
||||
config: &ToolsConfig,
|
||||
mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
deferred_mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
mcp_tools: Option<HashMap<ToolName, ToolInfo>>,
|
||||
deferred_mcp_tools: Option<HashMap<ToolName, ToolInfo>>,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> ToolRegistryBuilder {
|
||||
build_specs_with_discoverable_tools(
|
||||
@@ -796,24 +822,21 @@ fn search_tool_description_falls_back_to_connector_name_without_description() {
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
Some(HashMap::from([(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
callable_name: "_create_event".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
server_instructions: None,
|
||||
tool: mcp_tool(
|
||||
"calendar_create_event",
|
||||
"Create calendar event",
|
||||
serde_json::json!({"type": "object"}),
|
||||
),
|
||||
connector_id: Some("calendar".to_string()),
|
||||
connector_name: Some("Calendar".to_string()),
|
||||
plugin_display_names: Vec::new(),
|
||||
connector_description: None,
|
||||
},
|
||||
)])),
|
||||
Some(mcp_tool_map([ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
callable_name: "_create_event".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
server_instructions: None,
|
||||
tool: mcp_tool(
|
||||
"calendar_create_event",
|
||||
"Create calendar event",
|
||||
serde_json::json!({"type": "object"}),
|
||||
),
|
||||
connector_id: Some("calendar".to_string()),
|
||||
connector_name: Some("Calendar".to_string()),
|
||||
plugin_display_names: Vec::new(),
|
||||
connector_description: None,
|
||||
}])),
|
||||
&[],
|
||||
)
|
||||
.build();
|
||||
@@ -847,57 +870,48 @@ fn search_tool_registers_namespaced_mcp_tool_aliases() {
|
||||
let (_, registry) = build_specs(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
Some(HashMap::from([
|
||||
(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
callable_name: "_create_event".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
server_instructions: None,
|
||||
tool: mcp_tool(
|
||||
"calendar-create-event",
|
||||
"Create calendar event",
|
||||
serde_json::json!({"type": "object"}),
|
||||
),
|
||||
connector_id: Some("calendar".to_string()),
|
||||
connector_name: Some("Calendar".to_string()),
|
||||
connector_description: None,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"mcp__codex_apps__calendar_list_events".to_string(),
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
callable_name: "_list_events".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
server_instructions: None,
|
||||
tool: mcp_tool(
|
||||
"calendar-list-events",
|
||||
"List calendar events",
|
||||
serde_json::json!({"type": "object"}),
|
||||
),
|
||||
connector_id: Some("calendar".to_string()),
|
||||
connector_name: Some("Calendar".to_string()),
|
||||
connector_description: None,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"mcp__rmcp__echo".to_string(),
|
||||
ToolInfo {
|
||||
server_name: "rmcp".to_string(),
|
||||
callable_name: "echo".to_string(),
|
||||
callable_namespace: "mcp__rmcp__".to_string(),
|
||||
server_instructions: None,
|
||||
tool: mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})),
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
connector_description: None,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
),
|
||||
Some(mcp_tool_map([
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
callable_name: "_create_event".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
server_instructions: None,
|
||||
tool: mcp_tool(
|
||||
"calendar-create-event",
|
||||
"Create calendar event",
|
||||
serde_json::json!({"type": "object"}),
|
||||
),
|
||||
connector_id: Some("calendar".to_string()),
|
||||
connector_name: Some("Calendar".to_string()),
|
||||
connector_description: None,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
callable_name: "_list_events".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
server_instructions: None,
|
||||
tool: mcp_tool(
|
||||
"calendar-list-events",
|
||||
"List calendar events",
|
||||
serde_json::json!({"type": "object"}),
|
||||
),
|
||||
connector_id: Some("calendar".to_string()),
|
||||
connector_name: Some("Calendar".to_string()),
|
||||
connector_description: None,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
ToolInfo {
|
||||
server_name: "rmcp".to_string(),
|
||||
callable_name: "echo".to_string(),
|
||||
callable_namespace: "mcp__rmcp__".to_string(),
|
||||
server_instructions: None,
|
||||
tool: mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})),
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
connector_description: None,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
])),
|
||||
&[],
|
||||
)
|
||||
@@ -913,6 +927,40 @@ fn search_tool_registers_namespaced_mcp_tool_aliases() {
|
||||
assert!(registry.has_handler(&ToolName::plain("mcp__rmcp__echo")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_mcp_tools_register_namespaced_handlers() {
|
||||
let config = test_config();
|
||||
let model_info = construct_model_info_offline("gpt-5-codex", &config);
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
let available_models = Vec::new();
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
available_models: &available_models,
|
||||
features: &features,
|
||||
image_generation_tool_auth_allowed: true,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
session_source: SessionSource::Cli,
|
||||
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
});
|
||||
|
||||
let (_, registry) = build_specs(
|
||||
&tools_config,
|
||||
Some(mcp_tool_map([mcp_tool_info(mcp_tool(
|
||||
"echo",
|
||||
"Echo",
|
||||
serde_json::json!({"type": "object"}),
|
||||
))])),
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
)
|
||||
.build();
|
||||
|
||||
assert!(registry.has_handler(&ToolName::namespaced("mcp__test_server__", "echo")));
|
||||
assert!(!registry.has_handler(&ToolName::plain("mcp__test_server__echo")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mcp_tool_property_missing_type_defaults_to_string() {
|
||||
let config = test_config();
|
||||
@@ -933,9 +981,9 @@ fn test_mcp_tool_property_missing_type_defaults_to_string() {
|
||||
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"dash/search".to_string(),
|
||||
mcp_tool_info(mcp_tool(
|
||||
Some(mcp_tool_map([mcp_tool_info_with_display_name(
|
||||
"dash/search",
|
||||
mcp_tool(
|
||||
"search",
|
||||
"Search docs",
|
||||
serde_json::json!({
|
||||
@@ -944,7 +992,7 @@ fn test_mcp_tool_property_missing_type_defaults_to_string() {
|
||||
"query": {"description": "search query"}
|
||||
}
|
||||
}),
|
||||
)),
|
||||
),
|
||||
)])),
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
@@ -993,16 +1041,16 @@ fn test_mcp_tool_preserves_integer_schema() {
|
||||
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"dash/paginate".to_string(),
|
||||
mcp_tool_info(mcp_tool(
|
||||
Some(mcp_tool_map([mcp_tool_info_with_display_name(
|
||||
"dash/paginate",
|
||||
mcp_tool(
|
||||
"paginate",
|
||||
"Pagination",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {"page": {"type": "integer"}}
|
||||
}),
|
||||
)),
|
||||
),
|
||||
)])),
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
@@ -1052,16 +1100,16 @@ fn test_mcp_tool_array_without_items_gets_default_string_items() {
|
||||
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"dash/tags".to_string(),
|
||||
mcp_tool_info(mcp_tool(
|
||||
Some(mcp_tool_map([mcp_tool_info_with_display_name(
|
||||
"dash/tags",
|
||||
mcp_tool(
|
||||
"tags",
|
||||
"Tags",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {"tags": {"type": "array"}}
|
||||
}),
|
||||
)),
|
||||
),
|
||||
)])),
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
@@ -1113,9 +1161,9 @@ fn test_mcp_tool_anyof_defaults_to_string() {
|
||||
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"dash/value".to_string(),
|
||||
mcp_tool_info(mcp_tool(
|
||||
Some(mcp_tool_map([mcp_tool_info_with_display_name(
|
||||
"dash/value",
|
||||
mcp_tool(
|
||||
"value",
|
||||
"AnyOf Value",
|
||||
serde_json::json!({
|
||||
@@ -1124,7 +1172,7 @@ fn test_mcp_tool_anyof_defaults_to_string() {
|
||||
"value": {"anyOf": [{"type": "string"}, {"type": "number"}]}
|
||||
}
|
||||
}),
|
||||
)),
|
||||
),
|
||||
)])),
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
@@ -1178,9 +1226,9 @@ fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() {
|
||||
});
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"test_server/do_something_cool".to_string(),
|
||||
mcp_tool_info(mcp_tool(
|
||||
Some(mcp_tool_map([mcp_tool_info_with_display_name(
|
||||
"test_server/do_something_cool",
|
||||
mcp_tool(
|
||||
"do_something_cool",
|
||||
"Do something cool",
|
||||
serde_json::json!({
|
||||
@@ -1206,7 +1254,7 @@ fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() {
|
||||
}
|
||||
}
|
||||
}),
|
||||
)),
|
||||
),
|
||||
)])),
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
|
||||
@@ -2,8 +2,10 @@ pub mod account;
|
||||
mod agent_path;
|
||||
pub mod auth;
|
||||
mod thread_id;
|
||||
mod tool_name;
|
||||
pub use agent_path::AgentPath;
|
||||
pub use thread_id::ThreadId;
|
||||
pub use tool_name::ToolName;
|
||||
pub mod approvals;
|
||||
pub mod config_types;
|
||||
pub mod dynamic_tools;
|
||||
|
||||
@@ -7,6 +7,13 @@ pub struct ToolName {
|
||||
}
|
||||
|
||||
impl ToolName {
|
||||
pub fn new(namespace: Option<String>, name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
namespace,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn plain(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
@@ -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;
|
||||
|
||||
@@ -6,7 +6,6 @@ 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 +60,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 +74,9 @@ pub fn build_tool_registry_plan(
|
||||
.tool_namespaces
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|(name, detail)| {
|
||||
.map(|(tool_name, detail)| {
|
||||
(
|
||||
name.clone(),
|
||||
tool_name.display(),
|
||||
codex_code_mode::ToolNamespaceDescription {
|
||||
name: detail.name.clone(),
|
||||
description: detail.description.clone().unwrap_or_default(),
|
||||
@@ -266,10 +264,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,25 +466,23 @@ 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());
|
||||
|
||||
for (name, tool) in entries {
|
||||
match mcp_tool_to_responses_api_tool(name.clone(), tool) {
|
||||
for tool in entries {
|
||||
let display_name = tool.name.display();
|
||||
match mcp_tool_to_responses_api_tool(display_name.clone(), tool.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);
|
||||
plan.register_handler(tool.name, ToolHandlerKind::Mcp);
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
"Failed to convert {name:?} MCP tool to OpenAI tool: {error:?}"
|
||||
"Failed to convert {display_name:?} MCP tool to OpenAI tool: {error:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,10 @@ 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;
|
||||
@@ -1169,15 +1171,15 @@ 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"})),
|
||||
mcp_tool("do", "a", serde_json::json!({"type": "object"})),
|
||||
),
|
||||
(
|
||||
"test_server/something".to_string(),
|
||||
mcp_tool("b", "b", serde_json::json!({"type": "object"})),
|
||||
mcp_tool("something", "b", serde_json::json!({"type": "object"})),
|
||||
),
|
||||
(
|
||||
"test_server/cool".to_string(),
|
||||
mcp_tool("c", "c", serde_json::json!({"type": "object"})),
|
||||
mcp_tool("cool", "c", serde_json::json!({"type": "object"})),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -1886,14 +1888,31 @@ 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>>,
|
||||
tool_namespaces: Option<HashMap<ToolName, 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(|(qualified_name, tool)| {
|
||||
let raw_tool_name = tool.name.as_ref();
|
||||
let callable_namespace = qualified_name
|
||||
.strip_suffix(raw_tool_name)
|
||||
.filter(|namespace| !namespace.is_empty())
|
||||
.unwrap_or("mcp__test_server__");
|
||||
|
||||
ToolRegistryPlanMcpTool {
|
||||
name: ToolName::namespaced(callable_namespace.to_string(), raw_tool_name),
|
||||
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(),
|
||||
@@ -2018,8 +2037,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,
|
||||
|
||||
@@ -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,9 +57,9 @@ 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 tool_namespaces: Option<&'a HashMap<ToolName, ToolNamespace>>,
|
||||
pub discoverable_tools: Option<&'a [DiscoverableTool]>,
|
||||
pub dynamic_tools: &'a [DynamicToolSpec],
|
||||
pub default_agent_type_description: &'a str,
|
||||
@@ -73,10 +72,17 @@ pub struct ToolNamespace {
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// Direct MCP tool metadata needed to expose the flat Responses API 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>,
|
||||
|
||||
Reference in New Issue
Block a user