[codex][mcp] Add resource uri meta to tool call item. (#17831)

- [x] Add resource uri meta to tool call item so that the app-server
client can start prefetching resources immediately without loading mcp
server status.
This commit is contained in:
Matthew Zeng
2026-04-15 22:09:17 -07:00
committed by GitHub
Unverified
parent 77fe33bf72
commit 224dad41ac
32 changed files with 269 additions and 20 deletions
@@ -2891,6 +2891,12 @@
"id": {
"type": "string"
},
"mcpAppResourceUri": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -13369,6 +13369,12 @@
"id": {
"type": "string"
},
"mcpAppResourceUri": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -11213,6 +11213,12 @@
"id": {
"type": "string"
},
"mcpAppResourceUri": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -787,6 +787,12 @@
"id": {
"type": "string"
},
"mcpAppResourceUri": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -787,6 +787,12 @@
"id": {
"type": "string"
},
"mcpAppResourceUri": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -930,6 +930,12 @@
"id": {
"type": "string"
},
"mcpAppResourceUri": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -1444,6 +1444,12 @@
"id": {
"type": "string"
},
"mcpAppResourceUri": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -1206,6 +1206,12 @@
"id": {
"type": "string"
},
"mcpAppResourceUri": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -1206,6 +1206,12 @@
"id": {
"type": "string"
},
"mcpAppResourceUri": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -1206,6 +1206,12 @@
"id": {
"type": "string"
},
"mcpAppResourceUri": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -1444,6 +1444,12 @@
"id": {
"type": "string"
},
"mcpAppResourceUri": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -1206,6 +1206,12 @@
"id": {
"type": "string"
},
"mcpAppResourceUri": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -1444,6 +1444,12 @@
"id": {
"type": "string"
},
"mcpAppResourceUri": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -1206,6 +1206,12 @@
"id": {
"type": "string"
},
"mcpAppResourceUri": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -1206,6 +1206,12 @@
"id": {
"type": "string"
},
"mcpAppResourceUri": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -930,6 +930,12 @@
"id": {
"type": "string"
},
"mcpAppResourceUri": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -930,6 +930,12 @@
"id": {
"type": "string"
},
"mcpAppResourceUri": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -930,6 +930,12 @@
"id": {
"type": "string"
},
"mcpAppResourceUri": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -53,7 +53,7 @@ exitCode: number | null,
/**
* The duration of the command execution in milliseconds.
*/
durationMs: number | null, } | { "type": "fileChange", id: string, changes: Array<FileUpdateChange>, status: PatchApplyStatus, } | { "type": "mcpToolCall", id: string, server: string, tool: string, status: McpToolCallStatus, arguments: JsonValue, result: McpToolCallResult | null, error: McpToolCallError | null,
durationMs: number | null, } | { "type": "fileChange", id: string, changes: Array<FileUpdateChange>, status: PatchApplyStatus, } | { "type": "mcpToolCall", id: string, server: string, tool: string, status: McpToolCallStatus, arguments: JsonValue, mcpAppResourceUri?: string, result: McpToolCallResult | null, error: McpToolCallError | null,
/**
* The duration of the MCP tool call in milliseconds.
*/
@@ -764,6 +764,7 @@ macro_rules! server_notification_definitions {
Display,
ExperimentalApi,
)]
#[allow(clippy::large_enum_variant)]
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
#[strum(serialize_all = "camelCase")]
pub enum ServerNotification {
@@ -502,6 +502,7 @@ impl ThreadHistoryBuilder {
.arguments
.clone()
.unwrap_or(serde_json::Value::Null),
mcp_app_resource_uri: payload.mcp_app_resource_uri.clone(),
result: None,
error: None,
duration_ms: None,
@@ -518,11 +519,11 @@ impl ThreadHistoryBuilder {
let duration_ms = i64::try_from(payload.duration.as_millis()).ok();
let (result, error) = match &payload.result {
Ok(value) => (
Some(McpToolCallResult {
Some(Box::new(McpToolCallResult {
content: value.content.clone(),
structured_content: value.structured_content.clone(),
meta: value.meta.clone(),
}),
})),
None,
),
Err(message) => (
@@ -542,6 +543,7 @@ impl ThreadHistoryBuilder {
.arguments
.clone()
.unwrap_or(serde_json::Value::Null),
mcp_app_resource_uri: payload.mcp_app_resource_uri.clone(),
result,
error,
duration_ms,
@@ -1809,6 +1811,7 @@ mod tests {
tool: "lookup".into(),
arguments: Some(serde_json::json!({"id":"123"})),
},
mcp_app_resource_uri: None,
duration: Duration::from_millis(8),
result: Err("boom".into()),
}),
@@ -1857,6 +1860,7 @@ mod tests {
tool: "lookup".into(),
status: McpToolCallStatus::Failed,
arguments: serde_json::json!({"id":"123"}),
mcp_app_resource_uri: None,
result: None,
error: Some(McpToolCallError {
message: "boom".into(),
@@ -1882,6 +1886,7 @@ mod tests {
tool: "lookup".into(),
arguments: Some(serde_json::json!({"id":"123"})),
},
mcp_app_resource_uri: Some("ui://widget/lookup.html".into()),
duration: Duration::from_millis(8),
result: Ok(CallToolResult {
content: vec![serde_json::json!({
@@ -1911,7 +1916,8 @@ mod tests {
tool: "lookup".into(),
status: McpToolCallStatus::Completed,
arguments: serde_json::json!({"id":"123"}),
result: Some(McpToolCallResult {
mcp_app_resource_uri: Some("ui://widget/lookup.html".into()),
result: Some(Box::new(McpToolCallResult {
content: vec![serde_json::json!({
"type": "text",
"text": "result"
@@ -1920,7 +1926,7 @@ mod tests {
meta: Some(serde_json::json!({
"ui/resourceUri": "ui://widget/lookup.html"
})),
}),
})),
error: None,
duration_ms: Some(8),
}
@@ -4585,7 +4585,10 @@ pub enum ThreadItem {
tool: String,
status: McpToolCallStatus,
arguments: JsonValue,
result: Option<McpToolCallResult>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
mcp_app_resource_uri: Option<String>,
result: Option<Box<McpToolCallResult>>,
error: Option<McpToolCallError>,
/// The duration of the MCP tool call in milliseconds.
#[ts(type = "number | null")]
@@ -2910,6 +2910,7 @@ async fn construct_mcp_tool_call_notification(
tool: begin_event.invocation.tool,
status: McpToolCallStatus::InProgress,
arguments: begin_event.invocation.arguments.unwrap_or(JsonValue::Null),
mcp_app_resource_uri: begin_event.mcp_app_resource_uri,
result: None,
error: None,
duration_ms: None,
@@ -2936,11 +2937,11 @@ async fn construct_mcp_tool_call_end_notification(
let (result, error) = match &end_event.result {
Ok(value) => (
Some(McpToolCallResult {
Some(Box::new(McpToolCallResult {
content: value.content.clone(),
structured_content: value.structured_content.clone(),
meta: value.meta.clone(),
}),
})),
None,
),
Err(message) => (
@@ -2957,6 +2958,7 @@ async fn construct_mcp_tool_call_end_notification(
tool: end_event.invocation.tool,
status,
arguments: end_event.invocation.arguments.unwrap_or(JsonValue::Null),
mcp_app_resource_uri: end_event.mcp_app_resource_uri,
result,
error,
duration_ms,
@@ -4219,6 +4221,7 @@ mod tests {
tool: "list_mcp_resources".to_string(),
arguments: Some(serde_json::json!({"server": ""})),
},
mcp_app_resource_uri: Some("ui://widget/list-resources.html".to_string()),
};
let thread_id = ThreadId::new().to_string();
@@ -4239,6 +4242,7 @@ mod tests {
tool: begin_event.invocation.tool,
status: McpToolCallStatus::InProgress,
arguments: serde_json::json!({"server": ""}),
mcp_app_resource_uri: Some("ui://widget/list-resources.html".to_string()),
result: None,
error: None,
duration_ms: None,
@@ -4379,6 +4383,7 @@ mod tests {
tool: "list_mcp_resources".to_string(),
arguments: None,
},
mcp_app_resource_uri: None,
};
let thread_id = ThreadId::new().to_string();
@@ -4399,6 +4404,7 @@ mod tests {
tool: begin_event.invocation.tool,
status: McpToolCallStatus::InProgress,
arguments: JsonValue::Null,
mcp_app_resource_uri: None,
result: None,
error: None,
duration_ms: None,
@@ -4430,6 +4436,7 @@ mod tests {
tool: "list_mcp_resources".to_string(),
arguments: Some(serde_json::json!({"server": ""})),
},
mcp_app_resource_uri: Some("ui://widget/list-resources.html".to_string()),
duration: Duration::from_nanos(92708),
result: Ok(result),
};
@@ -4452,13 +4459,14 @@ mod tests {
tool: end_event.invocation.tool,
status: McpToolCallStatus::Completed,
arguments: serde_json::json!({"server": ""}),
result: Some(McpToolCallResult {
mcp_app_resource_uri: Some("ui://widget/list-resources.html".to_string()),
result: Some(Box::new(McpToolCallResult {
content,
structured_content: None,
meta: Some(serde_json::json!({
"ui/resourceUri": "ui://widget/list-resources.html"
})),
}),
})),
error: None,
duration_ms: Some(0),
},
@@ -4476,6 +4484,7 @@ mod tests {
tool: "list_mcp_resources".to_string(),
arguments: None,
},
mcp_app_resource_uri: None,
duration: Duration::from_millis(1),
result: Err("boom".to_string()),
};
@@ -4498,6 +4507,7 @@ mod tests {
tool: end_event.invocation.tool,
status: McpToolCallStatus::Failed,
arguments: JsonValue::Null,
mcp_app_resource_uri: None,
result: None,
error: Some(McpToolCallError {
message: "boom".to_string(),
+37
View File
@@ -101,6 +101,9 @@ pub(crate) async fn handle_mcp_tool_call(
let metadata =
lookup_mcp_tool_metadata(sess.as_ref(), turn_context.as_ref(), &server, &tool_name).await;
let mcp_app_resource_uri = metadata
.as_ref()
.and_then(|metadata| metadata.mcp_app_resource_uri.clone());
let app_tool_policy = if server == CODEX_APPS_MCP_SERVER_NAME {
connectors::app_tool_policy(
&turn_context.config,
@@ -130,6 +133,7 @@ pub(crate) async fn handle_mcp_tool_call(
turn_context.as_ref(),
&call_id,
invocation,
mcp_app_resource_uri.clone(),
"MCP tool call blocked by app configuration".to_string(),
/*already_started*/ false,
)
@@ -161,6 +165,7 @@ pub(crate) async fn handle_mcp_tool_call(
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;
@@ -213,6 +218,7 @@ pub(crate) async fn handle_mcp_tool_call(
let tool_call_end_event = EventMsg::McpToolCallEnd(McpToolCallEndEvent {
call_id: call_id.clone(),
invocation,
mcp_app_resource_uri: mcp_app_resource_uri.clone(),
duration,
result: result.clone(),
});
@@ -239,6 +245,7 @@ pub(crate) async fn handle_mcp_tool_call(
turn_context.as_ref(),
&call_id,
invocation,
mcp_app_resource_uri.clone(),
message,
/*already_started*/ true,
)
@@ -254,6 +261,7 @@ pub(crate) async fn handle_mcp_tool_call(
turn_context.as_ref(),
&call_id,
invocation,
mcp_app_resource_uri.clone(),
message,
/*already_started*/ true,
)
@@ -268,6 +276,7 @@ pub(crate) async fn handle_mcp_tool_call(
turn_context.as_ref(),
&call_id,
invocation,
mcp_app_resource_uri.clone(),
message,
/*already_started*/ true,
)
@@ -325,6 +334,7 @@ pub(crate) async fn handle_mcp_tool_call(
let tool_call_end_event = EventMsg::McpToolCallEnd(McpToolCallEndEvent {
call_id: call_id.clone(),
invocation,
mcp_app_resource_uri,
duration,
result: result.clone(),
});
@@ -642,11 +652,14 @@ pub(crate) struct McpToolApprovalMetadata {
connector_description: Option<String>,
tool_title: Option<String>,
tool_description: Option<String>,
mcp_app_resource_uri: Option<String>,
codex_apps_meta: Option<serde_json::Map<String, serde_json::Value>>,
openai_file_input_params: Option<Vec<String>>,
}
const MCP_TOOL_CODEX_APPS_META_KEY: &str = "_codex_apps";
const MCP_TOOL_OPENAI_OUTPUT_TEMPLATE_META_KEY: &str = "openai/outputTemplate";
const MCP_TOOL_UI_RESOURCE_URI_META_KEY: &str = "ui/resourceUri";
fn custom_mcp_tool_approval_mode(
turn_context: &TurnContext,
@@ -1100,6 +1113,7 @@ pub(crate) async fn lookup_mcp_tool_metadata(
connector_description,
tool_title: tool_info.tool.title,
tool_description: tool_info.tool.description.map(std::borrow::Cow::into_owned),
mcp_app_resource_uri: get_mcp_app_resource_uri(tool_info.tool.meta.as_deref()),
codex_apps_meta: tool_info
.tool
.meta
@@ -1114,6 +1128,26 @@ pub(crate) async fn lookup_mcp_tool_metadata(
})
}
fn get_mcp_app_resource_uri(
meta: Option<&serde_json::Map<String, serde_json::Value>>,
) -> Option<String> {
meta.and_then(|meta| {
meta.get("ui")
.and_then(serde_json::Value::as_object)
.and_then(|ui| ui.get("resourceUri"))
.and_then(serde_json::Value::as_str)
.or_else(|| {
meta.get(MCP_TOOL_UI_RESOURCE_URI_META_KEY)
.and_then(serde_json::Value::as_str)
})
.or_else(|| {
meta.get(MCP_TOOL_OPENAI_OUTPUT_TEMPLATE_META_KEY)
.and_then(serde_json::Value::as_str)
})
.map(str::to_string)
})
}
async fn lookup_mcp_app_usage_metadata(
sess: &Session,
server: &str,
@@ -1666,6 +1700,7 @@ async fn notify_mcp_tool_call_skip(
turn_context: &TurnContext,
call_id: &str,
invocation: McpInvocation,
mcp_app_resource_uri: Option<String>,
message: String,
already_started: bool,
) -> Result<CallToolResult, String> {
@@ -1673,6 +1708,7 @@ async fn notify_mcp_tool_call_skip(
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;
}
@@ -1680,6 +1716,7 @@ async fn notify_mcp_tool_call_skip(
let tool_call_end_event = EventMsg::McpToolCallEnd(McpToolCallEndEvent {
call_id: call_id.to_string(),
invocation,
mcp_app_resource_uri,
duration: Duration::ZERO,
result: Err(message.clone()),
});
+41
View File
@@ -59,6 +59,7 @@ fn approval_metadata(
connector_description: connector_description.map(str::to_string),
tool_title: tool_title.map(str::to_string),
tool_description: tool_description.map(str::to_string),
mcp_app_resource_uri: None,
codex_apps_meta: None,
openai_file_input_params: None,
}
@@ -74,6 +75,35 @@ fn prompt_options(
}
}
#[test]
fn mcp_app_resource_uri_reads_known_tool_meta_keys() {
let nested = serde_json::json!({
"ui": {
"resourceUri": "ui://widget/nested.html",
},
});
assert_eq!(
get_mcp_app_resource_uri(nested.as_object()),
Some("ui://widget/nested.html".to_string())
);
let flat = serde_json::json!({
"ui/resourceUri": "ui://widget/flat.html",
});
assert_eq!(
get_mcp_app_resource_uri(flat.as_object()),
Some("ui://widget/flat.html".to_string())
);
let output_template = serde_json::json!({
"openai/outputTemplate": "ui://widget/output-template.html",
});
assert_eq!(
get_mcp_app_resource_uri(output_template.as_object()),
Some("ui://widget/output-template.html".to_string())
);
}
#[test]
fn approval_required_when_read_only_false_and_destructive() {
let annotations = annotations(Some(false), Some(true), /*open_world*/ None);
@@ -589,6 +619,7 @@ async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps
connector_description: Some("Manage events".to_string()),
tool_title: Some("Create Event".to_string()),
tool_description: Some("Create a calendar event.".to_string()),
mcp_app_resource_uri: None,
codex_apps_meta: Some(
serde_json::json!({
"resource_uri": "connector://calendar/tools/calendar_create_event",
@@ -746,6 +777,7 @@ fn guardian_mcp_review_request_includes_annotations_when_present() {
connector_description: None,
tool_title: None,
tool_description: None,
mcp_app_resource_uri: None,
codex_apps_meta: None,
openai_file_input_params: None,
};
@@ -1272,6 +1304,7 @@ async fn approve_mode_skips_when_annotations_do_not_require_approval() {
connector_description: None,
tool_title: Some("Read Only Tool".to_string()),
tool_description: None,
mcp_app_resource_uri: None,
codex_apps_meta: None,
openai_file_input_params: None,
};
@@ -1340,6 +1373,7 @@ async fn guardian_mode_skips_auto_when_annotations_do_not_require_approval() {
connector_description: None,
tool_title: Some("Read Only Tool".to_string()),
tool_description: None,
mcp_app_resource_uri: None,
codex_apps_meta: None,
openai_file_input_params: None,
};
@@ -1411,6 +1445,7 @@ async fn guardian_mode_mcp_denial_returns_rationale_message() {
connector_description: None,
tool_title: Some("Dangerous Tool".to_string()),
tool_description: Some("Reads calendar data.".to_string()),
mcp_app_resource_uri: None,
codex_apps_meta: None,
openai_file_input_params: None,
};
@@ -1462,6 +1497,7 @@ async fn prompt_mode_waits_for_approval_when_annotations_do_not_require_approval
connector_description: None,
tool_title: Some("Read Only Tool".to_string()),
tool_description: None,
mcp_app_resource_uri: None,
codex_apps_meta: None,
openai_file_input_params: None,
};
@@ -1539,6 +1575,7 @@ async fn approve_mode_blocks_when_arc_returns_interrupt_for_model() {
connector_description: Some("Manage events".to_string()),
tool_title: Some("Dangerous Tool".to_string()),
tool_description: Some("Performs a risky action.".to_string()),
mcp_app_resource_uri: None,
codex_apps_meta: None,
openai_file_input_params: None,
};
@@ -1609,6 +1646,7 @@ async fn custom_approve_mode_blocks_when_arc_returns_interrupt_for_model() {
connector_description: None,
tool_title: Some("Dangerous Tool".to_string()),
tool_description: Some("Performs a risky action.".to_string()),
mcp_app_resource_uri: None,
codex_apps_meta: None,
openai_file_input_params: None,
};
@@ -1679,6 +1717,7 @@ async fn approve_mode_blocks_when_arc_returns_interrupt_without_annotations() {
connector_description: Some("Manage events".to_string()),
tool_title: Some("Dangerous Tool".to_string()),
tool_description: Some("Performs a risky action.".to_string()),
mcp_app_resource_uri: None,
codex_apps_meta: None,
openai_file_input_params: None,
};
@@ -1757,6 +1796,7 @@ async fn full_access_mode_skips_arc_monitor_for_all_approval_modes() {
connector_description: Some("Manage events".to_string()),
tool_title: Some("Dangerous Tool".to_string()),
tool_description: Some("Performs a risky action.".to_string()),
mcp_app_resource_uri: None,
codex_apps_meta: None,
openai_file_input_params: None,
};
@@ -1859,6 +1899,7 @@ async fn approve_mode_routes_arc_ask_user_to_guardian_when_guardian_reviewer_is_
connector_description: Some("Manage events".to_string()),
tool_title: Some("Dangerous Tool".to_string()),
tool_description: Some("Performs a risky action.".to_string()),
mcp_app_resource_uri: None,
codex_apps_meta: None,
openai_file_input_params: None,
};
@@ -562,6 +562,7 @@ async fn emit_tool_call_begin(
EventMsg::McpToolCallBegin(McpToolCallBeginEvent {
call_id: call_id.to_string(),
invocation,
mcp_app_resource_uri: None,
}),
)
.await;
@@ -581,6 +582,7 @@ async fn emit_tool_call_end(
EventMsg::McpToolCallEnd(McpToolCallEndEvent {
call_id: call_id.to_string(),
invocation,
mcp_app_resource_uri: None,
duration,
result,
}),
@@ -21,6 +21,8 @@ const SERVER_VERSION: &str = "1.0.0";
const SEARCHABLE_TOOL_COUNT: usize = 100;
pub const CALENDAR_CREATE_EVENT_RESOURCE_URI: &str =
"connector://calendar/tools/calendar_create_event";
pub const CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI: &str =
"ui://widget/calendar-create-event.html";
const CALENDAR_LIST_EVENTS_RESOURCE_URI: &str = "connector://calendar/tools/calendar_list_events";
pub const DOCUMENT_EXTRACT_TEXT_RESOURCE_URI: &str =
"connector://calendar/tools/calendar_extract_text";
@@ -206,6 +208,7 @@ impl Respond for CodexAppsJsonRpcResponder {
"connector_id": CONNECTOR_ID,
"connector_name": self.connector_name.clone(),
"connector_description": self.connector_description.clone(),
"openai/outputTemplate": CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI,
"_codex_apps": {
"resource_uri": CALENDAR_CREATE_EVENT_RESOURCE_URI,
"contains_mcp_source": true,
+1
View File
@@ -736,6 +736,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> {
tool: "image".to_string(),
arguments: Some(json!({})),
},
mcp_app_resource_uri: None,
},
);
+18
View File
@@ -15,6 +15,7 @@ use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::user_input::UserInput;
use core_test_support::apps_test_server::AppsTestServer;
use core_test_support::apps_test_server::CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI;
use core_test_support::apps_test_server::CALENDAR_CREATE_EVENT_RESOURCE_URI;
use core_test_support::responses::ResponsesRequest;
use core_test_support::responses::ev_assistant_message;
@@ -457,6 +458,19 @@ async fn tool_search_returns_deferred_tools_without_follow_up_tool_injection() -
})
.await?;
let EventMsg::McpToolCallBegin(begin) = wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::McpToolCallBegin(_))
})
.await
else {
unreachable!("event guard guarantees McpToolCallBegin");
};
assert_eq!(begin.call_id, "calendar-call-1");
assert_eq!(
begin.mcp_app_resource_uri.as_deref(),
Some(CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI)
);
let EventMsg::McpToolCallEnd(end) = wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::McpToolCallEnd(_))
})
@@ -465,6 +479,10 @@ async fn tool_search_returns_deferred_tools_without_follow_up_tool_injection() -
unreachable!("event guard guarantees McpToolCallEnd");
};
assert_eq!(end.call_id, "calendar-call-1");
assert_eq!(
end.mcp_app_resource_uri.as_deref(),
Some(CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI)
);
assert_eq!(
end.invocation,
McpInvocation {
@@ -464,6 +464,7 @@ fn mcp_tool_call_begin_and_end_emit_item_events() {
tool: "tool_x".to_string(),
status: ApiMcpToolCallStatus::InProgress,
arguments: json!({ "key": "value" }),
mcp_app_resource_uri: None,
result: None,
error: None,
duration_ms: None,
@@ -479,11 +480,12 @@ fn mcp_tool_call_begin_and_end_emit_item_events() {
tool: "tool_x".to_string(),
status: ApiMcpToolCallStatus::Completed,
arguments: json!({ "key": "value" }),
result: Some(McpToolCallResult {
mcp_app_resource_uri: None,
result: Some(Box::new(McpToolCallResult {
content: Vec::new(),
structured_content: None,
meta: None,
}),
})),
error: None,
duration_ms: Some(1_000),
},
@@ -547,6 +549,7 @@ fn mcp_tool_call_failure_sets_failed_status() {
tool: "tool_y".to_string(),
status: ApiMcpToolCallStatus::Failed,
arguments: json!({ "param": 42 }),
mcp_app_resource_uri: None,
result: None,
error: Some(McpToolCallError {
message: "tool exploded".to_string(),
@@ -593,6 +596,7 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() {
tool: "tool_z".to_string(),
status: ApiMcpToolCallStatus::InProgress,
arguments: serde_json::Value::Null,
mcp_app_resource_uri: None,
result: None,
error: None,
duration_ms: None,
@@ -608,14 +612,15 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() {
tool: "tool_z".to_string(),
status: ApiMcpToolCallStatus::Completed,
arguments: serde_json::Value::Null,
result: Some(McpToolCallResult {
mcp_app_resource_uri: None,
result: Some(Box::new(McpToolCallResult {
content: vec![json!({
"type": "text",
"text": "done",
})],
structured_content: Some(json!({ "status": "ok" })),
meta: None,
}),
})),
error: None,
duration_ms: Some(10),
},
+6
View File
@@ -2362,6 +2362,9 @@ pub struct McpToolCallBeginEvent {
/// Identifier so this can be paired with the McpToolCallEnd event.
pub call_id: String,
pub invocation: McpInvocation,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub mcp_app_resource_uri: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
@@ -2369,6 +2372,9 @@ pub struct McpToolCallEndEvent {
/// Identifier for the corresponding McpToolCallBegin that finished.
pub call_id: String,
pub invocation: McpInvocation,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub mcp_app_resource_uri: Option<String>,
#[ts(type = "string")]
pub duration: Duration,
/// Result of the tool call. Note this could be an error.
+14 -6
View File
@@ -4759,6 +4759,7 @@ impl ChatWidget {
invocation,
duration,
result,
..
} = ev;
let extra_cell = match self
@@ -5943,6 +5944,7 @@ impl ChatWidget {
server,
tool,
arguments,
mcp_app_resource_uri,
result,
error,
duration_ms,
@@ -5955,15 +5957,19 @@ impl ChatWidget {
tool,
arguments: Some(arguments),
},
mcp_app_resource_uri,
duration: Duration::from_millis(duration_ms.unwrap_or_default().max(0) as u64),
result: match (result, error) {
(_, Some(error)) => Err(error.message),
(Some(result), None) => Ok(codex_protocol::mcp::CallToolResult {
content: result.content,
structured_content: result.structured_content,
is_error: Some(false),
meta: None,
}),
(Some(result), None) => {
let result = *result;
Ok(codex_protocol::mcp::CallToolResult {
content: result.content,
structured_content: result.structured_content,
is_error: Some(false),
meta: None,
})
}
(None, None) => Err("MCP tool call completed without a result".to_string()),
},
});
@@ -6453,6 +6459,7 @@ impl ChatWidget {
server,
tool,
arguments,
mcp_app_resource_uri,
..
} => {
self.on_mcp_tool_call_begin(McpToolCallBeginEvent {
@@ -6462,6 +6469,7 @@ impl ChatWidget {
tool,
arguments: Some(arguments),
},
mcp_app_resource_uri,
});
}
ThreadItem::WebSearch { id, .. } => {