Expose plugin ownership in MCP server status (#37850)

## What changed

- Add `pluginId` to each `mcpServerStatus/list` result.
- Populate it for MCP servers contributed by installed or selected plugins, and
  return `null` for servers from other sources.
- Update the generated protocol schemas, TypeScript bindings, and app-server
  documentation.

## Testing

- Cover plugin-owned servers in plugin installation and executor flows.
- Verify non-plugin servers and protocol serialization return a null
  `pluginId`.

GitOrigin-RevId: 0e4b912252cf51be56dcb63e0c1c7365a5e870ee
This commit is contained in:
victor-openai authored and copyberry committed 2026-08-10 17:14:51 +00:00
1 parent 97729885d4
commit 78d3665d15
16 files changed
+107 -33

No files matched your search

@@ -13227,6 +13227,12 @@
"name": { "name": {
"type": "string" "type": "string"
}, },
"pluginId": {
"type": [
"string",
"null"
]
},
"resourceTemplates": { "resourceTemplates": {
"items": { "items": {
"$ref": "#/definitions/v2/ResourceTemplate" "$ref": "#/definitions/v2/ResourceTemplate"
@@ -9564,6 +9564,12 @@
"name": { "name": {
"type": "string" "type": "string"
}, },
"pluginId": {
"type": [
"string",
"null"
]
},
"resourceTemplates": { "resourceTemplates": {
"items": { "items": {
"$ref": "#/definitions/ResourceTemplate" "$ref": "#/definitions/ResourceTemplate"
@@ -60,6 +60,12 @@
"name": { "name": {
"type": "string" "type": "string"
}, },
"pluginId": {
"type": [
"string",
"null"
]
},
"resourceTemplates": { "resourceTemplates": {
"items": { "items": {
"$ref": "#/definitions/ResourceTemplate" "$ref": "#/definitions/ResourceTemplate"
@@ -7,4 +7,4 @@ import type { ResourceTemplate } from "../ResourceTemplate";
import type { Tool } from "../Tool"; import type { Tool } from "../Tool";
import type { McpAuthStatus } from "./McpAuthStatus"; import type { McpAuthStatus } from "./McpAuthStatus";
export type McpServerStatus = { name: string, serverInfo: McpServerInfo | null, tools: { [key in string]?: Tool }, resources: Array<Resource>, resourceTemplates: Array<ResourceTemplate>, authStatus: McpAuthStatus, }; export type McpServerStatus = { name: string, pluginId: string | null, serverInfo: McpServerInfo | null, tools: { [key in string]?: Tool }, resources: Array<Resource>, resourceTemplates: Array<ResourceTemplate>, authStatus: McpAuthStatus, };
@@ -61,6 +61,7 @@ pub enum McpServerStatusDetail {
#[ts(export_to = "v2/")] #[ts(export_to = "v2/")]
pub struct McpServerStatus { pub struct McpServerStatus {
pub name: String, pub name: String,
pub plugin_id: Option<String>,
pub server_info: Option<McpServerInfo>, pub server_info: Option<McpServerInfo>,
pub tools: std::collections::HashMap<String, McpTool>, pub tools: std::collections::HashMap<String, McpTool>,
pub resources: Vec<McpResource>, pub resources: Vec<McpResource>,
@@ -2422,6 +2422,7 @@ fn mcp_server_status_serializes_absent_server_info_as_null() {
let response = ListMcpServerStatusResponse { let response = ListMcpServerStatusResponse {
data: vec![McpServerStatus { data: vec![McpServerStatus {
name: "not-ready".to_string(), name: "not-ready".to_string(),
plugin_id: None,
server_info: None, server_info: None,
tools: HashMap::new(), tools: HashMap::new(),
resources: Vec::new(), resources: Vec::new(),
@@ -2436,6 +2437,7 @@ fn mcp_server_status_serializes_absent_server_info_as_null() {
json!({ json!({
"data": [{ "data": [{
"name": "not-ready", "name": "not-ready",
"pluginId": null,
"serverInfo": null, "serverInfo": null,
"tools": {}, "tools": {},
"resources": [], "resources": [],
@@ -2507,6 +2509,7 @@ fn mcp_server_status_serializes_absent_server_info_metadata_as_null() {
let response = ListMcpServerStatusResponse { let response = ListMcpServerStatusResponse {
data: vec![McpServerStatus { data: vec![McpServerStatus {
name: "initialized".to_string(), name: "initialized".to_string(),
plugin_id: Some("lookup@test".to_string()),
server_info: Some(McpServerInfo { server_info: Some(McpServerInfo {
name: "lookup-server".to_string(), name: "lookup-server".to_string(),
title: None, title: None,
@@ -2528,6 +2531,7 @@ fn mcp_server_status_serializes_absent_server_info_metadata_as_null() {
json!({ json!({
"data": [{ "data": [{
"name": "initialized", "name": "initialized",
"pluginId": "lookup@test",
"serverInfo": { "serverInfo": {
"name": "lookup-server", "name": "lookup-server",
"title": null, "title": null,
+1 -1
View File
@@ -267,7 +267,7 @@ Example with notification opt-out:
- `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; pass `threadId` to resolve servers from that thread's selected plugins and executor, and receive an `authorization_url` followed by `mcpServer/oauthLogin/completed` once the browser flow finishes. - `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; pass `threadId` to resolve servers from that thread's selected plugins and executor, and receive an `authorization_url` followed by `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). - `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental).
- `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server. - `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server.
- `mcpServerStatus/list` — enumerate configured MCP servers with their tools, auth status, server info, plus resources/resource templates for `full` detail; supports optional `threadId` and cursor+limit pagination. If `threadId` is omitted, the server reads from the latest global config directly. If `detail` is omitted, the server defaults to `full`. An `unknown` auth status means OAuth support could not be determined; `unsupported` means OAuth is known not to be supported. - `mcpServerStatus/list` — enumerate configured MCP servers with their tools, auth status, server info, owning `pluginId` (`null` for servers not contributed by a plugin), plus resources/resource templates for `full` detail; supports optional `threadId` and cursor+limit pagination. If `threadId` is omitted, the server reads from the latest global config directly. If `detail` is omitted, the server defaults to `full`. An `unknown` auth status means OAuth support could not be determined; `unsupported` means OAuth is known not to be supported.
- `mcpServer/resource/read` — read a resource from a configured MCP server by optional `threadId`, `server`, and `uri`, returning text/blob resource `contents`. If `threadId` is omitted, the server reads from the latest MCP config directly. - `mcpServer/resource/read` — read a resource from a configured MCP server by optional `threadId`, `server`, and `uri`, returning text/blob resource `contents`. If `threadId` is omitted, the server reads from the latest MCP config directly.
- `mcpServer/tool/call` — call a tool on a thread's configured MCP server by `threadId`, `server`, `tool`, optional `arguments`, and optional `_meta`, returning the MCP tool result. - `mcpServer/tool/call` — call a tool on a thread's configured MCP server by `threadId`, `server`, `tool`, optional `arguments`, and optional `_meta`, returning the MCP tool result.
- `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`. - `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`.
@@ -1,5 +1,6 @@
use super::*; use super::*;
use codex_core::McpManager; use codex_core::McpManager;
use codex_mcp::McpServerSource;
const MCP_TOOL_THREAD_ID_META_KEY: &str = "threadId"; const MCP_TOOL_THREAD_ID_META_KEY: &str = "threadId";
@@ -374,6 +375,17 @@ impl McpRequestProcessor {
.iter() .iter()
.map(|name| McpServerStatus { .map(|name| McpServerStatus {
name: name.clone(), name: name.clone(),
plugin_id: mcp_config.mcp_server_catalog.server(name).and_then(
|server| match server.source() {
McpServerSource::Plugin(plugin)
| McpServerSource::SelectedPlugin(plugin) => {
Some(plugin.plugin_id().to_owned())
}
McpServerSource::Config
| McpServerSource::Compatibility { .. }
| McpServerSource::Extension { .. } => None,
},
),
server_info: server_infos.get(name).cloned(), server_info: server_infos.get(name).cloned(),
tools: tools_by_server.get(name).cloned().unwrap_or_default(), tools: tools_by_server.get(name).cloned().unwrap_or_default(),
resources: resources.get(name).cloned().unwrap_or_default(), resources: resources.get(name).cloned().unwrap_or_default(),
@@ -11,6 +11,7 @@ use codex_app_server_protocol::ListMcpServerStatusParams;
use codex_app_server_protocol::ListMcpServerStatusResponse; use codex_app_server_protocol::ListMcpServerStatusResponse;
use codex_app_server_protocol::McpServerOauthLoginCompletedNotification; use codex_app_server_protocol::McpServerOauthLoginCompletedNotification;
use codex_app_server_protocol::McpServerOauthLoginResponse; use codex_app_server_protocol::McpServerOauthLoginResponse;
use codex_app_server_protocol::McpServerStatus;
use codex_app_server_protocol::McpServerToolCallParams; use codex_app_server_protocol::McpServerToolCallParams;
use codex_app_server_protocol::McpServerToolCallResponse; use codex_app_server_protocol::McpServerToolCallResponse;
use codex_app_server_protocol::RequestId; use codex_app_server_protocol::RequestId;
@@ -40,6 +41,7 @@ use rmcp::transport::StreamableHttpService;
use rmcp::transport::streamable_http_server::session::local::LocalSessionManager; use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
use serde_json::json; use serde_json::json;
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::BTreeMap;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tempfile::TempDir; use tempfile::TempDir;
@@ -447,26 +449,37 @@ startup_timeout_sec = 10
Some(json!("ECHOING: refresh applied")) Some(json!("ECHOING: refresh applied"))
); );
let selected_server_names = mcp_server_names(&mut app_server, selected_thread).await?; let selected_server_owners = mcp_server_statuses(&mut app_server, selected_thread)
assert!( .await?
selected_server_names .into_iter()
.iter() .map(|server| (server.name, server.plugin_id))
.any(|name| name == MCP_SERVER_NAME) .collect::<BTreeMap<_, _>>();
); assert_eq!(
assert!( selected_server_owners,
selected_server_names BTreeMap::from([
.iter() (
.any(|name| name == HTTP_MCP_SERVER_NAME) HTTP_MCP_SERVER_NAME.to_string(),
); Some("executor-demo@1".to_string()),
assert!( ),
selected_server_names (
.iter() MCP_SERVER_NAME.to_string(),
.any(|name| name == OAUTH_MCP_SERVER_NAME) Some("executor-demo@1".to_string()),
),
(
OAUTH_MCP_SERVER_NAME.to_string(),
Some("executor-demo@1".to_string()),
),
(REFRESH_PROBE_SERVER_NAME.to_string(), None),
])
); );
let unselected_thread = let unselected_thread =
start_thread(&mut app_server, /*selected_capability_roots*/ None).await?; start_thread(&mut app_server, /*selected_capability_roots*/ None).await?;
let unselected_server_names = mcp_server_names(&mut app_server, unselected_thread).await?; let unselected_server_names = mcp_server_statuses(&mut app_server, unselected_thread)
.await?
.into_iter()
.map(|server| server.name)
.collect::<Vec<_>>();
assert!(unselected_server_names.iter().all(|name| { assert!(unselected_server_names.iter().all(|name| {
name != MCP_SERVER_NAME && name != HTTP_MCP_SERVER_NAME && name != OAUTH_MCP_SERVER_NAME name != MCP_SERVER_NAME && name != HTTP_MCP_SERVER_NAME && name != OAUTH_MCP_SERVER_NAME
})); }));
@@ -525,10 +538,10 @@ impl ServerHandler for ExecutorHttpMcpServer {
} }
} }
async fn mcp_server_names( async fn mcp_server_statuses(
app_server: &mut TestAppServer, app_server: &mut TestAppServer,
thread_id: String, thread_id: String,
) -> Result<Vec<String>> { ) -> Result<Vec<McpServerStatus>> {
let request_id = app_server let request_id = app_server
.send_list_mcp_server_status_request(ListMcpServerStatusParams { .send_list_mcp_server_status_request(ListMcpServerStatusParams {
cursor: None, cursor: None,
@@ -539,11 +552,7 @@ async fn mcp_server_names(
.await?; .await?;
let response: ListMcpServerStatusResponse = let response: ListMcpServerStatusResponse =
timeout(DEFAULT_READ_TIMEOUT, app_server.read_response(request_id)).await??; timeout(DEFAULT_READ_TIMEOUT, app_server.read_response(request_id)).await??;
Ok(response Ok(response.data)
.data
.into_iter()
.map(|server| server.name)
.collect())
} }
async fn start_thread( async fn start_thread(
@@ -112,6 +112,7 @@ async fn mcp_server_status_list_returns_raw_server_and_tool_names() -> Result<()
assert_eq!(response.data.len(), 1); assert_eq!(response.data.len(), 1);
let status = &response.data[0]; let status = &response.data[0];
assert_eq!(status.name, "some-server"); assert_eq!(status.name, "some-server");
assert_eq!(status.plugin_id, None);
assert_eq!( assert_eq!(
status.tools.keys().cloned().collect::<BTreeSet<_>>(), status.tools.keys().cloned().collect::<BTreeSet<_>>(),
BTreeSet::from(["look-up.raw".to_string()]) BTreeSet::from(["look-up.raw".to_string()])
@@ -25,6 +25,8 @@ use codex_app_server_protocol::AppInfo;
use codex_app_server_protocol::AppSummary; use codex_app_server_protocol::AppSummary;
use codex_app_server_protocol::AppsListParams; use codex_app_server_protocol::AppsListParams;
use codex_app_server_protocol::AppsListResponse; use codex_app_server_protocol::AppsListResponse;
use codex_app_server_protocol::ListMcpServerStatusParams;
use codex_app_server_protocol::ListMcpServerStatusResponse;
use codex_app_server_protocol::PluginAuthPolicy; use codex_app_server_protocol::PluginAuthPolicy;
use codex_app_server_protocol::PluginAvailability; use codex_app_server_protocol::PluginAvailability;
use codex_app_server_protocol::PluginInstallParams; use codex_app_server_protocol::PluginInstallParams;
@@ -32,6 +34,7 @@ use codex_app_server_protocol::PluginInstallResponse;
use codex_app_server_protocol::RequestId; use codex_app_server_protocol::RequestId;
use codex_config::types::AuthCredentialsStoreMode; use codex_config::types::AuthCredentialsStoreMode;
use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBuf;
use core_test_support::stdio_server_bin;
use flate2::Compression; use flate2::Compression;
use flate2::write::GzEncoder; use flate2::write::GzEncoder;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@@ -2264,13 +2267,13 @@ async fn plugin_install_makes_bundled_mcp_servers_available_to_followup_requests
write_plugin_source(repo_root.path(), "sample-plugin", &[])?; write_plugin_source(repo_root.path(), "sample-plugin", &[])?;
std::fs::write( std::fs::write(
repo_root.path().join("sample-plugin/.mcp.json"), repo_root.path().join("sample-plugin/.mcp.json"),
r#"{ serde_json::to_vec(&json!({
"mcpServers": { "mcpServers": {
"sample-mcp": { "sample-mcp": {
"command": "echo" "command": stdio_server_bin()?,
} }
} }
}"#, }))?,
)?; )?;
let marketplace_path = let marketplace_path =
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?; AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
@@ -2294,7 +2297,28 @@ async fn plugin_install_makes_bundled_mcp_servers_available_to_followup_requests
assert_eq!(response.apps_needing_auth, Vec::<AppSummary>::new()); assert_eq!(response.apps_needing_auth, Vec::<AppSummary>::new());
let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?; let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?;
assert!(!config.contains("[mcp_servers.sample-mcp]")); assert!(!config.contains("[mcp_servers.sample-mcp]"));
assert!(!config.contains("command = \"echo\""));
let request_id = mcp
.send_list_mcp_server_status_request(ListMcpServerStatusParams {
cursor: None,
limit: None,
detail: None,
thread_id: None,
})
.await?;
let response: ListMcpServerStatusResponse =
timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??;
assert_eq!(
response
.data
.into_iter()
.map(|server| (server.name, server.plugin_id))
.collect::<Vec<_>>(),
vec![(
"sample-mcp".to_string(),
Some("sample-plugin@debug".to_string()),
)]
);
let request_id = mcp let request_id = mcp
.send_raw_request( .send_raw_request(
@@ -1443,6 +1443,7 @@ mod tests {
let statuses = vec![ let statuses = vec![
McpServerStatus { McpServerStatus {
name: "docs".to_string(), name: "docs".to_string(),
plugin_id: None,
server_info: None, server_info: None,
tools: HashMap::from([( tools: HashMap::from([(
"list".to_string(), "list".to_string(),
@@ -1463,6 +1464,7 @@ mod tests {
}, },
McpServerStatus { McpServerStatus {
name: "disabled".to_string(), name: "disabled".to_string(),
plugin_id: None,
server_info: None, server_info: None,
tools: HashMap::new(), tools: HashMap::new(),
resources: Vec::new(), resources: Vec::new(),
+1
View File
@@ -225,6 +225,7 @@ async fn handle_mcp_inventory_result_respects_origin_thread() {
app.handle_mcp_inventory_result( app.handle_mcp_inventory_result(
Ok(vec![McpServerStatus { Ok(vec![McpServerStatus {
name: "docs".to_string(), name: "docs".to_string(),
plugin_id: None,
server_info: None, server_info: None,
tools: HashMap::new(), tools: HashMap::new(),
resources: Vec::new(), resources: Vec::new(),
+2
View File
@@ -964,6 +964,7 @@ async fn mcp_tools_output_lists_tools_for_hyphenated_server_names() {
fn mcp_tools_output_from_statuses_renders_status_only_servers() { fn mcp_tools_output_from_statuses_renders_status_only_servers() {
let statuses = vec![McpServerStatus { let statuses = vec![McpServerStatus {
name: "plugin_docs".to_string(), name: "plugin_docs".to_string(),
plugin_id: None,
server_info: None, server_info: None,
tools: HashMap::from([( tools: HashMap::from([(
"lookup".to_string(), "lookup".to_string(),
@@ -994,6 +995,7 @@ fn mcp_tools_output_from_statuses_renders_status_only_servers() {
fn mcp_tools_output_from_statuses_renders_verbose_inventory() { fn mcp_tools_output_from_statuses_renders_verbose_inventory() {
let statuses = vec![McpServerStatus { let statuses = vec![McpServerStatus {
name: "plugin_docs".to_string(), name: "plugin_docs".to_string(),
plugin_id: None,
server_info: None, server_info: None,
tools: HashMap::from([( tools: HashMap::from([(
"lookup".to_string(), "lookup".to_string(),