Files
minecraft_protocol/00-overview.md
T
claude-timemachine d73c1c9537 minecraft_protocol: foundation + per-version protocol docs 1.7.10->26.2
8 topical docs (overview, data types, lifecycle, handshake, status/ping,
login+encryption, configuration, version-differences) + proxy-forwarding set
+ 16 per-version release-line docs, sourced from minecraft.wiki, ViaVersion
(source + commits), minecraft-data, node-minecraft-protocol, Velocity, BungeeCord.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 14:15:32 +02:00

11 KiB
Raw Blame History

Minecraft Java Edition Protocol — Overview

Scope: Java Edition only. Protocol details change with each release; version-specific differences are called out explicitly. "Latest" means 1.21.x as of mid-2026 unless noted.

Primary sources:

  • node-minecraft-protocol v1.66.2 (/tmp/mcproto-refs/node-minecraft-protocol/)
  • minecraft.wiki/w/Java_Edition_protocol (formerly wiki.vg) — Packet format / Data types

1. Transport

  • TCP, no UDP (voice-chat plugins add their own UDP layer out-of-band).
  • Default port: 25565 (configurable).
  • All data is big-endian except VarInt/VarLong, which use little-endian 7-bit groups (see 01-data-types.md).
  • Encryption is applied after the Login handshake (online-mode servers). The cipher is AES-128-CFB8, using the shared secret as both key and IV. Encryption wraps the raw TCP byte stream; packet framing runs on top of the encrypted layer.
    • Source: node-minecraft-protocol/src/transforms/encryption.js:6-9 (aes-128-cfb8, key=IV=shared secret).

2. Packet Framing

Every packet on the wire is length-prefixed. There are two modes: uncompressed (before the server sends Set Compression) and compressed (after).

2.1 Uncompressed packet format

Field Type Notes
Length VarInt Byte count of everything that follows (Packet ID + Data).
Packet ID VarInt State- and direction-scoped identifier.
Data bytes Payload; structure defined per packet.

The Framer prepends the length:

// node-minecraft-protocol/src/transforms/framing.js:16-21
const varIntSize = sizeOfVarInt(chunk.length)
const buffer = Buffer.alloc(varIntSize + chunk.length)
writeVarInt(chunk.length, buffer, 0)
chunk.copy(buffer, varIntSize)

The Splitter reads length, then slices exactly that many bytes — buffering partial reads across TCP segments (framing.js:48-73).

2.2 Compressed packet format

Enabled by the serverbound Set Compression packet (Login state). After that, every subsequent packet uses:

Field Type Notes
Packet Length VarInt Byte count of Data Length field + compressed payload.
Data Length VarInt Uncompressed size of (Packet ID + Data). 0 = not compressed.
Packet ID VarInt } zlib-deflated together when Data Length > 0
Data bytes }

Threshold rule: if len(Packet ID + Data) >= compressionThreshold, compress and set Data Length to the uncompressed size. Otherwise send uncompressed with Data Length = 0.

// node-minecraft-protocol/src/transforms/compression.js:22-40
if (chunk.length >= this.compressionThreshold) {
  const newChunk = zlib.deflateSync(chunk)
  // prepend VarInt(chunk.length), then compressed bytes
} else {
  // prepend VarInt(0), then raw bytes
}

Historical note: Compression was added in 1.8 (protocol 47). Per server/login.js:179, node-minecraft-protocol gates compression on protocolVersion >= 27 (snapshot 14w28a), which predates the 1.8 release — whole-protocol compression replaced earlier per-packet compression in that snapshot.

2.3 Packet size limits

  • Uncompressed maximum: 2²¹ 1 = 2,097,151 bytes (the Length VarInt may not exceed 3 bytes on the wire). — minecraft.wiki
  • Serverbound compressed: uncompressed (Packet ID + Data) must be ≤ 2²³ bytes (8,388,608).
  • Plugin message unrecognised-channel data: vanilla client caps at 1,048,576 bytes.

3. Connection State Machine

The protocol is divided into states. Each state has its own independent set of packet IDs. Transitions are triggered by specific packets.

// node-minecraft-protocol/src/states.js:3-8
const states = {
  HANDSHAKING:   'handshaking',
  STATUS:        'status',
  LOGIN:         'login',
  CONFIGURATION: 'configuration',   // added in 1.20.2 (protocol 764)
  PLAY:          'play'
}

State diagram

stateDiagram-v2
    [*] --> Handshaking : "TCP connect"

    Handshaking --> Status       : "Handshake (nextState=1)"
    Handshaking --> Login        : "Handshake (nextState=2)"
    Handshaking --> Login        : "Handshake (nextState=3, Transfer 1.20.5+)"

    Status --> [*] : "Status Response + Ping/Pong"

    Login --> Configuration      : "Login Success → Login Acknowledged (1.20.2+)"
    Login --> Play               : "Login Success (pre-1.20.2)"

    Configuration --> Play       : "Finish Configuration (both sides)"
    Play --> Configuration       : "Start Configuration (server-initiated, 1.20.2+)"
    Play --> [*]                 : "Disconnect"

3.1 Handshaking

Initial state on connect. Client sends exactly one packet:

  • Handshake (0x00, serverbound): carries protocolVersion, serverHost, serverPort, and nextState (1 = Status, 2 = Login, 3 = Transfer).

After this packet the server transitions immediately to the indicated next state.

// node-minecraft-protocol/src/server/handshake.js:34-38
if (packet.nextState === 1) {
  client.state = states.STATUS
} else if (packet.nextState === 2) {
  client.state = states.LOGIN
}

nextState = 3 (Transfer) was added in 1.20.5 for server-transfer support.

3.2 Status

Server-list ping. Two round-trips:

  1. Client → Status Request (0x00)
  2. Server → Status Response (0x00) — JSON payload with MOTD, player count, favicon
  3. Client → Ping Request (0x01) — 8-byte timestamp
  4. Server → Pong Response (0x01) — echoes timestamp

Connection is then closed by the client. The server does not transition to another state.

3.3 Login

Authentication and encryption negotiation.

Typical online-mode sequence:

  1. C→S: Login Start (username, UUID, optional profile-key signature)
  2. S→C: Encryption Request (server ID, RSA public key, verify token)
  3. C→S: Encryption Response (RSA-encrypted shared secret + verify token)
  4. (Both sides enable AES-128-CFB8 encryption)
  5. Server calls Mojang session-server to verify join
  6. S→C: Set Compression (threshold; gates compressed mode) — added in 1.8 / snapshot 14w28a
  7. S→C: Login Success (UUID, username, properties)
  8. C→S: Login Acknowledged — triggers transition to Configuration (1.20.2+)
// node-minecraft-protocol/src/server/login.js:179
if (client.protocolVersion >= 27) {
  client.write('compress', { threshold: 256 })
  client.compressionThreshold = 256
}
// node-minecraft-protocol/src/server/login.js:189-193
if (client.supportFeature('hasConfigurationState')) {
  client.once('login_acknowledged', onClientLoginAck)
} else {
  client.state = states.PLAY
}

3.4 Configuration (1.20.2+, protocol 764+)

Introduced in 1.20.2. Entered after Login Acknowledged; used for:

  • Registry codec data (dimension types, biomes, etc.)
  • Feature flags
  • Known packs negotiation
  • Server brand plugin channel

Both client and server exchange Finish Configuration to exit this state.

// node-minecraft-protocol/src/server/login.js:224-238 (onClientLoginAck)
client.state = states.CONFIGURATION
// ... send registry_data, then finish_configuration
client.once('finish_configuration', () => {
  client.state = states.PLAY
})

The server can re-enter Configuration from Play at any time by sending Start Configuration (serverbound: Configuration Acknowledged from client acknowledges the switch back).

// node-minecraft-protocol/src/client/play.js:43-44
client.on('start_configuration', () => enterConfigState())
// ...
if (client.state === states.PLAY) {
  client.write('configuration_acknowledged', {})
}

3.5 Play

Normal gameplay state. The bulk of all packets are Play-state packets. This state persists until disconnect or a server-initiated re-entry into Configuration.


4. Packet ID Scoping

Packet IDs are not globally unique. They are scoped by both state and direction:

  • State: Handshaking / Status / Login / Configuration / Play
  • Direction: serverbound (client → server) or clientbound (server → client)

A packet ID of 0x00 in Handshaking-serverbound (Handshake packet) is completely unrelated to 0x00 in Status-clientbound (Status Response), and so on. You need all three pieces — state, direction, ID — to unambiguously identify a packet.

The serializer key in node-minecraft-protocol reflects this:

// node-minecraft-protocol/src/transforms/serializer.js:47-51
function createSerializer ({ state = states.HANDSHAKING, isServer = false, version, ... }) {
  return new Serializer(
    createProtocol(state, !isServer ? 'toServer' : 'toClient', version, ...),
    'packet'
  )
}

Concrete packet ID assignments live in minecraft-data (a separate package) and change between protocol versions. Always consult the version-specific protocol data, not hard-coded constants.


5. Encryption Layer Position

The pipeline (innermost to outermost on send) is:

Packet (ID + payload)
  → Compression (optional, zlib deflate)
  → Framing (VarInt length prefix)
  → Encryption (AES-128-CFB8, wraps the byte stream after framing)
  → TCP

Framing runs before encryption so the receiver can split frames while decrypting. Both directions use the same shared secret but independent cipher/decipher instances.

Source: node-minecraft-protocol/src/transforms/encryption.js — AES-128-CFB8 with key = IV = sharedSecret.


6. Legacy Server List Ping

Clients predating the current framing (pre-1.7) send a 0xFE byte as the first byte of the TCP stream. node-minecraft-protocol detects this and shims it into the modern VarInt-framed pipeline:

// node-minecraft-protocol/src/transforms/framing.js:36-43
if (this.recognizeLegacyPing && this.buffer[0] === LEGACY_PING_PACKET_ID) {
  // Prefix a VarInt-encoded packet ID so the deserializer can handle it
}

This is a compatibility shim only; the modern protocol framing is always VarInt-prefixed as described in §2.


See 01-data-types.md for wire type encodings (VarInt, Position, NBT, etc.).