Query

Query is a UDP protocol introduced in beta 1.9pre4 for querying server properties. It's meant to be compatible with the UT3 (or GameSpot) Query Protocol

A slightly simpler alternative to the query protocol involves connecting to the main Minecraft TCP port and sending a Server List Ping packet, which returns MOTD, number of users and number of slots. More details on the main protocol page.

All data types are big-endian, with the exception of

Server Config
enable-query=true query.port=<1-65535>

The default port is the one established in.

Generating a Session ID
The session ID is used identify your requests. The following examples use session ID = 1 (encoded as  on the hex dumps).

Only the lower 4-bits on each byte of the session ID should be used as Minecraft does not process the higher 4-bits on each byte. To convert any 4-byte session ID to a valid Minecraft session ID, simply mask the bits with.

Request
Send a request with an empty payload.

Dump:

FE FD 09 00 00 00 01

Response
The response payload will be a challenge token encoded as a null-terminated string. You should convert it to an int32 and store it.

In this example, after parsing the string "9513307\0" as an integer and packing it as a big-endian int32, the result should be 00 91 29 5B

Dump:

09 00 00 00 01 39 35 31 33 33 30 37 00 | .....9513307.

Expiration of the challenge token
Quoting Dinnerbone's original post on the Minecraft query protocol:

Note that the challenge token is bound to your IP and port (as opposed to the [session ID]), and lasts up to 30 seconds. You read that right, it's up to; it's not "your token will expire after 30 seconds", it's "every token ever" is expired every 30 seconds. This means it's entirely possible that you may get a token and use it within the same second and have it expire.

Additionally:

You'll need to provide your challenge token, or you will not receive any reply. If you provide a token and it's wrong, you still won't receive a reply. With that in mind, if you're going to store your challenge token and use it later, then you may want to do some timeout on waiting for a reply, in case the server restarted and your token is no longer valid. It's impossible to identify between an offline server and a server that refused your challenge without any additional requests, so you'll want to try for another challenge token and if that fails then flag them as unavailable

Request
Your payload should be your challenge token, packed as an int32.

Dump:

FE FD 00 00 00 00 01 00 91 29 5B

Response
Dump:

00 00 00 00 01 41 20 4D 69 6E 65 63 72 61 66 74 | .....A Minecraft 20 53 65 72 76 65 72 00 53 4D 50 00 77 6F 72 6C | Server.SMP.worl 64 00 32 00 32 30 00 DD 63 31 32 37 2E 30 2E 30 | d.2.20.##127.0.0 2E 31 00                                       | .1.

Full stat
This method is cached every 5 seconds.

Request
The request is the same as in a basic stat, except the payload must be padded to 8 bytes. Sending 0x00 0x00 0x00 0x00 at the end works.

Dump:

FE FD 00 00 00 00 01 00 91 29 5B 00 00 00 00

Response
The response is in two parts. The first part is a list of null-terminated strings, representing (key1, value1, key2, value2 ...). The second part is another list of null-terminated strings, each representing a player.

A simple way to parse the payload is to ignore the first 11 bytes, and then split the response around the token. At the very end, there's an extra null byte.

Dump:

00 00 00 00 01 73 70 6C 69 74 6E 75 6D 00 80 00 | .....splitnum... 68 6F 73 74 6E 61 6D 65 00 41 20 4D 69 6E 65 63 | hostname.A minec 72 61 66 74 20 53 65 72 76 65 72 00 67 61 6D 65 | raft Server.game 74 79 70 65 00 53 4D 50 00 67 61 6D 65 5F 69 64 | type.SMP.game_id 00 4D 49 4E 45 43 52 41 46 54 00 76 65 72 73 69 | .MINECRAFT.versi 6F 6E 00 42 65 74 61 20 31 2E 39 20 50 72 65 72 | on.Beta 1.9 Prer 65 6C 65 61 73 65 20 34 00 70 6C 75 67 69 6E 73 | elease 4.plugins 00 00 6D 61 70 00 77 6F 72 6C 64 00 6E 75 6D 70 | ..map.world.nump 6C 61 79 65 72 73 00 32 00 6D 61 78 70 6C 61 79 | layers.2.maxplay 65 72 73 00 32 30 00 68 6F 73 74 70 6F 72 74 00 | ers.20.hostport. 32 35 35 36 35 00 68 6F 73 74 69 70 00 31 32 37 | 25565.hostip.127 2E 30 2E 30 2E 31 00 00 01 70 6C 61 79 65 72 5F | .0.0.1...player_ 00 00 62 61 72 6E 65 79 67 61 6C 65 00 56 69 76 | ..barneygale.Viv 61 6C 61 68 65 6C 76 69 67 00 00               | alahelvig..

Example implementations

 * https://github.com/barneygale/MCQuery (python)
 * https://github.com/kmpm/node-mcquery (node.js)
 * https://github.com/Dinnerbone/mcstatus (python)
 * https://github.com/A2PLab/minelib (scala)
 * https://github.com/ryanshawty/MCJQuery (java)
 * https://github.com/Erikvv/Minecraft-Query-cpp (c++)
 * https://github.com/winny-/mcstat (php)
 * https://github.com/coNQP/mcipc (python 3.6)