feat: show MCP tool calls in codex exec subcommand (#841)
This is analogous to the change for the TUI in https://github.com/openai/codex/pull/836, but for `codex exec`. To test, I ran: ``` cargo run --bin codex-exec -- 'what is the weather in wellesley ma tomorrow' ``` and saw: 
This commit is contained in:
2
codex-rs/Cargo.lock
generated
2
codex-rs/Cargo.lock
generated
@@ -531,7 +531,9 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"codex-core",
|
"codex-core",
|
||||||
|
"mcp-types",
|
||||||
"owo-colors 4.2.0",
|
"owo-colors 4.2.0",
|
||||||
|
"serde_json",
|
||||||
"shlex",
|
"shlex",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ anyhow = "1"
|
|||||||
chrono = "0.4.40"
|
chrono = "0.4.40"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
codex-core = { path = "../core", features = ["cli"] }
|
codex-core = { path = "../core", features = ["cli"] }
|
||||||
|
mcp-types = { path = "../mcp-types" }
|
||||||
owo-colors = "4.2.0"
|
owo-colors = "4.2.0"
|
||||||
|
serde_json = "1"
|
||||||
shlex = "1.3.0"
|
shlex = "1.3.0"
|
||||||
tokio = { version = "1", features = [
|
tokio = { version = "1", features = [
|
||||||
"io-std",
|
"io-std",
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ pub(crate) struct EventProcessor {
|
|||||||
call_id_to_command: HashMap<String, ExecCommandBegin>,
|
call_id_to_command: HashMap<String, ExecCommandBegin>,
|
||||||
call_id_to_patch: HashMap<String, PatchApplyBegin>,
|
call_id_to_patch: HashMap<String, PatchApplyBegin>,
|
||||||
|
|
||||||
|
/// Tracks in-flight MCP tool calls so we can calculate duration and print
|
||||||
|
/// a concise summary when the corresponding `McpToolCallEnd` event is
|
||||||
|
/// received.
|
||||||
|
call_id_to_tool_call: HashMap<String, McpToolCallBegin>,
|
||||||
|
|
||||||
// To ensure that --color=never is respected, ANSI escapes _must_ be added
|
// To ensure that --color=never is respected, ANSI escapes _must_ be added
|
||||||
// using .style() with one of these fields. If you need a new style, add a
|
// using .style() with one of these fields. If you need a new style, add a
|
||||||
// new field here.
|
// new field here.
|
||||||
@@ -30,6 +35,7 @@ impl EventProcessor {
|
|||||||
pub(crate) fn create_with_ansi(with_ansi: bool) -> Self {
|
pub(crate) fn create_with_ansi(with_ansi: bool) -> Self {
|
||||||
let call_id_to_command = HashMap::new();
|
let call_id_to_command = HashMap::new();
|
||||||
let call_id_to_patch = HashMap::new();
|
let call_id_to_patch = HashMap::new();
|
||||||
|
let call_id_to_tool_call = HashMap::new();
|
||||||
|
|
||||||
if with_ansi {
|
if with_ansi {
|
||||||
Self {
|
Self {
|
||||||
@@ -40,6 +46,7 @@ impl EventProcessor {
|
|||||||
magenta: Style::new().magenta(),
|
magenta: Style::new().magenta(),
|
||||||
red: Style::new().red(),
|
red: Style::new().red(),
|
||||||
green: Style::new().green(),
|
green: Style::new().green(),
|
||||||
|
call_id_to_tool_call,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Self {
|
Self {
|
||||||
@@ -50,6 +57,7 @@ impl EventProcessor {
|
|||||||
magenta: Style::new(),
|
magenta: Style::new(),
|
||||||
red: Style::new(),
|
red: Style::new(),
|
||||||
green: Style::new(),
|
green: Style::new(),
|
||||||
|
call_id_to_tool_call,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,6 +68,14 @@ struct ExecCommandBegin {
|
|||||||
start_time: chrono::DateTime<Utc>,
|
start_time: chrono::DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Metadata captured when an `McpToolCallBegin` event is received.
|
||||||
|
struct McpToolCallBegin {
|
||||||
|
/// Formatted invocation string, e.g. `server.tool({"city":"sf"})`.
|
||||||
|
invocation: String,
|
||||||
|
/// Timestamp when the call started so we can compute duration later.
|
||||||
|
start_time: chrono::DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
struct PatchApplyBegin {
|
struct PatchApplyBegin {
|
||||||
start_time: chrono::DateTime<Utc>,
|
start_time: chrono::DateTime<Utc>,
|
||||||
auto_approved: bool,
|
auto_approved: bool,
|
||||||
@@ -154,6 +170,78 @@ impl EventProcessor {
|
|||||||
}
|
}
|
||||||
println!("{}", truncated_output.style(self.dimmed));
|
println!("{}", truncated_output.style(self.dimmed));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle MCP tool calls (e.g. calling external functions via MCP).
|
||||||
|
EventMsg::McpToolCallBegin {
|
||||||
|
call_id,
|
||||||
|
server,
|
||||||
|
tool,
|
||||||
|
arguments,
|
||||||
|
} => {
|
||||||
|
// Build fully-qualified tool name: server.tool
|
||||||
|
let fq_tool_name = format!("{server}.{tool}");
|
||||||
|
|
||||||
|
// Format arguments as compact JSON so they fit on one line.
|
||||||
|
let args_str = arguments
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| serde_json::to_string(v).unwrap_or_else(|_| v.to_string()))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let invocation = if args_str.is_empty() {
|
||||||
|
format!("{fq_tool_name}()")
|
||||||
|
} else {
|
||||||
|
format!("{fq_tool_name}({args_str})")
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_id_to_tool_call.insert(
|
||||||
|
call_id.clone(),
|
||||||
|
McpToolCallBegin {
|
||||||
|
invocation: invocation.clone(),
|
||||||
|
start_time: Utc::now(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ts_println!(
|
||||||
|
"{} {}",
|
||||||
|
"tool".style(self.magenta),
|
||||||
|
invocation.style(self.bold),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
EventMsg::McpToolCallEnd {
|
||||||
|
call_id,
|
||||||
|
success,
|
||||||
|
result,
|
||||||
|
} => {
|
||||||
|
// Retrieve start time and invocation for duration calculation and labeling.
|
||||||
|
let info = self.call_id_to_tool_call.remove(&call_id);
|
||||||
|
|
||||||
|
let (duration, invocation) = if let Some(McpToolCallBegin {
|
||||||
|
invocation,
|
||||||
|
start_time,
|
||||||
|
..
|
||||||
|
}) = info
|
||||||
|
{
|
||||||
|
(format_duration(start_time), invocation)
|
||||||
|
} else {
|
||||||
|
(String::new(), format!("tool('{call_id}')"))
|
||||||
|
};
|
||||||
|
|
||||||
|
let status_str = if success { "success" } else { "failed" };
|
||||||
|
let title_style = if success { self.green } else { self.red };
|
||||||
|
let title = format!("{invocation} {status_str}{duration}:");
|
||||||
|
|
||||||
|
ts_println!("{}", title.style(title_style));
|
||||||
|
|
||||||
|
if let Some(res) = result {
|
||||||
|
let val: serde_json::Value = res.into();
|
||||||
|
let pretty =
|
||||||
|
serde_json::to_string_pretty(&val).unwrap_or_else(|_| val.to_string());
|
||||||
|
|
||||||
|
for line in pretty.lines().take(MAX_OUTPUT_LINES_FOR_EXEC_TOOL_CALL) {
|
||||||
|
println!("{}", line.style(self.dimmed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
EventMsg::PatchApplyBegin {
|
EventMsg::PatchApplyBegin {
|
||||||
call_id,
|
call_id,
|
||||||
auto_approved,
|
auto_approved,
|
||||||
|
|||||||
Reference in New Issue
Block a user