Preserve MCP App UI metadata in tool-call events and history (#45805)

## Why

Clients need widget presentation details to render MCP Apps without waiting for the full MCP catalog, including when replaying saved history.

## What changed

- Add `mcpToolCall.mcpAppUi` with the invoked descriptor's `resourceUri` and `preferredModelDisplayMode`, and carry it through tool-call events and saved history.
- Support `fullscreen` and default to `inline` when the display preference is missing or unsupported.
- Keep existing resource URI fields for compatibility. Leave `mcpAppUi` null for older history and tools that declare widgets only in result metadata, where clients still use catalog discovery.
- Update protocol schemas, TypeScript and Python types, and app-server documentation.

## Testing

Add parameterized integration coverage for fullscreen, missing and unsupported preferences, legacy URI metadata, and result-only widgets, verifying consistent tool events and preservation across session resume.

GitOrigin-RevId: 2a9bed804dcea8c2b4903b8406fa6710681f7f5e
This commit is contained in:
victor-openai
2026-09-15 22:52:56 +00:00
committed by copyberry
parent 872fc22f9c
commit b71af39fe6
51 changed files with 1027 additions and 47 deletions
@@ -5659,6 +5659,7 @@ async fn turn_event_counts_completed_tool_items() {
action_name: None,
}),
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: Some("sample@test".to_string()),
read_only_hint: None,
result: None,
@@ -5912,6 +5913,7 @@ async fn mcp_elicitation_classification_survives_turn_completion_and_preserves_c
action_name: None,
}),
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None,
read_only_hint: None,
result: None,
+1
View File
@@ -3869,6 +3869,7 @@ mod tests {
arguments: serde_json::json!({}),
app_context: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None,
read_only_hint: None,
result: None,
@@ -2693,6 +2693,29 @@
"LegacyAppPathString": {
"type": "string"
},
"McpAppDisplayMode": {
"enum": [
"inline",
"fullscreen"
],
"type": "string"
},
"McpAppUi": {
"description": "UI resource and display preference for model invocations, captured from the tool descriptor.",
"properties": {
"preferredModelDisplayMode": {
"$ref": "#/definitions/McpAppDisplayMode"
},
"resourceUri": {
"type": "string"
}
},
"required": [
"preferredModelDisplayMode",
"resourceUri"
],
"type": "object"
},
"McpServerEventNotification": {
"properties": {
"method": {
@@ -4917,12 +4940,23 @@
"type": "string"
},
"mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.",
"description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [
"string",
"null"
]
},
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": {
"type": [
"string",
@@ -14410,6 +14410,29 @@
"title": "MarketplaceUpgradeResponse",
"type": "object"
},
"McpAppDisplayMode": {
"enum": [
"inline",
"fullscreen"
],
"type": "string"
},
"McpAppUi": {
"description": "UI resource and display preference for model invocations, captured from the tool descriptor.",
"properties": {
"preferredModelDisplayMode": {
"$ref": "#/definitions/v2/McpAppDisplayMode"
},
"resourceUri": {
"type": "string"
}
},
"required": [
"preferredModelDisplayMode",
"resourceUri"
],
"type": "object"
},
"McpAuthStatus": {
"enum": [
"unknown",
@@ -21826,12 +21849,23 @@
"type": "string"
},
"mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.",
"description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [
"string",
"null"
]
},
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/v2/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": {
"type": [
"string",
@@ -10462,6 +10462,29 @@
"title": "MarketplaceUpgradeResponse",
"type": "object"
},
"McpAppDisplayMode": {
"enum": [
"inline",
"fullscreen"
],
"type": "string"
},
"McpAppUi": {
"description": "UI resource and display preference for model invocations, captured from the tool descriptor.",
"properties": {
"preferredModelDisplayMode": {
"$ref": "#/definitions/McpAppDisplayMode"
},
"resourceUri": {
"type": "string"
}
},
"required": [
"preferredModelDisplayMode",
"resourceUri"
],
"type": "object"
},
"McpAuthStatus": {
"enum": [
"unknown",
@@ -19540,12 +19563,23 @@
"type": "string"
},
"mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.",
"description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [
"string",
"null"
]
},
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": {
"type": [
"string",
@@ -510,6 +510,29 @@
"LegacyAppPathString": {
"type": "string"
},
"McpAppDisplayMode": {
"enum": [
"inline",
"fullscreen"
],
"type": "string"
},
"McpAppUi": {
"description": "UI resource and display preference for model invocations, captured from the tool descriptor.",
"properties": {
"preferredModelDisplayMode": {
"$ref": "#/definitions/McpAppDisplayMode"
},
"resourceUri": {
"type": "string"
}
},
"required": [
"preferredModelDisplayMode",
"resourceUri"
],
"type": "object"
},
"McpToolCallAppContext": {
"properties": {
"actionName": {
@@ -1137,12 +1160,23 @@
"type": "string"
},
"mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.",
"description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [
"string",
"null"
]
},
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": {
"type": [
"string",
@@ -510,6 +510,29 @@
"LegacyAppPathString": {
"type": "string"
},
"McpAppDisplayMode": {
"enum": [
"inline",
"fullscreen"
],
"type": "string"
},
"McpAppUi": {
"description": "UI resource and display preference for model invocations, captured from the tool descriptor.",
"properties": {
"preferredModelDisplayMode": {
"$ref": "#/definitions/McpAppDisplayMode"
},
"resourceUri": {
"type": "string"
}
},
"required": [
"preferredModelDisplayMode",
"resourceUri"
],
"type": "object"
},
"McpToolCallAppContext": {
"properties": {
"actionName": {
@@ -1137,12 +1160,23 @@
"type": "string"
},
"mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.",
"description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [
"string",
"null"
]
},
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": {
"type": [
"string",
@@ -650,6 +650,29 @@
"LegacyAppPathString": {
"type": "string"
},
"McpAppDisplayMode": {
"enum": [
"inline",
"fullscreen"
],
"type": "string"
},
"McpAppUi": {
"description": "UI resource and display preference for model invocations, captured from the tool descriptor.",
"properties": {
"preferredModelDisplayMode": {
"$ref": "#/definitions/McpAppDisplayMode"
},
"resourceUri": {
"type": "string"
}
},
"required": [
"preferredModelDisplayMode",
"resourceUri"
],
"type": "object"
},
"McpToolCallAppContext": {
"properties": {
"actionName": {
@@ -1325,12 +1348,23 @@
"type": "string"
},
"mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.",
"description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [
"string",
"null"
]
},
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": {
"type": [
"string",
@@ -754,6 +754,29 @@
"LegacyAppPathString": {
"type": "string"
},
"McpAppDisplayMode": {
"enum": [
"inline",
"fullscreen"
],
"type": "string"
},
"McpAppUi": {
"description": "UI resource and display preference for model invocations, captured from the tool descriptor.",
"properties": {
"preferredModelDisplayMode": {
"$ref": "#/definitions/McpAppDisplayMode"
},
"resourceUri": {
"type": "string"
}
},
"required": [
"preferredModelDisplayMode",
"resourceUri"
],
"type": "object"
},
"McpToolCallAppContext": {
"properties": {
"actionName": {
@@ -1939,12 +1962,23 @@
"type": "string"
},
"mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.",
"description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [
"string",
"null"
]
},
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": {
"type": [
"string",
@@ -510,6 +510,29 @@
"LegacyAppPathString": {
"type": "string"
},
"McpAppDisplayMode": {
"enum": [
"inline",
"fullscreen"
],
"type": "string"
},
"McpAppUi": {
"description": "UI resource and display preference for model invocations, captured from the tool descriptor.",
"properties": {
"preferredModelDisplayMode": {
"$ref": "#/definitions/McpAppDisplayMode"
},
"resourceUri": {
"type": "string"
}
},
"required": [
"preferredModelDisplayMode",
"resourceUri"
],
"type": "object"
},
"McpToolCallAppContext": {
"properties": {
"actionName": {
@@ -1137,12 +1160,23 @@
"type": "string"
},
"mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.",
"description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [
"string",
"null"
]
},
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": {
"type": [
"string",
@@ -676,6 +676,29 @@
"LegacyAppPathString": {
"type": "string"
},
"McpAppDisplayMode": {
"enum": [
"inline",
"fullscreen"
],
"type": "string"
},
"McpAppUi": {
"description": "UI resource and display preference for model invocations, captured from the tool descriptor.",
"properties": {
"preferredModelDisplayMode": {
"$ref": "#/definitions/McpAppDisplayMode"
},
"resourceUri": {
"type": "string"
}
},
"required": [
"preferredModelDisplayMode",
"resourceUri"
],
"type": "object"
},
"McpToolCallAppContext": {
"properties": {
"actionName": {
@@ -1730,12 +1753,23 @@
"type": "string"
},
"mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.",
"description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [
"string",
"null"
]
},
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": {
"type": [
"string",
@@ -676,6 +676,29 @@
"LegacyAppPathString": {
"type": "string"
},
"McpAppDisplayMode": {
"enum": [
"inline",
"fullscreen"
],
"type": "string"
},
"McpAppUi": {
"description": "UI resource and display preference for model invocations, captured from the tool descriptor.",
"properties": {
"preferredModelDisplayMode": {
"$ref": "#/definitions/McpAppDisplayMode"
},
"resourceUri": {
"type": "string"
}
},
"required": [
"preferredModelDisplayMode",
"resourceUri"
],
"type": "object"
},
"McpToolCallAppContext": {
"properties": {
"actionName": {
@@ -1730,12 +1753,23 @@
"type": "string"
},
"mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.",
"description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [
"string",
"null"
]
},
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": {
"type": [
"string",
@@ -676,6 +676,29 @@
"LegacyAppPathString": {
"type": "string"
},
"McpAppDisplayMode": {
"enum": [
"inline",
"fullscreen"
],
"type": "string"
},
"McpAppUi": {
"description": "UI resource and display preference for model invocations, captured from the tool descriptor.",
"properties": {
"preferredModelDisplayMode": {
"$ref": "#/definitions/McpAppDisplayMode"
},
"resourceUri": {
"type": "string"
}
},
"required": [
"preferredModelDisplayMode",
"resourceUri"
],
"type": "object"
},
"McpToolCallAppContext": {
"properties": {
"actionName": {
@@ -1730,12 +1753,23 @@
"type": "string"
},
"mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.",
"description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [
"string",
"null"
]
},
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": {
"type": [
"string",
@@ -770,6 +770,29 @@
"LegacyAppPathString": {
"type": "string"
},
"McpAppDisplayMode": {
"enum": [
"inline",
"fullscreen"
],
"type": "string"
},
"McpAppUi": {
"description": "UI resource and display preference for model invocations, captured from the tool descriptor.",
"properties": {
"preferredModelDisplayMode": {
"$ref": "#/definitions/McpAppDisplayMode"
},
"resourceUri": {
"type": "string"
}
},
"required": [
"preferredModelDisplayMode",
"resourceUri"
],
"type": "object"
},
"McpToolCallAppContext": {
"properties": {
"actionName": {
@@ -1991,12 +2014,23 @@
"type": "string"
},
"mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.",
"description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [
"string",
"null"
]
},
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": {
"type": [
"string",
@@ -676,6 +676,29 @@
"LegacyAppPathString": {
"type": "string"
},
"McpAppDisplayMode": {
"enum": [
"inline",
"fullscreen"
],
"type": "string"
},
"McpAppUi": {
"description": "UI resource and display preference for model invocations, captured from the tool descriptor.",
"properties": {
"preferredModelDisplayMode": {
"$ref": "#/definitions/McpAppDisplayMode"
},
"resourceUri": {
"type": "string"
}
},
"required": [
"preferredModelDisplayMode",
"resourceUri"
],
"type": "object"
},
"McpToolCallAppContext": {
"properties": {
"actionName": {
@@ -1730,12 +1753,23 @@
"type": "string"
},
"mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.",
"description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [
"string",
"null"
]
},
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": {
"type": [
"string",
@@ -754,6 +754,29 @@
"LegacyAppPathString": {
"type": "string"
},
"McpAppDisplayMode": {
"enum": [
"inline",
"fullscreen"
],
"type": "string"
},
"McpAppUi": {
"description": "UI resource and display preference for model invocations, captured from the tool descriptor.",
"properties": {
"preferredModelDisplayMode": {
"$ref": "#/definitions/McpAppDisplayMode"
},
"resourceUri": {
"type": "string"
}
},
"required": [
"preferredModelDisplayMode",
"resourceUri"
],
"type": "object"
},
"McpToolCallAppContext": {
"properties": {
"actionName": {
@@ -1939,12 +1962,23 @@
"type": "string"
},
"mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.",
"description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [
"string",
"null"
]
},
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": {
"type": [
"string",
@@ -676,6 +676,29 @@
"LegacyAppPathString": {
"type": "string"
},
"McpAppDisplayMode": {
"enum": [
"inline",
"fullscreen"
],
"type": "string"
},
"McpAppUi": {
"description": "UI resource and display preference for model invocations, captured from the tool descriptor.",
"properties": {
"preferredModelDisplayMode": {
"$ref": "#/definitions/McpAppDisplayMode"
},
"resourceUri": {
"type": "string"
}
},
"required": [
"preferredModelDisplayMode",
"resourceUri"
],
"type": "object"
},
"McpToolCallAppContext": {
"properties": {
"actionName": {
@@ -1730,12 +1753,23 @@
"type": "string"
},
"mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.",
"description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [
"string",
"null"
]
},
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": {
"type": [
"string",
@@ -650,6 +650,29 @@
"LegacyAppPathString": {
"type": "string"
},
"McpAppDisplayMode": {
"enum": [
"inline",
"fullscreen"
],
"type": "string"
},
"McpAppUi": {
"description": "UI resource and display preference for model invocations, captured from the tool descriptor.",
"properties": {
"preferredModelDisplayMode": {
"$ref": "#/definitions/McpAppDisplayMode"
},
"resourceUri": {
"type": "string"
}
},
"required": [
"preferredModelDisplayMode",
"resourceUri"
],
"type": "object"
},
"McpToolCallAppContext": {
"properties": {
"actionName": {
@@ -1325,12 +1348,23 @@
"type": "string"
},
"mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.",
"description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [
"string",
"null"
]
},
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": {
"type": [
"string",
@@ -676,6 +676,29 @@
"LegacyAppPathString": {
"type": "string"
},
"McpAppDisplayMode": {
"enum": [
"inline",
"fullscreen"
],
"type": "string"
},
"McpAppUi": {
"description": "UI resource and display preference for model invocations, captured from the tool descriptor.",
"properties": {
"preferredModelDisplayMode": {
"$ref": "#/definitions/McpAppDisplayMode"
},
"resourceUri": {
"type": "string"
}
},
"required": [
"preferredModelDisplayMode",
"resourceUri"
],
"type": "object"
},
"McpToolCallAppContext": {
"properties": {
"actionName": {
@@ -1730,12 +1753,23 @@
"type": "string"
},
"mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.",
"description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [
"string",
"null"
]
},
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": {
"type": [
"string",
@@ -650,6 +650,29 @@
"LegacyAppPathString": {
"type": "string"
},
"McpAppDisplayMode": {
"enum": [
"inline",
"fullscreen"
],
"type": "string"
},
"McpAppUi": {
"description": "UI resource and display preference for model invocations, captured from the tool descriptor.",
"properties": {
"preferredModelDisplayMode": {
"$ref": "#/definitions/McpAppDisplayMode"
},
"resourceUri": {
"type": "string"
}
},
"required": [
"preferredModelDisplayMode",
"resourceUri"
],
"type": "object"
},
"McpToolCallAppContext": {
"properties": {
"actionName": {
@@ -1325,12 +1348,23 @@
"type": "string"
},
"mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.",
"description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [
"string",
"null"
]
},
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": {
"type": [
"string",
@@ -650,6 +650,29 @@
"LegacyAppPathString": {
"type": "string"
},
"McpAppDisplayMode": {
"enum": [
"inline",
"fullscreen"
],
"type": "string"
},
"McpAppUi": {
"description": "UI resource and display preference for model invocations, captured from the tool descriptor.",
"properties": {
"preferredModelDisplayMode": {
"$ref": "#/definitions/McpAppDisplayMode"
},
"resourceUri": {
"type": "string"
}
},
"required": [
"preferredModelDisplayMode",
"resourceUri"
],
"type": "object"
},
"McpToolCallAppContext": {
"properties": {
"actionName": {
@@ -1325,12 +1348,23 @@
"type": "string"
},
"mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.",
"description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [
"string",
"null"
]
},
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": {
"type": [
"string",
@@ -650,6 +650,29 @@
"LegacyAppPathString": {
"type": "string"
},
"McpAppDisplayMode": {
"enum": [
"inline",
"fullscreen"
],
"type": "string"
},
"McpAppUi": {
"description": "UI resource and display preference for model invocations, captured from the tool descriptor.",
"properties": {
"preferredModelDisplayMode": {
"$ref": "#/definitions/McpAppDisplayMode"
},
"resourceUri": {
"type": "string"
}
},
"required": [
"preferredModelDisplayMode",
"resourceUri"
],
"type": "object"
},
"McpToolCallAppContext": {
"properties": {
"actionName": {
@@ -1325,12 +1348,23 @@
"type": "string"
},
"mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.",
"description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [
"string",
"null"
]
},
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": {
"type": [
"string",
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type McpAppDisplayMode = "inline" | "fullscreen";
@@ -0,0 +1,9 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { McpAppDisplayMode } from "./McpAppDisplayMode";
/**
* UI resource and display preference for model invocations, captured from the tool descriptor.
*/
export type McpAppUi = { resourceUri: string, preferredModelDisplayMode: McpAppDisplayMode, };
@@ -21,6 +21,7 @@ import type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputCo
import type { DynamicToolCallStatus } from "./DynamicToolCallStatus";
import type { FileUpdateChange } from "./FileUpdateChange";
import type { HookPromptFragment } from "./HookPromptFragment";
import type { McpAppUi } from "./McpAppUi";
import type { McpToolCallAppContext } from "./McpToolCallAppContext";
import type { McpToolCallError } from "./McpToolCallError";
import type { McpToolCallResult } from "./McpToolCallResult";
@@ -70,9 +71,13 @@ exitCode: number | 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, appContext: McpToolCallAppContext | null,
/**
* Deprecated: use `appContext.resourceUri` instead.
* Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.
*/
mcpAppResourceUri?: string, pluginId: string | null, readOnlyHint: boolean | null, result: McpToolCallResult | null, error: McpToolCallError | null,
mcpAppResourceUri?: string,
/**
* Presentation captured from the invoked descriptor; absent in older history.
*/
mcpAppUi: McpAppUi | null, pluginId: string | null, readOnlyHint: boolean | null, result: McpToolCallResult | null, error: McpToolCallError | null,
/**
* The duration of the MCP tool call in milliseconds.
*/
@@ -251,6 +251,8 @@ export type { MarketplaceRemoveResponse } from "./MarketplaceRemoveResponse";
export type { MarketplaceUpgradeErrorInfo } from "./MarketplaceUpgradeErrorInfo";
export type { MarketplaceUpgradeParams } from "./MarketplaceUpgradeParams";
export type { MarketplaceUpgradeResponse } from "./MarketplaceUpgradeResponse";
export type { McpAppDisplayMode } from "./McpAppDisplayMode";
export type { McpAppUi } from "./McpAppUi";
export type { McpAuthStatus } from "./McpAuthStatus";
export type { McpElicitationArrayType } from "./McpElicitationArrayType";
export type { McpElicitationBooleanSchema } from "./McpElicitationBooleanSchema";
@@ -784,6 +784,7 @@ impl ThreadHistoryBuilder {
action_name: payload.action_name.clone(),
}),
mcp_app_resource_uri: payload.mcp_app_resource_uri.clone(),
mcp_app_ui: payload.mcp_app_ui.clone(),
plugin_id: payload.plugin_id.clone(),
read_only_hint: payload.read_only_hint,
result: None,
@@ -841,6 +842,7 @@ impl ThreadHistoryBuilder {
action_name: payload.action_name.clone(),
}),
mcp_app_resource_uri: payload.mcp_app_resource_uri.clone(),
mcp_app_ui: payload.mcp_app_ui.clone(),
plugin_id: payload.plugin_id.clone(),
read_only_hint: payload.read_only_hint,
result,
@@ -3118,6 +3120,7 @@ mod tests {
},
connector_id: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
link_id: None,
app_name: None,
action_name: None,
@@ -3181,6 +3184,7 @@ mod tests {
arguments: serde_json::json!({"id":"123"}),
app_context: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None,
read_only_hint: None,
result: None,
@@ -3213,6 +3217,7 @@ mod tests {
},
connector_id: Some("calendar".into()),
mcp_app_resource_uri: Some("ui://widget/lookup.html".into()),
mcp_app_ui: None,
link_id: Some("link_calendar".into()),
app_name: Some("Calendar".into()),
action_name: Some("lookup".into()),
@@ -3255,6 +3260,7 @@ mod tests {
action_name: Some("lookup".into()),
}),
mcp_app_resource_uri: Some("ui://widget/lookup.html".into()),
mcp_app_ui: None,
plugin_id: Some("sample@test".into()),
read_only_hint: Some(false),
result: Some(Box::new(McpToolCallResult {
@@ -3646,6 +3652,7 @@ mod tests {
arguments: serde_json::json!({"query": "incident"}),
connector_id: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
link_id: None,
app_name: None,
action_name: None,
@@ -31,6 +31,8 @@ use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool;
use codex_protocol::items::CollabAgentToolCallStatus as CoreCollabAgentToolCallStatus;
use codex_protocol::items::CommandExecutionStatus as CoreCommandExecutionStatus;
use codex_protocol::items::DynamicToolCallStatus as CoreDynamicToolCallStatus;
pub use codex_protocol::items::McpAppDisplayMode;
pub use codex_protocol::items::McpAppUi;
use codex_protocol::items::McpToolCallStatus as CoreMcpToolCallStatus;
use codex_protocol::items::TurnItem as CoreTurnItem;
use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation;
@@ -337,8 +339,10 @@ pub enum ThreadItem {
app_context: Option<McpToolCallAppContext>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
/// Deprecated: use `appContext.resourceUri` instead.
/// Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.
mcp_app_resource_uri: Option<String>,
/// Presentation captured from the invoked descriptor; absent in older history.
mcp_app_ui: Option<McpAppUi>,
plugin_id: Option<String>,
read_only_hint: Option<bool>,
result: Option<Box<McpToolCallResult>>,
@@ -1038,6 +1042,7 @@ impl From<CoreTurnItem> for ThreadItem {
action_name: mcp.action_name,
}),
mcp_app_resource_uri: mcp.mcp_app_resource_uri,
mcp_app_ui: mcp.mcp_app_ui,
plugin_id: mcp.plugin_id,
read_only_hint: mcp.read_only_hint,
result: mcp.result.map(McpToolCallResult::from).map(Box::new),
@@ -3463,6 +3463,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
arguments: json!({"arg": "value"}),
connector_id: Some("calendar".to_string()),
mcp_app_resource_uri: Some("app://connector".to_string()),
mcp_app_ui: None,
link_id: Some("link_calendar".to_string()),
app_name: Some("Calendar".to_string()),
action_name: Some("create_event".to_string()),
@@ -3490,6 +3491,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
action_name: Some("create_event".to_string()),
}),
mcp_app_resource_uri: Some("app://connector".to_string()),
mcp_app_ui: None,
plugin_id: Some("sample@test".to_string()),
read_only_hint: Some(true),
result: None,
@@ -3505,6 +3507,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
arguments: JsonValue::Null,
connector_id: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
link_id: None,
app_name: None,
action_name: None,
@@ -3531,6 +3534,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
arguments: JsonValue::Null,
app_context: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None,
read_only_hint: Some(false),
result: Some(Box::new(McpToolCallResult {
@@ -3560,6 +3564,7 @@ fn mcp_tool_call_app_context_serializes_connector_id() {
action_name: Some("create_event".to_string()),
}),
mcp_app_resource_uri: Some("app://connector".to_string()),
mcp_app_ui: None,
plugin_id: None,
read_only_hint: Some(false),
result: None,
@@ -3584,6 +3589,7 @@ fn mcp_tool_call_app_context_serializes_connector_id() {
"actionName": "create_event",
},
"mcpAppResourceUri": "app://connector",
"mcpAppUi": null,
"pluginId": null,
"readOnlyHint": false,
"result": null,
+12
View File
@@ -1,3 +1,15 @@
# 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
UI information is preserved in tool-call events and saved history so clients can
render without waiting for the full MCP catalog.
The field is null for older history and tools that declare widgets only in
result metadata; clients retain catalog discovery for those calls. Existing
resource URI fields remain available for older clients.
# Initial Daybreak choice (experimental)
Persistent threads accept `daybreakEnabled` on `thread/start` with the
@@ -90,6 +90,7 @@ mod tests {
action_name: Some("lookup".to_string()),
}),
mcp_app_resource_uri: Some("ui://widget/lookup.html".to_string()),
mcp_app_ui: None,
plugin_id: Some("sample@test".to_string()),
read_only_hint: None,
result: Some(Box::new(McpToolCallResult {
@@ -146,6 +147,7 @@ mod tests {
action_name: Some("lookup".to_string()),
}),
mcp_app_resource_uri: Some("ui://widget/lookup.html".to_string()),
mcp_app_ui: None,
plugin_id: Some("sample@test".to_string()),
read_only_hint: None,
result: Some(Box::new(redacted_mcp_tool_call_result())),
@@ -165,6 +167,7 @@ mod tests {
arguments: serde_json::json!({"secret":"argument"}),
app_context: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None,
read_only_hint: None,
result: None,
@@ -186,6 +189,7 @@ mod tests {
arguments: JsonValue::String(REDACTED_PAYLOAD.to_string()),
app_context: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None,
read_only_hint: None,
result: None,
@@ -1087,6 +1087,7 @@ async fn mcp_tool_call_completion_notification_contains_truncated_large_result()
arguments: json!({ "message": LARGE_RESPONSE_MESSAGE }),
app_context: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None,
read_only_hint: None,
result: Some(result),
@@ -1198,6 +1199,7 @@ async fn mcp_tool_call_hint_survives_mid_call_thread_read_and_resume() -> Result
arguments: json!({ "message": ELICITATION_TRIGGER_MESSAGE }),
app_context: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None,
read_only_hint: Some(true),
result: None,
@@ -2770,6 +2770,7 @@ fn append_resume_redaction_history(
},
connector_id: Some("calendar".to_string()),
mcp_app_resource_uri: Some("ui://widget/lookup.html".to_string()),
mcp_app_ui: None,
link_id: Some("link_calendar".to_string()),
app_name: Some("Calendar".to_string()),
action_name: Some("lookup".to_string()),
+35 -8
View File
@@ -46,6 +46,8 @@ use codex_mcp::build_auth_elicitation_plan;
use codex_mcp::is_connector_auth_failure_from_tool_result;
use codex_mcp::mcp_permission_prompt_is_auto_approved;
use codex_protocol::approvals::ElicitationRequest;
use codex_protocol::items::McpAppDisplayMode;
use codex_protocol::items::McpAppUi;
use codex_protocol::items::McpToolCallError;
use codex_protocol::items::McpToolCallItem;
use codex_protocol::items::McpToolCallStatus;
@@ -390,7 +392,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>,
plugin_id: Option<String>,
@@ -408,8 +410,7 @@ 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
.and_then(|metadata| metadata.codex_apps_meta.as_ref())
@@ -1004,7 +1005,11 @@ 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_resource_uri,
mcp_app_resource_uri: item_metadata
.mcp_app_ui
.as_ref()
.map(|ui| ui.resource_uri.clone()),
mcp_app_ui: item_metadata.mcp_app_ui,
link_id: item_metadata.link_id,
app_name: item_metadata.app_name,
action_name: item_metadata.action_name,
@@ -1049,7 +1054,11 @@ 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_resource_uri,
mcp_app_resource_uri: item_metadata
.mcp_app_ui
.as_ref()
.map(|ui| ui.resource_uri.clone()),
mcp_app_ui: item_metadata.mcp_app_ui,
link_id: item_metadata.link_id,
app_name: item_metadata.app_name,
action_name: item_metadata.action_name,
@@ -1171,7 +1180,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>>>,
}
@@ -1633,7 +1642,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,
};
@@ -1783,6 +1792,24 @@ fn mcp_tool_metadata(
None
};
let mcp_app_ui = get_mcp_app_resource_uri(tool_info.tool.meta.as_deref()).map(|resource_uri| {
let preferred_model_display_mode = match tool_info
.tool
.meta
.as_ref()
.and_then(|meta| meta.get("openai/ui"))
.and_then(|ui| ui.get("preferredModelDisplayMode"))
.and_then(serde_json::Value::as_str)
{
Some("fullscreen") => McpAppDisplayMode::Fullscreen,
_ => McpAppDisplayMode::Inline,
};
McpAppUi {
resource_uri,
preferred_model_display_mode,
}
});
Ok(McpToolApprovalMetadata {
annotations: tool_info.tool.annotations,
connector_id: tool_info.connector_id,
@@ -1793,7 +1820,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: get_mcp_app_resource_uri(tool_info.tool.meta.as_deref()),
mcp_app_ui,
codex_apps_meta,
// Disallow custom MCPs from uploading files via fileParams.
openai_file_input_optional_fields: openai_file_input_optional_fields_for_server(
+15 -15
View File
@@ -90,7 +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,
}
@@ -1436,7 +1436,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()),
plugin_id: None,
@@ -1448,7 +1448,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,
plugin_id: None,
@@ -1473,7 +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()),
plugin_id: Some("sample@test".to_string()),
@@ -1523,7 +1523,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!({
"resource_uri": "connector://calendar/tools/calendar_create_event",
@@ -1974,7 +1974,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,
};
@@ -2738,7 +2738,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,
};
@@ -2819,7 +2819,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,
};
@@ -2882,7 +2882,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,
};
@@ -3033,7 +3033,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,
};
@@ -3134,7 +3134,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,
};
@@ -3213,7 +3213,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,
};
@@ -3299,7 +3299,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,
};
@@ -3364,7 +3364,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,
};
@@ -3423,7 +3423,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,
};
@@ -95,6 +95,7 @@ fn mcp_tool_call(id: &str, server: &str, status: McpToolCallStatus) -> TurnItem
arguments: serde_json::Value::Null,
connector_id: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
link_id: None,
app_name: None,
action_name: None,
@@ -222,6 +222,7 @@ async fn emit_tool_call_begin(
arguments: arguments.unwrap_or(Value::Null),
connector_id: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
link_id: None,
app_name: None,
action_name: None,
@@ -266,6 +267,7 @@ async fn emit_tool_call_end(
arguments: arguments.unwrap_or(Value::Null),
connector_id: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
link_id: None,
app_name: None,
action_name: None,
@@ -14,6 +14,8 @@ use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::items::McpAppDisplayMode;
use codex_protocol::items::McpAppUi;
use codex_protocol::items::McpToolCallStatus;
use codex_protocol::items::TurnItem;
use codex_protocol::models::NetworkPermissions;
@@ -23,6 +25,8 @@ use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::ElicitationAction;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ItemCompletedEvent;
use codex_protocol::protocol::ItemStartedEvent;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::TurnEnvironmentSelections;
@@ -71,6 +75,142 @@ use wiremock::matchers::body_partial_json;
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")]
async fn mcp_app_ui_survives_tool_events_and_resume(
mut metadata: Value,
expected_mode: Option<McpAppDisplayMode>,
) -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let apps_server = AppsTestServer::mount(&server).await?;
metadata["connector_id"] = json!("calendar");
metadata["connector_name"] = json!("Calendar");
Mock::given(method("POST"))
.and(path_regex("^/api/codex/ps/mcp/?$"))
.and(body_partial_json(json!({"method": "tools/list"})))
.respond_with(move |request: &Request| {
let body: Value = serde_json::from_slice(&request.body).expect("valid tools/list");
ResponseTemplate::new(/*status*/ 200).set_body_json(json!({
"jsonrpc": "2.0",
"id": body["id"],
"result": {"tools": [{
"name": "calendar_create_event",
"description": "Create a calendar event.",
"inputSchema": {"type": "object", "properties": {}},
"annotations": {"readOnlyHint": true},
"_meta": metadata
}]}
}))
})
.with_priority(/*priority*/ 1)
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path_regex("^/api/codex/ps/mcp/?$"))
.and(body_partial_json(json!({"method": "tools/call"})))
.respond_with(|request: &Request| {
let body: Value = serde_json::from_slice(&request.body).expect("valid tools/call");
ResponseTemplate::new(/*status*/ 200).set_body_json(json!({
"jsonrpc": "2.0",
"id": body["id"],
"result": {
"content": [{"type": "text", "text": "Calendar opened."}],
"_meta": {"openai/outputTemplate": "ui://calendar/result-widget"},
"isError": false
}
}))
})
.with_priority(/*priority*/ 1)
.mount(&server)
.await;
let call_id = "calendar-ui";
let responses = mount_sse_sequence(
&server,
vec![
sse(vec![
ev_response_created("resp-calendar"),
ev_function_call_with_namespace(
call_id,
SEARCH_CALENDAR_NAMESPACE,
SEARCH_CALENDAR_CREATE_TOOL,
"{}",
),
ev_completed("resp-calendar"),
]),
sse(vec![
ev_assistant_message("msg-done", "done"),
ev_completed("resp-done"),
]),
],
)
.await;
let mut builder = search_capable_apps_builder(apps_server.chatgpt_base_url);
let test = builder.build_with_auto_env(&server).await?;
submit_user_turn(
&test,
"Use [$calendar](app://calendar) to open the calendar widget.",
AskForApproval::Never,
PermissionProfile::Disabled,
/*collaboration_mode*/ None,
)
.await?;
let expected_ui = expected_mode.map(|preferred_model_display_mode| McpAppUi {
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 mut observed = Vec::new();
let mut completed = None;
wait_for_event(&test.codex, |event| {
match event {
EventMsg::ItemStarted(ItemStartedEvent {
item: TurnItem::McpToolCall(item),
..
})
| EventMsg::ItemCompleted(ItemCompletedEvent {
item: TurnItem::McpToolCall(item),
..
}) => {
observed.push((item.mcp_app_ui.clone(), item.mcp_app_resource_uri.clone()));
}
EventMsg::McpToolCallBegin(begin) => {
observed.push((begin.mcp_app_ui.clone(), begin.mcp_app_resource_uri.clone()));
}
EventMsg::McpToolCallEnd(end) => {
assert!(end.result.is_ok());
observed.push((end.mcp_app_ui.clone(), end.mcp_app_resource_uri.clone()));
completed = Some(event.clone());
}
_ => {}
}
matches!(event, EventMsg::TurnComplete(_))
})
.await;
assert_eq!(observed, vec![(expected_ui, expected_uri); 4]);
assert_eq!(responses.requests().len(), 2);
let resumed = builder.restart(&server, &test).await?;
let completed_history = resumed
.session_configured
.initial_messages
.expect("resumed history")
.into_iter()
.filter(|event| matches!(event, EventMsg::McpToolCallEnd(end) if end.call_id == call_id))
.collect::<Vec<_>>();
assert_eq!(
serde_json::to_value(completed_history)?,
serde_json::to_value(vec![completed.expect("completed MCP call")])?
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn auto_session_approval_is_scoped_to_tool_link_id() -> Result<()> {
skip_if_no_network!(Ok(()));
+1
View File
@@ -2529,6 +2529,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> {
},
connector_id: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
link_id: None,
app_name: None,
action_name: None,
@@ -103,6 +103,7 @@ fn mcp_tool_call_result_preserves_meta_in_jsonl_event() {
arguments: json!({"search_query": [{"q": "OpenAI Codex CLI documentation"}]}),
app_context: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None,
read_only_hint: None,
result: Some(Box::new(codex_app_server_protocol::McpToolCallResult {
@@ -492,6 +492,7 @@ fn mcp_tool_call_begin_and_end_emit_item_events() {
arguments: json!({ "key": "value" }),
app_context: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None,
read_only_hint: None,
result: None,
@@ -512,6 +513,7 @@ fn mcp_tool_call_begin_and_end_emit_item_events() {
arguments: json!({ "key": "value" }),
app_context: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None,
read_only_hint: None,
result: Some(Box::new(McpToolCallResult {
@@ -586,6 +588,7 @@ fn mcp_tool_call_failure_sets_failed_status() {
arguments: json!({ "param": 42 }),
app_context: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None,
read_only_hint: None,
result: None,
@@ -637,6 +640,7 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() {
arguments: serde_json::Value::Null,
app_context: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None,
read_only_hint: None,
result: None,
@@ -657,6 +661,7 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() {
arguments: serde_json::Value::Null,
app_context: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None,
read_only_hint: None,
result: Some(Box::new(McpToolCallResult {
+23
View File
@@ -425,6 +425,25 @@ pub struct FileChangeItem {
pub stderr: Option<String>,
}
/// UI resource and display preference for model invocations, captured from the tool descriptor.
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct McpAppUi {
pub resource_uri: String,
pub preferred_model_display_mode: McpAppDisplayMode,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub enum McpAppDisplayMode {
Inline,
Fullscreen,
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
@@ -436,11 +455,15 @@ pub struct McpToolCallItem {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub connector_id: Option<String>,
/// Legacy compatibility field; prefer `mcp_app_ui.resource_uri` when available.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub mcp_app_resource_uri: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub mcp_app_ui: Option<McpAppUi>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub link_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
+2
View File
@@ -508,6 +508,7 @@ impl McpToolCallItem {
},
connector_id: self.connector_id.clone(),
mcp_app_resource_uri: self.mcp_app_resource_uri.clone(),
mcp_app_ui: self.mcp_app_ui.clone(),
link_id: self.link_id.clone(),
app_name: self.app_name.clone(),
action_name: self.action_name.clone(),
@@ -532,6 +533,7 @@ impl McpToolCallItem {
arguments: (!self.arguments.is_null()).then(|| self.arguments.clone()),
},
mcp_app_resource_uri: self.mcp_app_resource_uri.clone(),
mcp_app_ui: self.mcp_app_ui.clone(),
connector_id: self.connector_id.clone(),
link_id: self.link_id.clone(),
app_name: self.app_name.clone(),
+8
View File
@@ -2670,6 +2670,9 @@ pub struct McpToolCallBeginEvent {
pub mcp_app_resource_uri: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub mcp_app_ui: Option<crate::items::McpAppUi>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub link_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
@@ -2702,6 +2705,9 @@ pub struct McpToolCallEndEvent {
pub mcp_app_resource_uri: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub mcp_app_ui: Option<crate::items::McpAppUi>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub link_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
@@ -5392,6 +5398,7 @@ mod tests {
arguments: json!({"arg": "value"}),
connector_id: Some("connector".into()),
mcp_app_resource_uri: Some("app://connector".into()),
mcp_app_ui: None,
link_id: Some("link_123".into()),
app_name: Some("Calendar".into()),
action_name: Some("create_event".into()),
@@ -5513,6 +5520,7 @@ mod tests {
arguments: json!({"arg": "value"}),
connector_id: Some("connector".into()),
mcp_app_resource_uri: Some("app://connector".into()),
mcp_app_ui: None,
link_id: Some("link_123".into()),
app_name: Some("Calendar".into()),
action_name: Some("create_event".into()),
@@ -185,6 +185,7 @@ pub(super) fn completed_item(
.unwrap_or(serde_json::Value::Null),
connector_id: event.connector_id.clone(),
mcp_app_resource_uri: event.mcp_app_resource_uri.clone(),
mcp_app_ui: event.mcp_app_ui.clone(),
link_id: event.link_id.clone(),
app_name: event.app_name.clone(),
action_name: event.action_name.clone(),
@@ -654,6 +654,7 @@ async fn misalignment_buffered_replay_preserves_input_after_continuation() {
arguments: serde_json::json!({}),
app_context: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None,
read_only_hint: None,
result: None,
@@ -12,6 +12,7 @@ fn computer_item(
arguments: json!({"title": format!("Inspect page {id}"), "code": "await cua.getState()"}),
app_context: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None,
read_only_hint: None,
result: Some(Box::new(codex_app_server_protocol::McpToolCallResult {
@@ -1223,6 +1223,7 @@ async fn replayed_in_progress_mcp_tool_call_stays_active() {
arguments: json!({"action": "wait"}),
app_context: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None,
read_only_hint: None,
result: None,
@@ -1258,6 +1259,7 @@ async fn failed_repl_mcp_tool_call_preserves_status_and_result() {
arguments: json!({"title": "Inspect workspace"}),
app_context: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None,
read_only_hint: None,
result: Some(Box::new(codex_app_server_protocol::McpToolCallResult {
@@ -1326,6 +1328,7 @@ async fn deferred_mcp_lifecycle_events_keep_fifo_after_stream_finishes() {
arguments: json!({"action": "wait"}),
app_context: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None,
read_only_hint: None,
result: None,
@@ -1343,6 +1346,7 @@ async fn deferred_mcp_lifecycle_events_keep_fifo_after_stream_finishes() {
arguments: json!({"action": "wait"}),
app_context: None,
mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None,
read_only_hint: None,
result: Some(Box::new(codex_app_server_protocol::McpToolCallResult {
@@ -2461,6 +2461,21 @@ class MarketplaceUpgradeResponse(BaseModel):
upgraded_roots: Annotated[list[AbsolutePathBuf], Field(alias="upgradedRoots")]
class McpAppDisplayMode(Enum):
inline = "inline"
fullscreen = "fullscreen"
class McpAppUi(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
preferred_model_display_mode: Annotated[
McpAppDisplayMode, Field(alias="preferredModelDisplayMode")
]
resource_uri: Annotated[str, Field(alias="resourceUri")]
class McpAuthStatus(Enum):
unknown = "unknown"
unsupported = "unsupported"
@@ -5303,7 +5318,14 @@ class McpToolCallThreadItem(BaseModel):
str | None,
Field(
alias="mcpAppResourceUri",
description="Deprecated: use `appContext.resourceUri` instead.",
description="Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
),
] = None
mcp_app_ui: Annotated[
McpAppUi | None,
Field(
alias="mcpAppUi",
description="Presentation captured from the invoked descriptor; absent in older history.",
),
] = None
plugin_id: Annotated[str | None, Field(alias="pluginId")] = None