@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
@@ -277,6 +277,7 @@ function useMcp(options) {
277
277
  "disconnected"
278
278
  );
279
279
  const [isInitializing, setIsInitializing] = react.useState(false);
280
+ const [sseClient, setSseClient] = react.useState(null);
280
281
  react.useEffect(() => {
281
282
  isMountedRef.current = true;
282
283
  const clientOptions = {
@@ -299,6 +300,7 @@ function useMcp(options) {
299
300
  };
300
301
  const client = new SSEClient(clientOptions);
301
302
  clientRef.current = client;
303
+ setSseClient(client);
302
304
  if (autoConnect) {
303
305
  client.connect();
304
306
  if (autoInitialize) {
@@ -354,18 +356,28 @@ function useMcp(options) {
354
356
  );
355
357
  }
356
358
  case "auth_required": {
357
- if (event.authUrl) {
358
- onLog?.("info", `OAuth required - redirecting to ${event.authUrl}`, { authUrl: event.authUrl });
359
- if (!suppressAuthRedirectSessionsRef.current.has(event.sessionId)) {
360
- if (onRedirect) {
361
- onRedirect(event.authUrl);
362
- } else if (typeof window !== "undefined") {
363
- window.location.href = event.authUrl;
364
- }
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;
365
377
  }
366
378
  }
367
379
  return prev.map(
368
- (c) => c.sessionId === event.sessionId ? { ...c, state: "AUTHENTICATING", authUrl: event.authUrl } : c
380
+ (c) => c.sessionId === event.sessionId ? { ...c, state: "AUTHENTICATING", authUrl: url2 } : c
369
381
  );
370
382
  }
371
383
  case "error": {
@@ -532,46 +544,137 @@ function useMcp(options) {
532
544
  },
533
545
  [getConnection]
534
546
  );
535
- return {
536
- connections,
537
- status,
538
- isInitializing,
539
- connect,
540
- disconnect,
541
- getConnection,
542
- getConnectionByServerId,
543
- isServerConnected,
544
- getTools,
545
- refresh,
546
- connectSSE,
547
- disconnectSSE,
548
- finishAuth,
549
- resumeAuth,
550
- callTool,
551
- listTools,
552
- listPrompts,
553
- getPrompt,
554
- listResources,
555
- readResource,
556
- sseClient: clientRef.current
557
- };
547
+ return react.useMemo(
548
+ () => ({
549
+ connections,
550
+ status,
551
+ isInitializing,
552
+ connect,
553
+ disconnect,
554
+ getConnection,
555
+ getConnectionByServerId,
556
+ isServerConnected,
557
+ getTools,
558
+ refresh,
559
+ connectSSE,
560
+ disconnectSSE,
561
+ finishAuth,
562
+ resumeAuth,
563
+ callTool,
564
+ listTools,
565
+ listPrompts,
566
+ getPrompt,
567
+ listResources,
568
+ readResource,
569
+ sseClient
570
+ }),
571
+ [
572
+ connections,
573
+ status,
574
+ isInitializing,
575
+ connect,
576
+ disconnect,
577
+ getConnection,
578
+ getConnectionByServerId,
579
+ isServerConnected,
580
+ getTools,
581
+ refresh,
582
+ connectSSE,
583
+ disconnectSSE,
584
+ finishAuth,
585
+ resumeAuth,
586
+ callTool,
587
+ listTools,
588
+ listPrompts,
589
+ getPrompt,
590
+ listResources,
591
+ readResource,
592
+ sseClient
593
+ ]
594
+ );
595
+ }
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 };
558
661
  }
559
- var HOST_INFO = { name: "mcp-ts-host", version: "1.0.0" };
560
- var SANDBOX_PERMISSIONS = [
561
- "allow-scripts",
562
- // Required for app JavaScript execution
563
- "allow-forms",
564
- // Required for form submissions
565
- "allow-same-origin",
566
- // Required for Blob URL correctness
567
- "allow-modals",
568
- // Required for dialogs/alerts
569
- "allow-popups",
570
- // Required for opening links
571
- "allow-downloads"
572
- // Required for file downloads
573
- ].join(" ");
574
- var MCP_URI_SCHEMES = ["ui://", "mcp-app://"];
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;
575
678
  var AppHost = class {
576
679
  constructor(client, iframe, options) {
577
680
  this.client = client;
@@ -580,10 +683,12 @@ var AppHost = class {
580
683
  __publicField(this, "sessionId");
581
684
  __publicField(this, "resourceCache", /* @__PURE__ */ new Map());
582
685
  __publicField(this, "debug");
583
- /** Callback for app messages (e.g., chat messages from the app) */
686
+ __publicField(this, "sandboxConfig");
687
+ __publicField(this, "options");
584
688
  __publicField(this, "onAppMessage");
585
- this.debug = options?.debug ?? false;
586
- this.configureSandbox();
689
+ this.options = options || {};
690
+ this.debug = this.options.debug ?? false;
691
+ this.sandboxConfig = this.options.sandbox;
587
692
  this.bridge = this.initializeBridge();
588
693
  }
589
694
  // ============================================
@@ -610,19 +715,35 @@ var AppHost = class {
610
715
  }
611
716
  }
612
717
  /**
613
- * 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.
614
719
  * Loads the HTML first, then establishes bridge connection.
615
720
  */
616
- async launch(url, sessionId) {
721
+ async launch(source, sessionId) {
617
722
  if (sessionId) this.sessionId = sessionId;
618
723
  const initializedPromise = this.onAppReady();
619
- if (this.isMcpUri(url)) {
620
- await this.launchMcpApp(url);
621
- } else {
622
- 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
+ });
623
746
  }
624
- await this.onIframeReady();
625
- await this.connectBridge();
626
747
  this.log("Waiting for app initialization");
627
748
  await Promise.race([
628
749
  initializedPromise,
@@ -633,6 +754,19 @@ var AppHost = class {
633
754
  ]);
634
755
  this.log("App launched and ready");
635
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
+ }
636
770
  /**
637
771
  * Wait for app to signal initialization complete
638
772
  */
@@ -683,14 +817,17 @@ var AppHost = class {
683
817
  this.log("Sending tool cancellation to app");
684
818
  this.bridge.sendToolCancelled({ reason });
685
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
+ }
686
828
  // ============================================
687
829
  // Private: Initialization
688
830
  // ============================================
689
- configureSandbox() {
690
- if (this.iframe.sandbox.value !== SANDBOX_PERMISSIONS) {
691
- this.iframe.sandbox.value = SANDBOX_PERMISSIONS;
692
- }
693
- }
694
831
  initializeBridge() {
695
832
  const bridge = new appBridge.AppBridge(
696
833
  null,
@@ -699,12 +836,10 @@ var AppHost = class {
699
836
  openLinks: {},
700
837
  serverTools: {},
701
838
  logging: {},
702
- // Declare support for model context updates
703
839
  updateModelContext: { text: {} }
704
840
  },
705
841
  {
706
- // Initial host context
707
- hostContext: {
842
+ hostContext: this.options.hostContext || {
708
843
  theme: "dark",
709
844
  platform: "web",
710
845
  containerDimensions: { maxHeight: 6e3 },
@@ -713,19 +848,56 @@ var AppHost = class {
713
848
  }
714
849
  }
715
850
  );
851
+ bridge.fallbackRequestHandler = this.options.onFallbackRequest;
716
852
  bridge.oncalltool = (params) => this.handleToolCall(params);
717
- bridge.onopenlink = this.handleOpenLink.bind(this);
718
- bridge.onmessage = this.handleMessage.bind(this);
719
- 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
+ };
720
883
  bridge.onupdatemodelcontext = async () => ({});
721
- bridge.onsizechange = async ({ width, height }) => {
722
- if (height !== void 0) this.iframe.style.height = `${height}px`;
723
- 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
+ }
724
893
  return {};
725
894
  };
726
- bridge.onrequestdisplaymode = async (params) => ({
727
- mode: params.mode === "fullscreen" ? "fullscreen" : "inline"
728
- });
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
+ };
729
901
  return bridge;
730
902
  }
731
903
  async connectBridge() {
@@ -739,6 +911,9 @@ var AppHost = class {
739
911
  this.log("Bridge connected successfully");
740
912
  } catch (error) {
741
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
+ }
742
917
  throw error;
743
918
  }
744
919
  }
@@ -746,8 +921,11 @@ var AppHost = class {
746
921
  // Private: Bridge Event Handlers
747
922
  // ============================================
748
923
  async handleToolCall(params) {
749
- if (!this.client.isConnected()) {
750
- 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");
751
929
  }
752
930
  const sessionId = await this.getSessionId();
753
931
  if (!sessionId) {
@@ -771,13 +949,19 @@ var AppHost = class {
771
949
  // ============================================
772
950
  // Private: Resource Loading
773
951
  // ============================================
774
- async launchMcpApp(uri) {
775
- if (!this.client.isConnected()) {
776
- 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));
777
957
  }
958
+ const { onReady } = await setupSandboxProxyIframe(this.iframe, url);
959
+ await onReady;
960
+ }
961
+ async readMcpAppHtml(uri) {
778
962
  const sessionId = await this.getSessionId();
779
- if (!sessionId) {
780
- throw new Error("No active session");
963
+ if (!sessionId && !this.options.onReadResource) {
964
+ throw new Error("No active session.");
781
965
  }
782
966
  const response = await this.fetchResourceWithCache(sessionId, uri);
783
967
  if (!response?.contents?.length) {
@@ -788,10 +972,18 @@ var AppHost = class {
788
972
  if (!html) {
789
973
  throw new Error(`Invalid content in resource: ${uri}`);
790
974
  }
791
- const blob = new Blob([html], { type: "text/html" });
792
- this.iframe.src = URL.createObjectURL(blob);
975
+ return html;
793
976
  }
794
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
+ }
795
987
  if (this.hasClientCache()) {
796
988
  return this.client.getOrFetchResource(sessionId, uri);
797
989
  }
@@ -804,8 +996,11 @@ var AppHost = class {
804
996
  }
805
997
  async preloadResource(uri) {
806
998
  try {
999
+ if (this.options.onReadResource) {
1000
+ return await this.options.onReadResource(uri);
1001
+ }
807
1002
  const sessionId = await this.getSessionId();
808
- if (!sessionId) return null;
1003
+ if (!sessionId || !this.client) return null;
809
1004
  return await this.client.readResource(sessionId, uri);
810
1005
  } catch (error) {
811
1006
  this.log(`Preload failed for ${uri}`, "warn");
@@ -817,6 +1012,7 @@ var AppHost = class {
817
1012
  // ============================================
818
1013
  async getSessionId() {
819
1014
  if (this.sessionId) return this.sessionId;
1015
+ if (!this.client) return void 0;
820
1016
  const result = await this.client.getSessions();
821
1017
  return result.sessions?.[0]?.sessionId;
822
1018
  }
@@ -824,6 +1020,7 @@ var AppHost = class {
824
1020
  return MCP_URI_SCHEMES.some((scheme) => url.startsWith(scheme));
825
1021
  }
826
1022
  hasClientCache() {
1023
+ if (!this.client) return false;
827
1024
  return "getOrFetchResource" in this.client && typeof this.client.getOrFetchResource === "function";
828
1025
  }
829
1026
  extractUiResourceUri(tool) {
@@ -867,10 +1064,7 @@ function useAppHost(client, iframeRef, options) {
867
1064
  initializingRef.current = true;
868
1065
  const initHost = async () => {
869
1066
  try {
870
- const appHost = new AppHost(client, iframeRef.current);
871
- appHost.onAppMessage = (params) => {
872
- onMessageRef.current?.(params);
873
- };
1067
+ const appHost = new AppHost(client, iframeRef.current, options);
874
1068
  setHost(appHost);
875
1069
  await appHost.start();
876
1070
  } catch (err) {
@@ -886,39 +1080,106 @@ function useAppHost(client, iframeRef, options) {
886
1080
  }, [client, iframeRef]);
887
1081
  return { host, error };
888
1082
  }
889
- var McpAppRenderer = react.memo(function McpAppRenderer2({
890
- mcpClient,
1083
+ var McpAppViewInner = react.forwardRef(function McpAppView({
1084
+ clientRef,
891
1085
  name,
1086
+ toolResourceUri,
1087
+ html,
892
1088
  input,
893
1089
  result,
894
- status,
895
- className
896
- }) {
897
- const getAppMetadata = react.useCallback(() => {
898
- if (!mcpClient) return void 0;
899
- const extractedName = extractToolName(name);
900
- for (const conn of mcpClient.connections) {
901
- for (const tool of conn.tools) {
902
- const candidateName = extractToolName(tool.name);
903
- const resourceUri = tool.mcpApp?.resourceUri ?? tool._meta?.ui?.resourceUri ?? tool._meta?.["ui/resourceUri"];
904
- if (resourceUri && candidateName === extractedName) {
905
- return {
906
- toolName: candidateName,
907
- resourceUri,
908
- sessionId: conn.sessionId
909
- };
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) {
1106
+ const mcpClient = clientRef.current;
1107
+ const metadata = getMcpAppMetadata(mcpClient, name);
1108
+ const sseClient = mcpClient?.sseClient ?? null;
1109
+ const resourceUri = toolResourceUri || metadata?.resourceUri;
1110
+ const appSessionId = metadata?.sessionId;
1111
+ const iframeRef = react.useRef(null);
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`;
910
1137
  }
1138
+ return;
911
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 };
912
1172
  }
913
- return void 0;
914
- }, [mcpClient, name]);
915
- const metadata = getAppMetadata();
916
- const sseClient = mcpClient?.sseClient ?? null;
917
- if (!metadata || !sseClient) {
918
- return null;
919
- }
920
- const iframeRef = react.useRef(null);
921
- const { host, error: hostError } = useAppHost(sseClient, iframeRef);
1173
+ });
1174
+ react.useImperativeHandle(
1175
+ ref,
1176
+ () => ({
1177
+ teardownResource: (params) => {
1178
+ host?.teardownResource(params ?? {});
1179
+ }
1180
+ }),
1181
+ [host]
1182
+ );
922
1183
  const [isLaunched, setIsLaunched] = react.useState(false);
923
1184
  const [error, setError] = react.useState(null);
924
1185
  const sentInputRef = react.useRef(false);
@@ -927,19 +1188,41 @@ var McpAppRenderer = react.memo(function McpAppRenderer2({
927
1188
  const lastResultRef = react.useRef(result);
928
1189
  const lastStatusRef = react.useRef(status);
929
1190
  react.useEffect(() => {
930
- if (!host || !metadata.resourceUri || !metadata.sessionId) return;
931
- host.launch(metadata.resourceUri, metadata.sessionId).then(() => setIsLaunched(true)).catch((err) => setError(err instanceof Error ? err : new Error(String(err))));
932
- }, [host, metadata.resourceUri, metadata.sessionId]);
1191
+ setIsLaunched(false);
1192
+ setError(null);
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
+ };
1201
+ react.useEffect(() => {
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
+ }, []);
933
1212
  react.useEffect(() => {
934
- if (!host || !isLaunched || !input) return;
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]);
1216
+ react.useEffect(() => {
1217
+ if (!host || !isLaunched || !resourceUri || !appSessionId || !input) return;
935
1218
  if (!sentInputRef.current || JSON.stringify(input) !== JSON.stringify(lastInputRef.current)) {
936
1219
  sentInputRef.current = true;
937
1220
  lastInputRef.current = input;
938
1221
  host.sendToolInput(input);
939
1222
  }
940
- }, [host, isLaunched, input]);
1223
+ }, [host, isLaunched, input, resourceUri, appSessionId, name]);
941
1224
  react.useEffect(() => {
942
- if (!host || !isLaunched || result === void 0) return;
1225
+ if (!host || !isLaunched || !resourceUri || !appSessionId || result === void 0) return;
943
1226
  if (status !== "complete") return;
944
1227
  if (!sentResultRef.current || JSON.stringify(result) !== JSON.stringify(lastResultRef.current)) {
945
1228
  sentResultRef.current = true;
@@ -947,7 +1230,7 @@ var McpAppRenderer = react.memo(function McpAppRenderer2({
947
1230
  const formattedResult = typeof result === "string" ? { content: [{ type: "text", text: result }] } : result;
948
1231
  host.sendToolResult(formattedResult);
949
1232
  }
950
- }, [host, isLaunched, result, status]);
1233
+ }, [host, isLaunched, result, status, resourceUri, appSessionId, name]);
951
1234
  react.useEffect(() => {
952
1235
  if (status === "executing" && lastStatusRef.current !== "executing") {
953
1236
  sentInputRef.current = false;
@@ -955,6 +1238,28 @@ var McpAppRenderer = react.memo(function McpAppRenderer2({
955
1238
  }
956
1239
  lastStatusRef.current = status;
957
1240
  }, [status]);
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) {
1261
+ return null;
1262
+ }
958
1263
  const displayError = error || hostError;
959
1264
  if (displayError) {
960
1265
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `p-4 bg-red-900/20 border border-red-700 rounded text-red-200 ${className || ""}`, children: [
@@ -962,50 +1267,90 @@ var McpAppRenderer = react.memo(function McpAppRenderer2({
962
1267
  displayError.message || String(displayError)
963
1268
  ] });
964
1269
  }
965
- 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
+ ) }),
966
1294
  /* @__PURE__ */ jsxRuntime.jsx(
967
1295
  "iframe",
968
1296
  {
969
1297
  ref: iframeRef,
970
1298
  sandbox: "allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-downloads",
971
- className: "w-full h-full min-h-96",
972
- style: { height: "auto" },
1299
+ allow: "fullscreen",
1300
+ className: iframeClass,
973
1301
  title: "MCP App"
974
1302
  }
975
1303
  ),
976
- !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 })
977
1305
  ] });
978
1306
  });
1307
+ var McpAppView2 = react.memo(McpAppViewInner);
1308
+ McpAppView2.displayName = "McpAppView";
979
1309
  function useMcpApps(mcpClient) {
1310
+ const clientRef = react.useRef(mcpClient);
1311
+ clientRef.current = mcpClient;
980
1312
  const getAppMetadata = react.useCallback(
981
- (toolName) => {
982
- if (!mcpClient) return void 0;
983
- const extractedName = extractToolName(toolName);
984
- for (const conn of mcpClient.connections) {
985
- for (const tool of conn.tools) {
986
- const candidateName = extractToolName(tool.name);
987
- const resourceUri = tool.mcpApp?.resourceUri ?? tool._meta?.ui?.resourceUri ?? tool._meta?.["ui/resourceUri"];
988
- if (resourceUri && candidateName === extractedName) {
989
- return {
990
- toolName: candidateName,
991
- resourceUri,
992
- sessionId: conn.sessionId
993
- };
994
- }
995
- }
996
- }
997
- return void 0;
998
- },
999
- [mcpClient]
1313
+ (toolName) => getMcpAppMetadata(clientRef.current, toolName),
1314
+ []
1000
1315
  );
1316
+ const McpAppRenderer = react.useMemo(() => {
1317
+ const Inner = react.forwardRef(function McpAppRenderer2(props, ref) {
1318
+ return /* @__PURE__ */ jsxRuntime.jsx(McpAppView2, { ref, clientRef, ...props });
1319
+ });
1320
+ const Renderer = react.memo(Inner);
1321
+ Renderer.displayName = "McpAppRenderer";
1322
+ return Renderer;
1323
+ }, []);
1001
1324
  return { getAppMetadata, McpAppRenderer };
1002
1325
  }
1003
1326
  function extractToolName(fullName) {
1004
1327
  const match = fullName.match(/(?:tool_[^_]+_)?(.+)$/);
1005
1328
  return match?.[1] || fullName;
1006
1329
  }
1330
+ function getMcpAppMetadata(mcpClient, toolName) {
1331
+ if (!mcpClient) return void 0;
1332
+ const extractedName = extractToolName(toolName);
1333
+ for (const conn of mcpClient.connections) {
1334
+ for (const tool of conn.tools) {
1335
+ const candidateName = extractToolName(tool.name);
1336
+ const resourceUri = tool.mcpApp?.resourceUri ?? tool._meta?.ui?.resourceUri ?? tool._meta?.["ui/resourceUri"];
1337
+ if (resourceUri && candidateName === extractedName) {
1338
+ return {
1339
+ toolName: candidateName,
1340
+ resourceUri,
1341
+ sessionId: conn.sessionId
1342
+ };
1343
+ }
1344
+ }
1345
+ }
1346
+ return void 0;
1347
+ }
1007
1348
 
1349
+ exports.APP_HOST_DEFAULTS = APP_HOST_DEFAULTS;
1008
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;
1009
1354
  exports.SSEClient = SSEClient;
1010
1355
  exports.useAppHost = useAppHost;
1011
1356
  exports.useMcp = useMcp;