Repository: dust-tt/dust
Pull request #12101: Fix: slow performance when loading mcp servers for UI
Author: login: Fraggle
Labels: name: migration-ack
description: 📁 Label to acknowledge that a migration is required.
Created: 2025-04-18T09:41:25Z | Merged: 2025-04-18T12:16:39Z

## Description

Fix performance issues with MCP servers (views) in UI.

We were doing some unneeded lookups:
- When using remote servers, we were doing a separate query per server => now we include them alongside the related views
- Internal actions are verified to be valid by checking if they exist in the system space => skip this check for **default** internal actions that are always available anyway
- Cache internal servers metadata for UI (5 minutes TTL, could be increased) as it's cheap but not "free" to retrieve them via the MCP client <-> server communication.

## Tests

Locally, seen a big performance boost.

## Risk

Low, worst case it breaks something for people with MCP actions enabled.

## Deploy Plan

Deploy `front` and monitor


Diff:
diff --git a/.github/workflows/build-and-lint-front.yml b/.github/workflows/build-and-lint-front.yml
index d9e0f2c344c8..10e11015b3c4 100644
--- a/.github/workflows/build-and-lint-front.yml
+++ b/.github/workflows/build-and-lint-front.yml
@@ -29,6 +29,12 @@ jobs:
       - name: Typecheck and Lint
         working-directory: front
         run: npm install && npm run tsc && npm run lint && npm run format:check && npm run docs:check
+      - name: Install Redis
+        uses: chenjuneking/redis-setup-action@v1
+        with:
+          version: "7.2.5"
+          hostPort: 5434
+          containerPort: 6379
       - name: Install Postgres
         uses: dust-tt/postgresql-action@6730e69fe23f4f2989c19edf9e6e0d7ffeb0a05c
         with:
@@ -42,6 +48,7 @@ jobs:
         working-directory: front
         env:
           FRONT_DATABASE_URI: "postgres://test:test@localhost:5433/front_test"
+          REDIS_CACHE_URI: "redis://localhost:5434"
           NODE_ENV: test
         run: npx tsx admin/db.ts && npm run test:ci
       - name: Build Tests Report
diff --git a/front/lib/actions/configuration/mcp.ts b/front/lib/actions/configuration/mcp.ts
index 249757051562..c59ed9ff9dee 100644
--- a/front/lib/actions/configuration/mcp.ts
+++ b/front/lib/actions/configuration/mcp.ts
@@ -126,8 +126,7 @@ export async function fetchMCPServerActionConfigurations(
       serverName = "Missing";
       serverDescription = "Missing";
     } else {
-      const { name, description } =
-        await mcpServerView.getMCPServerMetadata(auth);
+      const { name, description } = await mcpServerView.toJSON().server;
 
       serverName = name;
       serverDescription = description;
diff --git a/front/lib/models/assistant/actions/mcp_server_view.ts b/front/lib/models/assistant/actions/mcp_server_view.ts
index 0b2659119794..d12fb3f7a913 100644
--- a/front/lib/models/assistant/actions/mcp_server_view.ts
+++ b/front/lib/models/assistant/actions/mcp_server_view.ts
@@ -24,6 +24,7 @@ export class MCPServerViewModel extends SoftDeletableWorkspaceAwareModel<MCPServ
 
   declare editedByUser: NonAttribute<UserModel>;
   declare space: NonAttribute<SpaceModel>;
+  declare remoteMCPServer: NonAttribute<RemoteMCPServerModel>;
 }
 MCPServerViewModel.init(
   {
@@ -133,12 +134,12 @@ MCPServerViewModel.belongsTo(SpaceModel, {
 });
 
 RemoteMCPServerModel.hasMany(MCPServerViewModel, {
-  as: "remoteMCPServerForView",
+  as: "remoteMCPServer",
   foreignKey: { name: "remoteMCPServerId", allowNull: false },
   onDelete: "RESTRICT",
 });
 MCPServerViewModel.belongsTo(RemoteMCPServerModel, {
-  as: "remoteMCPServerForView",
+  as: "remoteMCPServer",
   foreignKey: { name: "remoteMCPServerId", allowNull: false },
 });
 
diff --git a/front/lib/resources/internal_mcp_server_in_memory_resource.ts b/front/lib/resources/internal_mcp_server_in_memory_resource.ts
index c2749a5a8346..75a98a8fba72 100644
--- a/front/lib/resources/internal_mcp_server_in_memory_resource.ts
+++ b/front/lib/resources/internal_mcp_server_in_memory_resource.ts
@@ -10,6 +10,7 @@ import {
   AVAILABLE_INTERNAL_MCP_SERVER_NAMES,
   getInternalMCPServerNameAndWorkspaceId,
   INTERNAL_TOOLS_STAKE_LEVEL,
+  isDefaultInternalMCPServer,
   isDefaultInternalMCPServerByName,
   isInternalMCPServerName,
 } from "@app/lib/actions/mcp_internal_actions/constants";
@@ -26,8 +27,11 @@ import { MCPServerViewModel } from "@app/lib/models/assistant/actions/mcp_server
 import { destroyMCPServerViewDependencies } from "@app/lib/models/assistant/actions/mcp_server_view_helper";
 import { SpaceResource } from "@app/lib/resources/space_resource";
 import { concurrentExecutor } from "@app/lib/utils/async_utils";
+import { cacheWithRedis } from "@app/lib/utils/cache";
 import { removeNulls } from "@app/types";
 
+const METADATA_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
+
 export class InternalMCPServerInMemoryResource {
   // SID of the internal MCP server, scoped to a workspace.
   readonly id: string;
@@ -50,29 +54,49 @@ export class InternalMCPServerInMemoryResource {
 
     const server = new InternalMCPServerInMemoryResource(id);
 
-    const s = await connectToMCPServer(auth, {
-      type: "mcpServerId",
-      mcpServerId: id,
-    });
+    // Getting the metadata is a relatively long operation, so we cache it for 5 minutes
+    // as internal servers are not expected to change often.
+    // In any case, when actually running the action, the metadata will be fetched from the MCP server.
+    const getCachedMetadata = cacheWithRedis(
+      async (id: string) => {
+        const s = await connectToMCPServer(auth, {
+          type: "mcpServerId",
+          mcpServerId: id,
+        });
+
+        if (s.isErr()) {
+          return null;
+        }
+
+        const mcpClient = s.value;
+        const md = extractMetadataFromServerVersion(
+          mcpClient.getServerVersion()
+        );
+
+        const metadata = {
+          ...md,
+          tools: extractMetadataFromTools(
+            (await mcpClient.listTools()).tools
+          ) as any,
+          isDefault: isInternalMCPServerName(md.name)
+            ? isDefaultInternalMCPServerByName(md.name)
+            : false,
+        };
+
+        await mcpClient.close();
 
-    if (s.isErr()) {
+        return metadata;
+      },
+      (id) => `internal-mcp-server-metadata-${id}`,
+      METADATA_CACHE_TTL_MS
+    );
+
+    const cachedMetadata = await getCachedMetadata(id);
+    if (!cachedMetadata) {
       return null;
     }
 
-    const mcpClient = s.value;
-    const md = extractMetadataFromServerVersion(mcpClient.getServerVersion());
-
-    server.metadata = {
-      ...md,
-      tools: extractMetadataFromTools(
-        (await mcpClient.listTools()).tools
-      ) as any,
-      isDefault: isInternalMCPServerName(md.name)
-        ? isDefaultInternalMCPServerByName(md.name)
-        : false,
-    };
-
-    await mcpClient.close();
+    server.metadata = cachedMetadata;
 
     return server;
   }
@@ -154,26 +178,26 @@ export class InternalMCPServerInMemoryResource {
   }
 
   static async fetchById(auth: Authenticator, id: string) {
-    const systemSpace = await SpaceResource.fetchWorkspaceSystemSpace(auth);
-
-    const server = await MCPServerViewModel.findOne({
-      attributes: ["internalMCPServerId"],
-      where: {
-        serverType: "internal",
-        internalMCPServerId: id,
-        workspaceId: auth.getNonNullableWorkspace().id,
-        vaultId: systemSpace.id,
-      },
-    });
+    // Fast path : Do not check for default internal MCP servers as they are always available.
+    if (!isDefaultInternalMCPServer(id)) {
+      const systemSpace = await SpaceResource.fetchWorkspaceSystemSpace(auth);
+
+      const server = await MCPServerViewModel.findOne({
+        attributes: ["internalMCPServerId"],
+        where: {
+          serverType: "internal",
+          internalMCPServerId: id,
+          workspaceId: auth.getNonNullableWorkspace().id,
+          vaultId: systemSpace.id,
+        },
+      });
 
-    if (!server || !server.internalMCPServerId) {
-      return null;
+      if (!server || !server.internalMCPServerId) {
+        return null;
+      }
     }
 
-    return InternalMCPServerInMemoryResource.init(
-      auth,
-      server.internalMCPServerId
-    );
+    return InternalMCPServerInMemoryResource.init(auth, id);
   }
 
   static async listAvailableInternalMCPServers(auth: Authenticator) {
diff --git a/front/lib/resources/mcp_server_view_resource.ts b/front/lib/resources/mcp_server_view_resource.ts
index 6e21d20b1f49..9099776567d9 100644
--- a/front/lib/resources/mcp_server_view_resource.ts
+++ b/front/lib/resources/mcp_server_view_resource.ts
@@ -19,11 +19,12 @@ import {
   isDefaultInternalMCPServerByName,
   isValidInternalMCPServerId,
 } from "@app/lib/actions/mcp_internal_actions/constants";
-import type { MCPServerType, MCPServerViewType } from "@app/lib/api/mcp";
+import type { MCPServerViewType } from "@app/lib/api/mcp";
 import type { Authenticator } from "@app/lib/auth";
 import { DustError } from "@app/lib/error";
 import { MCPServerViewModel } from "@app/lib/models/assistant/actions/mcp_server_view";
 import { destroyMCPServerViewDependencies } from "@app/lib/models/assistant/actions/mcp_server_view_helper";
+import { RemoteMCPServerModel } from "@app/lib/models/assistant/actions/remote_mcp_server";
 import { InternalMCPServerInMemoryResource } from "@app/lib/resources/internal_mcp_server_in_memory_resource";
 import { RemoteMCPServerResource } from "@app/lib/resources/remote_mcp_servers_resource";
 import { ResourceWithSpace } from "@app/lib/resources/resource_with_space";
@@ -50,8 +51,8 @@ export interface MCPServerViewResource
 export class MCPServerViewResource extends ResourceWithSpace<MCPServerViewModel> {
   static model: ModelStatic<MCPServerViewModel> = MCPServerViewModel;
   readonly editedByUser?: Attributes<UserModel>;
-
-  private remoteMCPServer?: RemoteMCPServerResource;
+  readonly remoteMCPServer?: Attributes<RemoteMCPServerModel>;
+  private remoteMCPServerResource?: RemoteMCPServerResource;
   private internalMCPServer?: InternalMCPServerInMemoryResource;
 
   constructor(
@@ -67,19 +68,21 @@ export class MCPServerViewResource extends ResourceWithSpace<MCPServerViewModel>
 
   private async init(auth: Authenticator): Promise<Result<void, DustError>> {
     if (this.remoteMCPServerId) {
-      const remoteServer = await RemoteMCPServerResource.findByPk(
-        auth,
-        this.remoteMCPServerId
-      );
-      if (!remoteServer) {
+      if (!this.remoteMCPServer) {
         return new Err(
           new DustError(
             "remote_server_not_found",
-            "Remote server not found, should never happen as we are suppose to clear the view when deleting a remote server."
+            "Remote server not found, it should have been fetched by the base fetch."
           )
         );
       }
-      this.remoteMCPServer = remoteServer;
+      const remoteServerResource = new RemoteMCPServerResource(
+        RemoteMCPServerModel,
+        // @ts-expect-error - Typescript is confused because as it doesn't understand that RemoteMCPServerModel is a regular model
+        this.remoteMCPServer.get()
+      );
+
+      this.remoteMCPServerResource = remoteServerResource;
       return new Ok(undefined);
     }
 
@@ -200,6 +203,10 @@ export class MCPServerViewResource extends ResourceWithSpace<MCPServerViewModel>
           model: UserModel,
           as: "editedByUser",
         },
+        {
+          model: RemoteMCPServerModel,
+          as: "remoteMCPServer",
+        },
       ],
     });
 
@@ -381,7 +388,7 @@ export class MCPServerViewResource extends ResourceWithSpace<MCPServerViewModel>
     return new Ok(deletedCount);
   }
 
-  getRemoteMCPServer(): RemoteMCPServerResource {
+  getRemoteMCPServerResource(): RemoteMCPServerResource {
     if (this.serverType !== "remote") {
       throw new Error("This MCP server view is not a remote server view");
     }
@@ -390,16 +397,16 @@ export class MCPServerViewResource extends ResourceWithSpace<MCPServerViewModel>
       throw new Error("This MCP server view is missing a remote server ID");
     }
 
-    if (!this.remoteMCPServer) {
+    if (!this.remoteMCPServerResource) {
       throw new Error(
         "This MCP server view is referencing a non-existent remote server"
       );
     }
 
-    return this.remoteMCPServer;
+    return this.remoteMCPServerResource;
   }
 
-  getInternalMCPServer(): InternalMCPServerInMemoryResource {
+  getInternalMCPServerResource(): InternalMCPServerInMemoryResource {
     if (this.serverType !== "internal") {
       throw new Error("This MCP server view is not an internal server view");
     }
@@ -417,39 +424,6 @@ export class MCPServerViewResource extends ResourceWithSpace<MCPServerViewModel>
     return this.internalMCPServer;
   }
 
-  async getMCPServerMetadata(auth: Authenticator): Promise<MCPServerType> {
-    switch (this.serverType) {
-      case "remote": {
-        const remoteMCPServer = this.getRemoteMCPServer();
-
-        // Note: this won't attempt to connect to remote servers and will use the cached metadata.
-        return remoteMCPServer.toJSON();
-      }
-      case "internal": {
-        if (!this.internalMCPServerId) {
-          throw new Error(
-            `Internal MCP server ID is required for internal server type.`
-          );
-        }
-
-        const internalMCPServer =
-          await InternalMCPServerInMemoryResource.fetchById(
-            auth,
-            this.internalMCPServerId
-          );
-        if (!internalMCPServer) {
-          throw new Error(
-            `Internal MCP server with ID ${this.internalMCPServerId} not found.`
-          );
-        }
-        return internalMCPServer.toJSON();
-      }
-      default: {
-        assertNever(this.serverType);
-      }
-    }
-  }
-
   get sId(): string {
     return MCPServerViewResource.modelIdToSId({
       id: this.id,
@@ -616,11 +590,13 @@ export class MCPServerViewResource extends ResourceWithSpace<MCPServerViewModel>
       spaceId: this.space.sId,
       server:
         this.serverType === "remote"
-          ? this.getRemoteMCPServer().toJSON()
-          : this.getInternalMCPServer().toJSON(),
+          ? this.getRemoteMCPServerResource().toJSON()
+          : this.getInternalMCPServerResource().toJSON(),
       editedByUser: this.makeEditedBy(
         this.editedByUser,
-        this.remoteMCPServer ? this.remoteMCPServer.updatedAt : this.updatedAt
+        this.remoteMCPServerResource
+          ? this.remoteMCPServerResource.updatedAt
+          : this.updatedAt
       ),
     };
   }
diff --git a/front/vite.globalSetup.ts b/front/vite.globalSetup.ts
index 05a810f58355..ad8e0924d32f 100644
--- a/front/vite.globalSetup.ts
+++ b/front/vite.globalSetup.ts
@@ -22,6 +22,7 @@ export default async function setup() {
 
     // Add any other essential vars you need to keep
     FRONT_DATABASE_URI: process.env.FRONT_DATABASE_URI,
+    REDIS_CACHE_URI: process.env.REDIS_CACHE_URI,
     NEXT_PUBLIC_DUST_CLIENT_FACING_URL: "http://fake-url",
     DUST_US_URL: "http://fake-url",
     LOG_LEVEL: process.env.TEST_LOG_LEVEL ?? "silent",


Reviews:
user: login: spolu
body: Not sure I understand the redis for internal stuff? Going to SQL should be ~ equivalent if properly indexed?

Remote MCP server stuff is not touched here? Is it cached?
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T11:04:17Z
user: login: Fraggle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T11:34:53Z
user: login: spolu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T12:55:37Z
user: login: Fraggle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T12:57:00Z
user: login: Fraggle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T12:57:45Z
user: login: spolu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T13:57:44Z
user: login: Fraggle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T14:06:05Z

Review comments:
diff_hunk: @@ -50,29 +54,49 @@ export class InternalMCPServerInMemoryResource {
 
     const server = new InternalMCPServerInMemoryResource(id);
 
-    const s = await connectToMCPServer(auth, {
-      type: "mcpServerId",
-      mcpServerId: id,
-    });
+    // Getting the metadata is a relatively long operation, so we cache it for 5 minutes
+    // as internal servers are not expected to change often.
+    // In any case, when actually running the action, the metadata will be fetched from the MCP server.
+    const getCachedMetadata = cacheWithRedis(
path: front/lib/resources/internal_mcp_server_in_memory_resource.ts
user: login: spolu
body: Wait internal mcp servers should be super fast?
created_at: 2025-04-18T11:01:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,29 +54,49 @@ export class InternalMCPServerInMemoryResource {
 
     const server = new InternalMCPServerInMemoryResource(id);
 
-    const s = await connectToMCPServer(auth, {
-      type: "mcpServerId",
-      mcpServerId: id,
-    });
+    // Getting the metadata is a relatively long operation, so we cache it for 5 minutes
+    // as internal servers are not expected to change often.
+    // In any case, when actually running the action, the metadata will be fetched from the MCP server.
+    const getCachedMetadata = cacheWithRedis(
path: front/lib/resources/internal_mcp_server_in_memory_resource.ts
user: login: Fraggle
body: > Wait internal mcp servers should be super fast?

I'm not sure to understand your question here.

> Not sure I understand the redis for internal stuff? Going to SQL should be ~ equivalent if properly indexed?

Going thru the mcp client <-> server to get metadata is a bit slow (we are talking a few tens of ms) so when you do it for multiple servers (listing for knowledge or AB), it can add up to a bit of time.

> Remote MCP server stuff is not touched here? Is it cached?

Remote MCP server have all metadata cached in SQL.
created_at: 2025-04-18T11:34:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,29 +54,49 @@ export class InternalMCPServerInMemoryResource {
 
     const server = new InternalMCPServerInMemoryResource(id);
 
-    const s = await connectToMCPServer(auth, {
-      type: "mcpServerId",
-      mcpServerId: id,
-    });
+    // Getting the metadata is a relatively long operation, so we cache it for 5 minutes
+    // as internal servers are not expected to change often.
+    // In any case, when actually running the action, the metadata will be fetched from the MCP server.
+    const getCachedMetadata = cacheWithRedis(
path: front/lib/resources/internal_mcp_server_in_memory_resource.ts
user: login: spolu
body: Let's see. A cleaner (yet less principled) optimization would be to be able to batch the SQL requests but that would break the interfaces of course.
created_at: 2025-04-18T12:55:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,29 +54,49 @@ export class InternalMCPServerInMemoryResource {
 
     const server = new InternalMCPServerInMemoryResource(id);
 
-    const s = await connectToMCPServer(auth, {
-      type: "mcpServerId",
-      mcpServerId: id,
-    });
+    // Getting the metadata is a relatively long operation, so we cache it for 5 minutes
+    // as internal servers are not expected to change often.
+    // In any case, when actually running the action, the metadata will be fetched from the MCP server.
+    const getCachedMetadata = cacheWithRedis(
path: front/lib/resources/internal_mcp_server_in_memory_resource.ts
user: login: Fraggle
body: @spolu there are not sql rows to fetch for internal servers :)
created_at: 2025-04-18T12:57:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,29 +54,49 @@ export class InternalMCPServerInMemoryResource {
 
     const server = new InternalMCPServerInMemoryResource(id);
 
-    const s = await connectToMCPServer(auth, {
-      type: "mcpServerId",
-      mcpServerId: id,
-    });
+    // Getting the metadata is a relatively long operation, so we cache it for 5 minutes
+    // as internal servers are not expected to change often.
+    // In any case, when actually running the action, the metadata will be fetched from the MCP server.
+    const getCachedMetadata = cacheWithRedis(
path: front/lib/resources/internal_mcp_server_in_memory_resource.ts
user: login: Fraggle
body: and if you are talking about remoteMCPServer, this PR fetch them along with the MCPServerView. (so no more separate request)
created_at: 2025-04-18T12:57:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,29 +54,49 @@ export class InternalMCPServerInMemoryResource {
 
     const server = new InternalMCPServerInMemoryResource(id);
 
-    const s = await connectToMCPServer(auth, {
-      type: "mcpServerId",
-      mcpServerId: id,
-    });
+    // Getting the metadata is a relatively long operation, so we cache it for 5 minutes
+    // as internal servers are not expected to change often.
+    // In any case, when actually running the action, the metadata will be fetched from the MCP server.
+    const getCachedMetadata = cacheWithRedis(
path: front/lib/resources/internal_mcp_server_in_memory_resource.ts
user: login: spolu
body: Ok so I still don't have a good mental model as to why fetching an internal server or many is slow?
created_at: 2025-04-18T13:57:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,29 +54,49 @@ export class InternalMCPServerInMemoryResource {
 
     const server = new InternalMCPServerInMemoryResource(id);
 
-    const s = await connectToMCPServer(auth, {
-      type: "mcpServerId",
-      mcpServerId: id,
-    });
+    // Getting the metadata is a relatively long operation, so we cache it for 5 minutes
+    // as internal servers are not expected to change often.
+    // In any case, when actually running the action, the metadata will be fetched from the MCP server.
+    const getCachedMetadata = cacheWithRedis(
path: front/lib/resources/internal_mcp_server_in_memory_resource.ts
user: login: Fraggle
body: Remember that we decided, to keep thing simple and have one source of truth, that we would internally call the internal servers to get their metadata ?

So we use an MCP client and in-memory transport for them, even for UI.

Turns out it's fast but not THAT fast, so when we list available internal MCP servers, the cost adds up, hence the caching to avoid doing it everytime.

We could have database rows for them but that would open the "how / when do we sync the metadata from the code to psql" ?

Once we have a more automated migration system, maybe we can revisit and sync via automated migrations.
created_at: 2025-04-18T14:06:05Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-18T09:43:07Z
body: 
<!--
  0 failure: 
  1 warning:  Files in `**/lib/...
  
  
  DangerID: danger-id-Danger;
-->


<table>
  <thead>
    <tr>
      <th width="50"></th>
      <th width="100%" data-danger-table="true">Warnings</th>
    </tr>
  </thead>
  <tbody><tr>
      <td>:warning:</td>
      <td>

  Files in `**/lib/models/` have been modified and the PR has the `migration-ack` label. Don't forget to run the migration from prodbox.
  </td>
    </tr>
  </tbody>
</table>



<p align="right">
  Generated by :no_entry_sign: <a href="https://danger.systems/js">dangerJS</a> against 0984c473a9913f1cf76924afa251aa65dea003ef
</p>

author_association: CONTRIBUTOR
user: login: spolu
created_at: 2025-04-18T16:03:13Z
body: Do we understand why that's not instantaneous? 
You say they stack, does that mean we load them sequentially?
author_association: CONTRIBUTOR
user: login: Fraggle
created_at: 2025-04-18T16:06:44Z
body: > Do we understand why that's not instantaneous?

I didn't dig beyond profiling which part was slow in our code.
My guess is that there is a bunch of instantiation going on internally, some parsing etc.. (again, we are talking about ~15ms each)

> You say they stack, does that mean we load them sequentially?

We load all the views at once in one query but then for each view, in case of internal servers, we need to "init" them individually. It's true that [we don't // the inits](https://github.com/dust-tt/dust/blob/0b76ee48c8fcbdf92cac5e92254854d279fd89b9/front/lib/resources/mcp_server_view_resource.ts#L215), good idea, will do that.
author_association: CONTRIBUTOR