Skip to content

Commit 9470a54

Browse files
committed
fix renderer
1 parent c410a7f commit 9470a54

2 files changed

Lines changed: 103 additions & 13 deletions

File tree

mcpjam-inspector/client/src/components/chat-v2/thread/mcp-apps/__tests__/mcp-apps-renderer.test.tsx

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import React from "react";
99
// vi.hoisted runs before imports, letting us capture bridge instances.
1010
const {
1111
mockBridge,
12+
mockAppBridgeCtor,
1213
mockPostMessageTransport,
1314
triggerReady,
1415
stableStoreFns,
1516
mockSandboxPostMessage,
1617
sandboxedIframePropsRef,
1718
sandboxProxyBehaviorRef,
19+
appBridgeArgsRef,
1820
} = vi.hoisted(() => {
1921
const bridge = {
2022
sendToolInput: vi.fn(),
@@ -40,6 +42,18 @@ const {
4042
onrequestdisplaymode: null as any,
4143
onupdatemodelcontext: null as any,
4244
};
45+
const appBridgeArgsRef = { current: null as any };
46+
const mockAppBridgeCtor = vi
47+
.fn()
48+
.mockImplementation((client, hostInfo, hostCapabilities, options) => {
49+
appBridgeArgsRef.current = {
50+
client,
51+
hostInfo,
52+
hostCapabilities,
53+
options,
54+
};
55+
return bridge;
56+
});
4357

4458
// Stable function references for store selectors — prevents useEffect deps
4559
// from changing on every render, which would teardown/reinitialize the bridge.
@@ -56,10 +70,12 @@ const {
5670

5771
return {
5872
mockBridge: bridge,
73+
mockAppBridgeCtor,
5974
mockPostMessageTransport: vi.fn(),
6075
mockSandboxPostMessage: vi.fn(),
6176
sandboxedIframePropsRef: { current: null as any },
6277
sandboxProxyBehaviorRef: { current: { autoReady: true } },
78+
appBridgeArgsRef,
6379
stableStoreFns: stableFns,
6480
/** Simulate the widget completing initialization. */
6581
triggerReady: () => {
@@ -97,7 +113,7 @@ const mockPlaygroundStoreState = {
97113

98114
// ── Module mocks ───────────────────────────────────────────────────────────
99115
vi.mock("@modelcontextprotocol/ext-apps/app-bridge", () => ({
100-
AppBridge: vi.fn().mockImplementation(() => mockBridge),
116+
AppBridge: mockAppBridgeCtor,
101117
PostMessageTransport: mockPostMessageTransport,
102118
}));
103119

@@ -232,10 +248,12 @@ describe("MCPAppsRenderer tool input streaming", () => {
232248
mockBridge.setHostContext.mockClear();
233249
mockBridge.close.mockClear().mockResolvedValue(undefined);
234250
mockBridge.teardownResource.mockClear().mockResolvedValue({});
251+
mockAppBridgeCtor.mockClear();
235252
mockBridge.oninitialized = null;
236253
mockSandboxPostMessage.mockClear();
237254
sandboxedIframePropsRef.current = null;
238255
sandboxProxyBehaviorRef.current.autoReady = true;
256+
appBridgeArgsRef.current = null;
239257

240258
vi.mocked(global.fetch).mockResolvedValue({
241259
ok: true,
@@ -357,6 +375,51 @@ describe("MCPAppsRenderer tool input streaming", () => {
357375
});
358376
});
359377

378+
it("filters non-standard host style variables out of the initialize payload", async () => {
379+
mockClientConfigStoreState.draftConfig = {
380+
version: 1,
381+
clientCapabilities: {},
382+
hostContext: {
383+
styles: {
384+
variables: {
385+
"--font-sans": "Custom Sans",
386+
"--mcpjam-theme-preset": "soft-pop",
387+
"--totally-unknown": "ignore-me",
388+
},
389+
},
390+
},
391+
};
392+
393+
render(<MCPAppsRenderer {...baseProps} />);
394+
395+
await vi.waitFor(() => {
396+
expect(mockBridge.connect).toHaveBeenCalled();
397+
});
398+
399+
expect(
400+
appBridgeArgsRef.current?.options?.hostContext?.styles?.variables,
401+
).toEqual({
402+
"--font-sans": "Custom Sans",
403+
});
404+
405+
await act(async () => {
406+
triggerReady();
407+
await Promise.resolve();
408+
});
409+
410+
await vi.waitFor(() => {
411+
expect(mockBridge.setHostContext).toHaveBeenLastCalledWith(
412+
expect.objectContaining({
413+
styles: expect.objectContaining({
414+
variables: {
415+
"--font-sans": "Custom Sans",
416+
},
417+
}),
418+
}),
419+
);
420+
});
421+
});
422+
360423
it("anchors desktop playground PiP to the playground shell instead of the viewport", async () => {
361424
Object.assign(mockPlaygroundStoreState, {
362425
isPlaygroundActive: true,

mcpjam-inspector/client/src/components/chat-v2/thread/mcp-apps/mcp-apps-renderer.tsx

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,37 @@ const DEFAULT_INPUT_SCHEMA = { type: "object" } as const;
8686

8787
const SUPPRESSED_UI_LOG_METHODS = new Set(["ui/notifications/size-changed"]);
8888
const PIP_MAX_HEIGHT = "min(40vh, 600px)";
89+
const VALID_HOST_STYLE_VARIABLE_KEYS = new Set<string>([
90+
...Object.keys(getClaudeDesktopStyleVariables("light")),
91+
...Object.keys(getChatGPTStyleVariables("light")),
92+
]);
8993

9094
type DisplayMode = "inline" | "pip" | "fullscreen";
95+
type HostStyleVariables = NonNullable<
96+
NonNullable<McpUiHostContext["styles"]>["variables"]
97+
>;
98+
99+
function sanitizeHostStyleVariables(
100+
variables: unknown,
101+
): HostStyleVariables | undefined {
102+
if (!variables || typeof variables !== "object" || Array.isArray(variables)) {
103+
return undefined;
104+
}
105+
106+
const sanitized: Record<string, string | undefined> = {};
107+
for (const [key, value] of Object.entries(variables)) {
108+
if (!VALID_HOST_STYLE_VARIABLE_KEYS.has(key)) {
109+
continue;
110+
}
111+
if (typeof value === "string" || value === undefined) {
112+
sanitized[key] = value;
113+
}
114+
}
115+
116+
return Object.keys(sanitized).length > 0
117+
? (sanitized as HostStyleVariables)
118+
: undefined;
119+
}
91120

92121
// CSP and permissions metadata types are now imported from SDK
93122

@@ -178,7 +207,6 @@ export function MCPAppsRenderer({
178207
}: MCPAppsRendererProps) {
179208
const sandboxRef = useRef<SandboxedIframeHandle>(null);
180209
const themeMode = usePreferencesStore((s) => s.themeMode);
181-
const themePreset = usePreferencesStore((s) => s.themePreset);
182210
const sharedHostStyle = usePreferencesStore((s) => s.hostStyle);
183211
const chatboxHostStyle = useChatboxHostStyle();
184212
const draftHostContext = useClientConfigStore(
@@ -772,21 +800,20 @@ export function MCPAppsRenderer({
772800
!Array.isArray(baseHostContext.styles)
773801
? (baseHostContext.styles as McpUiHostContext["styles"])
774802
: undefined;
803+
// The SDK validates styles.variables against the SEP key enum, so strip
804+
// host-specific custom properties before they enter ui/initialize.
805+
const configuredStyleVariables = useMemo(
806+
() => sanitizeHostStyleVariables(configuredStyles?.variables),
807+
[configuredStyles?.variables],
808+
);
775809
const mergedStyleVariables = useMemo(() => {
776-
const configuredVariables =
777-
configuredStyles?.variables &&
778-
typeof configuredStyles.variables === "object" &&
779-
!Array.isArray(configuredStyles.variables)
780-
? configuredStyles.variables
781-
: undefined;
782-
783810
return {
784-
...(configuredVariables && Object.keys(configuredVariables).length > 0
785-
? configuredVariables
811+
...(configuredStyleVariables &&
812+
Object.keys(configuredStyleVariables).length > 0
813+
? configuredStyleVariables
786814
: styleVariables),
787-
"--mcpjam-theme-preset": themePreset,
788815
};
789-
}, [configuredStyles?.variables, styleVariables, themePreset]);
816+
}, [configuredStyleVariables, styleVariables]);
790817
const mergedStyles = useMemo<McpUiHostContext["styles"]>(
791818
() => ({
792819
...configuredStyles,

0 commit comments

Comments
 (0)