From 6941f5c2c5a17a6f61bfbc9f4c96cb0c81401623 Mon Sep 17 00:00:00 2001 From: Miaolin Min Date: Sat, 16 May 2026 13:27:44 -0700 Subject: [PATCH] [codex] preserve MCP result meta in McpToolCallItemResult (#22946) ## Summary https://openai.slack.com/archives/C0ARA9UAQEA/p1778890981647319?thread_ts=1778888537.934319&cid=C0ARA9UAQEA - Add `_meta` to exec JSONL MCP tool call result events. - Copy MCP result metadata through the JSONL event conversion. - Add a focused test that verifies `_meta` is serialized as `_meta` and not `meta`. ## Verification https://www.notion.so/openai/Miaolin-0516-_meta-population-debug-3628e50b62b08074b365e0ce1ffb8f74 --- .../src/event_processor_with_jsonl_output.rs | 1 + ...event_processor_with_jsonl_output_tests.rs | 51 +++++++++++++++++++ codex-rs/exec/src/exec_events.rs | 3 ++ .../tests/event_processor_with_json_output.rs | 2 + sdk/typescript/src/items.ts | 1 + 5 files changed, 58 insertions(+) diff --git a/codex-rs/exec/src/event_processor_with_jsonl_output.rs b/codex-rs/exec/src/event_processor_with_jsonl_output.rs index 045baacc7b..89e41eacb1 100644 --- a/codex-rs/exec/src/event_processor_with_jsonl_output.rs +++ b/codex-rs/exec/src/event_processor_with_jsonl_output.rs @@ -223,6 +223,7 @@ impl EventProcessorWithJsonOutput { arguments, result: result.map(|result| McpToolCallItemResult { content: result.content, + meta: result.meta, structured_content: result.structured_content, }), error: error.map(|error| McpToolCallItemError { diff --git a/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs b/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs index 88fd042f7c..6bd358f6be 100644 --- a/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs +++ b/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs @@ -1,5 +1,6 @@ use super::*; use pretty_assertions::assert_eq; +use serde_json::json; use tempfile::tempdir; #[test] @@ -57,3 +58,53 @@ fn failed_turn_does_not_overwrite_output_last_message_file() { "keep existing contents" ); } + +#[test] +fn mcp_tool_call_result_preserves_meta_in_jsonl_event() { + let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); + + let collected = processor.collect_thread_events(ServerNotification::ItemCompleted( + codex_app_server_protocol::ItemCompletedNotification { + item: ThreadItem::McpToolCall { + id: "mcp-1".to_string(), + server: "search service".to_string(), + tool: "web_run".to_string(), + status: McpToolCallStatus::Completed, + arguments: json!({"search_query": [{"q": "OpenAI Codex CLI documentation"}]}), + mcp_app_resource_uri: None, + result: Some(Box::new(codex_app_server_protocol::McpToolCallResult { + content: vec![json!({"type": "text", "text": "search result"})], + structured_content: None, + meta: Some(json!({"raw_messages": [{"ref_id": "turn0search0"}]})), + })), + error: None, + duration_ms: Some(42), + }, + thread_id: "thread-1".to_string(), + turn_id: "turn-1".to_string(), + completed_at_ms: 0, + }, + )); + + assert_eq!(collected.status, CodexStatus::Running); + assert_eq!(collected.events.len(), 1); + + let ThreadEvent::ItemCompleted(ItemCompletedEvent { item }) = &collected.events[0] else { + panic!("expected item.completed event"); + }; + let ThreadItemDetails::McpToolCall(item) = &item.details else { + panic!("expected MCP tool call item"); + }; + let result = item.result.as_ref().expect("expected MCP tool result"); + assert_eq!( + result.meta, + Some(json!({"raw_messages": [{"ref_id": "turn0search0"}]})) + ); + + let serialized = serde_json::to_value(&collected.events[0]).expect("serialize event"); + assert_eq!( + serialized["item"]["result"]["_meta"], + json!({"raw_messages": [{"ref_id": "turn0search0"}]}) + ); + assert!(serialized["item"]["result"].get("meta").is_none()); +} diff --git a/codex-rs/exec/src/exec_events.rs b/codex-rs/exec/src/exec_events.rs index 4a84ef7494..078a876ef4 100644 --- a/codex-rs/exec/src/exec_events.rs +++ b/codex-rs/exec/src/exec_events.rs @@ -266,6 +266,9 @@ pub struct McpToolCallItemResult { // representations). Using `JsonValue` keeps the payload wire-shaped and // easy to export. pub content: Vec, + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub meta: Option, pub structured_content: Option, } diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index efda58f412..acbac8254c 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -540,6 +540,7 @@ fn mcp_tool_call_begin_and_end_emit_item_events() { arguments: json!({ "key": "value" }), result: Some(McpToolCallItemResult { content: Vec::new(), + meta: None, structured_content: None, }), error: None, @@ -681,6 +682,7 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() { "type": "text", "text": "done", })], + meta: None, structured_content: Some(json!({ "status": "ok" })), }), error: None, diff --git a/sdk/typescript/src/items.ts b/sdk/typescript/src/items.ts index 182878f55b..8fd3b2c7ff 100644 --- a/sdk/typescript/src/items.ts +++ b/sdk/typescript/src/items.ts @@ -60,6 +60,7 @@ export type McpToolCallItem = { /** Result payload returned by the MCP server for successful calls. */ result?: { content: McpContentBlock[]; + _meta?: unknown; structured_content: unknown; }; /** Error message reported for failed calls. */