Hide app-only MCP tools from model

This commit is contained in:
arun
2026-05-26 19:01:52 +00:00
parent a22706dfae
commit 4948a9d273
3 changed files with 87 additions and 2 deletions
+19
View File
@@ -59,6 +59,22 @@ impl ToolInfo {
pub fn canonical_tool_name(&self) -> ToolName {
ToolName::namespaced(self.callable_namespace.clone(), self.callable_name.clone())
}
/// Returns whether MCP Apps metadata allows this tool to be presented to the model.
pub fn is_model_visible(&self) -> bool {
self.tool
.meta
.as_deref()
.and_then(|meta| meta.get(META_UI))
.and_then(JsonValue::as_object)
.and_then(|ui| ui.get(META_UI_VISIBILITY))
.and_then(JsonValue::as_array)
.is_none_or(|visibility| {
visibility
.iter()
.any(|value| value.as_str() == Some(META_UI_VISIBILITY_MODEL))
})
}
}
pub fn declared_openai_file_input_param_names(
@@ -261,6 +277,9 @@ const MCP_TOOL_NAME_DELIMITER: &str = "__";
const MAX_TOOL_NAME_LENGTH: usize = 64;
const CALLABLE_NAME_HASH_LEN: usize = 12;
const META_OPENAI_FILE_PARAMS: &str = "openai/fileParams";
const META_UI: &str = "ui";
const META_UI_VISIBILITY: &str = "visibility";
const META_UI_VISIBILITY_MODEL: &str = "model";
fn callable_namespace_with_prefix(namespace: &str, prefix_mcp_tool_names: bool) -> String {
if !prefix_mcp_tool_names || namespace.starts_with(LEGACY_MCP_TOOL_NAME_PREFIX) {
+7 -2
View File
@@ -20,10 +20,15 @@ pub(crate) fn build_mcp_tool_exposure(
config: &Config,
search_tool_enabled: bool,
) -> McpToolExposure {
let mut deferred_tools = filter_non_codex_apps_mcp_tools_only(all_mcp_tools);
let model_visible_tools = all_mcp_tools
.iter()
.filter(|tool| tool.is_model_visible())
.cloned()
.collect::<Vec<_>>();
let mut deferred_tools = filter_non_codex_apps_mcp_tools_only(&model_visible_tools);
if let Some(connectors) = connectors {
deferred_tools.extend(filter_codex_apps_mcp_tools(
all_mcp_tools,
&model_visible_tools,
connectors,
config,
));
@@ -7,6 +7,7 @@ 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::*;
@@ -79,6 +80,20 @@ fn numbered_mcp_tools(count: usize) -> Vec<ToolInfo> {
.collect()
}
fn with_ui_visibility(mut tool: ToolInfo, visibility: &[&str]) -> ToolInfo {
tool.tool.meta = Some(Meta(
serde_json::json!({
"ui": {
"visibility": visibility,
},
})
.as_object()
.expect("object")
.clone(),
));
tool
}
fn tool_names(tools: &[ToolInfo]) -> HashSet<ToolName> {
tools
.iter()
@@ -86,6 +101,52 @@ fn tool_names(tools: &[ToolInfo]) -> HashSet<ToolName> {
.collect()
}
#[tokio::test]
async fn hides_app_only_tools_from_model_exposure() {
let config = test_config().await;
let public_tool = make_mcp_tool(
"rmcp",
"public_tool",
"mcp__rmcp",
"public_tool",
/*connector_id*/ None,
/*connector_name*/ None,
);
let app_only_tool = with_ui_visibility(
make_mcp_tool(
"rmcp",
"app_only_tool",
"mcp__rmcp",
"app_only_tool",
/*connector_id*/ None,
/*connector_name*/ None,
),
&["app"],
);
let model_only_tool = with_ui_visibility(
make_mcp_tool(
"rmcp",
"model_only_tool",
"mcp__rmcp",
"model_only_tool",
/*connector_id*/ None,
/*connector_name*/ None,
),
&["model"],
);
let mcp_tools = vec![public_tool.clone(), app_only_tool, model_only_tool.clone()];
let exposure = build_mcp_tool_exposure(
&mcp_tools, /*connectors*/ None, &config, /*search_tool_enabled*/ true,
);
assert_eq!(
tool_names(&exposure.direct_tools),
tool_names(&[public_tool, model_only_tool])
);
assert!(exposure.deferred_tools.is_none());
}
#[tokio::test]
async fn directly_exposes_small_effective_tool_sets() {
let config = test_config().await;