mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08: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:
+17
-75
@@ -25,6 +25,7 @@ use crate::config::ManagedFeatures;
|
||||
use crate::connectors;
|
||||
use crate::exec_policy::ExecPolicyManager;
|
||||
use crate::installation_id::resolve_installation_id;
|
||||
use crate::mcp_tool_exposure::build_mcp_tool_exposure;
|
||||
use crate::parse_turn_item;
|
||||
use crate::path_utils::normalize_for_native_workdir;
|
||||
use crate::realtime_conversation::RealtimeConversationManager;
|
||||
@@ -74,9 +75,7 @@ use codex_login::auth_env_telemetry::collect_auth_env_telemetry;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_mcp::McpConnectionManager;
|
||||
use codex_mcp::SandboxState;
|
||||
use codex_mcp::ToolInfo as McpToolInfo;
|
||||
use codex_mcp::codex_apps_tools_cache_key;
|
||||
use codex_mcp::filter_non_codex_apps_mcp_tools_only;
|
||||
#[cfg(test)]
|
||||
use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig;
|
||||
use codex_models_manager::manager::ModelsManager;
|
||||
@@ -323,7 +322,6 @@ use crate::util::backoff;
|
||||
use crate::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use codex_async_utils::OrCancelExt;
|
||||
use codex_git_utils::get_git_repo_root;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::compute_auth_statuses;
|
||||
use codex_mcp::with_codex_apps_mcp;
|
||||
use codex_otel::SessionTelemetry;
|
||||
@@ -442,8 +440,6 @@ pub(crate) const INITIAL_SUBMIT_ID: &str = "";
|
||||
pub(crate) const SUBMISSION_CHANNEL_CAPACITY: usize = 512;
|
||||
const CYBER_VERIFY_URL: &str = "https://chatgpt.com/cyber";
|
||||
const CYBER_SAFETY_URL: &str = "https://developers.openai.com/codex/concepts/cyber-safety";
|
||||
const DIRECT_APP_TOOL_EXPOSURE_THRESHOLD: usize = 100;
|
||||
|
||||
impl Codex {
|
||||
/// Spawn a new [`Codex`] and initialize the session.
|
||||
pub(crate) async fn spawn(args: CodexSpawnArgs) -> CodexResult<CodexSpawnOk> {
|
||||
@@ -6686,35 +6682,6 @@ fn connector_inserted_in_messages(
|
||||
connector_count == 1 && skill_count == 0 && mention_names_lower.contains(&mention_slug)
|
||||
}
|
||||
|
||||
fn filter_codex_apps_mcp_tools(
|
||||
mcp_tools: &HashMap<String, McpToolInfo>,
|
||||
connectors: &[connectors::AppInfo],
|
||||
config: &Config,
|
||||
) -> HashMap<String, McpToolInfo> {
|
||||
let allowed: HashSet<&str> = connectors
|
||||
.iter()
|
||||
.map(|connector| connector.id.as_str())
|
||||
.collect();
|
||||
|
||||
mcp_tools
|
||||
.iter()
|
||||
.filter(|(_, tool)| {
|
||||
if tool.server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return false;
|
||||
}
|
||||
let Some(connector_id) = codex_apps_connector_id(tool) else {
|
||||
return false;
|
||||
};
|
||||
allowed.contains(connector_id) && connectors::codex_app_tool_is_enabled(config, tool)
|
||||
})
|
||||
.map(|(name, tool)| (name.clone(), tool.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn codex_apps_connector_id(tool: &McpToolInfo) -> Option<&str> {
|
||||
tool.connector_id.as_deref()
|
||||
}
|
||||
|
||||
pub(crate) fn build_prompt(
|
||||
input: Vec<ResponseItem>,
|
||||
router: &ToolRouter,
|
||||
@@ -6901,7 +6868,7 @@ pub(crate) async fn built_tools(
|
||||
) -> CodexResult<Arc<ToolRouter>> {
|
||||
let mcp_connection_manager = sess.services.mcp_connection_manager.read().await;
|
||||
let has_mcp_servers = mcp_connection_manager.has_servers();
|
||||
let mut mcp_tools = mcp_connection_manager
|
||||
let all_mcp_tools = mcp_connection_manager
|
||||
.list_all_tools()
|
||||
.or_cancel(cancellation_token)
|
||||
.await?;
|
||||
@@ -6916,7 +6883,7 @@ pub(crate) async fn built_tools(
|
||||
|
||||
let apps_enabled = turn_context.apps_enabled();
|
||||
let accessible_connectors =
|
||||
apps_enabled.then(|| connectors::accessible_connectors_from_mcp_tools(&mcp_tools));
|
||||
apps_enabled.then(|| connectors::accessible_connectors_from_mcp_tools(&all_mcp_tools));
|
||||
let accessible_connectors_with_enabled_state =
|
||||
accessible_connectors.as_ref().map(|connectors| {
|
||||
connectors::with_app_enabled_state(connectors.clone(), &turn_context.config)
|
||||
@@ -6962,59 +6929,34 @@ pub(crate) async fn built_tools(
|
||||
None
|
||||
};
|
||||
|
||||
let app_tools = connectors.as_ref().map(|connectors| {
|
||||
filter_codex_apps_mcp_tools(&mcp_tools, connectors, &turn_context.config)
|
||||
});
|
||||
|
||||
if let Some(connectors) = connectors.as_ref() {
|
||||
let explicitly_enabled = if let Some(connectors) = connectors.as_ref() {
|
||||
let skill_name_counts_lower = skills_outcome.map_or_else(HashMap::new, |outcome| {
|
||||
build_skill_name_counts(&outcome.skills, &outcome.disabled_paths).1
|
||||
});
|
||||
|
||||
let explicitly_enabled = filter_connectors_for_input(
|
||||
filter_connectors_for_input(
|
||||
connectors,
|
||||
input,
|
||||
&effective_explicitly_enabled_connectors,
|
||||
&skill_name_counts_lower,
|
||||
);
|
||||
|
||||
let mut selected_mcp_tools = filter_non_codex_apps_mcp_tools_only(&mcp_tools);
|
||||
selected_mcp_tools.extend(filter_codex_apps_mcp_tools(
|
||||
&mcp_tools,
|
||||
explicitly_enabled.as_ref(),
|
||||
&turn_context.config,
|
||||
));
|
||||
|
||||
mcp_tools = selected_mcp_tools;
|
||||
}
|
||||
|
||||
// Expose app tools directly when tool_search is disabled, or when tool_search
|
||||
// is enabled but the accessible app tool set stays below the direct-exposure threshold.
|
||||
let expose_app_tools_directly = !turn_context.tools_config.search_tool
|
||||
|| app_tools
|
||||
.as_ref()
|
||||
.is_some_and(|tools| tools.len() < DIRECT_APP_TOOL_EXPOSURE_THRESHOLD);
|
||||
if expose_app_tools_directly && let Some(app_tools) = app_tools.as_ref() {
|
||||
mcp_tools.extend(app_tools.clone());
|
||||
}
|
||||
let app_tools = if expose_app_tools_directly {
|
||||
None
|
||||
)
|
||||
} else {
|
||||
app_tools
|
||||
Vec::new()
|
||||
};
|
||||
let mcp_tool_router_inputs =
|
||||
has_mcp_servers.then(|| crate::tools::router::map_mcp_tool_infos(&mcp_tools));
|
||||
let mcp_tool_exposure = build_mcp_tool_exposure(
|
||||
&all_mcp_tools,
|
||||
connectors.as_deref(),
|
||||
explicitly_enabled.as_slice(),
|
||||
&turn_context.config,
|
||||
&turn_context.tools_config,
|
||||
);
|
||||
let direct_mcp_tools = has_mcp_servers.then_some(mcp_tool_exposure.direct_tools);
|
||||
|
||||
Ok(Arc::new(ToolRouter::from_config(
|
||||
&turn_context.tools_config,
|
||||
ToolRouterParams {
|
||||
mcp_tools: mcp_tool_router_inputs
|
||||
.as_ref()
|
||||
.map(|inputs| inputs.mcp_tools.clone()),
|
||||
tool_namespaces: mcp_tool_router_inputs
|
||||
.as_ref()
|
||||
.map(|inputs| inputs.tool_namespaces.clone()),
|
||||
app_tools,
|
||||
deferred_mcp_tools: mcp_tool_exposure.deferred_tools,
|
||||
mcp_tools: direct_mcp_tools,
|
||||
discoverable_tools,
|
||||
dynamic_tools: turn_context.dynamic_tools.as_slice(),
|
||||
},
|
||||
|
||||
+116
-143
@@ -10,11 +10,14 @@ use crate::config_loader::RequirementSource;
|
||||
use crate::config_loader::Sourced;
|
||||
use crate::exec::ExecCapturePolicy;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::mcp_tool_exposure::DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD;
|
||||
use crate::mcp_tool_exposure::build_mcp_tool_exposure;
|
||||
use crate::shell::default_user_shell;
|
||||
use crate::tools::format_exec_output_str;
|
||||
|
||||
use codex_features::Features;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::ToolInfo;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
@@ -308,8 +311,7 @@ fn test_tool_runtime(session: Arc<Session>, turn_context: Arc<TurnContext>) -> T
|
||||
&turn_context.tools_config,
|
||||
crate::tools::router::ToolRouterParams {
|
||||
mcp_tools: None,
|
||||
tool_namespaces: None,
|
||||
app_tools: None,
|
||||
deferred_mcp_tools: None,
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn_context.dynamic_tools.as_slice(),
|
||||
},
|
||||
@@ -410,13 +412,13 @@ fn make_mcp_tool(
|
||||
.map(|connector_name| format!("mcp__{server_name}__{connector_name}"))
|
||||
.unwrap_or_else(|| server_name.to_string())
|
||||
} else {
|
||||
server_name.to_string()
|
||||
format!("mcp__{server_name}__")
|
||||
};
|
||||
|
||||
ToolInfo {
|
||||
server_name: server_name.to_string(),
|
||||
tool_name: tool_name.to_string(),
|
||||
tool_namespace,
|
||||
callable_name: tool_name.to_string(),
|
||||
callable_namespace: tool_namespace,
|
||||
server_instructions: None,
|
||||
tool: Tool {
|
||||
name: tool_name.to_string().into(),
|
||||
@@ -436,6 +438,42 @@ fn make_mcp_tool(
|
||||
}
|
||||
}
|
||||
|
||||
fn numbered_mcp_tools(count: usize) -> HashMap<String, 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,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn tools_config_for_mcp_tool_exposure(search_tool: bool) -> ToolsConfig {
|
||||
let config = test_config();
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests(
|
||||
"gpt-5-codex",
|
||||
&config.to_models_manager_config(),
|
||||
);
|
||||
let features = Features::with_defaults();
|
||||
let available_models = Vec::new();
|
||||
let mut 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,
|
||||
});
|
||||
tools_config.search_tool = search_tool;
|
||||
tools_config
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validated_network_policy_amendment_host_allows_normalized_match() {
|
||||
let amendment = NetworkPolicyAmendment {
|
||||
@@ -883,156 +921,93 @@ fn collect_explicit_app_ids_from_skill_items_skips_plain_mentions_with_skill_con
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_app_mcp_tools_remain_visible_without_search_selection() {
|
||||
let mcp_tools = HashMap::from([
|
||||
(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
make_mcp_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_create_event",
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
),
|
||||
),
|
||||
(
|
||||
"mcp__rmcp__echo".to_string(),
|
||||
make_mcp_tool(
|
||||
"rmcp", "echo", /*connector_id*/ None, /*connector_name*/ None,
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
let mut selected_mcp_tools = mcp_tools
|
||||
.iter()
|
||||
.filter(|(_, tool)| tool.server_name != CODEX_APPS_MCP_SERVER_NAME)
|
||||
.map(|(name, tool)| (name.clone(), tool.clone()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let connectors = connectors::accessible_connectors_from_mcp_tools(&mcp_tools);
|
||||
let explicitly_enabled_connectors = HashSet::new();
|
||||
let connectors = filter_connectors_for_input(
|
||||
&connectors,
|
||||
&[user_message("run echo")],
|
||||
&explicitly_enabled_connectors,
|
||||
&HashMap::new(),
|
||||
);
|
||||
fn mcp_tool_exposure_directly_exposes_small_effective_tool_sets() {
|
||||
let config = test_config();
|
||||
selected_mcp_tools.extend(filter_codex_apps_mcp_tools(
|
||||
&mcp_tools,
|
||||
&connectors,
|
||||
&config,
|
||||
));
|
||||
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true);
|
||||
let mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD - 1);
|
||||
|
||||
let mut tool_names: Vec<String> = selected_mcp_tools.into_keys().collect();
|
||||
tool_names.sort();
|
||||
assert_eq!(tool_names, vec!["mcp__rmcp__echo".to_string()]);
|
||||
let exposure = build_mcp_tool_exposure(
|
||||
&mcp_tools,
|
||||
/*connectors*/ None,
|
||||
&[],
|
||||
&config,
|
||||
&tools_config,
|
||||
);
|
||||
|
||||
let mut direct_tool_names: Vec<_> = exposure.direct_tools.keys().cloned().collect();
|
||||
direct_tool_names.sort();
|
||||
let mut expected_tool_names: Vec<_> = mcp_tools.keys().cloned().collect();
|
||||
expected_tool_names.sort();
|
||||
assert_eq!(direct_tool_names, expected_tool_names);
|
||||
assert!(exposure.deferred_tools.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_tool_selection_keeps_codex_apps_tools_without_mentions() {
|
||||
let selected_tool_names = [
|
||||
fn mcp_tool_exposure_searches_large_effective_tool_sets() {
|
||||
let config = test_config();
|
||||
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true);
|
||||
let mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD);
|
||||
|
||||
let exposure = build_mcp_tool_exposure(
|
||||
&mcp_tools,
|
||||
/*connectors*/ None,
|
||||
&[],
|
||||
&config,
|
||||
&tools_config,
|
||||
);
|
||||
|
||||
assert!(exposure.direct_tools.is_empty());
|
||||
let deferred_tools = exposure
|
||||
.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();
|
||||
deferred_tool_names.sort();
|
||||
let mut expected_tool_names: Vec<_> = mcp_tools.keys().cloned().collect();
|
||||
expected_tool_names.sort();
|
||||
assert_eq!(deferred_tool_names, expected_tool_names);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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(),
|
||||
"mcp__rmcp__echo".to_string(),
|
||||
];
|
||||
let mcp_tools = HashMap::from([
|
||||
(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
make_mcp_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_create_event",
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
),
|
||||
make_mcp_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_create_event",
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
),
|
||||
(
|
||||
"mcp__rmcp__echo".to_string(),
|
||||
make_mcp_tool(
|
||||
"rmcp", "echo", /*connector_id*/ None, /*connector_name*/ None,
|
||||
),
|
||||
),
|
||||
]);
|
||||
)]);
|
||||
let connectors = vec![make_connector("calendar", "Calendar")];
|
||||
|
||||
let mut selected_mcp_tools = mcp_tools
|
||||
.iter()
|
||||
.filter(|(name, _)| selected_tool_names.contains(name))
|
||||
.map(|(name, tool)| (name.clone(), tool.clone()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let connectors = connectors::accessible_connectors_from_mcp_tools(&mcp_tools);
|
||||
let explicitly_enabled_connectors = HashSet::new();
|
||||
let connectors = filter_connectors_for_input(
|
||||
&connectors,
|
||||
&[user_message("run the selected tools")],
|
||||
&explicitly_enabled_connectors,
|
||||
&HashMap::new(),
|
||||
);
|
||||
let config = test_config();
|
||||
selected_mcp_tools.extend(filter_codex_apps_mcp_tools(
|
||||
let exposure = build_mcp_tool_exposure(
|
||||
&mcp_tools,
|
||||
&connectors,
|
||||
Some(connectors.as_slice()),
|
||||
connectors.as_slice(),
|
||||
&config,
|
||||
));
|
||||
&tools_config,
|
||||
);
|
||||
|
||||
let mut tool_names: Vec<String> = selected_mcp_tools.into_keys().collect();
|
||||
let mut tool_names: Vec<String> = exposure.direct_tools.into_keys().collect();
|
||||
tool_names.sort();
|
||||
assert_eq!(
|
||||
tool_names,
|
||||
vec![
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
"mcp__rmcp__echo".to_string(),
|
||||
]
|
||||
vec!["mcp__codex_apps__calendar_create_event".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apps_mentions_add_codex_apps_tools_to_search_selected_set() {
|
||||
let selected_tool_names = ["mcp__rmcp__echo".to_string()];
|
||||
let mcp_tools = HashMap::from([
|
||||
(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
make_mcp_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_create_event",
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
),
|
||||
),
|
||||
(
|
||||
"mcp__rmcp__echo".to_string(),
|
||||
make_mcp_tool(
|
||||
"rmcp", "echo", /*connector_id*/ None, /*connector_name*/ None,
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
let mut selected_mcp_tools = mcp_tools
|
||||
.iter()
|
||||
.filter(|(name, _)| selected_tool_names.contains(name))
|
||||
.map(|(name, tool)| (name.clone(), tool.clone()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let connectors = connectors::accessible_connectors_from_mcp_tools(&mcp_tools);
|
||||
let explicitly_enabled_connectors = HashSet::new();
|
||||
let connectors = filter_connectors_for_input(
|
||||
&connectors,
|
||||
&[user_message("use $calendar and then echo the response")],
|
||||
&explicitly_enabled_connectors,
|
||||
&HashMap::new(),
|
||||
);
|
||||
let config = test_config();
|
||||
selected_mcp_tools.extend(filter_codex_apps_mcp_tools(
|
||||
&mcp_tools,
|
||||
&connectors,
|
||||
&config,
|
||||
));
|
||||
|
||||
let mut tool_names: Vec<String> = selected_mcp_tools.into_keys().collect();
|
||||
tool_names.sort();
|
||||
assert_eq!(
|
||||
tool_names,
|
||||
vec![
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
"mcp__rmcp__echo".to_string(),
|
||||
]
|
||||
exposure.deferred_tools.as_ref().map(HashMap::len),
|
||||
Some(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD)
|
||||
);
|
||||
let deferred_tools = exposure
|
||||
.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"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -5364,14 +5339,12 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
|
||||
.list_all_tools()
|
||||
.await
|
||||
};
|
||||
let app_tools = Some(tools.clone());
|
||||
let mcp_tool_router_inputs = crate::tools::router::map_mcp_tool_infos(&tools);
|
||||
let deferred_mcp_tools = Some(tools.clone());
|
||||
let router = ToolRouter::from_config(
|
||||
&turn_context.tools_config,
|
||||
crate::tools::router::ToolRouterParams {
|
||||
mcp_tools: Some(mcp_tool_router_inputs.mcp_tools),
|
||||
tool_namespaces: Some(mcp_tool_router_inputs.tool_namespaces),
|
||||
app_tools,
|
||||
deferred_mcp_tools,
|
||||
mcp_tools: Some(tools),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn_context.dynamic_tools.as_slice(),
|
||||
},
|
||||
|
||||
@@ -110,8 +110,8 @@ fn codex_app_tool(
|
||||
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
tool_name: tool_name.to_string(),
|
||||
tool_namespace,
|
||||
callable_name: tool_name.to_string(),
|
||||
callable_namespace: tool_namespace,
|
||||
server_instructions: None,
|
||||
tool: test_tool_definition(tool_name),
|
||||
connector_id: Some(connector_id.to_string()),
|
||||
@@ -189,8 +189,8 @@ fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() {
|
||||
"mcp__sample__echo".to_string(),
|
||||
ToolInfo {
|
||||
server_name: "sample".to_string(),
|
||||
tool_name: "echo".to_string(),
|
||||
tool_namespace: "sample".to_string(),
|
||||
callable_name: "echo".to_string(),
|
||||
callable_namespace: "sample".to_string(),
|
||||
server_instructions: None,
|
||||
tool: test_tool_definition("echo"),
|
||||
connector_id: None,
|
||||
@@ -314,8 +314,8 @@ fn accessible_connectors_from_mcp_tools_preserves_description() {
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
tool_name: "calendar_create_event".to_string(),
|
||||
tool_namespace: "mcp__codex_apps__calendar".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(),
|
||||
|
||||
@@ -46,6 +46,7 @@ pub use landlock::spawn_command_under_linux_sandbox;
|
||||
pub(crate) mod mcp;
|
||||
mod mcp_skill_dependencies;
|
||||
mod mcp_tool_approval_templates;
|
||||
mod mcp_tool_exposure;
|
||||
mod network_policy_decision;
|
||||
pub(crate) mod network_proxy_loader;
|
||||
pub use mcp::McpManager;
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
use std::collections::HashMap;
|
||||
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::ToolsConfig;
|
||||
|
||||
use crate::config::Config;
|
||||
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) fn build_mcp_tool_exposure(
|
||||
all_mcp_tools: &HashMap<String, McpToolInfo>,
|
||||
connectors: Option<&[connectors::AppInfo]>,
|
||||
explicitly_enabled_connectors: &[connectors::AppInfo],
|
||||
config: &Config,
|
||||
tools_config: &ToolsConfig,
|
||||
) -> McpToolExposure {
|
||||
let mut deferred_tools = filter_non_codex_apps_mcp_tools_only(all_mcp_tools);
|
||||
if let Some(connectors) = connectors {
|
||||
deferred_tools.extend(filter_codex_apps_mcp_tools(
|
||||
all_mcp_tools,
|
||||
connectors,
|
||||
config,
|
||||
));
|
||||
}
|
||||
|
||||
if !tools_config.search_tool || deferred_tools.len() < DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD {
|
||||
return McpToolExposure {
|
||||
direct_tools: deferred_tools,
|
||||
deferred_tools: None,
|
||||
};
|
||||
}
|
||||
|
||||
let direct_tools =
|
||||
filter_codex_apps_mcp_tools(all_mcp_tools, explicitly_enabled_connectors, config);
|
||||
McpToolExposure {
|
||||
direct_tools,
|
||||
deferred_tools: Some(deferred_tools),
|
||||
}
|
||||
}
|
||||
|
||||
fn filter_codex_apps_mcp_tools(
|
||||
mcp_tools: &HashMap<String, McpToolInfo>,
|
||||
connectors: &[connectors::AppInfo],
|
||||
config: &Config,
|
||||
) -> HashMap<String, McpToolInfo> {
|
||||
let allowed: HashSet<&str> = connectors
|
||||
.iter()
|
||||
.map(|connector| connector.id.as_str())
|
||||
.collect();
|
||||
|
||||
mcp_tools
|
||||
.iter()
|
||||
.filter(|(_, tool)| {
|
||||
if tool.server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return false;
|
||||
}
|
||||
let Some(connector_id) = tool.connector_id.as_deref() else {
|
||||
return false;
|
||||
};
|
||||
allowed.contains(connector_id) && connectors::codex_app_tool_is_enabled(config, tool)
|
||||
})
|
||||
.map(|(name, tool)| (name.clone(), tool.clone()))
|
||||
.collect()
|
||||
}
|
||||
@@ -258,14 +258,12 @@ async fn build_nested_router(exec: &ExecContext) -> ToolRouter {
|
||||
.await
|
||||
.list_all_tools()
|
||||
.await;
|
||||
let mcp_tool_router_inputs = crate::tools::router::map_mcp_tool_infos(&mcp_tools);
|
||||
|
||||
ToolRouter::from_config(
|
||||
&nested_tools_config,
|
||||
ToolRouterParams {
|
||||
mcp_tools: Some(mcp_tool_router_inputs.mcp_tools),
|
||||
tool_namespaces: Some(mcp_tool_router_inputs.tool_namespaces),
|
||||
app_tools: None,
|
||||
deferred_mcp_tools: None,
|
||||
mcp_tools: Some(mcp_tools),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: exec.turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
|
||||
@@ -6,21 +6,36 @@ use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use bm25::Document;
|
||||
use bm25::Language;
|
||||
use bm25::SearchEngine;
|
||||
use bm25::SearchEngineBuilder;
|
||||
use codex_mcp::ToolInfo;
|
||||
use codex_tools::TOOL_SEARCH_DEFAULT_LIMIT;
|
||||
use codex_tools::TOOL_SEARCH_TOOL_NAME;
|
||||
use codex_tools::ToolSearchResultSource;
|
||||
use codex_tools::collect_tool_search_output_tools;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct ToolSearchHandler {
|
||||
tools: HashMap<String, ToolInfo>,
|
||||
entries: Vec<(String, ToolInfo)>,
|
||||
search_engine: SearchEngine<usize>,
|
||||
}
|
||||
|
||||
impl ToolSearchHandler {
|
||||
pub fn new(tools: HashMap<String, ToolInfo>) -> Self {
|
||||
Self { tools }
|
||||
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));
|
||||
|
||||
let documents: Vec<Document<usize>> = entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, (name, info))| Document::new(idx, build_search_text(name, info)))
|
||||
.collect();
|
||||
let search_engine =
|
||||
SearchEngineBuilder::<usize>::with_documents(Language::English, documents).build();
|
||||
|
||||
Self {
|
||||
entries,
|
||||
search_engine,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,29 +75,20 @@ impl ToolHandler for ToolSearchHandler {
|
||||
));
|
||||
}
|
||||
|
||||
let mut entries: Vec<(String, ToolInfo)> = self.tools.clone().into_iter().collect();
|
||||
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
if entries.is_empty() {
|
||||
if self.entries.is_empty() {
|
||||
return Ok(ToolSearchOutput { tools: Vec::new() });
|
||||
}
|
||||
|
||||
let documents: Vec<Document<usize>> = entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, (name, info))| Document::new(idx, build_search_text(name, info)))
|
||||
.collect();
|
||||
let search_engine =
|
||||
SearchEngineBuilder::<usize>::with_documents(Language::English, documents).build();
|
||||
let results = search_engine.search(query, limit);
|
||||
let results = self.search_engine.search(query, limit);
|
||||
|
||||
let tools = collect_tool_search_output_tools(
|
||||
results
|
||||
.into_iter()
|
||||
.filter_map(|result| entries.get(result.document.id))
|
||||
.map(|(_name, tool)| ToolSearchResultSource {
|
||||
tool_namespace: tool.tool_namespace.as_str(),
|
||||
tool_name: tool.tool_name.as_str(),
|
||||
.filter_map(|result| self.entries.get(result.document.id))
|
||||
.map(|(_, tool)| ToolSearchResultSource {
|
||||
server_name: tool.server_name.as_str(),
|
||||
tool_namespace: tool.callable_namespace.as_str(),
|
||||
tool_name: tool.callable_name.as_str(),
|
||||
tool: &tool.tool,
|
||||
connector_name: tool.connector_name.as_deref(),
|
||||
connector_description: tool.connector_description.as_deref(),
|
||||
@@ -101,7 +107,8 @@ impl ToolHandler for ToolSearchHandler {
|
||||
fn build_search_text(name: &str, info: &ToolInfo) -> String {
|
||||
let mut parts = vec![
|
||||
name.to_string(),
|
||||
info.tool_name.clone(),
|
||||
info.callable_name.clone(),
|
||||
info.tool.name.to_string(),
|
||||
info.server_name.clone(),
|
||||
];
|
||||
|
||||
@@ -129,6 +136,15 @@ fn build_search_text(name: &str, info: &ToolInfo) -> String {
|
||||
parts.push(connector_description.to_string());
|
||||
}
|
||||
|
||||
parts.extend(
|
||||
info.plugin_display_names
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty())
|
||||
.map(str::to_string),
|
||||
);
|
||||
|
||||
parts.extend(
|
||||
info.tool
|
||||
.input_schema
|
||||
|
||||
@@ -1561,14 +1561,12 @@ impl JsReplManager {
|
||||
.await
|
||||
.list_all_tools()
|
||||
.await;
|
||||
let mcp_tool_router_inputs = crate::tools::router::map_mcp_tool_infos(&mcp_tools);
|
||||
|
||||
let router = ToolRouter::from_config(
|
||||
&exec.turn.tools_config,
|
||||
crate::tools::router::ToolRouterParams {
|
||||
mcp_tools: Some(mcp_tool_router_inputs.mcp_tools),
|
||||
tool_namespaces: Some(mcp_tool_router_inputs.tool_namespaces),
|
||||
app_tools: None,
|
||||
deferred_mcp_tools: None,
|
||||
mcp_tools: Some(mcp_tools),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: exec.turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
|
||||
@@ -16,10 +16,8 @@ use codex_protocol::models::SearchToolCallParams;
|
||||
use codex_protocol::models::ShellToolCallParams;
|
||||
use codex_tools::ConfiguredToolSpec;
|
||||
use codex_tools::DiscoverableTool;
|
||||
use codex_tools::ToolNamespace;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_tools::ToolsConfig;
|
||||
use rmcp::model::Tool;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tracing::instrument;
|
||||
@@ -41,53 +39,24 @@ pub struct ToolRouter {
|
||||
}
|
||||
|
||||
pub(crate) struct ToolRouterParams<'a> {
|
||||
pub(crate) mcp_tools: Option<HashMap<String, Tool>>,
|
||||
pub(crate) tool_namespaces: Option<HashMap<String, ToolNamespace>>,
|
||||
pub(crate) app_tools: Option<HashMap<String, ToolInfo>>,
|
||||
pub(crate) mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
pub(crate) deferred_mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
pub(crate) discoverable_tools: Option<Vec<DiscoverableTool>>,
|
||||
pub(crate) dynamic_tools: &'a [DynamicToolSpec],
|
||||
}
|
||||
|
||||
pub(crate) struct McpToolRouterInputs {
|
||||
pub(crate) mcp_tools: HashMap<String, Tool>,
|
||||
pub(crate) tool_namespaces: HashMap<String, ToolNamespace>,
|
||||
}
|
||||
|
||||
pub(crate) fn map_mcp_tool_infos(mcp_tools: &HashMap<String, ToolInfo>) -> McpToolRouterInputs {
|
||||
McpToolRouterInputs {
|
||||
mcp_tools: mcp_tools
|
||||
.iter()
|
||||
.map(|(name, tool)| (name.clone(), tool.tool.clone()))
|
||||
.collect(),
|
||||
tool_namespaces: mcp_tools
|
||||
.iter()
|
||||
.map(|(name, tool)| {
|
||||
(
|
||||
name.clone(),
|
||||
ToolNamespace {
|
||||
name: tool.tool_namespace.clone(),
|
||||
description: tool.server_instructions.clone(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolRouter {
|
||||
pub fn from_config(config: &ToolsConfig, params: ToolRouterParams<'_>) -> Self {
|
||||
let ToolRouterParams {
|
||||
mcp_tools,
|
||||
tool_namespaces,
|
||||
app_tools,
|
||||
deferred_mcp_tools,
|
||||
discoverable_tools,
|
||||
dynamic_tools,
|
||||
} = params;
|
||||
let builder = build_specs_with_discoverable_tools(
|
||||
config,
|
||||
mcp_tools,
|
||||
app_tools,
|
||||
tool_namespaces,
|
||||
deferred_mcp_tools,
|
||||
discoverable_tools,
|
||||
dynamic_tools,
|
||||
);
|
||||
|
||||
@@ -25,18 +25,12 @@ async fn js_repl_tools_only_blocks_direct_tool_calls() -> anyhow::Result<()> {
|
||||
.await
|
||||
.list_all_tools()
|
||||
.await;
|
||||
let app_tools = Some(mcp_tools.clone());
|
||||
let deferred_mcp_tools = Some(mcp_tools.clone());
|
||||
let router = ToolRouter::from_config(
|
||||
&turn.tools_config,
|
||||
ToolRouterParams {
|
||||
mcp_tools: Some(
|
||||
mcp_tools
|
||||
.into_iter()
|
||||
.map(|(name, tool)| (name, tool.tool))
|
||||
.collect(),
|
||||
),
|
||||
tool_namespaces: None,
|
||||
app_tools,
|
||||
deferred_mcp_tools,
|
||||
mcp_tools: Some(mcp_tools),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
@@ -84,18 +78,12 @@ async fn js_repl_tools_only_allows_js_repl_source_calls() -> anyhow::Result<()>
|
||||
.await
|
||||
.list_all_tools()
|
||||
.await;
|
||||
let app_tools = Some(mcp_tools.clone());
|
||||
let deferred_mcp_tools = Some(mcp_tools.clone());
|
||||
let router = ToolRouter::from_config(
|
||||
&turn.tools_config,
|
||||
ToolRouterParams {
|
||||
mcp_tools: Some(
|
||||
mcp_tools
|
||||
.into_iter()
|
||||
.map(|(name, tool)| (name, tool.tool))
|
||||
.collect(),
|
||||
),
|
||||
tool_namespaces: None,
|
||||
app_tools,
|
||||
deferred_mcp_tools,
|
||||
mcp_tools: Some(mcp_tools),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
|
||||
@@ -5,13 +5,12 @@ use crate::tools::handlers::multi_agents_common::DEFAULT_WAIT_TIMEOUT_MS;
|
||||
use crate::tools::handlers::multi_agents_common::MAX_WAIT_TIMEOUT_MS;
|
||||
use crate::tools::handlers::multi_agents_common::MIN_WAIT_TIMEOUT_MS;
|
||||
use crate::tools::registry::ToolRegistryBuilder;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::ToolInfo;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_tools::DiscoverableTool;
|
||||
use codex_tools::ToolHandlerKind;
|
||||
use codex_tools::ToolNamespace;
|
||||
use codex_tools::ToolRegistryPlanAppTool;
|
||||
use codex_tools::ToolRegistryPlanDeferredTool;
|
||||
use codex_tools::ToolRegistryPlanParams;
|
||||
use codex_tools::ToolUserShellType;
|
||||
use codex_tools::ToolsConfig;
|
||||
@@ -30,11 +29,36 @@ 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>,
|
||||
}
|
||||
|
||||
fn map_mcp_tools_for_plan(mcp_tools: &HashMap<String, ToolInfo>) -> McpToolPlanInputs {
|
||||
McpToolPlanInputs {
|
||||
mcp_tools: mcp_tools
|
||||
.iter()
|
||||
.map(|(name, tool)| (name.clone(), tool.tool.clone()))
|
||||
.collect(),
|
||||
tool_namespaces: mcp_tools
|
||||
.iter()
|
||||
.map(|(name, tool)| {
|
||||
(
|
||||
name.clone(),
|
||||
ToolNamespace {
|
||||
name: tool.callable_namespace.clone(),
|
||||
description: tool.server_instructions.clone(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_specs_with_discoverable_tools(
|
||||
config: &ToolsConfig,
|
||||
mcp_tools: Option<HashMap<String, rmcp::model::Tool>>,
|
||||
app_tools: Option<HashMap<String, ToolInfo>>,
|
||||
tool_namespaces: Option<HashMap<String, ToolNamespace>>,
|
||||
mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
deferred_mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
discoverable_tools: Option<Vec<DiscoverableTool>>,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> ToolRegistryBuilder {
|
||||
@@ -70,12 +94,13 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
use crate::tools::handlers::multi_agents_v2::WaitAgentHandler as WaitAgentHandlerV2;
|
||||
|
||||
let mut builder = ToolRegistryBuilder::new();
|
||||
let app_tool_sources = app_tools.as_ref().map(|app_tools| {
|
||||
app_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| ToolRegistryPlanAppTool {
|
||||
tool_name: tool.tool_name.as_str(),
|
||||
tool_namespace: tool.tool_namespace.as_str(),
|
||||
.map(|tool| ToolRegistryPlanDeferredTool {
|
||||
tool_name: tool.callable_name.as_str(),
|
||||
tool_namespace: tool.callable_namespace.as_str(),
|
||||
server_name: tool.server_name.as_str(),
|
||||
connector_name: tool.connector_name.as_deref(),
|
||||
connector_description: tool.connector_description.as_deref(),
|
||||
@@ -87,9 +112,13 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
let plan = build_tool_registry_plan(
|
||||
config,
|
||||
ToolRegistryPlanParams {
|
||||
mcp_tools: mcp_tools.as_ref(),
|
||||
tool_namespaces: tool_namespaces.as_ref(),
|
||||
app_tools: app_tool_sources.as_deref(),
|
||||
mcp_tools: mcp_tool_plan_inputs
|
||||
.as_ref()
|
||||
.map(|inputs| &inputs.mcp_tools),
|
||||
deferred_mcp_tools: deferred_mcp_tool_sources.as_deref(),
|
||||
tool_namespaces: mcp_tool_plan_inputs
|
||||
.as_ref()
|
||||
.map(|inputs| &inputs.tool_namespaces),
|
||||
discoverable_tools: discoverable_tools.as_deref(),
|
||||
dynamic_tools,
|
||||
default_agent_type_description: &default_agent_type_description,
|
||||
@@ -98,7 +127,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
min_timeout_ms: MIN_WAIT_TIMEOUT_MS,
|
||||
max_timeout_ms: MAX_WAIT_TIMEOUT_MS,
|
||||
},
|
||||
codex_apps_mcp_server_name: CODEX_APPS_MCP_SERVER_NAME,
|
||||
},
|
||||
);
|
||||
let shell_handler = Arc::new(ShellHandler);
|
||||
@@ -210,9 +238,9 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
}
|
||||
ToolHandlerKind::ToolSearch => {
|
||||
if tool_search_handler.is_none() {
|
||||
tool_search_handler = app_tools
|
||||
tool_search_handler = deferred_mcp_tools
|
||||
.as_ref()
|
||||
.map(|app_tools| Arc::new(ToolSearchHandler::new(app_tools.clone())));
|
||||
.map(|tools| Arc::new(ToolSearchHandler::new(tools.clone())));
|
||||
}
|
||||
if let Some(tool_search_handler) = tool_search_handler.as_ref() {
|
||||
builder.register_handler(handler.name, tool_search_handler.clone());
|
||||
|
||||
@@ -53,6 +53,20 @@ fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> r
|
||||
}
|
||||
}
|
||||
|
||||
fn mcp_tool_info(tool: rmcp::model::Tool) -> ToolInfo {
|
||||
ToolInfo {
|
||||
server_name: "test_server".to_string(),
|
||||
callable_name: tool.name.to_string(),
|
||||
callable_namespace: "mcp__test_server__".to_string(),
|
||||
server_instructions: None,
|
||||
tool,
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
plugin_display_names: Vec::new(),
|
||||
connector_description: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn discoverable_connector(id: &str, name: &str, description: &str) -> DiscoverableTool {
|
||||
let slug = name.replace(' ', "-").to_lowercase();
|
||||
DiscoverableTool::Connector(Box::new(AppInfo {
|
||||
@@ -182,7 +196,7 @@ fn multi_agent_v2_spawn_agent_description(tools_config: &ToolsConfig) -> String
|
||||
let (tools, _) = build_specs(
|
||||
tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
/*app_tools*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
)
|
||||
.build();
|
||||
@@ -208,15 +222,14 @@ 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, rmcp::model::Tool>>,
|
||||
app_tools: Option<HashMap<String, ToolInfo>>,
|
||||
mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
deferred_mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> ToolRegistryBuilder {
|
||||
build_specs_with_discoverable_tools(
|
||||
config,
|
||||
mcp_tools,
|
||||
app_tools,
|
||||
/*tool_namespaces*/ None,
|
||||
deferred_mcp_tools,
|
||||
/*discoverable_tools*/ None,
|
||||
dynamic_tools,
|
||||
)
|
||||
@@ -267,7 +280,7 @@ fn get_memory_requires_feature_flag() {
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
/*app_tools*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
)
|
||||
.build();
|
||||
@@ -300,8 +313,7 @@ fn assert_model_tools(
|
||||
&tools_config,
|
||||
ToolRouterParams {
|
||||
mcp_tools: None,
|
||||
tool_namespaces: None,
|
||||
app_tools: None,
|
||||
deferred_mcp_tools: None,
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: &[],
|
||||
},
|
||||
@@ -562,7 +574,7 @@ fn test_build_specs_default_shell_present() {
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::new()),
|
||||
/*app_tools*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
)
|
||||
.build();
|
||||
@@ -708,8 +720,7 @@ fn tool_suggest_requires_apps_and_plugins_features() {
|
||||
let (tools, _) = build_specs_with_discoverable_tools(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
/*app_tools*/ None,
|
||||
/*tool_namespaces*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
discoverable_tools.clone(),
|
||||
&[],
|
||||
)
|
||||
@@ -725,7 +736,7 @@ fn tool_suggest_requires_apps_and_plugins_features() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_tool_description_handles_no_enabled_apps() {
|
||||
fn search_tool_description_handles_no_enabled_mcp_tools() {
|
||||
let model_info = search_capable_model_info();
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::Apps);
|
||||
@@ -755,7 +766,7 @@ fn search_tool_description_handles_no_enabled_apps() {
|
||||
};
|
||||
|
||||
assert!(description.contains("None currently enabled."));
|
||||
assert!(!description.contains("{{app_descriptions}}"));
|
||||
assert!(!description.contains("{{source_descriptions}}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -783,8 +794,8 @@ fn search_tool_description_falls_back_to_connector_name_without_description() {
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
tool_name: "_create_event".to_string(),
|
||||
tool_namespace: "mcp__codex_apps__calendar".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",
|
||||
@@ -810,7 +821,7 @@ fn search_tool_description_falls_back_to_connector_name_without_description() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_tool_registers_namespaced_app_tool_aliases() {
|
||||
fn search_tool_registers_namespaced_mcp_tool_aliases() {
|
||||
let model_info = search_capable_model_info();
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::Apps);
|
||||
@@ -835,8 +846,8 @@ fn search_tool_registers_namespaced_app_tool_aliases() {
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
tool_name: "_create_event".to_string(),
|
||||
tool_namespace: "mcp__codex_apps__calendar".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",
|
||||
@@ -853,8 +864,8 @@ fn search_tool_registers_namespaced_app_tool_aliases() {
|
||||
"mcp__codex_apps__calendar_list_events".to_string(),
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
tool_name: "_list_events".to_string(),
|
||||
tool_namespace: "mcp__codex_apps__calendar".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",
|
||||
@@ -867,15 +878,31 @@ fn search_tool_registers_namespaced_app_tool_aliases() {
|
||||
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(),
|
||||
},
|
||||
),
|
||||
])),
|
||||
&[],
|
||||
)
|
||||
.build();
|
||||
|
||||
let alias = tool_handler_key("_create_event", Some("mcp__codex_apps__calendar"));
|
||||
let app_alias = tool_handler_key("_create_event", Some("mcp__codex_apps__calendar"));
|
||||
let mcp_alias = tool_handler_key("echo", Some("mcp__rmcp__"));
|
||||
|
||||
assert!(registry.has_handler(TOOL_SEARCH_TOOL_NAME, /*namespace*/ None));
|
||||
assert!(registry.has_handler(alias.as_str(), /*namespace*/ None));
|
||||
assert!(registry.has_handler(app_alias.as_str(), /*namespace*/ None));
|
||||
assert!(registry.has_handler(mcp_alias.as_str(), /*namespace*/ None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -900,7 +927,7 @@ fn test_mcp_tool_property_missing_type_defaults_to_string() {
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"dash/search".to_string(),
|
||||
mcp_tool(
|
||||
mcp_tool_info(mcp_tool(
|
||||
"search",
|
||||
"Search docs",
|
||||
serde_json::json!({
|
||||
@@ -909,9 +936,9 @@ fn test_mcp_tool_property_missing_type_defaults_to_string() {
|
||||
"query": {"description": "search query"}
|
||||
}
|
||||
}),
|
||||
),
|
||||
)),
|
||||
)])),
|
||||
/*app_tools*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
)
|
||||
.build();
|
||||
@@ -960,16 +987,16 @@ fn test_mcp_tool_preserves_integer_schema() {
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"dash/paginate".to_string(),
|
||||
mcp_tool(
|
||||
mcp_tool_info(mcp_tool(
|
||||
"paginate",
|
||||
"Pagination",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {"page": {"type": "integer"}}
|
||||
}),
|
||||
),
|
||||
)),
|
||||
)])),
|
||||
/*app_tools*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
)
|
||||
.build();
|
||||
@@ -1019,16 +1046,16 @@ fn test_mcp_tool_array_without_items_gets_default_string_items() {
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"dash/tags".to_string(),
|
||||
mcp_tool(
|
||||
mcp_tool_info(mcp_tool(
|
||||
"tags",
|
||||
"Tags",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {"tags": {"type": "array"}}
|
||||
}),
|
||||
),
|
||||
)),
|
||||
)])),
|
||||
/*app_tools*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
)
|
||||
.build();
|
||||
@@ -1080,7 +1107,7 @@ fn test_mcp_tool_anyof_defaults_to_string() {
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"dash/value".to_string(),
|
||||
mcp_tool(
|
||||
mcp_tool_info(mcp_tool(
|
||||
"value",
|
||||
"AnyOf Value",
|
||||
serde_json::json!({
|
||||
@@ -1089,9 +1116,9 @@ fn test_mcp_tool_anyof_defaults_to_string() {
|
||||
"value": {"anyOf": [{"type": "string"}, {"type": "number"}]}
|
||||
}
|
||||
}),
|
||||
),
|
||||
)),
|
||||
)])),
|
||||
/*app_tools*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
)
|
||||
.build();
|
||||
@@ -1145,7 +1172,7 @@ fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() {
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"test_server/do_something_cool".to_string(),
|
||||
mcp_tool(
|
||||
mcp_tool_info(mcp_tool(
|
||||
"do_something_cool",
|
||||
"Do something cool",
|
||||
serde_json::json!({
|
||||
@@ -1171,9 +1198,9 @@ fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() {
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
)),
|
||||
)])),
|
||||
/*app_tools*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
)
|
||||
.build();
|
||||
|
||||
Reference in New Issue
Block a user