[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:
pakrym-oai
2026-05-03 22:50:13 -07:00
committed by GitHub
parent 9ddfda9db7
commit c8c30d9d75
8 changed files with 431 additions and 299 deletions
@@ -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> {