sunpeak 0.18.2 → 0.18.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/bin/commands/inspect.mjs +392 -4
  2. package/bin/lib/sandbox-server.mjs +11 -1
  3. package/dist/chatgpt/index.cjs +1 -1
  4. package/dist/chatgpt/index.js +1 -1
  5. package/dist/claude/index.cjs +1 -1
  6. package/dist/claude/index.js +1 -1
  7. package/dist/hooks/index.d.ts +1 -0
  8. package/dist/hooks/use-request-teardown.d.ts +21 -0
  9. package/dist/host/chatgpt/index.cjs +1 -1
  10. package/dist/host/chatgpt/index.js +1 -1
  11. package/dist/index.cjs +110 -70
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.ts +3 -3
  14. package/dist/index.js +68 -31
  15. package/dist/index.js.map +1 -1
  16. package/dist/inspector/hosts.d.ts +12 -0
  17. package/dist/inspector/index.cjs +1 -1
  18. package/dist/inspector/index.js +1 -1
  19. package/dist/inspector/mcp-app-host.d.ts +3 -0
  20. package/dist/inspector/simple-sidebar.d.ts +1 -1
  21. package/dist/inspector/use-mcp-connection.d.ts +9 -1
  22. package/dist/{inspector-ClhpqKLi.js → inspector-CjSoXm6N.js} +497 -117
  23. package/dist/inspector-CjSoXm6N.js.map +1 -0
  24. package/dist/{inspector-CByJjmPD.cjs → inspector-DRD_Q66E.cjs} +498 -118
  25. package/dist/inspector-DRD_Q66E.cjs.map +1 -0
  26. package/dist/mcp/index.cjs +38 -33
  27. package/dist/mcp/index.cjs.map +1 -1
  28. package/dist/mcp/index.js +35 -30
  29. package/dist/mcp/index.js.map +1 -1
  30. package/dist/style.css +8 -0
  31. package/dist/{use-app-X7JbGskk.js → use-app-BNbz1uzj.js} +51 -42
  32. package/dist/use-app-BNbz1uzj.js.map +1 -0
  33. package/dist/{use-app-D2h-aiyr.cjs → use-app-Dqh20JPP.cjs} +93 -72
  34. package/dist/use-app-Dqh20JPP.cjs.map +1 -0
  35. package/package.json +2 -2
  36. package/template/dist/albums/albums.html +3 -3
  37. package/template/dist/albums/albums.json +1 -1
  38. package/template/dist/carousel/carousel.html +3 -3
  39. package/template/dist/carousel/carousel.json +1 -1
  40. package/template/dist/map/map.html +3 -3
  41. package/template/dist/map/map.json +1 -1
  42. package/template/dist/review/review.html +3 -3
  43. package/template/dist/review/review.json +1 -1
  44. package/template/node_modules/.vite/deps/_metadata.json +3 -3
  45. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps.js +51 -42
  46. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps.js.map +1 -1
  47. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_app-bridge.js +56 -50
  48. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_app-bridge.js.map +1 -1
  49. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_react.js +52 -43
  50. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_react.js.map +1 -1
  51. package/template/node_modules/.vite-mcp/deps/_metadata.json +22 -22
  52. package/template/tests/e2e/albums.spec.ts +19 -15
  53. package/template/tests/live/albums.spec.ts +10 -0
  54. package/dist/inspector-CByJjmPD.cjs.map +0 -1
  55. package/dist/inspector-ClhpqKLi.js.map +0 -1
  56. package/dist/use-app-D2h-aiyr.cjs.map +0 -1
  57. package/dist/use-app-X7JbGskk.js.map +0 -1
@@ -76,12 +76,96 @@ Examples:
76
76
  `);
77
77
  }
78
78
 
79
+ /**
80
+ * Create an in-memory OAuth client provider for the inspector.
81
+ * The provider stores tokens, client info, and code verifier in memory.
82
+ * When `redirectToAuthorization()` is called, it stores the URL for retrieval.
83
+ *
84
+ * @param {string} redirectUrl - The callback URL for OAuth redirects
85
+ * @param {{ clientId?: string, clientSecret?: string }} [opts]
86
+ * @returns {{ provider: import('@modelcontextprotocol/sdk/client/auth.js').OAuthClientProvider, getAuthUrl: () => URL | undefined }}
87
+ */
88
+ function createInMemoryOAuthProvider(redirectUrl, opts = {}) {
89
+ let _tokens;
90
+ let _clientInfo;
91
+ let _codeVerifier;
92
+ let _authUrl;
93
+ let _discoveryState;
94
+ // Cryptographic state parameter for CSRF protection on the OAuth callback.
95
+ const _stateParam = crypto.randomUUID();
96
+
97
+ // If pre-registered client credentials were provided, seed the client info
98
+ // so the SDK skips dynamic client registration.
99
+ if (opts.clientId) {
100
+ _clientInfo = {
101
+ client_id: opts.clientId,
102
+ ...(opts.clientSecret ? { client_secret: opts.clientSecret } : {}),
103
+ };
104
+ }
105
+
106
+ const provider = {
107
+ get redirectUrl() {
108
+ return redirectUrl;
109
+ },
110
+ get clientMetadata() {
111
+ return {
112
+ redirect_uris: [new URL(redirectUrl)],
113
+ client_name: 'sunpeak Inspector',
114
+ token_endpoint_auth_method: opts.clientSecret ? 'client_secret_post' : 'none',
115
+ grant_types: ['authorization_code', 'refresh_token'],
116
+ response_types: ['code'],
117
+ };
118
+ },
119
+ // Return the state parameter so the SDK includes it in the authorization URL.
120
+ state() {
121
+ return _stateParam;
122
+ },
123
+ clientInformation() {
124
+ return _clientInfo;
125
+ },
126
+ saveClientInformation(info) {
127
+ _clientInfo = info;
128
+ },
129
+ tokens() {
130
+ return _tokens;
131
+ },
132
+ saveTokens(tokens) {
133
+ _tokens = tokens;
134
+ },
135
+ redirectToAuthorization(url) {
136
+ _authUrl = url;
137
+ },
138
+ saveCodeVerifier(verifier) {
139
+ _codeVerifier = verifier;
140
+ },
141
+ codeVerifier() {
142
+ return _codeVerifier;
143
+ },
144
+ // Cache discovery state so the second auth() call (token exchange)
145
+ // doesn't re-discover metadata from scratch.
146
+ saveDiscoveryState(state) {
147
+ _discoveryState = state;
148
+ },
149
+ discoveryState() {
150
+ return _discoveryState;
151
+ },
152
+ };
153
+
154
+ return {
155
+ provider,
156
+ getAuthUrl: () => _authUrl,
157
+ hasTokens: () => !!_tokens,
158
+ stateParam: _stateParam,
159
+ };
160
+ }
161
+
79
162
  /**
80
163
  * Create an MCP client connection.
81
164
  * @param {string} serverArg - URL or command string
165
+ * @param {{ type?: 'none' | 'bearer' | 'oauth', bearerToken?: string, authProvider?: import('@modelcontextprotocol/sdk/client/auth.js').OAuthClientProvider }} [authConfig]
82
166
  * @returns {Promise<{ client: import('@modelcontextprotocol/sdk/client/index.js').Client, transport: import('@modelcontextprotocol/sdk/types.js').Transport }>}
83
167
  */
84
- async function createMcpConnection(serverArg) {
168
+ async function createMcpConnection(serverArg, authConfig) {
85
169
  const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
86
170
  const client = new Client({ name: 'sunpeak-inspector', version: '1.0.0' });
87
171
 
@@ -90,7 +174,18 @@ async function createMcpConnection(serverArg) {
90
174
  const { StreamableHTTPClientTransport } = await import(
91
175
  '@modelcontextprotocol/sdk/client/streamableHttp.js'
92
176
  );
93
- const transport = new StreamableHTTPClientTransport(new URL(serverArg));
177
+
178
+ const transportOpts = {};
179
+
180
+ if (authConfig?.type === 'bearer' && authConfig.bearerToken) {
181
+ transportOpts.requestInit = {
182
+ headers: { Authorization: `Bearer ${authConfig.bearerToken}` },
183
+ };
184
+ } else if (authConfig?.type === 'oauth' && authConfig.authProvider) {
185
+ transportOpts.authProvider = authConfig.authProvider;
186
+ }
187
+
188
+ const transport = new StreamableHTTPClientTransport(new URL(serverArg), transportOpts);
94
189
  await client.connect(transport);
95
190
  return { client, transport };
96
191
  } else {
@@ -283,6 +378,16 @@ root.render(
283
378
  * @param {{ callToolDirect?: (name: string, args: Record<string, unknown>) => Promise<object>, simulationsDir?: string | null }} [pluginOpts]
284
379
  */
285
380
  function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
381
+ // In-memory OAuth state keyed by server URL, persisted across reconnects.
382
+ /** @type {Map<string, { provider: any, getAuthUrl: () => URL | undefined, hasTokens: () => boolean, stateParam: string }>} */
383
+ const oauthProviders = new Map();
384
+ // Map OAuth state parameter → { serverUrl, oauthState } for CSRF-safe callback matching.
385
+ // Stores a direct reference to the provider that initiated the flow, so even if
386
+ // oauthProviders[serverUrl] is overwritten by a concurrent flow, the callback
387
+ // still completes with the correct provider (which holds the right codeVerifier
388
+ // and clientInformation).
389
+ /** @type {Map<string, { serverUrl: string, oauthState: any }>} */
390
+ const pendingOAuthFlows = new Map();
286
391
  return {
287
392
  name: 'sunpeak-inspect-endpoints',
288
393
  configureServer(server) {
@@ -421,8 +526,21 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
421
526
  // Close old connection (best effort)
422
527
  try { await getClient().close(); } catch { /* ignore */ }
423
528
 
529
+ // Build auth config from request
530
+ const authConfig = parsed.auth;
531
+ let connectionAuth;
532
+ if (authConfig?.type === 'bearer' && authConfig.bearerToken) {
533
+ connectionAuth = { type: 'bearer', bearerToken: authConfig.bearerToken };
534
+ } else if (authConfig?.type === 'oauth') {
535
+ // Reuse existing OAuth provider if we have one for this server
536
+ const existing = oauthProviders.get(url);
537
+ if (existing?.hasTokens()) {
538
+ connectionAuth = { type: 'oauth', authProvider: existing.provider };
539
+ }
540
+ }
541
+
424
542
  // Create new connection
425
- const newConnection = await createMcpConnection(url);
543
+ const newConnection = await createMcpConnection(url, connectionAuth);
426
544
  setClient(newConnection.client);
427
545
 
428
546
  // Discover tools and resources from the new server
@@ -439,6 +557,273 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
439
557
  }
440
558
  });
441
559
 
560
+ // ── OAuth flow endpoints ──
561
+
562
+ // Start OAuth: discover metadata, register client, return authorization URL
563
+ server.middlewares.use('/__sunpeak/oauth/start', async (req, res) => {
564
+ if (req.method !== 'POST') {
565
+ res.writeHead(405);
566
+ res.end('Method not allowed');
567
+ return;
568
+ }
569
+
570
+ const body = await readRequestBody(req);
571
+ let parsed;
572
+ try { parsed = JSON.parse(body); } catch {
573
+ res.writeHead(400, { 'Content-Type': 'application/json' });
574
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
575
+ return;
576
+ }
577
+
578
+ const { url: serverUrl, scope, clientId, clientSecret } = parsed;
579
+ if (!serverUrl) {
580
+ res.writeHead(400, { 'Content-Type': 'application/json' });
581
+ res.end(JSON.stringify({ error: 'Missing url' }));
582
+ return;
583
+ }
584
+
585
+ try {
586
+ // Determine callback URL from the Vite server's address
587
+ const addr = server.httpServer?.address();
588
+ const port = typeof addr === 'object' && addr ? addr.port : 3000;
589
+ const callbackUrl = `http://localhost:${port}/__sunpeak/oauth/callback`;
590
+
591
+ // Check if we already have a working provider with tokens for this server.
592
+ // If so, try to connect directly before creating a fresh provider.
593
+ const existingState = oauthProviders.get(serverUrl);
594
+ if (existingState?.hasTokens()) {
595
+ try {
596
+ // Close old connection (best effort)
597
+ try { await getClient().close(); } catch { /* ignore */ }
598
+
599
+ const newConnection = await createMcpConnection(serverUrl, {
600
+ type: 'oauth',
601
+ authProvider: existingState.provider,
602
+ });
603
+ setClient(newConnection.client);
604
+ const simulations = await discoverSimulations(newConnection.client);
605
+ if (pluginOpts.simulationsDir) {
606
+ mergeSimulationFixtures(pluginOpts.simulationsDir, simulations);
607
+ }
608
+ res.writeHead(200, { 'Content-Type': 'application/json' });
609
+ res.end(JSON.stringify({ status: 'authorized', simulations }));
610
+ return;
611
+ } catch {
612
+ // Tokens may be expired, fall through to fresh auth below
613
+ }
614
+ }
615
+
616
+ // Always create a fresh provider for an explicit Authorize click.
617
+ // This ensures the user's current credentials (or lack thereof) are
618
+ // used, not stale ones from a previous attempt.
619
+ const oauthState = createInMemoryOAuthProvider(callbackUrl, { clientId, clientSecret });
620
+ oauthProviders.set(serverUrl, oauthState);
621
+
622
+ // Run the SDK auth flow — will call redirectToAuthorization() if needed
623
+ const { auth } = await import('@modelcontextprotocol/sdk/client/auth.js');
624
+ const result = await auth(oauthState.provider, {
625
+ serverUrl,
626
+ scope,
627
+ });
628
+
629
+ if (result === 'REDIRECT') {
630
+ const authUrl = oauthState.getAuthUrl();
631
+ if (!authUrl) {
632
+ throw new Error('OAuth flow requested redirect but no authorization URL was generated');
633
+ }
634
+ // Register the state parameter so the callback can find the right provider.
635
+ // Clean up any stale pending flows for the same server URL first
636
+ // (e.g., user closed the popup without completing the previous attempt).
637
+ for (const [key, val] of pendingOAuthFlows) {
638
+ if (val.serverUrl === serverUrl) pendingOAuthFlows.delete(key);
639
+ }
640
+ pendingOAuthFlows.set(oauthState.stateParam, { serverUrl, oauthState });
641
+ res.writeHead(200, { 'Content-Type': 'application/json' });
642
+ res.end(JSON.stringify({ status: 'redirect', authUrl: authUrl.toString() }));
643
+ } else {
644
+ // AUTHORIZED — tokens were already available (shouldn't normally happen on first call)
645
+ try { await getClient().close(); } catch { /* ignore */ }
646
+ const newConnection = await createMcpConnection(serverUrl, {
647
+ type: 'oauth',
648
+ authProvider: oauthState.provider,
649
+ });
650
+ setClient(newConnection.client);
651
+ const simulations = await discoverSimulations(newConnection.client);
652
+ if (pluginOpts.simulationsDir) {
653
+ mergeSimulationFixtures(pluginOpts.simulationsDir, simulations);
654
+ }
655
+ res.writeHead(200, { 'Content-Type': 'application/json' });
656
+ res.end(JSON.stringify({ status: 'authorized', simulations }));
657
+ }
658
+ } catch (err) {
659
+ res.writeHead(500, { 'Content-Type': 'application/json' });
660
+ res.end(JSON.stringify({ error: err.message }));
661
+ }
662
+ });
663
+
664
+ // OAuth callback: serves an HTML page that sends the code back to the inspector.
665
+ // The state parameter is validated server-side in /__sunpeak/oauth/complete.
666
+ server.middlewares.use('/__sunpeak/oauth/callback', async (req, res) => {
667
+ // Parse code + state from query params
668
+ const reqUrl = new URL(req.url, 'http://localhost');
669
+ const code = reqUrl.searchParams.get('code');
670
+ const state = reqUrl.searchParams.get('state');
671
+ const error = reqUrl.searchParams.get('error');
672
+ const errorDescription = reqUrl.searchParams.get('error_description');
673
+
674
+ // Escape values for safe embedding in <script> — JSON.stringify alone
675
+ // doesn't escape "</script>" sequences which would break out of the tag.
676
+ const safeJson = (val) => JSON.stringify(val).replace(/</g, '\\u003c');
677
+
678
+ const html = `<!DOCTYPE html>
679
+ <html><head><title>OAuth Callback</title></head>
680
+ <body>
681
+ <script>
682
+ (function() {
683
+ var code = ${safeJson(code)};
684
+ var state = ${safeJson(state)};
685
+ var error = ${safeJson(error)};
686
+ var errorDescription = ${safeJson(errorDescription)};
687
+
688
+ // Use our own origin as the postMessage targetOrigin to prevent leaking data cross-origin.
689
+ var origin = location.origin;
690
+
691
+ // Send a message to the opener window. Uses postMessage when window.opener is
692
+ // available, falls back to BroadcastChannel for OAuth providers that set
693
+ // Cross-Origin-Opener-Policy (COOP) which nullifies window.opener.
694
+ function notify(msg) {
695
+ if (window.opener) {
696
+ window.opener.postMessage(msg, origin);
697
+ } else if (typeof BroadcastChannel !== 'undefined') {
698
+ var bc = new BroadcastChannel('sunpeak-oauth');
699
+ bc.postMessage(msg);
700
+ bc.close();
701
+ }
702
+ }
703
+
704
+ if (error) {
705
+ notify({ type: 'sunpeak-oauth-callback', error: error, errorDescription: errorDescription });
706
+ document.body.textContent = 'Authorization failed: ' + (errorDescription || error);
707
+ setTimeout(function() { window.close(); }, 2000);
708
+ return;
709
+ }
710
+
711
+ if (!code) {
712
+ document.body.textContent = 'No authorization code received.';
713
+ return;
714
+ }
715
+
716
+ document.body.textContent = 'Completing authorization...';
717
+
718
+ // Post the code + state to the server to exchange for tokens.
719
+ // The state is validated server-side to prevent CSRF.
720
+ fetch('/__sunpeak/oauth/complete', {
721
+ method: 'POST',
722
+ headers: { 'Content-Type': 'application/json' },
723
+ body: JSON.stringify({ code: code, state: state })
724
+ })
725
+ .then(function(res) { return res.json(); })
726
+ .then(function(data) {
727
+ if (data.error) {
728
+ notify({ type: 'sunpeak-oauth-callback', error: data.error });
729
+ document.body.textContent = 'Authorization failed: ' + data.error;
730
+ } else {
731
+ notify({ type: 'sunpeak-oauth-callback', success: true, simulations: data.simulations });
732
+ document.body.textContent = 'Authorized! You can close this window.';
733
+ }
734
+ setTimeout(function() { window.close(); }, 1000);
735
+ })
736
+ .catch(function(err) {
737
+ notify({ type: 'sunpeak-oauth-callback', error: err.message });
738
+ document.body.textContent = 'Error: ' + err.message;
739
+ });
740
+ })();
741
+ </script>
742
+ </body></html>`;
743
+
744
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
745
+ res.end(html);
746
+ });
747
+
748
+ // Complete OAuth: exchange authorization code for tokens and connect
749
+ server.middlewares.use('/__sunpeak/oauth/complete', async (req, res) => {
750
+ if (req.method !== 'POST') {
751
+ res.writeHead(405);
752
+ res.end('Method not allowed');
753
+ return;
754
+ }
755
+
756
+ const body = await readRequestBody(req);
757
+ let parsed;
758
+ try { parsed = JSON.parse(body); } catch {
759
+ res.writeHead(400, { 'Content-Type': 'application/json' });
760
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
761
+ return;
762
+ }
763
+
764
+ const { code, state } = parsed;
765
+ if (!code) {
766
+ res.writeHead(400, { 'Content-Type': 'application/json' });
767
+ res.end(JSON.stringify({ error: 'Missing authorization code' }));
768
+ return;
769
+ }
770
+ if (!state) {
771
+ res.writeHead(400, { 'Content-Type': 'application/json' });
772
+ res.end(JSON.stringify({ error: 'Missing state parameter' }));
773
+ return;
774
+ }
775
+
776
+ // Look up the provider via the state parameter (CSRF protection).
777
+ // Uses the direct provider reference from the pending flow, not the
778
+ // oauthProviders map, so concurrent flows for the same server URL
779
+ // don't clobber each other's codeVerifier/clientInformation.
780
+ const pending = pendingOAuthFlows.get(state);
781
+ pendingOAuthFlows.delete(state); // Consume — single-use
782
+
783
+ if (!pending) {
784
+ res.writeHead(400, { 'Content-Type': 'application/json' });
785
+ res.end(JSON.stringify({ error: 'Invalid or expired OAuth state. Start the flow again.' }));
786
+ return;
787
+ }
788
+
789
+ const { serverUrl, oauthState } = pending;
790
+
791
+ try {
792
+ // Exchange the code for tokens
793
+ const { auth } = await import('@modelcontextprotocol/sdk/client/auth.js');
794
+ const result = await auth(oauthState.provider, {
795
+ serverUrl,
796
+ authorizationCode: code,
797
+ });
798
+
799
+ if (result !== 'AUTHORIZED') {
800
+ throw new Error('Token exchange did not result in authorization');
801
+ }
802
+
803
+ // Store the now-authorized provider so reconnects can reuse tokens.
804
+ oauthProviders.set(serverUrl, oauthState);
805
+
806
+ // Create MCP connection with the authorized provider
807
+ try { await getClient().close(); } catch { /* ignore */ }
808
+ const newConnection = await createMcpConnection(serverUrl, {
809
+ type: 'oauth',
810
+ authProvider: oauthState.provider,
811
+ });
812
+ setClient(newConnection.client);
813
+
814
+ const simulations = await discoverSimulations(newConnection.client);
815
+ if (pluginOpts.simulationsDir) {
816
+ mergeSimulationFixtures(pluginOpts.simulationsDir, simulations);
817
+ }
818
+
819
+ res.writeHead(200, { 'Content-Type': 'application/json' });
820
+ res.end(JSON.stringify({ status: 'ok', simulations }));
821
+ } catch (err) {
822
+ res.writeHead(500, { 'Content-Type': 'application/json' });
823
+ res.end(JSON.stringify({ error: err.message }));
824
+ }
825
+ });
826
+
442
827
  // Read resource from connected server
443
828
  server.middlewares.use('/__sunpeak/read-resource', async (req, res) => {
444
829
  const url = new URL(req.url, 'http://localhost');
@@ -460,7 +845,10 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
460
845
  }
461
846
 
462
847
  const mimeType = content.mimeType || 'text/html';
463
- res.writeHead(200, { 'Content-Type': `${mimeType}; charset=utf-8` });
848
+ res.writeHead(200, {
849
+ 'Content-Type': `${mimeType}; charset=utf-8`,
850
+ 'X-Content-Type-Options': 'nosniff',
851
+ });
464
852
  if (typeof content.text === 'string') {
465
853
  res.end(content.text);
466
854
  } else if (content.blob) {
@@ -97,7 +97,7 @@ function generateProxyHtml(theme, platform) {
97
97
  <head>
98
98
  <meta name="color-scheme" content="${colorScheme}" />
99
99
  <style>
100
- html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; }
100
+ html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: transparent; }
101
101
  iframe { border: none; width: 100%; height: 100%; display: block; }
102
102
  </style>
103
103
  </head>
@@ -168,6 +168,16 @@ iframe { border: none; width: 100%; height: 100%; display: block; }
168
168
  return;
169
169
  }
170
170
 
171
+ // Sync color-scheme on the inner iframe element when theme changes.
172
+ // This ensures prefers-color-scheme resolves correctly inside the app.
173
+ // Important: do NOT set color-scheme on the proxy's own document —
174
+ // changing it from the initial 'dark' causes Chrome to re-evaluate
175
+ // the CSS Canvas as opaque white, blocking the host's conversation
176
+ // background from showing through the transparent proxy.
177
+ if (data.method === 'ui/notifications/host-context-changed' && data.params && data.params.theme) {
178
+ if (innerFrame) innerFrame.style.colorScheme = data.params.theme;
179
+ }
180
+
171
181
  // Forward all other messages to the inner iframe
172
182
  if (innerWindow) {
173
183
  try { innerWindow.postMessage(data, '*'); } catch(e) { /* detached */ }
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../chunk-9hOWP6kD.cjs");
3
3
  require("../protocol-jbxhzcnS.cjs");
4
- const require_inspector = require("../inspector-CByJjmPD.cjs");
4
+ const require_inspector = require("../inspector-DRD_Q66E.cjs");
5
5
  const require_inspector_url = require("../inspector-url-7qhtJwY6.cjs");
6
6
  const require_discovery = require("../discovery-Clu4uHp1.cjs");
7
7
  //#region src/chatgpt/index.ts
@@ -1,6 +1,6 @@
1
1
  import { r as __exportAll } from "../chunk-D6g4UhsZ.js";
2
2
  import "../protocol-DJmRaBzO.js";
3
- import { _ as McpAppHost, d as ThemeProvider, f as useThemeContext, g as extractResourceCSP, h as IframeResource, n as resolveServerToolResult, t as Inspector, v as SCREEN_WIDTHS } from "../inspector-ClhpqKLi.js";
3
+ import { _ as McpAppHost, d as ThemeProvider, f as useThemeContext, g as extractResourceCSP, h as IframeResource, n as resolveServerToolResult, t as Inspector, v as SCREEN_WIDTHS } from "../inspector-CjSoXm6N.js";
4
4
  import { t as createInspectorUrl } from "../inspector-url-DuEFmxLP.js";
5
5
  import { c as toPascalCase, i as findResourceKey, n as extractSimulationKey, r as findResourceDirs, s as getComponentName, t as extractResourceKey } from "../discovery-Cgoegt62.js";
6
6
  //#region src/chatgpt/index.ts
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  require("../chunk-9hOWP6kD.cjs");
3
3
  require("../protocol-jbxhzcnS.cjs");
4
- const require_inspector = require("../inspector-CByJjmPD.cjs");
4
+ const require_inspector = require("../inspector-DRD_Q66E.cjs");
5
5
  exports.Inspector = require_inspector.Inspector;
@@ -1,3 +1,3 @@
1
1
  import "../protocol-DJmRaBzO.js";
2
- import { t as Inspector } from "../inspector-ClhpqKLi.js";
2
+ import { t as Inspector } from "../inspector-CjSoXm6N.js";
3
3
  export { Inspector };
@@ -36,6 +36,7 @@ export type { OpenLinkParams } from './use-open-link';
36
36
  export { useReadServerResource } from './use-read-server-resource';
37
37
  export type { ReadServerResourceParams, ReadServerResourceResult, } from './use-read-server-resource';
38
38
  export { useRequestDisplayMode } from './use-request-display-mode';
39
+ export { useRequestTeardown } from './use-request-teardown';
39
40
  export type { AppDisplayMode } from './use-request-display-mode';
40
41
  export { useSendLog } from './use-send-log';
41
42
  export type { LogLevel, SendLogParams } from './use-send-log';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Hook to request the host to tear down this app.
3
+ *
4
+ * Sends a notification to the host requesting that it initiate teardown.
5
+ * If the host approves, it will send the standard teardown request back
6
+ * to the app (triggering any `useTeardown` callbacks) before unmounting.
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * function MyApp() {
11
+ * const requestTeardown = useRequestTeardown();
12
+ *
13
+ * return (
14
+ * <button onClick={() => requestTeardown()}>
15
+ * Close App
16
+ * </button>
17
+ * );
18
+ * }
19
+ * ```
20
+ */
21
+ export declare function useRequestTeardown(): () => Promise<void>;
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  require("../../chunk-9hOWP6kD.cjs");
3
3
  require("../../protocol-jbxhzcnS.cjs");
4
- const require_use_app = require("../../use-app-D2h-aiyr.cjs");
4
+ const require_use_app = require("../../use-app-Dqh20JPP.cjs");
5
5
  let react = require("react");
6
6
  //#region src/host/chatgpt/openai-types.ts
7
7
  /**
@@ -1,5 +1,5 @@
1
1
  import "../../protocol-DJmRaBzO.js";
2
- import { t as useApp } from "../../use-app-X7JbGskk.js";
2
+ import { t as useApp } from "../../use-app-BNbz1uzj.js";
3
3
  import { useCallback } from "react";
4
4
  //#region src/host/chatgpt/openai-types.ts
5
5
  /**