From 1a910fd874e5d88d2ecd372e47b4f81835b15850 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Thu, 19 Mar 2026 10:39:56 -0700 Subject: [PATCH 1/4] update --- codex-rs/app-server/README.md | 2 +- .../app-server/src/codex_message_processor.rs | 153 ++++++++++++++++-- .../tests/suite/v2/plugin_install.rs | 70 ++++++++ 3 files changed, 212 insertions(+), 13 deletions(-) diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 57798a99b08..2fc0218c1cc 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -168,7 +168,7 @@ Example with notification opt-out: - `skills/changed` — notification emitted when watched local skill files change. - `app/list` — list available apps. - `skills/config/write` — write user-level skill config by path. -- `plugin/install` — install a plugin from a discovered marketplace entry, rejecting marketplace entries marked unavailable for install, and return the effective plugin auth policy plus any apps that still need auth (**under development; do not call from production clients yet**). +- `plugin/install` — install a plugin from a discovered marketplace entry, rejecting marketplace entries marked unavailable for install, queue a reload for bundled plugin MCP servers, start MCP OAuth in the browser for bundled servers that support it, and return the effective plugin auth policy plus any apps that still need auth (**under development; do not call from production clients yet**). - `plugin/uninstall` — uninstall a plugin by id by removing its cached files and clearing its user-level config entry (**under development; do not call from production clients yet**). - `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `mcpServer/oauthLogin/completed` once the browser flow finishes. - `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental). diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index b6ece83ddf6..e81cc89a231 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -215,8 +215,11 @@ use codex_core::find_thread_name_by_id; use codex_core::find_thread_names_by_ids; use codex_core::find_thread_path_by_id_str; use codex_core::git_info::git_diff_to_remote; +use codex_core::mcp::auth::McpOAuthLoginSupport; use codex_core::mcp::auth::discover_supported_scopes; +use codex_core::mcp::auth::oauth_login_support; use codex_core::mcp::auth::resolve_oauth_scopes; +use codex_core::mcp::auth::should_retry_without_scopes; use codex_core::mcp::collect_mcp_snapshot; use codex_core::mcp::group_tools_by_server; use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; @@ -272,6 +275,7 @@ use codex_protocol::protocol::USER_MESSAGE_BEGIN; use codex_protocol::protocol::W3cTraceContext; use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS; use codex_protocol::user_input::UserInput as CoreInputItem; +use codex_rmcp_client::perform_oauth_login; use codex_rmcp_client::perform_oauth_login_return_url; use codex_state::StateRuntime; use codex_state::ThreadMetadata; @@ -4587,20 +4591,31 @@ impl CodexMessageProcessor { } }; + if let Err(error) = self.queue_mcp_server_refresh_for_config(&config).await { + self.outgoing.send_error(request_id, error).await; + return; + } + + let response = McpServerRefreshResponse {}; + self.outgoing.send_response(request_id, response).await; + } + + async fn queue_mcp_server_refresh_for_config( + &self, + config: &Config, + ) -> Result<(), JSONRPCErrorError> { let configured_servers = self .thread_manager .mcp_manager() - .configured_servers(&config); + .configured_servers(config); let mcp_servers = match serde_json::to_value(configured_servers) { Ok(value) => value, Err(err) => { - let error = JSONRPCErrorError { + return Err(JSONRPCErrorError { code: INTERNAL_ERROR_CODE, message: format!("failed to serialize MCP servers: {err}"), data: None, - }; - self.outgoing.send_error(request_id, error).await; - return; + }); } }; @@ -4608,15 +4623,13 @@ impl CodexMessageProcessor { match serde_json::to_value(config.mcp_oauth_credentials_store_mode) { Ok(value) => value, Err(err) => { - let error = JSONRPCErrorError { + return Err(JSONRPCErrorError { code: INTERNAL_ERROR_CODE, message: format!( "failed to serialize MCP OAuth credentials store mode: {err}" ), data: None, - }; - self.outgoing.send_error(request_id, error).await; - return; + }); } }; @@ -4629,8 +4642,7 @@ impl CodexMessageProcessor { // active turn to avoid work for threads that never resume. let thread_manager = Arc::clone(&self.thread_manager); thread_manager.refresh_mcp_servers(refresh_config).await; - let response = McpServerRefreshResponse {}; - self.outgoing.send_response(request_id, response).await; + Ok(()) } async fn mcp_server_oauth_login( @@ -5710,6 +5722,10 @@ impl CodexMessageProcessor { let config_cwd = marketplace_path.as_path().parent().map(Path::to_path_buf); let plugins_manager = self.thread_manager.plugins_manager(); + let plugin_read_request = PluginReadRequest { + plugin_name: plugin_name.clone(), + marketplace_path: marketplace_path.clone(), + }; let request = PluginInstallRequest { plugin_name, marketplace_path, @@ -5742,6 +5758,32 @@ impl CodexMessageProcessor { self.config.as_ref().clone() } }; + + self.clear_plugin_related_caches(); + + let plugin_mcp_server_names = + match plugins_manager.read_plugin_for_config(&config, &plugin_read_request) { + Ok(outcome) => outcome.plugin.mcp_server_names, + Err(err) => { + warn!( + plugin = result.plugin_id.as_key(), + "failed to read plugin MCP servers after install: {err:#}" + ); + Vec::new() + } + }; + + if !plugin_mcp_server_names.is_empty() { + if let Err(err) = self.queue_mcp_server_refresh_for_config(&config).await { + warn!( + plugin = result.plugin_id.as_key(), + "failed to queue MCP refresh after plugin install: {err:?}" + ); + } + self.start_plugin_mcp_oauth_logins(&config, plugin_mcp_server_names) + .await; + } + let plugin_apps = load_plugin_apps(result.installed_path.as_path()); let apps_needing_auth = if plugin_apps.is_empty() || !config.features.apps_enabled(Some(&self.auth_manager)).await @@ -5802,7 +5844,6 @@ impl CodexMessageProcessor { ) }; - self.clear_plugin_related_caches(); self.outgoing .send_response( request_id, @@ -5858,6 +5899,94 @@ impl CodexMessageProcessor { } } + async fn start_plugin_mcp_oauth_logins( + &self, + config: &Config, + plugin_mcp_server_names: Vec, + ) { + let configured_servers = self.thread_manager.mcp_manager().configured_servers(config); + + for name in plugin_mcp_server_names { + let Some(server) = configured_servers.get(&name).cloned() else { + warn!( + mcp_server = name, + "plugin MCP server was not found after install" + ); + continue; + }; + + let oauth_config = match oauth_login_support(&server.transport).await { + McpOAuthLoginSupport::Supported(config) => config, + McpOAuthLoginSupport::Unsupported => continue, + McpOAuthLoginSupport::Unknown(err) => { + warn!( + "MCP server may or may not require login for plugin install {name}: {err}" + ); + continue; + } + }; + + let resolved_scopes = resolve_oauth_scopes( + /*explicit_scopes*/ None, + server.scopes.clone(), + oauth_config.discovered_scopes.clone(), + ); + + let store_mode = config.mcp_oauth_credentials_store_mode; + let callback_port = config.mcp_oauth_callback_port; + let callback_url = config.mcp_oauth_callback_url.clone(); + let outgoing = Arc::clone(&self.outgoing); + let notification_name = name.clone(); + + tokio::spawn(async move { + let first_attempt = perform_oauth_login( + &name, + &oauth_config.url, + store_mode, + oauth_config.http_headers.clone(), + oauth_config.env_http_headers.clone(), + &resolved_scopes.scopes, + server.oauth_resource.as_deref(), + callback_port, + callback_url.as_deref(), + ) + .await; + + let final_result = match first_attempt { + Err(err) if should_retry_without_scopes(&resolved_scopes, &err) => { + perform_oauth_login( + &name, + &oauth_config.url, + store_mode, + oauth_config.http_headers, + oauth_config.env_http_headers, + &[], + server.oauth_resource.as_deref(), + callback_port, + callback_url.as_deref(), + ) + .await + } + result => result, + }; + + let (success, error) = match final_result { + Ok(()) => (true, None), + Err(err) => (false, Some(err.to_string())), + }; + + let notification = ServerNotification::McpServerOauthLoginCompleted( + McpServerOauthLoginCompletedNotification { + name: notification_name, + success, + error, + }, + ); + outgoing.send_server_notification(notification).await; + }); + } + } + async fn plugin_uninstall( &self, request_id: ConnectionRequestId, diff --git a/codex-rs/app-server/tests/suite/v2/plugin_install.rs b/codex-rs/app-server/tests/suite/v2/plugin_install.rs index a30107d3724..241add31177 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_install.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_install.rs @@ -527,6 +527,76 @@ async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()> { Ok(()) } +#[tokio::test] +async fn plugin_install_makes_bundled_mcp_servers_available_to_followup_requests() -> Result<()> { + let codex_home = TempDir::new()?; + std::fs::write( + codex_home.path().join("config.toml"), + "[features]\nplugins = true\n", + )?; + let repo_root = TempDir::new()?; + write_plugin_marketplace( + repo_root.path(), + "debug", + "sample-plugin", + "./sample-plugin", + None, + None, + )?; + write_plugin_source(repo_root.path(), "sample-plugin", &[])?; + std::fs::write( + repo_root.path().join("sample-plugin/.mcp.json"), + r#"{ + "mcpServers": { + "sample-mcp": { + "command": "echo" + } + } +}"#, + )?; + let marketplace_path = + AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_plugin_install_request(PluginInstallParams { + marketplace_path, + plugin_name: "sample-plugin".to_string(), + force_remote_sync: false, + }) + .await?; + let response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + let response: PluginInstallResponse = to_response(response)?; + assert_eq!(response.apps_needing_auth, Vec::::new()); + + let request_id = mcp + .send_raw_request( + "mcpServer/oauth/login", + Some(json!({ + "name": "sample-mcp", + })), + ) + .await?; + let err = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + + assert_eq!(err.error.code, -32600); + assert_eq!( + err.error.message, + "OAuth login is only supported for streamable HTTP servers." + ); + Ok(()) +} + #[derive(Clone)] struct AppsServerState { response: Arc>, From 3067fcdabc109264135c5b0b721c568862731d58 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Thu, 19 Mar 2026 13:50:32 -0700 Subject: [PATCH 2/4] update --- codex-rs/app-server/README.md | 2 +- .../app-server/src/codex_message_processor.rs | 75 +++++++++++-------- .../tests/suite/v2/plugin_install.rs | 7 +- codex-rs/core/src/plugins/manager.rs | 16 ++++ codex-rs/core/src/plugins/mod.rs | 1 + 5 files changed, 66 insertions(+), 35 deletions(-) diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 2fc0218c1cc..38fa2700de8 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -168,7 +168,7 @@ Example with notification opt-out: - `skills/changed` — notification emitted when watched local skill files change. - `app/list` — list available apps. - `skills/config/write` — write user-level skill config by path. -- `plugin/install` — install a plugin from a discovered marketplace entry, rejecting marketplace entries marked unavailable for install, queue a reload for bundled plugin MCP servers, start MCP OAuth in the browser for bundled servers that support it, and return the effective plugin auth policy plus any apps that still need auth (**under development; do not call from production clients yet**). +- `plugin/install` — install a plugin from a discovered marketplace entry, rejecting marketplace entries marked unavailable for install, install MCPs if any, and return the effective plugin auth policy plus any apps that still need auth (**under development; do not call from production clients yet**). - `plugin/uninstall` — uninstall a plugin by id by removing its cached files and clearing its user-level config entry (**under development; do not call from production clients yet**). - `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `mcpServer/oauthLogin/completed` once the browser flow finishes. - `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental). diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index e81cc89a231..d558d895471 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -197,6 +197,8 @@ use codex_core::config::ConfigOverrides; use codex_core::config::NetworkProxyAuditMetadata; use codex_core::config::edit::ConfigEdit; use codex_core::config::edit::ConfigEditsBuilder; +use codex_core::config::load_global_mcp_servers; +use codex_core::config::types::McpServerConfig; use codex_core::config::types::McpServerTransportConfig; use codex_core::config_loader::CloudRequirementsLoadError; use codex_core::config_loader::CloudRequirementsLoadErrorCode; @@ -232,6 +234,7 @@ use codex_core::plugins::PluginInstallRequest; use codex_core::plugins::PluginReadRequest; use codex_core::plugins::PluginUninstallError as CorePluginUninstallError; use codex_core::plugins::load_plugin_apps; +use codex_core::plugins::load_plugin_mcp_servers; use codex_core::read_head_for_summary; use codex_core::read_session_meta_line; use codex_core::rollout_date_parts; @@ -4604,10 +4607,7 @@ impl CodexMessageProcessor { &self, config: &Config, ) -> Result<(), JSONRPCErrorError> { - let configured_servers = self - .thread_manager - .mcp_manager() - .configured_servers(config); + let configured_servers = self.thread_manager.mcp_manager().configured_servers(config); let mcp_servers = match serde_json::to_value(configured_servers) { Ok(value) => value, Err(err) => { @@ -5722,10 +5722,6 @@ impl CodexMessageProcessor { let config_cwd = marketplace_path.as_path().parent().map(Path::to_path_buf); let plugins_manager = self.thread_manager.plugins_manager(); - let plugin_read_request = PluginReadRequest { - plugin_name: plugin_name.clone(), - marketplace_path: marketplace_path.clone(), - }; let request = PluginInstallRequest { plugin_name, marketplace_path, @@ -5749,7 +5745,7 @@ impl CodexMessageProcessor { match install_result { Ok(result) => { - let config = match self.load_latest_config(config_cwd).await { + let mut config = match self.load_latest_config(config_cwd.clone()).await { Ok(config) => config, Err(err) => { warn!( @@ -5761,26 +5757,55 @@ impl CodexMessageProcessor { self.clear_plugin_related_caches(); - let plugin_mcp_server_names = - match plugins_manager.read_plugin_for_config(&config, &plugin_read_request) { - Ok(outcome) => outcome.plugin.mcp_server_names, + let plugin_mcp_servers = load_plugin_mcp_servers(result.installed_path.as_path()); + + if !plugin_mcp_servers.is_empty() { + match load_global_mcp_servers(&config.codex_home).await { + Ok(mut global_mcp_servers) => { + let mut updated = false; + for (name, server_config) in &plugin_mcp_servers { + if global_mcp_servers.contains_key(name) { + continue; + } + global_mcp_servers.insert(name.clone(), server_config.clone()); + updated = true; + } + + if updated + && let Err(err) = ConfigEditsBuilder::new(&config.codex_home) + .replace_mcp_servers(&global_mcp_servers) + .apply() + .await + { + warn!( + plugin = result.plugin_id.as_key(), + "failed to persist plugin MCP servers to config.toml: {err:#}" + ); + } else if updated { + match self.load_latest_config(config_cwd).await { + Ok(latest_config) => config = latest_config, + Err(err) => { + warn!( + "failed to reload config after persisting plugin MCP servers, using previous config: {err:?}" + ); + } + } + } + } Err(err) => { warn!( plugin = result.plugin_id.as_key(), - "failed to read plugin MCP servers after install: {err:#}" + "failed to load global MCP servers after plugin install: {err:#}" ); - Vec::new() } - }; - - if !plugin_mcp_server_names.is_empty() { + } if let Err(err) = self.queue_mcp_server_refresh_for_config(&config).await { warn!( plugin = result.plugin_id.as_key(), "failed to queue MCP refresh after plugin install: {err:?}" ); } - self.start_plugin_mcp_oauth_logins(&config, plugin_mcp_server_names) + self.start_plugin_mcp_oauth_logins(&config, plugin_mcp_servers) .await; } @@ -5902,19 +5927,9 @@ impl CodexMessageProcessor { async fn start_plugin_mcp_oauth_logins( &self, config: &Config, - plugin_mcp_server_names: Vec, + plugin_mcp_servers: HashMap, ) { - let configured_servers = self.thread_manager.mcp_manager().configured_servers(config); - - for name in plugin_mcp_server_names { - let Some(server) = configured_servers.get(&name).cloned() else { - warn!( - mcp_server = name, - "plugin MCP server was not found after install" - ); - continue; - }; - + for (name, server) in plugin_mcp_servers { let oauth_config = match oauth_login_support(&server.transport).await { McpOAuthLoginSupport::Supported(config) => config, McpOAuthLoginSupport::Unsupported => continue, diff --git a/codex-rs/app-server/tests/suite/v2/plugin_install.rs b/codex-rs/app-server/tests/suite/v2/plugin_install.rs index 241add31177..2c624e6c2eb 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_install.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_install.rs @@ -530,10 +530,6 @@ async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()> { #[tokio::test] async fn plugin_install_makes_bundled_mcp_servers_available_to_followup_requests() -> Result<()> { let codex_home = TempDir::new()?; - std::fs::write( - codex_home.path().join("config.toml"), - "[features]\nplugins = true\n", - )?; let repo_root = TempDir::new()?; write_plugin_marketplace( repo_root.path(), @@ -574,6 +570,9 @@ async fn plugin_install_makes_bundled_mcp_servers_available_to_followup_requests .await??; let response: PluginInstallResponse = to_response(response)?; assert_eq!(response.apps_needing_auth, Vec::::new()); + let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?; + assert!(config.contains("[mcp_servers.sample-mcp]")); + assert!(config.contains("command = \"echo\"")); let request_id = mcp .send_raw_request( diff --git a/codex-rs/core/src/plugins/manager.rs b/codex-rs/core/src/plugins/manager.rs index f28bcc2c48f..936dc48fd30 100644 --- a/codex-rs/core/src/plugins/manager.rs +++ b/codex-rs/core/src/plugins/manager.rs @@ -1660,6 +1660,22 @@ pub fn plugin_telemetry_metadata_from_root( } } +pub fn load_plugin_mcp_servers(plugin_root: &Path) -> HashMap { + let Some(manifest) = load_plugin_manifest(plugin_root) else { + return HashMap::new(); + }; + + let mut mcp_servers = HashMap::new(); + for mcp_config_path in plugin_mcp_config_paths(plugin_root, &manifest.paths) { + let plugin_mcp = load_mcp_servers_from_file(plugin_root, &mcp_config_path); + for (name, config) in plugin_mcp.mcp_servers { + mcp_servers.entry(name).or_insert(config); + } + } + + mcp_servers +} + pub fn installed_plugin_telemetry_metadata( codex_home: &Path, plugin_id: &PluginId, diff --git a/codex-rs/core/src/plugins/mod.rs b/codex-rs/core/src/plugins/mod.rs index f518e3b2bd3..895a633e6bd 100644 --- a/codex-rs/core/src/plugins/mod.rs +++ b/codex-rs/core/src/plugins/mod.rs @@ -36,6 +36,7 @@ pub use manager::PluginsManager; pub use manager::RemotePluginSyncResult; pub use manager::installed_plugin_telemetry_metadata; pub use manager::load_plugin_apps; +pub use manager::load_plugin_mcp_servers; pub(crate) use manager::plugin_namespace_for_skill_path; pub use manager::plugin_telemetry_metadata_from_root; pub use manifest::PluginManifestInterface; From 824d265d6d8821d5bd7a8a54646b7428003f8cc9 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Thu, 19 Mar 2026 15:43:44 -0700 Subject: [PATCH 3/4] update --- .../app-server/src/codex_message_processor.rs | 42 +------------------ .../tests/suite/v2/plugin_install.rs | 8 +++- 2 files changed, 7 insertions(+), 43 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 9de86d0926b..50d7d0d8a77 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -197,7 +197,6 @@ use codex_core::config::ConfigOverrides; use codex_core::config::NetworkProxyAuditMetadata; use codex_core::config::edit::ConfigEdit; use codex_core::config::edit::ConfigEditsBuilder; -use codex_core::config::load_global_mcp_servers; use codex_core::config::types::McpServerConfig; use codex_core::config::types::McpServerTransportConfig; use codex_core::config_loader::CloudRequirementsLoadError; @@ -5745,7 +5744,7 @@ impl CodexMessageProcessor { match install_result { Ok(result) => { - let mut config = match self.load_latest_config(config_cwd.clone()).await { + let config = match self.load_latest_config(config_cwd).await { Ok(config) => config, Err(err) => { warn!( @@ -5760,45 +5759,6 @@ impl CodexMessageProcessor { let plugin_mcp_servers = load_plugin_mcp_servers(result.installed_path.as_path()); if !plugin_mcp_servers.is_empty() { - match load_global_mcp_servers(&config.codex_home).await { - Ok(mut global_mcp_servers) => { - let mut updated = false; - for (name, server_config) in &plugin_mcp_servers { - if global_mcp_servers.contains_key(name) { - continue; - } - global_mcp_servers.insert(name.clone(), server_config.clone()); - updated = true; - } - - if updated - && let Err(err) = ConfigEditsBuilder::new(&config.codex_home) - .replace_mcp_servers(&global_mcp_servers) - .apply() - .await - { - warn!( - plugin = result.plugin_id.as_key(), - "failed to persist plugin MCP servers to config.toml: {err:#}" - ); - } else if updated { - match self.load_latest_config(config_cwd).await { - Ok(latest_config) => config = latest_config, - Err(err) => { - warn!( - "failed to reload config after persisting plugin MCP servers, using previous config: {err:?}" - ); - } - } - } - } - Err(err) => { - warn!( - plugin = result.plugin_id.as_key(), - "failed to load global MCP servers after plugin install: {err:#}" - ); - } - } if let Err(err) = self.queue_mcp_server_refresh_for_config(&config).await { warn!( plugin = result.plugin_id.as_key(), diff --git a/codex-rs/app-server/tests/suite/v2/plugin_install.rs b/codex-rs/app-server/tests/suite/v2/plugin_install.rs index 2c624e6c2eb..93c07e63d94 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_install.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_install.rs @@ -530,6 +530,10 @@ async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()> { #[tokio::test] async fn plugin_install_makes_bundled_mcp_servers_available_to_followup_requests() -> Result<()> { let codex_home = TempDir::new()?; + std::fs::write( + codex_home.path().join("config.toml"), + "[features]\nplugins = true\n", + )?; let repo_root = TempDir::new()?; write_plugin_marketplace( repo_root.path(), @@ -571,8 +575,8 @@ async fn plugin_install_makes_bundled_mcp_servers_available_to_followup_requests let response: PluginInstallResponse = to_response(response)?; assert_eq!(response.apps_needing_auth, Vec::::new()); let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?; - assert!(config.contains("[mcp_servers.sample-mcp]")); - assert!(config.contains("command = \"echo\"")); + assert!(!config.contains("[mcp_servers.sample-mcp]")); + assert!(!config.contains("command = \"echo\"")); let request_id = mcp .send_raw_request( From 5504f1300fc9b5f6cc7eba2d08154f78811a47ec Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Thu, 19 Mar 2026 16:48:21 -0700 Subject: [PATCH 4/4] update --- .../app-server/src/codex_message_processor.rs | 84 +--------------- .../plugin_mcp_oauth.rs | 95 +++++++++++++++++++ 2 files changed, 96 insertions(+), 83 deletions(-) create mode 100644 codex-rs/app-server/src/codex_message_processor/plugin_mcp_oauth.rs diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 50d7d0d8a77..be6f813c4e7 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -197,7 +197,6 @@ use codex_core::config::ConfigOverrides; use codex_core::config::NetworkProxyAuditMetadata; use codex_core::config::edit::ConfigEdit; use codex_core::config::edit::ConfigEditsBuilder; -use codex_core::config::types::McpServerConfig; use codex_core::config::types::McpServerTransportConfig; use codex_core::config_loader::CloudRequirementsLoadError; use codex_core::config_loader::CloudRequirementsLoadErrorCode; @@ -216,11 +215,8 @@ use codex_core::find_thread_name_by_id; use codex_core::find_thread_names_by_ids; use codex_core::find_thread_path_by_id_str; use codex_core::git_info::git_diff_to_remote; -use codex_core::mcp::auth::McpOAuthLoginSupport; use codex_core::mcp::auth::discover_supported_scopes; -use codex_core::mcp::auth::oauth_login_support; use codex_core::mcp::auth::resolve_oauth_scopes; -use codex_core::mcp::auth::should_retry_without_scopes; use codex_core::mcp::collect_mcp_snapshot; use codex_core::mcp::group_tools_by_server; use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; @@ -277,7 +273,6 @@ use codex_protocol::protocol::USER_MESSAGE_BEGIN; use codex_protocol::protocol::W3cTraceContext; use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS; use codex_protocol::user_input::UserInput as CoreInputItem; -use codex_rmcp_client::perform_oauth_login; use codex_rmcp_client::perform_oauth_login_return_url; use codex_state::StateRuntime; use codex_state::ThreadMetadata; @@ -317,6 +312,7 @@ use codex_app_server_protocol::ServerRequest; mod apps_list_helpers; mod plugin_app_helpers; +mod plugin_mcp_oauth; use crate::filters::compute_source_filters; use crate::filters::source_kind_matches; @@ -5884,84 +5880,6 @@ impl CodexMessageProcessor { } } - async fn start_plugin_mcp_oauth_logins( - &self, - config: &Config, - plugin_mcp_servers: HashMap, - ) { - for (name, server) in plugin_mcp_servers { - let oauth_config = match oauth_login_support(&server.transport).await { - McpOAuthLoginSupport::Supported(config) => config, - McpOAuthLoginSupport::Unsupported => continue, - McpOAuthLoginSupport::Unknown(err) => { - warn!( - "MCP server may or may not require login for plugin install {name}: {err}" - ); - continue; - } - }; - - let resolved_scopes = resolve_oauth_scopes( - /*explicit_scopes*/ None, - server.scopes.clone(), - oauth_config.discovered_scopes.clone(), - ); - - let store_mode = config.mcp_oauth_credentials_store_mode; - let callback_port = config.mcp_oauth_callback_port; - let callback_url = config.mcp_oauth_callback_url.clone(); - let outgoing = Arc::clone(&self.outgoing); - let notification_name = name.clone(); - - tokio::spawn(async move { - let first_attempt = perform_oauth_login( - &name, - &oauth_config.url, - store_mode, - oauth_config.http_headers.clone(), - oauth_config.env_http_headers.clone(), - &resolved_scopes.scopes, - server.oauth_resource.as_deref(), - callback_port, - callback_url.as_deref(), - ) - .await; - - let final_result = match first_attempt { - Err(err) if should_retry_without_scopes(&resolved_scopes, &err) => { - perform_oauth_login( - &name, - &oauth_config.url, - store_mode, - oauth_config.http_headers, - oauth_config.env_http_headers, - &[], - server.oauth_resource.as_deref(), - callback_port, - callback_url.as_deref(), - ) - .await - } - result => result, - }; - - let (success, error) = match final_result { - Ok(()) => (true, None), - Err(err) => (false, Some(err.to_string())), - }; - - let notification = ServerNotification::McpServerOauthLoginCompleted( - McpServerOauthLoginCompletedNotification { - name: notification_name, - success, - error, - }, - ); - outgoing.send_server_notification(notification).await; - }); - } - } - async fn plugin_uninstall( &self, request_id: ConnectionRequestId, diff --git a/codex-rs/app-server/src/codex_message_processor/plugin_mcp_oauth.rs b/codex-rs/app-server/src/codex_message_processor/plugin_mcp_oauth.rs new file mode 100644 index 00000000000..0c13f5ed4c1 --- /dev/null +++ b/codex-rs/app-server/src/codex_message_processor/plugin_mcp_oauth.rs @@ -0,0 +1,95 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use codex_app_server_protocol::McpServerOauthLoginCompletedNotification; +use codex_app_server_protocol::ServerNotification; +use codex_core::config::Config; +use codex_core::config::types::McpServerConfig; +use codex_core::mcp::auth::McpOAuthLoginSupport; +use codex_core::mcp::auth::oauth_login_support; +use codex_core::mcp::auth::resolve_oauth_scopes; +use codex_core::mcp::auth::should_retry_without_scopes; +use codex_rmcp_client::perform_oauth_login; +use tracing::warn; + +use super::CodexMessageProcessor; + +impl CodexMessageProcessor { + pub(super) async fn start_plugin_mcp_oauth_logins( + &self, + config: &Config, + plugin_mcp_servers: HashMap, + ) { + for (name, server) in plugin_mcp_servers { + let oauth_config = match oauth_login_support(&server.transport).await { + McpOAuthLoginSupport::Supported(config) => config, + McpOAuthLoginSupport::Unsupported => continue, + McpOAuthLoginSupport::Unknown(err) => { + warn!( + "MCP server may or may not require login for plugin install {name}: {err}" + ); + continue; + } + }; + + let resolved_scopes = resolve_oauth_scopes( + /*explicit_scopes*/ None, + server.scopes.clone(), + oauth_config.discovered_scopes.clone(), + ); + + let store_mode = config.mcp_oauth_credentials_store_mode; + let callback_port = config.mcp_oauth_callback_port; + let callback_url = config.mcp_oauth_callback_url.clone(); + let outgoing = Arc::clone(&self.outgoing); + let notification_name = name.clone(); + + tokio::spawn(async move { + let first_attempt = perform_oauth_login( + &name, + &oauth_config.url, + store_mode, + oauth_config.http_headers.clone(), + oauth_config.env_http_headers.clone(), + &resolved_scopes.scopes, + server.oauth_resource.as_deref(), + callback_port, + callback_url.as_deref(), + ) + .await; + + let final_result = match first_attempt { + Err(err) if should_retry_without_scopes(&resolved_scopes, &err) => { + perform_oauth_login( + &name, + &oauth_config.url, + store_mode, + oauth_config.http_headers, + oauth_config.env_http_headers, + &[], + server.oauth_resource.as_deref(), + callback_port, + callback_url.as_deref(), + ) + .await + } + result => result, + }; + + let (success, error) = match final_result { + Ok(()) => (true, None), + Err(err) => (false, Some(err.to_string())), + }; + + let notification = ServerNotification::McpServerOauthLoginCompleted( + McpServerOauthLoginCompletedNotification { + name: notification_name, + success, + error, + }, + ); + outgoing.send_server_notification(notification).await; + }); + } + } +}