Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 29 additions & 19 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2054,14 +2054,18 @@ impl App {
/// tradeoff because the effect is limited to stale inventory output in history,
/// while request-token invalidation would add cross-cutting async state for a
/// low-severity path.
fn fetch_mcp_inventory(&mut self, app_server: &AppServerSession) {
fn fetch_mcp_inventory(
&mut self,
app_server: &AppServerSession,
detail: McpServerStatusDetail,
) {
let request_handle = app_server.request_handle();
let app_event_tx = self.app_event_tx.clone();
tokio::spawn(async move {
let result = fetch_all_mcp_server_statuses(request_handle)
let result = fetch_all_mcp_server_statuses(request_handle, detail)
.await
.map_err(|err| err.to_string());
app_event_tx.send(AppEvent::McpInventoryLoaded { result });
app_event_tx.send(AppEvent::McpInventoryLoaded { result, detail });
});
}

Expand Down Expand Up @@ -2371,7 +2375,11 @@ impl App {
///
/// When both the local config and the app-server report zero servers, a special
/// "empty" cell is shown instead of the full table.
fn handle_mcp_inventory_result(&mut self, result: Result<Vec<McpServerStatus>, String>) {
fn handle_mcp_inventory_result(
&mut self,
result: Result<Vec<McpServerStatus>, String>,
detail: McpServerStatusDetail,
) {
let config = self.chat_widget.config_ref().clone();
self.chat_widget.clear_mcp_inventory_loading();
self.clear_committed_mcp_inventory_loading();
Expand All @@ -2393,9 +2401,7 @@ impl App {

self.chat_widget
.add_to_history(history_cell::new_mcp_tools_output_from_statuses(
&config,
&statuses,
McpServerStatusDetail::ToolsAndAuthOnly,
&config, &statuses, detail,
));
}

Expand Down Expand Up @@ -4953,11 +4959,11 @@ impl App {
.on_plugin_enabled_set(cwd, plugin_id, enabled, result);
}
}
AppEvent::FetchMcpInventory => {
self.fetch_mcp_inventory(app_server);
AppEvent::FetchMcpInventory { detail } => {
self.fetch_mcp_inventory(app_server, detail);
}
AppEvent::McpInventoryLoaded { result } => {
self.handle_mcp_inventory_result(result);
AppEvent::McpInventoryLoaded { result, detail } => {
self.handle_mcp_inventory_result(result, detail);
}
AppEvent::SkillsListLoaded { result } => {
self.handle_skills_list_result(
Expand Down Expand Up @@ -6643,6 +6649,7 @@ fn side_return_shortcut_matches(key_event: KeyEvent) -> bool {
/// the inventory atomically. Each page requests up to 100 entries.
async fn fetch_all_mcp_server_statuses(
request_handle: AppServerRequestHandle,
detail: McpServerStatusDetail,
) -> Result<Vec<McpServerStatus>> {
let mut cursor = None;
let mut statuses = Vec::new();
Expand All @@ -6655,7 +6662,7 @@ async fn fetch_all_mcp_server_statuses(
params: ListMcpServerStatusParams {
cursor: cursor.clone(),
limit: Some(100),
detail: Some(McpServerStatusDetail::ToolsAndAuthOnly),
detail: Some(detail),
},
})
.await
Expand Down Expand Up @@ -7098,13 +7105,16 @@ mod tests {
/*animations_enabled*/ false,
)));

app.handle_mcp_inventory_result(Ok(vec![McpServerStatus {
name: "docs".to_string(),
tools: HashMap::new(),
resources: Vec::new(),
resource_templates: Vec::new(),
auth_status: codex_app_server_protocol::McpAuthStatus::Unsupported,
}]));
app.handle_mcp_inventory_result(
Ok(vec![McpServerStatus {
name: "docs".to_string(),
tools: HashMap::new(),
resources: Vec::new(),
resource_templates: Vec::new(),
auth_status: codex_app_server_protocol::McpAuthStatus::Unsupported,
}]),
McpServerStatusDetail::ToolsAndAuthOnly,
);

assert_eq!(app.transcript_cells.len(), 0);
}
Expand Down
6 changes: 5 additions & 1 deletion codex-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use std::path::PathBuf;

use codex_app_server_protocol::AppInfo;
use codex_app_server_protocol::McpServerStatus;
use codex_app_server_protocol::McpServerStatusDetail;
use codex_app_server_protocol::PluginInstallResponse;
use codex_app_server_protocol::PluginListResponse;
use codex_app_server_protocol::PluginReadParams;
Expand Down Expand Up @@ -322,11 +323,14 @@ pub(crate) enum AppEvent {
PluginInstallAuthAbandon,

/// Fetch MCP inventory via app-server RPCs and render it into history.
FetchMcpInventory,
FetchMcpInventory {
detail: McpServerStatusDetail,
},

/// Result of fetching MCP inventory via app-server RPCs.
McpInventoryLoaded {
result: Result<Vec<McpServerStatus>, String>,
detail: McpServerStatusDetail,
},

/// Result of the startup skills refresh that runs after the first frame is scheduled.
Expand Down
6 changes: 4 additions & 2 deletions codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ use codex_app_server_protocol::GuardianApprovalReviewAction;
use codex_app_server_protocol::ItemCompletedNotification;
use codex_app_server_protocol::ItemStartedNotification;
use codex_app_server_protocol::McpServerStartupState;
use codex_app_server_protocol::McpServerStatusDetail;
use codex_app_server_protocol::McpServerStatusUpdatedNotification;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequest;
Expand Down Expand Up @@ -10073,15 +10074,16 @@ impl ChatWidget {
///
/// The spinner lives in `active_cell` and is cleared by
/// [`clear_mcp_inventory_loading`] once the result arrives.
pub(crate) fn add_mcp_output(&mut self) {
pub(crate) fn add_mcp_output(&mut self, detail: McpServerStatusDetail) {
self.flush_answer_stream_with_separator();
self.flush_active_cell();
self.active_cell = Some(Box::new(history_cell::new_mcp_inventory_loading(
self.config.animations,
)));
self.bump_active_cell_revision();
self.request_redraw();
self.app_event_tx.send(AppEvent::FetchMcpInventory);
self.app_event_tx
.send(AppEvent::FetchMcpInventory { detail });
}

/// Remove the MCP loading spinner if it is still the active cell.
Expand Down
6 changes: 5 additions & 1 deletion codex-rs/tui/src/chatwidget/slash_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ impl ChatWidget {
self.add_app_server_stub_message("Memory maintenance");
}
SlashCommand::Mcp => {
self.add_mcp_output();
self.add_mcp_output(McpServerStatusDetail::ToolsAndAuthOnly);
}
SlashCommand::Apps => {
self.add_connectors_output();
Expand Down Expand Up @@ -543,6 +543,10 @@ impl ChatWidget {
}
}
}
SlashCommand::Mcp => match trimmed.to_ascii_lowercase().as_str() {
"verbose" => self.add_mcp_output(McpServerStatusDetail::Full),
_ => self.add_error_message("Usage: /mcp [verbose]".to_string()),
},
SlashCommand::Rename if !trimmed.is_empty() => {
if !self.ensure_thread_rename_allowed() {
return;
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/chatwidget/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ pub(super) use codex_app_server_protocol::ItemGuardianApprovalReviewStartedNotif
pub(super) use codex_app_server_protocol::ItemStartedNotification;
pub(super) use codex_app_server_protocol::MarketplaceInterface;
pub(super) use codex_app_server_protocol::McpServerStartupState;
pub(super) use codex_app_server_protocol::McpServerStatusDetail;
pub(super) use codex_app_server_protocol::McpServerStatusUpdatedNotification;
pub(super) use codex_app_server_protocol::PatchApplyStatus as AppServerPatchApplyStatus;
pub(super) use codex_app_server_protocol::PatchChangeKind;
Expand Down
43 changes: 42 additions & 1 deletion codex-rs/tui/src/chatwidget/tests/slash_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1111,7 +1111,48 @@ async fn slash_mcp_requests_inventory_via_app_server() {
chat.dispatch_command(SlashCommand::Mcp);

assert!(active_blob(&chat).contains("Loading MCP inventory"));
assert_matches!(rx.try_recv(), Ok(AppEvent::FetchMcpInventory));
assert_matches!(
rx.try_recv(),
Ok(AppEvent::FetchMcpInventory {
detail: McpServerStatusDetail::ToolsAndAuthOnly
})
);
assert!(op_rx.try_recv().is_err(), "expected no core op to be sent");
}

#[tokio::test]
async fn slash_mcp_verbose_requests_full_inventory_via_app_server() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;

submit_composer_text(&mut chat, "/mcp verbose");

assert!(active_blob(&chat).contains("Loading MCP inventory"));
assert_matches!(
rx.try_recv(),
Ok(AppEvent::FetchMcpInventory {
detail: McpServerStatusDetail::Full
})
);
assert!(op_rx.try_recv().is_err(), "expected no core op to be sent");
}

#[tokio::test]
async fn slash_mcp_invalid_args_show_usage() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;

submit_composer_text(&mut chat, "/mcp full");

let cells = drain_insert_history(&mut rx);
let rendered = cells
.iter()
.map(|cell| lines_to_single_string(cell))
.collect::<Vec<_>>()
.join("\n");
assert!(
rendered.contains("Usage: /mcp [verbose]"),
"expected usage message, got: {rendered:?}"
);
assert_eq!(recall_latest_after_clearing(&mut chat), "/mcp full");
assert!(op_rx.try_recv().is_err(), "expected no core op to be sent");
}

Expand Down
67 changes: 67 additions & 0 deletions codex-rs/tui/src/history_cell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2097,6 +2097,18 @@ pub(crate) fn new_mcp_tools_output_from_statuses(
let header: Vec<Span<'static>> = vec![" • ".into(), server.clone().into()];

lines.push(header.into());
if matches!(detail, McpServerStatusDetail::Full) {
let enabled = cfg.map(|cfg| cfg.enabled).unwrap_or(true);
let status_text = if enabled {
"enabled".green()
} else {
"disabled".red()
};
lines.push(vec![" • Status: ".into(), status_text].into());
if let Some(reason) = cfg.and_then(|cfg| cfg.disabled_reason.as_ref()) {
lines.push(vec![" • Reason: ".into(), reason.to_string().dim()].into());
}
}
let auth_status = status
.map(|status| match status.auth_status {
codex_app_server_protocol::McpAuthStatus::Unsupported => McpAuthStatus::Unsupported,
Expand Down Expand Up @@ -3446,6 +3458,61 @@ mod tests {
insta::assert_snapshot!(rendered);
}

#[tokio::test]
async fn mcp_tools_output_from_statuses_renders_verbose_inventory() {
let mut config = test_config().await;
let plugin_docs =
stdio_server_config("docs-server", vec!["--stdio"], /*env*/ None, vec![]);
let servers = HashMap::from([("plugin_docs".to_string(), plugin_docs)]);
config
.mcp_servers
.set(servers)
.expect("test mcp servers should accept any configuration");

let statuses = vec![McpServerStatus {
name: "plugin_docs".to_string(),
tools: HashMap::from([(
"lookup".to_string(),
Tool {
description: None,
name: "lookup".to_string(),
title: None,
input_schema: serde_json::json!({"type": "object", "properties": {}}),
output_schema: None,
annotations: None,
icons: None,
meta: None,
},
)]),
resources: vec![Resource {
annotations: None,
description: None,
mime_type: None,
name: "docs".to_string(),
size: None,
title: Some("Docs".to_string()),
uri: "file:///docs".to_string(),
icons: None,
meta: None,
}],
resource_templates: vec![ResourceTemplate {
annotations: None,
uri_template: "file:///docs/{id}".to_string(),
name: "doc-template".to_string(),
title: Some("Doc Template".to_string()),
description: None,
mime_type: None,
}],
auth_status: codex_app_server_protocol::McpAuthStatus::Unsupported,
}];

let cell =
new_mcp_tools_output_from_statuses(&config, &statuses, McpServerStatusDetail::Full);
let rendered = render_lines(&cell.display_lines(/*width*/ 120)).join("\n");

insta::assert_snapshot!(rendered);
}

#[test]
fn empty_agent_message_cell_transcript() {
let cell = AgentMessageCell::new(vec![Line::default()], /*is_first_line*/ false);
Expand Down
3 changes: 2 additions & 1 deletion codex-rs/tui/src/slash_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ impl SlashCommand {
}
SlashCommand::Experimental => "toggle experimental features",
SlashCommand::Memories => "configure memory use and generation",
SlashCommand::Mcp => "list configured MCP tools",
SlashCommand::Mcp => "list configured MCP tools; use /mcp verbose for details",
SlashCommand::Apps => "manage apps",
SlashCommand::Plugins => "browse plugins",
SlashCommand::Logout => "log out of Codex",
Expand All @@ -136,6 +136,7 @@ impl SlashCommand {
| SlashCommand::Rename
| SlashCommand::Plan
| SlashCommand::Fast
| SlashCommand::Mcp
| SlashCommand::Side
| SlashCommand::Resume
| SlashCommand::SandboxReadRoot
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
source: tui/src/history_cell.rs
expression: rendered
---
/mcp

🔌 MCP Tools

• plugin_docs
• Status: enabled
• Auth: Unsupported
• Command: docs-server --stdio
• Tools: lookup
• Resources: Docs (file:///docs)
• Resource templates: Doc Template (file:///docs/{id})
Loading