Allow ChatGPT-hosted MCP servers to use session auth (#29733)

## Why

ChatGPT session authentication was inferred from the reserved Codex Apps
server name. That couples credential routing to Codex Apps-specific
behavior and prevents other MCP endpoints hosted by ChatGPT from
explicitly using the current session.

The opt-in also needs a clear security boundary: an arbitrary MCP
configuration must not be able to redirect ChatGPT credentials to
another origin.

## What changed

- Add `use_chatgpt_auth` to HTTP MCP server configuration, defaulting to
`false`.
- Honor the setting only when the parsed server URL has the same HTTP(S)
origin as the configured `chatgpt_base_url`; otherwise remove the
capability before startup.
- Resolve bearer tokens and static or environment-backed authorization
headers before selecting authentication, with configured authorization
taking precedence over ChatGPT session auth.
- Enable the setting for the built-in Codex Apps and hosted plugin
runtime endpoints while keeping Codex Apps caching and tool
normalization scoped to the reserved server.
- Persist the setting through MCP config rewrite paths and expose it in
the generated config schema.
- Load the current login state for `codex mcp list` so reported auth
status matches runtime behavior.

## Verification

Core integration coverage exercises the complete streamable HTTP MCP
startup path and verifies that:

- a same-origin opted-in server receives the current ChatGPT access
token;
- an explicitly configured authorization header takes precedence;
- a different-origin server completes MCP initialization and tool
listing without receiving any ChatGPT authorization header.
This commit is contained in:
Ahmed Ibrahim authored and GitHub committed 2026-06-24 19:21:28 -07:00
1 parent a0d5fd772e
commit 4c0706e24a
29 files changed
+313 -59

No files matched your search

+3 -3
View File
@@ -16,8 +16,6 @@ use tracing::warn;
use crate::server::EffectiveMcpServer;
use super::CODEX_APPS_MCP_SERVER_NAME;
#[derive(Debug, Clone)]
pub struct McpOAuthLoginConfig {
pub url: String,
@@ -141,7 +139,9 @@ where
let futures = servers.into_iter().map(|(name, server)| {
let name = name.clone();
let config = server.configured_config().cloned();
let has_runtime_auth = name == CODEX_APPS_MCP_SERVER_NAME
let has_runtime_auth = config
.as_ref()
.is_some_and(|config| config.use_chatgpt_auth)
&& auth.is_some_and(CodexAuth::uses_codex_backend)
&& config.as_ref().is_some_and(|config| {
matches!(
+30 -3
View File
@@ -257,9 +257,26 @@ pub fn effective_mcp_servers_from_configured(
config: &McpConfig,
auth: Option<&CodexAuth>,
) -> HashMap<String, EffectiveMcpServer> {
let chatgpt_origin = url::Url::parse(&config.chatgpt_base_url)
.ok()
.filter(|url| matches!(url.scheme(), "http" | "https"))
.map(|url| url.origin());
let mut servers = configured_servers
.into_iter()
.map(|(name, server)| (name, EffectiveMcpServer::configured(server)))
.map(|(name, mut server)| {
if server.use_chatgpt_auth {
let server_origin = match &server.transport {
McpServerTransportConfig::StreamableHttp { url, .. } => url::Url::parse(url)
.ok()
.filter(|url| matches!(url.scheme(), "http" | "https"))
.map(|url| url.origin()),
McpServerTransportConfig::Stdio { .. } => None,
};
server.use_chatgpt_auth =
server_origin.is_some() && server_origin.as_ref() == chatgpt_origin.as_ref();
}
(name, EffectiveMcpServer::configured(server))
})
.collect::<HashMap<_, _>>();
if !host_owned_codex_apps_enabled(config, auth) {
servers.remove(CODEX_APPS_MCP_SERVER_NAME);
@@ -457,6 +474,7 @@ pub fn codex_apps_mcp_server_config(
mcp_server_config_for_url(
codex_apps_mcp_url_for_base_url(chatgpt_base_url),
apps_mcp_product_sku,
/*use_chatgpt_auth*/ true,
)
}
@@ -471,10 +489,18 @@ pub fn hosted_plugin_runtime_mcp_server_config(
} else {
format!("{base_url}/api/codex")
};
mcp_server_config_for_url(format!("{base_url}/ps/mcp"), apps_mcp_product_sku)
mcp_server_config_for_url(
format!("{base_url}/ps/mcp"),
apps_mcp_product_sku,
/*use_chatgpt_auth*/ true,
)
}
fn mcp_server_config_for_url(url: String, apps_mcp_product_sku: Option<&str>) -> McpServerConfig {
fn mcp_server_config_for_url(
url: String,
apps_mcp_product_sku: Option<&str>,
use_chatgpt_auth: bool,
) -> McpServerConfig {
let http_headers = apps_mcp_product_sku.map(|product_sku| {
HashMap::from([("X-OpenAI-Product-Sku".to_string(), product_sku.to_string())])
});
@@ -486,6 +512,7 @@ fn mcp_server_config_for_url(url: String, apps_mcp_product_sku: Option<&str>) ->
http_headers,
env_http_headers: None,
},
use_chatgpt_auth,
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
+2
View File
@@ -296,6 +296,7 @@ async fn effective_mcp_servers_preserve_runtime_servers() {
catalog.register(McpServerRegistration::from_config(
"sample".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::StreamableHttp {
url: "https://user.example/mcp".to_string(),
bearer_token_env_var: None,
@@ -321,6 +322,7 @@ async fn effective_mcp_servers_preserve_runtime_servers() {
catalog.register(McpServerRegistration::from_config(
"docs".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::StreamableHttp {
url: "https://docs.example/mcp".to_string(),
bearer_token_env_var: None,