Paper Plugin Guide
File mode0

Going further

Server internals (NMS) with paperweight

When the API is not enough: what NMS is, the risks, and the paperweight setup.

Advanced19 min read

Sometimes an idea is not possible with the Paper API. This page explains what NMS is, why you should treat it as a last resort, how to set up a project that can reach Minecraft's own server code, and how to keep that risky code in one small, safe corner of your plugin.

What is NMS?

The Paper API is a set of friendly classes like Player, World and ItemStack. They were designed for plugin authors, and Paper promises to keep them working from one version to the next.

Behind those friendly classes is the real Minecraft server, written by Mojang. It has its own classes for everything: its own player class, its own world class, its own network messages. People call that code NMS, short for net.minecraft.server, the old name of the package it lived in. Today its classes sit in packages starting with net.minecraft, for example net.minecraft.server.level.ServerPlayer.

The two sides are joined by a layer called CraftBukkit. Its classes all start with Craft: CraftPlayer implements the API's Player and secretly holds Mojang's ServerPlayer inside. Using NMS means reaching past the API, through the Craft layer, into Mojang's code.

The layers between your plugin and the game Your plugin talks to the Paper API, which is stable. Below it sits the CraftBukkit layer, which wraps the Minecraft server code. NMS means reaching past the API and the wrappers into the Minecraft server code, which changes with every version. Four layers, and how far down you reach Your plugin player.sendActionBar(...) Paper API (org.bukkit, io.papermc.paper) promises to keep working CraftBukkit layer (org.bukkit.craftbukkit) CraftPlayer wraps ServerPlayer Minecraft server code, "NMS" (net.minecraft) ServerPlayer, ServerLevel, packets Safe zone survives updates Danger zone names and methods can change in any Minecraft update The deeper you reach, the more you own when Minecraft updates.
Your plugin normally stays in the top two layers. NMS means reaching all the way down.

Why you should avoid it

  • It breaks on updates. Mojang does not care about plugins. Classes get renamed, methods move and arguments change, in almost every Minecraft version. For example, the class for names like minecraft:overworld was called ResourceLocation in Minecraft 1.21 and is called Identifier in 26.3. Code that used the old name simply stops compiling.
  • It is tied to one version. A plugin built against 26.3's internals only works on 26.3. The API, in contrast, stays compatible for a long time.
  • Mistakes crash the server. The API checks what you give it. Internal code often does not, so a wrong value can corrupt data or stop the whole server, with an error that points deep into Mojang's code.
  • Nobody can help you. Paper's documentation and community support the API. Internal code is "you are on your own".

Alternatives first

Before you reach for NMS, walk down this list. It will save you a lot of work.

Do you really need NMS? A decision path. First look for Paper API, then a library, and only then use NMS, keeping it in one small class. Ask these questions in order 1. Can the Paper API do it? search the docs and the Event Finder Use the API no 2. Does a library already do it? for example for packets or fake entities Use the library no 3. Is the value worth the upkeep? you fix it after every Minecraft update Skip the idea no yes 4. Use NMS, safely one small class, a version check at startup, and a test after every update
Most ideas end at step 1 or 2. Step 4 is for the few that really need it.

1. Look in the API. The Paper API grows with every release. Things that once needed NMS, such as sending action bar text, reading a player's ping, showing a fake block to one player or changing how mobs behave, now have normal methods. Search this guide, the Event Finder and the Paper Javadocs before you give up.

2. Use a library. For work with network packets, such as fake players (NPCs), hologram tricks or custom client messages, there are well-known libraries. Two popular ones are PacketEvents and ProtocolLib. They hide the version-specific code behind one stable interface and their authors fix it after each Minecraft update, so you do not have to. Always check that the version you pick supports Minecraft 26.3 before you build on it.

3. Ask if the idea is worth the upkeep. NMS code is code you must re-test and often rewrite after every Minecraft update. Is the feature worth that?

4. Only then use NMS. And do it carefully, as the rest of this page shows.

Setting up paperweight

Your normal Gradle project compiles against the paper-api jar. That jar contains only the public API, so the classes in net.minecraft do not exist for the compiler, and a line like import net.minecraft.server.level.ServerPlayer; turns red.

To compile against the real server classes you use paperweight, PaperMC's Gradle tooling, in its userdev form (for plugin developers). It downloads a dev bundle, a package with the API and Minecraft's server classes for exactly one version, and puts it on your compile classpath.

The project looks like a normal one. Only two lines of build.gradle.kts change:

build.gradle.kts
1plugins {2    java3    id("io.papermc.paperweight.userdev") version "2.0.0-beta.24"4    id("xyz.jpenilla.run-paper") version "3.1.0"5}6 7val minecraftVersion: String by project8val paperVersion: String by project9 10repositories {11    mavenCentral()12    maven("https://repo.papermc.io/repository/maven-public/")13}14 15dependencies {16    paperweight.paperDevBundle(paperVersion)17}18 19java {20    toolchain.languageVersion = JavaLanguageVersion.of(25)21}22 23tasks {24    compileJava {25        options.encoding = Charsets.UTF_8.name()26    }27 28    runServer {29        minecraftVersion(minecraftVersion)30        jvmArgs("-Xms2G", "-Xmx2G", "-Dcom.mojang.eula.agree=true")31    }32}
  1. The paperweight userdev Gradle plugin. This is the version that matches Paper 26.3.
  2. Replaces the usual compileOnly("io.papermc.paper:paper-api:...") line. The dev bundle already contains the API, so you do not list it twice.
  3. Paper 26.3 needs Java 25, the same as in every other project in this guide.
gradle.properties
1group=com.example2version=1.0.03minecraftVersion=26.34paperVersion=26.3.build.142-beta5org.gradle.jvmargs=-Xmx2G -Dfile.encoding=UTF-8
  1. The exact Paper build whose dev bundle Gradle downloads. Update it when you move to a newer build.

The first time Gradle syncs the project, it downloads and prepares the dev bundle. That can take several minutes and a lot of disk space, and later syncs are fast. Paperweight only works with Gradle, not with Maven.

After syncing, type net.minecraft. in any Java file and IntelliJ will suggest packages. You can Ctrl+click any class to read Mojang's code. Reading it is the only real documentation you will get.

The handle: from API object to Mojang's object

Every API object that stands for something in the game holds a real Mojang object inside, called its handle. To get it, you convert (cast) the API object to its Craft class and call getHandle().

API typeCraft class to cast togetHandle() gives you
PlayerCraftPlayerServerPlayer
EntityCraftEntitynet.minecraft.world.entity.Entity
WorldCraftWorldServerLevel
ServerCraftServer (getServer())MinecraftServer
ItemStackCraftItemStack (asNMSCopy)Mojang's ItemStack, as a copy

All the Craft classes live in org.bukkit.craftbukkit (and org.bukkit.craftbukkit.entity for entities). The cast is safe on Paper, because the player objects Paper creates are always CraftPlayer objects. It would fail only if some plugin made a fake Player.

Keep NMS in one small class

The best defense against updates is to confine NMS to one class and hand the rest of your plugin only normal Java values: text, numbers, API objects. When Minecraft updates and something breaks, you fix one file.

The example plugin does this. Its NmsBridge is the only class that imports net.minecraft and org.bukkit.craftbukkit:

NmsBridge.java
1package com.example.nmsinternalsdemo;2 3import net.minecraft.network.chat.Component;4import net.minecraft.network.protocol.game.ClientboundSetActionBarTextPacket;5import net.minecraft.server.level.ServerPlayer;6import org.bukkit.Bukkit;7import org.bukkit.craftbukkit.CraftServer;8import org.bukkit.craftbukkit.entity.CraftPlayer;9import org.bukkit.entity.Player;10 11final class NmsBridge {12 13    record PlayerInternals(String handleClass, String dimension, int foodLevel, int latencyMillis) {14    }15 16    private NmsBridge() {17    }18 19    static PlayerInternals inspect(Player player) {20        ServerPlayer handle = handleOf(player);21        return new PlayerInternals(22            handle.getClass().getName(),23            handle.level().dimension().identifier().toString(),24            handle.getFoodData().getFoodLevel(),25            handle.connection.latency());26    }27 28    static int serverTickCount() {29        return ((CraftServer) Bukkit.getServer()).getServer().getTickCount();30    }31 32    static void sendActionBarPacket(Player player, String text) {33        handleOf(player).connection.send(new ClientboundSetActionBarTextPacket(Component.literal(text)));34    }35 36    private static ServerPlayer handleOf(Player player) {37        return ((CraftPlayer) player).getHandle();38    }39}
  1. A small data holder with plain Java types only (text and whole numbers). The rest of the plugin sees this record and never sees a Mojang class.
  2. Nobody should create a bridge object. Everything in it is a static helper you call by class name.
  3. Gets Mojang's player. ServerPlayer is the type of the handle.
  4. Walks through Mojang's objects: the player's level (world), its dimension key, and the name of that key. The result is text like minecraft:overworld.
  5. Reads the hunger bar. The API can do this as well (player.getFoodLevel()). It is here to show how the handle works, not because you need NMS for it.
  6. connection is the player's network link. latency() is the measured delay in milliseconds.
  7. Casts the API's server to CraftServer and asks for Mojang's MinecraftServer. getTickCount() counts ticks since the server started.
  8. A packet: the exact network message that makes the action bar text appear. "Clientbound" means it goes from the server to the player's game.
  9. Sends the packet straight to this player, skipping every safety check of the API. The API method player.sendActionBar would do the same thing in one safe line, so use that in real plugins.
  10. Mojang has its own Component class for formatted text. It is a different class from Adventure's Component that the rest of this guide uses.

The command that uses the bridge only deals with the record, so it contains no NMS at all:

NmsPeekCommand.java
10@Override11public void execute(CommandSourceStack source, String[] args) {12    if (!(source.getExecutor() instanceof Player player)) {13        source.getSender().sendRichMessage("<red>Only players can use /nms-peek.");14        return;15    }16    NmsBridge.PlayerInternals internals = NmsBridge.inspect(player);17 18    player.sendRichMessage("<gold>Server internals for <yellow><name></yellow>:",19        Placeholder.unparsed("name", player.getName()));20    player.sendRichMessage("<gray>Handle class: <white><value>",21        Placeholder.unparsed("value", internals.handleClass()));22    player.sendRichMessage("<gray>Dimension: <white><value>",23        Placeholder.unparsed("value", internals.dimension()));24    player.sendRichMessage("<gray>Food level: <white><value>",25        Placeholder.unparsed("value", String.valueOf(internals.foodLevel())));26    player.sendRichMessage("<gray>Latency: <white><value> ms",27        Placeholder.unparsed("value", String.valueOf(internals.latencyMillis())));28    player.sendRichMessage("<gray>Server tick count: <white><value>",29        Placeholder.unparsed("value", String.valueOf(NmsBridge.serverTickCount())));30 31    NmsBridge.sendActionBarPacket(player, "Sent as a raw packet");32}
  1. The only line where NMS data enters. From here on it is a normal record.
  2. Plain text. This code compiles on any Paper version that has the bridge.

Check the version at startup

Because NMS code is written for exactly one Minecraft version, the main class should refuse to run on any other. A clear message at startup is far better than a confusing NoSuchMethodError an hour later.

NmsDemoPlugin.java
10@Override11public void onEnable() {12    String running = getServer().getMinecraftVersion();13    if (!SUPPORTED_MINECRAFT_VERSION.equals(running)) {14        getComponentLogger().error("This plugin uses server internals and was built for Minecraft {}, but this server runs {}. Disabling.",15            SUPPORTED_MINECRAFT_VERSION, running);16        getServer().getPluginManager().disablePlugin(this);17        return;18    }19    getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event ->20        event.registrar().register("nms-peek", "Shows a few values read from server internals",21            new NmsPeekCommand()));22}
  1. The Minecraft version the server runs, such as 26.3.
  2. Compares the two texts. Putting the constant first means the check never fails with a NullPointerException.
  3. Switches the plugin off cleanly, so none of its commands exist and none of its NMS code ever runs.
  4. Stops onEnable right here. Without it, the command would still be registered.
  • nms-internals-demo/
    • build.gradle.ktsUses the paperweight userdev plugin and the dev bundle
    • gradle.propertiesMinecraft and Paper version numbers
    • settings.gradle.ktsThe project name and plugin repositories
    • src/
      • main/
        • java/
          • com/example/nmsinternalsdemo/
            • NmsDemoPlugin.javaVersion check, then registers the command
            • NmsPeekCommand.javaShows values, contains no NMS
            • NmsBridge.javaThe only class that touches net.minecraft
        • resources/
          • plugin.ymlNormal plugin.yml, nothing special for NMS

Build it with gradlew build and run /nms-peek in the game. This plugin was compiled with the real 26.3 dev bundle and started on a real Paper 26.3 server, where it passed its version check and enabled. The command prints this, and shows "Sent as a raw packet" in your action bar:

Server internals for Steve:
Handle class: net.minecraft.server.level.ServerPlayer
Dimension: minecraft:overworld
Food level: 20
Latency: 12 ms
Server tick count: 48210

Your latency and tick count will differ, of course. The first three lines prove the handle works: the player you know as an API Player is really a net.minecraft.server.level.ServerPlayer underneath.

Checklist before you use NMS

  1. I searched the Paper API, the Javadocs and this guide, and there is no way to do it.
  2. I checked whether a library such as PacketEvents or ProtocolLib already does it.
  3. I know which Minecraft version my plugin supports, and I accept that it only supports that one.
  4. All NMS code lives in one or two small classes. Everything else uses plain Java types and the API.
  5. My plugin refuses to start on an unsupported Minecraft version.
  6. I wrote down what I will test after every Minecraft update.
  7. I tested on a copy of the server, never first on the live one.

When Minecraft updates

The routine is always the same. Raise the version numbers in gradle.properties, sync, and read the compiler errors. They point straight at the code that broke, and they all sit in your bridge class. Fix each one by reading Mojang's new code (Ctrl+click), then change the supported-version constant and test every feature by hand. This is the upkeep from step 3 of the decision path.

Mistakes to avoid

  • Using NMS when the API can do it. The demo reads the food level through NMS only to show how it works. In a real plugin, use player.getFoodLevel().
  • NMS code spread all over the plugin. When it breaks, every file breaks. Keep it in one class.
  • Calling NMS from another thread. Internals are even less forgiving about threads than the API. See Threads, performance and lag.
  • Following old tutorials. Posts with net.minecraft.server.v1_8_R3, reobfJar or Spigot mappings describe code from long ago. None of it compiles against 26.3.
  • Forgetting the dev bundle. With compileOnly(paper-api) instead of paperweight.paperDevBundle, the NMS imports stay red.
Try it

API, library or NMS?

For each idea, decide: can the plain Paper API do it, would you look for a library, or does it need NMS? Write down your answers before you open the solution.

  1. Show a player the text "Low health!" above their hotbar.
  2. Show a player a glass block where there is really air, without changing the world for anyone else.
  3. Read how many milliseconds of lag a player has.
  4. Make a zombie always walk toward the nearest iron golem.
  5. Add a fake player (an NPC) that stands at spawn and only exists as network messages.
Hint
For each one, try to guess a method name on Player or Mob first. If a guess sounds natural, check the Javadocs.
Show the solution
  1. API. player.sendActionBar(Component).
  2. API. player.sendBlockChange(location, blockData) shows a block change to one player only.
  3. API. player.getPing(). The demo reads latency() through NMS, but that was only for practice.
  4. API. Paper has a mob goal API, reached with Bukkit.getMobGoals(), for changing how mobs choose what to do.
  5. Library first. Fake players are made of packets, and the Paper API has no NPC feature. PacketEvents or ProtocolLib are the usual answer. Only if neither fits would you write the packets by hand with NMS.

Four out of five ideas never needed NMS. That is the normal ratio.

Recap

  • NMS is Mojang's own server code. The API wraps it, and the CraftBukkit layer (classes starting with Craft) connects the two.
  • NMS breaks on updates, ties you to one version and gives you no safety net. Use it last.
  • Check the API first, then a library such as PacketEvents or ProtocolLib.
  • paperweight userdev and paperweight.paperDevBundle(...) let Gradle compile against the server classes. Since 26.1 there is no remapping step.
  • Get a handle by casting to the Craft class and calling getHandle().
  • Keep all NMS in one class, check the Minecraft version at startup, and re-test after every update.

Quick quiz

  1. What does ((CraftPlayer) player).getHandle() return?

  2. Why is NMS risky to use?

  3. Which Gradle line gives your project access to net.minecraft classes?

  4. You want to show a player a fake block that nobody else sees. What should you try first?

  5. Why does the example plugin check getMinecraftVersion() in onEnable?

Next steps