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

+17
View File
@@ -114,6 +114,7 @@ use tempfile::TempDir;
fn stdio_mcp(command: &str) -> McpServerConfig {
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::Stdio {
command: command.to_string(),
args: Vec::new(),
@@ -140,6 +141,7 @@ fn stdio_mcp(command: &str) -> McpServerConfig {
fn http_mcp(url: &str) -> McpServerConfig {
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::StreamableHttp {
url: url.to_string(),
bearer_token_env_var: None,
@@ -5566,6 +5568,7 @@ async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> {
servers.insert(
"docs".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::Stdio {
command: "echo".to_string(),
args: vec!["hello".to_string()],
@@ -5923,6 +5926,7 @@ async fn replace_mcp_servers_serializes_env_sorted() -> anyhow::Result<()> {
let servers = BTreeMap::from([(
"docs".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::Stdio {
command: "docs-server".to_string(),
args: vec!["--verbose".to_string()],
@@ -6002,6 +6006,7 @@ async fn replace_mcp_servers_serializes_env_vars() -> anyhow::Result<()> {
let servers = BTreeMap::from([(
"docs".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::Stdio {
command: "docs-server".to_string(),
args: Vec::new(),
@@ -6057,6 +6062,7 @@ async fn replace_mcp_servers_serializes_sourced_env_vars() -> anyhow::Result<()>
let servers = BTreeMap::from([(
"docs".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::Stdio {
command: "docs-server".to_string(),
args: Vec::new(),
@@ -6115,6 +6121,7 @@ async fn replace_mcp_servers_serializes_cwd() -> anyhow::Result<()> {
let servers = BTreeMap::from([(
"docs".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::Stdio {
command: "docs-server".to_string(),
args: Vec::new(),
@@ -6170,6 +6177,7 @@ async fn replace_mcp_servers_streamable_http_serializes_bearer_token() -> anyhow
let servers = BTreeMap::from([(
"docs".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::StreamableHttp {
url: "https://example.com/mcp".to_string(),
bearer_token_env_var: Some("MCP_TOKEN".to_string()),
@@ -6237,6 +6245,7 @@ async fn replace_mcp_servers_streamable_http_serializes_custom_headers() -> anyh
let servers = BTreeMap::from([(
"docs".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::StreamableHttp {
url: "https://example.com/mcp".to_string(),
bearer_token_env_var: Some("MCP_TOKEN".to_string()),
@@ -6319,6 +6328,7 @@ async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyh
let mut servers = BTreeMap::from([(
"docs".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::StreamableHttp {
url: "https://example.com/mcp".to_string(),
bearer_token_env_var: Some("MCP_TOKEN".to_string()),
@@ -6357,6 +6367,7 @@ async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyh
servers.insert(
"docs".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::StreamableHttp {
url: "https://example.com/mcp".to_string(),
bearer_token_env_var: None,
@@ -6424,6 +6435,7 @@ async fn replace_mcp_servers_streamable_http_isolates_headers_between_servers()
(
"docs".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::StreamableHttp {
url: "https://example.com/mcp".to_string(),
bearer_token_env_var: Some("MCP_TOKEN".to_string()),
@@ -6452,6 +6464,7 @@ async fn replace_mcp_servers_streamable_http_isolates_headers_between_servers()
(
"logs".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::Stdio {
command: "logs-server".to_string(),
args: vec!["--follow".to_string()],
@@ -6540,6 +6553,7 @@ async fn replace_mcp_servers_serializes_disabled_flag() -> anyhow::Result<()> {
let servers = BTreeMap::from([(
"docs".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::Stdio {
command: "docs-server".to_string(),
args: Vec::new(),
@@ -6590,6 +6604,7 @@ async fn replace_mcp_servers_serializes_required_flag() -> anyhow::Result<()> {
let servers = BTreeMap::from([(
"docs".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::Stdio {
command: "docs-server".to_string(),
args: Vec::new(),
@@ -6640,6 +6655,7 @@ async fn replace_mcp_servers_serializes_tool_filters() -> anyhow::Result<()> {
let servers = BTreeMap::from([(
"docs".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::Stdio {
command: "docs-server".to_string(),
args: Vec::new(),
@@ -6695,6 +6711,7 @@ async fn replace_mcp_servers_streamable_http_serializes_oauth_resource() -> anyh
let servers = BTreeMap::from([(
"docs".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::StreamableHttp {
url: "https://example.com/mcp".to_string(),
bearer_token_env_var: None,
@@ -95,6 +95,9 @@ fn serialize_mcp_server_table(config: &McpServerConfig) -> TomlTable {
}
}
if config.use_chatgpt_auth {
entry["use_chatgpt_auth"] = value(true);
}
if !config.enabled {
entry["enabled"] = value(false);
}
+7
View File
@@ -873,6 +873,7 @@ fn blocking_replace_mcp_servers_round_trips() {
servers.insert(
"stdio".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::Stdio {
command: "cmd".to_string(),
args: vec!["--flag".to_string()],
@@ -907,6 +908,7 @@ fn blocking_replace_mcp_servers_round_trips() {
servers.insert(
"http".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::StreamableHttp {
url: "https://example.com".to_string(),
bearer_token_env_var: Some("TOKEN".to_string()),
@@ -981,6 +983,7 @@ fn blocking_replace_mcp_servers_serializes_tool_approval_overrides() {
servers.insert(
"docs".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::Stdio {
command: "docs-server".to_string(),
args: Vec::new(),
@@ -1041,6 +1044,7 @@ foo = { command = "cmd" }
servers.insert(
"foo".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::Stdio {
command: "cmd".to_string(),
args: Vec::new(),
@@ -1091,6 +1095,7 @@ foo = { command = "cmd" } # keep me
servers.insert(
"foo".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::Stdio {
command: "cmd".to_string(),
args: Vec::new(),
@@ -1140,6 +1145,7 @@ foo = { command = "cmd", args = ["--flag"] } # keep me
servers.insert(
"foo".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::Stdio {
command: "cmd".to_string(),
args: Vec::new(),
@@ -1190,6 +1196,7 @@ foo = { command = "cmd" }
servers.insert(
"foo".to_string(),
McpServerConfig {
use_chatgpt_auth: false,
transport: McpServerTransportConfig::Stdio {
command: "cmd".to_string(),
args: Vec::new(),