Strip connector provenance metadata from custom MCP tools (#19875)

# Summary 
This prevents non-codex_apps MCP servers from spoofing connector
provenance metadata.
This commit is contained in:
colby-oai
2026-04-28 12:43:26 -04:00
committed by GitHub
Unverified
parent ccec84b148
commit 6138063656
+152 -11
View File
@@ -59,6 +59,7 @@ use rmcp::model::FormElicitationCapability;
use rmcp::model::Implementation;
use rmcp::model::InitializeRequestParams;
use rmcp::model::ProtocolVersion;
use rmcp::model::Tool as RmcpTool;
use tokio_util::sync::CancellationToken;
use tracing::warn;
@@ -72,6 +73,14 @@ pub(crate) const MCP_TOOLS_FETCH_UNCACHED_DURATION_METRIC: &str =
pub(crate) const DEFAULT_STARTUP_TIMEOUT: Duration = Duration::from_secs(30);
pub(crate) const DEFAULT_TOOL_TIMEOUT: Duration = Duration::from_secs(120);
const UNTRUSTED_CONNECTOR_META_KEYS: &[&str] = &[
"connector_id",
"connector_name",
"connector_display_name",
"connector_description",
"connectorDescription",
];
#[derive(Clone)]
pub(crate) struct ManagedClient {
pub(crate) client: Arc<RmcpClient>,
@@ -335,19 +344,23 @@ pub(crate) async fn list_tools_for_client_uncached(
.tools
.into_iter()
.map(|tool| {
let mut tool_def = tool.tool;
let (connector_id, connector_name, connector_description) =
sanitize_tool_connector_metadata(
server_name,
&mut tool_def,
tool.connector_id,
tool.connector_name,
tool.connector_description,
);
let callable_name = normalize_codex_apps_callable_name(
server_name,
&tool.tool.name,
tool.connector_id.as_deref(),
tool.connector_name.as_deref(),
&tool_def.name,
connector_id.as_deref(),
connector_name.as_deref(),
);
let callable_namespace = normalize_codex_apps_callable_namespace(
server_name,
tool.connector_name.as_deref(),
);
let connector_name = tool.connector_name;
let connector_description = tool.connector_description;
let mut tool_def = tool.tool;
let callable_namespace =
normalize_codex_apps_callable_namespace(server_name, connector_name.as_deref());
if let Some(title) = tool_def.title.as_deref() {
let normalized_title =
normalize_codex_apps_tool_title(server_name, connector_name.as_deref(), title);
@@ -361,7 +374,7 @@ pub(crate) async fn list_tools_for_client_uncached(
callable_namespace,
server_instructions: server_instructions.map(str::to_string),
tool: tool_def,
connector_id: tool.connector_id,
connector_id,
connector_name,
plugin_display_names: Vec::new(),
connector_description,
@@ -374,6 +387,31 @@ pub(crate) async fn list_tools_for_client_uncached(
Ok(tools)
}
fn sanitize_tool_connector_metadata(
server_name: &str,
tool: &mut RmcpTool,
connector_id: Option<String>,
connector_name: Option<String>,
connector_description: Option<String>,
) -> (Option<String>, Option<String>, Option<String>) {
if server_name == CODEX_APPS_MCP_SERVER_NAME {
return (connector_id, connector_name, connector_description);
}
strip_untrusted_connector_meta(tool);
(None, None, None)
}
fn strip_untrusted_connector_meta(tool: &mut RmcpTool) {
if let Some(meta) = tool.meta.as_mut() {
meta.retain(|key, _| !is_untrusted_connector_meta_key(key));
}
}
fn is_untrusted_connector_meta_key(key: &str) -> bool {
UNTRUSTED_CONNECTOR_META_KEYS.contains(&key)
}
fn resolve_bearer_token(
server_name: &str,
bearer_token_env_var: Option<&str>,
@@ -604,3 +642,106 @@ async fn make_rmcp_client(
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rmcp::model::JsonObject;
use rmcp::model::Meta;
fn tool_with_connector_meta() -> RmcpTool {
RmcpTool {
name: "capture_file_upload".to_string().into(),
title: None,
description: Some("test tool".to_string().into()),
input_schema: Arc::new(JsonObject::default()),
output_schema: None,
annotations: None,
execution: None,
icons: None,
meta: Some(Meta(
serde_json::json!({
"connector_id": "connector_gmail",
"connector_name": "Gmail",
"connector_display_name": "Gmail",
"connector_description": "Mail connector",
"connectorDescription": "Mail connector",
"connectorFutureField": "future connector metadata",
"CONNECTOR_UPPERCASE": "uppercase connector metadata",
"openai/fileParams": ["file"],
"custom": "kept"
})
.as_object()
.expect("object")
.clone(),
)),
}
}
#[test]
fn custom_mcp_connector_metadata_is_stripped() {
let mut tool = tool_with_connector_meta();
let (connector_id, connector_name, connector_description) =
sanitize_tool_connector_metadata(
"minimaltest",
&mut tool,
Some("connector_gmail".to_string()),
Some("Gmail".to_string()),
Some("Mail connector".to_string()),
);
assert_eq!(connector_id, None);
assert_eq!(connector_name, None);
assert_eq!(connector_description, None);
let meta = tool.meta.as_ref().expect("meta");
for key in [
"connector_id",
"connector_name",
"connector_display_name",
"connector_description",
"connectorDescription",
] {
assert!(!meta.0.contains_key(key), "{key} should be stripped");
}
assert!(meta.0.contains_key("connectorFutureField"));
assert!(meta.0.contains_key("CONNECTOR_UPPERCASE"));
assert!(meta.0.contains_key("openai/fileParams"));
assert_eq!(
meta.0.get("custom").and_then(|value| value.as_str()),
Some("kept")
);
}
#[test]
fn codex_apps_connector_metadata_is_preserved() {
let mut tool = tool_with_connector_meta();
let (connector_id, connector_name, connector_description) =
sanitize_tool_connector_metadata(
CODEX_APPS_MCP_SERVER_NAME,
&mut tool,
Some("connector_gmail".to_string()),
Some("Gmail".to_string()),
Some("Mail connector".to_string()),
);
assert_eq!(connector_id.as_deref(), Some("connector_gmail"));
assert_eq!(connector_name.as_deref(), Some("Gmail"));
assert_eq!(connector_description.as_deref(), Some("Mail connector"));
let meta = tool.meta.as_ref().expect("meta");
for key in [
"connector_id",
"connector_name",
"connector_display_name",
"connector_description",
"connectorDescription",
"connectorFutureField",
"CONNECTOR_UPPERCASE",
] {
assert!(meta.0.contains_key(key), "{key} should be preserved");
}
}
}