[tui] Support /mcp command (#2430)

## Summary
Adds a `/mcp` command to list active tools. We can extend this command
to allow configuration of MCP tools, but for now a simple list command
will help debug if your config.toml and your tools are working as
expected.
This commit is contained in:
Dylan
2025-08-19 09:00:31 -07:00
committed by GitHub
parent 096bca2fa2
commit e7e5fe91c8
8 changed files with 137 additions and 0 deletions

View File

@@ -421,6 +421,11 @@ impl App<'_> {
widget.add_status_output();
}
}
SlashCommand::Mcp => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.add_mcp_output();
}
}
#[cfg(debug_assertions)]
SlashCommand::TestApproval => {
use codex_core::protocol::EventMsg;

View File

@@ -18,6 +18,7 @@ use codex_core::protocol::ExecApprovalRequestEvent;
use codex_core::protocol::ExecCommandBeginEvent;
use codex_core::protocol::ExecCommandEndEvent;
use codex_core::protocol::InputItem;
use codex_core::protocol::McpListToolsResponseEvent;
use codex_core::protocol::McpToolCallBeginEvent;
use codex_core::protocol::McpToolCallEndEvent;
use codex_core::protocol::Op;
@@ -647,6 +648,7 @@ impl ChatWidget<'_> {
EventMsg::McpToolCallBegin(ev) => self.on_mcp_tool_call_begin(ev),
EventMsg::McpToolCallEnd(ev) => self.on_mcp_tool_call_end(ev),
EventMsg::GetHistoryEntryResponse(ev) => self.on_get_history_entry_response(ev),
EventMsg::McpListToolsResponse(ev) => self.on_list_mcp_tools(ev),
EventMsg::ShutdownComplete => self.on_shutdown_complete(),
EventMsg::TurnDiff(TurnDiffEvent { unified_diff }) => self.on_turn_diff(unified_diff),
EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => {
@@ -685,6 +687,14 @@ impl ChatWidget<'_> {
));
}
pub(crate) fn add_mcp_output(&mut self) {
if self.config.mcp_servers.is_empty() {
self.add_to_history(&history_cell::empty_mcp_output());
} else {
self.submit_op(Op::ListMcpTools);
}
}
/// Forward file-search results to the bottom pane.
pub(crate) fn apply_file_search_result(&mut self, query: String, matches: Vec<FileMatch>) {
self.bottom_pane.on_file_search_result(query, matches);
@@ -726,6 +736,10 @@ impl ChatWidget<'_> {
}
}
fn on_list_mcp_tools(&mut self, ev: McpListToolsResponseEvent) {
self.add_to_history(&history_cell::new_mcp_tools_output(&self.config, ev.tools));
}
/// Programmatically submit a user text message as if typed in the
/// composer. The text will be added to conversation history and sent to
/// the agent.

View File

@@ -634,6 +634,87 @@ pub(crate) fn new_status_output(
PlainHistoryCell { lines }
}
/// Render a summary of configured MCP servers from the current `Config`.
pub(crate) fn empty_mcp_output() -> PlainHistoryCell {
let lines: Vec<Line<'static>> = vec![
Line::from("/mcp".magenta()),
Line::from(""),
Line::from(vec!["🔌 ".into(), "MCP Tools".bold()]),
Line::from(""),
Line::from(" • No MCP servers configured.".italic()),
Line::from(""),
];
PlainHistoryCell { lines }
}
/// Render MCP tools grouped by connection using the fully-qualified tool names.
pub(crate) fn new_mcp_tools_output(
config: &Config,
tools: std::collections::HashMap<String, mcp_types::Tool>,
) -> PlainHistoryCell {
let mut lines: Vec<Line<'static>> = vec![
Line::from("/mcp".magenta()),
Line::from(""),
Line::from(vec!["🔌 ".into(), "MCP Tools".bold()]),
Line::from(""),
];
if tools.is_empty() {
lines.push(Line::from(" • No MCP tools available.".italic()));
lines.push(Line::from(""));
return PlainHistoryCell { lines };
}
for (server, cfg) in config.mcp_servers.iter() {
let prefix = format!("{server}__");
let mut names: Vec<String> = tools
.keys()
.filter(|k| k.starts_with(&prefix))
.map(|k| k[prefix.len()..].to_string())
.collect();
names.sort();
lines.push(Line::from(vec![
" • Server: ".into(),
server.clone().into(),
]));
if !cfg.command.is_empty() {
let cmd_display = format!("{} {}", cfg.command, cfg.args.join(" "));
lines.push(Line::from(vec![
" • Command: ".into(),
cmd_display.into(),
]));
}
if let Some(env) = cfg.env.as_ref() {
if !env.is_empty() {
let mut env_pairs: Vec<String> =
env.iter().map(|(k, v)| format!("{k}={v}")).collect();
env_pairs.sort();
lines.push(Line::from(vec![
" • Env: ".into(),
env_pairs.join(" ").into(),
]));
}
}
if names.is_empty() {
lines.push(Line::from(" • Tools: (none)"));
} else {
lines.push(Line::from(vec![
" • Tools: ".into(),
names.join(", ").into(),
]));
}
lines.push(Line::from(""));
}
PlainHistoryCell { lines }
}
pub(crate) fn new_error_event(message: String) -> PlainHistoryCell {
let lines: Vec<Line<'static>> = vec![vec!["🖐 ".red().bold(), message.into()].into(), "".into()];
PlainHistoryCell { lines }

View File

@@ -18,6 +18,7 @@ pub enum SlashCommand {
Diff,
Mention,
Status,
Mcp,
Logout,
Quit,
#[cfg(debug_assertions)]
@@ -35,6 +36,7 @@ impl SlashCommand {
SlashCommand::Diff => "show git diff (including untracked files)",
SlashCommand::Mention => "mention a file",
SlashCommand::Status => "show current session configuration and token usage",
SlashCommand::Mcp => "list configured MCP tools",
SlashCommand::Logout => "log out of Codex",
#[cfg(debug_assertions)]
SlashCommand::TestApproval => "test approval request",