Expose advertised MCP server capabilities in status responses (#44826)

## What changed

Add `serverCapabilities` to `mcpServerStatus/list` responses in both `full` and `toolsAndAuthOnly` modes, including thread-scoped reads. Return the initialized server's capabilities object, including its `extensions` map, or `null` when unavailable.

Capture capabilities during initialization so they remain available when tool discovery fails. Clear them on each new connection attempt and keep them independent of shared tool caches. Update the protocol schemas, generated TypeScript and Python types, and documentation.

## Testing

Add coverage for extension capabilities surviving tool-discovery failures in both detail modes. Extend existing tests to check advertised extensions, absent capabilities after initialization failure, and serialization of unavailable capabilities as `null`.

GitOrigin-RevId: 8e9d67c10fac3a54e82bef8be5d99caaf82597d1
This commit is contained in:
Gabriel Peal
2026-09-11 13:55:24 +00:00
committed by copyberry
parent 624ccf7947
commit 7a6f469dcf
20 changed files with 199 additions and 9 deletions
@@ -14687,6 +14687,9 @@
],
"description": "Current thread-runtime connection state; null when unavailable or the configuration changed."
},
"serverCapabilities": {
"description": "Capabilities advertised by the initialized MCP server; null when unavailable."
},
"serverInfo": {
"anyOf": [
{
@@ -10739,6 +10739,9 @@
],
"description": "Current thread-runtime connection state; null when unavailable or the configuration changed."
},
"serverCapabilities": {
"description": "Capabilities advertised by the initialized MCP server; null when unavailable."
},
"serverInfo": {
"anyOf": [
{
@@ -101,6 +101,9 @@
],
"description": "Current thread-runtime connection state; null when unavailable or the configuration changed."
},
"serverCapabilities": {
"description": "Capabilities advertised by the initialized MCP server; null when unavailable."
},
"serverInfo": {
"anyOf": [
{
@@ -5,6 +5,7 @@ import type { McpServerInfo } from "../McpServerInfo";
import type { Resource } from "../Resource";
import type { ResourceTemplate } from "../ResourceTemplate";
import type { Tool } from "../Tool";
import type { JsonValue } from "../serde_json/JsonValue";
import type { McpAuthStatus } from "./McpAuthStatus";
import type { McpServerConnectionStatus } from "./McpServerConnectionStatus";
@@ -12,7 +13,11 @@ export type McpServerStatus = { name: string,
/**
* Current thread-runtime connection state; null when unavailable or the configuration changed.
*/
runtimeStatus: McpServerConnectionStatus | null, pluginId: string | null, serverInfo: McpServerInfo | null, tools: { [key in string]?: Tool },
runtimeStatus: McpServerConnectionStatus | null, pluginId: string | null, serverInfo: McpServerInfo | null,
/**
* Capabilities advertised by the initialized MCP server; null when unavailable.
*/
serverCapabilities: JsonValue | null, tools: { [key in string]?: Tool },
/**
* Tool discovery failed and no catalog was returned.
* Null when a catalog is returned, including cached or empty catalogs.
@@ -79,6 +79,8 @@ pub struct McpServerStatus {
pub runtime_status: Option<McpServerConnectionStatus>,
pub plugin_id: Option<String>,
pub server_info: Option<McpServerInfo>,
/// Capabilities advertised by the initialized MCP server; null when unavailable.
pub server_capabilities: Option<serde_json::Value>,
pub tools: std::collections::HashMap<String, McpTool>,
/// Tool discovery failed and no catalog was returned.
/// Null when a catalog is returned, including cached or empty catalogs.
@@ -2581,6 +2581,7 @@ fn mcp_server_elicitation_response_serializes_nullable_content() {
fn mcp_server_status_serializes_absent_server_info_as_null() {
let response = ListMcpServerStatusResponse {
data: vec![McpServerStatus {
server_capabilities: None,
tools_error: None,
name: "not-ready".to_string(),
runtime_status: None,
@@ -2602,6 +2603,7 @@ fn mcp_server_status_serializes_absent_server_info_as_null() {
"runtimeStatus": null,
"pluginId": null,
"serverInfo": null,
"serverCapabilities": null,
"tools": {},
"toolsError": null,
"resources": [],
@@ -2628,6 +2630,7 @@ fn mcp_server_status_accepts_older_inventory_without_runtime_status() {
assert_eq!(
status,
McpServerStatus {
server_capabilities: None,
tools_error: None,
name: "older-server".to_string(),
runtime_status: None,
@@ -2700,6 +2703,7 @@ fn mcp_server_status_updated_serializes_failure_reason() {
fn mcp_server_status_serializes_absent_server_info_metadata_as_null() {
let response = ListMcpServerStatusResponse {
data: vec![McpServerStatus {
server_capabilities: None,
tools_error: None,
name: "initialized".to_string(),
runtime_status: None,
@@ -2727,6 +2731,7 @@ fn mcp_server_status_serializes_absent_server_info_metadata_as_null() {
"name": "initialized",
"runtimeStatus": null,
"pluginId": "lookup@test",
"serverCapabilities": null,
"serverInfo": {
"name": "lookup-server",
"title": null,
+8
View File
@@ -174,3 +174,11 @@ Attachments record the resources currently associated with a thread, independent
`thread/attachment/list` accepts one `threadId` and returns at most 100 attachments per page, ordered by creation time and attachment id. Continue with `nextCursor` and the same `threadId` until the cursor is `null`. Each thread can retain up to 100 attachments. Removing an attachment frees a slot for a new attachment.
Attachment creation and deletion requests using the same thread ID are serialized across connections. The requesting client receives its response before the compact update is broadcast, and duplicate creates or absent deletes do not emit updates. Deleting the owning thread removes its attachments under the same lifecycle exclusion; queued attachment mutations then report that the thread was not found.
# MCP server capabilities
`mcpServerStatus/list` returns `serverCapabilities` for each initialized MCP server
in both `full` and `toolsAndAuthOnly` detail modes, including thread-scoped reads.
This is the server's advertised MCP capabilities object, including its `extensions`
map. It is null when the connection has not initialized successfully; capabilities
are never inferred from tools or copied from a shared catalog cache.
@@ -342,6 +342,7 @@ impl McpRequestProcessor {
};
let McpServerStatusSnapshot {
server_infos,
server_capabilities,
tools_by_server,
tools_errors,
resources,
@@ -396,6 +397,7 @@ impl McpRequestProcessor {
},
),
server_info: server_infos.get(name).cloned(),
server_capabilities: server_capabilities.get(name).cloned(),
tools: tools_by_server.get(name).cloned().unwrap_or_default(),
tools_error: tools_errors.get(name).cloned(),
resources: resources.get(name).cloned().unwrap_or_default(),
@@ -349,6 +349,7 @@ async fn oauth_login_automatically_selects_callback_specific_cimd_without_metada
move || {
Ok(McpStatusServer {
tool_name: Arc::clone(&tool_name),
tools_error: None,
})
},
Arc::new(LocalSessionManager::default()),
@@ -527,7 +528,8 @@ async fn oauth_login_automatically_selects_callback_specific_cimd_without_metada
#[tokio::test]
async fn mcp_server_status_list_returns_raw_server_and_tool_names() -> Result<()> {
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
let (mcp_server_url, mcp_server_handle) = start_mcp_server("look-up.raw").await?;
let (mcp_server_url, mcp_server_handle) =
start_mcp_server("look-up.raw", /*tools_error*/ None).await?;
let codex_home = TempDir::new()?;
mock_responses_config(&server.uri())
.with_extra_config(&format!(
@@ -568,6 +570,7 @@ async fn mcp_server_status_list_returns_raw_server_and_tool_names() -> Result<()
.find(|status| status.name == "broken-server")
.unwrap();
assert!(failed.tools.is_empty());
assert_eq!(failed.server_capabilities, None);
assert!(
failed
.tools_error
@@ -579,6 +582,15 @@ async fn mcp_server_status_list_returns_raw_server_and_tool_names() -> Result<()
.iter()
.find(|status| status.name == "some-server")
.unwrap();
assert_eq!(
status
.server_capabilities
.as_ref()
.and_then(|caps| caps.get("extensions")),
Some(&json!({
"openai/settings": { "readTool": "settings.read", "updateTool": "settings.update" }
}))
);
assert_eq!(status.tools_error, None);
assert_eq!(status.name, "some-server");
assert_eq!(status.runtime_status, None);
@@ -684,7 +696,8 @@ MCP_TEST_PID_FILE = {}
#[tokio::test]
async fn mcp_server_status_list_uses_thread_project_local_config() -> Result<()> {
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
let (mcp_server_url, mcp_server_handle) = start_mcp_server("project_lookup").await?;
let (mcp_server_url, mcp_server_handle) =
start_mcp_server("project_lookup", /*tools_error*/ None).await?;
let codex_home = TempDir::new()?;
let workspace = TempDir::new()?;
mock_responses_config(&server.uri()).write(codex_home.path())?;
@@ -758,7 +771,8 @@ url = "{mcp_server_url}/mcp"
#[tokio::test]
async fn mcp_server_status_list_reports_thread_runtime_connections() -> Result<()> {
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
let (mcp_server_url, mcp_server_handle) = start_mcp_server("lookup").await?;
let (mcp_server_url, mcp_server_handle) =
start_mcp_server("lookup", /*tools_error*/ None).await?;
let codex_home = TempDir::new()?;
mock_responses_config(&server.uri())
.with_extra_config(&format!(
@@ -811,7 +825,8 @@ async fn mcp_server_status_list_reports_thread_runtime_connections() -> Result<(
);
// Inventory uses the latest config, but a same-name replacement is not the
// connection that this thread started. Unchanged registrations retain status.
let (replacement_url, replacement_handle) = start_mcp_server("replacement_lookup").await?;
let (replacement_url, replacement_handle) =
start_mcp_server("replacement_lookup", /*tools_error*/ None).await?;
mock_responses_config(&server.uri())
.with_extra_config(&format!(
"[mcp_servers.connected]\nurl = \"{replacement_url}/mcp\"\n\
@@ -928,14 +943,81 @@ async fn mcp_server_status_list_reports_disconnected_stdio_transport() -> Result
Ok(())
}
#[tokio::test]
async fn mcp_server_status_retains_capabilities_when_tool_discovery_fails() -> Result<()> {
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
let (url, handle) = start_mcp_server(
"lookup",
Some(rmcp::ErrorData::internal_error(
"tool discovery unavailable",
/*data*/ None,
)),
)
.await?;
let codex_home = TempDir::new()?;
mock_responses_config(&server.uri())
.with_extra_config(&format!("[mcp_servers.degraded]\nurl = \"{url}/mcp\""))
.write(codex_home.path())?;
let mut app = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized()
.await?;
for detail in [None, Some(McpServerStatusDetail::ToolsAndAuthOnly)] {
let response: ListMcpServerStatusResponse = app
.request(|request_id| ClientRequest::McpServerStatusList {
request_id,
params: ListMcpServerStatusParams {
cursor: None,
limit: None,
detail,
thread_id: None,
},
})
.await?;
let status = response
.data
.iter()
.find(|status| status.name == "degraded")
.expect("configured server is included");
assert!(status.tools.is_empty());
assert!(
status
.tools_error
.as_ref()
.is_some_and(|error| error.contains("tool discovery unavailable"))
);
assert_eq!(
status
.server_capabilities
.as_ref()
.and_then(|caps| caps.get("extensions")),
Some(&json!({
"openai/settings": { "readTool": "settings.read", "updateTool": "settings.update" }
}))
);
}
handle.abort();
let _ = handle.await;
Ok(())
}
#[derive(Clone)]
struct McpStatusServer {
tool_name: Arc<String>,
tools_error: Option<rmcp::ErrorData>,
}
impl ServerHandler for McpStatusServer {
fn get_info(&self) -> ServerInfo {
ServerInfo::new(ServerCapabilities::builder().enable_tools().build()).with_server_info(
let mut capabilities = ServerCapabilities::builder().enable_tools().build();
capabilities.extensions = Some(BTreeMap::from([(
"openai/settings".to_string(),
serde_json::from_value(
json!({ "readTool": "settings.read", "updateTool": "settings.update" }),
)
.expect("settings capability is a JSON object"),
)]));
ServerInfo::new(capabilities).with_server_info(
Implementation::new("lookup-server", "1.0.0").with_title("Lookup Server"),
)
}
@@ -945,6 +1027,9 @@ impl ServerHandler for McpStatusServer {
_request: Option<rmcp::model::PaginatedRequestParams>,
_context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
) -> Result<ListToolsResult, rmcp::ErrorData> {
if let Some(error) = &self.tools_error {
return Err(error.clone());
}
let input_schema: JsonObject = serde_json::from_value(json!({
"type": "object",
"additionalProperties": false
@@ -1065,9 +1150,10 @@ async fn mcp_server_status_list_tools_and_auth_only_skips_slow_inventory_calls()
#[tokio::test]
async fn mcp_server_status_list_keeps_tools_for_sanitized_name_collisions() -> Result<()> {
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
let (dash_server_url, dash_server_handle) = start_mcp_server("dash_lookup").await?;
let (dash_server_url, dash_server_handle) =
start_mcp_server("dash_lookup", /*tools_error*/ None).await?;
let (underscore_server_url, underscore_server_handle) =
start_mcp_server("underscore_lookup").await?;
start_mcp_server("underscore_lookup", /*tools_error*/ None).await?;
let codex_home = TempDir::new()?;
mock_responses_config(&server.uri())
.with_extra_config(&format!(
@@ -1128,7 +1214,10 @@ url = "{underscore_server_url}/mcp"
Ok(())
}
async fn start_mcp_server(tool_name: &str) -> Result<(String, JoinHandle<()>)> {
async fn start_mcp_server(
tool_name: &str,
tools_error: Option<rmcp::ErrorData>,
) -> Result<(String, JoinHandle<()>)> {
let listener = TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
let tool_name = Arc::new(tool_name.to_string());
@@ -1136,6 +1225,7 @@ async fn start_mcp_server(tool_name: &str) -> Result<(String, JoinHandle<()>)> {
move || {
Ok(McpStatusServer {
tool_name: Arc::clone(&tool_name),
tools_error: tools_error.clone(),
})
},
Arc::new(LocalSessionManager::default()),
@@ -1000,6 +1000,22 @@ impl McpConnectionSet {
Ok(call_tool_result_from_rmcp(result))
}
/// Capabilities belong to the initialized connection, never a shared tool cache.
pub(crate) fn list_available_server_capabilities(&self) -> HashMap<String, serde_json::Value> {
self.servers
.iter()
.filter_map(|(name, view)| {
view.connection
.client
.server_capabilities
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
.map(|capabilities| (name.clone(), capabilities))
})
.collect()
}
/// Returns presentation metadata from the current connection.
/// Codex Apps metadata may come from its existing cache; regular MCP server information is
/// connection-specific, so pending regular clients are awaited.
@@ -552,6 +552,7 @@ pub(crate) async fn create_ready_async_managed_client(tools: Vec<ToolInfo>) -> A
.boxed()
.shared(),
is_codex_apps_mcp_server: false,
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
cached_server_info: None,
codex_apps_tools_cache_context: None,
tool_catalog_cache_context: None,
@@ -717,6 +718,7 @@ fn create_gated_async_managed_client(
AsyncManagedClient {
client,
is_codex_apps_mcp_server: false,
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
cached_server_info: None,
codex_apps_tools_cache_context: None,
tool_catalog_cache_context: None,
@@ -784,6 +786,7 @@ pub(crate) async fn create_test_manager_with_ready_apps_client(
.boxed()
.shared(),
is_codex_apps_mcp_server: true,
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
cached_server_info: Some(create_test_server_info("Codex Apps")),
codex_apps_tools_cache_context: Some(cache_context),
tool_catalog_cache_context: None,
@@ -835,6 +838,7 @@ fn create_test_manager_with_failed_apps_startup(
AsyncManagedClient {
client,
is_codex_apps_mcp_server: true,
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
cached_server_info: None,
codex_apps_tools_cache_context: Some(cache_context),
tool_catalog_cache_context: None,
@@ -2208,6 +2212,7 @@ async fn list_all_tools_uses_shared_codex_apps_cache_while_client_is_pending() {
AsyncManagedClient {
client: pending_client,
is_codex_apps_mcp_server: true,
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
cached_server_info: None,
codex_apps_tools_cache_context: Some(cache_context),
tool_catalog_cache_context: None,
@@ -2266,6 +2271,7 @@ async fn capture_binding_uses_the_ready_clients_own_tools() {
AsyncManagedClient {
client: futures::future::ready(Ok(ready_client)).boxed().shared(),
is_codex_apps_mcp_server: true,
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
cached_server_info: None,
codex_apps_tools_cache_context: Some(cache_context),
tool_catalog_cache_context: None,
@@ -2629,6 +2635,7 @@ async fn list_available_server_infos_uses_cache_while_client_is_pending() {
AsyncManagedClient {
client: pending_client,
is_codex_apps_mcp_server: true,
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
cached_server_info: Some(server_info.clone()),
codex_apps_tools_cache_context: None,
tool_catalog_cache_context: None,
@@ -2725,6 +2732,7 @@ async fn capture_binding_exposes_cached_tools_before_startup() {
AsyncManagedClient {
client: pending_client,
is_codex_apps_mcp_server: true,
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
cached_server_info: None,
codex_apps_tools_cache_context: Some(cache_context),
tool_catalog_cache_context: None,
@@ -2825,6 +2833,7 @@ async fn capture_binding_skips_pending_optional_servers_after_configured_shared_
.boxed()
.shared(),
is_codex_apps_mcp_server: false,
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
cached_server_info: None,
codex_apps_tools_cache_context: None,
tool_catalog_cache_context: None,
@@ -3076,6 +3085,7 @@ async fn capture_binding_shares_optional_startup_grace_across_connection_sets()
.boxed()
.shared(),
is_codex_apps_mcp_server: false,
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
cached_server_info: None,
codex_apps_tools_cache_context: None,
tool_catalog_cache_context: Some(cache_context.clone()),
@@ -3709,6 +3719,7 @@ async fn list_all_tools_blocks_while_client_is_pending_without_cached_tools() {
AsyncManagedClient {
client: pending_client,
is_codex_apps_mcp_server: true,
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
cached_server_info: None,
codex_apps_tools_cache_context: None,
tool_catalog_cache_context: None,
@@ -3766,6 +3777,7 @@ async fn shutdown_cancels_pending_tool_listing() {
AsyncManagedClient {
client: pending_client,
is_codex_apps_mcp_server: true,
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
cached_server_info: None,
codex_apps_tools_cache_context: None,
tool_catalog_cache_context: None,
@@ -3812,6 +3824,7 @@ async fn shutdown_continues_after_caller_is_aborted() {
AsyncManagedClient {
client: blocking_client,
is_codex_apps_mcp_server: true,
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
cached_server_info: None,
codex_apps_tools_cache_context: None,
tool_catalog_cache_context: None,
@@ -3864,6 +3877,7 @@ async fn list_all_tools_does_not_block_when_shared_codex_apps_cache_is_empty() {
AsyncManagedClient {
client: pending_client,
is_codex_apps_mcp_server: true,
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
cached_server_info: None,
codex_apps_tools_cache_context: Some(cache_context),
tool_catalog_cache_context: None,
@@ -3916,6 +3930,7 @@ async fn list_all_tools_uses_shared_codex_apps_cache_when_client_startup_fails()
AsyncManagedClient {
client: failed_client,
is_codex_apps_mcp_server: true,
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
cached_server_info: Some(server_info.clone()),
codex_apps_tools_cache_context: Some(cache_context),
tool_catalog_cache_context: None,
@@ -5350,6 +5365,7 @@ async fn reconciliation_reuses_connection_without_relisting_regular_tools() -> a
client: AsyncManagedClient {
client: futures::future::ready(Ok(managed_client)).boxed().shared(),
is_codex_apps_mcp_server: false,
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
cached_server_info: None,
codex_apps_tools_cache_context: None,
tool_catalog_cache_context: None,
@@ -5915,6 +5931,7 @@ async fn reconciliation_replaces_closed_connections() -> anyhow::Result<()> {
.boxed()
.shared(),
is_codex_apps_mcp_server: false,
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
cached_server_info: None,
codex_apps_tools_cache_context: None,
tool_catalog_cache_context: None,
+3
View File
@@ -467,6 +467,7 @@ pub async fn read_mcp_resource(
#[derive(Debug, Clone)]
pub struct McpServerStatusSnapshot {
pub server_infos: HashMap<String, McpServerInfo>,
pub server_capabilities: HashMap<String, serde_json::Value>,
pub tools_by_server: HashMap<String, HashMap<String, Tool>>,
pub tools_errors: HashMap<String, String>,
pub resources: HashMap<String, Vec<Resource>>,
@@ -488,6 +489,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
if mcp_servers.is_empty() {
return McpServerStatusSnapshot {
server_infos: HashMap::new(),
server_capabilities: HashMap::new(),
tools_by_server: HashMap::new(),
tools_errors: HashMap::new(),
resources: HashMap::new(),
@@ -819,6 +821,7 @@ async fn collect_mcp_server_status_snapshot_from_manager(
}
McpServerStatusSnapshot {
server_capabilities: mcp_connection_manager.list_available_server_capabilities(),
server_infos,
tools_by_server,
tools_errors,
+20
View File
@@ -3,6 +3,7 @@
//! This module owns startup of individual RMCP clients: building the transport,
//! initializing the server, listing raw tools, applying per-server tool filters,
//! and exposing cached Codex Apps tools while a client is still connecting.
//! Initialization capabilities survive tool-discovery failures and reset on a new attempt.
//! Higher-level aggregation and resource/tool APIs live in
//! [`crate::connection_manager`].
@@ -302,10 +303,16 @@ struct ManagedClientStartup {
catalog_item_limit: usize,
cancel_token: CancellationToken,
startup_complete: Arc<AtomicBool>,
server_capabilities: Arc<StdMutex<Option<serde_json::Value>>>,
}
impl ManagedClientStartup {
fn start(&self) -> ManagedClientFuture {
// A new attempt must not expose capabilities from an earlier connection.
*self
.server_capabilities
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
let Self {
server_name,
server,
@@ -326,6 +333,7 @@ impl ManagedClientStartup {
catalog_item_limit,
cancel_token,
startup_complete,
server_capabilities,
} = self.clone();
let is_codex_apps_mcp_server = server_name == CODEX_APPS_MCP_SERVER_NAME;
let startup_timeout = server
@@ -381,6 +389,7 @@ impl ManagedClientStartup {
client_mcp_extensions,
auth_changes,
catalog_item_limit,
server_capabilities,
},
)
.await
@@ -419,6 +428,8 @@ pub(crate) struct AsyncManagedClient {
pub(crate) client: ManagedClientFuture,
pub(crate) is_codex_apps_mcp_server: bool,
pub(crate) cached_server_info: Option<McpServerInfo>,
/// Retained after initialization even if subsequent tool discovery fails.
pub(crate) server_capabilities: Arc<StdMutex<Option<serde_json::Value>>>,
pub(crate) codex_apps_tools_cache_context: Option<ConnectorRuntimeContext<ToolInfo>>,
pub(crate) tool_catalog_cache_context: Option<McpToolCatalogCacheContext>,
pub(crate) startup_complete: Arc<AtomicBool>,
@@ -463,6 +474,7 @@ impl AsyncManagedClient {
None
};
let startup_complete = Arc::new(AtomicBool::new(false));
let server_capabilities = Arc::new(StdMutex::new(None));
let startup = Arc::new(ManagedClientStartup {
server_name,
server,
@@ -483,6 +495,7 @@ impl AsyncManagedClient {
catalog_item_limit,
cancel_token: cancel_token.clone(),
startup_complete: Arc::clone(&startup_complete),
server_capabilities: Arc::clone(&server_capabilities),
});
let client = startup.start();
let startup_reconnect = is_codex_apps_mcp_server.then(|| {
@@ -500,6 +513,7 @@ impl AsyncManagedClient {
client,
is_codex_apps_mcp_server,
cached_server_info,
server_capabilities,
codex_apps_tools_cache_context,
tool_catalog_cache_context,
startup_complete,
@@ -907,6 +921,7 @@ async fn start_server_task(
client_mcp_extensions,
auth_changes,
catalog_item_limit,
server_capabilities,
} = params;
let send_elicitation =
elicitation_requests.make_sender(server_name.clone(), tx_event, &client_mcp_extensions);
@@ -934,6 +949,10 @@ async fn start_server_task(
&initialize_result,
);
let initialize_result = initialize_result.map_err(StartupOutcomeError::from)?;
*server_capabilities
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
Some(serde_json::json!(initialize_result.capabilities));
let auth_change_notifications = crate::auth_changes::start(
Arc::clone(&client),
@@ -1098,6 +1117,7 @@ fn mcp_server_info_from_implementation(
}
struct StartServerTaskParams {
server_capabilities: Arc<StdMutex<Option<serde_json::Value>>>,
is_codex_apps_mcp_server: bool,
startup_timeout: Option<Duration>, // TODO: cancel_token should handle this.
tx_event: Option<Sender<Event>>,
@@ -1591,6 +1591,7 @@ mod tests {
fn mcp_inventory_maps_prefix_tool_names_by_server() {
let statuses = vec![
McpServerStatus {
server_capabilities: None,
tools_error: None,
name: "docs".to_string(),
runtime_status: None,
@@ -1614,6 +1615,7 @@ mod tests {
auth_status: codex_app_server_protocol::McpAuthStatus::Unsupported,
},
McpServerStatus {
server_capabilities: None,
tools_error: None,
name: "disabled".to_string(),
runtime_status: None,
+1
View File
@@ -398,6 +398,7 @@ async fn handle_mcp_inventory_result_respects_origin_thread() {
app.handle_mcp_inventory_result(
Ok(vec![McpServerStatus {
server_capabilities: None,
tools_error: None,
name: "docs".to_string(),
runtime_status: None,
@@ -23,6 +23,7 @@ fn mcp_inventory_connection_states() {
]
.into_iter()
.map(|(name, runtime_status)| McpServerStatus {
server_capabilities: None,
name: name.to_string(),
runtime_status,
plugin_id: None,
+2
View File
@@ -1050,6 +1050,7 @@ async fn mcp_tools_output_lists_tools_for_hyphenated_server_names() {
#[test]
fn mcp_tools_output_from_statuses_renders_status_only_servers() {
let statuses = vec![McpServerStatus {
server_capabilities: None,
tools_error: None,
name: "plugin_docs".to_string(),
runtime_status: None,
@@ -1083,6 +1084,7 @@ fn mcp_tools_output_from_statuses_renders_status_only_servers() {
#[test]
fn mcp_tools_output_from_statuses_renders_verbose_inventory() {
let statuses = vec![McpServerStatus {
server_capabilities: None,
tools_error: None,
name: "plugin_docs".to_string(),
runtime_status: None,
@@ -8340,6 +8340,13 @@ class McpServerStatus(BaseModel):
description="Current thread-runtime connection state; null when unavailable or the configuration changed.",
),
] = None
server_capabilities: Annotated[
Any | None,
Field(
alias="serverCapabilities",
description="Capabilities advertised by the initialized MCP server; null when unavailable.",
),
] = None
server_info: Annotated[McpServerInfo | None, Field(alias="serverInfo")] = None
tools: dict[str, Tool]
tools_error: Annotated[