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>
14 KiB
04 — Server List Ping (SLP)
How the multiplayer server list fetches a server's MOTD, version, and player count.
Covers modern SLP (1.7+) and the three legacy 0xFE variants.
1. Modern SLP (1.7+)
1.1 Packet sequence
sequenceDiagram
participant C as Client
participant S as Server
C->>S: TCP connect (port 25565)
C->>S: Handshake (0x00, nextState=1)
C->>S: Status Request (0x00, no fields)
S-->>C: Status Response (0x00, JSON string)
C->>S: Ping Request (0x01, Long timestamp)
S-->>C: Pong Response (0x01, echo Long)
S->>C: (server closes connection)
The client may close after receiving the Status Response without sending Ping (e.g. when only scraping the MOTD). Notchian servers will wait up to ~30 s for the Ping Request before timing out if the client leaves the connection open.
1.2 Packet definitions
All packets use the standard Minecraft packet framing (VarInt length prefix + VarInt packet ID + fields).
Handshaking state → Status
| Direction | ID | Name | Fields |
|---|---|---|---|
| C→S | 0x00 | Handshake | protocolVersion VarInt, serverAddress String(255), serverPort Unsigned Short, nextState VarInt (1=Status) |
Source: minecraft.wiki/w/Java_Edition_protocol/Packets
The wiki uses Intent as the field name (as of 1.20.5+); older docs call it
nextState. The value 1 always means "go to Status state."
Status state
| Direction | ID | Name | Fields |
|---|---|---|---|
| C→S | 0x00 | Status Request | (none) |
| S→C | 0x00 | Status Response | jsonResponse String(32767) |
| C→S | 0x01 | Ping Request | timestamp Long |
| S→C | 0x01 | Pong Response | timestamp Long (echoed) |
Source: minecraft.wiki/w/Java_Edition_protocol/Packets
The Ping payload is a Long (8 bytes, signed, big-endian) — typically a
millisecond epoch timestamp the client uses to measure round-trip latency.
node-minecraft-protocol sends [0, 0] (two 32-bit zero words that compose
one 64-bit zero) and measures wall-clock delta separately
(src/ping.js:55, src/server/ping.js:62).
1.3 The Status Response JSON
The server writes the entire status as a single JSON string inside the Status Response packet.
Annotated example
{
"version": {
"name": "1.21.8", // free string — shown in client UI on version mismatch
"protocol": 772 // actual protocol number client compares
},
"players": {
"max": 20, // max player slots (can be overridden freely)
"online": 1, // current player count
"sample": [ // optional list shown in hover tooltip
{
"name": "thinkofdeath",
"id": "4566e69f-c907-48ee-8d71-d7ba5aa00d20" // must be well-formed UUID
}
]
},
"description": { // the MOTD — see §1.4 for evolution
"text": "Hello, world!"
},
"favicon": "data:image/png;base64,<data>", // 64×64 PNG, no newlines (1.13+)
"enforcesSecureChat": false // added in 1.19.1
}
Source: minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping — example JSON
node-minecraft-protocol server-side builds this object at
src/server/ping.js:31-39:
const response = {
version: responseVersion,
players: { max: server.maxPlayers, online: server.playerCount, sample: [] },
description: server.motdMsg ?? { text: server.motd },
favicon: server.favicon
}
Field reference
| Field | Type | Required | Notes |
|---|---|---|---|
version.name |
String | No (1.20+: omission shows "Old") | Display label — not version-checked by client |
version.protocol |
Integer | No | Client compares against its own; mismatch → red/orange label |
players.max |
Integer | No | ??? shown if players omitted; max value 2³¹−1 |
players.online |
Integer | No | Same |
players.sample |
Array | No | Objects with name (String) + id (UUID String) |
description |
Text Component | No | See §1.4 |
favicon |
String | No | data:image/png;base64,…; exactly 64×64 px; no newlines since 1.13 |
enforcesSecureChat |
Boolean | No | Added 1.19.1 — client shows warning if server value differs from expectation |
previewsChat |
Boolean | No | Added alongside enforcesSecureChat; deprecated/removed in later 1.19.x releases |
Sources: minecraft.wiki SLP page;
enforcesSecureChat version from wiki field table.
1.4 Description field — chat component evolution
The description field has gone through three forms:
| Era | Format | Example |
|---|---|---|
| 1.7–1.8 | Plain string with § codes |
"§aA §bcoloured §cMOTD" |
| 1.9–1.15 | JSON Text Component object, but vanilla still embeds § codes inside {"text":"…"} |
{"text":"§aHello"} |
| 1.16+ | Full structured Text Component (Spigot/Paper); vanilla still uses §-embedded strings |
{"text":"Hello","color":"green"} |
The wiki notes: "Notchian servers embed section-sign-based codes within the text value, while third-party servers such as Spigot and Paper will return full components." Both forms are valid — clients accept either.
node-minecraft-protocol's server prefers a structured object (server.motdMsg)
but falls back to { text: server.motd } with a plain string
(src/server/ping.js:38).
1.5 Version spoofing
version.name is a free string — the server can write anything here.
version.protocol is the number the client actually compares against its own.
Common patterns:
| Scenario | version.name |
version.protocol |
|---|---|---|
| Matching client | "1.21.8" |
772 |
| Outdated client (server newer) | "1.21.8" |
772 — client shows "Outdated client!" |
| Outdated server (client newer) | "1.20.4" |
765 — client shows "Outdated server!" |
| ViaVersion (accepts many) | "Requires 1.21+ (1.7-1.20.6 supported)" |
client's own protocol number |
| Discovery ping (unknown client ver) | "Paper 1.21" |
-1 (sentinel) |
ViaVersion sets version.protocol to the connecting client's protocol number
so the client always sees a match, then handles translation internally.
The wiki notes that for 1.6 legacy responses from a 1.7+ server, the protocol
version in the response is always 127 (an incompatibility sentinel).
2. Legacy 0xFE Server List Ping
Pre-1.7 clients used a simple kick-packet hack. There are three variants, distinguished by how much data the client sends.
2.1 Overview
| Client version | Client sends | Response separator |
|---|---|---|
| Beta 1.8 – 1.3.2 | FE |
§ (section sign, U+00A7) |
| 1.4 – 1.5.2 | FE 01 |
\0 (null, U+0000) |
| 1.6.x | FE 01 FA … (plugin message) |
\0 (null) |
All responses are sent as a kick packet (0xFF) containing a UTF-16BE
encoded string.
2.2 Variant A — Beta 1.8 through 1.3.2
Client → Server: single byte 0xFE
Server → Client (kick packet):
FF packet ID
XX XX UInt16BE: number of UTF-16 code units in the response string
[UTF-16BE data]
Response string format (fields joined with §):
<MOTD>§<online>§<max>
Example: A Minecraft Server§5§20
node-minecraft-protocol handles this as payload === undefined (neither 0 nor
1), falling into the else branch at src/server/ping.js:73:
sendPingResponse([server.motd, server.playerCount.toString(),
server.maxPlayers.toString()].join('\xa7'))
(\xa7 = §)
2.3 Variant B — 1.4.2 through 1.5.2
Client → Server: two bytes FE 01
Server → Client: same kick packet format, but fields joined with null
(�), and a §1 prefix to signal the extended format:
§1<NUL><protocol><NUL><version><NUL><MOTD><NUL><online><NUL><max>
Where <NUL> is U+0000 (null character).
node-minecraft-protocol handles this at src/server/ping.js:68-71
(packet.payload === 1):
sendPingResponse('\xa7' + [pingVersion, server.mcversion.version,
server.mcversion.minecraftVersion, server.motd,
server.playerCount.toString(), server.maxPlayers.toString()].join('\0'))
Note: pingVersion is the hardcoded constant 1 (the ping format version,
not the Minecraft protocol version).
2.4 Variant C — 1.6.x
Client → Server (full hex):
FE packet ID
01 payload
FA plugin message packet ID
00 0B string length: 11 (UTF-16BE code units)
00 4D 00 43 00 7C "MC|PingHost" in UTF-16BE
00 50 00 69 00 6E
00 67 00 48 00 6F
00 73 00 74
XX XX rest-of-data byte length = 7 + len(hostname bytes)
XX client's protocol version (single byte)
XX XX hostname length in UTF-16BE code units
[hostname in UTF-16BE]
XX XX XX XX server port (big-endian int32)
The wiki notes: "All Notchian servers only care about the first 3 bytes
(FE 01 FA)." The hostname/port data is informational and typically ignored.
Server → Client — same kick packet, same null-delimited format as variant B:
FF
XX XX UInt16BE length
00 A7 00 31 00 00 U+00A7 "§" + U+0031 "1" + U+0000 null separator
[UTF-16BE: protocol<NUL>version<NUL>MOTD<NUL>online<NUL>max]
For a 1.7+ server responding to a 1.6 client, protocol is always 127
(incompatibility sentinel). Source: minecraft.wiki SLP — Legacy ping
2.5 Response wire encoding
All legacy responses share the same packet structure
(src/server/ping.js:77-93):
function sendPingResponse (responseString) {
function utf16be (s) {
return endianToggle(Buffer.from(s, 'utf16le'), 16) // swap to big-endian
}
const responseBuffer = utf16be(responseString)
const length = responseString.length // char count, not byte count
const lengthBuffer = Buffer.alloc(2)
lengthBuffer.writeUInt16BE(length)
const raw = Buffer.concat([Buffer.from('ff', 'hex'), lengthBuffer, responseBuffer])
client.socket.write(raw) // bypasses packet framer
}
The response is written directly to the socket (not through the packet framer) because the length field counts UTF-16 code units, not bytes, and the normal framer would prepend a VarInt length which legacy clients don't expect.
2.6 Detection on the server
A 1.7+ server recognises a legacy ping by the 0xFE first byte, which is not a
valid VarInt-prefixed packet. node-minecraft-protocol maps this to the
legacy_server_list_ping event and distinguishes variants by packet.payload:
packet.payload |
Variant |
|---|---|
undefined |
Beta 1.8–1.3.2 (FE only) |
1 |
1.4–1.5.2 (FE 01) or 1.6.x (FE 01 FA …) |
Source: src/server/ping.js:67-93
3. Protocol number reference
A selection of protocol numbers for version context:
| Release | Protocol |
|---|---|
| 1.7.10 | 5 |
| 1.8.9 | 47 |
| 1.12.2 | 340 |
| 1.16.5 | 754 |
| 1.19.1 | 760 |
| 1.20.4 | 765 |
| 1.21.1 | 767 |
| 1.21.8 | 772 (example from wiki) |
Current protocol: 775 (as of the Packets page at time of writing). Source: minecraft.wiki/w/Java_Edition_protocol/Packets
4. Sources
| Source | Used for |
|---|---|
| minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping | JSON field table, legacy ping bytes, response formats, favicon 1.13 note, §1 sentinel |
| minecraft.wiki/w/Java_Edition_protocol/Packets | Packet IDs, field types, Handshake Intent field name |
node-minecraft-protocol src/ping.js |
Client-side modern SLP flow (lines 41-68) |
node-minecraft-protocol src/server/ping.js |
Server-side response construction (lines 1-94) |
5. VERIFY flags (open questions)
<!-- VERIFY -->enforcesSecureChatexact version — wiki does not specify version inline; attributed to 1.19.1 based on the 1.19.1 secure-chat feature rollout. Confirm againstwiki/w/Java_Edition_1.19.1.<!-- VERIFY -->previewsChatadd/remove versions — present in 1.19.1 initial rollout, removed or no-op'd by 1.19.3. Confirm.<!-- VERIFY -->nextState→Intentfield rename version — wiki Packets page uses "Intent" as of current; confirm when the rename happened (cosmetic doc change, wire format unchanged).<!-- VERIFY -->ViaVersion protocol echo vs −1 — confirm ViaVersion echoes the connecting client's protocol number (rather than using a fixed sentinel).<!-- VERIFY -->Protocol 772 vs 775 — wiki SLP example JSON shows"protocol": 772but Packets page header shows current 775; resolve to exact MC versions.<!-- VERIFY -->Description plain-string support in 1.7 — confirm that"description": "plain string"(no wrapping object) was accepted by the official client in 1.7.x.