[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-16 05:09:17 +00:00
committed by GitHub
parent 77fe33bf72
commit 224dad41ac
32 changed files with 269 additions and 20 deletions
+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,
}),