[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:
@@ -1199,6 +1199,22 @@ async fn submission_loop(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Op::ListMcpTools => {
|
||||||
|
let tx_event = sess.tx_event.clone();
|
||||||
|
let sub_id = sub.id.clone();
|
||||||
|
|
||||||
|
// This is a cheap lookup from the connection manager's cache.
|
||||||
|
let tools = sess.mcp_connection_manager.list_all_tools();
|
||||||
|
let event = Event {
|
||||||
|
id: sub_id,
|
||||||
|
msg: EventMsg::McpListToolsResponse(
|
||||||
|
crate::protocol::McpListToolsResponseEvent { tools },
|
||||||
|
),
|
||||||
|
};
|
||||||
|
if let Err(e) = tx_event.send(event).await {
|
||||||
|
warn!("failed to send McpListToolsResponse event: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
Op::Compact => {
|
Op::Compact => {
|
||||||
// Create a summarization request as user input
|
// Create a summarization request as user input
|
||||||
const SUMMARIZATION_PROMPT: &str = include_str!("prompt_for_compact_command.md");
|
const SUMMARIZATION_PROMPT: &str = include_str!("prompt_for_compact_command.md");
|
||||||
|
|||||||
@@ -523,6 +523,9 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
|||||||
EventMsg::GetHistoryEntryResponse(_) => {
|
EventMsg::GetHistoryEntryResponse(_) => {
|
||||||
// Currently ignored in exec output.
|
// Currently ignored in exec output.
|
||||||
}
|
}
|
||||||
|
EventMsg::McpListToolsResponse(_) => {
|
||||||
|
// Currently ignored in exec output.
|
||||||
|
}
|
||||||
EventMsg::TurnAborted(abort_reason) => match abort_reason.reason {
|
EventMsg::TurnAborted(abort_reason) => match abort_reason.reason {
|
||||||
TurnAbortReason::Interrupted => {
|
TurnAbortReason::Interrupted => {
|
||||||
ts_println!(self, "task interrupted");
|
ts_println!(self, "task interrupted");
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ async fn run_codex_tool_session_inner(
|
|||||||
| EventMsg::AgentReasoningSectionBreak(_)
|
| EventMsg::AgentReasoningSectionBreak(_)
|
||||||
| EventMsg::McpToolCallBegin(_)
|
| EventMsg::McpToolCallBegin(_)
|
||||||
| EventMsg::McpToolCallEnd(_)
|
| EventMsg::McpToolCallEnd(_)
|
||||||
|
| EventMsg::McpListToolsResponse(_)
|
||||||
| EventMsg::ExecCommandBegin(_)
|
| EventMsg::ExecCommandBegin(_)
|
||||||
| EventMsg::ExecCommandOutputDelta(_)
|
| EventMsg::ExecCommandOutputDelta(_)
|
||||||
| EventMsg::ExecCommandEnd(_)
|
| EventMsg::ExecCommandEnd(_)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use std::str::FromStr;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use mcp_types::CallToolResult;
|
use mcp_types::CallToolResult;
|
||||||
|
use mcp_types::Tool as McpTool;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_bytes::ByteBuf;
|
use serde_bytes::ByteBuf;
|
||||||
@@ -136,6 +137,10 @@ pub enum Op {
|
|||||||
/// Request a single history entry identified by `log_id` + `offset`.
|
/// Request a single history entry identified by `log_id` + `offset`.
|
||||||
GetHistoryEntryRequest { offset: usize, log_id: u64 },
|
GetHistoryEntryRequest { offset: usize, log_id: u64 },
|
||||||
|
|
||||||
|
/// Request the list of MCP tools available across all configured servers.
|
||||||
|
/// Reply is delivered via `EventMsg::McpListToolsResponse`.
|
||||||
|
ListMcpTools,
|
||||||
|
|
||||||
/// Request the agent to summarize the current conversation context.
|
/// Request the agent to summarize the current conversation context.
|
||||||
/// The agent will use its existing context (either conversation history or previous response id)
|
/// The agent will use its existing context (either conversation history or previous response id)
|
||||||
/// to generate a summary which will be returned as an AgentMessage event.
|
/// to generate a summary which will be returned as an AgentMessage event.
|
||||||
@@ -453,6 +458,9 @@ pub enum EventMsg {
|
|||||||
/// Response to GetHistoryEntryRequest.
|
/// Response to GetHistoryEntryRequest.
|
||||||
GetHistoryEntryResponse(GetHistoryEntryResponseEvent),
|
GetHistoryEntryResponse(GetHistoryEntryResponseEvent),
|
||||||
|
|
||||||
|
/// List of MCP tools available to the agent.
|
||||||
|
McpListToolsResponse(McpListToolsResponseEvent),
|
||||||
|
|
||||||
PlanUpdate(UpdatePlanArgs),
|
PlanUpdate(UpdatePlanArgs),
|
||||||
|
|
||||||
TurnAborted(TurnAbortedEvent),
|
TurnAborted(TurnAbortedEvent),
|
||||||
@@ -749,6 +757,13 @@ pub struct GetHistoryEntryResponseEvent {
|
|||||||
pub entry: Option<HistoryEntry>,
|
pub entry: Option<HistoryEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Response payload for `Op::ListMcpTools`.
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct McpListToolsResponseEvent {
|
||||||
|
/// Fully qualified tool name -> tool definition.
|
||||||
|
pub tools: std::collections::HashMap<String, McpTool>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
|
||||||
pub struct SessionConfiguredEvent {
|
pub struct SessionConfiguredEvent {
|
||||||
/// Unique id for this session.
|
/// Unique id for this session.
|
||||||
|
|||||||
@@ -421,6 +421,11 @@ impl App<'_> {
|
|||||||
widget.add_status_output();
|
widget.add_status_output();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SlashCommand::Mcp => {
|
||||||
|
if let AppState::Chat { widget } = &mut self.app_state {
|
||||||
|
widget.add_mcp_output();
|
||||||
|
}
|
||||||
|
}
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
SlashCommand::TestApproval => {
|
SlashCommand::TestApproval => {
|
||||||
use codex_core::protocol::EventMsg;
|
use codex_core::protocol::EventMsg;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use codex_core::protocol::ExecApprovalRequestEvent;
|
|||||||
use codex_core::protocol::ExecCommandBeginEvent;
|
use codex_core::protocol::ExecCommandBeginEvent;
|
||||||
use codex_core::protocol::ExecCommandEndEvent;
|
use codex_core::protocol::ExecCommandEndEvent;
|
||||||
use codex_core::protocol::InputItem;
|
use codex_core::protocol::InputItem;
|
||||||
|
use codex_core::protocol::McpListToolsResponseEvent;
|
||||||
use codex_core::protocol::McpToolCallBeginEvent;
|
use codex_core::protocol::McpToolCallBeginEvent;
|
||||||
use codex_core::protocol::McpToolCallEndEvent;
|
use codex_core::protocol::McpToolCallEndEvent;
|
||||||
use codex_core::protocol::Op;
|
use codex_core::protocol::Op;
|
||||||
@@ -647,6 +648,7 @@ impl ChatWidget<'_> {
|
|||||||
EventMsg::McpToolCallBegin(ev) => self.on_mcp_tool_call_begin(ev),
|
EventMsg::McpToolCallBegin(ev) => self.on_mcp_tool_call_begin(ev),
|
||||||
EventMsg::McpToolCallEnd(ev) => self.on_mcp_tool_call_end(ev),
|
EventMsg::McpToolCallEnd(ev) => self.on_mcp_tool_call_end(ev),
|
||||||
EventMsg::GetHistoryEntryResponse(ev) => self.on_get_history_entry_response(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::ShutdownComplete => self.on_shutdown_complete(),
|
||||||
EventMsg::TurnDiff(TurnDiffEvent { unified_diff }) => self.on_turn_diff(unified_diff),
|
EventMsg::TurnDiff(TurnDiffEvent { unified_diff }) => self.on_turn_diff(unified_diff),
|
||||||
EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => {
|
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.
|
/// Forward file-search results to the bottom pane.
|
||||||
pub(crate) fn apply_file_search_result(&mut self, query: String, matches: Vec<FileMatch>) {
|
pub(crate) fn apply_file_search_result(&mut self, query: String, matches: Vec<FileMatch>) {
|
||||||
self.bottom_pane.on_file_search_result(query, matches);
|
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
|
/// Programmatically submit a user text message as if typed in the
|
||||||
/// composer. The text will be added to conversation history and sent to
|
/// composer. The text will be added to conversation history and sent to
|
||||||
/// the agent.
|
/// the agent.
|
||||||
|
|||||||
@@ -634,6 +634,87 @@ pub(crate) fn new_status_output(
|
|||||||
PlainHistoryCell { lines }
|
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 {
|
pub(crate) fn new_error_event(message: String) -> PlainHistoryCell {
|
||||||
let lines: Vec<Line<'static>> = vec![vec!["🖐 ".red().bold(), message.into()].into(), "".into()];
|
let lines: Vec<Line<'static>> = vec![vec!["🖐 ".red().bold(), message.into()].into(), "".into()];
|
||||||
PlainHistoryCell { lines }
|
PlainHistoryCell { lines }
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ pub enum SlashCommand {
|
|||||||
Diff,
|
Diff,
|
||||||
Mention,
|
Mention,
|
||||||
Status,
|
Status,
|
||||||
|
Mcp,
|
||||||
Logout,
|
Logout,
|
||||||
Quit,
|
Quit,
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
@@ -35,6 +36,7 @@ impl SlashCommand {
|
|||||||
SlashCommand::Diff => "show git diff (including untracked files)",
|
SlashCommand::Diff => "show git diff (including untracked files)",
|
||||||
SlashCommand::Mention => "mention a file",
|
SlashCommand::Mention => "mention a file",
|
||||||
SlashCommand::Status => "show current session configuration and token usage",
|
SlashCommand::Status => "show current session configuration and token usage",
|
||||||
|
SlashCommand::Mcp => "list configured MCP tools",
|
||||||
SlashCommand::Logout => "log out of Codex",
|
SlashCommand::Logout => "log out of Codex",
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
SlashCommand::TestApproval => "test approval request",
|
SlashCommand::TestApproval => "test approval request",
|
||||||
|
|||||||
Reference in New Issue
Block a user