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 authored and copyberry committed 2026-09-15 22:52:56 +00:00
1 parent 872fc22f9c
commit b71af39fe6
51 files changed
+1027 -47

No files matched your search

@@ -5659,6 +5659,7 @@ async fn turn_event_counts_completed_tool_items() {
action_name: None, action_name: None,
}), }),
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: Some("sample@test".to_string()), plugin_id: Some("sample@test".to_string()),
read_only_hint: None, read_only_hint: None,
result: None, result: None,
@@ -5912,6 +5913,7 @@ async fn mcp_elicitation_classification_survives_turn_completion_and_preserves_c
action_name: None, action_name: None,
}), }),
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: None, read_only_hint: None,
result: None, result: None,
+1
View File
@@ -3869,6 +3869,7 @@ mod tests {
arguments: serde_json::json!({}), arguments: serde_json::json!({}),
app_context: None, app_context: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: None, read_only_hint: None,
result: None, result: None,
@@ -2693,6 +2693,29 @@
"LegacyAppPathString": { "LegacyAppPathString": {
"type": "string" "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": { "McpServerEventNotification": {
"properties": { "properties": {
"method": { "method": {
@@ -4917,12 +4940,23 @@
"type": "string" "type": "string"
}, },
"mcpAppResourceUri": { "mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.", "description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": { "pluginId": {
"type": [ "type": [
"string", "string",
@@ -14410,6 +14410,29 @@
"title": "MarketplaceUpgradeResponse", "title": "MarketplaceUpgradeResponse",
"type": "object" "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": { "McpAuthStatus": {
"enum": [ "enum": [
"unknown", "unknown",
@@ -21826,12 +21849,23 @@
"type": "string" "type": "string"
}, },
"mcpAppResourceUri": { "mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.", "description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/v2/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": { "pluginId": {
"type": [ "type": [
"string", "string",
@@ -10462,6 +10462,29 @@
"title": "MarketplaceUpgradeResponse", "title": "MarketplaceUpgradeResponse",
"type": "object" "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": { "McpAuthStatus": {
"enum": [ "enum": [
"unknown", "unknown",
@@ -19540,12 +19563,23 @@
"type": "string" "type": "string"
}, },
"mcpAppResourceUri": { "mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.", "description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": { "pluginId": {
"type": [ "type": [
"string", "string",
@@ -510,6 +510,29 @@
"LegacyAppPathString": { "LegacyAppPathString": {
"type": "string" "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": { "McpToolCallAppContext": {
"properties": { "properties": {
"actionName": { "actionName": {
@@ -1137,12 +1160,23 @@
"type": "string" "type": "string"
}, },
"mcpAppResourceUri": { "mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.", "description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": { "pluginId": {
"type": [ "type": [
"string", "string",
@@ -510,6 +510,29 @@
"LegacyAppPathString": { "LegacyAppPathString": {
"type": "string" "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": { "McpToolCallAppContext": {
"properties": { "properties": {
"actionName": { "actionName": {
@@ -1137,12 +1160,23 @@
"type": "string" "type": "string"
}, },
"mcpAppResourceUri": { "mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.", "description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": { "pluginId": {
"type": [ "type": [
"string", "string",
@@ -650,6 +650,29 @@
"LegacyAppPathString": { "LegacyAppPathString": {
"type": "string" "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": { "McpToolCallAppContext": {
"properties": { "properties": {
"actionName": { "actionName": {
@@ -1325,12 +1348,23 @@
"type": "string" "type": "string"
}, },
"mcpAppResourceUri": { "mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.", "description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": { "pluginId": {
"type": [ "type": [
"string", "string",
@@ -754,6 +754,29 @@
"LegacyAppPathString": { "LegacyAppPathString": {
"type": "string" "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": { "McpToolCallAppContext": {
"properties": { "properties": {
"actionName": { "actionName": {
@@ -1939,12 +1962,23 @@
"type": "string" "type": "string"
}, },
"mcpAppResourceUri": { "mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.", "description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": { "pluginId": {
"type": [ "type": [
"string", "string",
@@ -510,6 +510,29 @@
"LegacyAppPathString": { "LegacyAppPathString": {
"type": "string" "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": { "McpToolCallAppContext": {
"properties": { "properties": {
"actionName": { "actionName": {
@@ -1137,12 +1160,23 @@
"type": "string" "type": "string"
}, },
"mcpAppResourceUri": { "mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.", "description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": { "pluginId": {
"type": [ "type": [
"string", "string",
@@ -676,6 +676,29 @@
"LegacyAppPathString": { "LegacyAppPathString": {
"type": "string" "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": { "McpToolCallAppContext": {
"properties": { "properties": {
"actionName": { "actionName": {
@@ -1730,12 +1753,23 @@
"type": "string" "type": "string"
}, },
"mcpAppResourceUri": { "mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.", "description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": { "pluginId": {
"type": [ "type": [
"string", "string",
@@ -676,6 +676,29 @@
"LegacyAppPathString": { "LegacyAppPathString": {
"type": "string" "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": { "McpToolCallAppContext": {
"properties": { "properties": {
"actionName": { "actionName": {
@@ -1730,12 +1753,23 @@
"type": "string" "type": "string"
}, },
"mcpAppResourceUri": { "mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.", "description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": { "pluginId": {
"type": [ "type": [
"string", "string",
@@ -676,6 +676,29 @@
"LegacyAppPathString": { "LegacyAppPathString": {
"type": "string" "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": { "McpToolCallAppContext": {
"properties": { "properties": {
"actionName": { "actionName": {
@@ -1730,12 +1753,23 @@
"type": "string" "type": "string"
}, },
"mcpAppResourceUri": { "mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.", "description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": { "pluginId": {
"type": [ "type": [
"string", "string",
@@ -770,6 +770,29 @@
"LegacyAppPathString": { "LegacyAppPathString": {
"type": "string" "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": { "McpToolCallAppContext": {
"properties": { "properties": {
"actionName": { "actionName": {
@@ -1991,12 +2014,23 @@
"type": "string" "type": "string"
}, },
"mcpAppResourceUri": { "mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.", "description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": { "pluginId": {
"type": [ "type": [
"string", "string",
@@ -676,6 +676,29 @@
"LegacyAppPathString": { "LegacyAppPathString": {
"type": "string" "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": { "McpToolCallAppContext": {
"properties": { "properties": {
"actionName": { "actionName": {
@@ -1730,12 +1753,23 @@
"type": "string" "type": "string"
}, },
"mcpAppResourceUri": { "mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.", "description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": { "pluginId": {
"type": [ "type": [
"string", "string",
@@ -754,6 +754,29 @@
"LegacyAppPathString": { "LegacyAppPathString": {
"type": "string" "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": { "McpToolCallAppContext": {
"properties": { "properties": {
"actionName": { "actionName": {
@@ -1939,12 +1962,23 @@
"type": "string" "type": "string"
}, },
"mcpAppResourceUri": { "mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.", "description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": { "pluginId": {
"type": [ "type": [
"string", "string",
@@ -676,6 +676,29 @@
"LegacyAppPathString": { "LegacyAppPathString": {
"type": "string" "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": { "McpToolCallAppContext": {
"properties": { "properties": {
"actionName": { "actionName": {
@@ -1730,12 +1753,23 @@
"type": "string" "type": "string"
}, },
"mcpAppResourceUri": { "mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.", "description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": { "pluginId": {
"type": [ "type": [
"string", "string",
@@ -650,6 +650,29 @@
"LegacyAppPathString": { "LegacyAppPathString": {
"type": "string" "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": { "McpToolCallAppContext": {
"properties": { "properties": {
"actionName": { "actionName": {
@@ -1325,12 +1348,23 @@
"type": "string" "type": "string"
}, },
"mcpAppResourceUri": { "mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.", "description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": { "pluginId": {
"type": [ "type": [
"string", "string",
@@ -676,6 +676,29 @@
"LegacyAppPathString": { "LegacyAppPathString": {
"type": "string" "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": { "McpToolCallAppContext": {
"properties": { "properties": {
"actionName": { "actionName": {
@@ -1730,12 +1753,23 @@
"type": "string" "type": "string"
}, },
"mcpAppResourceUri": { "mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.", "description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": { "pluginId": {
"type": [ "type": [
"string", "string",
@@ -650,6 +650,29 @@
"LegacyAppPathString": { "LegacyAppPathString": {
"type": "string" "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": { "McpToolCallAppContext": {
"properties": { "properties": {
"actionName": { "actionName": {
@@ -1325,12 +1348,23 @@
"type": "string" "type": "string"
}, },
"mcpAppResourceUri": { "mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.", "description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": { "pluginId": {
"type": [ "type": [
"string", "string",
@@ -650,6 +650,29 @@
"LegacyAppPathString": { "LegacyAppPathString": {
"type": "string" "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": { "McpToolCallAppContext": {
"properties": { "properties": {
"actionName": { "actionName": {
@@ -1325,12 +1348,23 @@
"type": "string" "type": "string"
}, },
"mcpAppResourceUri": { "mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.", "description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": { "pluginId": {
"type": [ "type": [
"string", "string",
@@ -650,6 +650,29 @@
"LegacyAppPathString": { "LegacyAppPathString": {
"type": "string" "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": { "McpToolCallAppContext": {
"properties": { "properties": {
"actionName": { "actionName": {
@@ -1325,12 +1348,23 @@
"type": "string" "type": "string"
}, },
"mcpAppResourceUri": { "mcpAppResourceUri": {
"description": "Deprecated: use `appContext.resourceUri` instead.", "description": "Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"mcpAppUi": {
"anyOf": [
{
"$ref": "#/definitions/McpAppUi"
},
{
"type": "null"
}
],
"description": "Presentation captured from the invoked descriptor; absent in older history."
},
"pluginId": { "pluginId": {
"type": [ "type": [
"string", "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 { DynamicToolCallStatus } from "./DynamicToolCallStatus";
import type { FileUpdateChange } from "./FileUpdateChange"; import type { FileUpdateChange } from "./FileUpdateChange";
import type { HookPromptFragment } from "./HookPromptFragment"; import type { HookPromptFragment } from "./HookPromptFragment";
import type { McpAppUi } from "./McpAppUi";
import type { McpToolCallAppContext } from "./McpToolCallAppContext"; import type { McpToolCallAppContext } from "./McpToolCallAppContext";
import type { McpToolCallError } from "./McpToolCallError"; import type { McpToolCallError } from "./McpToolCallError";
import type { McpToolCallResult } from "./McpToolCallResult"; 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, 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. * 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 { MarketplaceUpgradeErrorInfo } from "./MarketplaceUpgradeErrorInfo";
export type { MarketplaceUpgradeParams } from "./MarketplaceUpgradeParams"; export type { MarketplaceUpgradeParams } from "./MarketplaceUpgradeParams";
export type { MarketplaceUpgradeResponse } from "./MarketplaceUpgradeResponse"; export type { MarketplaceUpgradeResponse } from "./MarketplaceUpgradeResponse";
export type { McpAppDisplayMode } from "./McpAppDisplayMode";
export type { McpAppUi } from "./McpAppUi";
export type { McpAuthStatus } from "./McpAuthStatus"; export type { McpAuthStatus } from "./McpAuthStatus";
export type { McpElicitationArrayType } from "./McpElicitationArrayType"; export type { McpElicitationArrayType } from "./McpElicitationArrayType";
export type { McpElicitationBooleanSchema } from "./McpElicitationBooleanSchema"; export type { McpElicitationBooleanSchema } from "./McpElicitationBooleanSchema";
@@ -784,6 +784,7 @@ impl ThreadHistoryBuilder {
action_name: payload.action_name.clone(), action_name: payload.action_name.clone(),
}), }),
mcp_app_resource_uri: payload.mcp_app_resource_uri.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(), plugin_id: payload.plugin_id.clone(),
read_only_hint: payload.read_only_hint, read_only_hint: payload.read_only_hint,
result: None, result: None,
@@ -841,6 +842,7 @@ impl ThreadHistoryBuilder {
action_name: payload.action_name.clone(), action_name: payload.action_name.clone(),
}), }),
mcp_app_resource_uri: payload.mcp_app_resource_uri.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(), plugin_id: payload.plugin_id.clone(),
read_only_hint: payload.read_only_hint, read_only_hint: payload.read_only_hint,
result, result,
@@ -3118,6 +3120,7 @@ mod tests {
}, },
connector_id: None, connector_id: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
link_id: None, link_id: None,
app_name: None, app_name: None,
action_name: None, action_name: None,
@@ -3181,6 +3184,7 @@ mod tests {
arguments: serde_json::json!({"id":"123"}), arguments: serde_json::json!({"id":"123"}),
app_context: None, app_context: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: None, read_only_hint: None,
result: None, result: None,
@@ -3213,6 +3217,7 @@ mod tests {
}, },
connector_id: Some("calendar".into()), connector_id: Some("calendar".into()),
mcp_app_resource_uri: Some("ui://widget/lookup.html".into()), mcp_app_resource_uri: Some("ui://widget/lookup.html".into()),
mcp_app_ui: None,
link_id: Some("link_calendar".into()), link_id: Some("link_calendar".into()),
app_name: Some("Calendar".into()), app_name: Some("Calendar".into()),
action_name: Some("lookup".into()), action_name: Some("lookup".into()),
@@ -3255,6 +3260,7 @@ mod tests {
action_name: Some("lookup".into()), action_name: Some("lookup".into()),
}), }),
mcp_app_resource_uri: Some("ui://widget/lookup.html".into()), mcp_app_resource_uri: Some("ui://widget/lookup.html".into()),
mcp_app_ui: None,
plugin_id: Some("sample@test".into()), plugin_id: Some("sample@test".into()),
read_only_hint: Some(false), read_only_hint: Some(false),
result: Some(Box::new(McpToolCallResult { result: Some(Box::new(McpToolCallResult {
@@ -3646,6 +3652,7 @@ mod tests {
arguments: serde_json::json!({"query": "incident"}), arguments: serde_json::json!({"query": "incident"}),
connector_id: None, connector_id: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
link_id: None, link_id: None,
app_name: None, app_name: None,
action_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::CollabAgentToolCallStatus as CoreCollabAgentToolCallStatus;
use codex_protocol::items::CommandExecutionStatus as CoreCommandExecutionStatus; use codex_protocol::items::CommandExecutionStatus as CoreCommandExecutionStatus;
use codex_protocol::items::DynamicToolCallStatus as CoreDynamicToolCallStatus; 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::McpToolCallStatus as CoreMcpToolCallStatus;
use codex_protocol::items::TurnItem as CoreTurnItem; use codex_protocol::items::TurnItem as CoreTurnItem;
use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation; use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation;
@@ -337,8 +339,10 @@ pub enum ThreadItem {
app_context: Option<McpToolCallAppContext>, app_context: Option<McpToolCallAppContext>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)] #[ts(optional)]
/// Deprecated: use `appContext.resourceUri` instead. /// Legacy compatibility field; prefer `mcpAppUi.resourceUri` when available.
mcp_app_resource_uri: Option<String>, 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>, plugin_id: Option<String>,
read_only_hint: Option<bool>, read_only_hint: Option<bool>,
result: Option<Box<McpToolCallResult>>, result: Option<Box<McpToolCallResult>>,
@@ -1038,6 +1042,7 @@ impl From<CoreTurnItem> for ThreadItem {
action_name: mcp.action_name, action_name: mcp.action_name,
}), }),
mcp_app_resource_uri: mcp.mcp_app_resource_uri, mcp_app_resource_uri: mcp.mcp_app_resource_uri,
mcp_app_ui: mcp.mcp_app_ui,
plugin_id: mcp.plugin_id, plugin_id: mcp.plugin_id,
read_only_hint: mcp.read_only_hint, read_only_hint: mcp.read_only_hint,
result: mcp.result.map(McpToolCallResult::from).map(Box::new), 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"}), arguments: json!({"arg": "value"}),
connector_id: Some("calendar".to_string()), connector_id: Some("calendar".to_string()),
mcp_app_resource_uri: Some("app://connector".to_string()), mcp_app_resource_uri: Some("app://connector".to_string()),
mcp_app_ui: None,
link_id: Some("link_calendar".to_string()), link_id: Some("link_calendar".to_string()),
app_name: Some("Calendar".to_string()), app_name: Some("Calendar".to_string()),
action_name: Some("create_event".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()), action_name: Some("create_event".to_string()),
}), }),
mcp_app_resource_uri: Some("app://connector".to_string()), mcp_app_resource_uri: Some("app://connector".to_string()),
mcp_app_ui: None,
plugin_id: Some("sample@test".to_string()), plugin_id: Some("sample@test".to_string()),
read_only_hint: Some(true), read_only_hint: Some(true),
result: None, result: None,
@@ -3505,6 +3507,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
arguments: JsonValue::Null, arguments: JsonValue::Null,
connector_id: None, connector_id: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
link_id: None, link_id: None,
app_name: None, app_name: None,
action_name: None, action_name: None,
@@ -3531,6 +3534,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
arguments: JsonValue::Null, arguments: JsonValue::Null,
app_context: None, app_context: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: Some(false), read_only_hint: Some(false),
result: Some(Box::new(McpToolCallResult { 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()), action_name: Some("create_event".to_string()),
}), }),
mcp_app_resource_uri: Some("app://connector".to_string()), mcp_app_resource_uri: Some("app://connector".to_string()),
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: Some(false), read_only_hint: Some(false),
result: None, result: None,
@@ -3584,6 +3589,7 @@ fn mcp_tool_call_app_context_serializes_connector_id() {
"actionName": "create_event", "actionName": "create_event",
}, },
"mcpAppResourceUri": "app://connector", "mcpAppResourceUri": "app://connector",
"mcpAppUi": null,
"pluginId": null, "pluginId": null,
"readOnlyHint": false, "readOnlyHint": false,
"result": null, "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) # Initial Daybreak choice (experimental)
Persistent threads accept `daybreakEnabled` on `thread/start` with the Persistent threads accept `daybreakEnabled` on `thread/start` with the
@@ -90,6 +90,7 @@ mod tests {
action_name: Some("lookup".to_string()), action_name: Some("lookup".to_string()),
}), }),
mcp_app_resource_uri: Some("ui://widget/lookup.html".to_string()), mcp_app_resource_uri: Some("ui://widget/lookup.html".to_string()),
mcp_app_ui: None,
plugin_id: Some("sample@test".to_string()), plugin_id: Some("sample@test".to_string()),
read_only_hint: None, read_only_hint: None,
result: Some(Box::new(McpToolCallResult { result: Some(Box::new(McpToolCallResult {
@@ -146,6 +147,7 @@ mod tests {
action_name: Some("lookup".to_string()), action_name: Some("lookup".to_string()),
}), }),
mcp_app_resource_uri: Some("ui://widget/lookup.html".to_string()), mcp_app_resource_uri: Some("ui://widget/lookup.html".to_string()),
mcp_app_ui: None,
plugin_id: Some("sample@test".to_string()), plugin_id: Some("sample@test".to_string()),
read_only_hint: None, read_only_hint: None,
result: Some(Box::new(redacted_mcp_tool_call_result())), result: Some(Box::new(redacted_mcp_tool_call_result())),
@@ -165,6 +167,7 @@ mod tests {
arguments: serde_json::json!({"secret":"argument"}), arguments: serde_json::json!({"secret":"argument"}),
app_context: None, app_context: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: None, read_only_hint: None,
result: None, result: None,
@@ -186,6 +189,7 @@ mod tests {
arguments: JsonValue::String(REDACTED_PAYLOAD.to_string()), arguments: JsonValue::String(REDACTED_PAYLOAD.to_string()),
app_context: None, app_context: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: None, read_only_hint: None,
result: None, result: None,
@@ -1087,6 +1087,7 @@ async fn mcp_tool_call_completion_notification_contains_truncated_large_result()
arguments: json!({ "message": LARGE_RESPONSE_MESSAGE }), arguments: json!({ "message": LARGE_RESPONSE_MESSAGE }),
app_context: None, app_context: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: None, read_only_hint: None,
result: Some(result), 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 }), arguments: json!({ "message": ELICITATION_TRIGGER_MESSAGE }),
app_context: None, app_context: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: Some(true), read_only_hint: Some(true),
result: None, result: None,
@@ -2770,6 +2770,7 @@ fn append_resume_redaction_history(
}, },
connector_id: Some("calendar".to_string()), connector_id: Some("calendar".to_string()),
mcp_app_resource_uri: Some("ui://widget/lookup.html".to_string()), mcp_app_resource_uri: Some("ui://widget/lookup.html".to_string()),
mcp_app_ui: None,
link_id: Some("link_calendar".to_string()), link_id: Some("link_calendar".to_string()),
app_name: Some("Calendar".to_string()), app_name: Some("Calendar".to_string()),
action_name: Some("lookup".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::is_connector_auth_failure_from_tool_result;
use codex_mcp::mcp_permission_prompt_is_auto_approved; use codex_mcp::mcp_permission_prompt_is_auto_approved;
use codex_protocol::approvals::ElicitationRequest; use codex_protocol::approvals::ElicitationRequest;
use codex_protocol::items::McpAppDisplayMode;
use codex_protocol::items::McpAppUi;
use codex_protocol::items::McpToolCallError; use codex_protocol::items::McpToolCallError;
use codex_protocol::items::McpToolCallItem; use codex_protocol::items::McpToolCallItem;
use codex_protocol::items::McpToolCallStatus; use codex_protocol::items::McpToolCallStatus;
@@ -390,7 +392,7 @@ pub(crate) struct HandledMcpToolCall {
struct McpToolCallItemMetadata { struct McpToolCallItemMetadata {
connector_id: Option<String>, connector_id: Option<String>,
link_id: Option<String>, link_id: Option<String>,
mcp_app_resource_uri: Option<String>, mcp_app_ui: Option<McpAppUi>,
app_name: Option<String>, app_name: Option<String>,
action_name: Option<String>, action_name: Option<String>,
plugin_id: Option<String>, plugin_id: Option<String>,
@@ -408,8 +410,7 @@ impl McpToolCallItemMetadata {
connector_id: trusted_mcp_app_metadata connector_id: trusted_mcp_app_metadata
.and_then(|metadata| metadata.connector_id.clone()), .and_then(|metadata| metadata.connector_id.clone()),
link_id: trusted_mcp_app_metadata.and_then(|metadata| metadata.link_id.clone()), link_id: trusted_mcp_app_metadata.and_then(|metadata| metadata.link_id.clone()),
mcp_app_resource_uri: metadata mcp_app_ui: metadata.and_then(|metadata| metadata.mcp_app_ui.clone()),
.and_then(|metadata| metadata.mcp_app_resource_uri.clone()),
app_name: trusted_mcp_app_metadata.and_then(|metadata| metadata.connector_name.clone()), app_name: trusted_mcp_app_metadata.and_then(|metadata| metadata.connector_name.clone()),
action_name: trusted_mcp_app_metadata action_name: trusted_mcp_app_metadata
.and_then(|metadata| metadata.codex_apps_meta.as_ref()) .and_then(|metadata| metadata.codex_apps_meta.as_ref())
@@ -1004,7 +1005,11 @@ async fn notify_mcp_tool_call_started(
tool, tool,
arguments: arguments.unwrap_or(JsonValue::Null), arguments: arguments.unwrap_or(JsonValue::Null),
connector_id: item_metadata.connector_id, 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, link_id: item_metadata.link_id,
app_name: item_metadata.app_name, app_name: item_metadata.app_name,
action_name: item_metadata.action_name, action_name: item_metadata.action_name,
@@ -1049,7 +1054,11 @@ async fn notify_mcp_tool_call_completed(
tool, tool,
arguments: arguments.unwrap_or(JsonValue::Null), arguments: arguments.unwrap_or(JsonValue::Null),
connector_id: item_metadata.connector_id, 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, link_id: item_metadata.link_id,
app_name: item_metadata.app_name, app_name: item_metadata.app_name,
action_name: item_metadata.action_name, action_name: item_metadata.action_name,
@@ -1171,7 +1180,7 @@ pub(crate) struct McpToolApprovalMetadata {
plugin_id: Option<String>, plugin_id: Option<String>,
tool_title: Option<String>, tool_title: Option<String>,
tool_description: 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>>, codex_apps_meta: Option<serde_json::Map<String, serde_json::Value>>,
openai_file_input_optional_fields: Option<HashMap<String, Vec<String>>>, 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, plugin_id: None,
tool_title: tool_title.clone(), tool_title: tool_title.clone(),
tool_description: tool_description.clone(), tool_description: tool_description.clone(),
mcp_app_resource_uri: None, mcp_app_ui: None,
codex_apps_meta: None, codex_apps_meta: None,
openai_file_input_optional_fields: None, openai_file_input_optional_fields: None,
}; };
@@ -1783,6 +1792,24 @@ fn mcp_tool_metadata(
None 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 { Ok(McpToolApprovalMetadata {
annotations: tool_info.tool.annotations, annotations: tool_info.tool.annotations,
connector_id: tool_info.connector_id, connector_id: tool_info.connector_id,
@@ -1793,7 +1820,7 @@ fn mcp_tool_metadata(
plugin_id: plugin_id.map(str::to_string), plugin_id: plugin_id.map(str::to_string),
tool_title: tool_info.tool.title, tool_title: tool_info.tool.title,
tool_description: tool_info.tool.description.map(std::borrow::Cow::into_owned), 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, codex_apps_meta,
// Disallow custom MCPs from uploading files via fileParams. // Disallow custom MCPs from uploading files via fileParams.
openai_file_input_optional_fields: openai_file_input_optional_fields_for_server( 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, plugin_id: None,
tool_title: tool_title.map(str::to_string), tool_title: tool_title.map(str::to_string),
tool_description: tool_description.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, codex_apps_meta: None,
openai_file_input_optional_fields: None, openai_file_input_optional_fields: None,
} }
@@ -1436,7 +1436,7 @@ fn mcp_tool_call_item_metadata_only_trusts_codex_apps_identity() {
McpToolCallItemMetadata { McpToolCallItemMetadata {
connector_id: Some("asdk_app_0123456789abcdef0123456789abcdef".to_string()), connector_id: Some("asdk_app_0123456789abcdef0123456789abcdef".to_string()),
link_id: Some("link_fedcba9876543210fedcba9876543210".to_string()), link_id: Some("link_fedcba9876543210fedcba9876543210".to_string()),
mcp_app_resource_uri: None, mcp_app_ui: None,
app_name: Some("Calendar".to_string()), app_name: Some("Calendar".to_string()),
action_name: Some("create_event".to_string()), action_name: Some("create_event".to_string()),
plugin_id: None, plugin_id: None,
@@ -1448,7 +1448,7 @@ fn mcp_tool_call_item_metadata_only_trusts_codex_apps_identity() {
McpToolCallItemMetadata { McpToolCallItemMetadata {
connector_id: None, connector_id: None,
link_id: None, link_id: None,
mcp_app_resource_uri: None, mcp_app_ui: None,
app_name: None, app_name: None,
action_name: None, action_name: None,
plugin_id: None, plugin_id: None,
@@ -1473,7 +1473,7 @@ async fn mcp_tool_call_item_includes_app_identity() {
McpToolCallItemMetadata { McpToolCallItemMetadata {
connector_id: Some("asdk_app_0123456789abcdef0123456789abcdef".to_string()), connector_id: Some("asdk_app_0123456789abcdef0123456789abcdef".to_string()),
link_id: Some("link_fedcba9876543210fedcba9876543210".to_string()), link_id: Some("link_fedcba9876543210fedcba9876543210".to_string()),
mcp_app_resource_uri: None, mcp_app_ui: None,
app_name: Some("Calendar".to_string()), app_name: Some("Calendar".to_string()),
action_name: Some("create_event".to_string()), action_name: Some("create_event".to_string()),
plugin_id: Some("sample@test".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, plugin_id: None,
tool_title: Some("Create Event".to_string()), tool_title: Some("Create Event".to_string()),
tool_description: Some("Create a calendar 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( codex_apps_meta: Some(
serde_json::json!({ serde_json::json!({
"resource_uri": "connector://calendar/tools/calendar_create_event", "resource_uri": "connector://calendar/tools/calendar_create_event",
@@ -1974,7 +1974,7 @@ fn guardian_mcp_review_request_includes_annotations_when_present() {
plugin_id: None, plugin_id: None,
tool_title: None, tool_title: None,
tool_description: None, tool_description: None,
mcp_app_resource_uri: None, mcp_app_ui: None,
codex_apps_meta: None, codex_apps_meta: None,
openai_file_input_optional_fields: 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, plugin_id: None,
tool_title: Some("Read Only Tool".to_string()), tool_title: Some("Read Only Tool".to_string()),
tool_description: None, tool_description: None,
mcp_app_resource_uri: None, mcp_app_ui: None,
codex_apps_meta: None, codex_apps_meta: None,
openai_file_input_optional_fields: 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, plugin_id: None,
tool_title: Some("Read Only Tool".to_string()), tool_title: Some("Read Only Tool".to_string()),
tool_description: None, tool_description: None,
mcp_app_resource_uri: None, mcp_app_ui: None,
codex_apps_meta: None, codex_apps_meta: None,
openai_file_input_optional_fields: None, openai_file_input_optional_fields: None,
}; };
@@ -2882,7 +2882,7 @@ async fn permission_request_hook_allows_mcp_tool_call() {
plugin_id: None, plugin_id: None,
tool_title: Some("Create entities".to_string()), tool_title: Some("Create entities".to_string()),
tool_description: None, tool_description: None,
mcp_app_resource_uri: None, mcp_app_ui: None,
codex_apps_meta: None, codex_apps_meta: None,
openai_file_input_optional_fields: None, openai_file_input_optional_fields: None,
}; };
@@ -3033,7 +3033,7 @@ async fn permission_request_hook_runs_after_remembered_mcp_approval() {
plugin_id: None, plugin_id: None,
tool_title: Some("Create entities".to_string()), tool_title: Some("Create entities".to_string()),
tool_description: None, tool_description: None,
mcp_app_resource_uri: None, mcp_app_ui: None,
codex_apps_meta: None, codex_apps_meta: None,
openai_file_input_optional_fields: 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, plugin_id: None,
tool_title: Some("Dangerous Tool".to_string()), tool_title: Some("Dangerous Tool".to_string()),
tool_description: Some("Reads calendar data.".to_string()), tool_description: Some("Reads calendar data.".to_string()),
mcp_app_resource_uri: None, mcp_app_ui: None,
codex_apps_meta: None, codex_apps_meta: None,
openai_file_input_optional_fields: None, openai_file_input_optional_fields: None,
}; };
@@ -3213,7 +3213,7 @@ async fn assert_mcp_user_approval_persistence(
plugin_id: None, plugin_id: None,
tool_title: Some("Create entities".to_string()), tool_title: Some("Create entities".to_string()),
tool_description: None, tool_description: None,
mcp_app_resource_uri: None, mcp_app_ui: None,
codex_apps_meta: None, codex_apps_meta: None,
openai_file_input_optional_fields: 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, plugin_id: None,
tool_title: Some("Read Only Tool".to_string()), tool_title: Some("Read Only Tool".to_string()),
tool_description: None, tool_description: None,
mcp_app_resource_uri: None, mcp_app_ui: None,
codex_apps_meta: None, codex_apps_meta: None,
openai_file_input_optional_fields: 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, plugin_id: None,
tool_title: Some("Dangerous Tool".to_string()), tool_title: Some("Dangerous Tool".to_string()),
tool_description: Some("Performs a risky action.".to_string()), tool_description: Some("Performs a risky action.".to_string()),
mcp_app_resource_uri: None, mcp_app_ui: None,
codex_apps_meta: None, codex_apps_meta: None,
openai_file_input_optional_fields: None, openai_file_input_optional_fields: None,
}; };
@@ -3423,7 +3423,7 @@ async fn approve_mode_skips_guardian_in_every_permission_mode() {
plugin_id: None, plugin_id: None,
tool_title: Some("Dangerous Tool".to_string()), tool_title: Some("Dangerous Tool".to_string()),
tool_description: Some("Performs a risky action.".to_string()), tool_description: Some("Performs a risky action.".to_string()),
mcp_app_resource_uri: None, mcp_app_ui: None,
codex_apps_meta: None, codex_apps_meta: None,
openai_file_input_optional_fields: 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, arguments: serde_json::Value::Null,
connector_id: None, connector_id: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
link_id: None, link_id: None,
app_name: None, app_name: None,
action_name: None, action_name: None,
@@ -222,6 +222,7 @@ async fn emit_tool_call_begin(
arguments: arguments.unwrap_or(Value::Null), arguments: arguments.unwrap_or(Value::Null),
connector_id: None, connector_id: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
link_id: None, link_id: None,
app_name: None, app_name: None,
action_name: None, action_name: None,
@@ -266,6 +267,7 @@ async fn emit_tool_call_end(
arguments: arguments.unwrap_or(Value::Null), arguments: arguments.unwrap_or(Value::Null),
connector_id: None, connector_id: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
link_id: None, link_id: None,
app_name: None, app_name: None,
action_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::CollaborationMode;
use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings; 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::McpToolCallStatus;
use codex_protocol::items::TurnItem; use codex_protocol::items::TurnItem;
use codex_protocol::models::NetworkPermissions; use codex_protocol::models::NetworkPermissions;
@@ -23,6 +25,8 @@ use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::ElicitationAction; use codex_protocol::protocol::ElicitationAction;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ItemCompletedEvent;
use codex_protocol::protocol::ItemStartedEvent;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadSettingsOverrides; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::TurnEnvironmentSelections; use codex_protocol::protocol::TurnEnvironmentSelections;
@@ -71,6 +75,142 @@ use wiremock::matchers::body_partial_json;
use wiremock::matchers::method; use wiremock::matchers::method;
use wiremock::matchers::path_regex; 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn auto_session_approval_is_scoped_to_tool_link_id() -> Result<()> { async fn auto_session_approval_is_scoped_to_tool_link_id() -> Result<()> {
skip_if_no_network!(Ok(())); skip_if_no_network!(Ok(()));
+1
View File
@@ -2529,6 +2529,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> {
}, },
connector_id: None, connector_id: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
link_id: None, link_id: None,
app_name: None, app_name: None,
action_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"}]}), arguments: json!({"search_query": [{"q": "OpenAI Codex CLI documentation"}]}),
app_context: None, app_context: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: None, read_only_hint: None,
result: Some(Box::new(codex_app_server_protocol::McpToolCallResult { 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" }), arguments: json!({ "key": "value" }),
app_context: None, app_context: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: None, read_only_hint: None,
result: None, result: None,
@@ -512,6 +513,7 @@ fn mcp_tool_call_begin_and_end_emit_item_events() {
arguments: json!({ "key": "value" }), arguments: json!({ "key": "value" }),
app_context: None, app_context: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: None, read_only_hint: None,
result: Some(Box::new(McpToolCallResult { result: Some(Box::new(McpToolCallResult {
@@ -586,6 +588,7 @@ fn mcp_tool_call_failure_sets_failed_status() {
arguments: json!({ "param": 42 }), arguments: json!({ "param": 42 }),
app_context: None, app_context: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: None, read_only_hint: None,
result: None, result: None,
@@ -637,6 +640,7 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() {
arguments: serde_json::Value::Null, arguments: serde_json::Value::Null,
app_context: None, app_context: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: None, read_only_hint: None,
result: None, result: None,
@@ -657,6 +661,7 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() {
arguments: serde_json::Value::Null, arguments: serde_json::Value::Null,
app_context: None, app_context: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: None, read_only_hint: None,
result: Some(Box::new(McpToolCallResult { result: Some(Box::new(McpToolCallResult {
+23
View File
@@ -425,6 +425,25 @@ pub struct FileChangeItem {
pub stderr: Option<String>, 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)] #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")] #[ts(rename_all = "camelCase")]
@@ -436,11 +455,15 @@ pub struct McpToolCallItem {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)] #[ts(optional)]
pub connector_id: Option<String>, pub connector_id: Option<String>,
/// Legacy compatibility field; prefer `mcp_app_ui.resource_uri` when available.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)] #[ts(optional)]
pub mcp_app_resource_uri: Option<String>, pub mcp_app_resource_uri: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)] #[ts(optional)]
pub mcp_app_ui: Option<McpAppUi>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub link_id: Option<String>, pub link_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)] #[ts(optional)]
+2
View File
@@ -508,6 +508,7 @@ impl McpToolCallItem {
}, },
connector_id: self.connector_id.clone(), connector_id: self.connector_id.clone(),
mcp_app_resource_uri: self.mcp_app_resource_uri.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(), link_id: self.link_id.clone(),
app_name: self.app_name.clone(), app_name: self.app_name.clone(),
action_name: self.action_name.clone(), action_name: self.action_name.clone(),
@@ -532,6 +533,7 @@ impl McpToolCallItem {
arguments: (!self.arguments.is_null()).then(|| self.arguments.clone()), arguments: (!self.arguments.is_null()).then(|| self.arguments.clone()),
}, },
mcp_app_resource_uri: self.mcp_app_resource_uri.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(), connector_id: self.connector_id.clone(),
link_id: self.link_id.clone(), link_id: self.link_id.clone(),
app_name: self.app_name.clone(), app_name: self.app_name.clone(),
+8
View File
@@ -2670,6 +2670,9 @@ pub struct McpToolCallBeginEvent {
pub mcp_app_resource_uri: Option<String>, pub mcp_app_resource_uri: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)] #[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>, pub link_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)] #[ts(optional)]
@@ -2702,6 +2705,9 @@ pub struct McpToolCallEndEvent {
pub mcp_app_resource_uri: Option<String>, pub mcp_app_resource_uri: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)] #[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>, pub link_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)] #[ts(optional)]
@@ -5392,6 +5398,7 @@ mod tests {
arguments: json!({"arg": "value"}), arguments: json!({"arg": "value"}),
connector_id: Some("connector".into()), connector_id: Some("connector".into()),
mcp_app_resource_uri: Some("app://connector".into()), mcp_app_resource_uri: Some("app://connector".into()),
mcp_app_ui: None,
link_id: Some("link_123".into()), link_id: Some("link_123".into()),
app_name: Some("Calendar".into()), app_name: Some("Calendar".into()),
action_name: Some("create_event".into()), action_name: Some("create_event".into()),
@@ -5513,6 +5520,7 @@ mod tests {
arguments: json!({"arg": "value"}), arguments: json!({"arg": "value"}),
connector_id: Some("connector".into()), connector_id: Some("connector".into()),
mcp_app_resource_uri: Some("app://connector".into()), mcp_app_resource_uri: Some("app://connector".into()),
mcp_app_ui: None,
link_id: Some("link_123".into()), link_id: Some("link_123".into()),
app_name: Some("Calendar".into()), app_name: Some("Calendar".into()),
action_name: Some("create_event".into()), action_name: Some("create_event".into()),
@@ -185,6 +185,7 @@ pub(super) fn completed_item(
.unwrap_or(serde_json::Value::Null), .unwrap_or(serde_json::Value::Null),
connector_id: event.connector_id.clone(), connector_id: event.connector_id.clone(),
mcp_app_resource_uri: event.mcp_app_resource_uri.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(), link_id: event.link_id.clone(),
app_name: event.app_name.clone(), app_name: event.app_name.clone(),
action_name: event.action_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!({}), arguments: serde_json::json!({}),
app_context: None, app_context: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: None, read_only_hint: None,
result: None, result: None,
@@ -12,6 +12,7 @@ fn computer_item(
arguments: json!({"title": format!("Inspect page {id}"), "code": "await cua.getState()"}), arguments: json!({"title": format!("Inspect page {id}"), "code": "await cua.getState()"}),
app_context: None, app_context: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: None, read_only_hint: None,
result: Some(Box::new(codex_app_server_protocol::McpToolCallResult { 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"}), arguments: json!({"action": "wait"}),
app_context: None, app_context: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: None, read_only_hint: None,
result: None, result: None,
@@ -1258,6 +1259,7 @@ async fn failed_repl_mcp_tool_call_preserves_status_and_result() {
arguments: json!({"title": "Inspect workspace"}), arguments: json!({"title": "Inspect workspace"}),
app_context: None, app_context: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: None, read_only_hint: None,
result: Some(Box::new(codex_app_server_protocol::McpToolCallResult { 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"}), arguments: json!({"action": "wait"}),
app_context: None, app_context: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: None, read_only_hint: None,
result: None, result: None,
@@ -1343,6 +1346,7 @@ async fn deferred_mcp_lifecycle_events_keep_fifo_after_stream_finishes() {
arguments: json!({"action": "wait"}), arguments: json!({"action": "wait"}),
app_context: None, app_context: None,
mcp_app_resource_uri: None, mcp_app_resource_uri: None,
mcp_app_ui: None,
plugin_id: None, plugin_id: None,
read_only_hint: None, read_only_hint: None,
result: Some(Box::new(codex_app_server_protocol::McpToolCallResult { result: Some(Box::new(codex_app_server_protocol::McpToolCallResult {
@@ -2461,6 +2461,21 @@ class MarketplaceUpgradeResponse(BaseModel):
upgraded_roots: Annotated[list[AbsolutePathBuf], Field(alias="upgradedRoots")] 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): class McpAuthStatus(Enum):
unknown = "unknown" unknown = "unknown"
unsupported = "unsupported" unsupported = "unsupported"
@@ -5303,7 +5318,14 @@ class McpToolCallThreadItem(BaseModel):
str | None, str | None,
Field( Field(
alias="mcpAppResourceUri", 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 ] = None
plugin_id: Annotated[str | None, Field(alias="pluginId")] = None plugin_id: Annotated[str | None, Field(alias="pluginId")] = None