Server internals (NMS) with paperweight
When the API is not enough: what NMS is, the risks, and the paperweight setup.
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.
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:overworldwas calledResourceLocationin Minecraft 1.21 and is calledIdentifierin 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.
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:
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}- The paperweight userdev Gradle plugin. This is the version that matches Paper 26.3.
- Replaces the usual
compileOnly("io.papermc.paper:paper-api:...")line. The dev bundle already contains the API, so you do not list it twice. - Paper 26.3 needs Java 25, the same as in every other project in this guide.
1group=com.example2version=1.0.03minecraftVersion=26.34paperVersion=26.3.build.142-beta5org.gradle.jvmargs=-Xmx2G -Dfile.encoding=UTF-8- 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 type | Craft class to cast to | getHandle() gives you |
|---|---|---|
Player | CraftPlayer | ServerPlayer |
Entity | CraftEntity | net.minecraft.world.entity.Entity |
World | CraftWorld | ServerLevel |
Server | CraftServer (getServer()) | MinecraftServer |
ItemStack | CraftItemStack (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:
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}- 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.
- Nobody should create a bridge object. Everything in it is a
statichelper you call by class name. - Gets Mojang's player.
ServerPlayeris the type of the handle. - 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. - 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. connectionis the player's network link.latency()is the measured delay in milliseconds.- Casts the API's server to
CraftServerand asks for Mojang'sMinecraftServer.getTickCount()counts ticks since the server started. - 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. - Sends the packet straight to this player, skipping every safety check of the API. The API method
player.sendActionBarwould do the same thing in one safe line, so use that in real plugins. - Mojang has its own
Componentclass for formatted text. It is a different class from Adventure'sComponentthat the rest of this guide uses.
The command that uses the bridge only deals with the record, so it contains no NMS at all:
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}- The only line where NMS data enters. From here on it is a normal record.
- 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.
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}- The Minecraft version the server runs, such as
26.3. - Compares the two texts. Putting the constant first means the check never fails with a
NullPointerException. - Switches the plugin off cleanly, so none of its commands exist and none of its NMS code ever runs.
- Stops
onEnableright 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
- com/example/nmsinternalsdemo/
- resources/
- plugin.ymlNormal plugin.yml, nothing special for NMS
- java/
- main/
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:
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
- I searched the Paper API, the Javadocs and this guide, and there is no way to do it.
- I checked whether a library such as PacketEvents or ProtocolLib already does it.
- I know which Minecraft version my plugin supports, and I accept that it only supports that one.
- All NMS code lives in one or two small classes. Everything else uses plain Java types and the API.
- My plugin refuses to start on an unsupported Minecraft version.
- I wrote down what I will test after every Minecraft update.
- 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,reobfJaror Spigot mappings describe code from long ago. None of it compiles against 26.3. - Forgetting the dev bundle. With
compileOnly(paper-api)instead ofpaperweight.paperDevBundle, the NMS imports stay red.
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.
- Show a player the text "Low health!" above their hotbar.
- Show a player a glass block where there is really air, without changing the world for anyone else.
- Read how many milliseconds of lag a player has.
- Make a zombie always walk toward the nearest iron golem.
- Add a fake player (an NPC) that stands at spawn and only exists as network messages.
Hint
Player or Mob first. If a guess sounds natural, check the Javadocs.Show the solution
- API.
player.sendActionBar(Component). - API.
player.sendBlockChange(location, blockData)shows a block change to one player only. - API.
player.getPing(). The demo readslatency()through NMS, but that was only for practice. - API. Paper has a mob goal API, reached with
Bukkit.getMobGoals(), for changing how mobs choose what to do. - 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
What does
((CraftPlayer) player).getHandle()return?Every API object wraps a real Minecraft object, its handle. For a player that is aServerPlayer. Wrapping is why the cast toCraftPlayeris needed: only the Craft class hasgetHandle().Why is NMS risky to use?
NMS is not slower, and it has nothing to do with permissions. The danger is that class and method names change between versions, so code that worked last month stops compiling.Which Gradle line gives your project access to
net.minecraftclasses?Thepaper-apijar holds only the public API, sonet.minecraftclasses are missing from it. The dev bundle includes the server classes.runServeronly starts a test server.You want to show a player a fake block that nobody else sees. What should you try first?
The API already offers a method for exactly this. Changing the real block would show it to everyone, and a hand-written packet is the hardest option for no gain.Why does the example plugin check
getMinecraftVersion()inonEnable?On a different version, internal code can fail with confusing errors at random moments. A clear message at startup, followed by a clean disable, is much easier to understand.
Next steps
- Learn why internals are touchy about threads in Threads, performance and lag.
- Revisit Registries, keys and data components: many things people once did with NMS, such as custom enchantments, now have real API.
- See the Paper userdev documentation for the official setup guide.