diff --git a/build-data/paper.at b/build-data/paper.at
index 4945a796a6..26cdb6eafe 100644
--- a/build-data/paper.at
+++ b/build-data/paper.at
@@ -725,6 +725,7 @@ public-f net.minecraft.world.item.trading.MerchantOffer rewardExp
 public-f net.minecraft.world.item.trading.MerchantOffer xp
 public-f net.minecraft.world.level.LevelSettings hardcore
 public-f net.minecraft.world.level.LevelSettings levelName
+public-f net.minecraft.world.level.block.ChestBlock MENU_PROVIDER_COMBINER
 public-f net.minecraft.world.level.block.entity.BannerBlockEntity baseColor
 public-f net.minecraft.world.level.block.entity.trialspawner.TrialSpawner normalConfig
 public-f net.minecraft.world.level.block.entity.trialspawner.TrialSpawner ominousConfig
diff --git a/paper-api/src/main/java/org/bukkit/Bukkit.java b/paper-api/src/main/java/org/bukkit/Bukkit.java
index 9eecd3a68a..9196b1e62b 100644
--- a/paper-api/src/main/java/org/bukkit/Bukkit.java
+++ b/paper-api/src/main/java/org/bukkit/Bukkit.java
@@ -40,6 +40,7 @@ import org.bukkit.inventory.InventoryHolder;
 import org.bukkit.inventory.ItemCraftResult;
 import org.bukkit.inventory.ItemFactory;
 import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.MenuType;
 import org.bukkit.inventory.Merchant;
 import org.bukkit.inventory.Recipe;
 import org.bukkit.inventory.meta.ItemMeta;
@@ -1925,7 +1926,10 @@ public final class Bukkit {
      * @param title the title of the corresponding merchant inventory, displayed
      * when the merchant inventory is viewed
      * @return a new merchant
+     * @deprecated The title parameter is no-longer needed when used with
+     * {@link MenuType#MERCHANT} and {@link MenuType.Typed#builder()}.
      */
+    @Deprecated(since = "1.21.4")
     public static @NotNull Merchant createMerchant(net.kyori.adventure.text.@Nullable Component title) {
         return server.createMerchant(title);
     }
@@ -1936,7 +1940,8 @@ public final class Bukkit {
      * @param title the title of the corresponding merchant inventory, displayed
      * when the merchant inventory is viewed
      * @return a new merchant
-     * @deprecated in favour of {@link #createMerchant(net.kyori.adventure.text.Component)}
+     * @deprecated in favour of {@link #createMerchant(net.kyori.adventure.text.Component)}. The title parameter is
+     * no-longer needed when used with {@link MenuType#MERCHANT} and {@link MenuType.Typed#builder()}
      */
     @NotNull
     @Deprecated // Paper
@@ -1944,6 +1949,16 @@ public final class Bukkit {
         return server.createMerchant(title);
     }
 
+    /**
+     * Creates an empty merchant.
+     *
+     * @return a new merchant
+     */
+    @NotNull
+    public static Merchant createMerchant() {
+        return server.createMerchant();
+    }
+
     /**
      * Gets the amount of consecutive neighbor updates before skipping
      * additional ones.
diff --git a/paper-api/src/main/java/org/bukkit/Server.java b/paper-api/src/main/java/org/bukkit/Server.java
index 041ebeb283..11923ef0ea 100644
--- a/paper-api/src/main/java/org/bukkit/Server.java
+++ b/paper-api/src/main/java/org/bukkit/Server.java
@@ -43,6 +43,7 @@ import org.bukkit.inventory.InventoryHolder;
 import org.bukkit.inventory.ItemCraftResult;
 import org.bukkit.inventory.ItemFactory;
 import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.MenuType;
 import org.bukkit.inventory.Merchant;
 import org.bukkit.inventory.Recipe;
 import org.bukkit.inventory.meta.ItemMeta;
@@ -1565,11 +1566,11 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
      * <br>
      * {@link InventoryType#WORKBENCH} will not process crafting recipes if
      * created with this method. Use
-     * {@link Player#openWorkbench(Location, boolean)} instead.
+     * {@link MenuType#CRAFTING} instead.
      * <br>
      * {@link InventoryType#ENCHANTING} will not process {@link ItemStack}s
      * for possible enchanting results. Use
-     * {@link Player#openEnchanting(Location, boolean)} instead.
+     * {@link MenuType#ENCHANTMENT} instead.
      *
      * @param owner the holder of the inventory, or null to indicate no holder
      * @param type the type of inventory to create
@@ -1592,11 +1593,11 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
      * <br>
      * {@link InventoryType#WORKBENCH} will not process crafting recipes if
      * created with this method. Use
-     * {@link Player#openWorkbench(Location, boolean)} instead.
+     * {@link MenuType#CRAFTING} instead.
      * <br>
      * {@link InventoryType#ENCHANTING} will not process {@link ItemStack}s
      * for possible enchanting results. Use
-     * {@link Player#openEnchanting(Location, boolean)} instead.
+     * {@link MenuType#ENCHANTMENT} instead.
      *
      * @param owner The holder of the inventory; can be null if there's no holder.
      * @param type The type of inventory to create.
@@ -1620,11 +1621,11 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
      * <br>
      * {@link InventoryType#WORKBENCH} will not process crafting recipes if
      * created with this method. Use
-     * {@link Player#openWorkbench(Location, boolean)} instead.
+     * {@link MenuType#CRAFTING} instead.
      * <br>
      * {@link InventoryType#ENCHANTING} will not process {@link ItemStack}s
      * for possible enchanting results. Use
-     * {@link Player#openEnchanting(Location, boolean)} instead.
+     * {@link MenuType#ENCHANTMENT} instead.
      *
      * @param owner The holder of the inventory; can be null if there's no holder.
      * @param type The type of inventory to create.
@@ -1691,7 +1692,10 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
      * @param title the title of the corresponding merchant inventory, displayed
      * when the merchant inventory is viewed
      * @return a new merchant
+     * @deprecated The title parameter is no-longer needed when used with
+     * {@link MenuType#MERCHANT} and {@link MenuType.Typed#builder()}.
      */
+    @Deprecated(since = "1.21.4")
     @NotNull Merchant createMerchant(net.kyori.adventure.text.@Nullable Component title);
     // Paper start
     /**
@@ -1700,7 +1704,8 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
      * @param title the title of the corresponding merchant inventory, displayed
      * when the merchant inventory is viewed
      * @return a new merchant
-     * @deprecated in favour of {@link #createMerchant(net.kyori.adventure.text.Component)}
+     * @deprecated in favour of {@link #createMerchant(net.kyori.adventure.text.Component)}, The title parameter is
+     * no-longer needed when used with {@link MenuType#MERCHANT} and {@link MenuType.Typed#builder()}.
      */
     @NotNull
     @Deprecated // Paper
@@ -1715,6 +1720,14 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
      */
     int getMaxChainedNeighborUpdates();
 
+    /**
+     * Creates an empty merchant.
+     *
+     * @return a new merchant
+     */
+    @NotNull
+    Merchant createMerchant();
+
     /**
      * Gets user-specified limit for number of monsters that can spawn in a
      * chunk.
diff --git a/paper-api/src/main/java/org/bukkit/entity/HumanEntity.java b/paper-api/src/main/java/org/bukkit/entity/HumanEntity.java
index 6303c68b21..f04a6bf9e5 100644
--- a/paper-api/src/main/java/org/bukkit/entity/HumanEntity.java
+++ b/paper-api/src/main/java/org/bukkit/entity/HumanEntity.java
@@ -13,6 +13,7 @@ import org.bukkit.inventory.InventoryHolder;
 import org.bukkit.inventory.InventoryView;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.MainHand;
+import org.bukkit.inventory.MenuType;
 import org.bukkit.inventory.Merchant;
 import org.bukkit.inventory.PlayerInventory;
 import org.bukkit.inventory.meta.FireworkMeta;
@@ -126,7 +127,10 @@ public interface HumanEntity extends LivingEntity, AnimalTamer, InventoryHolder
      *     no inventory will be opened and null will be returned.
      * @return The newly opened inventory view, or null if it could not be
      *     opened.
+     * @deprecated This method should be replaced by {@link MenuType#CRAFTING}
+     * see {@link MenuType.Typed#builder()} and its options for more information.
      */
+    @Deprecated(since = "1.21.4")
     @Nullable
     public InventoryView openWorkbench(@Nullable Location location, boolean force);
 
@@ -140,7 +144,10 @@ public interface HumanEntity extends LivingEntity, AnimalTamer, InventoryHolder
      *     location, no inventory will be opened and null will be returned.
      * @return The newly opened inventory view, or null if it could not be
      *     opened.
+     * @deprecated This method should be replaced by {@link MenuType#ENCHANTMENT}
+     * see {@link MenuType.Typed#builder()} and its options for more information.
      */
+    @Deprecated(since = "1.21.4")
     @Nullable
     public InventoryView openEnchanting(@Nullable Location location, boolean force);
 
@@ -166,8 +173,10 @@ public interface HumanEntity extends LivingEntity, AnimalTamer, InventoryHolder
      * @param trader The merchant to trade with. Cannot be null.
      * @param force whether to force the trade even if another player is trading
      * @return The newly opened inventory view, or null if it could not be
-     * opened.
+     * @deprecated This method can be replaced by using {@link MenuType#MERCHANT}
+     * in conjunction with {@link #openInventory(InventoryView)}.
      */
+    @Deprecated(since = "1.21.4")
     @Nullable
     public InventoryView openMerchant(@NotNull Villager trader, boolean force);
 
@@ -180,8 +189,10 @@ public interface HumanEntity extends LivingEntity, AnimalTamer, InventoryHolder
      * @param merchant The merchant to trade with. Cannot be null.
      * @param force whether to force the trade even if another player is trading
      * @return The newly opened inventory view, or null if it could not be
-     * opened.
+     * @deprecated This method can be replaced by using {@link MenuType#MERCHANT}
+     * in conjunction with {@link #openInventory(InventoryView)}.
      */
+    @Deprecated(since = "1.21.4")
     @Nullable
     public InventoryView openMerchant(@NotNull Merchant merchant, boolean force);
 
@@ -196,7 +207,10 @@ public interface HumanEntity extends LivingEntity, AnimalTamer, InventoryHolder
      *     no inventory will be opened and null will be returned.
      * @return The newly opened inventory view, or null if it could not be
      *     opened.
+     * @deprecated This method should be replaced by {@link MenuType#ANVIL}
+     * see {@link MenuType.Typed#builder()} and its options for more information.
      */
+    @Deprecated(since = "1.21.4")
     @Nullable
     public InventoryView openAnvil(@Nullable Location location, boolean force);
 
@@ -210,7 +224,10 @@ public interface HumanEntity extends LivingEntity, AnimalTamer, InventoryHolder
      *     no inventory will be opened and null will be returned.
      * @return The newly opened inventory view, or null if it could not be
      *     opened.
+     * @deprecated This method should be replaced by {@link MenuType#CARTOGRAPHY_TABLE}
+     * see {@link MenuType.Typed#builder()} and its options for more information.
      */
+    @Deprecated(since = "1.21.4")
     @Nullable
     public InventoryView openCartographyTable(@Nullable Location location, boolean force);
 
@@ -224,7 +241,10 @@ public interface HumanEntity extends LivingEntity, AnimalTamer, InventoryHolder
      *     no inventory will be opened and null will be returned.
      * @return The newly opened inventory view, or null if it could not be
      *     opened.
+     * @deprecated This method should be replaced by {@link MenuType#GRINDSTONE}
+     * see {@link MenuType.Typed#builder()} and its options for more information.
      */
+    @Deprecated(since = "1.21.4")
     @Nullable
     public InventoryView openGrindstone(@Nullable Location location, boolean force);
 
@@ -238,7 +258,10 @@ public interface HumanEntity extends LivingEntity, AnimalTamer, InventoryHolder
      *     no inventory will be opened and null will be returned.
      * @return The newly opened inventory view, or null if it could not be
      *     opened.
+     * @deprecated This method should be replaced by {@link MenuType#LOOM}
+     * see {@link MenuType.Typed#builder()} and its options for more information.
      */
+    @Deprecated(since = "1.21.4")
     @Nullable
     public InventoryView openLoom(@Nullable Location location, boolean force);
 
@@ -252,7 +275,10 @@ public interface HumanEntity extends LivingEntity, AnimalTamer, InventoryHolder
      *     no inventory will be opened and null will be returned.
      * @return The newly opened inventory view, or null if it could not be
      *     opened.
+     * @deprecated This method should be replaced by {@link MenuType#SMITHING}
+     * see {@link MenuType.Typed#builder()} and its options for more information.
      */
+    @Deprecated(since = "1.21.4")
     @Nullable
     public InventoryView openSmithingTable(@Nullable Location location, boolean force);
 
@@ -266,7 +292,10 @@ public interface HumanEntity extends LivingEntity, AnimalTamer, InventoryHolder
      *     no inventory will be opened and null will be returned.
      * @return The newly opened inventory view, or null if it could not be
      *     opened.
+     * @deprecated This method should be replaced by {@link MenuType#STONECUTTER}
+     * see {@link MenuType.Typed#builder()} and its options for more information.
      */
+    @Deprecated(since = "1.21.4")
     @Nullable
     public InventoryView openStonecutter(@Nullable Location location, boolean force);
     // Paper end
diff --git a/paper-api/src/main/java/org/bukkit/inventory/MenuType.java b/paper-api/src/main/java/org/bukkit/inventory/MenuType.java
index 529143c900..2442361ce7 100644
--- a/paper-api/src/main/java/org/bukkit/inventory/MenuType.java
+++ b/paper-api/src/main/java/org/bukkit/inventory/MenuType.java
@@ -14,6 +14,9 @@ import org.bukkit.inventory.view.LecternView;
 import org.bukkit.inventory.view.LoomView;
 import org.bukkit.inventory.view.MerchantView;
 import org.bukkit.inventory.view.StonecutterView;
+import org.bukkit.inventory.view.builder.InventoryViewBuilder;
+import org.bukkit.inventory.view.builder.LocationInventoryViewBuilder;
+import org.bukkit.inventory.view.builder.MerchantInventoryViewBuilder;
 import org.jetbrains.annotations.ApiStatus;
 import org.jetbrains.annotations.NotNull;
 
@@ -27,104 +30,104 @@ public interface MenuType extends Keyed, io.papermc.paper.world.flag.FeatureDepe
     /**
      * A MenuType which represents a chest with 1 row.
      */
-    MenuType.Typed<InventoryView> GENERIC_9X1 = get("generic_9x1");
+    MenuType.Typed<InventoryView, InventoryViewBuilder<InventoryView>> GENERIC_9X1 = get("generic_9x1");
     /**
      * A MenuType which represents a chest with 2 rows.
      */
-    MenuType.Typed<InventoryView> GENERIC_9X2 = get("generic_9x2");
+    MenuType.Typed<InventoryView, InventoryViewBuilder<InventoryView>> GENERIC_9X2 = get("generic_9x2");
     /**
      * A MenuType which represents a chest with 3 rows.
      */
-    MenuType.Typed<InventoryView> GENERIC_9X3 = get("generic_9x3");
+    MenuType.Typed<InventoryView, LocationInventoryViewBuilder<InventoryView>> GENERIC_9X3 = get("generic_9x3");
     /**
      * A MenuType which represents a chest with 4 rows.
      */
-    MenuType.Typed<InventoryView> GENERIC_9X4 = get("generic_9x4");
+    MenuType.Typed<InventoryView, InventoryViewBuilder<InventoryView>> GENERIC_9X4 = get("generic_9x4");
     /**
      * A MenuType which represents a chest with 5 rows.
      */
-    MenuType.Typed<InventoryView> GENERIC_9X5 = get("generic_9x5");
+    MenuType.Typed<InventoryView, InventoryViewBuilder<InventoryView>> GENERIC_9X5 = get("generic_9x5");
     /**
      * A MenuType which represents a chest with 6 rows.
      */
-    MenuType.Typed<InventoryView> GENERIC_9X6 = get("generic_9x6");
+    MenuType.Typed<InventoryView, LocationInventoryViewBuilder<InventoryView>> GENERIC_9X6 = get("generic_9x6");
     /**
      * A MenuType which represents a dispenser/dropper like menu with 3 columns
      * and 3 rows.
      */
-    MenuType.Typed<InventoryView> GENERIC_3X3 = get("generic_3x3");
+    MenuType.Typed<InventoryView, LocationInventoryViewBuilder<InventoryView>> GENERIC_3X3 = get("generic_3x3");
     /**
      * A MenuType which represents a crafter
      */
-    MenuType.Typed<CrafterView> CRAFTER_3X3 = get("crafter_3x3");
+    MenuType.Typed<CrafterView, LocationInventoryViewBuilder<CrafterView>> CRAFTER_3X3 = get("crafter_3x3");
     /**
      * A MenuType which represents an anvil.
      */
-    MenuType.Typed<AnvilView> ANVIL = get("anvil");
+    MenuType.Typed<AnvilView, LocationInventoryViewBuilder<AnvilView>> ANVIL = get("anvil");
     /**
      * A MenuType which represents a beacon.
      */
-    MenuType.Typed<BeaconView> BEACON = get("beacon");
+    MenuType.Typed<BeaconView, LocationInventoryViewBuilder<BeaconView>> BEACON = get("beacon");
     /**
      * A MenuType which represents a blast furnace.
      */
-    MenuType.Typed<FurnaceView> BLAST_FURNACE = get("blast_furnace");
+    MenuType.Typed<FurnaceView, LocationInventoryViewBuilder<FurnaceView>> BLAST_FURNACE = get("blast_furnace");
     /**
      * A MenuType which represents a brewing stand.
      */
-    MenuType.Typed<BrewingStandView> BREWING_STAND = get("brewing_stand");
+    MenuType.Typed<BrewingStandView, LocationInventoryViewBuilder<BrewingStandView>> BREWING_STAND = get("brewing_stand");
     /**
      * A MenuType which represents a crafting table.
      */
-    MenuType.Typed<InventoryView> CRAFTING = get("crafting");
+    MenuType.Typed<InventoryView, LocationInventoryViewBuilder<InventoryView>> CRAFTING = get("crafting");
     /**
      * A MenuType which represents an enchantment table.
      */
-    MenuType.Typed<EnchantmentView> ENCHANTMENT = get("enchantment");
+    MenuType.Typed<EnchantmentView, LocationInventoryViewBuilder<EnchantmentView>> ENCHANTMENT = get("enchantment");
     /**
      * A MenuType which represents a furnace.
      */
-    MenuType.Typed<FurnaceView> FURNACE = get("furnace");
+    MenuType.Typed<FurnaceView, LocationInventoryViewBuilder<FurnaceView>> FURNACE = get("furnace");
     /**
      * A MenuType which represents a grindstone.
      */
-    MenuType.Typed<InventoryView> GRINDSTONE = get("grindstone");
+    MenuType.Typed<InventoryView, LocationInventoryViewBuilder<InventoryView>> GRINDSTONE = get("grindstone");
     /**
      * A MenuType which represents a hopper.
      */
-    MenuType.Typed<InventoryView> HOPPER = get("hopper");
+    MenuType.Typed<InventoryView, LocationInventoryViewBuilder<InventoryView>> HOPPER = get("hopper");
     /**
      * A MenuType which represents a lectern, a book like view.
      */
-    MenuType.Typed<LecternView> LECTERN = get("lectern");
+    MenuType.Typed<LecternView, LocationInventoryViewBuilder<LecternView>> LECTERN = get("lectern");
     /**
      * A MenuType which represents a loom.
      */
-    MenuType.Typed<LoomView> LOOM = get("loom");
+    MenuType.Typed<LoomView, LocationInventoryViewBuilder<LoomView>> LOOM = get("loom");
     /**
      * A MenuType which represents a merchant.
      */
-    MenuType.Typed<MerchantView> MERCHANT = get("merchant");
+    MenuType.Typed<MerchantView, MerchantInventoryViewBuilder<MerchantView>> MERCHANT = get("merchant");
     /**
      * A MenuType which represents a shulker box.
      */
-    MenuType.Typed<InventoryView> SHULKER_BOX = get("shulker_box");
+    MenuType.Typed<InventoryView, LocationInventoryViewBuilder<InventoryView>> SHULKER_BOX = get("shulker_box");
     /**
      * A MenuType which represents a stonecutter.
      */
-    MenuType.Typed<InventoryView> SMITHING = get("smithing");
+    MenuType.Typed<InventoryView, LocationInventoryViewBuilder<InventoryView>> SMITHING = get("smithing");
     /**
      * A MenuType which represents a smoker.
      */
-    MenuType.Typed<FurnaceView> SMOKER = get("smoker");
+    MenuType.Typed<FurnaceView, LocationInventoryViewBuilder<FurnaceView>> SMOKER = get("smoker");
     /**
      * A MenuType which represents a cartography table.
      */
-    MenuType.Typed<InventoryView> CARTOGRAPHY_TABLE = get("cartography_table");
+    MenuType.Typed<InventoryView, LocationInventoryViewBuilder<InventoryView>> CARTOGRAPHY_TABLE = get("cartography_table");
     /**
      * A MenuType which represents a stonecutter.
      */
-    MenuType.Typed<StonecutterView> STONECUTTER = get("stonecutter");
+    MenuType.Typed<StonecutterView, LocationInventoryViewBuilder<StonecutterView>> STONECUTTER = get("stonecutter");
 
     /**
      * Typed represents a subtype of {@link MenuType}s that have a known
@@ -133,7 +136,7 @@ public interface MenuType extends Keyed, io.papermc.paper.world.flag.FeatureDepe
      * @param <V> the generic type of {@link InventoryView} that represents the
      * view type.
      */
-    interface Typed<V extends InventoryView> extends MenuType {
+    interface Typed<V extends InventoryView, B extends InventoryViewBuilder<V>> extends MenuType {
 
         /**
          * Creates a view of the specified menu type.
@@ -166,6 +169,9 @@ public interface MenuType extends Keyed, io.papermc.paper.world.flag.FeatureDepe
         @NotNull
         V create(@NotNull HumanEntity player, @NotNull net.kyori.adventure.text.Component title);
         // Paper end - adventure
+
+        @NotNull
+        B builder();
     }
 
     // Paper start - adventure
@@ -191,7 +197,7 @@ public interface MenuType extends Keyed, io.papermc.paper.world.flag.FeatureDepe
      * @return the typed MenuType.
      */
     @NotNull
-    MenuType.Typed<InventoryView> typed();
+    MenuType.Typed<InventoryView, InventoryViewBuilder<InventoryView>> typed();
 
     /**
      * Yields this MenuType as a typed version of itself with a specific
@@ -201,12 +207,14 @@ public interface MenuType extends Keyed, io.papermc.paper.world.flag.FeatureDepe
      * {@link InventoryView} with.
      * @param <V> the generic type of the InventoryView to get this MenuType
      * with
+     * @param <B> the generic type of the InventoryViewBuilder to get this
+     * MenuType with
      * @return the typed MenuType
      * @throws IllegalArgumentException if the provided viewClass cannot be
      * typed to this MenuType
      */
     @NotNull
-    <V extends InventoryView> MenuType.Typed<V> typed(@NotNull final Class<V> viewClass) throws IllegalArgumentException;
+    <V extends InventoryView, B extends InventoryViewBuilder<V>> MenuType.Typed<V, B> typed(@NotNull final Class<V> viewClass) throws IllegalArgumentException;
 
     /**
      * Gets the {@link InventoryView} class of this MenuType.
diff --git a/paper-api/src/main/java/org/bukkit/inventory/view/builder/InventoryViewBuilder.java b/paper-api/src/main/java/org/bukkit/inventory/view/builder/InventoryViewBuilder.java
new file mode 100644
index 0000000000..9f07522283
--- /dev/null
+++ b/paper-api/src/main/java/org/bukkit/inventory/view/builder/InventoryViewBuilder.java
@@ -0,0 +1,38 @@
+package org.bukkit.inventory.view.builder;
+
+import net.kyori.adventure.text.Component;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.inventory.InventoryView;
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * Generic Builder for InventoryView's with no special attributes or parameters
+ *
+ * @param <V> the type of InventoryView created from this builder
+ */
+@ApiStatus.Experimental
+public interface InventoryViewBuilder<V extends InventoryView> {
+
+    /**
+     * Makes a copy of this builder
+     *
+     * @return a copy of this builder
+     */
+    InventoryViewBuilder<V> copy();
+
+    /**
+     * Sets the title of the builder
+     *
+     * @param title the title
+     * @return this builder
+     */
+    InventoryViewBuilder<V> title(final Component title);
+
+    /**
+     * Builds this builder into a InventoryView
+     *
+     * @param player the player to assign to the view
+     * @return the created InventoryView
+     */
+    V build(final HumanEntity player);
+}
diff --git a/paper-api/src/main/java/org/bukkit/inventory/view/builder/LocationInventoryViewBuilder.java b/paper-api/src/main/java/org/bukkit/inventory/view/builder/LocationInventoryViewBuilder.java
new file mode 100644
index 0000000000..9666aa3569
--- /dev/null
+++ b/paper-api/src/main/java/org/bukkit/inventory/view/builder/LocationInventoryViewBuilder.java
@@ -0,0 +1,55 @@
+package org.bukkit.inventory.view.builder;
+
+import net.kyori.adventure.text.Component;
+import org.bukkit.Location;
+import org.bukkit.inventory.InventoryView;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * An InventoryViewBuilder that can be bound by location within the world
+ *
+ * @param <V> the type of InventoryView created from this builder
+ */
+@ApiStatus.Experimental
+public interface LocationInventoryViewBuilder<V extends InventoryView> extends InventoryViewBuilder<V> {
+
+    @Override
+    LocationInventoryViewBuilder<V> copy();
+
+    @Override
+    LocationInventoryViewBuilder<V> title(final @NotNull Component title);
+
+    /**
+     * Determines whether or not the server should check if the player can reach
+     * the location.
+     * <p>
+     * Not providing a location but setting checkReachable to true will
+     * automatically close the view when opened.
+     * <p>
+     * If checkReachable is set to false and a location is set on the builder if
+     * the target block exists and this builder is the correct menu for that
+     * block, e.g. MenuType.GENERIC_9X3 builder and target block set to chest,
+     * if that block is destroyed the view would persist.
+     *
+     * @param checkReachable whether or not to check if the view is "reachable"
+     * @return this builder
+     */
+    LocationInventoryViewBuilder<V> checkReachable(final boolean checkReachable);
+
+    /**
+     * Binds a location to this builder.
+     * <p>
+     * By binding a location in an unloaded chunk to this builder it is likely
+     * that the given chunk the location is will load. That means that when,
+     * building this view it may come with the costs associated with chunk
+     * loading.
+     * <p>
+     * Providing a location of a tile entity with a non matching menu comes with
+     * extra costs associated with ensuring that the correct view is created.
+     *
+     * @param location the location to bind to this view
+     * @return this builder
+     */
+    LocationInventoryViewBuilder<V> location(final Location location);
+}
diff --git a/paper-api/src/main/java/org/bukkit/inventory/view/builder/MerchantInventoryViewBuilder.java b/paper-api/src/main/java/org/bukkit/inventory/view/builder/MerchantInventoryViewBuilder.java
new file mode 100644
index 0000000000..76aecb54a9
--- /dev/null
+++ b/paper-api/src/main/java/org/bukkit/inventory/view/builder/MerchantInventoryViewBuilder.java
@@ -0,0 +1,44 @@
+package org.bukkit.inventory.view.builder;
+
+import net.kyori.adventure.text.Component;
+import org.bukkit.Server;
+import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.Merchant;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * An InventoryViewBuilder for creating merchant views
+ *
+ * @param <V> the type of InventoryView created by this builder
+ */
+@ApiStatus.Experimental
+public interface MerchantInventoryViewBuilder<V extends InventoryView> extends InventoryViewBuilder<V> {
+
+    @Override
+    MerchantInventoryViewBuilder<V> copy();
+
+    @Override
+    MerchantInventoryViewBuilder<V> title(final @NotNull Component title);
+
+    /**
+     * Adds a merchant to this builder
+     *
+     * @param merchant the merchant
+     * @return this builder
+     */
+    MerchantInventoryViewBuilder<V> merchant(final Merchant merchant);
+
+    /**
+     * Determines whether or not the server should check if the player can reach
+     * the location.
+     * <p>
+     * Given checkReachable is provided and a virtual merchant is provided to
+     * the builder from {@link Server#createMerchant(net.kyori.adventure.text.Component)} this method will
+     * have no effect on the actual menu status.
+     *
+     * @param checkReachable whether or not to check if the view is "reachable"
+     * @return this builder
+     */
+    MerchantInventoryViewBuilder<V> checkReachable(final boolean checkReachable);
+}
diff --git a/paper-api/src/main/java/org/bukkit/inventory/view/builder/package-info.java b/paper-api/src/main/java/org/bukkit/inventory/view/builder/package-info.java
new file mode 100644
index 0000000000..b1e4203dae
--- /dev/null
+++ b/paper-api/src/main/java/org/bukkit/inventory/view/builder/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * A Package that contains builders for building InventoryViews.
+ */
+@NullMarked
+@ApiStatus.Experimental
+package org.bukkit.inventory.view.builder;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jspecify.annotations.NullMarked;
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/AbstractContainerMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/AbstractContainerMenu.java.patch
index e33c611feb..6b7e7d70cb 100644
--- a/paper-server/patches/sources/net/minecraft/world/inventory/AbstractContainerMenu.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/AbstractContainerMenu.java.patch
@@ -9,7 +9,7 @@
  import net.minecraft.server.level.ServerPlayer;
  import net.minecraft.util.Mth;
  import net.minecraft.world.Container;
-@@ -63,6 +_,31 @@
+@@ -63,6 +_,32 @@
      @Nullable
      private ContainerSynchronizer synchronizer;
      private boolean suppressRemoteUpdates;
@@ -37,6 +37,7 @@
 +        com.google.common.base.Preconditions.checkState(this.title == null, "Title already set");
 +        this.title = title;
 +    }
++    public void startOpen() {}
 +    // CraftBukkit end
  
      protected AbstractContainerMenu(@Nullable MenuType<?> menuType, int containerId) {
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/ChestMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/ChestMenu.java.patch
index c86ead27cc..1be2d1bc41 100644
--- a/paper-server/patches/sources/net/minecraft/world/inventory/ChestMenu.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/ChestMenu.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/inventory/ChestMenu.java
 +++ b/net/minecraft/world/inventory/ChestMenu.java
-@@ -9,6 +_,29 @@
+@@ -9,6 +_,34 @@
  public class ChestMenu extends AbstractContainerMenu {
      private final Container container;
      private final int containerRows;
@@ -26,14 +26,21 @@
 +        this.bukkitEntity = new org.bukkit.craftbukkit.inventory.CraftInventoryView(this.player.player.getBukkitEntity(), inventory, this);
 +        return this.bukkitEntity;
 +    }
++
++    @Override
++    public void startOpen() {
++        this.container.startOpen(this.player.player);
++    }
 +    // CraftBukkit end
  
      private ChestMenu(MenuType<?> type, int containerId, Inventory playerInventory, int rows) {
          this(type, containerId, playerInventory, new SimpleContainer(9 * rows), rows);
-@@ -52,6 +_,9 @@
+@@ -51,7 +_,10 @@
+         checkContainerSize(container, rows * 9);
          this.container = container;
          this.containerRows = rows;
-         container.startOpen(playerInventory.player);
+-        container.startOpen(playerInventory.player);
++        // container.startOpen(playerInventory.player); // Paper - don't startOpen until menu actually opens
 +        // CraftBukkit start - Save player
 +        this.player = playerInventory;
 +        // CraftBukkit end
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/MerchantMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/MerchantMenu.java.patch
index d3c402326c..f1c3c78a5b 100644
--- a/paper-server/patches/sources/net/minecraft/world/inventory/MerchantMenu.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/MerchantMenu.java.patch
@@ -27,6 +27,14 @@
          this.addStandardInventorySlots(playerInventory, 108, 84);
      }
  
+@@ -61,6 +_,7 @@
+ 
+     @Override
+     public boolean stillValid(Player player) {
++        if (!checkReachable) return true; // Paper - checkReachable
+         return this.trader.stillValid(player);
+     }
+ 
 @@ -105,12 +_,12 @@
              ItemStack item = slot.getItem();
              itemStack = item.copy();
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/ShulkerBoxMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/ShulkerBoxMenu.java.patch
index a65b4c24dc..590e2788dd 100644
--- a/paper-server/patches/sources/net/minecraft/world/inventory/ShulkerBoxMenu.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/ShulkerBoxMenu.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/inventory/ShulkerBoxMenu.java
 +++ b/net/minecraft/world/inventory/ShulkerBoxMenu.java
-@@ -9,6 +_,20 @@
+@@ -9,6 +_,25 @@
  public class ShulkerBoxMenu extends AbstractContainerMenu {
      private static final int CONTAINER_SIZE = 27;
      private final Container container;
@@ -17,18 +17,25 @@
 +        this.bukkitEntity = new org.bukkit.craftbukkit.inventory.CraftInventoryView(this.player.player.getBukkitEntity(), new org.bukkit.craftbukkit.inventory.CraftInventory(this.container), this);
 +        return this.bukkitEntity;
 +    }
++
++    @Override
++    public void startOpen() {
++        container.startOpen(player.player);
++    }
 +    // CraftBukkit end
  
      public ShulkerBoxMenu(int containerId, Inventory playerInventory) {
          this(containerId, playerInventory, new SimpleContainer(27));
-@@ -18,6 +_,7 @@
+@@ -18,7 +_,8 @@
          super(MenuType.SHULKER_BOX, containerId);
          checkContainerSize(container, 27);
          this.container = container;
+-        container.startOpen(playerInventory.player);
 +        this.player = playerInventory; // CraftBukkit - save player
-         container.startOpen(playerInventory.player);
++        // container.startOpen(playerInventory.player); // Paper - don't startOpen until menu actually opens
          int i = 3;
          int i1 = 9;
+ 
 @@ -33,6 +_,7 @@
  
      @Override
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index fda9daa636..d2de789967 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -258,6 +258,7 @@ import org.bukkit.scoreboard.Criteria;
 import org.bukkit.structure.StructureManager;
 import org.bukkit.util.StringUtil;
 import org.bukkit.util.permissions.DefaultPermissions;
+import org.jetbrains.annotations.NotNull;
 import org.yaml.snakeyaml.LoaderOptions;
 import org.yaml.snakeyaml.Yaml;
 import org.yaml.snakeyaml.constructor.SafeConstructor;
@@ -2485,6 +2486,11 @@ public final class CraftServer implements Server {
         return new CraftMerchantCustom(title == null ? InventoryType.MERCHANT.getDefaultTitle() : title);
     }
 
+    @Override
+    public @NotNull Merchant createMerchant() {
+        return new CraftMerchantCustom(net.kyori.adventure.text.Component.empty());
+    }
+
     @Override
     public int getMaxChainedNeighborUpdates() {
         return this.getServer().getMaxChainedNeighborUpdates();
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java
index a1f42f860f..cafd8c5349 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java
@@ -25,6 +25,7 @@ import net.minecraft.world.entity.player.Player;
 import net.minecraft.world.entity.projectile.FireworkRocketEntity;
 import net.minecraft.world.inventory.AbstractContainerMenu;
 import net.minecraft.world.inventory.MenuType;
+import net.minecraft.world.inventory.MerchantMenu;
 import net.minecraft.world.item.ItemCooldowns;
 import net.minecraft.world.item.crafting.RecipeHolder;
 import net.minecraft.world.item.crafting.RecipeManager;
@@ -50,6 +51,8 @@ import org.bukkit.craftbukkit.inventory.CraftInventoryView;
 import org.bukkit.craftbukkit.inventory.CraftItemStack;
 import org.bukkit.craftbukkit.inventory.CraftMerchantCustom;
 import org.bukkit.craftbukkit.inventory.CraftRecipe;
+import org.bukkit.craftbukkit.inventory.util.CraftMenus;
+import org.bukkit.craftbukkit.util.CraftChatMessage;
 import org.bukkit.craftbukkit.util.CraftLocation;
 import org.bukkit.entity.Firework;
 import org.bukkit.entity.HumanEntity;
@@ -467,6 +470,11 @@ public class CraftHumanEntity extends CraftLivingEntity implements HumanEntity {
 
         // Now open the window
         MenuType<?> windowType = CraftContainer.getNotchInventoryType(inventory.getTopInventory());
+        // we can open these now, delegate for now
+        if (windowType == MenuType.MERCHANT) {
+            CraftMenus.openMerchantMenu(player, (MerchantMenu) container);
+            return;
+        }
 
         //String title = inventory.getTitle(); // Paper - comment
         net.kyori.adventure.text.Component adventure$title = inventory.title(); // Paper
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
index e37aaf77f9..d7a52220e9 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
@@ -1430,6 +1430,7 @@ public class CraftEventFactory {
     }
     public static com.mojang.datafixers.util.Pair<net.kyori.adventure.text.@org.jetbrains.annotations.Nullable Component, @org.jetbrains.annotations.Nullable AbstractContainerMenu> callInventoryOpenEventWithTitle(ServerPlayer player, AbstractContainerMenu container, boolean cancelled) {
         // Paper end - Add titleOverride to InventoryOpenEvent
+        container.startOpen(); // delegate start open logic to before InventoryOpenEvent is fired
         if (player.containerMenu != player.inventoryMenu) { // fire INVENTORY_CLOSE if one already open
             player.connection.handleContainerClose(new ServerboundContainerClosePacket(player.containerMenu.containerId), InventoryCloseEvent.Reason.OPEN_NEW); // Paper - Inventory close reason
         }
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java
index 6d3f9d5dab..1ce328bed5 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java
@@ -132,7 +132,7 @@ public class CraftContainer extends AbstractContainerMenu {
                 if (menu == null) {
                     return net.minecraft.world.inventory.MenuType.GENERIC_9x3;
                 } else {
-                    return ((CraftMenuType<?>) menu).getHandle();
+                    return ((CraftMenuType<?, ?>) menu).getHandle();
                 }
         }
     }
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftMenuType.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftMenuType.java
index e4d81ef26a..4c6cf43cee 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftMenuType.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftMenuType.java
@@ -15,12 +15,14 @@ import org.bukkit.craftbukkit.util.Handleable;
 import org.bukkit.entity.HumanEntity;
 import org.bukkit.inventory.InventoryView;
 import org.bukkit.inventory.MenuType;
+import org.bukkit.inventory.view.builder.InventoryViewBuilder;
+import org.jetbrains.annotations.NotNull;
 
-public class CraftMenuType<V extends InventoryView> implements MenuType.Typed<V>, Handleable<net.minecraft.world.inventory.MenuType<?>>, io.papermc.paper.world.flag.PaperFeatureDependent { // Paper - make FeatureDependant
+public class CraftMenuType<V extends InventoryView, B extends InventoryViewBuilder<V>> implements MenuType.Typed<V, B>, Handleable<net.minecraft.world.inventory.MenuType<?>>, io.papermc.paper.world.flag.PaperFeatureDependent { // Paper - make FeatureDependant
 
     private final NamespacedKey key;
     private final net.minecraft.world.inventory.MenuType<?> handle;
-    private final Supplier<CraftMenus.MenuTypeData<V>> typeData;
+    private final Supplier<CraftMenus.MenuTypeData<V, B>> typeData;
 
     public CraftMenuType(NamespacedKey key, net.minecraft.world.inventory.MenuType<?> handle) {
         this.key = key;
@@ -36,33 +38,28 @@ public class CraftMenuType<V extends InventoryView> implements MenuType.Typed<V>
     @Override
     public V create(final HumanEntity player, final String title) {
     // Paper start - adventure
-        return create(player, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(title));
+        return builder().title(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(title)).build(player);
     }
     @Override
     public V create(final HumanEntity player, final net.kyori.adventure.text.Component title) {
     // Paper end - adventure
-        Preconditions.checkArgument(player != null, "The given player must not be null");
-        Preconditions.checkArgument(title != null, "The given title must not be null");
-        Preconditions.checkArgument(player instanceof CraftHumanEntity, "The given player must be a CraftHumanEntity");
-        final CraftHumanEntity craftHuman = (CraftHumanEntity) player;
-        Preconditions.checkArgument(craftHuman.getHandle() instanceof ServerPlayer, "The given player must be an EntityPlayer");
-        final ServerPlayer serverPlayer = (ServerPlayer) craftHuman.getHandle();
-
-        final AbstractContainerMenu container = this.typeData.get().menuBuilder().build(serverPlayer, this.handle);
-        container.setTitle(io.papermc.paper.adventure.PaperAdventure.asVanilla(title)); // Paper - adventure
-        container.checkReachable = false;
-        return (V) container.getBukkitView();
+        return builder().title(title).build(player);
     }
 
     @Override
-    public Typed<InventoryView> typed() {
+    public B builder() {
+        return typeData.get().viewBuilder().get();
+    }
+
+    @Override
+    public Typed<InventoryView, InventoryViewBuilder<InventoryView>> typed() {
         return this.typed(InventoryView.class);
     }
 
     @Override
-    public <V extends InventoryView> Typed<V> typed(Class<V> clazz) {
+    public <V extends InventoryView, B extends InventoryViewBuilder<V>> Typed<V, B> typed(Class<V> clazz) {
         if (clazz.isAssignableFrom(this.typeData.get().viewClass())) {
-            return (Typed<V>) this;
+            return (Typed<V, B>) this;
         }
 
         throw new IllegalArgumentException("Cannot type InventoryView " + this.key.toString() + " to InventoryView type " + clazz.getSimpleName());
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftMenus.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftMenus.java
index 66e93f8444..84c35792c4 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftMenus.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftMenus.java
@@ -1,16 +1,18 @@
 package org.bukkit.craftbukkit.inventory.util;
 
-import static org.bukkit.craftbukkit.inventory.util.CraftMenuBuilder.*;
-
-import net.minecraft.network.chat.Component;
-import net.minecraft.world.SimpleMenuProvider;
+import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.npc.Villager;
 import net.minecraft.world.inventory.AnvilMenu;
 import net.minecraft.world.inventory.CartographyTableMenu;
 import net.minecraft.world.inventory.CraftingMenu;
 import net.minecraft.world.inventory.EnchantmentMenu;
 import net.minecraft.world.inventory.GrindstoneMenu;
+import net.minecraft.world.inventory.MerchantMenu;
 import net.minecraft.world.inventory.SmithingMenu;
 import net.minecraft.world.inventory.StonecutterMenu;
+import net.minecraft.world.item.trading.Merchant;
+import net.minecraft.world.item.trading.MerchantOffers;
 import net.minecraft.world.level.block.Blocks;
 import net.minecraft.world.level.block.entity.BeaconBlockEntity;
 import net.minecraft.world.level.block.entity.BlastFurnaceBlockEntity;
@@ -20,8 +22,15 @@ import net.minecraft.world.level.block.entity.DispenserBlockEntity;
 import net.minecraft.world.level.block.entity.FurnaceBlockEntity;
 import net.minecraft.world.level.block.entity.HopperBlockEntity;
 import net.minecraft.world.level.block.entity.LecternBlockEntity;
+import net.minecraft.world.level.block.entity.ShulkerBoxBlockEntity;
 import net.minecraft.world.level.block.entity.SmokerBlockEntity;
 import org.bukkit.craftbukkit.inventory.CraftMenuType;
+import org.bukkit.craftbukkit.inventory.CraftMerchant;
+import org.bukkit.craftbukkit.inventory.view.builder.CraftAccessLocationInventoryViewBuilder;
+import org.bukkit.craftbukkit.inventory.view.builder.CraftBlockEntityInventoryViewBuilder;
+import org.bukkit.craftbukkit.inventory.view.builder.CraftDoubleChestInventoryViewBuilder;
+import org.bukkit.craftbukkit.inventory.view.builder.CraftMerchantInventoryViewBuilder;
+import org.bukkit.craftbukkit.inventory.view.builder.CraftStandardInventoryViewBuilder;
 import org.bukkit.inventory.InventoryView;
 import org.bukkit.inventory.MenuType;
 import org.bukkit.inventory.view.AnvilView;
@@ -34,83 +43,120 @@ import org.bukkit.inventory.view.LecternView;
 import org.bukkit.inventory.view.LoomView;
 import org.bukkit.inventory.view.MerchantView;
 import org.bukkit.inventory.view.StonecutterView;
+import org.bukkit.inventory.view.builder.InventoryViewBuilder;
+import org.jspecify.annotations.NullMarked;
 
+import java.util.function.Supplier;
+
+@NullMarked
 public final class CraftMenus {
 
-    public record MenuTypeData<V extends InventoryView>(Class<V> viewClass, CraftMenuBuilder menuBuilder) {
+    public record MenuTypeData<V extends InventoryView, B extends InventoryViewBuilder<V>>(Class<V> viewClass, Supplier<B> viewBuilder) {
     }
 
-    private static final CraftMenuBuilder STANDARD = (player, menuType) -> menuType.create(player.nextContainerCounter(), player.getInventory());
+    // This is a temporary measure that will likely be removed with the rewrite of HumanEntity#open[] methods
+    public static void openMerchantMenu(final ServerPlayer player, final MerchantMenu merchant) {
+        final Merchant minecraftMerchant = ((CraftMerchant) merchant.getBukkitView().getMerchant()).getMerchant();
+        int level = 1;
+        if (minecraftMerchant instanceof final Villager villager) {
+            level = villager.getVillagerData().getLevel();
+        }
 
-    public static <V extends InventoryView> MenuTypeData<V> getMenuTypeData(CraftMenuType<?> menuType) {
+        if (minecraftMerchant.getTradingPlayer() != null) { // merchant's can only have one trader
+            minecraftMerchant.getTradingPlayer().closeContainer();
+        }
+
+        minecraftMerchant.setTradingPlayer(player);
+
+        player.connection.send(new ClientboundOpenScreenPacket(merchant.containerId, net.minecraft.world.inventory.MenuType.MERCHANT, merchant.getTitle()));
+        player.containerMenu = merchant;
+        player.initMenu(merchant);
+        // Copy IMerchant#openTradingScreen
+        MerchantOffers merchantrecipelist = minecraftMerchant.getOffers();
+
+        if (!merchantrecipelist.isEmpty()) {
+            player.sendMerchantOffers(merchant.containerId, merchantrecipelist, level, minecraftMerchant.getVillagerXp(), minecraftMerchant.showProgressBar(), minecraftMerchant.canRestock());
+        }
+        // End Copy IMerchant#openTradingScreen
+    }
+
+    public static <V extends InventoryView, B extends InventoryViewBuilder<V>> MenuTypeData<V, B> getMenuTypeData(final CraftMenuType<?, ?> menuType) {
+        final net.minecraft.world.inventory.MenuType<?> handle = menuType.getHandle();
+        // this sucks horribly but it should work for now
+        if (menuType == MenuType.GENERIC_9X6) {
+            return asType(new MenuTypeData<>(InventoryView.class, () -> new CraftDoubleChestInventoryViewBuilder<>(handle)));
+        }
+        if (menuType == MenuType.GENERIC_9X3) {
+            return asType(new MenuTypeData<>(InventoryView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.CHEST, null)));
+        }
         // this isn't ideal as both dispenser and dropper are 3x3, InventoryType can't currently handle generic 3x3s with size 9
         // this needs to be removed when inventory creation is overhauled
         if (menuType == MenuType.GENERIC_3X3) {
-            return CraftMenus.asType(new MenuTypeData<>(InventoryView.class, tileEntity(DispenserBlockEntity::new, Blocks.DISPENSER)));
+            return asType(new MenuTypeData<>(InventoryView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.DISPENSER, DispenserBlockEntity::new)));
         }
         if (menuType == MenuType.CRAFTER_3X3) {
-            return CraftMenus.asType(new MenuTypeData<>(CrafterView.class, tileEntity(CrafterBlockEntity::new, Blocks.CRAFTER)));
+            return asType(new MenuTypeData<>(CrafterView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.CRAFTER, CrafterBlockEntity::new)));
         }
         if (menuType == MenuType.ANVIL) {
-            return CraftMenus.asType(new MenuTypeData<>(AnvilView.class, worldAccess(AnvilMenu::new)));
+            return asType(new MenuTypeData<>(AnvilView.class, () -> new CraftAccessLocationInventoryViewBuilder<>(handle, AnvilMenu::new)));
         }
         if (menuType == MenuType.BEACON) {
-            return CraftMenus.asType(new MenuTypeData<>(BeaconView.class, tileEntity(BeaconBlockEntity::new, Blocks.BEACON)));
+            return asType(new MenuTypeData<>(BeaconView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.BEACON, BeaconBlockEntity::new)));
         }
         if (menuType == MenuType.BLAST_FURNACE) {
-            return CraftMenus.asType(new MenuTypeData<>(FurnaceView.class, tileEntity(BlastFurnaceBlockEntity::new, Blocks.BLAST_FURNACE)));
+            return asType(new MenuTypeData<>(FurnaceView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.BLAST_FURNACE, BlastFurnaceBlockEntity::new)));
         }
         if (menuType == MenuType.BREWING_STAND) {
-            return CraftMenus.asType(new MenuTypeData<>(BrewingStandView.class, tileEntity(BrewingStandBlockEntity::new, Blocks.BREWING_STAND)));
+            return asType(new MenuTypeData<>(BrewingStandView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.BREWING_STAND, BrewingStandBlockEntity::new)));
         }
         if (menuType == MenuType.CRAFTING) {
-            return CraftMenus.asType(new MenuTypeData<>(InventoryView.class, worldAccess(CraftingMenu::new)));
+            return asType(new MenuTypeData<>(InventoryView.class, () -> new CraftAccessLocationInventoryViewBuilder<>(handle, CraftingMenu::new)));
         }
         if (menuType == MenuType.ENCHANTMENT) {
-            return CraftMenus.asType(new MenuTypeData<>(EnchantmentView.class, (player, type) -> {
-                return new SimpleMenuProvider((syncId, inventory, human) -> {
-                    return worldAccess(EnchantmentMenu::new).build(player, type);
-                }, Component.empty()).createMenu(player.nextContainerCounter(), player.getInventory(), player);
-            }));
+            return asType(new MenuTypeData<>(EnchantmentView.class, () -> new CraftAccessLocationInventoryViewBuilder<>(handle, EnchantmentMenu::new)));
         }
         if (menuType == MenuType.FURNACE) {
-            return CraftMenus.asType(new MenuTypeData<>(FurnaceView.class, tileEntity(FurnaceBlockEntity::new, Blocks.FURNACE)));
+            return asType(new MenuTypeData<>(FurnaceView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.FURNACE, FurnaceBlockEntity::new)));
         }
         if (menuType == MenuType.GRINDSTONE) {
-            return CraftMenus.asType(new MenuTypeData<>(InventoryView.class, worldAccess(GrindstoneMenu::new)));
+            return asType(new MenuTypeData<>(InventoryView.class, () -> new CraftAccessLocationInventoryViewBuilder<>(handle, GrindstoneMenu::new)));
         }
         // We really don't need to be creating a tile entity for hopper but currently InventoryType doesn't have capacity
         // to understand otherwise
         if (menuType == MenuType.HOPPER) {
-            return CraftMenus.asType(new MenuTypeData<>(InventoryView.class, tileEntity(HopperBlockEntity::new, Blocks.HOPPER)));
+            return asType(new MenuTypeData<>(InventoryView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.HOPPER, HopperBlockEntity::new)));
         }
         // We also don't need to create a tile entity for lectern, but again InventoryType isn't smart enough to know any better
         if (menuType == MenuType.LECTERN) {
-            return CraftMenus.asType(new MenuTypeData<>(LecternView.class, tileEntity(LecternBlockEntity::new, Blocks.LECTERN)));
+            return asType(new MenuTypeData<>(LecternView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.LECTERN, LecternBlockEntity::new)));
         }
         if (menuType == MenuType.LOOM) {
-            return CraftMenus.asType(new MenuTypeData<>(LoomView.class, CraftMenus.STANDARD));
+            return asType(new MenuTypeData<>(LoomView.class, () -> new CraftStandardInventoryViewBuilder<>(handle)));
         }
         if (menuType == MenuType.MERCHANT) {
-            return CraftMenus.asType(new MenuTypeData<>(MerchantView.class, CraftMenus.STANDARD));
+            return asType(new MenuTypeData<>(MerchantView.class, () -> new CraftMerchantInventoryViewBuilder<>(handle)));
+        }
+        if (menuType == MenuType.SHULKER_BOX) {
+            return asType(new MenuTypeData<>(InventoryView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.SHULKER_BOX, ShulkerBoxBlockEntity::new)));
         }
         if (menuType == MenuType.SMITHING) {
-            return CraftMenus.asType(new MenuTypeData<>(InventoryView.class, worldAccess(SmithingMenu::new)));
+            return asType(new MenuTypeData<>(InventoryView.class, () -> new CraftAccessLocationInventoryViewBuilder<>(handle, SmithingMenu::new)));
         }
         if (menuType == MenuType.SMOKER) {
-            return CraftMenus.asType(new MenuTypeData<>(FurnaceView.class, tileEntity(SmokerBlockEntity::new, Blocks.SMOKER)));
+            return asType(new MenuTypeData<>(FurnaceView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.SMOKER, SmokerBlockEntity::new)));
         }
         if (menuType == MenuType.CARTOGRAPHY_TABLE) {
-            return CraftMenus.asType(new MenuTypeData<>(InventoryView.class, worldAccess(CartographyTableMenu::new)));
+            return asType(new MenuTypeData<>(InventoryView.class, () -> new CraftAccessLocationInventoryViewBuilder<>(handle, CartographyTableMenu::new)));
         }
         if (menuType == MenuType.STONECUTTER) {
-            return CraftMenus.asType(new MenuTypeData<>(StonecutterView.class, worldAccess(StonecutterMenu::new)));
+            return asType(new MenuTypeData<>(StonecutterView.class, () -> new CraftAccessLocationInventoryViewBuilder<>(handle, StonecutterMenu::new)));
         }
 
-        return CraftMenus.asType(new MenuTypeData<>(InventoryView.class, CraftMenus.STANDARD));
+        return asType(new MenuTypeData<>(InventoryView.class, () -> new CraftStandardInventoryViewBuilder<>(handle)));
     }
 
-    private static <V extends InventoryView> MenuTypeData<V> asType(MenuTypeData<?> data) {
-        return (MenuTypeData<V>) data;
+    @SuppressWarnings("unchecked")
+    private static <V extends InventoryView, B extends InventoryViewBuilder<V>> MenuTypeData<V, B> asType(final MenuTypeData<?, ?> data) {
+        return (MenuTypeData<V, B>) data;
     }
 }
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftAbstractInventoryViewBuilder.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftAbstractInventoryViewBuilder.java
new file mode 100644
index 0000000000..185ad0fc16
--- /dev/null
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftAbstractInventoryViewBuilder.java
@@ -0,0 +1,48 @@
+package org.bukkit.craftbukkit.inventory.view.builder;
+
+import com.google.common.base.Preconditions;
+import io.papermc.paper.adventure.PaperAdventure;
+import net.kyori.adventure.text.Component;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.inventory.AbstractContainerMenu;
+import net.minecraft.world.inventory.MenuType;
+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.view.builder.InventoryViewBuilder;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+public abstract class CraftAbstractInventoryViewBuilder<V extends InventoryView> implements InventoryViewBuilder<V> {
+
+    protected final MenuType<?> handle;
+
+    protected boolean checkReachable = false;
+    protected @MonotonicNonNull Component title = null;
+
+    public CraftAbstractInventoryViewBuilder(final MenuType<?> handle) {
+        this.handle = handle;
+    }
+
+    @Override
+    public InventoryViewBuilder<V> title(final Component title) {
+        this.title = title;
+        return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public V build(final HumanEntity player) {
+        Preconditions.checkArgument(player != null, "The given player must not be null");
+        Preconditions.checkArgument(this.title != null, "The given title must not be null");
+        Preconditions.checkArgument(player instanceof CraftHumanEntity, "The given player must be a CraftHumanEntity");
+        final CraftHumanEntity craftHuman = (CraftHumanEntity) player;
+        Preconditions.checkArgument(craftHuman.getHandle() instanceof ServerPlayer, "The given player must be an EntityPlayer");
+        final ServerPlayer serverPlayer = (ServerPlayer) craftHuman.getHandle();
+        final AbstractContainerMenu container = buildContainer(serverPlayer);
+        container.checkReachable = this.checkReachable;
+        container.setTitle(PaperAdventure.asVanilla(this.title));
+        return (V) container.getBukkitView();
+    }
+
+    protected abstract AbstractContainerMenu buildContainer(ServerPlayer player);
+}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftAbstractLocationInventoryViewBuilder.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftAbstractLocationInventoryViewBuilder.java
new file mode 100644
index 0000000000..7a894ca078
--- /dev/null
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftAbstractLocationInventoryViewBuilder.java
@@ -0,0 +1,48 @@
+package org.bukkit.craftbukkit.inventory.view.builder;
+
+import com.google.common.base.Preconditions;
+import net.kyori.adventure.text.Component;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.inventory.MenuType;
+import net.minecraft.world.level.Level;
+import org.bukkit.Location;
+import org.bukkit.craftbukkit.CraftWorld;
+import org.bukkit.craftbukkit.util.CraftLocation;
+import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.view.builder.LocationInventoryViewBuilder;
+import org.jspecify.annotations.Nullable;
+
+public abstract class CraftAbstractLocationInventoryViewBuilder<V extends InventoryView> extends CraftAbstractInventoryViewBuilder<V> implements LocationInventoryViewBuilder<V> {
+
+    protected @Nullable Level world;
+    protected @Nullable BlockPos position;
+
+    public CraftAbstractLocationInventoryViewBuilder(final MenuType<?> handle) {
+        super(handle);
+    }
+
+    @Override
+    public LocationInventoryViewBuilder<V> title(final Component title) {
+        return (LocationInventoryViewBuilder<V>) super.title(title);
+    }
+
+    @Override
+    public LocationInventoryViewBuilder<V> copy() {
+        throw new UnsupportedOperationException("copy is not implemented on CraftAbstractLocationInventoryViewBuilder");
+    }
+
+    @Override
+    public LocationInventoryViewBuilder<V> checkReachable(final boolean checkReachable) {
+        super.checkReachable = checkReachable;
+        return this;
+    }
+
+    @Override
+    public LocationInventoryViewBuilder<V> location(final Location location) {
+        Preconditions.checkArgument(location != null, "The provided location must not be null");
+        Preconditions.checkArgument(location.getWorld() != null, "The provided location must be associated with a world");
+        this.world = ((CraftWorld) location.getWorld()).getHandle();
+        this.position = CraftLocation.toBlockPosition(location);
+        return this;
+    }
+}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftAccessLocationInventoryViewBuilder.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftAccessLocationInventoryViewBuilder.java
new file mode 100644
index 0000000000..096f3ebf81
--- /dev/null
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftAccessLocationInventoryViewBuilder.java
@@ -0,0 +1,45 @@
+package org.bukkit.craftbukkit.inventory.view.builder;
+
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.player.Inventory;
+import net.minecraft.world.inventory.AbstractContainerMenu;
+import net.minecraft.world.inventory.ContainerLevelAccess;
+import net.minecraft.world.inventory.MenuType;
+import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.view.builder.LocationInventoryViewBuilder;
+
+public class CraftAccessLocationInventoryViewBuilder<V extends InventoryView> extends CraftAbstractLocationInventoryViewBuilder<V> {
+
+    private final CraftAccessContainerObjectBuilder containerBuilder;
+
+    public CraftAccessLocationInventoryViewBuilder(final MenuType<?> handle, final CraftAccessContainerObjectBuilder containerBuilder) {
+        super(handle);
+        this.containerBuilder = containerBuilder;
+    }
+
+    @Override
+    protected AbstractContainerMenu buildContainer(final ServerPlayer player) {
+        final ContainerLevelAccess access;
+        if (super.position == null) {
+            access = ContainerLevelAccess.create(player.level(), player.blockPosition());
+        } else {
+            access = ContainerLevelAccess.create(super.world, super.position);
+        }
+
+        return this.containerBuilder.build(player.nextContainerCounter(), player.getInventory(), access);
+    }
+
+    @Override
+    public LocationInventoryViewBuilder<V> copy() {
+        final CraftAccessLocationInventoryViewBuilder<V> copy = new CraftAccessLocationInventoryViewBuilder<>(this.handle, this.containerBuilder);
+        copy.world = super.world;
+        copy.position = super.position;
+        copy.checkReachable = super.checkReachable;
+        copy.title = title;
+        return copy;
+    }
+
+    public interface CraftAccessContainerObjectBuilder {
+        AbstractContainerMenu build(final int syncId, final Inventory inventory, ContainerLevelAccess access);
+    }
+}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftBlockEntityInventoryViewBuilder.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftBlockEntityInventoryViewBuilder.java
new file mode 100644
index 0000000000..2625814440
--- /dev/null
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftBlockEntityInventoryViewBuilder.java
@@ -0,0 +1,74 @@
+package org.bukkit.craftbukkit.inventory.view.builder;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.MenuProvider;
+import net.minecraft.world.inventory.AbstractContainerMenu;
+import net.minecraft.world.inventory.MenuConstructor;
+import net.minecraft.world.inventory.MenuType;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.world.level.block.state.BlockState;
+import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.view.builder.LocationInventoryViewBuilder;
+import org.jspecify.annotations.Nullable;
+
+public class CraftBlockEntityInventoryViewBuilder<V extends InventoryView> extends CraftAbstractLocationInventoryViewBuilder<V> {
+
+    private final Block block;
+    private final @Nullable CraftTileInventoryBuilder builder;
+
+    public CraftBlockEntityInventoryViewBuilder(final MenuType<?> handle, final Block block, final @Nullable CraftTileInventoryBuilder builder) {
+        super(handle);
+        this.block = block;
+        this.builder = builder;
+    }
+
+    @Override
+    protected AbstractContainerMenu buildContainer(final ServerPlayer player) {
+        if (this.world == null) {
+            this.world = player.level();
+        }
+
+        if (this.position == null) {
+            this.position = player.blockPosition();
+        }
+
+        final BlockEntity entity = this.world.getBlockEntity(position);
+        if (!(entity instanceof final MenuConstructor container)) {
+            return buildFakeTile(player);
+        }
+
+        final AbstractContainerMenu atBlock = container.createMenu(player.nextContainerCounter(), player.getInventory(), player);
+        if (atBlock.getType() != super.handle) {
+            return buildFakeTile(player);
+        }
+
+        return atBlock;
+    }
+
+    private AbstractContainerMenu buildFakeTile(final ServerPlayer player) {
+        if (this.builder == null) {
+            return handle.create(player.nextContainerCounter(), player.getInventory());
+        }
+        final MenuProvider inventory = this.builder.build(this.position, this.block.defaultBlockState());
+        if (inventory instanceof final BlockEntity tile) {
+            tile.setLevel(this.world);
+        }
+        return inventory.createMenu(player.nextContainerCounter(), player.getInventory(), player);
+    }
+
+    @Override
+    public LocationInventoryViewBuilder<V> copy() {
+        final CraftBlockEntityInventoryViewBuilder<V> copy = new CraftBlockEntityInventoryViewBuilder<>(super.handle, this.block, this.builder);
+        copy.world = this.world;
+        copy.position = this.position;
+        copy.checkReachable = super.checkReachable;
+        copy.title = title;
+        return copy;
+    }
+
+    public interface CraftTileInventoryBuilder {
+        MenuProvider build(BlockPos blockPosition, BlockState blockData);
+    }
+}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftDoubleChestInventoryViewBuilder.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftDoubleChestInventoryViewBuilder.java
new file mode 100644
index 0000000000..331e3797a5
--- /dev/null
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftDoubleChestInventoryViewBuilder.java
@@ -0,0 +1,48 @@
+package org.bukkit.craftbukkit.inventory.view.builder;
+
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.MenuProvider;
+import net.minecraft.world.inventory.AbstractContainerMenu;
+import net.minecraft.world.inventory.MenuType;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.ChestBlock;
+import net.minecraft.world.level.block.DoubleBlockCombiner;
+import net.minecraft.world.level.block.entity.ChestBlockEntity;
+import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.view.builder.LocationInventoryViewBuilder;
+
+public class CraftDoubleChestInventoryViewBuilder<V extends InventoryView> extends CraftAbstractLocationInventoryViewBuilder<V> {
+
+    public CraftDoubleChestInventoryViewBuilder(final MenuType<?> handle) {
+        super(handle);
+    }
+
+    @Override
+    protected AbstractContainerMenu buildContainer(final ServerPlayer player) {
+        if (super.world == null) {
+            return handle.create(player.nextContainerCounter(), player.getInventory());
+        }
+
+        final ChestBlock chest = (ChestBlock) Blocks.CHEST;
+        final DoubleBlockCombiner.NeighborCombineResult<? extends ChestBlockEntity> result = chest.combine(super.world.getBlockState(super.position), super.world, super.position, false);
+        if (result instanceof DoubleBlockCombiner.NeighborCombineResult.Single<? extends ChestBlockEntity>) {
+            return handle.create(player.nextContainerCounter(), player.getInventory());
+        }
+
+        final MenuProvider combined = result.apply(ChestBlock.MENU_PROVIDER_COMBINER).orElse(null);
+        if (combined == null) {
+            return handle.create(player.nextContainerCounter(), player.getInventory());
+        }
+        return combined.createMenu(player.nextContainerCounter(), player.getInventory(), player);
+    }
+
+    @Override
+    public LocationInventoryViewBuilder<V> copy() {
+        final CraftDoubleChestInventoryViewBuilder<V> copy = new CraftDoubleChestInventoryViewBuilder<>(super.handle);
+        copy.world = this.world;
+        copy.position = this.position;
+        copy.checkReachable = super.checkReachable;
+        copy.title = title;
+        return copy;
+    }
+}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftMerchantInventoryViewBuilder.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftMerchantInventoryViewBuilder.java
new file mode 100644
index 0000000000..7f7518aa73
--- /dev/null
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftMerchantInventoryViewBuilder.java
@@ -0,0 +1,78 @@
+package org.bukkit.craftbukkit.inventory.view.builder;
+
+import com.google.common.base.Preconditions;
+import io.papermc.paper.adventure.PaperAdventure;
+import net.kyori.adventure.text.Component;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.inventory.AbstractContainerMenu;
+import net.minecraft.world.inventory.MenuType;
+import net.minecraft.world.inventory.MerchantMenu;
+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
+import org.bukkit.craftbukkit.inventory.CraftMerchant;
+import org.bukkit.craftbukkit.inventory.CraftMerchantCustom;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.Merchant;
+import org.bukkit.inventory.view.builder.MerchantInventoryViewBuilder;
+import org.jspecify.annotations.Nullable;
+
+public class CraftMerchantInventoryViewBuilder<V extends InventoryView> extends CraftAbstractInventoryViewBuilder<V> implements MerchantInventoryViewBuilder<V> {
+
+    private net.minecraft.world.item.trading.@Nullable Merchant merchant;
+
+    public CraftMerchantInventoryViewBuilder(final MenuType<?> handle) {
+        super(handle);
+    }
+
+    @Override
+    public MerchantInventoryViewBuilder<V> title(final Component title) {
+        return (MerchantInventoryViewBuilder<V>) super.title(title);
+    }
+
+    @Override
+    public MerchantInventoryViewBuilder<V> merchant(final Merchant merchant) {
+        this.merchant = ((CraftMerchant) merchant).getMerchant();
+        return this;
+    }
+
+    @Override
+    public MerchantInventoryViewBuilder<V> checkReachable(final boolean checkReachable) {
+        super.checkReachable = checkReachable;
+        return this;
+    }
+
+    @Override
+    public V build(final HumanEntity player) {
+        Preconditions.checkArgument(player != null, "The given player must not be null");
+        Preconditions.checkArgument(this.title != null, "The given title must not be null");
+        Preconditions.checkArgument(player instanceof CraftHumanEntity, "The given player must be a CraftHumanEntity");
+        final CraftHumanEntity craftHuman = (CraftHumanEntity) player;
+        Preconditions.checkArgument(craftHuman.getHandle() instanceof ServerPlayer, "The given player must be an EntityPlayer");
+        final ServerPlayer serverPlayer = (ServerPlayer) craftHuman.getHandle();
+
+        final MerchantMenu container;
+        if (this.merchant == null) {
+            container = new MerchantMenu(serverPlayer.nextContainerCounter(), serverPlayer.getInventory(), new CraftMerchantCustom(title).getMerchant());
+        } else {
+            container = new MerchantMenu(serverPlayer.nextContainerCounter(), serverPlayer.getInventory(), this.merchant);
+        }
+
+        container.checkReachable = super.checkReachable;
+        container.setTitle(PaperAdventure.asVanilla(this.title));
+        return (V) container.getBukkitView();
+    }
+
+    @Override
+    protected AbstractContainerMenu buildContainer(final ServerPlayer player) {
+        throw new UnsupportedOperationException("buildContainer is not supported for CraftMerchantInventoryViewBuilder");
+    }
+
+    @Override
+    public MerchantInventoryViewBuilder<V> copy() {
+        final CraftMerchantInventoryViewBuilder<V> copy = new CraftMerchantInventoryViewBuilder<>(super.handle);
+        copy.checkReachable = super.checkReachable;
+        copy.merchant = this.merchant;
+        copy.title = title;
+        return copy;
+    }
+}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftStandardInventoryViewBuilder.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftStandardInventoryViewBuilder.java
new file mode 100644
index 0000000000..e528facbe0
--- /dev/null
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftStandardInventoryViewBuilder.java
@@ -0,0 +1,26 @@
+package org.bukkit.craftbukkit.inventory.view.builder;
+
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.inventory.AbstractContainerMenu;
+import net.minecraft.world.inventory.MenuType;
+import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.view.builder.InventoryViewBuilder;
+
+public class CraftStandardInventoryViewBuilder<V extends InventoryView> extends CraftAbstractInventoryViewBuilder<V> {
+
+    public CraftStandardInventoryViewBuilder(final MenuType<?> handle) {
+        super(handle);
+    }
+
+    @Override
+    protected AbstractContainerMenu buildContainer(final ServerPlayer player) {
+        return super.handle.create(player.nextContainerCounter(), player.getInventory());
+    }
+
+    @Override
+    public InventoryViewBuilder<V> copy() {
+        final CraftStandardInventoryViewBuilder<V> copy = new CraftStandardInventoryViewBuilder<>(handle);
+        copy.title = this.title;
+        return copy;
+    }
+}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/package-info.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/package-info.java
new file mode 100644
index 0000000000..157ce9fd75
--- /dev/null
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/package-info.java
@@ -0,0 +1,4 @@
+@NullMarked
+package org.bukkit.craftbukkit.inventory.view.builder;
+
+import org.jspecify.annotations.NullMarked;