mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
fix(app-server): expose remote MCP servers in plugin read (#26453)
## Why Remote plugin detail responses include MCP server metadata under `release.mcp_servers`, but Codex did not deserialize or propagate that field. As a result, `plugin/read` always returned an empty `mcpServers` list for remote plugins, so the plugin details pane omitted the MCP Servers section even when the remote plugin declares one. This affects uninstalled plugins as well: the remote detail API is the source of truth and returns MCP server keys without requiring a local plugin bundle. ## What changed - Deserialize MCP server entries from remote plugin detail responses. - Normalize their keys into a sorted, deduplicated list on `RemotePluginDetail`. - Return those keys from app-server `plugin/read` instead of hardcoding an empty list. - Add regression coverage proving an uninstalled remote plugin returns its MCP server names. ## Test plan - `just test -p codex-core-plugins` - `just test -p codex-app-server plugin_read`
This commit is contained in:
committed by
GitHub
Unverified
parent
59ca34206b
commit
769c231aa1
@@ -2026,7 +2026,7 @@ fn remote_plugin_detail_to_info(
|
||||
.collect(),
|
||||
hooks: Vec::new(),
|
||||
apps,
|
||||
mcp_servers: Vec::new(),
|
||||
mcp_servers: detail.mcp_servers,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ 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_returns_remote_mcp_servers_when_uninstalled() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
std::fs::write(
|
||||
@@ -148,22 +148,31 @@ 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": []
|
||||
"skills": [],
|
||||
"mcp_servers": [
|
||||
{
|
||||
"key": "example-server",
|
||||
"metadata": {
|
||||
"command": "example-mcp"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
let installed_body = r#"{
|
||||
@@ -211,12 +220,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 +238,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(())
|
||||
}
|
||||
|
||||
@@ -167,6 +167,7 @@ pub struct RemotePluginDetail {
|
||||
pub app_manifest: Option<JsonValue>,
|
||||
pub skills: Vec<RemotePluginSkill>,
|
||||
pub app_ids: Vec<String>,
|
||||
pub mcp_servers: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -417,6 +418,13 @@ struct RemotePluginReleaseResponse {
|
||||
interface: RemotePluginReleaseInterfaceResponse,
|
||||
#[serde(default)]
|
||||
skills: Vec<RemotePluginSkillResponse>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
mcp_servers: Vec<RemotePluginMcpServerResponse>,
|
||||
}
|
||||
|
||||
#[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_servers = plugin
|
||||
.release
|
||||
.mcp_servers
|
||||
.iter()
|
||||
.map(|server| server.key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
mcp_servers.sort_unstable();
|
||||
mcp_servers.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_servers,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user