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
This commit is contained in:
Konstantine Kahadze
2026-07-15 20:54:54 +00:00
committed by copyberry
parent 9ab3dd8b9f
commit 2b486b4676
4 changed files with 151 additions and 0 deletions
+1
View File
@@ -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;
@@ -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<dyn HttpClient>,
) -> Arc<dyn HttpClient> {
if mcp_server_url == OPENAI_DEVELOPER_DOCS_MCP_URL {
Arc::new(OpenAiDocsHttpClient { http_client })
} else {
http_client
}
}
struct OpenAiDocsHttpClient {
http_client: Arc<dyn HttpClient>,
}
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<HttpRequestResponse, ExecServerError>> {
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;
@@ -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<Vec<String>>,
}
impl HttpClient for RecordingHttpClient {
fn http_request(
&self,
params: HttpRequestParams,
) -> BoxFuture<'_, Result<HttpRequestResponse, ExecServerError>> {
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<dyn HttpClient>)
));
}
+2
View File
@@ -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<dyn HttpClient>,
|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,