mirror of
https://github.com/openai/codex.git
synced 2026-11-10 16:44:38 +00:00
Add oauth_resource handling for MCP login flows (#12866)
Addresses bug https://github.com/openai/codex/issues/12589 Builds on community PR #12763. This adds `oauth_resource` support for MCP `streamable_http` servers and wires it through the relevant config and login paths. It fixes the bug where the configured OAuth resource was not reliably included in the authorization request, causing MCP login to omit the expected `resource` parameter.
This commit is contained in:
1 parent
6fe3dc2e22
commit
cee009d117
14 files changed
+201
-1
No files matched your search
@@ -195,6 +195,11 @@ mod document_helpers {
|
||||
{
|
||||
entry["scopes"] = array_from_iter(scopes.iter().cloned());
|
||||
}
|
||||
if let Some(resource) = &config.oauth_resource
|
||||
&& !resource.is_empty()
|
||||
{
|
||||
entry["oauth_resource"] = value(resource.clone());
|
||||
}
|
||||
|
||||
entry
|
||||
}
|
||||
@@ -1465,6 +1470,7 @@ gpt-5 = "gpt-5.1"
|
||||
enabled_tools: Some(vec!["one".to_string(), "two".to_string()]),
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1489,6 +1495,7 @@ gpt-5 = "gpt-5.1"
|
||||
enabled_tools: None,
|
||||
disabled_tools: Some(vec!["forbidden".to_string()]),
|
||||
scopes: None,
|
||||
oauth_resource: Some("https://resource.example.com".to_string()),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1507,6 +1514,7 @@ bearer_token_env_var = \"TOKEN\"
|
||||
enabled = false
|
||||
startup_timeout_sec = 5.0
|
||||
disabled_tools = [\"forbidden\"]
|
||||
oauth_resource = \"https://resource.example.com\"
|
||||
|
||||
[mcp_servers.http.http_headers]
|
||||
Z-Header = \"z\"
|
||||
@@ -1556,6 +1564,7 @@ foo = { command = "cmd" }
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1602,6 +1611,7 @@ foo = { command = "cmd" } # keep me
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1647,6 +1657,7 @@ foo = { command = "cmd", args = ["--flag"] } # keep me
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1693,6 +1704,7 @@ foo = { command = "cmd" }
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -2433,6 +2433,7 @@ mod tests {
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2452,6 +2453,7 @@ mod tests {
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3489,6 +3491,7 @@ profile = "project"
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -3646,6 +3649,7 @@ bearer_token = "secret"
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
@@ -3717,6 +3721,7 @@ ZIG_VAR = "3"
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
@@ -3768,6 +3773,7 @@ ZIG_VAR = "3"
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
@@ -3817,6 +3823,7 @@ ZIG_VAR = "3"
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
@@ -3882,6 +3889,7 @@ startup_timeout_sec = 2.0
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
apply_blocking(
|
||||
@@ -3959,6 +3967,7 @@ X-Auth = "DOCS_AUTH"
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
@@ -3989,6 +3998,7 @@ X-Auth = "DOCS_AUTH"
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
apply_blocking(
|
||||
@@ -4057,6 +4067,7 @@ url = "https://example.com/mcp"
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
@@ -4077,6 +4088,7 @@ url = "https://example.com/mcp"
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
@@ -4160,6 +4172,7 @@ url = "https://example.com/mcp"
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
@@ -4205,6 +4218,7 @@ url = "https://example.com/mcp"
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
@@ -4250,6 +4264,7 @@ url = "https://example.com/mcp"
|
||||
enabled_tools: Some(vec!["allowed".to_string()]),
|
||||
disabled_tools: Some(vec!["blocked".to_string()]),
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
@@ -4278,6 +4293,51 @@ url = "https://example.com/mcp"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replace_mcp_servers_streamable_http_serializes_oauth_resource() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://example.com/mcp".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: Some("https://resource.example.com".to_string()),
|
||||
},
|
||||
)]);
|
||||
|
||||
apply_blocking(
|
||||
codex_home.path(),
|
||||
None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
|
||||
)?;
|
||||
|
||||
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
|
||||
let serialized = std::fs::read_to_string(&config_path)?;
|
||||
assert!(serialized.contains(r#"oauth_resource = "https://resource.example.com""#));
|
||||
|
||||
let loaded = load_global_mcp_servers(codex_home.path()).await?;
|
||||
let docs = loaded.get("docs").expect("docs entry");
|
||||
assert_eq!(
|
||||
docs.oauth_resource.as_deref(),
|
||||
Some("https://resource.example.com")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_model_updates_defaults() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -99,6 +99,10 @@ pub struct McpServerConfig {
|
||||
/// Optional OAuth scopes to request during MCP login.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub scopes: Option<Vec<String>>,
|
||||
|
||||
/// Optional OAuth resource parameter to include during MCP login (RFC 8707).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub oauth_resource: Option<String>,
|
||||
}
|
||||
|
||||
// Raw MCP config shape used for deserialization and JSON Schema generation.
|
||||
@@ -143,6 +147,8 @@ pub(crate) struct RawMcpServerConfig {
|
||||
pub disabled_tools: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub scopes: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub oauth_resource: Option<String>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for McpServerConfig {
|
||||
@@ -166,6 +172,7 @@ impl<'de> Deserialize<'de> for McpServerConfig {
|
||||
let enabled_tools = raw.enabled_tools.clone();
|
||||
let disabled_tools = raw.disabled_tools.clone();
|
||||
let scopes = raw.scopes.clone();
|
||||
let oauth_resource = raw.oauth_resource.clone();
|
||||
|
||||
fn throw_if_set<E, T>(transport: &str, field: &str, value: Option<&T>) -> Result<(), E>
|
||||
where
|
||||
@@ -189,6 +196,7 @@ impl<'de> Deserialize<'de> for McpServerConfig {
|
||||
throw_if_set("stdio", "bearer_token", raw.bearer_token.as_ref())?;
|
||||
throw_if_set("stdio", "http_headers", raw.http_headers.as_ref())?;
|
||||
throw_if_set("stdio", "env_http_headers", raw.env_http_headers.as_ref())?;
|
||||
throw_if_set("stdio", "oauth_resource", raw.oauth_resource.as_ref())?;
|
||||
McpServerTransportConfig::Stdio {
|
||||
command,
|
||||
args: raw.args.clone().unwrap_or_default(),
|
||||
@@ -222,6 +230,7 @@ impl<'de> Deserialize<'de> for McpServerConfig {
|
||||
enabled_tools,
|
||||
disabled_tools,
|
||||
scopes,
|
||||
oauth_resource,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1093,6 +1102,22 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_streamable_http_server_config_with_oauth_resource() {
|
||||
let cfg: McpServerConfig = toml::from_str(
|
||||
r#"
|
||||
url = "https://example.com/mcp"
|
||||
oauth_resource = "https://api.example.com"
|
||||
"#,
|
||||
)
|
||||
.expect("should deserialize http config with oauth_resource");
|
||||
|
||||
assert_eq!(
|
||||
cfg.oauth_resource,
|
||||
Some("https://api.example.com".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_server_config_with_tool_filters() {
|
||||
let cfg: McpServerConfig = toml::from_str(
|
||||
@@ -1147,6 +1172,20 @@ mod tests {
|
||||
"#,
|
||||
)
|
||||
.expect_err("should reject env_http_headers for stdio transport");
|
||||
|
||||
let err = toml::from_str::<McpServerConfig>(
|
||||
r#"
|
||||
command = "echo"
|
||||
oauth_resource = "https://api.example.com"
|
||||
"#,
|
||||
)
|
||||
.expect_err("should reject oauth_resource for stdio transport");
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("oauth_resource is not supported for stdio"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -139,6 +139,7 @@ fn codex_apps_mcp_server_config(config: &Config, auth: Option<&CodexAuth>) -> Mc
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -241,6 +241,7 @@ pub(crate) async fn maybe_install_mcp_dependencies(
|
||||
oauth_config.http_headers,
|
||||
oauth_config.env_http_headers,
|
||||
&[],
|
||||
server_config.oauth_resource.as_deref(),
|
||||
config.mcp_oauth_callback_port,
|
||||
config.mcp_oauth_callback_url.as_deref(),
|
||||
)
|
||||
@@ -387,6 +388,7 @@ fn mcp_dependency_to_server_config(
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -411,6 +413,7 @@ fn mcp_dependency_to_server_config(
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -468,6 +471,7 @@ mod tests {
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
@@ -516,6 +520,7 @@ mod tests {
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
|
||||
@@ -2115,6 +2115,7 @@ mod tests {
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
auth_status: McpAuthStatus::Unsupported,
|
||||
};
|
||||
@@ -2162,6 +2163,7 @@ mod tests {
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
auth_status: McpAuthStatus::Unsupported,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user