mirror of
https://github.com/openai/codex.git
synced 2026-09-28 08:43:01 +08:00
Preserve MCP app resource URIs without defaulting display mode (#48764)
## Why Defaulting missing or unsupported display preferences to `inline` prevents clients from applying resource display defaults. ## What changed Populate `mcpAppUi` only when a tool descriptor has a resource URI and an explicit `inline` or `fullscreen` preference. Preserve `mcpAppResourceUri` independently in tool-call events, including when `mcpAppUi` is unset, and document the behavior. ## Testing Update integration coverage for explicit display modes, missing and unsupported preferences, and legacy resource URIs, checking tool-call events and resumed history. GitOrigin-RevId: 4876cced068d8e464adeb265abd053d8abe667ce
This commit is contained in:
@@ -10,8 +10,9 @@ and caching behavior remain in effect while the provider satisfies current requi
|
||||
# MCP App UI
|
||||
|
||||
`mcpToolCall.mcpAppUi` records the invoked descriptor's `resourceUri`
|
||||
and `preferredModelDisplayMode` (`inline` or `fullscreen`). Descriptors with a widget
|
||||
URI default to `inline` when the preference is missing or unsupported. The
|
||||
and explicit `preferredModelDisplayMode` (`inline` or `fullscreen`). Missing or
|
||||
unsupported preferences leave `mcpAppUi` unset; `mcpAppResourceUri` retains the URI
|
||||
so clients can apply resource display defaults. The
|
||||
UI information is preserved in tool-call events and saved history so clients can
|
||||
render without waiting for the full MCP catalog.
|
||||
|
||||
|
||||
@@ -400,6 +400,7 @@ pub(crate) struct HandledMcpToolCall {
|
||||
struct McpToolCallItemMetadata {
|
||||
connector_id: Option<String>,
|
||||
link_id: Option<String>,
|
||||
mcp_app_resource_uri: Option<String>,
|
||||
mcp_app_ui: Option<McpAppUi>,
|
||||
app_name: Option<String>,
|
||||
action_name: Option<String>,
|
||||
@@ -418,6 +419,8 @@ impl McpToolCallItemMetadata {
|
||||
connector_id: trusted_mcp_app_metadata
|
||||
.and_then(|metadata| metadata.connector_id.clone()),
|
||||
link_id: trusted_mcp_app_metadata.and_then(|metadata| metadata.link_id.clone()),
|
||||
mcp_app_resource_uri: metadata
|
||||
.and_then(|metadata| metadata.mcp_app_resource_uri.clone()),
|
||||
mcp_app_ui: metadata.and_then(|metadata| metadata.mcp_app_ui.clone()),
|
||||
app_name: trusted_mcp_app_metadata.and_then(|metadata| metadata.connector_name.clone()),
|
||||
action_name: trusted_mcp_app_metadata
|
||||
@@ -1038,10 +1041,7 @@ async fn notify_mcp_tool_call_started(
|
||||
tool,
|
||||
arguments: arguments.unwrap_or(JsonValue::Null),
|
||||
connector_id: item_metadata.connector_id,
|
||||
mcp_app_resource_uri: item_metadata
|
||||
.mcp_app_ui
|
||||
.as_ref()
|
||||
.map(|ui| ui.resource_uri.clone()),
|
||||
mcp_app_resource_uri: item_metadata.mcp_app_resource_uri,
|
||||
mcp_app_ui: item_metadata.mcp_app_ui,
|
||||
link_id: item_metadata.link_id,
|
||||
app_name: item_metadata.app_name,
|
||||
@@ -1087,10 +1087,7 @@ async fn notify_mcp_tool_call_completed(
|
||||
tool,
|
||||
arguments: arguments.unwrap_or(JsonValue::Null),
|
||||
connector_id: item_metadata.connector_id,
|
||||
mcp_app_resource_uri: item_metadata
|
||||
.mcp_app_ui
|
||||
.as_ref()
|
||||
.map(|ui| ui.resource_uri.clone()),
|
||||
mcp_app_resource_uri: item_metadata.mcp_app_resource_uri,
|
||||
mcp_app_ui: item_metadata.mcp_app_ui,
|
||||
link_id: item_metadata.link_id,
|
||||
app_name: item_metadata.app_name,
|
||||
@@ -1214,6 +1211,7 @@ pub(crate) struct McpToolApprovalMetadata {
|
||||
plugin_id: Option<String>,
|
||||
tool_title: Option<String>,
|
||||
tool_description: Option<String>,
|
||||
mcp_app_resource_uri: Option<String>,
|
||||
mcp_app_ui: Option<McpAppUi>,
|
||||
codex_apps_meta: Option<serde_json::Map<String, serde_json::Value>>,
|
||||
openai_file_input_optional_fields: Option<HashMap<String, Vec<String>>>,
|
||||
@@ -1677,6 +1675,7 @@ pub(crate) async fn request_mcp_tool_user_approval(
|
||||
plugin_id: None,
|
||||
tool_title: tool_title.clone(),
|
||||
tool_description: tool_description.clone(),
|
||||
mcp_app_resource_uri: None,
|
||||
mcp_app_ui: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_optional_fields: None,
|
||||
@@ -1834,7 +1833,8 @@ fn mcp_tool_metadata(
|
||||
None
|
||||
};
|
||||
|
||||
let mcp_app_ui = get_mcp_app_resource_uri(tool_info.tool.meta.as_deref()).map(|resource_uri| {
|
||||
let mcp_app_resource_uri = get_mcp_app_resource_uri(tool_info.tool.meta.as_deref());
|
||||
let mcp_app_ui = mcp_app_resource_uri.as_ref().and_then(|resource_uri| {
|
||||
let preferred_model_display_mode = match tool_info
|
||||
.tool
|
||||
.meta
|
||||
@@ -1843,13 +1843,14 @@ fn mcp_tool_metadata(
|
||||
.and_then(|ui| ui.get("preferredModelDisplayMode"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
{
|
||||
Some("inline") => McpAppDisplayMode::Inline,
|
||||
Some("fullscreen") => McpAppDisplayMode::Fullscreen,
|
||||
_ => McpAppDisplayMode::Inline,
|
||||
_ => return None,
|
||||
};
|
||||
McpAppUi {
|
||||
resource_uri,
|
||||
Some(McpAppUi {
|
||||
resource_uri: resource_uri.clone(),
|
||||
preferred_model_display_mode,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
Ok(McpToolApprovalMetadata {
|
||||
@@ -1862,6 +1863,7 @@ fn mcp_tool_metadata(
|
||||
plugin_id: plugin_id.map(str::to_string),
|
||||
tool_title: tool_info.tool.title,
|
||||
tool_description: tool_info.tool.description.map(std::borrow::Cow::into_owned),
|
||||
mcp_app_resource_uri,
|
||||
mcp_app_ui,
|
||||
codex_apps_meta,
|
||||
// Disallow custom MCPs from uploading files via fileParams.
|
||||
|
||||
@@ -90,6 +90,7 @@ fn approval_metadata(
|
||||
plugin_id: None,
|
||||
tool_title: tool_title.map(str::to_string),
|
||||
tool_description: tool_description.map(str::to_string),
|
||||
mcp_app_resource_uri: None,
|
||||
mcp_app_ui: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_optional_fields: None,
|
||||
@@ -1433,6 +1434,7 @@ fn mcp_tool_call_item_metadata_only_trusts_codex_apps_identity() {
|
||||
McpToolCallItemMetadata {
|
||||
connector_id: Some("asdk_app_0123456789abcdef0123456789abcdef".to_string()),
|
||||
link_id: Some("link_fedcba9876543210fedcba9876543210".to_string()),
|
||||
mcp_app_resource_uri: None,
|
||||
mcp_app_ui: None,
|
||||
app_name: Some("Calendar".to_string()),
|
||||
action_name: Some("create_event".to_string()),
|
||||
@@ -1445,6 +1447,7 @@ fn mcp_tool_call_item_metadata_only_trusts_codex_apps_identity() {
|
||||
McpToolCallItemMetadata {
|
||||
connector_id: None,
|
||||
link_id: None,
|
||||
mcp_app_resource_uri: None,
|
||||
mcp_app_ui: None,
|
||||
app_name: None,
|
||||
action_name: None,
|
||||
@@ -1470,6 +1473,7 @@ async fn mcp_tool_call_item_includes_app_identity() {
|
||||
McpToolCallItemMetadata {
|
||||
connector_id: Some("asdk_app_0123456789abcdef0123456789abcdef".to_string()),
|
||||
link_id: Some("link_fedcba9876543210fedcba9876543210".to_string()),
|
||||
mcp_app_resource_uri: None,
|
||||
mcp_app_ui: None,
|
||||
app_name: Some("Calendar".to_string()),
|
||||
action_name: Some("create_event".to_string()),
|
||||
@@ -1520,6 +1524,7 @@ async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps
|
||||
plugin_id: None,
|
||||
tool_title: Some("Create Event".to_string()),
|
||||
tool_description: Some("Create a calendar event.".to_string()),
|
||||
mcp_app_resource_uri: None,
|
||||
mcp_app_ui: None,
|
||||
codex_apps_meta: Some(
|
||||
serde_json::json!({
|
||||
@@ -1979,6 +1984,7 @@ fn guardian_mcp_review_request_includes_annotations_when_present() {
|
||||
plugin_id: None,
|
||||
tool_title: None,
|
||||
tool_description: None,
|
||||
mcp_app_resource_uri: None,
|
||||
mcp_app_ui: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_optional_fields: None,
|
||||
@@ -2744,6 +2750,7 @@ async fn approve_mode_skips_when_annotations_do_not_require_approval() {
|
||||
plugin_id: None,
|
||||
tool_title: Some("Read Only Tool".to_string()),
|
||||
tool_description: None,
|
||||
mcp_app_resource_uri: None,
|
||||
mcp_app_ui: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_optional_fields: None,
|
||||
@@ -2825,6 +2832,7 @@ async fn guardian_mode_skips_auto_when_annotations_do_not_require_approval() {
|
||||
plugin_id: None,
|
||||
tool_title: Some("Read Only Tool".to_string()),
|
||||
tool_description: None,
|
||||
mcp_app_resource_uri: None,
|
||||
mcp_app_ui: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_optional_fields: None,
|
||||
@@ -2888,6 +2896,7 @@ async fn permission_request_hook_allows_mcp_tool_call() {
|
||||
plugin_id: None,
|
||||
tool_title: Some("Create entities".to_string()),
|
||||
tool_description: None,
|
||||
mcp_app_resource_uri: None,
|
||||
mcp_app_ui: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_optional_fields: None,
|
||||
@@ -3039,6 +3048,7 @@ async fn permission_request_hook_runs_after_remembered_mcp_approval() {
|
||||
plugin_id: None,
|
||||
tool_title: Some("Create entities".to_string()),
|
||||
tool_description: None,
|
||||
mcp_app_resource_uri: None,
|
||||
mcp_app_ui: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_optional_fields: None,
|
||||
@@ -3140,6 +3150,7 @@ async fn strict_auto_review_forces_guardian_for_mcp_policy_skip() {
|
||||
plugin_id: None,
|
||||
tool_title: Some("Dangerous Tool".to_string()),
|
||||
tool_description: Some("Reads calendar data.".to_string()),
|
||||
mcp_app_resource_uri: None,
|
||||
mcp_app_ui: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_optional_fields: None,
|
||||
@@ -3219,6 +3230,7 @@ async fn assert_mcp_user_approval_persistence(
|
||||
plugin_id: None,
|
||||
tool_title: Some("Create entities".to_string()),
|
||||
tool_description: None,
|
||||
mcp_app_resource_uri: None,
|
||||
mcp_app_ui: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_optional_fields: None,
|
||||
@@ -3305,6 +3317,7 @@ async fn prompt_mode_waits_for_approval_when_annotations_do_not_require_approval
|
||||
plugin_id: None,
|
||||
tool_title: Some("Read Only Tool".to_string()),
|
||||
tool_description: None,
|
||||
mcp_app_resource_uri: None,
|
||||
mcp_app_ui: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_optional_fields: None,
|
||||
@@ -3370,6 +3383,7 @@ async fn full_access_mode_skips_mcp_tool_approval_for_all_approval_modes() {
|
||||
plugin_id: None,
|
||||
tool_title: Some("Dangerous Tool".to_string()),
|
||||
tool_description: Some("Performs a risky action.".to_string()),
|
||||
mcp_app_resource_uri: None,
|
||||
mcp_app_ui: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_optional_fields: None,
|
||||
@@ -3429,6 +3443,7 @@ async fn approve_mode_skips_guardian_in_every_permission_mode() {
|
||||
plugin_id: None,
|
||||
tool_title: Some("Dangerous Tool".to_string()),
|
||||
tool_description: Some("Performs a risky action.".to_string()),
|
||||
mcp_app_resource_uri: None,
|
||||
mcp_app_ui: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_optional_fields: None,
|
||||
|
||||
@@ -76,14 +76,16 @@ use wiremock::matchers::method;
|
||||
use wiremock::matchers::path_regex;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[test_case(json!({"ui": {"resourceUri": "ui://calendar/widget"}, "openai/ui": {"preferredModelDisplayMode": "fullscreen"}}), Some(McpAppDisplayMode::Fullscreen); "fullscreen")]
|
||||
#[test_case(json!({"ui": {"resourceUri": "ui://calendar/widget"}}), Some(McpAppDisplayMode::Inline); "missing preference")]
|
||||
#[test_case(json!({"ui": {"resourceUri": "ui://calendar/widget"}, "openai/ui": {"preferredModelDisplayMode": "unsupported"}}), Some(McpAppDisplayMode::Inline); "unsupported preference")]
|
||||
#[test_case(json!({"openai/outputTemplate": "ui://calendar/widget"}), Some(McpAppDisplayMode::Inline); "legacy uri")]
|
||||
#[test_case(json!({}), None; "result only ui")]
|
||||
#[test_case(json!({"ui": {"resourceUri": "ui://calendar/widget"}, "openai/ui": {"preferredModelDisplayMode": "fullscreen"}}), Some(McpAppDisplayMode::Fullscreen), Some("ui://calendar/widget"); "fullscreen")]
|
||||
#[test_case(json!({"ui": {"resourceUri": "ui://calendar/widget"}, "openai/ui": {"preferredModelDisplayMode": "inline"}}), Some(McpAppDisplayMode::Inline), Some("ui://calendar/widget"); "inline")]
|
||||
#[test_case(json!({"ui": {"resourceUri": "ui://calendar/widget"}}), None, Some("ui://calendar/widget"); "missing preference")]
|
||||
#[test_case(json!({"ui": {"resourceUri": "ui://calendar/widget"}, "openai/ui": {"preferredModelDisplayMode": "unsupported"}}), None, Some("ui://calendar/widget"); "unsupported preference")]
|
||||
#[test_case(json!({"openai/outputTemplate": "ui://calendar/widget"}), None, Some("ui://calendar/widget"); "legacy uri")]
|
||||
#[test_case(json!({}), None, None; "result only ui")]
|
||||
async fn mcp_app_ui_survives_tool_events_and_resume(
|
||||
mut metadata: Value,
|
||||
expected_mode: Option<McpAppDisplayMode>,
|
||||
expected_uri: Option<&str>,
|
||||
) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
@@ -165,7 +167,7 @@ async fn mcp_app_ui_survives_tool_events_and_resume(
|
||||
resource_uri: "ui://calendar/widget".to_string(),
|
||||
preferred_model_display_mode,
|
||||
});
|
||||
let expected_uri = expected_ui.as_ref().map(|ui| ui.resource_uri.clone());
|
||||
let expected_uri = expected_uri.map(str::to_string);
|
||||
let mut observed = Vec::new();
|
||||
let mut completed = None;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
|
||||
Reference in New Issue
Block a user