@mcp-ts/sdk 1.3.9 → 1.4.0

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 (47) hide show
  1. package/README.md +0 -1
  2. package/dist/adapters/langchain-adapter.js.map +1 -1
  3. package/dist/adapters/langchain-adapter.mjs.map +1 -1
  4. package/dist/client/index.d.mts +3 -189
  5. package/dist/client/index.d.ts +3 -189
  6. package/dist/client/index.js +218 -54
  7. package/dist/client/index.js.map +1 -1
  8. package/dist/client/index.mjs +215 -55
  9. package/dist/client/index.mjs.map +1 -1
  10. package/dist/client/react.d.mts +29 -40
  11. package/dist/client/react.d.ts +29 -40
  12. package/dist/client/react.js +492 -147
  13. package/dist/client/react.js.map +1 -1
  14. package/dist/client/react.mjs +490 -149
  15. package/dist/client/react.mjs.map +1 -1
  16. package/dist/client/vue.d.mts +3 -2
  17. package/dist/client/vue.d.ts +3 -2
  18. package/dist/client/vue.js +239 -63
  19. package/dist/client/vue.js.map +1 -1
  20. package/dist/client/vue.mjs +236 -64
  21. package/dist/client/vue.mjs.map +1 -1
  22. package/dist/index-CQr9q0bF.d.mts +295 -0
  23. package/dist/index-nE_7Io0I.d.ts +295 -0
  24. package/dist/index.d.mts +2 -1
  25. package/dist/index.d.ts +2 -1
  26. package/dist/index.js +315 -64
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +303 -65
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/server/index.js +93 -10
  31. package/dist/server/index.js.map +1 -1
  32. package/dist/server/index.mjs +88 -10
  33. package/dist/server/index.mjs.map +1 -1
  34. package/package.json +13 -11
  35. package/src/adapters/langchain-adapter.ts +1 -1
  36. package/src/client/core/app-host.ts +252 -65
  37. package/src/client/core/constants.ts +30 -0
  38. package/src/client/index.ts +6 -1
  39. package/src/client/react/index.ts +6 -1
  40. package/src/client/react/use-app-host.ts +13 -19
  41. package/src/client/react/use-mcp-apps.tsx +297 -125
  42. package/src/client/react/use-mcp.ts +75 -36
  43. package/src/client/utils/app-host-utils.ts +62 -0
  44. package/src/client/vue/use-mcp.ts +23 -12
  45. package/src/server/mcp/oauth-client.ts +31 -8
  46. package/src/server/storage/crypto.ts +92 -0
  47. package/src/server/storage/supabase-backend.ts +7 -6
@@ -1,4 +1,4 @@
1
- import { memo, useCallback, useRef, useState, useEffect } from 'react';
1
+ import { forwardRef, useRef, useState, useImperativeHandle, useEffect, memo, useCallback, useMemo } from 'react';
2
2
  import { nanoid } from 'nanoid';
3
3
  import { AppBridge, PostMessageTransport } from '@modelcontextprotocol/ext-apps/app-bridge';
4
4
  import { jsxs, jsx } from 'react/jsx-runtime';
@@ -275,6 +275,7 @@ function useMcp(options) {
275
275
  "disconnected"
276
276
  );
277
277
  const [isInitializing, setIsInitializing] = useState(false);
278
+ const [sseClient, setSseClient] = useState(null);
278
279
  useEffect(() => {
279
280
  isMountedRef.current = true;
280
281
  const clientOptions = {
@@ -297,6 +298,7 @@ function useMcp(options) {
297
298
  };
298
299
  const client = new SSEClient(clientOptions);
299
300
  clientRef.current = client;
301
+ setSseClient(client);
300
302
  if (autoConnect) {
301
303
  client.connect();
302
304
  if (autoInitialize) {
@@ -352,18 +354,28 @@ function useMcp(options) {
352
354
  );
353
355
  }
354
356
  case "auth_required": {
355
- if (event.authUrl) {
356
- onLog?.("info", `OAuth required - redirecting to ${event.authUrl}`, { authUrl: event.authUrl });
357
- if (!suppressAuthRedirectSessionsRef.current.has(event.sessionId)) {
358
- if (onRedirect) {
359
- onRedirect(event.authUrl);
360
- } else if (typeof window !== "undefined") {
361
- window.location.href = event.authUrl;
362
- }
357
+ const url2 = (event.authUrl || "").trim();
358
+ if (!url2) {
359
+ onLog?.("error", "OAuth required but authorization URL is missing", { sessionId: event.sessionId });
360
+ return prev.map(
361
+ (c) => c.sessionId === event.sessionId ? {
362
+ ...c,
363
+ state: "FAILED",
364
+ error: "OAuth authorization URL not available",
365
+ authUrl: void 0
366
+ } : c
367
+ );
368
+ }
369
+ onLog?.("info", `OAuth required - redirecting to ${url2}`, { authUrl: url2 });
370
+ if (!suppressAuthRedirectSessionsRef.current.has(event.sessionId)) {
371
+ if (onRedirect) {
372
+ onRedirect(url2);
373
+ } else if (typeof window !== "undefined") {
374
+ window.location.href = url2;
363
375
  }
364
376
  }
365
377
  return prev.map(
366
- (c) => c.sessionId === event.sessionId ? { ...c, state: "AUTHENTICATING", authUrl: event.authUrl } : c
378
+ (c) => c.sessionId === event.sessionId ? { ...c, state: "AUTHENTICATING", authUrl: url2 } : c
367
379
  );
368
380
  }
369
381
  case "error": {
@@ -530,46 +542,137 @@ function useMcp(options) {
530
542
  },
531
543
  [getConnection]
532
544
  );
533
- return {
534
- connections,
535
- status,
536
- isInitializing,
537
- connect,
538
- disconnect,
539
- getConnection,
540
- getConnectionByServerId,
541
- isServerConnected,
542
- getTools,
543
- refresh,
544
- connectSSE,
545
- disconnectSSE,
546
- finishAuth,
547
- resumeAuth,
548
- callTool,
549
- listTools,
550
- listPrompts,
551
- getPrompt,
552
- listResources,
553
- readResource,
554
- sseClient: clientRef.current
555
- };
545
+ return useMemo(
546
+ () => ({
547
+ connections,
548
+ status,
549
+ isInitializing,
550
+ connect,
551
+ disconnect,
552
+ getConnection,
553
+ getConnectionByServerId,
554
+ isServerConnected,
555
+ getTools,
556
+ refresh,
557
+ connectSSE,
558
+ disconnectSSE,
559
+ finishAuth,
560
+ resumeAuth,
561
+ callTool,
562
+ listTools,
563
+ listPrompts,
564
+ getPrompt,
565
+ listResources,
566
+ readResource,
567
+ sseClient
568
+ }),
569
+ [
570
+ connections,
571
+ status,
572
+ isInitializing,
573
+ connect,
574
+ disconnect,
575
+ getConnection,
576
+ getConnectionByServerId,
577
+ isServerConnected,
578
+ getTools,
579
+ refresh,
580
+ connectSSE,
581
+ disconnectSSE,
582
+ finishAuth,
583
+ resumeAuth,
584
+ callTool,
585
+ listTools,
586
+ listPrompts,
587
+ getPrompt,
588
+ listResources,
589
+ readResource,
590
+ sseClient
591
+ ]
592
+ );
593
+ }
594
+
595
+ // src/client/core/constants.ts
596
+ var SANDBOX_PROXY_READY_METHOD = "ui/notifications/sandbox-proxy-ready";
597
+ var SANDBOX_RESOURCE_READY_METHOD = "ui/notifications/sandbox-resource-ready";
598
+ var APP_HOST_DEFAULTS = {
599
+ /** Default timeout for waiting for the sandbox proxy to be ready (ms). */
600
+ SANDBOX_TIMEOUT_MS: 1e4,
601
+ /** Default host info reported to guest apps. */
602
+ HOST_INFO: { name: "mcp-ts-host", version: "1.0.0" },
603
+ /** Supported MCP App URI schemes. */
604
+ URI_SCHEMES: ["ui://", "mcp-app://"],
605
+ /** Default theme for the host context. */
606
+ THEME: "dark",
607
+ /** Default platform for the host context. */
608
+ PLATFORM: "web",
609
+ /** Default max height for the iframe container (px). */
610
+ MAX_HEIGHT: 6e3
611
+ };
612
+
613
+ // src/client/utils/app-host-utils.ts
614
+ var DEFAULT_SANDBOX_TIMEOUT_MS = APP_HOST_DEFAULTS.SANDBOX_TIMEOUT_MS;
615
+ async function setupSandboxProxyIframe(iframe, sandboxProxyUrl) {
616
+ iframe.style.width = "100%";
617
+ iframe.style.height = "100%";
618
+ iframe.style.border = "none";
619
+ iframe.style.backgroundColor = "transparent";
620
+ iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-downloads");
621
+ const onReady = new Promise((resolve, reject) => {
622
+ let settled = false;
623
+ const cleanup = () => {
624
+ window.removeEventListener("message", messageListener);
625
+ iframe.removeEventListener("error", errorListener);
626
+ };
627
+ const timeoutId = setTimeout(() => {
628
+ if (!settled) {
629
+ settled = true;
630
+ cleanup();
631
+ reject(new Error("Timed out waiting for sandbox proxy iframe to be ready"));
632
+ }
633
+ }, DEFAULT_SANDBOX_TIMEOUT_MS);
634
+ const messageListener = (event) => {
635
+ if (event.source === iframe.contentWindow) {
636
+ if (event.data?.method === SANDBOX_PROXY_READY_METHOD) {
637
+ if (!settled) {
638
+ settled = true;
639
+ clearTimeout(timeoutId);
640
+ cleanup();
641
+ resolve();
642
+ }
643
+ }
644
+ }
645
+ };
646
+ const errorListener = () => {
647
+ if (!settled) {
648
+ settled = true;
649
+ clearTimeout(timeoutId);
650
+ cleanup();
651
+ reject(new Error("Failed to load sandbox proxy iframe"));
652
+ }
653
+ };
654
+ window.addEventListener("message", messageListener);
655
+ iframe.addEventListener("error", errorListener);
656
+ });
657
+ iframe.src = sandboxProxyUrl.href;
658
+ return { onReady };
556
659
  }
557
- var HOST_INFO = { name: "mcp-ts-host", version: "1.0.0" };
558
- var SANDBOX_PERMISSIONS = [
559
- "allow-scripts",
560
- // Required for app JavaScript execution
561
- "allow-forms",
562
- // Required for form submissions
563
- "allow-same-origin",
564
- // Required for Blob URL correctness
565
- "allow-modals",
566
- // Required for dialogs/alerts
567
- "allow-popups",
568
- // Required for opening links
569
- "allow-downloads"
570
- // Required for file downloads
571
- ].join(" ");
572
- var MCP_URI_SCHEMES = ["ui://", "mcp-app://"];
660
+
661
+ // src/client/core/app-host.ts
662
+ var DEFAULT_MCP_APP_CSP = {
663
+ "default-src": "'self'",
664
+ "script-src": "'self' 'unsafe-inline' 'unsafe-eval' https: blob:",
665
+ "style-src": "'self' 'unsafe-inline' https:",
666
+ "connect-src": "'self' https: wss:",
667
+ "img-src": "'self' data: https: blob:",
668
+ "font-src": "'self' data: https:",
669
+ "media-src": "'self' https: blob:",
670
+ "frame-src": "'none'",
671
+ "object-src": "'none'",
672
+ "base-uri": "'self'"
673
+ };
674
+ var HOST_INFO = APP_HOST_DEFAULTS.HOST_INFO;
675
+ var MCP_URI_SCHEMES = APP_HOST_DEFAULTS.URI_SCHEMES;
573
676
  var AppHost = class {
574
677
  constructor(client, iframe, options) {
575
678
  this.client = client;
@@ -578,10 +681,12 @@ var AppHost = class {
578
681
  __publicField(this, "sessionId");
579
682
  __publicField(this, "resourceCache", /* @__PURE__ */ new Map());
580
683
  __publicField(this, "debug");
581
- /** Callback for app messages (e.g., chat messages from the app) */
684
+ __publicField(this, "sandboxConfig");
685
+ __publicField(this, "options");
582
686
  __publicField(this, "onAppMessage");
583
- this.debug = options?.debug ?? false;
584
- this.configureSandbox();
687
+ this.options = options || {};
688
+ this.debug = this.options.debug ?? false;
689
+ this.sandboxConfig = this.options.sandbox;
585
690
  this.bridge = this.initializeBridge();
586
691
  }
587
692
  // ============================================
@@ -608,19 +713,35 @@ var AppHost = class {
608
713
  }
609
714
  }
610
715
  /**
611
- * Launch an MCP App from a URL or MCP resource URI.
716
+ * Launch an MCP App from a URL, MCP resource URI, or RAW HTML.
612
717
  * Loads the HTML first, then establishes bridge connection.
613
718
  */
614
- async launch(url, sessionId) {
719
+ async launch(source, sessionId) {
615
720
  if (sessionId) this.sessionId = sessionId;
616
721
  const initializedPromise = this.onAppReady();
617
- if (this.isMcpUri(url)) {
618
- await this.launchMcpApp(url);
619
- } else {
620
- this.iframe.src = url;
722
+ let htmlToRender = source.html;
723
+ if (!htmlToRender && source.uri) {
724
+ if (this.isMcpUri(source.uri)) {
725
+ htmlToRender = await this.readMcpAppHtml(source.uri);
726
+ }
727
+ }
728
+ if (!htmlToRender && source.uri && !this.isMcpUri(source.uri)) {
729
+ this.iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-downloads");
730
+ this.iframe.src = source.uri;
731
+ await this.onIframeReady();
732
+ await this.connectBridge();
733
+ } else if (htmlToRender) {
734
+ if (!this.sandboxConfig) {
735
+ throw new Error("Sandbox configuration requires a proxy URL to render HTML safely.");
736
+ }
737
+ await this.launchSandboxedHtml(htmlToRender, this.sandboxConfig);
738
+ await this.connectBridge();
739
+ this.log("Sending HTML resource to sandbox proxy (MCP Apps notification)");
740
+ await this.bridge.sendSandboxResourceReady({
741
+ html: htmlToRender,
742
+ csp: this.sandboxConfig.csp
743
+ });
621
744
  }
622
- await this.onIframeReady();
623
- await this.connectBridge();
624
745
  this.log("Waiting for app initialization");
625
746
  await Promise.race([
626
747
  initializedPromise,
@@ -631,6 +752,19 @@ var AppHost = class {
631
752
  ]);
632
753
  this.log("App launched and ready");
633
754
  }
755
+ // Set host context manually
756
+ setHostContext(context) {
757
+ this.options.hostContext = context;
758
+ if (this.bridge) {
759
+ this.bridge.setHostContext(context);
760
+ }
761
+ }
762
+ // Send streaming inputs manually
763
+ sendToolInputPartial(params) {
764
+ if (this.bridge) {
765
+ this.bridge.sendToolInputPartial(params);
766
+ }
767
+ }
634
768
  /**
635
769
  * Wait for app to signal initialization complete
636
770
  */
@@ -681,14 +815,17 @@ var AppHost = class {
681
815
  this.log("Sending tool cancellation to app");
682
816
  this.bridge.sendToolCancelled({ reason });
683
817
  }
818
+ /**
819
+ * Tell the guest UI the resource is being torn down (unload / cleanup).
820
+ * Forwards to {@link AppBridge.teardownResource} on `@modelcontextprotocol/ext-apps/app-bridge`.
821
+ */
822
+ teardownResource(params = {}) {
823
+ this.log("Sending resource teardown to app");
824
+ this.bridge.teardownResource(params);
825
+ }
684
826
  // ============================================
685
827
  // Private: Initialization
686
828
  // ============================================
687
- configureSandbox() {
688
- if (this.iframe.sandbox.value !== SANDBOX_PERMISSIONS) {
689
- this.iframe.sandbox.value = SANDBOX_PERMISSIONS;
690
- }
691
- }
692
829
  initializeBridge() {
693
830
  const bridge = new AppBridge(
694
831
  null,
@@ -697,12 +834,10 @@ var AppHost = class {
697
834
  openLinks: {},
698
835
  serverTools: {},
699
836
  logging: {},
700
- // Declare support for model context updates
701
837
  updateModelContext: { text: {} }
702
838
  },
703
839
  {
704
- // Initial host context
705
- hostContext: {
840
+ hostContext: this.options.hostContext || {
706
841
  theme: "dark",
707
842
  platform: "web",
708
843
  containerDimensions: { maxHeight: 6e3 },
@@ -711,19 +846,56 @@ var AppHost = class {
711
846
  }
712
847
  }
713
848
  );
849
+ bridge.fallbackRequestHandler = this.options.onFallbackRequest;
714
850
  bridge.oncalltool = (params) => this.handleToolCall(params);
715
- bridge.onopenlink = this.handleOpenLink.bind(this);
716
- bridge.onmessage = this.handleMessage.bind(this);
717
- bridge.onloggingmessage = (params) => this.log(`App log [${params.level}]: ${params.data}`);
851
+ if (this.options.onReadResource) {
852
+ bridge.onreadresource = async (params) => {
853
+ const resp = await this.options.onReadResource(params.uri);
854
+ return {
855
+ contents: resp.contents.map((c) => ({
856
+ uri: params.uri,
857
+ text: c.text,
858
+ blob: c.blob
859
+ }))
860
+ };
861
+ };
862
+ }
863
+ bridge.onopenlink = async (params, extra) => {
864
+ if (this.options.onOpenLink) {
865
+ return await this.options.onOpenLink(params, extra);
866
+ }
867
+ return this.handleOpenLink(params);
868
+ };
869
+ bridge.onmessage = async (params, extra) => {
870
+ if (this.options.onMessage) {
871
+ return await this.options.onMessage(params, extra);
872
+ }
873
+ return this.handleMessage(params);
874
+ };
875
+ bridge.onloggingmessage = (params) => {
876
+ this.log(`App log [${params.level}]: ${params.data}`);
877
+ if (this.options.onLoggingMessage) {
878
+ this.options.onLoggingMessage(params);
879
+ }
880
+ };
718
881
  bridge.onupdatemodelcontext = async () => ({});
719
- bridge.onsizechange = async ({ width, height }) => {
720
- if (height !== void 0) this.iframe.style.height = `${height}px`;
721
- if (width !== void 0) this.iframe.style.minWidth = `min(${width}px, 100%)`;
882
+ bridge.onsizechange = async (params) => {
883
+ const { width, height } = params;
884
+ if (height !== void 0 && height > 0) {
885
+ this.iframe.style.height = `${height}px`;
886
+ }
887
+ if (width !== void 0 && width > 0) this.iframe.style.minWidth = `min(${width}px, 100%)`;
888
+ if (this.options.onSizeChanged) {
889
+ this.options.onSizeChanged(params);
890
+ }
722
891
  return {};
723
892
  };
724
- bridge.onrequestdisplaymode = async (params) => ({
725
- mode: params.mode === "fullscreen" ? "fullscreen" : "inline"
726
- });
893
+ bridge.onrequestdisplaymode = async (params, extra) => {
894
+ if (this.options.onRequestDisplayMode) {
895
+ return await this.options.onRequestDisplayMode(params, extra);
896
+ }
897
+ return { mode: params.mode === "fullscreen" ? "fullscreen" : "inline" };
898
+ };
727
899
  return bridge;
728
900
  }
729
901
  async connectBridge() {
@@ -737,6 +909,9 @@ var AppHost = class {
737
909
  this.log("Bridge connected successfully");
738
910
  } catch (error) {
739
911
  this.log("Bridge connection failed", "error");
912
+ if (this.options.onError) {
913
+ this.options.onError(error instanceof Error ? error : new Error(String(error)));
914
+ }
740
915
  throw error;
741
916
  }
742
917
  }
@@ -744,8 +919,11 @@ var AppHost = class {
744
919
  // Private: Bridge Event Handlers
745
920
  // ============================================
746
921
  async handleToolCall(params) {
747
- if (!this.client.isConnected()) {
748
- throw new Error("Client disconnected");
922
+ if (this.options.onCallTool) {
923
+ return await this.options.onCallTool(params);
924
+ }
925
+ if (!this.client || !this.client.isConnected()) {
926
+ throw new Error("Client disconnected or not provided");
749
927
  }
750
928
  const sessionId = await this.getSessionId();
751
929
  if (!sessionId) {
@@ -769,13 +947,19 @@ var AppHost = class {
769
947
  // ============================================
770
948
  // Private: Resource Loading
771
949
  // ============================================
772
- async launchMcpApp(uri) {
773
- if (!this.client.isConnected()) {
774
- throw new Error("Client must be connected");
950
+ async launchSandboxedHtml(html, config) {
951
+ const sandboxUrlString = config.url instanceof URL ? config.url.href : config.url;
952
+ const url = new URL(sandboxUrlString, globalThis.location?.href);
953
+ if (config.csp && Object.keys(config.csp).length > 0) {
954
+ url.searchParams.set("csp", JSON.stringify(config.csp));
775
955
  }
956
+ const { onReady } = await setupSandboxProxyIframe(this.iframe, url);
957
+ await onReady;
958
+ }
959
+ async readMcpAppHtml(uri) {
776
960
  const sessionId = await this.getSessionId();
777
- if (!sessionId) {
778
- throw new Error("No active session");
961
+ if (!sessionId && !this.options.onReadResource) {
962
+ throw new Error("No active session.");
779
963
  }
780
964
  const response = await this.fetchResourceWithCache(sessionId, uri);
781
965
  if (!response?.contents?.length) {
@@ -786,10 +970,18 @@ var AppHost = class {
786
970
  if (!html) {
787
971
  throw new Error(`Invalid content in resource: ${uri}`);
788
972
  }
789
- const blob = new Blob([html], { type: "text/html" });
790
- this.iframe.src = URL.createObjectURL(blob);
973
+ return html;
791
974
  }
792
975
  async fetchResourceWithCache(sessionId, uri) {
976
+ if (this.options.onReadResource) {
977
+ return await this.options.onReadResource(uri);
978
+ }
979
+ if (!sessionId) {
980
+ throw new Error("No active session");
981
+ }
982
+ if (!this.client) {
983
+ throw new Error("No client to read resource from");
984
+ }
793
985
  if (this.hasClientCache()) {
794
986
  return this.client.getOrFetchResource(sessionId, uri);
795
987
  }
@@ -802,8 +994,11 @@ var AppHost = class {
802
994
  }
803
995
  async preloadResource(uri) {
804
996
  try {
997
+ if (this.options.onReadResource) {
998
+ return await this.options.onReadResource(uri);
999
+ }
805
1000
  const sessionId = await this.getSessionId();
806
- if (!sessionId) return null;
1001
+ if (!sessionId || !this.client) return null;
807
1002
  return await this.client.readResource(sessionId, uri);
808
1003
  } catch (error) {
809
1004
  this.log(`Preload failed for ${uri}`, "warn");
@@ -815,6 +1010,7 @@ var AppHost = class {
815
1010
  // ============================================
816
1011
  async getSessionId() {
817
1012
  if (this.sessionId) return this.sessionId;
1013
+ if (!this.client) return void 0;
818
1014
  const result = await this.client.getSessions();
819
1015
  return result.sessions?.[0]?.sessionId;
820
1016
  }
@@ -822,6 +1018,7 @@ var AppHost = class {
822
1018
  return MCP_URI_SCHEMES.some((scheme) => url.startsWith(scheme));
823
1019
  }
824
1020
  hasClientCache() {
1021
+ if (!this.client) return false;
825
1022
  return "getOrFetchResource" in this.client && typeof this.client.getOrFetchResource === "function";
826
1023
  }
827
1024
  extractUiResourceUri(tool) {
@@ -865,10 +1062,7 @@ function useAppHost(client, iframeRef, options) {
865
1062
  initializingRef.current = true;
866
1063
  const initHost = async () => {
867
1064
  try {
868
- const appHost = new AppHost(client, iframeRef.current);
869
- appHost.onAppMessage = (params) => {
870
- onMessageRef.current?.(params);
871
- };
1065
+ const appHost = new AppHost(client, iframeRef.current, options);
872
1066
  setHost(appHost);
873
1067
  await appHost.start();
874
1068
  } catch (err) {
@@ -884,39 +1078,106 @@ function useAppHost(client, iframeRef, options) {
884
1078
  }, [client, iframeRef]);
885
1079
  return { host, error };
886
1080
  }
887
- var McpAppRenderer = memo(function McpAppRenderer2({
888
- mcpClient,
1081
+ var McpAppViewInner = forwardRef(function McpAppView({
1082
+ clientRef,
889
1083
  name,
1084
+ toolResourceUri,
1085
+ html,
890
1086
  input,
891
1087
  result,
892
- status,
893
- className
894
- }) {
895
- const getAppMetadata = useCallback(() => {
896
- if (!mcpClient) return void 0;
897
- const extractedName = extractToolName(name);
898
- for (const conn of mcpClient.connections) {
899
- for (const tool of conn.tools) {
900
- const candidateName = extractToolName(tool.name);
901
- const resourceUri = tool.mcpApp?.resourceUri ?? tool._meta?.ui?.resourceUri ?? tool._meta?.["ui/resourceUri"];
902
- if (resourceUri && candidateName === extractedName) {
903
- return {
904
- toolName: candidateName,
905
- resourceUri,
906
- sessionId: conn.sessionId
907
- };
1088
+ status = "idle",
1089
+ toolInputPartial,
1090
+ toolCancelled,
1091
+ sandbox,
1092
+ hostContext,
1093
+ onCallTool,
1094
+ onReadResource,
1095
+ onFallbackRequest,
1096
+ onMessage,
1097
+ onOpenLink,
1098
+ onLoggingMessage,
1099
+ onSizeChanged,
1100
+ onError: onHostError,
1101
+ className,
1102
+ loader
1103
+ }, ref) {
1104
+ const mcpClient = clientRef.current;
1105
+ const metadata = getMcpAppMetadata(mcpClient, name);
1106
+ const sseClient = mcpClient?.sseClient ?? null;
1107
+ const resourceUri = toolResourceUri || metadata?.resourceUri;
1108
+ const appSessionId = metadata?.sessionId;
1109
+ const iframeRef = useRef(null);
1110
+ const containerRef = useRef(null);
1111
+ const preFullscreenHeightRef = useRef(null);
1112
+ const displayModeRef = useRef("inline");
1113
+ const [displayMode, setDisplayMode] = useState("inline");
1114
+ const setDisplayModeWithRef = (mode) => {
1115
+ displayModeRef.current = mode;
1116
+ setDisplayMode(mode);
1117
+ };
1118
+ const { host, error: hostError } = useAppHost(sseClient, iframeRef, {
1119
+ sandbox,
1120
+ hostContext,
1121
+ onCallTool,
1122
+ onReadResource,
1123
+ onFallbackRequest,
1124
+ onMessage,
1125
+ onOpenLink,
1126
+ onLoggingMessage,
1127
+ // Intercept onSizeChanged: when exiting fullscreen, ignore guest resize events
1128
+ // that arrive with the shrunken viewport dimensions, and restore the pre-fullscreen height.
1129
+ onSizeChanged: (params) => {
1130
+ if (displayModeRef.current === "inline" && preFullscreenHeightRef.current !== null) {
1131
+ const savedHeight = preFullscreenHeightRef.current;
1132
+ preFullscreenHeightRef.current = null;
1133
+ if (iframeRef.current) {
1134
+ iframeRef.current.style.height = `${savedHeight}px`;
908
1135
  }
1136
+ return;
909
1137
  }
1138
+ onSizeChanged?.(params);
1139
+ },
1140
+ onError: onHostError,
1141
+ onRequestDisplayMode: async (params) => {
1142
+ if (params.mode === "fullscreen") {
1143
+ if (iframeRef.current) {
1144
+ const h = iframeRef.current.getBoundingClientRect().height;
1145
+ if (h > 0) preFullscreenHeightRef.current = h;
1146
+ }
1147
+ try {
1148
+ if (containerRef.current?.requestFullscreen) {
1149
+ await containerRef.current.requestFullscreen();
1150
+ } else if (containerRef.current?.webkitRequestFullscreen) {
1151
+ await containerRef.current.webkitRequestFullscreen();
1152
+ }
1153
+ setDisplayModeWithRef("fullscreen");
1154
+ } catch (err) {
1155
+ console.warn("[McpAppHost] requestFullscreen failed:", err);
1156
+ preFullscreenHeightRef.current = null;
1157
+ return { mode: "inline" };
1158
+ }
1159
+ } else if (params.mode === "inline") {
1160
+ restoreHeightAfterFullscreen();
1161
+ try {
1162
+ if (document.fullscreenElement) {
1163
+ await document.exitFullscreen();
1164
+ }
1165
+ } catch (err) {
1166
+ }
1167
+ setDisplayModeWithRef("inline");
1168
+ }
1169
+ return { mode: params.mode };
910
1170
  }
911
- return void 0;
912
- }, [mcpClient, name]);
913
- const metadata = getAppMetadata();
914
- const sseClient = mcpClient?.sseClient ?? null;
915
- if (!metadata || !sseClient) {
916
- return null;
917
- }
918
- const iframeRef = useRef(null);
919
- const { host, error: hostError } = useAppHost(sseClient, iframeRef);
1171
+ });
1172
+ useImperativeHandle(
1173
+ ref,
1174
+ () => ({
1175
+ teardownResource: (params) => {
1176
+ host?.teardownResource(params ?? {});
1177
+ }
1178
+ }),
1179
+ [host]
1180
+ );
920
1181
  const [isLaunched, setIsLaunched] = useState(false);
921
1182
  const [error, setError] = useState(null);
922
1183
  const sentInputRef = useRef(false);
@@ -925,19 +1186,41 @@ var McpAppRenderer = memo(function McpAppRenderer2({
925
1186
  const lastResultRef = useRef(result);
926
1187
  const lastStatusRef = useRef(status);
927
1188
  useEffect(() => {
928
- if (!host || !metadata.resourceUri || !metadata.sessionId) return;
929
- host.launch(metadata.resourceUri, metadata.sessionId).then(() => setIsLaunched(true)).catch((err) => setError(err instanceof Error ? err : new Error(String(err))));
930
- }, [host, metadata.resourceUri, metadata.sessionId]);
1189
+ setIsLaunched(false);
1190
+ setError(null);
1191
+ }, [resourceUri, appSessionId]);
1192
+ const restoreHeightAfterFullscreen = () => {
1193
+ const savedHeight = preFullscreenHeightRef.current;
1194
+ if (savedHeight && iframeRef.current) {
1195
+ iframeRef.current.style.height = `${savedHeight}px`;
1196
+ }
1197
+ preFullscreenHeightRef.current = null;
1198
+ };
1199
+ useEffect(() => {
1200
+ const onFullscreenChange = () => {
1201
+ const isFullscreen = !!document.fullscreenElement;
1202
+ if (!isFullscreen && displayModeRef.current === "fullscreen") {
1203
+ restoreHeightAfterFullscreen();
1204
+ setDisplayModeWithRef("inline");
1205
+ }
1206
+ };
1207
+ document.addEventListener("fullscreenchange", onFullscreenChange);
1208
+ return () => document.removeEventListener("fullscreenchange", onFullscreenChange);
1209
+ }, []);
931
1210
  useEffect(() => {
932
- if (!host || !isLaunched || !input) return;
1211
+ if (!host || !resourceUri && !html) return;
1212
+ host.launch({ uri: resourceUri, html }, appSessionId).then(() => setIsLaunched(true)).catch((err) => setError(err instanceof Error ? err : new Error(String(err))));
1213
+ }, [host, resourceUri, html, appSessionId]);
1214
+ useEffect(() => {
1215
+ if (!host || !isLaunched || !resourceUri || !appSessionId || !input) return;
933
1216
  if (!sentInputRef.current || JSON.stringify(input) !== JSON.stringify(lastInputRef.current)) {
934
1217
  sentInputRef.current = true;
935
1218
  lastInputRef.current = input;
936
1219
  host.sendToolInput(input);
937
1220
  }
938
- }, [host, isLaunched, input]);
1221
+ }, [host, isLaunched, input, resourceUri, appSessionId, name]);
939
1222
  useEffect(() => {
940
- if (!host || !isLaunched || result === void 0) return;
1223
+ if (!host || !isLaunched || !resourceUri || !appSessionId || result === void 0) return;
941
1224
  if (status !== "complete") return;
942
1225
  if (!sentResultRef.current || JSON.stringify(result) !== JSON.stringify(lastResultRef.current)) {
943
1226
  sentResultRef.current = true;
@@ -945,7 +1228,7 @@ var McpAppRenderer = memo(function McpAppRenderer2({
945
1228
  const formattedResult = typeof result === "string" ? { content: [{ type: "text", text: result }] } : result;
946
1229
  host.sendToolResult(formattedResult);
947
1230
  }
948
- }, [host, isLaunched, result, status]);
1231
+ }, [host, isLaunched, result, status, resourceUri, appSessionId, name]);
949
1232
  useEffect(() => {
950
1233
  if (status === "executing" && lastStatusRef.current !== "executing") {
951
1234
  sentInputRef.current = false;
@@ -953,6 +1236,28 @@ var McpAppRenderer = memo(function McpAppRenderer2({
953
1236
  }
954
1237
  lastStatusRef.current = status;
955
1238
  }, [status]);
1239
+ useEffect(() => {
1240
+ if (!host) return;
1241
+ const mergedCtx = {
1242
+ theme: APP_HOST_DEFAULTS.THEME,
1243
+ platform: APP_HOST_DEFAULTS.PLATFORM,
1244
+ containerDimensions: { maxHeight: APP_HOST_DEFAULTS.MAX_HEIGHT },
1245
+ availableDisplayModes: ["inline", "fullscreen"],
1246
+ ...hostContext || {},
1247
+ displayMode
1248
+ // always override with our authoritative state
1249
+ };
1250
+ host.setHostContext(mergedCtx);
1251
+ }, [host, hostContext, displayMode]);
1252
+ useEffect(() => {
1253
+ if (host && toolInputPartial) host.sendToolInputPartial(toolInputPartial);
1254
+ }, [host, toolInputPartial]);
1255
+ useEffect(() => {
1256
+ if (host && toolCancelled) host.sendToolCancelled("User cancelled");
1257
+ }, [host, toolCancelled]);
1258
+ if (!metadata && !html && !toolResourceUri) {
1259
+ return null;
1260
+ }
956
1261
  const displayError = error || hostError;
957
1262
  if (displayError) {
958
1263
  return /* @__PURE__ */ jsxs("div", { className: `p-4 bg-red-900/20 border border-red-700 rounded text-red-200 ${className || ""}`, children: [
@@ -960,49 +1265,85 @@ var McpAppRenderer = memo(function McpAppRenderer2({
960
1265
  displayError.message || String(displayError)
961
1266
  ] });
962
1267
  }
963
- return /* @__PURE__ */ jsxs("div", { className: `w-full border border-gray-700 rounded overflow-hidden bg-white min-h-96 my-2 relative ${className || ""}`, children: [
1268
+ const opacityClass = isLaunched ? "opacity-100" : "opacity-0";
1269
+ let containerClass = `w-full border border-gray-700 rounded bg-transparent my-2 relative ${className || ""}`;
1270
+ let iframeClass = `w-full transition-opacity duration-300 ${opacityClass}`;
1271
+ if (displayMode === "fullscreen") {
1272
+ containerClass = `w-full h-full bg-black m-0 p-0 flex flex-col relative`;
1273
+ iframeClass = `w-full flex-1 transition-opacity duration-300 ${opacityClass}`;
1274
+ }
1275
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: containerClass, children: [
1276
+ displayMode === "fullscreen" && /* @__PURE__ */ jsx("div", { className: "absolute top-0 right-0 p-2 z-[100000] w-full bg-gradient-to-b from-black/80 to-transparent flex justify-end", children: /* @__PURE__ */ jsxs(
1277
+ "button",
1278
+ {
1279
+ title: "Exit Fullscreen",
1280
+ onClick: () => {
1281
+ restoreHeightAfterFullscreen();
1282
+ if (document.fullscreenElement) document.exitFullscreen();
1283
+ setDisplayModeWithRef("inline");
1284
+ },
1285
+ className: "px-4 py-2 bg-gray-800 hover:bg-gray-700 text-white rounded-md shadow flex items-center gap-2 border border-gray-600 transition-colors",
1286
+ children: [
1287
+ /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3" }) }),
1288
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Exit" })
1289
+ ]
1290
+ }
1291
+ ) }),
964
1292
  /* @__PURE__ */ jsx(
965
1293
  "iframe",
966
1294
  {
967
1295
  ref: iframeRef,
968
1296
  sandbox: "allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-downloads",
969
- className: "w-full h-full min-h-96",
970
- style: { height: "auto" },
1297
+ allow: "fullscreen",
1298
+ className: iframeClass,
971
1299
  title: "MCP App"
972
1300
  }
973
1301
  ),
974
- !isLaunched && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-gray-900/50 flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsx("div", { className: "w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" }) })
1302
+ !isLaunched && loader && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-transparent flex items-center justify-center pointer-events-none z-10", children: loader })
975
1303
  ] });
976
1304
  });
1305
+ var McpAppView2 = memo(McpAppViewInner);
1306
+ McpAppView2.displayName = "McpAppView";
977
1307
  function useMcpApps(mcpClient) {
1308
+ const clientRef = useRef(mcpClient);
1309
+ clientRef.current = mcpClient;
978
1310
  const getAppMetadata = useCallback(
979
- (toolName) => {
980
- if (!mcpClient) return void 0;
981
- const extractedName = extractToolName(toolName);
982
- for (const conn of mcpClient.connections) {
983
- for (const tool of conn.tools) {
984
- const candidateName = extractToolName(tool.name);
985
- const resourceUri = tool.mcpApp?.resourceUri ?? tool._meta?.ui?.resourceUri ?? tool._meta?.["ui/resourceUri"];
986
- if (resourceUri && candidateName === extractedName) {
987
- return {
988
- toolName: candidateName,
989
- resourceUri,
990
- sessionId: conn.sessionId
991
- };
992
- }
993
- }
994
- }
995
- return void 0;
996
- },
997
- [mcpClient]
1311
+ (toolName) => getMcpAppMetadata(clientRef.current, toolName),
1312
+ []
998
1313
  );
1314
+ const McpAppRenderer = useMemo(() => {
1315
+ const Inner = forwardRef(function McpAppRenderer2(props, ref) {
1316
+ return /* @__PURE__ */ jsx(McpAppView2, { ref, clientRef, ...props });
1317
+ });
1318
+ const Renderer = memo(Inner);
1319
+ Renderer.displayName = "McpAppRenderer";
1320
+ return Renderer;
1321
+ }, []);
999
1322
  return { getAppMetadata, McpAppRenderer };
1000
1323
  }
1001
1324
  function extractToolName(fullName) {
1002
1325
  const match = fullName.match(/(?:tool_[^_]+_)?(.+)$/);
1003
1326
  return match?.[1] || fullName;
1004
1327
  }
1328
+ function getMcpAppMetadata(mcpClient, toolName) {
1329
+ if (!mcpClient) return void 0;
1330
+ const extractedName = extractToolName(toolName);
1331
+ for (const conn of mcpClient.connections) {
1332
+ for (const tool of conn.tools) {
1333
+ const candidateName = extractToolName(tool.name);
1334
+ const resourceUri = tool.mcpApp?.resourceUri ?? tool._meta?.ui?.resourceUri ?? tool._meta?.["ui/resourceUri"];
1335
+ if (resourceUri && candidateName === extractedName) {
1336
+ return {
1337
+ toolName: candidateName,
1338
+ resourceUri,
1339
+ sessionId: conn.sessionId
1340
+ };
1341
+ }
1342
+ }
1343
+ }
1344
+ return void 0;
1345
+ }
1005
1346
 
1006
- export { AppHost, SSEClient, useAppHost, useMcp, useMcpApps };
1347
+ export { APP_HOST_DEFAULTS, AppHost, DEFAULT_MCP_APP_CSP, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, SSEClient, useAppHost, useMcp, useMcpApps };
1007
1348
  //# sourceMappingURL=react.mjs.map
1008
1349
  //# sourceMappingURL=react.mjs.map