Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-rs/codex-mcp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
56 changes: 56 additions & 0 deletions codex-rs/codex-mcp/src/openai_docs_source_attribution.rs
Original file line number Diff line number Diff line change
@@ -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;
92 changes: 92 additions & 0 deletions codex-rs/codex-mcp/src/openai_docs_source_attribution_tests.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions codex-rs/codex-mcp/src/rmcp_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Loading