From 809ad310a983d0d332bb59217bed4abf3f0882fc Mon Sep 17 00:00:00 2001
From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com>
Date: Tue, 4 Aug 2026 04:40:28 -0700
Subject: [PATCH] fix(mcp): decode blob app HTML, sanitize object-shaped
ui_resources, use persisted toolArgs
Decode a base64 blob-embedded app resource as inline HTML in useAppBridge so blob apps render in
read-only views and skip the resources/read fallback (shared decodeBase64Utf8 helper reused by
fetchMCPResourceHtml).
Redact resultMeta from the object-shaped ({ data: [...] }) ui_resources attachment during share
serialization, not just the bare-array shape.
Prefer the persisted UIResource.toolArgs over the display tool-call args when auto-rendering an app
so it hydrates with the exact initial input.
---
.../Chat/Messages/Content/ToolCall.tsx | 4 +-
client/src/hooks/MCP/useAppBridge.ts | 17 ++++++--
client/src/utils/mcpApps.ts | 11 +++--
.../data-schemas/src/methods/share.test.ts | 40 +++++++++++++++++++
packages/data-schemas/src/methods/share.ts | 18 +++++++--
5 files changed, 78 insertions(+), 12 deletions(-)
diff --git a/client/src/components/Chat/Messages/Content/ToolCall.tsx b/client/src/components/Chat/Messages/Content/ToolCall.tsx
index a214a2dc3d..6763df3868 100644
--- a/client/src/components/Chat/Messages/Content/ToolCall.tsx
+++ b/client/src/components/Chat/Messages/Content/ToolCall.tsx
@@ -405,7 +405,9 @@ export default function ToolCall({
hi
' }); expect(resource).not.toHaveProperty('resultMeta'); }); + + test('strips resultMeta from object-shaped ui_resources ({ data: [...] })', async () => { + const userId = new mongoose.Types.ObjectId().toString(); + const conversationId = `conv_${nanoid()}`; + const shareId = `share_${nanoid()}`; + + const message = await Message.create({ + messageId: `msg_${nanoid()}`, + conversationId, + user: userId, + text: 'has app', + isCreatedByUser: false, + attachments: [ + { + type: 'ui_resources', + ui_resources: { + data: [ + { + resourceId: 'res1', + uri: 'ui://app', + mimeType: 'text/html;profile=mcp-app', + text: 'hi
', + resultMeta: { secret: 'hidden-from-model' }, + }, + ], + }, + }, + ], + }); + + await SharedLink.create({ shareId, conversationId, user: userId, messages: [message._id] }); + + const result = await shareMethods.getSharedMessages(shareId); + const attachment = result?.messages[0]?.attachments?.[0] as unknown as { + ui_resources?: { data?: Arrayhi
' }); + expect(resource).not.toHaveProperty('resultMeta'); + }); }); describe('getSharedLinks', () => { diff --git a/packages/data-schemas/src/methods/share.ts b/packages/data-schemas/src/methods/share.ts index a0dee47296..2781bd06e7 100644 --- a/packages/data-schemas/src/methods/share.ts +++ b/packages/data-schemas/src/methods/share.ts @@ -80,6 +80,19 @@ function sanitizeSharedUIResource(resource: unknown): unknown { return rest; } +/** ui_resources is stored either as a bare array or as the `{ data: UIResource[] }` artifact + * shape; redact resultMeta in both so it never reaches a shared transcript. */ +function sanitizeSharedUIResources(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(sanitizeSharedUIResource); + } + if (value && typeof value === 'object' && Array.isArray((value as { data?: unknown }).data)) { + const obj = value as { data: unknown[] }; + return { ...obj, data: obj.data.map(sanitizeSharedUIResource) }; + } + return value; +} + /** * Strip storage/identity-internal fields from a file or attachment while keeping * render-relevant data (including tool-call payloads keyed by tool name). @@ -94,10 +107,7 @@ function sanitizeSharedFile(value: unknown): t.SharedFile | null { if (SENSITIVE_SHARED_FILE_FIELDS.has(key)) { continue; } - result[key] = - key === 'ui_resources' && Array.isArray(fieldValue) - ? fieldValue.map(sanitizeSharedUIResource) - : fieldValue; + result[key] = key === 'ui_resources' ? sanitizeSharedUIResources(fieldValue) : fieldValue; } return Object.keys(result).length > 0 ? result : null;