@@ -9,12 +9,14 @@ import React from "react";
99// vi.hoisted runs before imports, letting us capture bridge instances.
1010const {
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 ───────────────────────────────────────────────────────────
99115vi . 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 ,
0 commit comments