Add single-server MCP status discovery with thread connection reuse (#48783)

## Why

Inspecting one MCP server should not require full-inventory discovery or a separate connection when a thread already has one.

## What changed

- Add optional `serverName` to `mcpServerStatus/list` and update generated schemas and bindings.
- With `threadId`, apply any pending runtime refresh and reuse the thread's current connection and tool catalog, waiting only for the selected server during discovery.
- Without `threadId`, create a discovery connection for only the selected server. Unknown names return an empty page; omitting `serverName` preserves full-inventory discovery.

## Testing

Add a regression test covering both status detail modes, checking that tool metadata is preserved and that status reads do not repeat `initialize` or `tools/list` requests.

GitOrigin-RevId: 1b361b67a993ab45cae493e88c146f50cfaf753a
This commit is contained in:
victor-openai
2026-09-27 21:07:54 +00:00
committed by copyberry
parent 21eb35513d
commit ea64727556
27 changed files with 389 additions and 112 deletions
@@ -1733,6 +1733,13 @@
"null"
]
},
"serverName": {
"description": "Limit discovery to one server. With a thread ID, reuse that thread's MCP connection.",
"type": [
"string",
"null"
]
},
"threadId": {
"type": [
"string",
@@ -13938,6 +13938,13 @@
"null"
]
},
"serverName": {
"description": "Limit discovery to one server. With a thread ID, reuse that thread's MCP connection.",
"type": [
"string",
"null"
]
},
"threadId": {
"type": [
"string",
@@ -9970,6 +9970,13 @@
"null"
]
},
"serverName": {
"description": "Limit discovery to one server. With a thread ID, reuse that thread's MCP connection.",
"type": [
"string",
"null"
]
},
"threadId": {
"type": [
"string",
@@ -37,6 +37,13 @@
"null"
]
},
"serverName": {
"description": "Limit discovery to one server. With a thread ID, reuse that thread's MCP connection.",
"type": [
"string",
"null"
]
},
"threadId": {
"type": [
"string",
@@ -16,4 +16,8 @@ limit?: number | null,
* Controls how much MCP inventory data to fetch for each server.
* Defaults to `Full` when omitted.
*/
detail?: McpServerStatusDetail | null, threadId?: string | null, };
detail?: McpServerStatusDetail | null, threadId?: string | null,
/**
* Limit discovery to one server. With a thread ID, reuse that thread's MCP connection.
*/
serverName?: string | null, };
@@ -60,6 +60,9 @@ pub struct ListMcpServerStatusParams {
pub detail: Option<McpServerStatusDetail>,
#[ts(optional = nullable)]
pub thread_id: Option<String>,
/// Limit discovery to one server. With a thread ID, reuse that thread's MCP connection.
#[ts(optional = nullable)]
pub server_name: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
+6
View File
@@ -313,6 +313,12 @@ This is the server's advertised MCP capabilities object, including its `extensio
map. It is null when the connection has not initialized successfully; capabilities
are never inferred from tools or copied from a shared catalog cache.
Pass `serverName` to discover only that server. With `threadId`, the request
reuses the thread's current MCP connection and tool catalog after any pending
runtime refresh; discovery then waits only for that server. Without `threadId`,
discovery creates a connection for the selected server. An unknown name returns an empty page.
Omitting `serverName` preserves full-inventory discovery.
# MCP OAuth login
`mcpServer/oauth/login` only returns HTTP(S) authorization URLs. Authorization
@@ -1,6 +1,5 @@
use super::thread_input::ensure_direct_input_allowed;
use super::*;
use codex_core::McpManager;
use codex_mcp::McpServerSource;
use codex_mcp::ReadResourceRequestParams;
use codex_mcp::resolve_oauth_callback;
@@ -273,59 +272,23 @@ impl McpRequestProcessor {
let request = request_id.clone();
let outgoing = Arc::clone(&self.outgoing);
let (config, thread) = match params.thread_id.as_deref() {
Some(thread_id) => {
let (_, thread) = self.load_thread(thread_id).await?;
let thread_config = thread.config().await;
let config = self
.config_manager
.load_latest_config_with_session_layers(
&thread_config.config_layer_stack,
&thread_config.cwd,
)
.await
.map_err(|err| internal_error(format!("failed to reload config: {err}")))?;
(config, Some(thread))
}
None => (self.load_latest_config(/*fallback_cwd*/ None).await?, None),
let thread = match params.thread_id.as_deref() {
Some(thread_id) => Some(self.load_thread(thread_id).await?.1),
None => None,
};
let mcp_manager = self.thread_manager.mcp_manager();
let auth = self.auth_manager.auth().await;
let environment_manager = self.thread_manager.environment_manager();
let processor = self.clone();
tokio::spawn(async move {
let (mcp_config, runtime_context) = match thread.as_ref() {
Some(thread) => thread.runtime_mcp_config_and_context(&config).await,
None => {
let mcp_config = mcp_manager.runtime_config(&config).await;
let runtime_context =
McpRuntimeContext::new(environment_manager, config.cwd.to_path_buf());
(mcp_config, runtime_context)
}
};
let result = Self::list_mcp_server_status_response(
request.request_id.to_string(),
params,
mcp_config,
auth,
runtime_context,
mcp_manager,
thread,
)
.await;
let result = processor
.list_mcp_server_status_response(params, thread)
.await;
outgoing.send_result(request, result).await;
});
Ok(())
}
async fn list_mcp_server_status_response(
request_id: String,
&self,
params: ListMcpServerStatusParams,
mcp_config: codex_mcp::McpConfig,
auth: Option<CodexAuth>,
runtime_context: McpRuntimeContext,
mcp_manager: Arc<McpManager>,
thread: Option<Arc<codex_core::CodexThread>>,
) -> Result<ListMcpServerStatusResponse, JSONRPCErrorError> {
let detail = match params.detail.unwrap_or(McpServerStatusDetail::Full) {
@@ -333,16 +296,53 @@ impl McpRequestProcessor {
McpServerStatusDetail::ToolsAndAuthOnly => McpSnapshotDetail::ToolsAndAuthOnly,
};
let snapshot = collect_mcp_server_status_snapshot_with_detail(
&mcp_config,
auth.as_ref(),
request_id,
runtime_context,
mcp_manager.codex_apps_tools_cache(),
mcp_manager.tool_catalog_cache(),
detail,
)
.await;
let (mcp_config, snapshot) = if let (Some(thread), Some(server)) =
(thread.as_ref(), params.server_name.as_deref())
{
thread
.mcp_server_status_snapshot(server, detail)
.await
.map_err(|error| {
internal_error(format!("failed to read MCP server status: {error:#}"))
})?
} else {
let config = match thread.as_ref() {
Some(thread) => {
let thread_config = thread.config().await;
self.config_manager
.load_latest_config_with_session_layers(
&thread_config.config_layer_stack,
&thread_config.cwd,
)
.await
.map_err(|err| internal_error(format!("failed to reload config: {err}")))?
}
None => self.load_latest_config(/*fallback_cwd*/ None).await?,
};
let mcp_manager = self.thread_manager.mcp_manager();
let auth = self.auth_manager.auth().await;
let (mcp_config, runtime_context) = match thread.as_ref() {
Some(thread) => thread.runtime_mcp_config_and_context(&config).await,
None => (
mcp_manager.runtime_config(&config).await,
McpRuntimeContext::new(
self.thread_manager.environment_manager(),
config.cwd.to_path_buf(),
),
),
};
let snapshot = collect_mcp_server_status_snapshot_with_detail(
&mcp_config,
auth.as_ref(),
runtime_context,
mcp_manager.codex_apps_tools_cache(),
mcp_manager.tool_catalog_cache(),
detail,
params.server_name.as_deref(),
)
.await;
(Arc::new(mcp_config), snapshot)
};
let runtime_statuses = match thread {
Some(thread) => thread.mcp_connection_statuses(&mcp_config).await,
@@ -368,6 +368,12 @@ impl McpRequestProcessor {
);
server_names.sort();
server_names.dedup();
server_names.retain(|name| {
params
.server_name
.as_deref()
.is_none_or(|server| name == server)
});
let total = server_names.len();
let limit = params.limit.unwrap_or(total as u32).max(1) as usize;
@@ -1552,6 +1552,7 @@ async fn mcp_server_statuses(
) -> Result<Vec<McpServerStatus>> {
let request_id = app_server
.send_list_mcp_server_status_request(ListMcpServerStatusParams {
server_name: None,
cursor: None,
limit: None,
detail: None,
@@ -72,6 +72,103 @@ use wiremock::matchers::path;
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10);
/// Selected status reads return the thread's tools without repeating MCP discovery.
#[tokio::test]
async fn mcp_server_status_list_reuses_thread_connection() -> Result<()> {
let server = MockServer::start().await;
let tool = json!({
"name": "open_widget",
"title": "Open widget",
"inputSchema": { "type": "object" },
"_meta": { "ui": { "resourceUri": "ui://widget.html" } }
});
let response_tool = tool.clone();
Mock::given(method("POST"))
.and(path("/mcp"))
.respond_with(move |request: &wiremock::Request| {
let request: serde_json::Value = serde_json::from_slice(&request.body).unwrap();
let result = match request["method"].as_str() {
Some("initialize") => json!({
"protocolVersion": "2025-11-25",
"capabilities": { "tools": {} },
"serverInfo": { "name": "widget", "version": "1.0.0" }
}),
Some("tools/list") => json!({ "tools": [response_tool.clone()] }),
Some("resources/list") => json!({ "resources": [] }),
Some("resources/templates/list") => json!({ "resourceTemplates": [] }),
_ => return ResponseTemplate::new(202),
};
ResponseTemplate::new(200).set_body_json(json!({
"jsonrpc": "2.0", "id": request["id"], "result": result
}))
})
.mount(&server)
.await;
let codex_home = TempDir::new()?;
mock_responses_config(&server.uri())
.with_extra_config(&format!(
"[mcp_servers.widget]\nurl = \"{}/mcp\"",
server.uri()
))
.write(codex_home.path())?;
let mut app = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized()
.await?;
let thread = app.start_thread(ThreadStartParams::default()).await?.thread;
timeout(
DEFAULT_READ_TIMEOUT,
app.read_stream_until_matching_notification("widget MCP ready", |notification| {
notification.method == "mcpServer/startupStatus/updated"
&& notification.params.as_ref().is_some_and(|params| {
params["threadId"] == thread.id
&& params["name"] == "widget"
&& params["status"] == "ready"
})
}),
)
.await??;
for detail in [
McpServerStatusDetail::ToolsAndAuthOnly,
McpServerStatusDetail::Full,
] {
let response: ListMcpServerStatusResponse = app
.request(|request_id| ClientRequest::McpServerStatusList {
request_id,
params: ListMcpServerStatusParams {
thread_id: Some(thread.id.clone()),
server_name: Some("widget".to_string()),
detail: Some(detail),
cursor: None,
limit: None,
},
})
.await?;
assert_eq!(response.data.len(), 1);
assert_eq!(response.data[0].name, "widget");
assert_eq!(
response.data[0].tools,
std::collections::HashMap::from([(
"open_widget".to_string(),
serde_json::from_value(tool.clone())?
)])
);
assert_eq!(response.next_cursor, None);
}
let discovery_methods: Vec<_> = server
.received_requests()
.await
.unwrap()
.iter()
.filter_map(|request| serde_json::from_slice::<serde_json::Value>(&request.body).ok())
.filter_map(|request| request["method"].as_str().map(str::to_owned))
.filter(|method| matches!(method.as_str(), "initialize" | "tools/list"))
.collect();
assert_eq!(discovery_methods, ["initialize", "tools/list"]);
Ok(())
}
#[test_case(false, None, None, None, None, true; "legacy callback")]
#[test_case(
false,
@@ -555,6 +652,7 @@ async fn oauth_login_automatically_selects_callback_specific_cimd_without_metada
.request(|request_id| ClientRequest::McpServerStatusList {
request_id,
params: ListMcpServerStatusParams {
server_name: None,
cursor: None,
limit: None,
detail: Some(McpServerStatusDetail::Full),
@@ -644,6 +742,7 @@ async fn mcp_server_status_list_returns_raw_server_and_tool_names(plugin: bool)
.request(|request_id| ClientRequest::McpServerStatusList {
request_id,
params: ListMcpServerStatusParams {
server_name: None,
cursor: None,
limit: None,
detail: None,
@@ -750,6 +849,7 @@ MCP_TEST_PID_FILE = {}
.request(|request_id| ClientRequest::McpServerStatusList {
request_id,
params: ListMcpServerStatusParams {
server_name: None,
cursor: None,
limit: None,
detail: Some(McpServerStatusDetail::ToolsAndAuthOnly),
@@ -763,6 +863,7 @@ MCP_TEST_PID_FILE = {}
std::fs::remove_file(&barrier_file)?;
let second_request_id = mcp
.send_list_mcp_server_status_request(ListMcpServerStatusParams {
server_name: None,
cursor: None,
limit: None,
detail: Some(McpServerStatusDetail::ToolsAndAuthOnly),
@@ -826,6 +927,7 @@ url = "{mcp_server_url}/mcp"
.request(|request_id| ClientRequest::McpServerStatusList {
request_id,
params: ListMcpServerStatusParams {
server_name: None,
cursor: None,
limit: None,
detail: Some(McpServerStatusDetail::ToolsAndAuthOnly),
@@ -839,6 +941,7 @@ url = "{mcp_server_url}/mcp"
.request(|request_id| ClientRequest::McpServerStatusList {
request_id,
params: ListMcpServerStatusParams {
server_name: None,
cursor: None,
limit: None,
detail: Some(McpServerStatusDetail::ToolsAndAuthOnly),
@@ -894,6 +997,7 @@ async fn mcp_server_status_list_reports_thread_runtime_connections() -> Result<(
.request(|request_id| ClientRequest::McpServerStatusList {
request_id,
params: ListMcpServerStatusParams {
server_name: None,
cursor: None,
limit: None,
detail: Some(McpServerStatusDetail::ToolsAndAuthOnly),
@@ -932,6 +1036,7 @@ async fn mcp_server_status_list_reports_thread_runtime_connections() -> Result<(
.request(|request_id| ClientRequest::McpServerStatusList {
request_id,
params: ListMcpServerStatusParams {
server_name: None,
cursor: None,
limit: None,
detail: Some(McpServerStatusDetail::ToolsAndAuthOnly),
@@ -1015,6 +1120,7 @@ async fn mcp_server_status_list_reports_disconnected_stdio_transport() -> Result
.request(|request_id| ClientRequest::McpServerStatusList {
request_id,
params: ListMcpServerStatusParams {
server_name: None,
cursor: None,
limit: None,
detail: Some(McpServerStatusDetail::ToolsAndAuthOnly),
@@ -1062,6 +1168,7 @@ async fn mcp_server_status_retains_capabilities_when_tool_discovery_fails() -> R
.request(|request_id| ClientRequest::McpServerStatusList {
request_id,
params: ListMcpServerStatusParams {
server_name: None,
cursor: None,
limit: None,
detail,
@@ -1216,6 +1323,7 @@ async fn mcp_server_status_list_tools_and_auth_only_skips_slow_inventory_calls()
let request_id = mcp
.send_list_mcp_server_status_request(ListMcpServerStatusParams {
server_name: None,
cursor: None,
limit: None,
detail: Some(McpServerStatusDetail::ToolsAndAuthOnly),
@@ -1270,6 +1378,7 @@ url = "{underscore_server_url}/mcp"
.request(|request_id| ClientRequest::McpServerStatusList {
request_id,
params: ListMcpServerStatusParams {
server_name: None,
cursor: None,
limit: None,
detail: None,
@@ -2466,6 +2466,7 @@ async fn plugin_install_makes_bundled_mcp_servers_available_to_followup_requests
let request_id = mcp
.send_list_mcp_server_status_request(ListMcpServerStatusParams {
server_name: None,
cursor: None,
limit: None,
detail: None,
@@ -1160,6 +1160,7 @@ async fn wait_for_selected_mcp_server(
loop {
let request_id = app_server
.send_list_mcp_server_status_request(ListMcpServerStatusParams {
server_name: None,
cursor: None,
limit: None,
detail: None,
@@ -1651,6 +1651,7 @@ required = true
.request(|request_id| ClientRequest::McpServerStatusList {
request_id,
params: ListMcpServerStatusParams {
server_name: None,
cursor: None,
limit: None,
detail: Some(McpServerStatusDetail::ToolsAndAuthOnly),
+5 -2
View File
@@ -1049,9 +1049,12 @@ impl McpConnectionSet {
/// 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.
pub(crate) async fn list_available_server_infos(&self) -> HashMap<String, McpServerInfo> {
pub(crate) async fn list_available_server_infos(
&self,
include_server: impl Fn(&str) -> bool,
) -> HashMap<String, McpServerInfo> {
let mut server_infos = HashMap::new();
for (server_name, view) in &self.servers {
for (server_name, view) in self.servers.iter().filter(|(name, _)| include_server(name)) {
let client = &view.connection.client;
if !client.startup_complete.load(Ordering::Acquire)
&& let Some(server_info) = client.cached_server_info.clone()
@@ -179,49 +179,57 @@ impl McpConnectionSet {
/// Returns all tools with model-visible names normalized.
pub async fn list_all_tools(&self) -> Vec<ToolInfo> {
self.list_tools_with_errors().await.0
Box::pin(self.list_tools_with_errors(|_| true)).await.0
}
#[instrument(level = "trace", skip_all, fields(mcp_server_count = self.servers.len()))]
pub(crate) async fn list_tools_with_errors(&self) -> (Vec<ToolInfo>, HashMap<String, String>) {
pub(crate) async fn list_tools_with_errors(
&self,
include_server: impl Fn(&str) -> bool,
) -> (Vec<ToolInfo>, HashMap<String, String>) {
let mut tools = Vec::new();
let mut errors = HashMap::new();
let mut available_server_count = 0;
let mut unavailable_server_count = 0;
let server_results = join_all(self.servers.iter().map(|(server_name, view)| async move {
view.connection.client.reconnect_failed_startup().await;
let has_cached_tools = view.connection.client.has_cached_tools();
let startup_complete = view
.connection
.client
.startup_complete
.load(Ordering::Acquire);
let server_tools = view
.listed_tools(&self.tool_plugin_context)
.instrument(trace_span!(
"list_tools_for_server",
server_name = %server_name,
has_cached_tools,
startup_complete
))
.await;
let result = match server_tools {
Ok(server_tools) => Ok(server_tools
.into_iter()
.map(|tool| Self::with_server_metadata(tool, &view.metadata))
.collect::<Vec<_>>()),
Err(error) => {
trace!(
server_name = %server_name,
has_cached_tools,
startup_complete,
"MCP server tools unavailable while building tool list"
);
Err(error)
}
};
(server_name, result)
}))
let server_results = join_all(
self.servers
.iter()
.filter(|(name, _)| include_server(name))
.map(|(server_name, view)| async move {
view.connection.client.reconnect_failed_startup().await;
let has_cached_tools = view.connection.client.has_cached_tools();
let startup_complete = view
.connection
.client
.startup_complete
.load(Ordering::Acquire);
let server_tools = view
.listed_tools(&self.tool_plugin_context)
.instrument(trace_span!(
"list_tools_for_server",
server_name = %server_name,
has_cached_tools,
startup_complete
))
.await;
let result = match server_tools {
Ok(server_tools) => Ok(server_tools
.into_iter()
.map(|tool| Self::with_server_metadata(tool, &view.metadata))
.collect::<Vec<_>>()),
Err(error) => {
trace!(
server_name = %server_name,
has_cached_tools,
startup_complete,
"MCP server tools unavailable while building tool list"
);
Err(error)
}
};
(server_name, result)
}),
)
.await;
for (server_name, server_tools) in server_results {
match server_tools {
@@ -2718,7 +2718,7 @@ async fn list_available_server_infos_uses_cache_while_client_is_pending() {
let timeout_result = tokio::time::timeout(
Duration::from_millis(10),
manager.list_available_server_infos(),
manager.list_available_server_infos(|_| true),
)
.await;
let server_infos = timeout_result.expect("server info lookup should not block on startup");
@@ -4024,7 +4024,7 @@ async fn list_all_tools_uses_shared_codex_apps_cache_when_client_startup_fails()
assert_eq!(tool.callable_name, "calendar_create_event");
assert_eq!(
manager
.list_available_server_infos()
.list_available_server_infos(|_| true)
.await
.get(CODEX_APPS_MCP_SERVER_NAME),
Some(&server_info)
+17 -8
View File
@@ -497,13 +497,14 @@ pub struct McpServerStatusSnapshot {
pub async fn collect_mcp_server_status_snapshot_with_detail(
config: &McpConfig,
auth: Option<&CodexAuth>,
submit_id: String,
runtime_context: McpRuntimeContext,
codex_apps_tools_cache: ConnectorRuntimeManager<ToolInfo>,
tool_catalog_cache: crate::McpToolCatalogCache,
detail: McpSnapshotDetail,
server_name: Option<&str>,
) -> McpServerStatusSnapshot {
let mcp_servers = effective_mcp_servers(config, auth);
let mut mcp_servers = effective_mcp_servers(config, auth);
mcp_servers.retain(|name, _| server_name.is_none_or(|selected| name == selected));
if mcp_servers.is_empty() {
return McpServerStatusSnapshot {
server_infos: HashMap::new(),
@@ -539,7 +540,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
plugins_available: false,
ready_selected_capability_roots: Vec::new(),
mcp_servers,
submit_id,
submit_id: String::new(),
tx_event: None,
startup_cancellation_token: cancel_token.clone(),
runtime_context,
@@ -788,21 +789,29 @@ fn convert_mcp_resource_templates(
.collect::<HashMap<_, _>>()
}
async fn collect_mcp_server_status_snapshot_from_manager(
pub(crate) async fn collect_mcp_server_status_snapshot_from_manager(
mcp_connection_manager: &McpConnectionSet,
auth_status_entries: HashMap<String, crate::mcp::auth::McpAuthStatusEntry>,
server_names: Vec<String>,
detail: McpSnapshotDetail,
) -> McpServerStatusSnapshot {
let selected_servers: HashSet<&str> = server_names.iter().map(String::as_str).collect();
let include_server = |name: &str| selected_servers.contains(name);
let ((server_infos, (tools, tools_errors)), resources, resource_templates) = tokio::join!(
async {
let server_infos = mcp_connection_manager.list_available_server_infos().await;
let tools = mcp_connection_manager.list_tools_with_errors().await;
let server_infos = mcp_connection_manager
.list_available_server_infos(include_server)
.await;
let tools = mcp_connection_manager
.list_tools_with_errors(include_server)
.await;
(server_infos, tools)
},
async {
if detail.include_resources() {
mcp_connection_manager.list_all_resources(|_| true).await
mcp_connection_manager
.list_all_resources(include_server)
.await
} else {
HashMap::new()
}
@@ -810,7 +819,7 @@ async fn collect_mcp_server_status_snapshot_from_manager(
async {
if detail.include_resources() {
mcp_connection_manager
.list_all_resource_templates(|_| true)
.list_all_resource_templates(include_server)
.await
} else {
HashMap::new()
+2
View File
@@ -5,6 +5,8 @@
//! [`crate::rmcp_client`] and connection-set behavior lives in
//! [`crate::connection_manager`].
mod status;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::PathBuf;
+53
View File
@@ -0,0 +1,53 @@
//! Read selected-server metadata from one published thread runtime.
use std::sync::Arc;
use super::McpRuntime;
use super::McpRuntimeContext;
use crate::McpConfig;
use crate::McpServerStatusSnapshot;
use crate::McpSnapshotDetail;
use crate::mcp::collect_mcp_server_status_snapshot_from_manager;
use crate::mcp::compute_auth_statuses;
use crate::mcp::effective_mcp_servers;
impl McpRuntime {
/// Reuses the selected connection and catalog without starting unrelated servers.
/// Configuration and metadata belong to the same captured runtime publication.
pub async fn server_status_snapshot(
&self,
server: &str,
detail: McpSnapshotDetail,
runtime_context: &McpRuntimeContext,
) -> anyhow::Result<(Arc<McpConfig>, McpServerStatusSnapshot)> {
let current = self.current.load_full();
let runtime_context = runtime_context.clone().with_selected_environments(
Arc::clone(&current.environment_selections),
current.ready_environments.clone(),
);
let config = Arc::clone(
current
.config
.as_ref()
.ok_or_else(|| anyhow::anyhow!("MCP runtime is not configured"))?,
);
let mut servers = effective_mcp_servers(&config, current.auth.as_ref());
servers.retain(|name, _| name == server);
let auth_statuses = compute_auth_statuses(
servers.iter(),
config.mcp_oauth_credentials_store_mode,
config.auth_keyring_backend_kind,
current.auth.as_ref(),
&runtime_context,
)
.await;
let snapshot = collect_mcp_server_status_snapshot_from_manager(
&current.connections,
auth_statuses,
servers.into_keys().collect(),
detail,
)
.await;
Ok((config, snapshot))
}
}
+18
View File
@@ -1034,6 +1034,24 @@ impl CodexThread {
Ok(serde_json::to_value(result)?)
}
/// Inspects one server through this thread's current MCP connection.
pub async fn mcp_server_status_snapshot(
&self,
server: &str,
detail: codex_mcp::McpSnapshotDetail,
) -> anyhow::Result<(
Arc<codex_mcp::McpConfig>,
codex_mcp::McpServerStatusSnapshot,
)> {
self.session.refresh_mcp_if_dirty().await;
let runtime_context = self.session.current_mcp_runtime_context().await;
self.session
.services
.mcp_runtime
.server_status_snapshot(server, detail, &runtime_context)
.await
}
/// Reads an app resource using the current authority of its originating tool call.
pub async fn read_mcp_resource_for_call(
&self,
+19 -4
View File
@@ -1,5 +1,6 @@
use super::mcp_refresh::McpRefreshInvalidationGuard;
use super::*;
use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::environment_selection::combine_selected_capability_roots;
use codex_exec_server::ExecutorCapabilityDiscoveryCache;
use codex_exec_server::ExecutorCapabilityDiscoverySnapshot;
@@ -147,19 +148,33 @@ impl Session {
.project_selected_environment_mcp_servers(config, &environments, mcp_projection)
.await
.config;
let runtime_context = self.mcp_runtime_context(&environments, &host_fallback_cwd);
(mcp_config, runtime_context)
}
pub(crate) async fn current_mcp_runtime_context(&self) -> McpRuntimeContext {
let host_fallback_cwd = self.state.lock().await.session_configuration.cwd().clone();
let environments = self.services.turn_environments.snapshot().await;
self.mcp_runtime_context(&environments, &host_fallback_cwd)
}
fn mcp_runtime_context(
&self,
environments: &TurnEnvironmentSnapshot,
host_fallback_cwd: &std::path::Path,
) -> McpRuntimeContext {
let local_process_cwd = environments
.local_environment_cwd()
.map(|cwd| cwd.to_path_buf())
.unwrap_or_else(|| host_fallback_cwd.to_path_buf());
let runtime_context = McpRuntimeContext::new(
McpRuntimeContext::new(
self.services.turn_environments.environment_manager(),
local_process_cwd,
)
.with_selected_environments(
environment_selections.into(),
environments.all_selections().into(),
environments.ready_environment_handles(),
);
(mcp_config, runtime_context)
)
}
pub(crate) async fn runtime_mcp_servers(
@@ -768,6 +768,7 @@ pub(super) async fn fetch_all_mcp_server_statuses(
limit: Some(100),
detail: Some(detail),
thread_id: thread_id.clone(),
server_name: None,
},
})
.await
@@ -26,7 +26,7 @@ working=true, response rows=1, 80x6
? for shortcuts
working=false, response rows=1, 80x12
Show a response.
› Show a response.
• Done.
└ Tip: Try /help.
@@ -40,7 +40,7 @@ Show a response.
? for shortcuts
working=false, response rows=30, 80x12
Show a response.
› Show a response.
• Done.
└ Tip: Try /help.
@@ -65,7 +65,7 @@ working=false, response rows=1, 80x9
? for shortcuts
working=false, response rows=1, 80x10
Show a response.
› Show a response.
• Done.
[completion time]
@@ -77,7 +77,7 @@ Show a response.
? for shortcuts
working=false, response rows=1, 80x11
Show a response.
› Show a response.
• Done.
└ Tip: Try /help.
@@ -1123,6 +1123,7 @@ async fn local_daemon_registers_approval_gated_mcp_tools_for_both_start_paths()
.request_typed(ClientRequest::McpServerStatusList {
request_id: AppServerRequestId::String("tui-tool-inventory".to_string()),
params: codex_app_server_protocol::ListMcpServerStatusParams {
server_name: None,
cursor: None,
limit: None,
detail: Some(codex_app_server_protocol::McpServerStatusDetail::ToolsAndAuthOnly),