[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:
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user