fix(ui): preserve MCP App previews across reloads (#105728)

* fix(mcp): harden app preview lifecycle

* fix(ui): defer MCP app initialization payload

* test(mcp): align current policy expectations

* fix(ui): preserve MCP app previews

* fix(mcp): reject disabled app restoration

* fix(mcp): preserve reconstructed app state

* fix(mcp): bind reconstructable app data

* fix(ui): deduplicate MCP app preview copies

* style(ui): format MCP app preview test

* fix(mcp): satisfy metadata access lint

* style(mcp): format metadata access
This commit is contained in:
Jason (Json)
2026-07-12 17:30:34 -06:00
committed by GitHub
parent cca22d93f6
commit 1663c8801d
19 changed files with 1498 additions and 83 deletions
+19 -2
View File
@@ -14,10 +14,27 @@ describe("extractCanvasFromText", () => {
kind: "canvas",
view: { id: "cv_app" },
presentation: { target: "assistant_message", sandbox: "scripts" },
mcpApp: { viewId: "cv_app" },
mcpApp: {
viewId: "cv_app",
serverName: "demo",
toolName: "show",
uiResourceUri: "ui://demo/app",
toolCallId: "call-1",
resultMetaState: "unavailable",
},
},
}),
).toMatchObject({ viewId: "cv_app", mcpApp: { viewId: "cv_app" } });
).toMatchObject({
viewId: "cv_app",
mcpApp: {
viewId: "cv_app",
serverName: "demo",
toolName: "show",
uiResourceUri: "ui://demo/app",
toolCallId: "call-1",
resultMetaState: "unavailable",
},
});
});
it("keeps MCP App previews opaque while preserving model-visible results", () => {
+49 -5
View File
@@ -9,6 +9,15 @@ import { parseFenceSpans } from "../../packages/markdown-core/src/fences.js";
type CanvasSurface = "assistant_message";
type CanvasSandbox = "strict" | "scripts";
export type McpAppPreviewDescriptor = {
viewId: string;
serverName?: string;
toolName?: string;
uiResourceUri?: string;
toolCallId?: string;
resultMetaState?: "unavailable";
};
type CanvasPreview = {
kind: "canvas";
surface: CanvasSurface;
@@ -20,7 +29,7 @@ type CanvasPreview = {
className?: string;
style?: string;
sandbox?: CanvasSandbox;
mcpApp?: { viewId: string };
mcpApp?: McpAppPreviewDescriptor;
};
function getRecordStringField(
@@ -47,6 +56,40 @@ function getNestedRecord(
return asOptionalRecord(value);
}
function coerceMcpAppDescriptor(
record: Record<string, unknown> | undefined,
): McpAppPreviewDescriptor | undefined {
const viewId = getRecordStringField(record, "viewId");
if (!viewId || viewId.length > 128) {
return undefined;
}
const serverName = getRecordStringField(record, "serverName");
const toolName = getRecordStringField(record, "toolName");
const uiResourceUri = getRecordStringField(record, "uiResourceUri");
const toolCallId = getRecordStringField(record, "toolCallId");
const resultMetaState = record?.resultMetaState === "unavailable" ? "unavailable" : undefined;
const hasCompleteDescriptor = Boolean(
serverName &&
serverName.length <= 256 &&
toolName &&
toolName.length <= 256 &&
uiResourceUri?.startsWith("ui://") &&
uiResourceUri.length <= 2048 &&
toolCallId &&
toolCallId.length <= 512,
);
return hasCompleteDescriptor
? {
viewId,
serverName,
toolName,
uiResourceUri,
toolCallId,
...(resultMetaState ? { resultMetaState } : {}),
}
: { viewId };
}
function normalizeSurface(value: string | undefined): CanvasSurface | undefined {
return value === "assistant_message" ? value : undefined;
}
@@ -75,7 +118,8 @@ function coerceCanvasPreview(
const view = getNestedRecord(record, "view");
const source = getNestedRecord(record, "source");
const mcpAppRecord = getNestedRecord(record, "mcpApp");
const mcpAppViewId = getRecordStringField(mcpAppRecord, "viewId");
const mcpApp = coerceMcpAppDescriptor(mcpAppRecord);
const mcpAppViewId = mcpApp?.viewId;
const requestedSurface =
getRecordStringField(presentation, "target") ?? getRecordStringField(record, "target");
const surface = requestedSurface ? normalizeSurface(requestedSurface) : "assistant_message";
@@ -105,7 +149,7 @@ function coerceCanvasPreview(
...(title ? { title } : {}),
...(preferredHeight ? { preferredHeight } : {}),
...(sandbox ? { sandbox } : {}),
mcpApp: { viewId: mcpAppViewId },
mcpApp,
};
}
if (viewUrl) {
@@ -120,7 +164,7 @@ function coerceCanvasPreview(
...(className ? { className } : {}),
...(style ? { style } : {}),
...(sandbox ? { sandbox } : {}),
...(mcpAppViewId ? { mcpApp: { viewId: mcpAppViewId } } : {}),
...(mcpApp ? { mcpApp } : {}),
};
}
const sourceType = getRecordStringField(source, "type")?.trim().toLowerCase();
@@ -139,7 +183,7 @@ function coerceCanvasPreview(
...(className ? { className } : {}),
...(style ? { style } : {}),
...(sandbox ? { sandbox } : {}),
...(mcpAppViewId ? { mcpApp: { viewId: mcpAppViewId } } : {}),
...(mcpApp ? { mcpApp } : {}),
};
}
return undefined;