From 550b1a1342e03ca7cd83ab4ba8417c11ea14bf85 Mon Sep 17 00:00:00 2001 From: Eric Ning Date: Thu, 4 Jun 2026 12:46:41 -0700 Subject: [PATCH 1/5] fix(app-server): expose curated MCP servers in plugin read --- codex-rs/app-server/README.md | 2 +- .../src/request_processors/plugins.rs | 13 +++- .../plugins/remote_plugin_detail.rs | 67 +++++++++++++++++++ .../app-server/tests/suite/v2/plugin_read.rs | 54 ++++++++++++--- 4 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 codex-rs/app-server/src/request_processors/plugins/remote_plugin_detail.rs diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index dc02ff8c049f..785e1493d04f 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -203,7 +203,7 @@ Example with notification opt-out: - `marketplace/upgrade` — upgrade all configured Git plugin marketplaces, or one named marketplace when `marketplaceName` is provided. Returns selected marketplace names, upgraded roots, and per-marketplace errors. - `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, plugin `availability` (`AVAILABLE` by default or `DISABLED_BY_ADMIN` for remote plugins blocked upstream), fail-open `marketplaceLoadErrors` entries for marketplace files that could not be parsed or loaded, and best-effort `featuredPluginIds` for the official curated marketplace. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category (**under development; do not call from production clients yet**). - `plugin/installed` — list installed plugin rows plus any explicitly requested local install-suggestion plugin names, without fetching the broader remote catalog. Mention surfaces can use this narrower view when they need plugin mention payloads rather than plugin-page discovery data (**under development; do not call from production clients yet**). -- `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/hooks/apps/MCP server names. Returned plugin skills include their current `enabled` state after local config filtering; bundled hooks are returned as lightweight declaration summaries keyed for correlation with `hooks/list`. Plugin app summaries also include `needsAuth` when the server can determine connector accessibility (**under development; do not call from production clients yet**). +- `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/hooks/apps/MCP server names. Remote global-catalog reads hydrate MCP server names from a same-version synced local curated bundle when available. Returned plugin skills include their current `enabled` state after local config filtering; bundled hooks are returned as lightweight declaration summaries keyed for correlation with `hooks/list`. Plugin app summaries also include `needsAuth` when the server can determine connector accessibility (**under development; do not call from production clients yet**). - `plugin/skill/read` — read remote plugin skill markdown on demand by `remoteMarketplaceName`, `remotePluginId`, and `skillName`. This lets clients preview uninstalled remote plugin skills without downloading the plugin bundle. - `skills/changed` — notification emitted when watched local skill files change. - `app/list` — list available apps. diff --git a/codex-rs/app-server/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs index 23356948567b..7a6f3c5f58a7 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -1,3 +1,5 @@ +mod remote_plugin_detail; + use super::*; use crate::error_code::internal_error; use crate::error_code::invalid_request; @@ -1099,7 +1101,13 @@ impl PluginRequestProcessor { Arc::clone(&environment_manager), ) .await; - remote_plugin_detail_to_info(remote_detail, app_summaries) + let mcp_server_names = remote_plugin_detail::local_curated_mcp_server_names( + plugins_manager.as_ref(), + &plugins_input, + &remote_detail, + ) + .await; + remote_plugin_detail_to_info(remote_detail, app_summaries, mcp_server_names) } }; @@ -2006,6 +2014,7 @@ fn remote_plugin_share_discoverability_to_info( fn remote_plugin_detail_to_info( detail: RemoteCatalogPluginDetail, apps: Vec, + mcp_servers: Vec, ) -> PluginDetail { PluginDetail { marketplace_name: detail.marketplace_name, @@ -2026,7 +2035,7 @@ fn remote_plugin_detail_to_info( .collect(), hooks: Vec::new(), apps, - mcp_servers: Vec::new(), + mcp_servers, } } diff --git a/codex-rs/app-server/src/request_processors/plugins/remote_plugin_detail.rs b/codex-rs/app-server/src/request_processors/plugins/remote_plugin_detail.rs new file mode 100644 index 000000000000..5b7c07b7ef7d --- /dev/null +++ b/codex-rs/app-server/src/request_processors/plugins/remote_plugin_detail.rs @@ -0,0 +1,67 @@ +use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME; +use codex_core_plugins::PluginReadRequest; +use codex_core_plugins::PluginsConfigInput; +use codex_core_plugins::PluginsManager; +use codex_core_plugins::remote::REMOTE_GLOBAL_MARKETPLACE_NAME; +use codex_core_plugins::remote::RemotePluginDetail; +use tracing::warn; + +pub(super) async fn local_curated_mcp_server_names( + plugins_manager: &PluginsManager, + plugins_input: &PluginsConfigInput, + remote_detail: &RemotePluginDetail, +) -> Vec { + if remote_detail.marketplace_name != REMOTE_GLOBAL_MARKETPLACE_NAME { + return Vec::new(); + } + + let remote_plugin_name = &remote_detail.summary.name; + let marketplace_path = match plugins_manager.list_marketplaces_for_config(plugins_input, &[]) { + Ok(outcome) => outcome + .marketplaces + .into_iter() + .find(|marketplace| marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME) + .map(|marketplace| marketplace.path), + Err(err) => { + warn!( + plugin = %remote_plugin_name, + error = %err, + "failed to list local curated plugins for remote plugin MCP hydration" + ); + return Vec::new(); + } + }; + let Some(marketplace_path) = marketplace_path else { + return Vec::new(); + }; + + let outcome = match plugins_manager + .read_plugin_for_config( + plugins_input, + &PluginReadRequest { + plugin_name: remote_plugin_name.clone(), + marketplace_path, + }, + ) + .await + { + Ok(outcome) => outcome, + Err(err) => { + warn!( + plugin = %remote_plugin_name, + error = %err, + "failed to hydrate remote plugin MCP server names from local curated plugin" + ); + return Vec::new(); + } + }; + if remote_detail + .release_version + .as_deref() + .is_some_and(|version| outcome.plugin.local_version.as_deref() != Some(version)) + { + return Vec::new(); + } + + outcome.plugin.mcp_server_names +} diff --git a/codex-rs/app-server/tests/suite/v2/plugin_read.rs b/codex-rs/app-server/tests/suite/v2/plugin_read.rs index f78cd2a0881e..afc7bcd9e571 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_read.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_read.rs @@ -122,9 +122,37 @@ async fn plugin_read_rejects_multiple_read_sources() -> Result<()> { } #[tokio::test] -async fn plugin_read_reads_remote_plugin_details_when_remote_plugin_is_disabled() -> Result<()> { +async fn plugin_read_hydrates_remote_curated_mcp_servers_when_remote_plugin_is_disabled() +-> Result<()> { let codex_home = TempDir::new()?; let server = MockServer::start().await; + let curated_root = codex_home.path().join(".tmp/plugins"); + write_plugin_marketplace( + &curated_root, + "openai-curated", + "example-plugin", + "./example-plugin", + )?; + let plugin_root = curated_root.join("example-plugin"); + std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?; + std::fs::write( + plugin_root.join(".codex-plugin/plugin.json"), + r#"{ + "name": "example-plugin", + "version": "1.2.1", + "mcpServers": "./.mcp.json" +}"#, + )?; + std::fs::write( + plugin_root.join(".mcp.json"), + r#"{ + "mcpServers": { + "example-server": { + "command": "example-mcp" + } + } +}"#, + )?; std::fs::write( codex_home.path().join("config.toml"), format!( @@ -148,19 +176,20 @@ plugins = true let detail_body = r#"{ "id": "plugins~Plugin_00000000000000000000000000000000", - "name": "linear", + "name": "example-plugin", "scope": "GLOBAL", "installation_policy": "AVAILABLE", "authentication_policy": "ON_USE", "release": { - "display_name": "Linear", - "description": "Track work in Linear", + "version": "1.2.1", + "display_name": "Example Plugin", + "description": "Example plugin", "app_ids": [], "keywords": [], "interface": { - "short_description": "Plan and track work", + "short_description": "Example plugin", "capabilities": [], - "default_prompt": "Use the legacy Linear prompt", + "default_prompt": "Use the legacy example prompt", "default_prompts": [] }, "skills": [] @@ -211,12 +240,15 @@ plugins = true let response: PluginReadResponse = to_response(response)?; assert_eq!(response.plugin.marketplace_name, "openai-curated-remote"); - assert_eq!(response.plugin.summary.id, "linear@openai-curated-remote"); + assert_eq!( + response.plugin.summary.id, + "example-plugin@openai-curated-remote" + ); assert_eq!( response.plugin.summary.remote_plugin_id.as_deref(), Some("plugins~Plugin_00000000000000000000000000000000") ); - assert_eq!(response.plugin.summary.name, "linear"); + assert_eq!(response.plugin.summary.name, "example-plugin"); assert_eq!(response.plugin.summary.source, PluginSource::Remote); assert_eq!(response.plugin.summary.share_context, None); assert_eq!( @@ -226,7 +258,11 @@ plugins = true .interface .as_ref() .and_then(|interface| interface.default_prompt.clone()), - Some(vec!["Use the legacy Linear prompt".to_string()]) + Some(vec!["Use the legacy example prompt".to_string()]) + ); + assert_eq!( + response.plugin.mcp_servers, + vec!["example-server".to_string()] ); Ok(()) } From 6600b2eb04ba20d89cef5e59efa8858383cbe4d3 Mon Sep 17 00:00:00 2001 From: Eric Ning Date: Thu, 4 Jun 2026 13:03:11 -0700 Subject: [PATCH 2/5] update tests --- .../plugins/remote_plugin_detail.rs | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/codex-rs/app-server/src/request_processors/plugins/remote_plugin_detail.rs b/codex-rs/app-server/src/request_processors/plugins/remote_plugin_detail.rs index 5b7c07b7ef7d..ba163897434f 100644 --- a/codex-rs/app-server/src/request_processors/plugins/remote_plugin_detail.rs +++ b/codex-rs/app-server/src/request_processors/plugins/remote_plugin_detail.rs @@ -1,5 +1,4 @@ use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME; -use codex_core_plugins::PluginReadRequest; use codex_core_plugins::PluginsConfigInput; use codex_core_plugins::PluginsManager; use codex_core_plugins::remote::REMOTE_GLOBAL_MARKETPLACE_NAME; @@ -16,12 +15,26 @@ pub(super) async fn local_curated_mcp_server_names( } let remote_plugin_name = &remote_detail.summary.name; - let marketplace_path = match plugins_manager.list_marketplaces_for_config(plugins_input, &[]) { + let local_plugin = match plugins_manager.list_marketplaces_for_config(plugins_input, &[]) { Ok(outcome) => outcome .marketplaces .into_iter() .find(|marketplace| marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME) - .map(|marketplace| marketplace.path), + .and_then(|marketplace| { + marketplace + .plugins + .into_iter() + .find(|plugin| { + plugin.name == *remote_plugin_name + && remote_detail + .release_version + .as_deref() + .is_none_or(|version| { + plugin.local_version.as_deref() == Some(version) + }) + }) + .map(|plugin| (marketplace.name, plugin)) + }), Err(err) => { warn!( plugin = %remote_plugin_name, @@ -31,37 +44,22 @@ pub(super) async fn local_curated_mcp_server_names( return Vec::new(); } }; - let Some(marketplace_path) = marketplace_path else { + let Some((marketplace_name, plugin)) = local_plugin else { return Vec::new(); }; - let outcome = match plugins_manager - .read_plugin_for_config( - plugins_input, - &PluginReadRequest { - plugin_name: remote_plugin_name.clone(), - marketplace_path, - }, - ) + match plugins_manager + .read_plugin_detail_for_marketplace_plugin(plugins_input, &marketplace_name, plugin) .await { - Ok(outcome) => outcome, + Ok(plugin) => plugin.mcp_server_names, Err(err) => { warn!( plugin = %remote_plugin_name, error = %err, "failed to hydrate remote plugin MCP server names from local curated plugin" ); - return Vec::new(); + Vec::new() } - }; - if remote_detail - .release_version - .as_deref() - .is_some_and(|version| outcome.plugin.local_version.as_deref() != Some(version)) - { - return Vec::new(); } - - outcome.plugin.mcp_server_names } From 7be1a756bd2b30fbd72c31accfd50bb8cca39573 Mon Sep 17 00:00:00 2001 From: Eric Ning Date: Thu, 4 Jun 2026 14:07:50 -0700 Subject: [PATCH 3/5] fix(app-server): read remote MCP servers from plugin detail --- codex-rs/app-server/README.md | 2 +- .../src/request_processors/plugins.rs | 13 +--- .../plugins/remote_plugin_detail.rs | 65 ------------------- .../app-server/tests/suite/v2/plugin_read.rs | 40 +++--------- codex-rs/core-plugins/src/remote.rs | 17 +++++ 5 files changed, 30 insertions(+), 107 deletions(-) delete mode 100644 codex-rs/app-server/src/request_processors/plugins/remote_plugin_detail.rs diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 785e1493d04f..76108d7854dd 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -203,7 +203,7 @@ Example with notification opt-out: - `marketplace/upgrade` — upgrade all configured Git plugin marketplaces, or one named marketplace when `marketplaceName` is provided. Returns selected marketplace names, upgraded roots, and per-marketplace errors. - `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, plugin `availability` (`AVAILABLE` by default or `DISABLED_BY_ADMIN` for remote plugins blocked upstream), fail-open `marketplaceLoadErrors` entries for marketplace files that could not be parsed or loaded, and best-effort `featuredPluginIds` for the official curated marketplace. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category (**under development; do not call from production clients yet**). - `plugin/installed` — list installed plugin rows plus any explicitly requested local install-suggestion plugin names, without fetching the broader remote catalog. Mention surfaces can use this narrower view when they need plugin mention payloads rather than plugin-page discovery data (**under development; do not call from production clients yet**). -- `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/hooks/apps/MCP server names. Remote global-catalog reads hydrate MCP server names from a same-version synced local curated bundle when available. Returned plugin skills include their current `enabled` state after local config filtering; bundled hooks are returned as lightweight declaration summaries keyed for correlation with `hooks/list`. Plugin app summaries also include `needsAuth` when the server can determine connector accessibility (**under development; do not call from production clients yet**). +- `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/hooks/apps/MCP server names. Remote reads use MCP server keys returned by the remote plugin detail API. Returned plugin skills include their current `enabled` state after local config filtering; bundled hooks are returned as lightweight declaration summaries keyed for correlation with `hooks/list`. Plugin app summaries also include `needsAuth` when the server can determine connector accessibility (**under development; do not call from production clients yet**). - `plugin/skill/read` — read remote plugin skill markdown on demand by `remoteMarketplaceName`, `remotePluginId`, and `skillName`. This lets clients preview uninstalled remote plugin skills without downloading the plugin bundle. - `skills/changed` — notification emitted when watched local skill files change. - `app/list` — list available apps. diff --git a/codex-rs/app-server/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs index 7a6f3c5f58a7..5740d25a000e 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -1,5 +1,3 @@ -mod remote_plugin_detail; - use super::*; use crate::error_code::internal_error; use crate::error_code::invalid_request; @@ -1101,13 +1099,7 @@ impl PluginRequestProcessor { Arc::clone(&environment_manager), ) .await; - let mcp_server_names = remote_plugin_detail::local_curated_mcp_server_names( - plugins_manager.as_ref(), - &plugins_input, - &remote_detail, - ) - .await; - remote_plugin_detail_to_info(remote_detail, app_summaries, mcp_server_names) + remote_plugin_detail_to_info(remote_detail, app_summaries) } }; @@ -2014,7 +2006,6 @@ fn remote_plugin_share_discoverability_to_info( fn remote_plugin_detail_to_info( detail: RemoteCatalogPluginDetail, apps: Vec, - mcp_servers: Vec, ) -> PluginDetail { PluginDetail { marketplace_name: detail.marketplace_name, @@ -2035,7 +2026,7 @@ fn remote_plugin_detail_to_info( .collect(), hooks: Vec::new(), apps, - mcp_servers, + mcp_servers: detail.mcp_server_names, } } diff --git a/codex-rs/app-server/src/request_processors/plugins/remote_plugin_detail.rs b/codex-rs/app-server/src/request_processors/plugins/remote_plugin_detail.rs deleted file mode 100644 index ba163897434f..000000000000 --- a/codex-rs/app-server/src/request_processors/plugins/remote_plugin_detail.rs +++ /dev/null @@ -1,65 +0,0 @@ -use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME; -use codex_core_plugins::PluginsConfigInput; -use codex_core_plugins::PluginsManager; -use codex_core_plugins::remote::REMOTE_GLOBAL_MARKETPLACE_NAME; -use codex_core_plugins::remote::RemotePluginDetail; -use tracing::warn; - -pub(super) async fn local_curated_mcp_server_names( - plugins_manager: &PluginsManager, - plugins_input: &PluginsConfigInput, - remote_detail: &RemotePluginDetail, -) -> Vec { - if remote_detail.marketplace_name != REMOTE_GLOBAL_MARKETPLACE_NAME { - return Vec::new(); - } - - let remote_plugin_name = &remote_detail.summary.name; - let local_plugin = match plugins_manager.list_marketplaces_for_config(plugins_input, &[]) { - Ok(outcome) => outcome - .marketplaces - .into_iter() - .find(|marketplace| marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME) - .and_then(|marketplace| { - marketplace - .plugins - .into_iter() - .find(|plugin| { - plugin.name == *remote_plugin_name - && remote_detail - .release_version - .as_deref() - .is_none_or(|version| { - plugin.local_version.as_deref() == Some(version) - }) - }) - .map(|plugin| (marketplace.name, plugin)) - }), - Err(err) => { - warn!( - plugin = %remote_plugin_name, - error = %err, - "failed to list local curated plugins for remote plugin MCP hydration" - ); - return Vec::new(); - } - }; - let Some((marketplace_name, plugin)) = local_plugin else { - return Vec::new(); - }; - - match plugins_manager - .read_plugin_detail_for_marketplace_plugin(plugins_input, &marketplace_name, plugin) - .await - { - Ok(plugin) => plugin.mcp_server_names, - Err(err) => { - warn!( - plugin = %remote_plugin_name, - error = %err, - "failed to hydrate remote plugin MCP server names from local curated plugin" - ); - Vec::new() - } - } -} diff --git a/codex-rs/app-server/tests/suite/v2/plugin_read.rs b/codex-rs/app-server/tests/suite/v2/plugin_read.rs index afc7bcd9e571..8f6651673436 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_read.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_read.rs @@ -122,37 +122,9 @@ async fn plugin_read_rejects_multiple_read_sources() -> Result<()> { } #[tokio::test] -async fn plugin_read_hydrates_remote_curated_mcp_servers_when_remote_plugin_is_disabled() --> Result<()> { +async fn plugin_read_returns_remote_mcp_server_names_when_uninstalled() -> Result<()> { let codex_home = TempDir::new()?; let server = MockServer::start().await; - let curated_root = codex_home.path().join(".tmp/plugins"); - write_plugin_marketplace( - &curated_root, - "openai-curated", - "example-plugin", - "./example-plugin", - )?; - let plugin_root = curated_root.join("example-plugin"); - std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?; - std::fs::write( - plugin_root.join(".codex-plugin/plugin.json"), - r#"{ - "name": "example-plugin", - "version": "1.2.1", - "mcpServers": "./.mcp.json" -}"#, - )?; - std::fs::write( - plugin_root.join(".mcp.json"), - r#"{ - "mcpServers": { - "example-server": { - "command": "example-mcp" - } - } -}"#, - )?; std::fs::write( codex_home.path().join("config.toml"), format!( @@ -192,7 +164,15 @@ plugins = true "default_prompt": "Use the legacy example prompt", "default_prompts": [] }, - "skills": [] + "skills": [], + "mcp_servers": [ + { + "key": "example-server", + "metadata": { + "command": "example-mcp" + } + } + ] } }"#; let installed_body = r#"{ diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index 045ad8e18423..027ec79ea08d 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -167,6 +167,7 @@ pub struct RemotePluginDetail { pub app_manifest: Option, pub skills: Vec, pub app_ids: Vec, + pub mcp_server_names: Vec, } #[derive(Debug, Clone, PartialEq)] @@ -417,6 +418,13 @@ struct RemotePluginReleaseResponse { interface: RemotePluginReleaseInterfaceResponse, #[serde(default)] skills: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + mcp_servers: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +struct RemotePluginMcpServerResponse { + key: String, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] @@ -948,6 +956,14 @@ async fn build_remote_plugin_detail( enabled: !disabled_skill_names.contains(&skill.name), }) .collect(); + let mut mcp_server_names = plugin + .release + .mcp_servers + .iter() + .map(|server| server.key.clone()) + .collect::>(); + mcp_server_names.sort_unstable(); + mcp_server_names.dedup(); Ok(RemotePluginDetail { marketplace_name, @@ -959,6 +975,7 @@ async fn build_remote_plugin_detail( app_manifest: plugin.release.app_manifest, skills, app_ids: plugin.release.app_ids, + mcp_server_names, }) } From b61d927a6c85b8a80c5addd40438a75c828e013a Mon Sep 17 00:00:00 2001 From: Eric Ning Date: Thu, 4 Jun 2026 14:10:03 -0700 Subject: [PATCH 4/5] docs(app-server): leave plugin read documentation unchanged --- codex-rs/app-server/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 76108d7854dd..dc02ff8c049f 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -203,7 +203,7 @@ Example with notification opt-out: - `marketplace/upgrade` — upgrade all configured Git plugin marketplaces, or one named marketplace when `marketplaceName` is provided. Returns selected marketplace names, upgraded roots, and per-marketplace errors. - `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, plugin `availability` (`AVAILABLE` by default or `DISABLED_BY_ADMIN` for remote plugins blocked upstream), fail-open `marketplaceLoadErrors` entries for marketplace files that could not be parsed or loaded, and best-effort `featuredPluginIds` for the official curated marketplace. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category (**under development; do not call from production clients yet**). - `plugin/installed` — list installed plugin rows plus any explicitly requested local install-suggestion plugin names, without fetching the broader remote catalog. Mention surfaces can use this narrower view when they need plugin mention payloads rather than plugin-page discovery data (**under development; do not call from production clients yet**). -- `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/hooks/apps/MCP server names. Remote reads use MCP server keys returned by the remote plugin detail API. Returned plugin skills include their current `enabled` state after local config filtering; bundled hooks are returned as lightweight declaration summaries keyed for correlation with `hooks/list`. Plugin app summaries also include `needsAuth` when the server can determine connector accessibility (**under development; do not call from production clients yet**). +- `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/hooks/apps/MCP server names. Returned plugin skills include their current `enabled` state after local config filtering; bundled hooks are returned as lightweight declaration summaries keyed for correlation with `hooks/list`. Plugin app summaries also include `needsAuth` when the server can determine connector accessibility (**under development; do not call from production clients yet**). - `plugin/skill/read` — read remote plugin skill markdown on demand by `remoteMarketplaceName`, `remotePluginId`, and `skillName`. This lets clients preview uninstalled remote plugin skills without downloading the plugin bundle. - `skills/changed` — notification emitted when watched local skill files change. - `app/list` — list available apps. From 7063b7793b68c98ef2eb8f95f8eda6c10271e000 Mon Sep 17 00:00:00 2001 From: Eric Ning Date: Thu, 4 Jun 2026 14:46:23 -0700 Subject: [PATCH 5/5] fix(app-server): align remote MCP server naming --- codex-rs/app-server/src/request_processors/plugins.rs | 2 +- codex-rs/app-server/tests/suite/v2/plugin_read.rs | 2 +- codex-rs/core-plugins/src/remote.rs | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/codex-rs/app-server/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs index 5740d25a000e..316353244972 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -2026,7 +2026,7 @@ fn remote_plugin_detail_to_info( .collect(), hooks: Vec::new(), apps, - mcp_servers: detail.mcp_server_names, + mcp_servers: detail.mcp_servers, } } diff --git a/codex-rs/app-server/tests/suite/v2/plugin_read.rs b/codex-rs/app-server/tests/suite/v2/plugin_read.rs index 8f6651673436..9e9e8442280c 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_read.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_read.rs @@ -122,7 +122,7 @@ async fn plugin_read_rejects_multiple_read_sources() -> Result<()> { } #[tokio::test] -async fn plugin_read_returns_remote_mcp_server_names_when_uninstalled() -> Result<()> { +async fn plugin_read_returns_remote_mcp_servers_when_uninstalled() -> Result<()> { let codex_home = TempDir::new()?; let server = MockServer::start().await; std::fs::write( diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index 027ec79ea08d..9b34fd293cc6 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -167,7 +167,7 @@ pub struct RemotePluginDetail { pub app_manifest: Option, pub skills: Vec, pub app_ids: Vec, - pub mcp_server_names: Vec, + pub mcp_servers: Vec, } #[derive(Debug, Clone, PartialEq)] @@ -956,14 +956,14 @@ async fn build_remote_plugin_detail( enabled: !disabled_skill_names.contains(&skill.name), }) .collect(); - let mut mcp_server_names = plugin + let mut mcp_servers = plugin .release .mcp_servers .iter() .map(|server| server.key.clone()) .collect::>(); - mcp_server_names.sort_unstable(); - mcp_server_names.dedup(); + mcp_servers.sort_unstable(); + mcp_servers.dedup(); Ok(RemotePluginDetail { marketplace_name, @@ -975,7 +975,7 @@ async fn build_remote_plugin_detail( app_manifest: plugin.release.app_manifest, skills, app_ids: plugin.release.app_ids, - mcp_server_names, + mcp_servers, }) }