mirror of
https://github.com/openai/codex.git
synced 2026-05-22 20:14:17 +00:00
Compare commits
1 Commits
pr23943
...
dev/sayan/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc961b75f1 |
@@ -108,6 +108,15 @@ See `codex-rs/tui/styles.md`.
|
||||
|
||||
## Tests
|
||||
|
||||
When reviewing or adding tests:
|
||||
|
||||
- Ask whether the test makes sense.
|
||||
- Ask whether it actually asserts something valuable that you care about.
|
||||
- Ask how much the test would need to change if the surrounding system were refactored.
|
||||
- Ask whether you or someone else would notice the regression if Codex auto-updated the test.
|
||||
|
||||
If a unit test mostly covers deep implementation details, static data, removed behavior, or requires heavy monkeypatching, prefer deleting it and writing an integration test that protects meaningful end-to-end behavior instead.
|
||||
|
||||
### Snapshot tests
|
||||
|
||||
This repo uses snapshot tests (via `insta`), especially in `codex-rs/tui`, to validate rendered output.
|
||||
|
||||
@@ -21,7 +21,7 @@ use serde::Serialize;
|
||||
use sha1::Digest;
|
||||
use sha1::Sha1;
|
||||
|
||||
pub(crate) const CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION: u8 = 2;
|
||||
pub(crate) const CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION: u8 = 3;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CodexAppsToolsCacheKey {
|
||||
@@ -115,7 +115,7 @@ pub(crate) fn normalize_codex_apps_callable_name(
|
||||
&& let Some(stripped) = tool_name.strip_prefix(&connector_name)
|
||||
&& !stripped.is_empty()
|
||||
{
|
||||
return stripped.to_string();
|
||||
return stripped.strip_prefix('_').unwrap_or(stripped).to_string();
|
||||
}
|
||||
|
||||
if let Some(connector_id) = connector_id
|
||||
@@ -125,7 +125,7 @@ pub(crate) fn normalize_codex_apps_callable_name(
|
||||
&& let Some(stripped) = tool_name.strip_prefix(&connector_id)
|
||||
&& !stripped.is_empty()
|
||||
{
|
||||
return stripped.to_string();
|
||||
return stripped.strip_prefix('_').unwrap_or(stripped).to_string();
|
||||
}
|
||||
|
||||
tool_name
|
||||
@@ -138,9 +138,9 @@ pub(crate) fn normalize_codex_apps_callable_namespace(
|
||||
if server_name == CODEX_APPS_MCP_SERVER_NAME
|
||||
&& let Some(connector_name) = connector_name
|
||||
{
|
||||
format!("mcp__{}__{}", server_name, sanitize_name(connector_name))
|
||||
format!("{}__{}", server_name, sanitize_name(connector_name))
|
||||
} else {
|
||||
format!("mcp__{server_name}__")
|
||||
server_name.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ use std::sync::Arc;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo {
|
||||
let tool_namespace = format!("mcp__{server_name}__");
|
||||
let tool_namespace = server_name.to_string();
|
||||
ToolInfo {
|
||||
server_name: server_name.to_string(),
|
||||
callable_name: tool_name.to_string(),
|
||||
@@ -71,6 +71,18 @@ fn create_test_tool_with_connector(
|
||||
tool
|
||||
}
|
||||
|
||||
fn create_codex_apps_test_tool(tool_name: &str, callable_name: &str) -> ToolInfo {
|
||||
let mut tool = create_test_tool_with_connector(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
tool_name,
|
||||
"calendar",
|
||||
Some("Calendar"),
|
||||
);
|
||||
tool.callable_name = callable_name.to_string();
|
||||
tool.callable_namespace = "codex_apps__calendar".to_string();
|
||||
tool
|
||||
}
|
||||
|
||||
fn create_codex_apps_tools_cache_context(
|
||||
codex_home: PathBuf,
|
||||
account_id: Option<&str>,
|
||||
@@ -276,8 +288,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("server1__tool1"));
|
||||
assert!(qualified_tools.contains_key("server1__tool2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -291,7 +303,7 @@ 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("server1__duplicate_tool"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -317,7 +329,7 @@ fn test_qualify_tools_long_names_same_server() {
|
||||
keys.sort();
|
||||
|
||||
assert!(keys.iter().all(|key| key.len() == 64));
|
||||
assert!(keys.iter().all(|key| key.starts_with("mcp__my_server__")));
|
||||
assert!(keys.iter().all(|key| key.starts_with("my_server__")));
|
||||
assert!(
|
||||
keys.iter()
|
||||
.all(|key| key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')),
|
||||
@@ -333,16 +345,16 @@ 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, "server_one__tool_two_three");
|
||||
assert_eq!(
|
||||
format!("{}{}", tool.callable_namespace, tool.callable_name),
|
||||
ToolName::namespaced(&tool.callable_namespace, &tool.callable_name).display(),
|
||||
qualified_name
|
||||
);
|
||||
|
||||
// The key and callable parts are sanitized for model-visible tool calls, but
|
||||
// the raw MCP name is preserved for the actual MCP call.
|
||||
assert_eq!(tool.server_name, "server.one");
|
||||
assert_eq!(tool.callable_namespace, "mcp__server_one__");
|
||||
assert_eq!(tool.callable_namespace, "server_one");
|
||||
assert_eq!(tool.callable_name, "tool_two_three");
|
||||
assert_eq!(tool.tool.name, "tool.two-three");
|
||||
|
||||
@@ -362,8 +374,8 @@ 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!(tool.callable_namespace, "mcp__music_studio__");
|
||||
assert_eq!(qualified_name, "music_studio__get_strudel_guide");
|
||||
assert_eq!(tool.callable_namespace, "music_studio");
|
||||
assert_eq!(tool.callable_name, "get_strudel_guide");
|
||||
assert_eq!(tool.tool.name, "get-strudel-guide");
|
||||
}
|
||||
@@ -624,10 +636,7 @@ fn startup_cached_codex_apps_tools_loads_from_disk_cache() {
|
||||
Some("account-one"),
|
||||
Some("user-one"),
|
||||
);
|
||||
let cached_tools = vec![create_test_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_search",
|
||||
)];
|
||||
let cached_tools = vec![create_codex_apps_test_tool("calendar_search", "search")];
|
||||
write_cached_codex_apps_tools(&cache_context, &cached_tools);
|
||||
|
||||
let startup_snapshot = load_startup_cached_codex_apps_tools_snapshot(
|
||||
@@ -638,14 +647,14 @@ fn startup_cached_codex_apps_tools_loads_from_disk_cache() {
|
||||
|
||||
assert_eq!(startup_tools.len(), 1);
|
||||
assert_eq!(startup_tools[0].server_name, CODEX_APPS_MCP_SERVER_NAME);
|
||||
assert_eq!(startup_tools[0].callable_name, "calendar_search");
|
||||
assert_eq!(startup_tools[0].callable_name, "search");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_all_tools_uses_startup_snapshot_while_client_is_pending() {
|
||||
let startup_tools = vec![create_test_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
let startup_tools = vec![create_codex_apps_test_tool(
|
||||
"calendar_create_event",
|
||||
"create_event",
|
||||
)];
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
@@ -667,10 +676,10 @@ 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("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");
|
||||
assert_eq!(tool.callable_name, "create_event");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -695,11 +704,11 @@ async fn resolve_tool_info_accepts_canonical_namespaced_tool_names() {
|
||||
);
|
||||
|
||||
let tool = manager
|
||||
.resolve_tool_info(&ToolName::namespaced("mcp__rmcp__", "echo"))
|
||||
.resolve_tool_info(&ToolName::namespaced("rmcp", "echo"))
|
||||
.await
|
||||
.expect("split MCP tool namespace and name should resolve");
|
||||
|
||||
let expected = ("rmcp", "mcp__rmcp__", "echo", "echo");
|
||||
let expected = ("rmcp", "rmcp", "echo", "echo");
|
||||
assert_eq!(
|
||||
(
|
||||
tool.server_name.as_str(),
|
||||
@@ -764,9 +773,9 @@ async fn list_all_tools_does_not_block_when_startup_snapshot_cache_hit_is_empty(
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() {
|
||||
let startup_tools = vec![create_test_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
let startup_tools = vec![create_codex_apps_test_tool(
|
||||
"calendar_create_event",
|
||||
"create_event",
|
||||
)];
|
||||
let failed_client = futures::future::ready::<Result<ManagedClient, StartupOutcomeError>>(Err(
|
||||
StartupOutcomeError::Failed {
|
||||
@@ -793,10 +802,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("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");
|
||||
assert_eq!(tool.callable_name, "create_event");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -41,7 +41,6 @@ use crate::connection_manager::McpConnectionManager;
|
||||
use crate::runtime::McpRuntimeEnvironment;
|
||||
|
||||
pub const CODEX_APPS_MCP_SERVER_NAME: &str = "codex_apps";
|
||||
const MCP_TOOL_NAME_PREFIX: &str = "mcp";
|
||||
const MCP_TOOL_NAME_DELIMITER: &str = "__";
|
||||
const CODEX_CONNECTORS_TOKEN_ENV_VAR: &str = "CODEX_CONNECTORS_TOKEN";
|
||||
|
||||
@@ -59,9 +58,10 @@ impl McpSnapshotDetail {
|
||||
}
|
||||
|
||||
pub fn qualified_mcp_tool_name_prefix(server_name: &str) -> String {
|
||||
sanitize_responses_api_tool_name(&format!(
|
||||
"{MCP_TOOL_NAME_PREFIX}{MCP_TOOL_NAME_DELIMITER}{server_name}{MCP_TOOL_NAME_DELIMITER}"
|
||||
))
|
||||
format!(
|
||||
"{}{MCP_TOOL_NAME_DELIMITER}",
|
||||
sanitize_responses_api_tool_name(server_name)
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns true when MCP permission prompts should resolve as approved instead
|
||||
|
||||
@@ -36,7 +36,7 @@ fn test_mcp_config(codex_home: PathBuf) -> McpConfig {
|
||||
fn qualified_mcp_tool_name_prefix_sanitizes_server_names_without_lowercasing() {
|
||||
assert_eq!(
|
||||
qualified_mcp_tool_name_prefix("Some-Server"),
|
||||
"mcp__Some_Server__".to_string()
|
||||
"Some_Server__".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ pub(crate) fn filter_tools(tools: Vec<ToolInfo>, filter: &ToolFilter) -> Vec<Too
|
||||
///
|
||||
/// 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.
|
||||
/// every flattened model-visible `namespace__tool` name is unique and <= 64 bytes.
|
||||
pub(crate) fn qualify_tools<I>(tools: I) -> HashMap<String, ToolInfo>
|
||||
where
|
||||
I: IntoIterator<Item = ToolInfo>,
|
||||
@@ -327,17 +327,22 @@ fn fit_callable_parts_with_hash(
|
||||
raw_identity: &str,
|
||||
) -> (String, String) {
|
||||
let suffix = callable_name_hash_suffix(raw_identity);
|
||||
let max_tool_len = MAX_TOOL_NAME_LENGTH.saturating_sub(namespace.len());
|
||||
if max_tool_len >= suffix.len() {
|
||||
let prefix_len = max_tool_len - suffix.len();
|
||||
return (
|
||||
namespace.to_string(),
|
||||
format!("{}{}", truncate_name(tool_name, prefix_len), suffix),
|
||||
);
|
||||
|
||||
for prefix_len in (0..=tool_name.chars().count()).rev() {
|
||||
let candidate_name = format!("{}{}", truncate_name(tool_name, prefix_len), suffix);
|
||||
if qualified_name(namespace, &candidate_name).len() <= MAX_TOOL_NAME_LENGTH {
|
||||
return (namespace.to_string(), candidate_name);
|
||||
}
|
||||
}
|
||||
|
||||
let max_namespace_len = MAX_TOOL_NAME_LENGTH - suffix.len();
|
||||
(truncate_name(namespace, max_namespace_len), suffix)
|
||||
for namespace_len in (0..=namespace.chars().count()).rev() {
|
||||
let candidate_namespace = truncate_name(namespace, namespace_len);
|
||||
if qualified_name(&candidate_namespace, &suffix).len() <= MAX_TOOL_NAME_LENGTH {
|
||||
return (candidate_namespace, suffix);
|
||||
}
|
||||
}
|
||||
|
||||
(String::new(), suffix)
|
||||
}
|
||||
|
||||
fn unique_callable_parts(
|
||||
@@ -346,9 +351,15 @@ fn unique_callable_parts(
|
||||
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);
|
||||
let qualified_display_name = qualified_name(namespace, tool_name);
|
||||
if qualified_display_name.len() <= MAX_TOOL_NAME_LENGTH
|
||||
&& used_names.insert(qualified_display_name.clone())
|
||||
{
|
||||
return (
|
||||
namespace.to_string(),
|
||||
tool_name.to_string(),
|
||||
qualified_display_name,
|
||||
);
|
||||
}
|
||||
|
||||
let mut attempt = 0_u32;
|
||||
@@ -360,10 +371,14 @@ 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}");
|
||||
let qualified_name = qualified_name(&namespace, &tool_name);
|
||||
if used_names.insert(qualified_name.clone()) {
|
||||
return (namespace, tool_name, qualified_name);
|
||||
}
|
||||
attempt = attempt.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn qualified_name(namespace: &str, tool_name: &str) -> String {
|
||||
ToolName::namespaced(namespace, tool_name).display()
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ fn codex_app_tool(
|
||||
) -> ToolInfo {
|
||||
let tool_namespace = connector_name
|
||||
.map(sanitize_name)
|
||||
.map(|connector_name| format!("mcp__{CODEX_APPS_MCP_SERVER_NAME}__{connector_name}"))
|
||||
.map(|connector_name| format!("{CODEX_APPS_MCP_SERVER_NAME}__{connector_name}"))
|
||||
.unwrap_or_else(|| CODEX_APPS_MCP_SERVER_NAME.to_string());
|
||||
|
||||
ToolInfo {
|
||||
@@ -175,7 +175,7 @@ fn merge_connectors_replaces_plugin_placeholder_name_with_accessible_name() {
|
||||
fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() {
|
||||
let tools = HashMap::from([
|
||||
(
|
||||
"mcp__codex_apps__calendar_list_events".to_string(),
|
||||
"codex_apps__calendar__list_events".to_string(),
|
||||
codex_app_tool(
|
||||
"calendar_list_events",
|
||||
"calendar",
|
||||
@@ -184,7 +184,7 @@ fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() {
|
||||
),
|
||||
),
|
||||
(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
"codex_apps__calendar__create_event".to_string(),
|
||||
codex_app_tool(
|
||||
"calendar_create_event",
|
||||
"calendar",
|
||||
@@ -193,7 +193,7 @@ fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() {
|
||||
),
|
||||
),
|
||||
(
|
||||
"mcp__sample__echo".to_string(),
|
||||
"sample__echo".to_string(),
|
||||
ToolInfo {
|
||||
server_name: "sample".to_string(),
|
||||
callable_name: "echo".to_string(),
|
||||
@@ -242,7 +242,7 @@ async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_instal
|
||||
let cache_key = accessible_connectors_cache_key(&config, /*auth*/ None);
|
||||
let tools = HashMap::from([
|
||||
(
|
||||
"mcp__codex_apps__calendar_list_events".to_string(),
|
||||
"codex_apps__calendar__list_events".to_string(),
|
||||
codex_app_tool(
|
||||
"calendar_list_events",
|
||||
"calendar",
|
||||
@@ -251,7 +251,7 @@ async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_instal
|
||||
),
|
||||
),
|
||||
(
|
||||
"mcp__codex_apps__openai_hidden".to_string(),
|
||||
"codex_apps__openai_hidden".to_string(),
|
||||
codex_app_tool(
|
||||
"openai_hidden",
|
||||
"connector_openai_hidden",
|
||||
@@ -318,11 +318,11 @@ 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(),
|
||||
"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(),
|
||||
callable_namespace: "codex_apps__calendar".to_string(),
|
||||
server_instructions: None,
|
||||
tool: Tool {
|
||||
name: "calendar_create_event".to_string().into(),
|
||||
|
||||
@@ -1907,7 +1907,7 @@ async fn approve_mode_skips_when_annotations_do_not_require_approval() {
|
||||
&turn_context,
|
||||
"call-1",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
"test__tool",
|
||||
Some(&metadata),
|
||||
AppToolApproval::Approve,
|
||||
)
|
||||
@@ -1980,7 +1980,7 @@ async fn guardian_mode_skips_auto_when_annotations_do_not_require_approval() {
|
||||
&turn_context,
|
||||
"call-guardian",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
"test__tool",
|
||||
Some(&metadata),
|
||||
AppToolApproval::Auto,
|
||||
)
|
||||
@@ -1995,7 +1995,7 @@ async fn permission_request_hook_allows_mcp_tool_call() {
|
||||
let log_path = install_mcp_permission_request_hook(
|
||||
&mut session,
|
||||
&turn_context,
|
||||
"mcp__memory__.*",
|
||||
"memory__.*",
|
||||
&serde_json::json!({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PermissionRequest",
|
||||
@@ -2036,7 +2036,7 @@ async fn permission_request_hook_allows_mcp_tool_call() {
|
||||
&turn_context,
|
||||
"call-mcp-hook",
|
||||
&invocation,
|
||||
"mcp__memory__create_entities",
|
||||
"memory__create_entities",
|
||||
Some(&metadata),
|
||||
AppToolApproval::Auto,
|
||||
)
|
||||
@@ -2057,7 +2057,7 @@ async fn permission_request_hook_allows_mcp_tool_call() {
|
||||
"transcript_path": null,
|
||||
"model": turn_context.model_info.slug,
|
||||
"permission_mode": "default",
|
||||
"tool_name": "mcp__memory__create_entities",
|
||||
"tool_name": "memory__create_entities",
|
||||
"hook_event_name": "PermissionRequest",
|
||||
"tool_input": {
|
||||
"entities": [{
|
||||
@@ -2075,7 +2075,7 @@ async fn permission_request_hook_uses_hook_tool_name_without_metadata() {
|
||||
let log_path = install_mcp_permission_request_hook(
|
||||
&mut session,
|
||||
&turn_context,
|
||||
"mcp__memory__.*",
|
||||
"memory__.*",
|
||||
&serde_json::json!({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PermissionRequest",
|
||||
@@ -2096,7 +2096,7 @@ async fn permission_request_hook_uses_hook_tool_name_without_metadata() {
|
||||
&turn_context,
|
||||
"call-mcp-hook-no-metadata",
|
||||
&invocation,
|
||||
"mcp__memory__create_entities",
|
||||
"memory__create_entities",
|
||||
/*metadata*/ None,
|
||||
AppToolApproval::Auto,
|
||||
)
|
||||
@@ -2117,7 +2117,7 @@ async fn permission_request_hook_uses_hook_tool_name_without_metadata() {
|
||||
"transcript_path": null,
|
||||
"model": turn_context.model_info.slug,
|
||||
"permission_mode": "default",
|
||||
"tool_name": "mcp__memory__create_entities",
|
||||
"tool_name": "memory__create_entities",
|
||||
"hook_event_name": "PermissionRequest",
|
||||
"tool_input": { "entities": [] }
|
||||
})]
|
||||
@@ -2130,7 +2130,7 @@ async fn permission_request_hook_runs_after_remembered_mcp_approval() {
|
||||
let log_path = install_mcp_permission_request_hook(
|
||||
&mut session,
|
||||
&turn_context,
|
||||
"mcp__memory__.*",
|
||||
"memory__.*",
|
||||
&serde_json::json!({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PermissionRequest",
|
||||
@@ -2173,7 +2173,7 @@ async fn permission_request_hook_runs_after_remembered_mcp_approval() {
|
||||
&turn_context,
|
||||
"call-mcp-remembered",
|
||||
&invocation,
|
||||
"mcp__memory__create_entities",
|
||||
"memory__create_entities",
|
||||
Some(&metadata),
|
||||
AppToolApproval::Auto,
|
||||
)
|
||||
@@ -2253,7 +2253,7 @@ async fn guardian_mode_mcp_denial_returns_rationale_message() {
|
||||
&turn_context,
|
||||
"call-guardian-deny",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
"test__tool",
|
||||
Some(&metadata),
|
||||
AppToolApproval::Auto,
|
||||
)
|
||||
@@ -2310,7 +2310,7 @@ async fn prompt_mode_waits_for_approval_when_annotations_do_not_require_approval
|
||||
&turn_context,
|
||||
"call-prompt",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
"test__tool",
|
||||
Some(&metadata),
|
||||
AppToolApproval::Prompt,
|
||||
)
|
||||
@@ -2385,7 +2385,7 @@ async fn approve_mode_blocks_when_arc_returns_interrupt_for_model() {
|
||||
&turn_context,
|
||||
"call-2",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
"test__tool",
|
||||
Some(&metadata),
|
||||
AppToolApproval::Approve,
|
||||
)
|
||||
@@ -2457,7 +2457,7 @@ async fn custom_approve_mode_blocks_when_arc_returns_interrupt_for_model() {
|
||||
&turn_context,
|
||||
"call-2-custom",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
"test__tool",
|
||||
Some(&metadata),
|
||||
AppToolApproval::Approve,
|
||||
)
|
||||
@@ -2529,7 +2529,7 @@ async fn approve_mode_blocks_when_arc_returns_interrupt_without_annotations() {
|
||||
&turn_context,
|
||||
"call-3",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
"test__tool",
|
||||
Some(&metadata),
|
||||
AppToolApproval::Approve,
|
||||
)
|
||||
@@ -2611,7 +2611,7 @@ async fn full_access_mode_skips_arc_monitor_for_all_approval_modes() {
|
||||
&turn_context,
|
||||
"call-2",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
"test__tool",
|
||||
Some(&metadata),
|
||||
approval_mode,
|
||||
)
|
||||
@@ -2701,7 +2701,7 @@ async fn approve_mode_skips_arc_and_guardian_when_guardian_reviewer_is_enabled()
|
||||
&turn_context,
|
||||
"call-3",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
"test__tool",
|
||||
Some(&metadata),
|
||||
AppToolApproval::Approve,
|
||||
)
|
||||
|
||||
@@ -48,10 +48,10 @@ fn make_mcp_tool(
|
||||
let tool_namespace = if server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
connector_name
|
||||
.map(sanitize_name)
|
||||
.map(|connector_name| format!("mcp__{server_name}__{connector_name}"))
|
||||
.map(|connector_name| format!("{server_name}__{connector_name}"))
|
||||
.unwrap_or_else(|| server_name.to_string())
|
||||
} else {
|
||||
format!("mcp__{server_name}__")
|
||||
server_name.to_string()
|
||||
};
|
||||
|
||||
ToolInfo {
|
||||
@@ -82,7 +82,7 @@ fn numbered_mcp_tools(count: usize) -> HashMap<String, ToolInfo> {
|
||||
.map(|index| {
|
||||
let tool_name = format!("tool_{index}");
|
||||
(
|
||||
format!("mcp__rmcp__{tool_name}"),
|
||||
format!("rmcp__{tool_name}"),
|
||||
make_mcp_tool(
|
||||
"rmcp", &tool_name, /*connector_id*/ None, /*connector_name*/ None,
|
||||
),
|
||||
@@ -165,7 +165,7 @@ async fn directly_exposes_explicit_apps_without_deferred_overlap() {
|
||||
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
|
||||
let mut mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD - 1);
|
||||
mcp_tools.extend([(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
"codex_apps__calendar__create_event".to_string(),
|
||||
make_mcp_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_create_event",
|
||||
@@ -187,7 +187,7 @@ async fn directly_exposes_explicit_apps_without_deferred_overlap() {
|
||||
tool_names.sort();
|
||||
assert_eq!(
|
||||
tool_names,
|
||||
vec!["mcp__codex_apps__calendar_create_event".to_string()]
|
||||
vec!["codex_apps__calendar__create_event".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
exposure.deferred_tools.as_ref().map(HashMap::len),
|
||||
@@ -203,8 +203,8 @@ async fn directly_exposes_explicit_apps_without_deferred_overlap() {
|
||||
.all(|direct_tool_name| !deferred_tools.contains_key(direct_tool_name)),
|
||||
"direct tools should not also be deferred: {tool_names:?}"
|
||||
);
|
||||
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("codex_apps__calendar__create_event"));
|
||||
assert!(deferred_tools.contains_key("rmcp__tool_0"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -217,13 +217,13 @@ async fn always_defer_feature_preserves_explicit_apps() {
|
||||
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
|
||||
let mcp_tools = HashMap::from([
|
||||
(
|
||||
"mcp__rmcp__tool".to_string(),
|
||||
"rmcp__tool".to_string(),
|
||||
make_mcp_tool(
|
||||
"rmcp", "tool", /*connector_id*/ None, /*connector_name*/ None,
|
||||
),
|
||||
),
|
||||
(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
"codex_apps__calendar__create_event".to_string(),
|
||||
make_mcp_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_create_event",
|
||||
@@ -246,12 +246,12 @@ async fn always_defer_feature_preserves_explicit_apps() {
|
||||
direct_tool_names.sort();
|
||||
assert_eq!(
|
||||
direct_tool_names,
|
||||
vec!["mcp__codex_apps__calendar_create_event".to_string()]
|
||||
vec!["codex_apps__calendar__create_event".to_string()]
|
||||
);
|
||||
let deferred_tools = exposure
|
||||
.deferred_tools
|
||||
.as_ref()
|
||||
.expect("MCP tools should be discoverable through tool_search");
|
||||
assert!(deferred_tools.contains_key("mcp__rmcp__tool"));
|
||||
assert!(!deferred_tools.contains_key("mcp__codex_apps__calendar_create_event"));
|
||||
assert!(deferred_tools.contains_key("rmcp__tool"));
|
||||
assert!(!deferred_tools.contains_key("codex_apps__calendar__create_event"));
|
||||
}
|
||||
|
||||
@@ -142,12 +142,12 @@ mod tests {
|
||||
cancellation_token: tokio_util::sync::CancellationToken::new(),
|
||||
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
|
||||
call_id: "call-mcp-pre".to_string(),
|
||||
tool_name: codex_tools::ToolName::namespaced("mcp__memory__", "create_entities"),
|
||||
tool_name: codex_tools::ToolName::namespaced("memory", "create_entities"),
|
||||
source: ToolCallSource::Direct,
|
||||
payload,
|
||||
}),
|
||||
Some(PreToolUsePayload {
|
||||
tool_name: HookToolName::new("mcp__memory__create_entities"),
|
||||
tool_name: HookToolName::new("memory__create_entities"),
|
||||
tool_input: json!({
|
||||
"entities": [{
|
||||
"name": "Ada",
|
||||
@@ -191,14 +191,14 @@ mod tests {
|
||||
cancellation_token: tokio_util::sync::CancellationToken::new(),
|
||||
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
|
||||
call_id: "call-mcp-post".to_string(),
|
||||
tool_name: codex_tools::ToolName::namespaced("mcp__filesystem__", "read_file"),
|
||||
tool_name: codex_tools::ToolName::namespaced("filesystem", "read_file"),
|
||||
source: ToolCallSource::Direct,
|
||||
payload,
|
||||
};
|
||||
assert_eq!(
|
||||
McpHandler.post_tool_use_payload(&invocation, &output),
|
||||
Some(PostToolUsePayload {
|
||||
tool_name: HookToolName::new("mcp__filesystem__read_file"),
|
||||
tool_name: HookToolName::new("filesystem__read_file"),
|
||||
tool_use_id: "call-mcp-post".to_string(),
|
||||
tool_input: json!({
|
||||
"path": {
|
||||
|
||||
@@ -202,11 +202,11 @@ mod tests {
|
||||
let handler = handler_from_tools(
|
||||
Some(&std::collections::HashMap::from([
|
||||
(
|
||||
"mcp__calendar__create_event".to_string(),
|
||||
"calendar__create_event".to_string(),
|
||||
tool_info("calendar", "create_event", "Create events"),
|
||||
),
|
||||
(
|
||||
"mcp__calendar__list_events".to_string(),
|
||||
"calendar__list_events".to_string(),
|
||||
tool_info("calendar", "list_events", "List events"),
|
||||
),
|
||||
])),
|
||||
@@ -226,8 +226,8 @@ mod tests {
|
||||
tools,
|
||||
vec![
|
||||
LoadableToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: "mcp__calendar__".to_string(),
|
||||
description: "Tools in the mcp__calendar__ namespace.".to_string(),
|
||||
name: "calendar".to_string(),
|
||||
description: "Tools in the calendar namespace.".to_string(),
|
||||
tools: vec![
|
||||
ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
||||
name: "create_event".to_string(),
|
||||
@@ -380,7 +380,7 @@ mod tests {
|
||||
.map(|index| {
|
||||
let tool_name = format!("tool_{index:03}");
|
||||
(
|
||||
format!("mcp__{server_name}__{tool_name}"),
|
||||
format!("{server_name}__{tool_name}"),
|
||||
tool_info(server_name, &tool_name, description_prefix),
|
||||
)
|
||||
})
|
||||
@@ -391,7 +391,7 @@ mod tests {
|
||||
ToolInfo {
|
||||
server_name: server_name.to_string(),
|
||||
callable_name: tool_name.to_string(),
|
||||
callable_namespace: format!("mcp__{server_name}__"),
|
||||
callable_namespace: server_name.to_string(),
|
||||
server_instructions: None,
|
||||
tool: Tool {
|
||||
name: tool_name.to_string().into(),
|
||||
|
||||
@@ -23,7 +23,7 @@ impl ToolHandler for TestHandler {
|
||||
fn handler_looks_up_namespaced_aliases_explicitly() {
|
||||
let plain_handler = Arc::new(TestHandler) as Arc<dyn AnyToolHandler>;
|
||||
let namespaced_handler = Arc::new(TestHandler) as Arc<dyn AnyToolHandler>;
|
||||
let namespace = "mcp__codex_apps__gmail";
|
||||
let namespace = "codex_apps__gmail";
|
||||
let tool_name = "gmail_get_recent_emails";
|
||||
let plain_name = codex_tools::ToolName::plain(tool_name);
|
||||
let namespaced_name = codex_tools::ToolName::namespaced(namespace, tool_name);
|
||||
@@ -35,7 +35,7 @@ fn handler_looks_up_namespaced_aliases_explicitly() {
|
||||
let plain = registry.handler(&plain_name);
|
||||
let namespaced = registry.handler(&namespaced_name);
|
||||
let missing_namespaced = registry.handler(&codex_tools::ToolName::namespaced(
|
||||
"mcp__codex_apps__calendar",
|
||||
"codex_apps__calendar",
|
||||
tool_name,
|
||||
));
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ async fn parallel_support_does_not_match_namespaced_local_tool_names() -> anyhow
|
||||
.expect("test session should expose a parallel shell-like tool");
|
||||
|
||||
assert!(!router.tool_supports_parallel(&ToolCall {
|
||||
tool_name: ToolName::namespaced("mcp__server__", parallel_tool_name),
|
||||
tool_name: ToolName::namespaced("server", parallel_tool_name),
|
||||
call_id: "call-namespaced-tool".to_string(),
|
||||
payload: ToolPayload::Function {
|
||||
arguments: "{}".to_string(),
|
||||
@@ -76,7 +76,7 @@ async fn build_tool_call_uses_namespace_for_registry_name() -> anyhow::Result<()
|
||||
ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
name: tool_name.clone(),
|
||||
namespace: Some("mcp__codex_apps__calendar".to_string()),
|
||||
namespace: Some("codex_apps__calendar".to_string()),
|
||||
arguments: "{}".to_string(),
|
||||
call_id: "call-namespace".to_string(),
|
||||
},
|
||||
@@ -86,7 +86,7 @@ async fn build_tool_call_uses_namespace_for_registry_name() -> anyhow::Result<()
|
||||
|
||||
assert_eq!(
|
||||
call.tool_name,
|
||||
ToolName::namespaced("mcp__codex_apps__calendar", tool_name)
|
||||
ToolName::namespaced("codex_apps__calendar", tool_name)
|
||||
);
|
||||
assert_eq!(call.call_id, "call-namespace");
|
||||
match call.payload {
|
||||
@@ -115,7 +115,7 @@ async fn mcp_parallel_support_uses_exact_payload_server() -> anyhow::Result<()>
|
||||
);
|
||||
|
||||
let deferred_call = ToolCall {
|
||||
tool_name: ToolName::namespaced("mcp__echo__", "query_with_delay"),
|
||||
tool_name: ToolName::namespaced("echo", "query_with_delay"),
|
||||
call_id: "call-deferred".to_string(),
|
||||
payload: ToolPayload::Mcp {
|
||||
server: "echo".to_string(),
|
||||
@@ -126,7 +126,7 @@ async fn mcp_parallel_support_uses_exact_payload_server() -> anyhow::Result<()>
|
||||
assert!(router.tool_supports_parallel(&deferred_call));
|
||||
|
||||
let different_server_call = ToolCall {
|
||||
tool_name: ToolName::namespaced("mcp__hello_echo__", "query_with_delay"),
|
||||
tool_name: ToolName::namespaced("hello_echo", "query_with_delay"),
|
||||
call_id: "call-other-server".to_string(),
|
||||
payload: ToolPayload::Mcp {
|
||||
server: "hello_echo".to_string(),
|
||||
|
||||
@@ -61,7 +61,7 @@ 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(),
|
||||
callable_namespace: "test_server".to_string(),
|
||||
server_instructions: None,
|
||||
tool,
|
||||
connector_id: None,
|
||||
@@ -133,7 +133,7 @@ fn deferred_responses_api_tool_serializes_with_defer_loading() {
|
||||
|
||||
let serialized = serde_json::to_value(ToolSpec::Function(
|
||||
mcp_tool_to_deferred_responses_api_tool(
|
||||
&ToolName::namespaced("mcp__codex_apps__", "lookup_order"),
|
||||
&ToolName::namespaced("codex_apps", "lookup_order"),
|
||||
&tool,
|
||||
)
|
||||
.expect("convert deferred tool"),
|
||||
@@ -893,11 +893,11 @@ async fn search_tool_description_falls_back_to_connector_name_without_descriptio
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
Some(HashMap::from([(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
"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(),
|
||||
callable_name: "create_event".to_string(),
|
||||
callable_namespace: "codex_apps__calendar".to_string(),
|
||||
server_instructions: None,
|
||||
tool: mcp_tool(
|
||||
"calendar_create_event",
|
||||
@@ -945,11 +945,11 @@ async fn search_tool_registers_namespaced_mcp_tool_aliases() {
|
||||
/*mcp_tools*/ None,
|
||||
Some(HashMap::from([
|
||||
(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
"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(),
|
||||
callable_name: "create_event".to_string(),
|
||||
callable_namespace: "codex_apps__calendar".to_string(),
|
||||
server_instructions: None,
|
||||
tool: mcp_tool(
|
||||
"calendar-create-event",
|
||||
@@ -963,11 +963,11 @@ async fn search_tool_registers_namespaced_mcp_tool_aliases() {
|
||||
},
|
||||
),
|
||||
(
|
||||
"mcp__codex_apps__calendar_list_events".to_string(),
|
||||
"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(),
|
||||
callable_name: "list_events".to_string(),
|
||||
callable_namespace: "codex_apps__calendar".to_string(),
|
||||
server_instructions: None,
|
||||
tool: mcp_tool(
|
||||
"calendar-list-events",
|
||||
@@ -981,11 +981,11 @@ async fn search_tool_registers_namespaced_mcp_tool_aliases() {
|
||||
},
|
||||
),
|
||||
(
|
||||
"mcp__rmcp__echo".to_string(),
|
||||
"rmcp__echo".to_string(),
|
||||
ToolInfo {
|
||||
server_name: "rmcp".to_string(),
|
||||
callable_name: "echo".to_string(),
|
||||
callable_namespace: "mcp__rmcp__".to_string(),
|
||||
callable_namespace: "rmcp".to_string(),
|
||||
server_instructions: None,
|
||||
tool: mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})),
|
||||
connector_id: None,
|
||||
@@ -999,8 +999,8 @@ async fn search_tool_registers_namespaced_mcp_tool_aliases() {
|
||||
)
|
||||
.build();
|
||||
|
||||
let app_alias = ToolName::namespaced("mcp__codex_apps__calendar", "_create_event");
|
||||
let mcp_alias = ToolName::namespaced("mcp__rmcp__", "echo");
|
||||
let app_alias = ToolName::namespaced("codex_apps__calendar", "create_event");
|
||||
let mcp_alias = ToolName::namespaced("rmcp", "echo");
|
||||
|
||||
assert!(registry.has_handler(&ToolName::plain(TOOL_SEARCH_TOOL_NAME)));
|
||||
assert!(registry.has_handler(&app_alias));
|
||||
@@ -1025,7 +1025,7 @@ async fn tool_search_entries_skip_namespace_outputs_when_namespace_tools_are_dis
|
||||
});
|
||||
tools_config.namespace_tools = false;
|
||||
let mcp_tools = HashMap::from([(
|
||||
"mcp__test_server__echo".to_string(),
|
||||
"test_server__echo".to_string(),
|
||||
mcp_tool_info(mcp_tool(
|
||||
"echo",
|
||||
"Echo",
|
||||
@@ -1084,7 +1084,7 @@ async fn direct_mcp_tools_register_namespaced_handlers() {
|
||||
let (_, registry) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"mcp__test_server__echo".to_string(),
|
||||
"test_server__echo".to_string(),
|
||||
mcp_tool_info(mcp_tool(
|
||||
"echo",
|
||||
"Echo",
|
||||
@@ -1096,8 +1096,8 @@ async fn direct_mcp_tools_register_namespaced_handlers() {
|
||||
)
|
||||
.build();
|
||||
|
||||
assert!(registry.has_handler(&ToolName::namespaced("mcp__test_server__", "echo")));
|
||||
assert!(!registry.has_handler(&ToolName::plain("mcp__test_server__echo")));
|
||||
assert!(registry.has_handler(&ToolName::namespaced("test_server", "echo")));
|
||||
assert!(!registry.has_handler(&ToolName::plain("test_server__echo")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1118,7 +1118,7 @@ async fn unavailable_mcp_tools_are_exposed_as_dummy_function_tools() {
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
});
|
||||
|
||||
let unavailable_tool = ToolName::namespaced("mcp__codex_apps__calendar", "_create_event");
|
||||
let unavailable_tool = ToolName::namespaced("codex_apps__calendar", "create_event");
|
||||
let (tools, registry) = build_specs_with_unavailable_tools(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
@@ -1128,7 +1128,7 @@ async fn unavailable_mcp_tools_are_exposed_as_dummy_function_tools() {
|
||||
)
|
||||
.build();
|
||||
|
||||
let tool = find_tool(&tools, "mcp__codex_apps__calendar_create_event");
|
||||
let tool = find_tool(&tools, "codex_apps__calendar__create_event");
|
||||
let ToolSpec::Function(ResponsesApiTool {
|
||||
description,
|
||||
parameters,
|
||||
@@ -1143,10 +1143,10 @@ async fn unavailable_mcp_tools_are_exposed_as_dummy_function_tools() {
|
||||
Some(AdditionalProperties::Boolean(false))
|
||||
);
|
||||
assert!(registry.has_handler(&ToolName::namespaced(
|
||||
"mcp__codex_apps__calendar",
|
||||
"_create_event"
|
||||
"codex_apps__calendar",
|
||||
"create_event"
|
||||
)));
|
||||
assert!(!registry.has_handler(&ToolName::plain("mcp__codex_apps__calendar_create_event")));
|
||||
assert!(!registry.has_handler(&ToolName::plain("codex_apps__calendar__create_event")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -39,7 +39,10 @@ pub(crate) fn collect_unavailable_called_tools(
|
||||
}
|
||||
|
||||
fn should_collect_unavailable_tool(name: &str, namespace: Option<&str>) -> bool {
|
||||
namespace.is_some_and(|namespace| namespace.starts_with("mcp__")) || name.starts_with("mcp__")
|
||||
// New histories preserve the namespace split, so any missing namespaced call
|
||||
// is eligible for a placeholder. Keep the flattened MCP branch for rollouts
|
||||
// written before namespaced MCP calls were preserved in history.
|
||||
namespace.is_some() || name.starts_with("mcp__")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -58,11 +61,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_unavailable_called_tools_detects_mcp_function_calls() {
|
||||
fn collect_unavailable_called_tools_detects_namespaced_and_legacy_mcp_calls() {
|
||||
let input = vec![
|
||||
function_call("shell", /*namespace*/ None),
|
||||
function_call("mcp__server__lookup", /*namespace*/ None),
|
||||
function_call("_create_event", Some("mcp__codex_apps__calendar")),
|
||||
function_call("lookup", Some("calendar")),
|
||||
];
|
||||
|
||||
let tools = collect_unavailable_called_tools(&input, &HashSet::new());
|
||||
@@ -70,6 +74,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
tools,
|
||||
vec![
|
||||
ToolName::namespaced("calendar", "lookup"),
|
||||
ToolName::namespaced("mcp__codex_apps__calendar", "_create_event"),
|
||||
ToolName::plain("mcp__server__lookup"),
|
||||
]
|
||||
|
||||
@@ -367,7 +367,7 @@ async fn code_mode_only_guides_all_tools_search_and_calls_deferred_app_tools() -
|
||||
"exec",
|
||||
r#"
|
||||
const tool = ALL_TOOLS.find(
|
||||
({ name }) => name === "mcp__codex_apps__calendar_timezone_option_99"
|
||||
({ name }) => name === "codex_apps__calendar_timezone_option_99"
|
||||
);
|
||||
if (!tool) {
|
||||
text(JSON.stringify({ found: false }));
|
||||
@@ -2075,7 +2075,7 @@ async fn code_mode_can_use_mcp_image_result_with_image_helper() -> Result<()> {
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let code = r#"
|
||||
const out = await tools.mcp__rmcp__image_scenario({
|
||||
const out = await tools.rmcp_image_scenario({
|
||||
scenario: "image_only_original_detail",
|
||||
});
|
||||
const imageItem = out.content.find((item) => item.type === "image");
|
||||
@@ -2176,7 +2176,7 @@ async fn code_mode_can_print_structured_mcp_tool_result_fields() -> Result<()> {
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let code = r#"
|
||||
const { content, structuredContent, isError } = await tools.mcp__rmcp__echo({
|
||||
const { content, structuredContent, isError } = await tools.rmcp_echo({
|
||||
message: "ping",
|
||||
});
|
||||
text(
|
||||
@@ -2214,7 +2214,7 @@ async fn code_mode_only_can_call_mcp_tool() -> Result<()> {
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let code = r#"
|
||||
const result = await tools.mcp__rmcp__echo({ message: "ping" });
|
||||
const result = await tools.rmcp_echo({ message: "ping" });
|
||||
text(`echo=${result.structuredContent?.echo ?? "missing"}`);
|
||||
"#;
|
||||
|
||||
@@ -2244,12 +2244,12 @@ async fn code_mode_exposes_mcp_tools_on_global_tools_object() -> Result<()> {
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let code = r#"
|
||||
const { content, structuredContent, isError } = await tools.mcp__rmcp__echo({
|
||||
const { content, structuredContent, isError } = await tools.rmcp_echo({
|
||||
message: "ping",
|
||||
});
|
||||
text(
|
||||
`hasEcho=${String(Object.keys(tools).includes("mcp__rmcp__echo"))}\n` +
|
||||
`echoType=${typeof tools.mcp__rmcp__echo}\n` +
|
||||
`hasEcho=${String(Object.keys(tools).includes("rmcp_echo"))}\n` +
|
||||
`echoType=${typeof tools.rmcp_echo}\n` +
|
||||
`echo=${structuredContent?.echo ?? "missing"}\n` +
|
||||
`isError=${String(isError)}\n` +
|
||||
`contentLength=${content.length}`
|
||||
@@ -2287,7 +2287,7 @@ async fn code_mode_exposes_namespaced_mcp_tools_on_global_tools_object() -> Resu
|
||||
let code = r#"
|
||||
text(JSON.stringify({
|
||||
hasExecCommand: typeof tools.exec_command === "function",
|
||||
hasNamespacedEcho: typeof tools.mcp__rmcp__echo === "function",
|
||||
hasNamespacedEcho: typeof tools.rmcp_echo === "function",
|
||||
}));
|
||||
"#;
|
||||
|
||||
@@ -2321,7 +2321,7 @@ async fn code_mode_exposes_normalized_illegal_mcp_tool_names() -> Result<()> {
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let code = r#"
|
||||
const result = await tools.mcp__rmcp__echo_tool({ message: "ping" });
|
||||
const result = await tools.rmcp_echo_tool({ message: "ping" });
|
||||
text(`echo=${result.structuredContent.echo}`);
|
||||
"#;
|
||||
|
||||
@@ -2502,7 +2502,7 @@ async fn code_mode_exports_all_tools_metadata_for_namespaced_mcp_tools() -> Resu
|
||||
let server = responses::start_mock_server().await;
|
||||
let code = r#"
|
||||
const tool = ALL_TOOLS.find(
|
||||
({ name }) => name === "mcp__rmcp__echo"
|
||||
({ name }) => name === "rmcp_echo"
|
||||
);
|
||||
text(JSON.stringify(tool));
|
||||
"#;
|
||||
@@ -2525,12 +2525,12 @@ text(JSON.stringify(tool));
|
||||
assert_eq!(
|
||||
parsed,
|
||||
serde_json::json!({
|
||||
"name": "mcp__rmcp__echo",
|
||||
"name": "rmcp_echo",
|
||||
"description": concat!(
|
||||
"Echo back the provided message and include environment data.\n\n",
|
||||
"exec tool declaration:\n",
|
||||
"```ts\n",
|
||||
"declare const tools: { mcp__rmcp__echo(args: { env_var?: string; message: string; }): ",
|
||||
"declare const tools: { rmcp_echo(args: { env_var?: string; message: string; }): ",
|
||||
"Promise<CallToolResult<{ echo: string; env: string | null; }>>; };\n",
|
||||
"```",
|
||||
),
|
||||
@@ -2701,7 +2701,7 @@ async fn code_mode_can_print_content_only_mcp_tool_result_fields() -> Result<()>
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let code = r#"
|
||||
const { content, structuredContent, isError } = await tools.mcp__rmcp__image_scenario({
|
||||
const { content, structuredContent, isError } = await tools.rmcp_image_scenario({
|
||||
scenario: "text_only",
|
||||
caption: "caption from mcp",
|
||||
});
|
||||
@@ -2744,7 +2744,7 @@ async fn code_mode_can_print_error_mcp_tool_result_fields() -> Result<()> {
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let code = r#"
|
||||
const { content, structuredContent, isError } = await tools.mcp__rmcp__echo({});
|
||||
const { content, structuredContent, isError } = await tools.rmcp_echo({});
|
||||
const firstText = content[0]?.text ?? "";
|
||||
const mentionsMissingMessage =
|
||||
firstText.includes("missing field") && firstText.includes("message");
|
||||
|
||||
@@ -26,9 +26,9 @@ use serde_json::Value;
|
||||
use serde_json::json;
|
||||
|
||||
const RMCP_SERVER: &str = "rmcp";
|
||||
const RMCP_NAMESPACE: &str = "mcp__rmcp__";
|
||||
const RMCP_ECHO_TOOL_NAME: &str = "mcp__rmcp__echo";
|
||||
const RMCP_HOOK_MATCHER: &str = "mcp__rmcp__.*";
|
||||
const RMCP_NAMESPACE: &str = "rmcp";
|
||||
const RMCP_ECHO_TOOL_NAME: &str = "rmcp__echo";
|
||||
const RMCP_HOOK_MATCHER: &str = "rmcp__.*";
|
||||
const RMCP_ECHO_MESSAGE: &str = "hook e2e ping";
|
||||
|
||||
fn write_pre_tool_use_hook(home: &Path, reason: &str) -> Result<()> {
|
||||
|
||||
@@ -30,9 +30,9 @@ use wiremock::matchers::header;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
const DOCUMENT_EXTRACT_NAMESPACE: &str = "mcp__codex_apps__calendar";
|
||||
const DOCUMENT_EXTRACT_TOOL: &str = "_extract_text";
|
||||
const DOCUMENT_EXTRACT_HOOK_MATCHER: &str = "mcp__codex_apps__calendar_extract_text";
|
||||
const DOCUMENT_EXTRACT_NAMESPACE: &str = "codex_apps__calendar";
|
||||
const DOCUMENT_EXTRACT_TOOL: &str = "extract_text";
|
||||
const DOCUMENT_EXTRACT_HOOK_MATCHER: &str = "codex_apps__calendar__extract_text";
|
||||
|
||||
fn configure_apps(config: &mut Config, chatgpt_base_url: &str) {
|
||||
if let Err(err) = config.features.enable(Feature::Apps) {
|
||||
|
||||
@@ -347,11 +347,11 @@ async fn explicit_plugin_mentions_inject_plugin_guidance() -> Result<()> {
|
||||
assert!(
|
||||
request_tools
|
||||
.iter()
|
||||
.any(|name| name == "mcp__codex_apps__google_calendar"),
|
||||
.any(|name| name == "codex_apps__google_calendar"),
|
||||
"expected plugin app tools to become visible for this turn: {request_tools:?}"
|
||||
);
|
||||
let echo_tool = request
|
||||
.tool_by_name("mcp__sample__", "echo")
|
||||
.tool_by_name("sample", "echo")
|
||||
.expect("plugin MCP tool should be present");
|
||||
let echo_description = echo_tool
|
||||
.get("description")
|
||||
@@ -362,7 +362,7 @@ async fn explicit_plugin_mentions_inject_plugin_guidance() -> Result<()> {
|
||||
"expected plugin MCP provenance in tool description: {echo_description:?}"
|
||||
);
|
||||
let calendar_tool = request
|
||||
.tool_by_name("mcp__codex_apps__google_calendar", "_create_event")
|
||||
.tool_by_name("codex_apps__google_calendar", "create_event")
|
||||
.expect("plugin app tool should be present");
|
||||
let calendar_description = calendar_tool
|
||||
.get("description")
|
||||
@@ -471,8 +471,8 @@ async fn plugin_mcp_tools_are_listed() -> Result<()> {
|
||||
let mut available_tools: Vec<&str> = tool_list.tools.keys().map(String::as_str).collect();
|
||||
available_tools.sort_unstable();
|
||||
assert!(
|
||||
tool_list.tools.contains_key("mcp__sample__echo")
|
||||
&& tool_list.tools.contains_key("mcp__sample__image"),
|
||||
tool_list.tools.contains_key("sample__echo")
|
||||
&& tool_list.tools.contains_key("sample__image"),
|
||||
"expected plugin MCP tools to be listed; discovered tools: {available_tools:?}"
|
||||
);
|
||||
|
||||
|
||||
@@ -336,7 +336,7 @@ async fn call_cwd_tool(
|
||||
server_name: &str,
|
||||
call_id: &str,
|
||||
) -> anyhow::Result<Value> {
|
||||
let namespace = format!("mcp__{server_name}__");
|
||||
let namespace = server_name.to_string();
|
||||
mount_sse_once(
|
||||
server,
|
||||
responses::sse(vec![
|
||||
@@ -421,7 +421,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> {
|
||||
|
||||
let call_id = "call-123";
|
||||
let server_name = "rmcp";
|
||||
let namespace = format!("mcp__{server_name}__");
|
||||
let namespace = server_name.to_string();
|
||||
|
||||
let call_mock = mount_sse_once(
|
||||
&server,
|
||||
@@ -730,7 +730,7 @@ async fn stdio_mcp_tool_call_includes_sandbox_state_meta() -> anyhow::Result<()>
|
||||
|
||||
let call_id = "sandbox-meta-call";
|
||||
let server_name = "rmcp";
|
||||
let namespace = format!("mcp__{server_name}__");
|
||||
let namespace = server_name.to_string();
|
||||
let tool_name = format!("{namespace}sandbox_meta");
|
||||
|
||||
let call_mock = mount_sse_once(
|
||||
@@ -848,7 +848,7 @@ async fn stdio_mcp_parallel_tool_calls_default_false_runs_serially() -> anyhow::
|
||||
let first_call_id = "sync-serial-1";
|
||||
let second_call_id = "sync-serial-2";
|
||||
let server_name = "rmcp";
|
||||
let namespace = format!("mcp__{server_name}__");
|
||||
let namespace = server_name.to_string();
|
||||
let args = json!({ "sleep_after_ms": 100 }).to_string();
|
||||
|
||||
mount_sse_once(
|
||||
@@ -957,7 +957,7 @@ async fn stdio_mcp_parallel_tool_calls_opt_in_runs_concurrently() -> anyhow::Res
|
||||
let first_call_id = "sync-1";
|
||||
let second_call_id = "sync-2";
|
||||
let server_name = "rmcp";
|
||||
let namespace = format!("mcp__{server_name}__");
|
||||
let namespace = server_name.to_string();
|
||||
let args = json!({
|
||||
"sleep_after_ms": 100,
|
||||
"barrier": {
|
||||
@@ -1039,8 +1039,8 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> {
|
||||
|
||||
let call_id = "img-1";
|
||||
let server_name = "rmcp";
|
||||
let tool_name = format!("mcp__{server_name}__image");
|
||||
let namespace = format!("mcp__{server_name}__");
|
||||
let tool_name = format!("{server_name}__image");
|
||||
let namespace = server_name.to_string();
|
||||
|
||||
// First stream: model decides to call the image tool.
|
||||
mount_sse_once(
|
||||
@@ -1176,8 +1176,8 @@ async fn stdio_image_responses_preserve_original_detail_metadata() -> anyhow::Re
|
||||
|
||||
let call_id = "img-original-detail-1";
|
||||
let server_name = "rmcp";
|
||||
let tool_name = format!("mcp__{server_name}__image_scenario");
|
||||
let namespace = format!("mcp__{server_name}__");
|
||||
let tool_name = format!("{server_name}__image_scenario");
|
||||
let namespace = server_name.to_string();
|
||||
|
||||
mount_sse_once(
|
||||
&server,
|
||||
@@ -1263,7 +1263,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
|
||||
|
||||
let call_id = "img-text-only-1";
|
||||
let server_name = "rmcp";
|
||||
let namespace = format!("mcp__{server_name}__");
|
||||
let namespace = server_name.to_string();
|
||||
let text_only_model_slug = "rmcp-text-only-model";
|
||||
|
||||
let models_mock = mount_models_once(
|
||||
@@ -1408,7 +1408,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> {
|
||||
|
||||
let call_id = "call-1234";
|
||||
let server_name = "rmcp_whitelist";
|
||||
let namespace = format!("mcp__{server_name}__");
|
||||
let namespace = server_name.to_string();
|
||||
|
||||
mount_sse_once(
|
||||
&server,
|
||||
@@ -1522,7 +1522,7 @@ async fn stdio_server_propagates_explicit_local_env_var_source() -> anyhow::Resu
|
||||
let server = responses::start_mock_server().await;
|
||||
let call_id = "call-local-source";
|
||||
let server_name = "rmcp_local_source";
|
||||
let namespace = format!("mcp__{server_name}__");
|
||||
let namespace = server_name.to_string();
|
||||
let env_name = "MCP_TEST_LOCAL_SOURCE";
|
||||
let expected_env_value = "propagated-explicit-local-source";
|
||||
|
||||
@@ -1615,7 +1615,7 @@ async fn remote_stdio_env_var_source_does_not_copy_local_env() -> anyhow::Result
|
||||
let server = responses::start_mock_server().await;
|
||||
let call_id = "call-remote-source";
|
||||
let server_name = "rmcp_remote_source";
|
||||
let namespace = format!("mcp__{server_name}__");
|
||||
let namespace = server_name.to_string();
|
||||
let env_name = "MCP_TEST_REMOTE_SOURCE_ONLY";
|
||||
|
||||
mount_sse_once(
|
||||
@@ -1787,7 +1787,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> {
|
||||
|
||||
let call_id = "call-456";
|
||||
let server_name = "rmcp_http";
|
||||
let namespace = format!("mcp__{server_name}__");
|
||||
let namespace = server_name.to_string();
|
||||
|
||||
mount_sse_once(
|
||||
&server,
|
||||
@@ -1954,8 +1954,8 @@ async fn streamable_http_with_oauth_round_trip_impl() -> anyhow::Result<()> {
|
||||
|
||||
let call_id = "call-789";
|
||||
let server_name = "rmcp_http_oauth";
|
||||
let tool_name = format!("mcp__{server_name}__echo");
|
||||
let namespace = format!("mcp__{server_name}__");
|
||||
let tool_name = format!("{server_name}__echo");
|
||||
let namespace = server_name.to_string();
|
||||
|
||||
mount_sse_once(
|
||||
&server,
|
||||
|
||||
@@ -47,11 +47,11 @@ const SEARCH_TOOL_DESCRIPTION_SNIPPETS: [&str; 2] = [
|
||||
"- Calendar: Plan events and manage your calendar.",
|
||||
];
|
||||
const TOOL_SEARCH_TOOL_NAME: &str = "tool_search";
|
||||
const CALENDAR_CREATE_TOOL: &str = "mcp__codex_apps__calendar_create_event";
|
||||
const CALENDAR_LIST_TOOL: &str = "mcp__codex_apps__calendar_list_events";
|
||||
const SEARCH_CALENDAR_NAMESPACE: &str = "mcp__codex_apps__calendar";
|
||||
const SEARCH_CALENDAR_CREATE_TOOL: &str = "_create_event";
|
||||
const SEARCH_CALENDAR_LIST_TOOL: &str = "_list_events";
|
||||
const CALENDAR_CREATE_TOOL: &str = "codex_apps__calendar__create_event";
|
||||
const CALENDAR_LIST_TOOL: &str = "codex_apps__calendar__list_events";
|
||||
const SEARCH_CALENDAR_NAMESPACE: &str = "codex_apps__calendar";
|
||||
const SEARCH_CALENDAR_CREATE_TOOL: &str = "create_event";
|
||||
const SEARCH_CALENDAR_LIST_TOOL: &str = "list_events";
|
||||
|
||||
fn tool_names(body: &Value) -> Vec<String> {
|
||||
body.get("tools")
|
||||
@@ -235,7 +235,7 @@ async fn always_defer_feature_hides_small_app_tool_sets() -> Result<()> {
|
||||
"small app tool sets should be deferred behind tool_search: {tools:?}"
|
||||
);
|
||||
assert!(
|
||||
tools.iter().all(|name| !name.starts_with("mcp__")),
|
||||
tools.iter().all(|name| !name.starts_with("codex_apps__")),
|
||||
"MCP tools should not be directly exposed: {tools:?}"
|
||||
);
|
||||
|
||||
@@ -996,19 +996,17 @@ async fn tool_search_indexes_only_enabled_non_app_mcp_tools() -> Result<()> {
|
||||
"first request should advertise tool_search: {first_request_tools:?}"
|
||||
);
|
||||
assert!(
|
||||
!first_request_tools
|
||||
.iter()
|
||||
.any(|name| name == "mcp__rmcp__echo"),
|
||||
!first_request_tools.iter().any(|name| name == "rmcp__echo"),
|
||||
"non-app MCP tools should be hidden before search in large-search mode: {first_request_tools:?}"
|
||||
);
|
||||
assert!(
|
||||
!first_request_tools.iter().any(|name| name == "mcp__rmcp__"),
|
||||
!first_request_tools.iter().any(|name| name == "rmcp"),
|
||||
"non-app MCP namespace should be hidden before search in large-search mode: {first_request_tools:?}"
|
||||
);
|
||||
|
||||
let echo_tools = tool_search_output_tools(&requests[1], echo_call_id);
|
||||
let echo_output = json!({ "tools": echo_tools });
|
||||
let rmcp_echo_tool = namespace_child_tool(&echo_output, "mcp__rmcp__", "echo")
|
||||
let rmcp_echo_tool = namespace_child_tool(&echo_output, "rmcp", "echo")
|
||||
.expect("tool_search should return rmcp echo as a namespace child tool");
|
||||
assert_eq!(
|
||||
rmcp_echo_tool.get("type").and_then(Value::as_str),
|
||||
@@ -1018,7 +1016,7 @@ async fn tool_search_indexes_only_enabled_non_app_mcp_tools() -> Result<()> {
|
||||
let image_tools = tool_search_output_tools(&requests[1], image_call_id);
|
||||
let found_rmcp_image_tool = image_tools
|
||||
.iter()
|
||||
.filter(|tool| tool.get("name").and_then(Value::as_str) == Some("mcp__rmcp__"))
|
||||
.filter(|tool| tool.get("name").and_then(Value::as_str) == Some("rmcp"))
|
||||
.flat_map(|namespace| namespace.get("tools").and_then(Value::as_array))
|
||||
.flatten()
|
||||
.any(|tool| tool.get("name").and_then(Value::as_str).is_some());
|
||||
|
||||
@@ -328,7 +328,7 @@ async fn mcp_call_marks_thread_memory_mode_polluted_when_configured() -> Result<
|
||||
let server = start_mock_server().await;
|
||||
let call_id = "call-123";
|
||||
let server_name = "rmcp";
|
||||
let namespace = format!("mcp__{server_name}__");
|
||||
let namespace = server_name.to_string();
|
||||
mount_sse_once(
|
||||
&server,
|
||||
responses::sse(vec![
|
||||
|
||||
@@ -349,6 +349,184 @@ async fn historical_unavailable_mcp_call_is_exposed_as_placeholder_tool() -> Res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn historical_unavailable_namespaced_call_is_exposed_as_placeholder_tool() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let historical_call_id = "historical-namespaced-call";
|
||||
let retry_call_id = "retry-namespaced-call";
|
||||
let unavailable_tool_namespace = "legacy_tools";
|
||||
let unavailable_tool_name = "missing_tool";
|
||||
let unavailable_tool_display_name = "legacy_tools__missing_tool";
|
||||
let server = start_mock_server().await;
|
||||
let codex_home = Arc::new(TempDir::new()?);
|
||||
let mut builder = test_codex()
|
||||
.with_model("gpt-5.4")
|
||||
.with_home(Arc::clone(&codex_home))
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::UnavailableDummyTools)
|
||||
.expect("unavailable dummy tools should be enabled for this test");
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
let first_turn_mock = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_namespaced_function_call(
|
||||
historical_call_id,
|
||||
unavailable_tool_namespace,
|
||||
unavailable_tool_name,
|
||||
r#"{}"#,
|
||||
),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
test.submit_turn("call a namespaced tool").await?;
|
||||
let rollout_path = test.codex.rollout_path().context("rollout path")?;
|
||||
assert_eq!(first_turn_mock.requests().len(), 2);
|
||||
drop(test);
|
||||
|
||||
let retry_mock = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-3"),
|
||||
ev_namespaced_function_call(
|
||||
retry_call_id,
|
||||
unavailable_tool_namespace,
|
||||
unavailable_tool_name,
|
||||
r#"{}"#,
|
||||
),
|
||||
ev_completed("resp-3"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-4"),
|
||||
ev_assistant_message("msg-2", "done"),
|
||||
ev_completed("resp-4"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut resume_builder = test_codex().with_model("gpt-5.4").with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::UnavailableDummyTools)
|
||||
.expect("unavailable dummy tools should be enabled for this test");
|
||||
});
|
||||
let test = resume_builder
|
||||
.resume(&server, codex_home, rollout_path)
|
||||
.await?;
|
||||
|
||||
test.submit_turn("retry the namespaced tool").await?;
|
||||
|
||||
let requests = retry_mock.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
let first_request_tools = tool_names(&requests[0].body_json());
|
||||
assert!(
|
||||
first_request_tools
|
||||
.iter()
|
||||
.any(|name| name == unavailable_tool_display_name),
|
||||
"historical unavailable namespaced call should add a placeholder tool; got {first_request_tools:?}"
|
||||
);
|
||||
let output_text = requests[1]
|
||||
.function_call_output_text(retry_call_id)
|
||||
.context("placeholder tool output present")?;
|
||||
assert!(output_text.contains("not currently available"));
|
||||
assert!(
|
||||
!output_text.contains("unsupported call"),
|
||||
"placeholder handler should answer instead of falling back to unsupported call: {output_text}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[serial(mcp_test_value)]
|
||||
async fn reserved_mcp_namespace_is_not_exposed() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let response_mock = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let rmcp_test_server_bin = match stdio_server_bin() {
|
||||
Ok(bin) => bin,
|
||||
Err(err) => {
|
||||
eprintln!("test_stdio_server binary not available, skipping test: {err}");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let mut builder = test_codex().with_config(move |config| {
|
||||
let mut servers = config.mcp_servers.get().clone();
|
||||
for server_name in ["rmcp", "tools"] {
|
||||
servers.insert(
|
||||
server_name.to_string(),
|
||||
McpServerConfig {
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: rmcp_test_server_bin.clone(),
|
||||
args: Vec::new(),
|
||||
env: Some(HashMap::new()),
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: Some(Duration::from_secs(10)),
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
enabled_tools: Some(vec!["echo".to_string()]),
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
tools: HashMap::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
config
|
||||
.mcp_servers
|
||||
.set(servers)
|
||||
.expect("test mcp servers should accept any configuration");
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("which tools are available?").await?;
|
||||
|
||||
let tools = tool_names(&response_mock.single_request().body_json());
|
||||
assert!(
|
||||
tools.iter().any(|name| name == "rmcp"),
|
||||
"non-reserved MCP namespace should remain visible; got {tools:?}"
|
||||
);
|
||||
assert!(
|
||||
!tools.iter().any(|name| name == "tools"),
|
||||
"reserved MCP namespace should be skipped; got {tools:?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn shell_escalated_permissions_rejected_then_ok() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
@@ -343,7 +343,7 @@ async fn mcp_tool_call_output_exceeds_limit_truncated_for_model() -> Result<()>
|
||||
|
||||
let call_id = "rmcp-truncated";
|
||||
let server_name = "rmcp";
|
||||
let namespace = format!("mcp__{server_name}__");
|
||||
let namespace = server_name.to_string();
|
||||
|
||||
// Build a very large message to exceed 10KiB once serialized.
|
||||
let large_msg = "long-message-with-newlines-".repeat(6000);
|
||||
@@ -445,7 +445,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> {
|
||||
|
||||
let call_id = "rmcp-image-no-trunc";
|
||||
let server_name = "rmcp";
|
||||
let namespace = format!("mcp__{server_name}__");
|
||||
let namespace = server_name.to_string();
|
||||
|
||||
mount_sse_once(
|
||||
&server,
|
||||
@@ -726,7 +726,7 @@ async fn mcp_tool_call_output_not_truncated_with_custom_limit() -> Result<()> {
|
||||
|
||||
let call_id = "rmcp-untruncated";
|
||||
let server_name = "rmcp";
|
||||
let namespace = format!("mcp__{server_name}__");
|
||||
let namespace = server_name.to_string();
|
||||
let large_msg = "a".repeat(80_000);
|
||||
let args_json = serde_json::json!({ "message": large_msg });
|
||||
|
||||
|
||||
@@ -2090,8 +2090,8 @@ mod tests {
|
||||
fn function_call_deserializes_optional_namespace() {
|
||||
let item: ResponseItem = serde_json::from_value(serde_json::json!({
|
||||
"type": "function_call",
|
||||
"name": "mcp__codex_apps__gmail_get_recent_emails",
|
||||
"namespace": "mcp__codex_apps__gmail",
|
||||
"name": "get_recent_emails",
|
||||
"namespace": "codex_apps__gmail",
|
||||
"arguments": "{\"top_k\":5}",
|
||||
"call_id": "call-1",
|
||||
}))
|
||||
@@ -2101,8 +2101,8 @@ mod tests {
|
||||
item,
|
||||
ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
name: "mcp__codex_apps__gmail_get_recent_emails".to_string(),
|
||||
namespace: Some("mcp__codex_apps__gmail".to_string()),
|
||||
name: "get_recent_emails".to_string(),
|
||||
namespace: Some("codex_apps__gmail".to_string()),
|
||||
arguments: "{\"top_k\":5}".to_string(),
|
||||
call_id: "call-1".to_string(),
|
||||
}
|
||||
@@ -2616,7 +2616,7 @@ mod tests {
|
||||
execution: "client".to_string(),
|
||||
tools: vec![serde_json::json!({
|
||||
"type": "function",
|
||||
"name": "mcp__codex_apps__calendar_create_event",
|
||||
"name": "codex_apps__calendar__create_event",
|
||||
"description": "Create a calendar event.",
|
||||
"defer_loading": true,
|
||||
"parameters": {
|
||||
@@ -2637,7 +2637,7 @@ mod tests {
|
||||
execution: "client".to_string(),
|
||||
tools: vec![serde_json::json!({
|
||||
"type": "function",
|
||||
"name": "mcp__codex_apps__calendar_create_event",
|
||||
"name": "codex_apps__calendar__create_event",
|
||||
"description": "Create a calendar event.",
|
||||
"defer_loading": true,
|
||||
"parameters": {
|
||||
@@ -2661,7 +2661,7 @@ mod tests {
|
||||
"execution": "client",
|
||||
"tools": [{
|
||||
"type": "function",
|
||||
"name": "mcp__codex_apps__calendar_create_event",
|
||||
"name": "codex_apps__calendar__create_event",
|
||||
"description": "Create a calendar event.",
|
||||
"defer_loading": true,
|
||||
"parameters": {
|
||||
|
||||
@@ -34,7 +34,7 @@ impl ToolName {
|
||||
|
||||
pub fn display(&self) -> String {
|
||||
match &self.namespace {
|
||||
Some(namespace) => format!("{namespace}{}", self.name),
|
||||
Some(namespace) => flatten_namespaced_tool_name(namespace, &self.name),
|
||||
None => self.name.clone(),
|
||||
}
|
||||
}
|
||||
@@ -43,12 +43,20 @@ impl ToolName {
|
||||
impl fmt::Display for ToolName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self.namespace {
|
||||
Some(namespace) => write!(f, "{namespace}{}", self.name),
|
||||
Some(namespace) => f.write_str(&flatten_namespaced_tool_name(namespace, &self.name)),
|
||||
None => f.write_str(&self.name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flatten_namespaced_tool_name(namespace: &str, name: &str) -> String {
|
||||
if namespace.ends_with("__") || name.starts_with('_') {
|
||||
format!("{namespace}{name}")
|
||||
} else {
|
||||
format!("{namespace}__{name}")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ToolName {
|
||||
fn from(name: String) -> Self {
|
||||
Self::plain(name)
|
||||
@@ -60,3 +68,25 @@ impl From<&str> for ToolName {
|
||||
Self::plain(name)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ToolName;
|
||||
|
||||
#[test]
|
||||
fn display_flattens_namespaced_tools_with_double_underscore_separator() {
|
||||
assert_eq!(ToolName::namespaced("foo_", "bar").display(), "foo___bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_preserves_legacy_flattened_namespaced_tools() {
|
||||
assert_eq!(
|
||||
ToolName::namespaced("mcp__rmcp__", "echo").display(),
|
||||
"mcp__rmcp__echo"
|
||||
);
|
||||
assert_eq!(
|
||||
ToolName::namespaced("mcp__codex_apps__calendar", "_create_event").display(),
|
||||
"mcp__codex_apps__calendar_create_event"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ fn mcp_tool_to_deferred_responses_api_tool_sets_defer_loading() {
|
||||
|
||||
assert_eq!(
|
||||
mcp_tool_to_deferred_responses_api_tool(
|
||||
&ToolName::namespaced("mcp__codex_apps__", "lookup_order"),
|
||||
&ToolName::namespaced("codex_apps", "lookup_order"),
|
||||
&tool,
|
||||
)
|
||||
.expect("convert deferred tool"),
|
||||
@@ -133,7 +133,7 @@ fn mcp_tool_to_deferred_responses_api_tool_sets_defer_loading() {
|
||||
#[test]
|
||||
fn loadable_tool_spec_namespace_serializes_with_deferred_child_tools() {
|
||||
let namespace = LoadableToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: "mcp__codex_apps__calendar".to_string(),
|
||||
name: "codex_apps__calendar".to_string(),
|
||||
description: "Plan events".to_string(),
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
||||
name: "create_event".to_string(),
|
||||
@@ -155,7 +155,7 @@ fn loadable_tool_spec_namespace_serializes_with_deferred_child_tools() {
|
||||
value,
|
||||
json!({
|
||||
"type": "namespace",
|
||||
"name": "mcp__codex_apps__calendar",
|
||||
"name": "codex_apps__calendar",
|
||||
"description": "Plan events",
|
||||
"tools": [
|
||||
{
|
||||
|
||||
@@ -22,9 +22,9 @@ fn tool_definition() -> ToolDefinition {
|
||||
#[test]
|
||||
fn renamed_overrides_name_only() {
|
||||
assert_eq!(
|
||||
tool_definition().renamed("mcp__orders__lookup_order".to_string()),
|
||||
tool_definition().renamed("orders__lookup_order".to_string()),
|
||||
ToolDefinition {
|
||||
name: "mcp__orders__lookup_order".to_string(),
|
||||
name: "orders__lookup_order".to_string(),
|
||||
..tool_definition()
|
||||
}
|
||||
);
|
||||
|
||||
@@ -68,6 +68,7 @@ use crate::tool_registry_plan_types::agent_type_description;
|
||||
use codex_protocol::openai_models::ApplyPatchToolType;
|
||||
use codex_protocol::openai_models::ConfigShellToolType;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub fn build_tool_registry_plan(
|
||||
config: &ToolsConfig,
|
||||
@@ -260,53 +261,6 @@ pub fn build_tool_registry_plan(
|
||||
plan.register_handler("request_permissions", ToolHandlerKind::RequestPermissions);
|
||||
}
|
||||
|
||||
let deferred_dynamic_tools = params
|
||||
.dynamic_tools
|
||||
.iter()
|
||||
.filter(|tool| tool.defer_loading && (config.namespace_tools || tool.namespace.is_none()))
|
||||
.collect::<Vec<_>>();
|
||||
let deferred_mcp_tools_for_search = if config.namespace_tools {
|
||||
params.deferred_mcp_tools
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if config.search_tool
|
||||
&& (deferred_mcp_tools_for_search.is_some() || !deferred_dynamic_tools.is_empty())
|
||||
{
|
||||
let mut search_source_infos = deferred_mcp_tools_for_search
|
||||
.map(|deferred_mcp_tools| {
|
||||
collect_tool_search_source_infos(deferred_mcp_tools.iter().map(|tool| {
|
||||
ToolSearchSource {
|
||||
server_name: tool.server_name,
|
||||
connector_name: tool.connector_name,
|
||||
connector_description: tool.connector_description,
|
||||
}
|
||||
}))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if !deferred_dynamic_tools.is_empty() {
|
||||
search_source_infos.push(ToolSearchSourceInfo {
|
||||
name: "Dynamic tools".to_string(),
|
||||
description: Some("Tools provided by the current Codex thread.".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
plan.push_spec(
|
||||
create_tool_search_tool(&search_source_infos, TOOL_SEARCH_DEFAULT_LIMIT),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
plan.register_handler(TOOL_SEARCH_TOOL_NAME, ToolHandlerKind::ToolSearch);
|
||||
|
||||
if let Some(deferred_mcp_tools) = deferred_mcp_tools_for_search {
|
||||
for tool in deferred_mcp_tools {
|
||||
plan.register_handler(tool.name.clone(), ToolHandlerKind::Mcp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if config.tool_suggest
|
||||
&& let Some(discoverable_tools) =
|
||||
params.discoverable_tools.filter(|tools| !tools.is_empty())
|
||||
@@ -506,8 +460,96 @@ pub fn build_tool_registry_plan(
|
||||
}
|
||||
}
|
||||
|
||||
let deferred_dynamic_tools = params
|
||||
.dynamic_tools
|
||||
.iter()
|
||||
.filter(|tool| tool.defer_loading && (config.namespace_tools || tool.namespace.is_none()))
|
||||
.collect::<Vec<_>>();
|
||||
let mut claimed_non_mcp_top_level_names =
|
||||
claimed_non_mcp_top_level_names(&plan, params.dynamic_tools);
|
||||
if config.search_tool
|
||||
&& (params.deferred_mcp_tools.is_some() || !deferred_dynamic_tools.is_empty())
|
||||
{
|
||||
claimed_non_mcp_top_level_names.insert(TOOL_SEARCH_TOOL_NAME.to_string());
|
||||
}
|
||||
let mut skipped_mcp_namespaces = HashSet::new();
|
||||
let mut should_expose_mcp_namespace = |namespace: &str| {
|
||||
if reserved_mcp_namespace(namespace) || claimed_non_mcp_top_level_names.contains(namespace)
|
||||
{
|
||||
if skipped_mcp_namespaces.insert(namespace.to_string()) {
|
||||
tracing::warn!(
|
||||
"skipping MCP namespace `{namespace}` because that top-level tool name is reserved or already claimed"
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
true
|
||||
};
|
||||
|
||||
let deferred_mcp_tools_for_search = if config.namespace_tools {
|
||||
params.deferred_mcp_tools.map(|tools| {
|
||||
tools
|
||||
.iter()
|
||||
.filter(|tool| {
|
||||
tool.name
|
||||
.namespace
|
||||
.as_deref()
|
||||
.is_some_and(&mut should_expose_mcp_namespace)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if config.search_tool
|
||||
&& (deferred_mcp_tools_for_search.is_some() || !deferred_dynamic_tools.is_empty())
|
||||
{
|
||||
let mut search_source_infos = deferred_mcp_tools_for_search
|
||||
.as_ref()
|
||||
.map(|deferred_mcp_tools| {
|
||||
collect_tool_search_source_infos(deferred_mcp_tools.iter().map(|tool| {
|
||||
ToolSearchSource {
|
||||
server_name: tool.server_name,
|
||||
connector_name: tool.connector_name,
|
||||
connector_description: tool.connector_description,
|
||||
}
|
||||
}))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if !deferred_dynamic_tools.is_empty() {
|
||||
search_source_infos.push(ToolSearchSourceInfo {
|
||||
name: "Dynamic tools".to_string(),
|
||||
description: Some("Tools provided by the current Codex thread.".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
plan.push_spec(
|
||||
create_tool_search_tool(&search_source_infos, TOOL_SEARCH_DEFAULT_LIMIT),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
plan.register_handler(TOOL_SEARCH_TOOL_NAME, ToolHandlerKind::ToolSearch);
|
||||
|
||||
if let Some(deferred_mcp_tools) = deferred_mcp_tools_for_search.as_ref() {
|
||||
for tool in deferred_mcp_tools {
|
||||
plan.register_handler(tool.name.clone(), ToolHandlerKind::Mcp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mcp_tools) = params.mcp_tools {
|
||||
let mut entries = mcp_tools.to_vec();
|
||||
let mut entries = mcp_tools
|
||||
.iter()
|
||||
.filter(|tool| {
|
||||
tool.name
|
||||
.namespace
|
||||
.as_deref()
|
||||
.is_some_and(&mut should_expose_mcp_namespace)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
entries.sort_by_key(|tool| tool.name.display());
|
||||
let mut namespace_entries = BTreeMap::new();
|
||||
|
||||
@@ -615,6 +657,25 @@ fn compare_code_mode_tools(
|
||||
.then_with(|| left.name.cmp(&right.name))
|
||||
}
|
||||
|
||||
fn claimed_non_mcp_top_level_names(
|
||||
plan: &ToolRegistryPlan,
|
||||
dynamic_tools: &[codex_protocol::dynamic_tools::DynamicToolSpec],
|
||||
) -> HashSet<String> {
|
||||
plan.specs
|
||||
.iter()
|
||||
.map(|tool| tool.name().to_string())
|
||||
.chain(
|
||||
dynamic_tools
|
||||
.iter()
|
||||
.map(|tool| tool.namespace.clone().unwrap_or_else(|| tool.name.clone())),
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn reserved_mcp_namespace(namespace: &str) -> bool {
|
||||
matches!(namespace, "functions" | "tools" | "web")
|
||||
}
|
||||
|
||||
fn code_mode_namespace_name<'a>(
|
||||
tool: &codex_code_mode::ToolDefinition,
|
||||
namespace_descriptions: &'a BTreeMap<String, codex_code_mode::ToolNamespaceDescription>,
|
||||
|
||||
@@ -1205,16 +1205,16 @@ fn namespace_specs_are_hidden_when_namespace_tools_are_disabled() {
|
||||
let (tools, handlers) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
ToolName::namespaced("mcp__sample__", "echo"),
|
||||
ToolName::namespaced("sample", "echo"),
|
||||
mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})),
|
||||
)])),
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
);
|
||||
|
||||
assert_lacks_tool_name(&tools, "mcp__sample__");
|
||||
assert_lacks_tool_name(&tools, "sample");
|
||||
assert!(handlers.contains(&ToolHandlerSpec {
|
||||
name: ToolName::namespaced("mcp__sample__", "echo"),
|
||||
name: ToolName::namespaced("sample", "echo"),
|
||||
kind: ToolHandlerKind::Mcp,
|
||||
}));
|
||||
}
|
||||
@@ -1374,7 +1374,7 @@ fn search_tool_description_lists_each_mcp_source_once() {
|
||||
&tools_config,
|
||||
Some(HashMap::from([
|
||||
(
|
||||
ToolName::namespaced("mcp__codex_apps__calendar", "_create_event"),
|
||||
ToolName::namespaced("codex_apps__calendar", "create_event"),
|
||||
mcp_tool(
|
||||
"calendar_create_event",
|
||||
"Create calendar event",
|
||||
@@ -1382,37 +1382,34 @@ fn search_tool_description_lists_each_mcp_source_once() {
|
||||
),
|
||||
),
|
||||
(
|
||||
ToolName::namespaced("mcp__rmcp__", "echo"),
|
||||
ToolName::namespaced("rmcp", "echo"),
|
||||
mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})),
|
||||
),
|
||||
])),
|
||||
Some(vec![
|
||||
deferred_mcp_tool(
|
||||
"_create_event",
|
||||
"mcp__codex_apps__calendar",
|
||||
"create_event",
|
||||
"codex_apps__calendar",
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some("Calendar"),
|
||||
Some("Plan events and manage your calendar."),
|
||||
),
|
||||
deferred_mcp_tool(
|
||||
"_list_events",
|
||||
"mcp__codex_apps__calendar",
|
||||
"list_events",
|
||||
"codex_apps__calendar",
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some("Calendar"),
|
||||
Some("Plan events and manage your calendar."),
|
||||
),
|
||||
deferred_mcp_tool(
|
||||
"_search_threads",
|
||||
"mcp__codex_apps__gmail",
|
||||
"search_threads",
|
||||
"codex_apps__gmail",
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some("Gmail"),
|
||||
Some("Find and summarize email threads."),
|
||||
),
|
||||
deferred_mcp_tool(
|
||||
"echo",
|
||||
"mcp__rmcp__",
|
||||
"rmcp",
|
||||
/*connector_name*/ None,
|
||||
"echo", "rmcp", "rmcp", /*connector_name*/ None,
|
||||
/*connector_description*/ None,
|
||||
),
|
||||
]),
|
||||
@@ -1433,14 +1430,14 @@ fn search_tool_description_lists_each_mcp_source_once() {
|
||||
1
|
||||
);
|
||||
assert!(description.contains("- rmcp"));
|
||||
assert!(!description.contains("mcp__rmcp__echo"));
|
||||
assert!(!description.contains("rmcp__echo"));
|
||||
|
||||
assert!(handlers.contains(&ToolHandlerSpec {
|
||||
name: ToolName::namespaced("mcp__codex_apps__calendar", "_create_event"),
|
||||
name: ToolName::namespaced("codex_apps__calendar", "create_event"),
|
||||
kind: ToolHandlerKind::Mcp,
|
||||
}));
|
||||
assert!(handlers.contains(&ToolHandlerSpec {
|
||||
name: ToolName::namespaced("mcp__rmcp__", "echo"),
|
||||
name: ToolName::namespaced("rmcp", "echo"),
|
||||
kind: ToolHandlerKind::Mcp,
|
||||
}));
|
||||
}
|
||||
@@ -1449,8 +1446,8 @@ fn search_tool_description_lists_each_mcp_source_once() {
|
||||
fn search_tool_requires_model_capability_and_enabled_feature() {
|
||||
let model_info = search_capable_model_info();
|
||||
let deferred_mcp_tools = Some(vec![deferred_mcp_tool(
|
||||
"_create_event",
|
||||
"mcp__codex_apps__calendar",
|
||||
"create_event",
|
||||
"codex_apps__calendar",
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some("Calendar"),
|
||||
/*connector_description*/ None,
|
||||
@@ -1540,8 +1537,8 @@ fn search_tool_is_hidden_when_only_deferred_namespace_tools_are_available() {
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
Some(vec![deferred_mcp_tool(
|
||||
"_create_event",
|
||||
"mcp__codex_apps__calendar",
|
||||
"create_event",
|
||||
"codex_apps__calendar",
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some("Calendar"),
|
||||
Some("Plan events and manage your calendar."),
|
||||
@@ -1691,6 +1688,53 @@ fn search_tool_keeps_plain_deferred_dynamic_tools_when_namespace_tools_are_disab
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_namespace_colliding_with_dynamic_namespace_is_skipped() {
|
||||
let model_info = model_info();
|
||||
let features = Features::with_defaults();
|
||||
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,
|
||||
permission_profile: &PermissionProfile::Disabled,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
});
|
||||
let dynamic_tools = vec![DynamicToolSpec {
|
||||
namespace: Some("shared".to_string()),
|
||||
name: "automation_update".to_string(),
|
||||
description: "Create or update automations.".to_string(),
|
||||
input_schema: json!({"type": "object", "properties": {}}),
|
||||
defer_loading: false,
|
||||
}];
|
||||
|
||||
let (tools, handlers) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
ToolName::namespaced("shared", "echo"),
|
||||
mcp_tool("echo", "Echo", json!({"type": "object"})),
|
||||
)])),
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&dynamic_tools,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
namespace_function_names(&tools, "shared"),
|
||||
vec!["automation_update".to_string()]
|
||||
);
|
||||
assert!(!handlers.contains(&ToolHandlerSpec {
|
||||
name: ToolName::namespaced("shared", "echo"),
|
||||
kind: ToolHandlerKind::Mcp,
|
||||
}));
|
||||
assert!(handlers.contains(&ToolHandlerSpec {
|
||||
name: ToolName::namespaced("shared", "automation_update"),
|
||||
kind: ToolHandlerKind::DynamicTool,
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_suggest_is_not_registered_without_feature_flag() {
|
||||
let model_info = search_capable_model_info();
|
||||
@@ -1916,7 +1960,7 @@ fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() {
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
ToolName::namespaced("mcp__sample__", "echo"),
|
||||
ToolName::namespaced("sample", "echo"),
|
||||
mcp_tool(
|
||||
"echo",
|
||||
"Echo text",
|
||||
@@ -1935,7 +1979,7 @@ fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() {
|
||||
);
|
||||
|
||||
let ResponsesApiTool { description, .. } =
|
||||
find_namespace_function_tool(&tools, "mcp__sample__", "echo");
|
||||
find_namespace_function_tool(&tools, "sample", "echo");
|
||||
|
||||
assert_eq!(
|
||||
description,
|
||||
@@ -1943,7 +1987,7 @@ fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() {
|
||||
|
||||
exec tool declaration:
|
||||
```ts
|
||||
declare const tools: { mcp__sample__echo(args: { message: string; }): Promise<CallToolResult>; };
|
||||
declare const tools: { sample_echo(args: { message: string; }): Promise<CallToolResult>; };
|
||||
```"#
|
||||
);
|
||||
}
|
||||
@@ -1969,7 +2013,7 @@ fn code_mode_preserves_nullable_and_literal_mcp_input_shapes() {
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
ToolName::namespaced("mcp__sample__", "fn"),
|
||||
ToolName::namespaced("sample", "fn"),
|
||||
mcp_tool(
|
||||
"fn",
|
||||
"Sample fn",
|
||||
@@ -2020,13 +2064,12 @@ fn code_mode_preserves_nullable_and_literal_mcp_input_shapes() {
|
||||
&[],
|
||||
);
|
||||
|
||||
let ResponsesApiTool { description, .. } =
|
||||
find_namespace_function_tool(&tools, "mcp__sample__", "fn");
|
||||
let ResponsesApiTool { description, .. } = find_namespace_function_tool(&tools, "sample", "fn");
|
||||
|
||||
assert!(description.contains(
|
||||
r#"exec tool declaration:
|
||||
```ts
|
||||
declare const tools: { mcp__sample__fn(args: { open?: Array<{ lineno?: number | null; ref_id: string; }> | null; response_length?: "short" | "medium" | "long"; tagged_list?: Array<{ kind: "tagged"; scope: "one" | "two"; variant: "alpha" | "beta"; }> | null; }): Promise<CallToolResult>; };
|
||||
declare const tools: { sample_fn(args: { open?: Array<{ lineno?: number | null; ref_id: string; }> | null; response_length?: "short" | "medium" | "long"; tagged_list?: Array<{ kind: "tagged"; scope: "one" | "two"; variant: "alpha" | "beta"; }> | null; }): Promise<CallToolResult>; };
|
||||
```"#
|
||||
));
|
||||
}
|
||||
@@ -2311,7 +2354,7 @@ fn code_mode_augments_mcp_tool_descriptions_with_structured_output_sample() {
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
ToolName::namespaced("mcp__sample__", "echo"),
|
||||
ToolName::namespaced("sample", "echo"),
|
||||
tool,
|
||||
)])),
|
||||
/*deferred_mcp_tools*/ None,
|
||||
@@ -2319,7 +2362,7 @@ fn code_mode_augments_mcp_tool_descriptions_with_structured_output_sample() {
|
||||
);
|
||||
|
||||
let ResponsesApiTool { description, .. } =
|
||||
find_namespace_function_tool(&tools, "mcp__sample__", "echo");
|
||||
find_namespace_function_tool(&tools, "sample", "echo");
|
||||
|
||||
assert_eq!(
|
||||
description,
|
||||
@@ -2327,7 +2370,7 @@ fn code_mode_augments_mcp_tool_descriptions_with_structured_output_sample() {
|
||||
|
||||
exec tool declaration:
|
||||
```ts
|
||||
declare const tools: { mcp__sample__echo(args: { message: string; }): Promise<CallToolResult<{ echo: string; env: string | null; }>>; };
|
||||
declare const tools: { sample_echo(args: { message: string; }): Promise<CallToolResult<{ echo: string; env: string | null; }>>; };
|
||||
```"#
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,12 +38,12 @@ fn tool_spec_name_covers_all_variants() {
|
||||
);
|
||||
assert_eq!(
|
||||
ToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: "mcp__demo__".to_string(),
|
||||
name: "demo".to_string(),
|
||||
description: "Demo tools".to_string(),
|
||||
tools: Vec::new(),
|
||||
})
|
||||
.name(),
|
||||
"mcp__demo__"
|
||||
"demo"
|
||||
);
|
||||
assert_eq!(
|
||||
ToolSpec::ToolSearch {
|
||||
@@ -178,7 +178,7 @@ fn create_tools_json_for_responses_api_includes_top_level_name() {
|
||||
fn namespace_tool_spec_serializes_expected_wire_shape() {
|
||||
assert_eq!(
|
||||
serde_json::to_value(ToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: "mcp__demo__".to_string(),
|
||||
name: "demo".to_string(),
|
||||
description: "Demo tools".to_string(),
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
||||
name: "lookup_order".to_string(),
|
||||
@@ -199,7 +199,7 @@ fn namespace_tool_spec_serializes_expected_wire_shape() {
|
||||
.expect("serialize namespace tool"),
|
||||
json!({
|
||||
"type": "namespace",
|
||||
"name": "mcp__demo__",
|
||||
"name": "demo",
|
||||
"description": "Demo tools",
|
||||
"tools": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user