From 2b486b46766f0326d059cdcf2d96e4045c6477f8 Mon Sep 17 00:00:00 2001 From: Konstantine Kahadze Date: Wed, 15 Jul 2026 20:47:41 +0000 Subject: [PATCH] Attribute OpenAI docs MCP requests to Codex (#33424) ## What changed - Add `source=codex` to requests sent to the OpenAI developer docs MCP endpoint. - Preserve OAuth discovery URLs and leave clients for other MCP servers unchanged. ## Testing - Add unit coverage for regular and streaming HTTP requests, OAuth discovery, and non-docs MCP clients. GitOrigin-RevId: d04e19fe6fc2db0b5cd7bfb429f0cb821c49b28a --- codex-rs/codex-mcp/src/lib.rs | 1 + .../src/openai_docs_source_attribution.rs | 56 +++++++++++ .../openai_docs_source_attribution_tests.rs | 92 +++++++++++++++++++ codex-rs/codex-mcp/src/rmcp_client.rs | 2 + 4 files changed, 151 insertions(+) create mode 100644 codex-rs/codex-mcp/src/openai_docs_source_attribution.rs create mode 100644 codex-rs/codex-mcp/src/openai_docs_source_attribution_tests.rs diff --git a/codex-rs/codex-mcp/src/lib.rs b/codex-rs/codex-mcp/src/lib.rs index fa1d19023bf8..949ef4f7af30 100644 --- a/codex-rs/codex-mcp/src/lib.rs +++ b/codex-rs/codex-mcp/src/lib.rs @@ -86,6 +86,7 @@ pub(crate) mod codex_apps; pub(crate) mod connection_manager; pub(crate) mod elicitation; pub(crate) mod mcp; +mod openai_docs_source_attribution; mod plugin_config; mod resource_client; pub(crate) mod rmcp_client; diff --git a/codex-rs/codex-mcp/src/openai_docs_source_attribution.rs b/codex-rs/codex-mcp/src/openai_docs_source_attribution.rs new file mode 100644 index 000000000000..a3baf5c87fc1 --- /dev/null +++ b/codex-rs/codex-mcp/src/openai_docs_source_attribution.rs @@ -0,0 +1,56 @@ +use std::sync::Arc; + +use codex_exec_server::ExecServerError; +use codex_exec_server::HttpClient; +use codex_exec_server::HttpRequestParams; +use codex_exec_server::HttpRequestResponse; +use codex_exec_server::HttpResponseBodyStream; +use futures::future::BoxFuture; + +const OPENAI_DEVELOPER_DOCS_MCP_URL: &str = "https://developers.openai.com/mcp"; +const OPENAI_DEVELOPER_DOCS_MCP_CODEX_URL: &str = "https://developers.openai.com/mcp?source=codex"; + +pub(crate) fn maybe_with_openai_docs_source_attribution( + mcp_server_url: &str, + http_client: Arc, +) -> Arc { + if mcp_server_url == OPENAI_DEVELOPER_DOCS_MCP_URL { + Arc::new(OpenAiDocsHttpClient { http_client }) + } else { + http_client + } +} + +struct OpenAiDocsHttpClient { + http_client: Arc, +} + +impl OpenAiDocsHttpClient { + fn attribute_mcp_request(&self, params: &mut HttpRequestParams) { + if params.url == OPENAI_DEVELOPER_DOCS_MCP_URL { + params.url = OPENAI_DEVELOPER_DOCS_MCP_CODEX_URL.to_string(); + } + } +} + +impl HttpClient for OpenAiDocsHttpClient { + fn http_request( + &self, + mut params: HttpRequestParams, + ) -> BoxFuture<'_, Result> { + self.attribute_mcp_request(&mut params); + self.http_client.http_request(params) + } + + fn http_request_stream( + &self, + mut params: HttpRequestParams, + ) -> BoxFuture<'_, Result<(HttpRequestResponse, HttpResponseBodyStream), ExecServerError>> { + self.attribute_mcp_request(&mut params); + self.http_client.http_request_stream(params) + } +} + +#[cfg(test)] +#[path = "openai_docs_source_attribution_tests.rs"] +mod tests; diff --git a/codex-rs/codex-mcp/src/openai_docs_source_attribution_tests.rs b/codex-rs/codex-mcp/src/openai_docs_source_attribution_tests.rs new file mode 100644 index 000000000000..7447debabf1b --- /dev/null +++ b/codex-rs/codex-mcp/src/openai_docs_source_attribution_tests.rs @@ -0,0 +1,92 @@ +use std::sync::Arc; +use std::sync::Mutex; + +use codex_exec_server::ExecServerError; +use codex_exec_server::HttpClient; +use codex_exec_server::HttpRedirectPolicy; +use codex_exec_server::HttpRequestParams; +use codex_exec_server::HttpRequestResponse; +use codex_exec_server::HttpResponseBodyStream; +use futures::FutureExt; +use futures::future::BoxFuture; +use pretty_assertions::assert_eq; + +use super::OPENAI_DEVELOPER_DOCS_MCP_CODEX_URL; +use super::OPENAI_DEVELOPER_DOCS_MCP_URL; +use super::maybe_with_openai_docs_source_attribution; + +#[derive(Default)] +struct RecordingHttpClient { + urls: Mutex>, +} + +impl HttpClient for RecordingHttpClient { + fn http_request( + &self, + params: HttpRequestParams, + ) -> BoxFuture<'_, Result> { + self.urls.lock().unwrap().push(params.url); + async { Err(ExecServerError::HttpRequest("test response".to_string())) }.boxed() + } + + fn http_request_stream( + &self, + params: HttpRequestParams, + ) -> BoxFuture<'_, Result<(HttpRequestResponse, HttpResponseBodyStream), ExecServerError>> { + self.urls.lock().unwrap().push(params.url); + async { Err(ExecServerError::HttpRequest("test response".to_string())) }.boxed() + } +} + +fn request(url: &str) -> HttpRequestParams { + HttpRequestParams { + method: "POST".to_string(), + url: url.to_string(), + headers: Vec::new(), + body: None, + timeout_ms: None, + redirect_policy: HttpRedirectPolicy::Follow, + request_id: "test-request".to_string(), + stream_response: true, + } +} + +#[tokio::test] +async fn attributes_only_docs_mcp_requests() { + let recording_client = Arc::new(RecordingHttpClient::default()); + let http_client = maybe_with_openai_docs_source_attribution( + OPENAI_DEVELOPER_DOCS_MCP_URL, + recording_client.clone(), + ); + + let _ = http_client + .http_request_stream(request(OPENAI_DEVELOPER_DOCS_MCP_URL)) + .await; + let _ = http_client + .http_request(request( + "https://developers.openai.com/.well-known/oauth-protected-resource/mcp", + )) + .await; + + assert_eq!( + recording_client.urls.lock().unwrap().as_slice(), + [ + OPENAI_DEVELOPER_DOCS_MCP_CODEX_URL, + "https://developers.openai.com/.well-known/oauth-protected-resource/mcp", + ] + ); +} + +#[test] +fn leaves_other_mcp_clients_unwrapped() { + let recording_client = Arc::new(RecordingHttpClient::default()); + let http_client = maybe_with_openai_docs_source_attribution( + "https://example.com/mcp", + recording_client.clone(), + ); + + assert!(Arc::ptr_eq( + &http_client, + &(recording_client as Arc) + )); +} diff --git a/codex-rs/codex-mcp/src/rmcp_client.rs b/codex-rs/codex-mcp/src/rmcp_client.rs index 413b7eaa92ba..565549ef0a5f 100644 --- a/codex-rs/codex-mcp/src/rmcp_client.rs +++ b/codex-rs/codex-mcp/src/rmcp_client.rs @@ -25,6 +25,7 @@ use crate::codex_apps::prepare_openai_file_params_for_model; use crate::elicitation::ElicitationRequestManager; use crate::mcp::CODEX_APPS_MCP_SERVER_NAME; use crate::mcp::ToolPluginProvenance; +use crate::openai_docs_source_attribution::maybe_with_openai_docs_source_attribution; use crate::runtime::McpRuntimeContext; use crate::runtime::emit_duration; use crate::server::EffectiveMcpServer; @@ -1077,6 +1078,7 @@ async fn make_rmcp_client( || Arc::new(ReqwestHttpClient) as Arc, |environment| environment.get_http_client(), ); + let http_client = maybe_with_openai_docs_source_attribution(&url, http_client); let resolved_bearer_token = match resolve_bearer_token(server_name, bearer_token_env_var.as_deref()) { Ok(token) => token,