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
2026-06-24 19:21:28 -07:00
committed by GitHub
Unverified
parent a0d5fd772e
commit 4c0706e24a
29 changed files with 313 additions and 59 deletions
@@ -677,6 +677,7 @@ async fn load_plugins_loads_default_skills_and_mcp_servers() {
mcp_servers: HashMap::from([(
"sample".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::StreamableHttp {
url: "https://sample.example/mcp".to_string(),
bearer_token_env_var: None,
@@ -768,6 +769,7 @@ enabled = true
HashMap::from([(
"counter".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::StreamableHttp {
url: "https://sample.example/counter/mcp".to_string(),
bearer_token_env_var: None,
@@ -1662,6 +1664,7 @@ async fn load_plugins_uses_manifest_configured_component_paths() {
HashMap::from([(
"custom".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::StreamableHttp {
url: "https://custom.example/mcp".to_string(),
bearer_token_env_var: None,
@@ -1842,6 +1845,7 @@ async fn load_plugins_ignores_manifest_component_paths_without_dot_slash() {
HashMap::from([(
"default".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::StreamableHttp {
url: "https://default.example/mcp".to_string(),
bearer_token_env_var: None,
@@ -2092,6 +2096,7 @@ fn capability_index_filters_inactive_and_zero_capability_plugins() {
let connector = |id: &str| AppConnectorId(id.to_string());
let app = |name: &str, connector_id: &str| app_declaration(name, connector_id);
let http_server = |url: &str| McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::StreamableHttp {
url: url.to_string(),
bearer_token_env_var: None,