Compare commits

...

2 Commits

Author SHA1 Message Date
Casey Chow
ec8439009a Gate apps_mcp_url override to dev profile only 2026-02-12 16:37:37 -05:00
Casey Chow
676ac26573 feat(core): allow configurable apps MCP URL 2026-02-12 16:37:37 -05:00
4 changed files with 94 additions and 2 deletions

View File

@@ -1222,6 +1222,10 @@
"default": null,
"description": "Settings for app-specific controls."
},
"apps_mcp_url": {
"description": "Optional override for the Codex Apps MCP endpoint URL.",
"type": "string"
},
"chatgpt_base_url": {
"description": "Base URL for requests to ChatGPT (as opposed to the OpenAI API).",
"type": "string"

View File

@@ -324,6 +324,8 @@ pub struct Config {
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
pub chatgpt_base_url: String,
/// Optional override for the Codex Apps MCP endpoint URL.
pub apps_mcp_url: Option<String>,
/// When set, restricts ChatGPT login to a specific workspace identifier.
pub forced_chatgpt_workspace_id: Option<String>,
@@ -989,6 +991,8 @@ pub struct ConfigToml {
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
pub chatgpt_base_url: Option<String>,
/// Optional override for the Codex Apps MCP endpoint URL.
pub apps_mcp_url: Option<String>,
pub projects: Option<HashMap<String, ProjectConfig>>,
@@ -1790,6 +1794,10 @@ impl Config {
.chatgpt_base_url
.or(cfg.chatgpt_base_url)
.unwrap_or("https://chatgpt.com/backend-api/".to_string()),
apps_mcp_url: cfg
.apps_mcp_url
.map(|url| url.trim().to_string())
.filter(|url| !url.is_empty()),
forced_chatgpt_workspace_id,
forced_login_method,
include_apply_patch_tool: include_apply_patch_tool_flag,
@@ -4054,6 +4062,7 @@ model_verbosity = "high"
model_verbosity: None,
personality: Some(Personality::Pragmatic),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
apps_mcp_url: None,
base_instructions: None,
developer_instructions: None,
compact_prompt: None,
@@ -4161,6 +4170,7 @@ model_verbosity = "high"
model_verbosity: None,
personality: Some(Personality::Pragmatic),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
apps_mcp_url: None,
base_instructions: None,
developer_instructions: None,
compact_prompt: None,
@@ -4266,6 +4276,7 @@ model_verbosity = "high"
model_verbosity: None,
personality: Some(Personality::Pragmatic),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
apps_mcp_url: None,
base_instructions: None,
developer_instructions: None,
compact_prompt: None,
@@ -4357,6 +4368,7 @@ model_verbosity = "high"
model_verbosity: Some(Verbosity::High),
personality: Some(Personality::Pragmatic),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
apps_mcp_url: None,
base_instructions: None,
developer_instructions: None,
compact_prompt: None,

View File

@@ -65,7 +65,7 @@ fn codex_apps_mcp_http_headers(auth: Option<&CodexAuth>) -> Option<HashMap<Strin
}
}
fn codex_apps_mcp_url(base_url: &str) -> String {
fn default_codex_apps_mcp_url(base_url: &str) -> String {
let mut base_url = base_url.trim_end_matches('/').to_string();
if (base_url.starts_with("https://chatgpt.com")
|| base_url.starts_with("https://chat.openai.com"))
@@ -82,6 +82,22 @@ fn codex_apps_mcp_url(base_url: &str) -> String {
}
}
fn codex_apps_mcp_url(
chatgpt_base_url: &str,
configured_url: Option<&str>,
active_profile: Option<&str>,
) -> String {
if let Some(configured_url) = configured_url
.filter(|_| active_profile == Some("dev"))
.map(str::trim)
.filter(|url| !url.is_empty())
{
return configured_url.to_string();
}
default_codex_apps_mcp_url(chatgpt_base_url)
}
fn codex_apps_mcp_server_config(config: &Config, auth: Option<&CodexAuth>) -> McpServerConfig {
let bearer_token_env_var = codex_apps_mcp_bearer_token_env_var();
let http_headers = if bearer_token_env_var.is_some() {
@@ -89,7 +105,11 @@ fn codex_apps_mcp_server_config(config: &Config, auth: Option<&CodexAuth>) -> Mc
} else {
codex_apps_mcp_http_headers(auth)
};
let url = codex_apps_mcp_url(&config.chatgpt_base_url);
let url = codex_apps_mcp_url(
&config.chatgpt_base_url,
config.apps_mcp_url.as_deref(),
config.active_profile.as_deref(),
);
McpServerConfig {
transport: McpServerTransportConfig::StreamableHttp {
@@ -385,4 +405,48 @@ mod tests {
assert_eq!(group_tools_by_server(&tools), expected);
}
#[test]
fn codex_apps_mcp_url_defaults_to_original_chatgpt_base_behavior() {
assert_eq!(
codex_apps_mcp_url("https://chatgpt.com", None, None),
"https://chatgpt.com/backend-api/wham/apps"
);
}
#[test]
fn codex_apps_mcp_url_uses_original_path_behavior_for_capi() {
assert_eq!(
codex_apps_mcp_url("https://example.com/api/codex", None, None),
"https://example.com/api/codex/apps"
);
assert_eq!(
codex_apps_mcp_url("https://example.com/api/codex/", None, None),
"https://example.com/api/codex/apps"
);
}
#[test]
fn codex_apps_mcp_url_uses_configured_url_for_dev_profile() {
assert_eq!(
codex_apps_mcp_url(
"https://chatgpt.com",
Some("https://example.com/custom/path"),
Some("dev")
),
"https://example.com/custom/path"
);
}
#[test]
fn codex_apps_mcp_url_uses_default_for_non_dev_profile() {
assert_eq!(
codex_apps_mcp_url(
"https://chatgpt.com",
Some("https://example.com/custom/path"),
Some("prod")
),
"https://chatgpt.com/backend-api/wham/apps"
);
}
}

View File

@@ -65,6 +65,7 @@ struct StatusHistoryCell {
model_details: Vec<String>,
directory: PathBuf,
permissions: String,
app_mcp_url: Option<String>,
agents_summary: String,
collaboration_mode: Option<String>,
model_provider: Option<String>,
@@ -251,6 +252,11 @@ impl StatusHistoryCell {
model_details,
directory: config.cwd.clone(),
permissions,
app_mcp_url: if config.active_profile.as_deref() == Some("dev") {
config.apps_mcp_url.clone()
} else {
None
},
agents_summary,
collaboration_mode: collaboration_mode.map(ToString::to_string),
model_provider,
@@ -437,6 +443,9 @@ impl HistoryCell for StatusHistoryCell {
let mut seen: BTreeSet<String> = labels.iter().cloned().collect();
let thread_name = self.thread_name.as_deref().filter(|name| !name.is_empty());
if self.app_mcp_url.is_some() {
push_label(&mut labels, &mut seen, "App MCP URL");
}
if self.model_provider.is_some() {
push_label(&mut labels, &mut seen, "Model provider");
}
@@ -497,6 +506,9 @@ impl HistoryCell for StatusHistoryCell {
}
lines.push(formatter.line("Directory", vec![Span::from(directory_value)]));
lines.push(formatter.line("Permissions", vec![Span::from(self.permissions.clone())]));
if let Some(app_mcp_url) = self.app_mcp_url.as_ref() {
lines.push(formatter.line("App MCP URL", vec![Span::from(app_mcp_url.clone())]));
}
lines.push(formatter.line("Agents.md", vec![Span::from(self.agents_summary.clone())]));
if let Some(account_value) = account_value {