Codex Handbook
core/src/mcp_tool_exposure_test.rs 301 lines
use std::collections::HashSet;use std::sync::Arc;use codex_features::Feature;use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;use codex_mcp::ToolInfo;use codex_tools::ToolName;use pretty_assertions::assert_eq;use rmcp::model::JsonObject;use rmcp::model::Meta;use rmcp::model::Tool;use super::*;use crate::config::CONFIG_TOML_FILE;use crate::config::ConfigBuilder;use crate::config::test_config;use crate::connectors::AppInfo;use tempfile::tempdir;fn make_connector(id: &str, name: &str) -> AppInfo {    AppInfo {        id: id.to_string(),        name: name.to_string(),        description: None,        logo_url: None,        logo_url_dark: None,        distribution_channel: None,        branding: None,        app_metadata: None,        labels: None,        install_url: None,        is_accessible: true,        is_enabled: true,        plugin_display_names: Vec::new(),    }}fn make_mcp_tool(    server_name: &str,    tool_name: &str,    callable_namespace: &str,    callable_name: &str,    connector_id: Option<&str>,    connector_name: Option<&str>,) -> ToolInfo {    ToolInfo {        server_name: server_name.to_string(),        supports_parallel_tool_calls: false,        server_origin: None,        callable_name: callable_name.to_string(),        callable_namespace: callable_namespace.to_string(),        namespace_description: None,        tool: Tool::new(            tool_name.to_string(),            format!("Test tool: {tool_name}"),            Arc::new(JsonObject::default()),        ),        connector_id: connector_id.map(str::to_string),        connector_name: connector_name.map(str::to_string),        plugin_display_names: Vec::new(),    }}fn numbered_mcp_tools(count: usize) -> Vec<ToolInfo> {    (0..count)        .map(|index| {            let tool_name = format!("tool_{index}");            make_mcp_tool(                "rmcp",                &tool_name,                "mcp__rmcp",                &tool_name,                /*connector_id*/ None,                /*connector_name*/ None,            )        })        .collect()}fn tool_names(tools: &[ToolInfo]) -> HashSet<ToolName> {    tools        .iter()        .map(codex_mcp::ToolInfo::canonical_tool_name)        .collect()}fn with_visibility(mut tool: ToolInfo, visibility: &[&str]) -> ToolInfo {    tool.tool.meta = Some(Meta(        serde_json::json!({ "ui": { "visibility": visibility } })            .as_object()            .expect("metadata object")            .clone(),    ));    tool}#[tokio::test]async fn directly_exposes_small_effective_tool_sets() {    let config = test_config().await;    let mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD - 1);    let exposure = build_mcp_tool_exposure(        &mcp_tools, /*connectors*/ None, &config, /*search_tool_enabled*/ true,    );    assert_eq!(tool_names(&exposure.direct_tools), tool_names(&mcp_tools));    assert!(exposure.deferred_tools.is_none());}#[tokio::test]async fn excludes_tools_hidden_from_model_exposure() {    let config = test_config().await;    let visible_tool = make_mcp_tool(        "rmcp",        "visible_tool",        "mcp__rmcp",        "visible_tool",        /*connector_id*/ None,        /*connector_name*/ None,    );    let hidden_tool = with_visibility(        make_mcp_tool(            "rmcp",            "hidden_tool",            "mcp__rmcp",            "hidden_tool",            /*connector_id*/ None,            /*connector_name*/ None,        ),        &["app"],    );    let empty_visibility_tool = with_visibility(        make_mcp_tool(            "rmcp",            "empty_visibility_tool",            "mcp__rmcp",            "empty_visibility_tool",            /*connector_id*/ None,            /*connector_name*/ None,        ),        &[],    );    let visible_app_tool = with_visibility(        make_mcp_tool(            CODEX_APPS_MCP_SERVER_NAME,            "calendar_read",            "mcp__codex_apps__calendar",            "read",            Some("calendar"),            Some("Calendar"),        ),        &["app", "model"],    );    let hidden_app_tool = with_visibility(        make_mcp_tool(            CODEX_APPS_MCP_SERVER_NAME,            "calendar_open",            "mcp__codex_apps__calendar",            "open",            Some("calendar"),            Some("Calendar"),        ),        &["app"],    );    let mcp_tools = vec![        visible_tool.clone(),        hidden_tool,        empty_visibility_tool,        visible_app_tool.clone(),        hidden_app_tool,    ];    let connectors = vec![make_connector("calendar", "Calendar")];    let exposure = build_mcp_tool_exposure(        &mcp_tools,        Some(connectors.as_slice()),        &config,        /*search_tool_enabled*/ false,    );    assert_eq!(        tool_names(&exposure.direct_tools),        tool_names(&[visible_tool, visible_app_tool])    );    assert!(exposure.deferred_tools.is_none());}#[tokio::test]async fn applies_per_tool_app_policy_across_the_exposure_build() {    let codex_home = tempdir().expect("tempdir should succeed");    std::fs::write(        codex_home.path().join(CONFIG_TOML_FILE),        r#"[apps.calendar]default_tools_enabled = false[apps.calendar.tools."events/create"]enabled = true"#,    )    .expect("write config");    let config = ConfigBuilder::default()        .codex_home(codex_home.path().to_path_buf())        .build()        .await        .expect("config should build");    let enabled_tool = make_mcp_tool(        CODEX_APPS_MCP_SERVER_NAME,        "events/create",        "mcp__codex_apps__calendar",        "create",        Some("calendar"),        Some("Calendar"),    );    let disabled_tool = make_mcp_tool(        CODEX_APPS_MCP_SERVER_NAME,        "events/list",        "mcp__codex_apps__calendar",        "list",        Some("calendar"),        Some("Calendar"),    );    let connectors = vec![make_connector("calendar", "Calendar")];    let exposure = build_mcp_tool_exposure(        &[enabled_tool.clone(), disabled_tool],        Some(connectors.as_slice()),        &config,        /*search_tool_enabled*/ false,    );    assert_eq!(        tool_names(&exposure.direct_tools),        tool_names(&[enabled_tool])    );    assert!(exposure.deferred_tools.is_none());}#[tokio::test]async fn searches_large_effective_tool_sets() {    let config = test_config().await;    let mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD);    let exposure = build_mcp_tool_exposure(        &mcp_tools, /*connectors*/ None, &config, /*search_tool_enabled*/ true,    );    assert!(exposure.direct_tools.is_empty());    let deferred_tools = exposure        .deferred_tools        .as_ref()        .expect("large tool sets should be discoverable through tool_search");    assert_eq!(tool_names(deferred_tools), tool_names(&mcp_tools));}#[tokio::test]async fn always_defer_feature_defers_apps_too() {    let mut config = test_config().await;    config        .features        .enable(Feature::ToolSearchAlwaysDeferMcpTools)        .expect("test config should allow feature update");    let mcp_tools = vec![        make_mcp_tool(            "rmcp",            "tool",            "mcp__rmcp",            "tool",            /*connector_id*/ None,            /*connector_name*/ None,        ),        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(        &mcp_tools,        Some(connectors.as_slice()),        &config,        /*search_tool_enabled*/ true,    );    assert!(exposure.direct_tools.is_empty());    let deferred_tools = exposure        .deferred_tools        .as_ref()        .expect("MCP tools should be discoverable through tool_search");    let deferred_tool_names = tool_names(deferred_tools);    assert!(deferred_tool_names.contains(&ToolName::namespaced("mcp__rmcp", "tool")));    assert!(deferred_tool_names.contains(&ToolName::namespaced(        "mcp__codex_apps__calendar",        "_create_event"    )));}