@mcp-ts/sdk 1.3.10 → 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 (44) hide show
  1. package/dist/adapters/langchain-adapter.js.map +1 -1
  2. package/dist/adapters/langchain-adapter.mjs.map +1 -1
  3. package/dist/client/index.d.mts +3 -189
  4. package/dist/client/index.d.ts +3 -189
  5. package/dist/client/index.js +218 -54
  6. package/dist/client/index.js.map +1 -1
  7. package/dist/client/index.mjs +215 -55
  8. package/dist/client/index.mjs.map +1 -1
  9. package/dist/client/react.d.mts +21 -14
  10. package/dist/client/react.d.ts +21 -14
  11. package/dist/client/react.js +402 -83
  12. package/dist/client/react.js.map +1 -1
  13. package/dist/client/react.mjs +400 -85
  14. package/dist/client/react.mjs.map +1 -1
  15. package/dist/client/vue.d.mts +3 -2
  16. package/dist/client/vue.d.ts +3 -2
  17. package/dist/client/vue.js +239 -63
  18. package/dist/client/vue.js.map +1 -1
  19. package/dist/client/vue.mjs +236 -64
  20. package/dist/client/vue.mjs.map +1 -1
  21. package/dist/index-CQr9q0bF.d.mts +295 -0
  22. package/dist/index-nE_7Io0I.d.ts +295 -0
  23. package/dist/index.d.mts +2 -1
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.js +237 -58
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +230 -59
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/server/index.js +15 -4
  30. package/dist/server/index.js.map +1 -1
  31. package/dist/server/index.mjs +15 -4
  32. package/dist/server/index.mjs.map +1 -1
  33. package/package.json +13 -11
  34. package/src/adapters/langchain-adapter.ts +1 -1
  35. package/src/client/core/app-host.ts +252 -65
  36. package/src/client/core/constants.ts +30 -0
  37. package/src/client/index.ts +6 -1
  38. package/src/client/react/index.ts +1 -0
  39. package/src/client/react/use-app-host.ts +8 -15
  40. package/src/client/react/use-mcp-apps.tsx +221 -26
  41. package/src/client/react/use-mcp.ts +23 -12
  42. package/src/client/utils/app-host-utils.ts +62 -0
  43. package/src/client/vue/use-mcp.ts +23 -12
  44. package/src/server/mcp/oauth-client.ts +31 -8
@@ -356,18 +356,28 @@ function useMcp(options) {
356
356
  );
357
357
  }
358
358
  case "auth_required": {
359
- if (event.authUrl) {
360
- onLog?.("info", `OAuth required - redirecting to ${event.authUrl}`, { authUrl: event.authUrl });
361
- if (!suppressAuthRedirectSessionsRef.current.has(event.sessionId)) {
362
- if (onRedirect) {
363
- onRedirect(event.authUrl);
364
- } else if (typeof window !== "undefined") {
365
- window.location.href = event.authUrl;
366
- }
359
+ const url2 = (event.authUrl || "").trim();
360
+ if (!url2) {
361
+ onLog?.("error", "OAuth required but authorization URL is missing", { sessionId: event.sessionId });
362
+ return prev.map(
363
+ (c) => c.sessionId === event.sessionId ? {
364
+ ...c,
365
+ state: "FAILED",
366
+ error: "OAuth authorization URL not available",
367
+ authUrl: void 0
368
+ } : c
369
+ );
370
+ }
371
+ onLog?.("info", `OAuth required - redirecting to ${url2}`, { authUrl: url2 });
372
+ if (!suppressAuthRedirectSessionsRef.current.has(event.sessionId)) {
373
+ if (onRedirect) {
374
+ onRedirect(url2);
375
+ } else if (typeof window !== "undefined") {
376
+ window.location.href = url2;
367
377
  }
368
378
  }
369
379
  return prev.map(
370
- (c) => c.sessionId === event.sessionId ? { ...c, state: "AUTHENTICATING", authUrl: event.authUrl } : c
380
+ (c) => c.sessionId === event.sessionId ? { ...c, state: "AUTHENTICATING", authUrl: url2 } : c
371
381
  );
372
382
  }
373
383
  case "error": {
@@ -583,22 +593,88 @@ function useMcp(options) {
583
593
  ]
584
594
  );
585
595
  }
586
- var HOST_INFO = { name: "mcp-ts-host", version: "1.0.0" };
587
- var SANDBOX_PERMISSIONS = [
588
- "allow-scripts",
589
- // Required for app JavaScript execution
590
- "allow-forms",
591
- // Required for form submissions
592
- "allow-same-origin",
593
- // Required for Blob URL correctness
594
- "allow-modals",
595
- // Required for dialogs/alerts
596
- "allow-popups",
597
- // Required for opening links
598
- "allow-downloads"
599
- // Required for file downloads
600
- ].join(" ");
601
- var MCP_URI_SCHEMES = ["ui://", "mcp-app://"];
596
+
597
+ // src/client/core/constants.ts
598
+ var SANDBOX_PROXY_READY_METHOD = "ui/notifications/sandbox-proxy-ready";
599
+ var SANDBOX_RESOURCE_READY_METHOD = "ui/notifications/sandbox-resource-ready";
600
+ var APP_HOST_DEFAULTS = {
601
+ /** Default timeout for waiting for the sandbox proxy to be ready (ms). */
602
+ SANDBOX_TIMEOUT_MS: 1e4,
603
+ /** Default host info reported to guest apps. */
604
+ HOST_INFO: { name: "mcp-ts-host", version: "1.0.0" },
605
+ /** Supported MCP App URI schemes. */
606
+ URI_SCHEMES: ["ui://", "mcp-app://"],
607
+ /** Default theme for the host context. */
608
+ THEME: "dark",
609
+ /** Default platform for the host context. */
610
+ PLATFORM: "web",
611
+ /** Default max height for the iframe container (px). */
612
+ MAX_HEIGHT: 6e3
613
+ };
614
+
615
+ // src/client/utils/app-host-utils.ts
616
+ var DEFAULT_SANDBOX_TIMEOUT_MS = APP_HOST_DEFAULTS.SANDBOX_TIMEOUT_MS;
617
+ async function setupSandboxProxyIframe(iframe, sandboxProxyUrl) {
618
+ iframe.style.width = "100%";
619
+ iframe.style.height = "100%";
620
+ iframe.style.border = "none";
621
+ iframe.style.backgroundColor = "transparent";
622
+ iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-downloads");
623
+ const onReady = new Promise((resolve, reject) => {
624
+ let settled = false;
625
+ const cleanup = () => {
626
+ window.removeEventListener("message", messageListener);
627
+ iframe.removeEventListener("error", errorListener);
628
+ };
629
+ const timeoutId = setTimeout(() => {
630
+ if (!settled) {
631
+ settled = true;
632
+ cleanup();
633
+ reject(new Error("Timed out waiting for sandbox proxy iframe to be ready"));
634
+ }
635
+ }, DEFAULT_SANDBOX_TIMEOUT_MS);
636
+ const messageListener = (event) => {
637
+ if (event.source === iframe.contentWindow) {
638
+ if (event.data?.method === SANDBOX_PROXY_READY_METHOD) {
639
+ if (!settled) {
640
+ settled = true;
641
+ clearTimeout(timeoutId);
642
+ cleanup();
643
+ resolve();
644
+ }
645
+ }
646
+ }
647
+ };
648
+ const errorListener = () => {
649
+ if (!settled) {
650
+ settled = true;
651
+ clearTimeout(timeoutId);
652
+ cleanup();
653
+ reject(new Error("Failed to load sandbox proxy iframe"));
654
+ }
655
+ };
656
+ window.addEventListener("message", messageListener);
657
+ iframe.addEventListener("error", errorListener);
658
+ });
659
+ iframe.src = sandboxProxyUrl.href;
660
+ return { onReady };
661
+ }
662
+
663
+ // src/client/core/app-host.ts
664
+ var DEFAULT_MCP_APP_CSP = {
665
+ "default-src": "'self'",
666
+ "script-src": "'self' 'unsafe-inline' 'unsafe-eval' https: blob:",
667
+ "style-src": "'self' 'unsafe-inline' https:",
668
+ "connect-src": "'self' https: wss:",
669
+ "img-src": "'self' data: https: blob:",
670
+ "font-src": "'self' data: https:",
671
+ "media-src": "'self' https: blob:",
672
+ "frame-src": "'none'",
673
+ "object-src": "'none'",
674
+ "base-uri": "'self'"
675
+ };
676
+ var HOST_INFO = APP_HOST_DEFAULTS.HOST_INFO;
677
+ var MCP_URI_SCHEMES = APP_HOST_DEFAULTS.URI_SCHEMES;
602
678
  var AppHost = class {
603
679
  constructor(client, iframe, options) {
604
680
  this.client = client;
@@ -607,10 +683,12 @@ var AppHost = class {
607
683
  __publicField(this, "sessionId");
608
684
  __publicField(this, "resourceCache", /* @__PURE__ */ new Map());
609
685
  __publicField(this, "debug");
610
- /** Callback for app messages (e.g., chat messages from the app) */
686
+ __publicField(this, "sandboxConfig");
687
+ __publicField(this, "options");
611
688
  __publicField(this, "onAppMessage");
612
- this.debug = options?.debug ?? false;
613
- this.configureSandbox();
689
+ this.options = options || {};
690
+ this.debug = this.options.debug ?? false;
691
+ this.sandboxConfig = this.options.sandbox;
614
692
  this.bridge = this.initializeBridge();
615
693
  }
616
694
  // ============================================
@@ -637,19 +715,35 @@ var AppHost = class {
637
715
  }
638
716
  }
639
717
  /**
640
- * Launch an MCP App from a URL or MCP resource URI.
718
+ * Launch an MCP App from a URL, MCP resource URI, or RAW HTML.
641
719
  * Loads the HTML first, then establishes bridge connection.
642
720
  */
643
- async launch(url, sessionId) {
721
+ async launch(source, sessionId) {
644
722
  if (sessionId) this.sessionId = sessionId;
645
723
  const initializedPromise = this.onAppReady();
646
- if (this.isMcpUri(url)) {
647
- await this.launchMcpApp(url);
648
- } else {
649
- this.iframe.src = url;
724
+ let htmlToRender = source.html;
725
+ if (!htmlToRender && source.uri) {
726
+ if (this.isMcpUri(source.uri)) {
727
+ htmlToRender = await this.readMcpAppHtml(source.uri);
728
+ }
729
+ }
730
+ if (!htmlToRender && source.uri && !this.isMcpUri(source.uri)) {
731
+ this.iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-downloads");
732
+ this.iframe.src = source.uri;
733
+ await this.onIframeReady();
734
+ await this.connectBridge();
735
+ } else if (htmlToRender) {
736
+ if (!this.sandboxConfig) {
737
+ throw new Error("Sandbox configuration requires a proxy URL to render HTML safely.");
738
+ }
739
+ await this.launchSandboxedHtml(htmlToRender, this.sandboxConfig);
740
+ await this.connectBridge();
741
+ this.log("Sending HTML resource to sandbox proxy (MCP Apps notification)");
742
+ await this.bridge.sendSandboxResourceReady({
743
+ html: htmlToRender,
744
+ csp: this.sandboxConfig.csp
745
+ });
650
746
  }
651
- await this.onIframeReady();
652
- await this.connectBridge();
653
747
  this.log("Waiting for app initialization");
654
748
  await Promise.race([
655
749
  initializedPromise,
@@ -660,6 +754,19 @@ var AppHost = class {
660
754
  ]);
661
755
  this.log("App launched and ready");
662
756
  }
757
+ // Set host context manually
758
+ setHostContext(context) {
759
+ this.options.hostContext = context;
760
+ if (this.bridge) {
761
+ this.bridge.setHostContext(context);
762
+ }
763
+ }
764
+ // Send streaming inputs manually
765
+ sendToolInputPartial(params) {
766
+ if (this.bridge) {
767
+ this.bridge.sendToolInputPartial(params);
768
+ }
769
+ }
663
770
  /**
664
771
  * Wait for app to signal initialization complete
665
772
  */
@@ -710,14 +817,17 @@ var AppHost = class {
710
817
  this.log("Sending tool cancellation to app");
711
818
  this.bridge.sendToolCancelled({ reason });
712
819
  }
820
+ /**
821
+ * Tell the guest UI the resource is being torn down (unload / cleanup).
822
+ * Forwards to {@link AppBridge.teardownResource} on `@modelcontextprotocol/ext-apps/app-bridge`.
823
+ */
824
+ teardownResource(params = {}) {
825
+ this.log("Sending resource teardown to app");
826
+ this.bridge.teardownResource(params);
827
+ }
713
828
  // ============================================
714
829
  // Private: Initialization
715
830
  // ============================================
716
- configureSandbox() {
717
- if (this.iframe.sandbox.value !== SANDBOX_PERMISSIONS) {
718
- this.iframe.sandbox.value = SANDBOX_PERMISSIONS;
719
- }
720
- }
721
831
  initializeBridge() {
722
832
  const bridge = new appBridge.AppBridge(
723
833
  null,
@@ -726,12 +836,10 @@ var AppHost = class {
726
836
  openLinks: {},
727
837
  serverTools: {},
728
838
  logging: {},
729
- // Declare support for model context updates
730
839
  updateModelContext: { text: {} }
731
840
  },
732
841
  {
733
- // Initial host context
734
- hostContext: {
842
+ hostContext: this.options.hostContext || {
735
843
  theme: "dark",
736
844
  platform: "web",
737
845
  containerDimensions: { maxHeight: 6e3 },
@@ -740,19 +848,56 @@ var AppHost = class {
740
848
  }
741
849
  }
742
850
  );
851
+ bridge.fallbackRequestHandler = this.options.onFallbackRequest;
743
852
  bridge.oncalltool = (params) => this.handleToolCall(params);
744
- bridge.onopenlink = this.handleOpenLink.bind(this);
745
- bridge.onmessage = this.handleMessage.bind(this);
746
- bridge.onloggingmessage = (params) => this.log(`App log [${params.level}]: ${params.data}`);
853
+ if (this.options.onReadResource) {
854
+ bridge.onreadresource = async (params) => {
855
+ const resp = await this.options.onReadResource(params.uri);
856
+ return {
857
+ contents: resp.contents.map((c) => ({
858
+ uri: params.uri,
859
+ text: c.text,
860
+ blob: c.blob
861
+ }))
862
+ };
863
+ };
864
+ }
865
+ bridge.onopenlink = async (params, extra) => {
866
+ if (this.options.onOpenLink) {
867
+ return await this.options.onOpenLink(params, extra);
868
+ }
869
+ return this.handleOpenLink(params);
870
+ };
871
+ bridge.onmessage = async (params, extra) => {
872
+ if (this.options.onMessage) {
873
+ return await this.options.onMessage(params, extra);
874
+ }
875
+ return this.handleMessage(params);
876
+ };
877
+ bridge.onloggingmessage = (params) => {
878
+ this.log(`App log [${params.level}]: ${params.data}`);
879
+ if (this.options.onLoggingMessage) {
880
+ this.options.onLoggingMessage(params);
881
+ }
882
+ };
747
883
  bridge.onupdatemodelcontext = async () => ({});
748
- bridge.onsizechange = async ({ width, height }) => {
749
- if (height !== void 0) this.iframe.style.height = `${height}px`;
750
- if (width !== void 0) this.iframe.style.minWidth = `min(${width}px, 100%)`;
884
+ bridge.onsizechange = async (params) => {
885
+ const { width, height } = params;
886
+ if (height !== void 0 && height > 0) {
887
+ this.iframe.style.height = `${height}px`;
888
+ }
889
+ if (width !== void 0 && width > 0) this.iframe.style.minWidth = `min(${width}px, 100%)`;
890
+ if (this.options.onSizeChanged) {
891
+ this.options.onSizeChanged(params);
892
+ }
751
893
  return {};
752
894
  };
753
- bridge.onrequestdisplaymode = async (params) => ({
754
- mode: params.mode === "fullscreen" ? "fullscreen" : "inline"
755
- });
895
+ bridge.onrequestdisplaymode = async (params, extra) => {
896
+ if (this.options.onRequestDisplayMode) {
897
+ return await this.options.onRequestDisplayMode(params, extra);
898
+ }
899
+ return { mode: params.mode === "fullscreen" ? "fullscreen" : "inline" };
900
+ };
756
901
  return bridge;
757
902
  }
758
903
  async connectBridge() {
@@ -766,6 +911,9 @@ var AppHost = class {
766
911
  this.log("Bridge connected successfully");
767
912
  } catch (error) {
768
913
  this.log("Bridge connection failed", "error");
914
+ if (this.options.onError) {
915
+ this.options.onError(error instanceof Error ? error : new Error(String(error)));
916
+ }
769
917
  throw error;
770
918
  }
771
919
  }
@@ -773,8 +921,11 @@ var AppHost = class {
773
921
  // Private: Bridge Event Handlers
774
922
  // ============================================
775
923
  async handleToolCall(params) {
776
- if (!this.client.isConnected()) {
777
- throw new Error("Client disconnected");
924
+ if (this.options.onCallTool) {
925
+ return await this.options.onCallTool(params);
926
+ }
927
+ if (!this.client || !this.client.isConnected()) {
928
+ throw new Error("Client disconnected or not provided");
778
929
  }
779
930
  const sessionId = await this.getSessionId();
780
931
  if (!sessionId) {
@@ -798,13 +949,19 @@ var AppHost = class {
798
949
  // ============================================
799
950
  // Private: Resource Loading
800
951
  // ============================================
801
- async launchMcpApp(uri) {
802
- if (!this.client.isConnected()) {
803
- throw new Error("Client must be connected");
952
+ async launchSandboxedHtml(html, config) {
953
+ const sandboxUrlString = config.url instanceof URL ? config.url.href : config.url;
954
+ const url = new URL(sandboxUrlString, globalThis.location?.href);
955
+ if (config.csp && Object.keys(config.csp).length > 0) {
956
+ url.searchParams.set("csp", JSON.stringify(config.csp));
804
957
  }
958
+ const { onReady } = await setupSandboxProxyIframe(this.iframe, url);
959
+ await onReady;
960
+ }
961
+ async readMcpAppHtml(uri) {
805
962
  const sessionId = await this.getSessionId();
806
- if (!sessionId) {
807
- throw new Error("No active session");
963
+ if (!sessionId && !this.options.onReadResource) {
964
+ throw new Error("No active session.");
808
965
  }
809
966
  const response = await this.fetchResourceWithCache(sessionId, uri);
810
967
  if (!response?.contents?.length) {
@@ -815,10 +972,18 @@ var AppHost = class {
815
972
  if (!html) {
816
973
  throw new Error(`Invalid content in resource: ${uri}`);
817
974
  }
818
- const blob = new Blob([html], { type: "text/html" });
819
- this.iframe.src = URL.createObjectURL(blob);
975
+ return html;
820
976
  }
821
977
  async fetchResourceWithCache(sessionId, uri) {
978
+ if (this.options.onReadResource) {
979
+ return await this.options.onReadResource(uri);
980
+ }
981
+ if (!sessionId) {
982
+ throw new Error("No active session");
983
+ }
984
+ if (!this.client) {
985
+ throw new Error("No client to read resource from");
986
+ }
822
987
  if (this.hasClientCache()) {
823
988
  return this.client.getOrFetchResource(sessionId, uri);
824
989
  }
@@ -831,8 +996,11 @@ var AppHost = class {
831
996
  }
832
997
  async preloadResource(uri) {
833
998
  try {
999
+ if (this.options.onReadResource) {
1000
+ return await this.options.onReadResource(uri);
1001
+ }
834
1002
  const sessionId = await this.getSessionId();
835
- if (!sessionId) return null;
1003
+ if (!sessionId || !this.client) return null;
836
1004
  return await this.client.readResource(sessionId, uri);
837
1005
  } catch (error) {
838
1006
  this.log(`Preload failed for ${uri}`, "warn");
@@ -844,6 +1012,7 @@ var AppHost = class {
844
1012
  // ============================================
845
1013
  async getSessionId() {
846
1014
  if (this.sessionId) return this.sessionId;
1015
+ if (!this.client) return void 0;
847
1016
  const result = await this.client.getSessions();
848
1017
  return result.sessions?.[0]?.sessionId;
849
1018
  }
@@ -851,6 +1020,7 @@ var AppHost = class {
851
1020
  return MCP_URI_SCHEMES.some((scheme) => url.startsWith(scheme));
852
1021
  }
853
1022
  hasClientCache() {
1023
+ if (!this.client) return false;
854
1024
  return "getOrFetchResource" in this.client && typeof this.client.getOrFetchResource === "function";
855
1025
  }
856
1026
  extractUiResourceUri(tool) {
@@ -894,10 +1064,7 @@ function useAppHost(client, iframeRef, options) {
894
1064
  initializingRef.current = true;
895
1065
  const initHost = async () => {
896
1066
  try {
897
- const appHost = new AppHost(client, iframeRef.current);
898
- appHost.onAppMessage = (params) => {
899
- onMessageRef.current?.(params);
900
- };
1067
+ const appHost = new AppHost(client, iframeRef.current, options);
901
1068
  setHost(appHost);
902
1069
  await appHost.start();
903
1070
  } catch (err) {
@@ -913,21 +1080,106 @@ function useAppHost(client, iframeRef, options) {
913
1080
  }, [client, iframeRef]);
914
1081
  return { host, error };
915
1082
  }
916
- var McpAppView = react.memo(function McpAppView2({
1083
+ var McpAppViewInner = react.forwardRef(function McpAppView({
917
1084
  clientRef,
918
1085
  name,
1086
+ toolResourceUri,
1087
+ html,
919
1088
  input,
920
1089
  result,
921
- status,
922
- className
923
- }) {
1090
+ status = "idle",
1091
+ toolInputPartial,
1092
+ toolCancelled,
1093
+ sandbox,
1094
+ hostContext,
1095
+ onCallTool,
1096
+ onReadResource,
1097
+ onFallbackRequest,
1098
+ onMessage,
1099
+ onOpenLink,
1100
+ onLoggingMessage,
1101
+ onSizeChanged,
1102
+ onError: onHostError,
1103
+ className,
1104
+ loader
1105
+ }, ref) {
924
1106
  const mcpClient = clientRef.current;
925
1107
  const metadata = getMcpAppMetadata(mcpClient, name);
926
1108
  const sseClient = mcpClient?.sseClient ?? null;
927
- const resourceUri = metadata?.resourceUri;
1109
+ const resourceUri = toolResourceUri || metadata?.resourceUri;
928
1110
  const appSessionId = metadata?.sessionId;
929
1111
  const iframeRef = react.useRef(null);
930
- const { host, error: hostError } = useAppHost(sseClient, iframeRef);
1112
+ const containerRef = react.useRef(null);
1113
+ const preFullscreenHeightRef = react.useRef(null);
1114
+ const displayModeRef = react.useRef("inline");
1115
+ const [displayMode, setDisplayMode] = react.useState("inline");
1116
+ const setDisplayModeWithRef = (mode) => {
1117
+ displayModeRef.current = mode;
1118
+ setDisplayMode(mode);
1119
+ };
1120
+ const { host, error: hostError } = useAppHost(sseClient, iframeRef, {
1121
+ sandbox,
1122
+ hostContext,
1123
+ onCallTool,
1124
+ onReadResource,
1125
+ onFallbackRequest,
1126
+ onMessage,
1127
+ onOpenLink,
1128
+ onLoggingMessage,
1129
+ // Intercept onSizeChanged: when exiting fullscreen, ignore guest resize events
1130
+ // that arrive with the shrunken viewport dimensions, and restore the pre-fullscreen height.
1131
+ onSizeChanged: (params) => {
1132
+ if (displayModeRef.current === "inline" && preFullscreenHeightRef.current !== null) {
1133
+ const savedHeight = preFullscreenHeightRef.current;
1134
+ preFullscreenHeightRef.current = null;
1135
+ if (iframeRef.current) {
1136
+ iframeRef.current.style.height = `${savedHeight}px`;
1137
+ }
1138
+ return;
1139
+ }
1140
+ onSizeChanged?.(params);
1141
+ },
1142
+ onError: onHostError,
1143
+ onRequestDisplayMode: async (params) => {
1144
+ if (params.mode === "fullscreen") {
1145
+ if (iframeRef.current) {
1146
+ const h = iframeRef.current.getBoundingClientRect().height;
1147
+ if (h > 0) preFullscreenHeightRef.current = h;
1148
+ }
1149
+ try {
1150
+ if (containerRef.current?.requestFullscreen) {
1151
+ await containerRef.current.requestFullscreen();
1152
+ } else if (containerRef.current?.webkitRequestFullscreen) {
1153
+ await containerRef.current.webkitRequestFullscreen();
1154
+ }
1155
+ setDisplayModeWithRef("fullscreen");
1156
+ } catch (err) {
1157
+ console.warn("[McpAppHost] requestFullscreen failed:", err);
1158
+ preFullscreenHeightRef.current = null;
1159
+ return { mode: "inline" };
1160
+ }
1161
+ } else if (params.mode === "inline") {
1162
+ restoreHeightAfterFullscreen();
1163
+ try {
1164
+ if (document.fullscreenElement) {
1165
+ await document.exitFullscreen();
1166
+ }
1167
+ } catch (err) {
1168
+ }
1169
+ setDisplayModeWithRef("inline");
1170
+ }
1171
+ return { mode: params.mode };
1172
+ }
1173
+ });
1174
+ react.useImperativeHandle(
1175
+ ref,
1176
+ () => ({
1177
+ teardownResource: (params) => {
1178
+ host?.teardownResource(params ?? {});
1179
+ }
1180
+ }),
1181
+ [host]
1182
+ );
931
1183
  const [isLaunched, setIsLaunched] = react.useState(false);
932
1184
  const [error, setError] = react.useState(null);
933
1185
  const sentInputRef = react.useRef(false);
@@ -939,10 +1191,28 @@ var McpAppView = react.memo(function McpAppView2({
939
1191
  setIsLaunched(false);
940
1192
  setError(null);
941
1193
  }, [resourceUri, appSessionId]);
1194
+ const restoreHeightAfterFullscreen = () => {
1195
+ const savedHeight = preFullscreenHeightRef.current;
1196
+ if (savedHeight && iframeRef.current) {
1197
+ iframeRef.current.style.height = `${savedHeight}px`;
1198
+ }
1199
+ preFullscreenHeightRef.current = null;
1200
+ };
942
1201
  react.useEffect(() => {
943
- if (!host || !resourceUri || !appSessionId) return;
944
- host.launch(resourceUri, appSessionId).then(() => setIsLaunched(true)).catch((err) => setError(err instanceof Error ? err : new Error(String(err))));
945
- }, [host, resourceUri, appSessionId]);
1202
+ const onFullscreenChange = () => {
1203
+ const isFullscreen = !!document.fullscreenElement;
1204
+ if (!isFullscreen && displayModeRef.current === "fullscreen") {
1205
+ restoreHeightAfterFullscreen();
1206
+ setDisplayModeWithRef("inline");
1207
+ }
1208
+ };
1209
+ document.addEventListener("fullscreenchange", onFullscreenChange);
1210
+ return () => document.removeEventListener("fullscreenchange", onFullscreenChange);
1211
+ }, []);
1212
+ react.useEffect(() => {
1213
+ if (!host || !resourceUri && !html) return;
1214
+ host.launch({ uri: resourceUri, html }, appSessionId).then(() => setIsLaunched(true)).catch((err) => setError(err instanceof Error ? err : new Error(String(err))));
1215
+ }, [host, resourceUri, html, appSessionId]);
946
1216
  react.useEffect(() => {
947
1217
  if (!host || !isLaunched || !resourceUri || !appSessionId || !input) return;
948
1218
  if (!sentInputRef.current || JSON.stringify(input) !== JSON.stringify(lastInputRef.current)) {
@@ -968,7 +1238,26 @@ var McpAppView = react.memo(function McpAppView2({
968
1238
  }
969
1239
  lastStatusRef.current = status;
970
1240
  }, [status]);
971
- if (!metadata || !sseClient) {
1241
+ react.useEffect(() => {
1242
+ if (!host) return;
1243
+ const mergedCtx = {
1244
+ theme: APP_HOST_DEFAULTS.THEME,
1245
+ platform: APP_HOST_DEFAULTS.PLATFORM,
1246
+ containerDimensions: { maxHeight: APP_HOST_DEFAULTS.MAX_HEIGHT },
1247
+ availableDisplayModes: ["inline", "fullscreen"],
1248
+ ...hostContext || {},
1249
+ displayMode
1250
+ // always override with our authoritative state
1251
+ };
1252
+ host.setHostContext(mergedCtx);
1253
+ }, [host, hostContext, displayMode]);
1254
+ react.useEffect(() => {
1255
+ if (host && toolInputPartial) host.sendToolInputPartial(toolInputPartial);
1256
+ }, [host, toolInputPartial]);
1257
+ react.useEffect(() => {
1258
+ if (host && toolCancelled) host.sendToolCancelled("User cancelled");
1259
+ }, [host, toolCancelled]);
1260
+ if (!metadata && !html && !toolResourceUri) {
972
1261
  return null;
973
1262
  }
974
1263
  const displayError = error || hostError;
@@ -978,20 +1267,45 @@ var McpAppView = react.memo(function McpAppView2({
978
1267
  displayError.message || String(displayError)
979
1268
  ] });
980
1269
  }
981
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `w-full border border-gray-700 rounded overflow-hidden bg-white min-h-96 my-2 relative ${className || ""}`, children: [
1270
+ const opacityClass = isLaunched ? "opacity-100" : "opacity-0";
1271
+ let containerClass = `w-full border border-gray-700 rounded bg-transparent my-2 relative ${className || ""}`;
1272
+ let iframeClass = `w-full transition-opacity duration-300 ${opacityClass}`;
1273
+ if (displayMode === "fullscreen") {
1274
+ containerClass = `w-full h-full bg-black m-0 p-0 flex flex-col relative`;
1275
+ iframeClass = `w-full flex-1 transition-opacity duration-300 ${opacityClass}`;
1276
+ }
1277
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, className: containerClass, children: [
1278
+ displayMode === "fullscreen" && /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsxs(
1279
+ "button",
1280
+ {
1281
+ title: "Exit Fullscreen",
1282
+ onClick: () => {
1283
+ restoreHeightAfterFullscreen();
1284
+ if (document.fullscreenElement) document.exitFullscreen();
1285
+ setDisplayModeWithRef("inline");
1286
+ },
1287
+ 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",
1288
+ children: [
1289
+ /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.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" }) }),
1290
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: "Exit" })
1291
+ ]
1292
+ }
1293
+ ) }),
982
1294
  /* @__PURE__ */ jsxRuntime.jsx(
983
1295
  "iframe",
984
1296
  {
985
1297
  ref: iframeRef,
986
1298
  sandbox: "allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-downloads",
987
- className: "w-full h-full min-h-96",
988
- style: { height: "auto" },
1299
+ allow: "fullscreen",
1300
+ className: iframeClass,
989
1301
  title: "MCP App"
990
1302
  }
991
1303
  ),
992
- !isLaunched && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 bg-gray-900/50 flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" }) })
1304
+ !isLaunched && loader && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 bg-transparent flex items-center justify-center pointer-events-none z-10", children: loader })
993
1305
  ] });
994
1306
  });
1307
+ var McpAppView2 = react.memo(McpAppViewInner);
1308
+ McpAppView2.displayName = "McpAppView";
995
1309
  function useMcpApps(mcpClient) {
996
1310
  const clientRef = react.useRef(mcpClient);
997
1311
  clientRef.current = mcpClient;
@@ -1000,9 +1314,10 @@ function useMcpApps(mcpClient) {
1000
1314
  []
1001
1315
  );
1002
1316
  const McpAppRenderer = react.useMemo(() => {
1003
- const Renderer = react.memo(function McpAppRenderer2(props) {
1004
- return /* @__PURE__ */ jsxRuntime.jsx(McpAppView, { clientRef, ...props });
1317
+ const Inner = react.forwardRef(function McpAppRenderer2(props, ref) {
1318
+ return /* @__PURE__ */ jsxRuntime.jsx(McpAppView2, { ref, clientRef, ...props });
1005
1319
  });
1320
+ const Renderer = react.memo(Inner);
1006
1321
  Renderer.displayName = "McpAppRenderer";
1007
1322
  return Renderer;
1008
1323
  }, []);
@@ -1031,7 +1346,11 @@ function getMcpAppMetadata(mcpClient, toolName) {
1031
1346
  return void 0;
1032
1347
  }
1033
1348
 
1349
+ exports.APP_HOST_DEFAULTS = APP_HOST_DEFAULTS;
1034
1350
  exports.AppHost = AppHost;
1351
+ exports.DEFAULT_MCP_APP_CSP = DEFAULT_MCP_APP_CSP;
1352
+ exports.SANDBOX_PROXY_READY_METHOD = SANDBOX_PROXY_READY_METHOD;
1353
+ exports.SANDBOX_RESOURCE_READY_METHOD = SANDBOX_RESOURCE_READY_METHOD;
1035
1354
  exports.SSEClient = SSEClient;
1036
1355
  exports.useAppHost = useAppHost;
1037
1356
  exports.useMcp = useMcp;