mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Emit MCP tool calls as turn items (#20677)
## Why `McpToolCall` was still an app-server item synthesized from deprecated legacy begin/end events. Recent item migrations moved this ownership into core `TurnItem`s, so MCP tool calls now follow the same canonical lifecycle and leave legacy events as compatibility fanout. Keeping the core item close to the v2 `ThreadItem::McpToolCall` shape also avoids spreading MCP result semantics across app-server conversion code. Core now owns whether a completed call is `completed` or `failed`, and whether the payload is a tool result or an error. ## What changed - Added core `TurnItem::McpToolCall` with flattened `server`, `tool`, `arguments`, `status`, `result`, and `error` fields. - Updated MCP tool call emitters, including MCP resource tools, to emit `ItemStarted`/`ItemCompleted` around directly constructed core MCP items. - Updated app-server v2 conversion to project the core MCP item into `ThreadItem::McpToolCall` without deriving status or splitting `Result` locally. - Ignored live deprecated MCP legacy fanout in app-server v2 to avoid duplicate item notifications, while keeping thread history replay on the legacy event path. ## Verification - `cargo test -p codex-protocol` - `cargo test -p codex-app-server-protocol` - `cargo test -p codex-core --lib mcp_tool_call` - `cargo check -p codex-app-server` - `cargo test -p codex-app-server mcp_tool_call_completion_notification_contains_truncated_large_result`
This commit is contained in:
committed by
GitHub
Unverified
parent
9ddfda9db7
commit
c8c30d9d75
@@ -45,12 +45,13 @@ use codex_mcp::SandboxState;
|
||||
use codex_mcp::declared_openai_file_input_param_names;
|
||||
use codex_mcp::mcp_permission_prompt_is_auto_approved;
|
||||
use codex_otel::sanitize_metric_tag_value;
|
||||
use codex_protocol::items::McpToolCallError;
|
||||
use codex_protocol::items::McpToolCallItem;
|
||||
use codex_protocol::items::McpToolCallStatus;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::openai_models::InputModality;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::McpInvocation;
|
||||
use codex_protocol::protocol::McpToolCallBeginEvent;
|
||||
use codex_protocol::protocol::McpToolCallEndEvent;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::request_user_input::RequestUserInputAnswer;
|
||||
use codex_protocol::request_user_input::RequestUserInputArgs;
|
||||
@@ -87,8 +88,8 @@ const MCP_RESULT_TELEMETRY_SERVER_USER_FLOW_SPAN_ATTR: &str =
|
||||
const MCP_RESULT_TELEMETRY_TARGET_ID_MAX_CHARS: usize = 256;
|
||||
const MCP_TOOL_CALL_EVENT_RESULT_MAX_BYTES: usize = DEFAULT_OUTPUT_BYTES_CAP;
|
||||
|
||||
/// Handles the specified tool call dispatches the appropriate
|
||||
/// `McpToolCallBegin` and `McpToolCallEnd` events to the `Session`.
|
||||
/// Handles the specified tool call and dispatches the appropriate MCP tool-call
|
||||
/// item lifecycle events to the `Session`.
|
||||
pub(crate) async fn handle_mcp_tool_call(
|
||||
sess: Arc<Session>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
@@ -186,12 +187,14 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.connector_name.clone());
|
||||
|
||||
let tool_call_begin_event = EventMsg::McpToolCallBegin(McpToolCallBeginEvent {
|
||||
call_id: call_id.clone(),
|
||||
invocation: invocation.clone(),
|
||||
mcp_app_resource_uri: mcp_app_resource_uri.clone(),
|
||||
});
|
||||
notify_mcp_tool_call_event(sess.as_ref(), turn_context.as_ref(), tool_call_begin_event).await;
|
||||
notify_mcp_tool_call_started(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
&call_id,
|
||||
invocation.clone(),
|
||||
mcp_app_resource_uri.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Some(decision) = maybe_request_mcp_tool_approval(
|
||||
&sess,
|
||||
@@ -362,14 +365,16 @@ async fn handle_approved_mcp_tool_call(
|
||||
tracing::warn!("MCP tool call error: {error:?}");
|
||||
}
|
||||
let duration = start.elapsed();
|
||||
let tool_call_end_event = EventMsg::McpToolCallEnd(McpToolCallEndEvent {
|
||||
call_id: call_id.to_string(),
|
||||
notify_mcp_tool_call_completed(
|
||||
sess,
|
||||
turn_context,
|
||||
call_id,
|
||||
invocation,
|
||||
mcp_app_resource_uri,
|
||||
duration,
|
||||
result: truncate_mcp_tool_result_for_event(&result),
|
||||
});
|
||||
notify_mcp_tool_call_event(sess, turn_context, tool_call_end_event.clone()).await;
|
||||
truncate_mcp_tool_result_for_event(&result),
|
||||
)
|
||||
.await;
|
||||
maybe_track_codex_app_used(sess, turn_context, &server, &tool_name).await;
|
||||
|
||||
let status = if result.is_ok() { "ok" } else { "error" };
|
||||
@@ -658,7 +663,7 @@ fn truncate_mcp_tool_result_for_event(
|
||||
) -> Result<CallToolResult, String> {
|
||||
match result {
|
||||
Ok(call_tool_result) => {
|
||||
// The app-server rebuilds `ThreadItem::McpToolCall` from this event,
|
||||
// The app-server rebuilds `ThreadItem::McpToolCall` from this item,
|
||||
// so avoid persisting multi-megabyte results in rollout storage.
|
||||
let Ok(serialized) = serde_json::to_string(call_tool_result) else {
|
||||
return Ok(call_tool_result.clone());
|
||||
@@ -697,8 +702,69 @@ fn truncate_mcp_tool_result_for_event(
|
||||
}
|
||||
}
|
||||
|
||||
async fn notify_mcp_tool_call_event(sess: &Session, turn_context: &TurnContext, event: EventMsg) {
|
||||
sess.send_event(turn_context, event).await;
|
||||
async fn notify_mcp_tool_call_started(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
call_id: &str,
|
||||
invocation: McpInvocation,
|
||||
mcp_app_resource_uri: Option<String>,
|
||||
) {
|
||||
let McpInvocation {
|
||||
server,
|
||||
tool,
|
||||
arguments,
|
||||
} = invocation;
|
||||
let item = TurnItem::McpToolCall(McpToolCallItem {
|
||||
id: call_id.to_string(),
|
||||
server,
|
||||
tool,
|
||||
arguments: arguments.unwrap_or(JsonValue::Null),
|
||||
mcp_app_resource_uri,
|
||||
status: McpToolCallStatus::InProgress,
|
||||
result: None,
|
||||
error: None,
|
||||
duration: None,
|
||||
});
|
||||
sess.emit_turn_item_started(turn_context, &item).await;
|
||||
}
|
||||
|
||||
async fn notify_mcp_tool_call_completed(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
call_id: &str,
|
||||
invocation: McpInvocation,
|
||||
mcp_app_resource_uri: Option<String>,
|
||||
duration: Duration,
|
||||
result: Result<CallToolResult, String>,
|
||||
) {
|
||||
let (status, result, error) = match result {
|
||||
Ok(result) if result.is_error.unwrap_or(false) => {
|
||||
(McpToolCallStatus::Failed, Some(result), None)
|
||||
}
|
||||
Ok(result) => (McpToolCallStatus::Completed, Some(result), None),
|
||||
Err(message) => (
|
||||
McpToolCallStatus::Failed,
|
||||
None,
|
||||
Some(McpToolCallError { message }),
|
||||
),
|
||||
};
|
||||
let McpInvocation {
|
||||
server,
|
||||
tool,
|
||||
arguments,
|
||||
} = invocation;
|
||||
let item = TurnItem::McpToolCall(McpToolCallItem {
|
||||
id: call_id.to_string(),
|
||||
server,
|
||||
tool,
|
||||
arguments: arguments.unwrap_or(JsonValue::Null),
|
||||
mcp_app_resource_uri,
|
||||
status,
|
||||
result,
|
||||
error,
|
||||
duration: Some(duration),
|
||||
});
|
||||
sess.emit_turn_item_completed(turn_context, item).await;
|
||||
}
|
||||
|
||||
struct McpAppUsageMetadata {
|
||||
@@ -1979,22 +2045,26 @@ async fn notify_mcp_tool_call_skip(
|
||||
already_started: bool,
|
||||
) -> Result<CallToolResult, String> {
|
||||
if !already_started {
|
||||
let tool_call_begin_event = EventMsg::McpToolCallBegin(McpToolCallBeginEvent {
|
||||
call_id: call_id.to_string(),
|
||||
invocation: invocation.clone(),
|
||||
mcp_app_resource_uri: mcp_app_resource_uri.clone(),
|
||||
});
|
||||
notify_mcp_tool_call_event(sess, turn_context, tool_call_begin_event).await;
|
||||
notify_mcp_tool_call_started(
|
||||
sess,
|
||||
turn_context,
|
||||
call_id,
|
||||
invocation.clone(),
|
||||
mcp_app_resource_uri.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let tool_call_end_event = EventMsg::McpToolCallEnd(McpToolCallEndEvent {
|
||||
call_id: call_id.to_string(),
|
||||
notify_mcp_tool_call_completed(
|
||||
sess,
|
||||
turn_context,
|
||||
call_id,
|
||||
invocation,
|
||||
mcp_app_resource_uri,
|
||||
duration: Duration::ZERO,
|
||||
result: truncate_mcp_tool_result_for_event(&Err(message.clone())),
|
||||
});
|
||||
notify_mcp_tool_call_event(sess, turn_context, tool_call_end_event).await;
|
||||
Duration::ZERO,
|
||||
truncate_mcp_tool_result_for_event(&Err(message.clone())),
|
||||
)
|
||||
.await;
|
||||
Err(message)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use codex_protocol::items::McpToolCallError;
|
||||
use codex_protocol::items::McpToolCallItem;
|
||||
use codex_protocol::items::McpToolCallStatus;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::models::function_call_output_content_items_to_text;
|
||||
use rmcp::model::ListResourceTemplatesResult;
|
||||
@@ -25,10 +29,7 @@ use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::McpInvocation;
|
||||
use codex_protocol::protocol::McpToolCallBeginEvent;
|
||||
use codex_protocol::protocol::McpToolCallEndEvent;
|
||||
|
||||
pub struct McpResourceHandler;
|
||||
|
||||
@@ -564,16 +565,23 @@ async fn emit_tool_call_begin(
|
||||
call_id: &str,
|
||||
invocation: McpInvocation,
|
||||
) {
|
||||
session
|
||||
.send_event(
|
||||
turn,
|
||||
EventMsg::McpToolCallBegin(McpToolCallBeginEvent {
|
||||
call_id: call_id.to_string(),
|
||||
invocation,
|
||||
mcp_app_resource_uri: None,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let McpInvocation {
|
||||
server,
|
||||
tool,
|
||||
arguments,
|
||||
} = invocation;
|
||||
let item = TurnItem::McpToolCall(McpToolCallItem {
|
||||
id: call_id.to_string(),
|
||||
server,
|
||||
tool,
|
||||
arguments: arguments.unwrap_or(Value::Null),
|
||||
mcp_app_resource_uri: None,
|
||||
status: McpToolCallStatus::InProgress,
|
||||
result: None,
|
||||
error: None,
|
||||
duration: None,
|
||||
});
|
||||
session.emit_turn_item_started(turn, &item).await;
|
||||
}
|
||||
|
||||
async fn emit_tool_call_end(
|
||||
@@ -584,18 +592,34 @@ async fn emit_tool_call_end(
|
||||
duration: Duration,
|
||||
result: Result<CallToolResult, String>,
|
||||
) {
|
||||
session
|
||||
.send_event(
|
||||
turn,
|
||||
EventMsg::McpToolCallEnd(McpToolCallEndEvent {
|
||||
call_id: call_id.to_string(),
|
||||
invocation,
|
||||
mcp_app_resource_uri: None,
|
||||
duration,
|
||||
result,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (status, result, error) = match result {
|
||||
Ok(result) if result.is_error.unwrap_or(false) => {
|
||||
(McpToolCallStatus::Failed, Some(result), None)
|
||||
}
|
||||
Ok(result) => (McpToolCallStatus::Completed, Some(result), None),
|
||||
Err(message) => (
|
||||
McpToolCallStatus::Failed,
|
||||
None,
|
||||
Some(McpToolCallError { message }),
|
||||
),
|
||||
};
|
||||
let McpInvocation {
|
||||
server,
|
||||
tool,
|
||||
arguments,
|
||||
} = invocation;
|
||||
let item = TurnItem::McpToolCall(McpToolCallItem {
|
||||
id: call_id.to_string(),
|
||||
server,
|
||||
tool,
|
||||
arguments: arguments.unwrap_or(Value::Null),
|
||||
mcp_app_resource_uri: None,
|
||||
status,
|
||||
result,
|
||||
error,
|
||||
duration: Some(duration),
|
||||
});
|
||||
session.emit_turn_item_completed(turn, item).await;
|
||||
}
|
||||
|
||||
fn normalize_optional_string(input: Option<String>) -> Option<String> {
|
||||
|
||||
Reference in New Issue
Block a user