mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
Allow selective MCP tool preloading
This commit is contained in:
@@ -23,6 +23,7 @@ use crate::types::PluginConfig;
|
||||
use crate::types::SandboxWorkspaceWrite;
|
||||
use crate::types::ShellEnvironmentPolicyToml;
|
||||
use crate::types::SkillsConfig;
|
||||
use crate::types::ToolSearchConfig;
|
||||
use crate::types::ToolSuggestConfig;
|
||||
use crate::types::Tui;
|
||||
use crate::types::UriBasedFileOpener;
|
||||
@@ -436,6 +437,9 @@ pub struct ConfigToml {
|
||||
/// Additional discoverable tools that can be suggested for installation.
|
||||
pub tool_suggest: Option<ToolSuggestConfig>,
|
||||
|
||||
/// MCP tools to expose directly instead of discovering through tool_search.
|
||||
pub tool_search: Option<ToolSearchConfig>,
|
||||
|
||||
/// Agent-related settings (thread limits, etc.).
|
||||
pub agents: Option<AgentsToml>,
|
||||
|
||||
|
||||
@@ -276,6 +276,14 @@ pub struct ToolSuggestConfig {
|
||||
pub disabled_tools: Vec<ToolSuggestDisabledTool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ToolSearchConfig {
|
||||
/// Raw MCP tool names to expose directly while leaving other MCP tools deferred.
|
||||
#[serde(default)]
|
||||
pub preloaded_tools: Vec<String>,
|
||||
}
|
||||
|
||||
/// Memories settings loaded from config.toml.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
|
||||
@@ -2884,6 +2884,20 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ToolSearchConfig": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"preloaded_tools": {
|
||||
"default": [],
|
||||
"description": "Raw MCP tool names to expose directly while leaving other MCP tools deferred.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ToolSuggestConfig": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -5436,6 +5450,14 @@
|
||||
"minimum": 0.0,
|
||||
"type": "integer"
|
||||
},
|
||||
"tool_search": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolSearchConfig"
|
||||
}
|
||||
],
|
||||
"description": "MCP tools to expose directly instead of discovering through tool_search."
|
||||
},
|
||||
"tool_suggest": {
|
||||
"allOf": [
|
||||
{
|
||||
|
||||
@@ -46,6 +46,7 @@ use codex_config::types::ModelAvailabilityNuxConfig;
|
||||
use codex_config::types::Notice;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_config::types::SessionPickerViewMode;
|
||||
use codex_config::types::ToolSearchConfig;
|
||||
use codex_config::types::ToolSuggestConfig;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use codex_config::types::ToolSuggestDiscoverable;
|
||||
@@ -1073,6 +1074,9 @@ pub struct Config {
|
||||
/// Configured discoverable tools for tool suggestions.
|
||||
pub tool_suggest: ToolSuggestConfig,
|
||||
|
||||
/// MCP tools to expose directly instead of discovering through tool_search.
|
||||
pub tool_search: ToolSearchConfig,
|
||||
|
||||
/// OTEL configuration (exporter type, endpoint, headers, etc.).
|
||||
pub otel: codex_config::types::OtelConfig,
|
||||
}
|
||||
@@ -3015,6 +3019,7 @@ impl Config {
|
||||
}
|
||||
|
||||
let tool_suggest = resolve_tool_suggest_config(&cfg, &config_layer_stack);
|
||||
let tool_search = cfg.tool_search.clone().unwrap_or_default();
|
||||
let feature_overrides = FeatureOverrides {
|
||||
web_search_request: override_tools_web_search_request,
|
||||
};
|
||||
@@ -3926,6 +3931,7 @@ impl Config {
|
||||
.and_then(|feedback| feedback.enabled)
|
||||
.unwrap_or(true),
|
||||
tool_suggest,
|
||||
tool_search,
|
||||
tui_notifications: cfg
|
||||
.tui
|
||||
.as_ref()
|
||||
|
||||
@@ -22,9 +22,9 @@ pub(crate) fn build_mcp_tool_exposure(
|
||||
config: &Config,
|
||||
search_tool_enabled: bool,
|
||||
) -> McpToolExposure {
|
||||
let mut deferred_tools = filter_non_codex_apps_mcp_tools_only(all_mcp_tools);
|
||||
let mut eligible_tools = filter_non_codex_apps_mcp_tools_only(all_mcp_tools);
|
||||
if let Some(connectors) = connectors {
|
||||
deferred_tools.extend(filter_codex_apps_mcp_tools(
|
||||
eligible_tools.extend(filter_codex_apps_mcp_tools(
|
||||
all_mcp_tools,
|
||||
connectors,
|
||||
config,
|
||||
@@ -33,13 +33,21 @@ pub(crate) fn build_mcp_tool_exposure(
|
||||
|
||||
if !search_tool_enabled {
|
||||
return McpToolExposure {
|
||||
direct_tools: deferred_tools,
|
||||
direct_tools: eligible_tools,
|
||||
deferred_tools: None,
|
||||
};
|
||||
}
|
||||
|
||||
let (direct_tools, deferred_tools) = eligible_tools.into_iter().partition(|tool| {
|
||||
config
|
||||
.tool_search
|
||||
.preloaded_tools
|
||||
.iter()
|
||||
.any(|name| name == tool.tool.name.as_ref())
|
||||
});
|
||||
|
||||
McpToolExposure {
|
||||
direct_tools: Vec::new(),
|
||||
direct_tools,
|
||||
deferred_tools: (!deferred_tools.is_empty()).then_some(deferred_tools),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,3 +294,43 @@ async fn defers_apps_and_non_app_mcp_tools() {
|
||||
"_create_event"
|
||||
)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn preloaded_tools_stay_direct_when_other_tools_are_deferred() {
|
||||
let mut config = test_config().await;
|
||||
config.tool_search.preloaded_tools = vec!["calendar_create_event".to_string()];
|
||||
let deferred_tool = make_mcp_tool(
|
||||
"rmcp",
|
||||
"tool",
|
||||
"mcp__rmcp",
|
||||
"tool",
|
||||
/*connector_id*/ None,
|
||||
/*connector_name*/ None,
|
||||
);
|
||||
let preloaded_tool = make_mcp_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_create_event",
|
||||
"mcp__codex_apps__calendar",
|
||||
"_create_event",
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
);
|
||||
let connectors = vec![make_connector("calendar", "Calendar")];
|
||||
|
||||
let exposure = build_mcp_tool_exposure(
|
||||
&[deferred_tool.clone(), preloaded_tool.clone()],
|
||||
Some(connectors.as_slice()),
|
||||
&config,
|
||||
/*search_tool_enabled*/ true,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
tool_names(&exposure.direct_tools),
|
||||
tool_names(&[preloaded_tool])
|
||||
);
|
||||
let deferred_tools = exposure
|
||||
.deferred_tools
|
||||
.as_ref()
|
||||
.expect("non-preloaded MCP tools should remain deferred");
|
||||
assert_eq!(tool_names(deferred_tools), tool_names(&[deferred_tool]));
|
||||
}
|
||||
|
||||
@@ -217,6 +217,58 @@ async fn small_app_tool_sets_are_deferred_by_default() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn preloaded_app_tool_stays_direct_when_other_tools_are_deferred() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let apps_server = AppsTestServer::mount(&server).await?;
|
||||
let mock = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder =
|
||||
configured_builder(apps_server.chatgpt_base_url.clone()).with_config(|config| {
|
||||
config.tool_search.preloaded_tools = vec!["calendar_create_event".to_string()];
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn_with_approval_and_permission_profile(
|
||||
"list tools",
|
||||
AskForApproval::Never,
|
||||
PermissionProfile::Disabled,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let body = mock.single_request().body_json();
|
||||
let tools = tool_names(&body);
|
||||
assert!(
|
||||
tools.iter().any(|name| name == TOOL_SEARCH_TOOL_NAME),
|
||||
"tool_search should remain available for deferred tools: {tools:?}"
|
||||
);
|
||||
assert!(
|
||||
namespace_child_tool(
|
||||
&body,
|
||||
SEARCH_CALENDAR_NAMESPACE,
|
||||
SEARCH_CALENDAR_CREATE_TOOL,
|
||||
)
|
||||
.is_some(),
|
||||
"preloaded app tool should be directly exposed: {tools:?}"
|
||||
);
|
||||
assert!(
|
||||
namespace_child_tool(&body, SEARCH_CALENDAR_NAMESPACE, SEARCH_CALENDAR_LIST_TOOL).is_none(),
|
||||
"non-preloaded app tools should remain deferred: {tools:?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn app_only_tools_are_not_visible_or_runnable_by_direct_model_calls() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
@@ -292,6 +292,7 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
|
||||
analytics_enabled: Some(false),
|
||||
feedback_enabled: false,
|
||||
tool_suggest: ToolSuggestConfig::default(),
|
||||
tool_search: Default::default(),
|
||||
otel: OtelConfig::default(),
|
||||
};
|
||||
config
|
||||
|
||||
Reference in New Issue
Block a user