MindPy includes a complete, asyncio-native Minecraft Java Edition protocol implementation — no any any other external MC protocol library is required.
This guide covers:
- How the protocol layer works
- Supported packet types per version
- Writing custom packet handlers
- Adding support for a new version
- Using the low-level connection directly
mindpy.protocol/
├── types.py VarInt, VarLong, PacketBuffer, UUID, Position helpers
├── states.py ConnectionState enum
├── packets.py Packet ABC, PacketDirection, PacketRegistry
├── connection.py MinecraftConnection — TCP, encryption, compression, read loop
├── encryption.py AES-128-CFB8 via `cryptography`
├── compression.py zlib compress/decompress helpers
├── auth.py Microsoft OAuth2 → Xbox → Minecraft auth
├── codec.py ProtocolRegistry — version-to-registry mapping
├── login.py LoginOrchestrator — HANDSHAKING → LOGIN → PLAY
└── versions/
├── __init__.py auto-loads all version modules
├── base.py shared packets (Handshake, Status, Ping)
├── v47.py 1.8 — all Login + core Play packets
├── v340.py 1.12.2 additions/overrides
├── v754.py 1.16.5 additions/overrides
├── v762.py 1.19.4 additions/overrides
├── v765.py 1.20.4 additions/overrides
└── v769.py 1.21+ additions/overrides
TCP connect
│
▼
HANDSHAKING ──next_state=1──→ STATUS (ping → pong → disconnect)
│
└──next_state=2──→ LOGIN
│
(1.20.2+) CONFIGURATION
│
└──────────→ PLAY
The LoginOrchestrator drives this automatically. In PLAY state, the MinecraftConnection._read_loop runs in the background, calling registered packet handlers as packets arrive.
Uncompressed:
[VarInt length] [VarInt packet_id] [... fields ...]
Compressed (after Set Compression):
[VarInt frame_length]
[VarInt data_length] ← uncompressed size; 0 = not compressed
[... data ...] ← zlib-compressed if data_length > 0
Encrypted (after EncryptionResponse):
AES-128-CFB8 wraps the entire wire bytes.
Key = IV = 16-byte shared_secret negotiated during login.
| Protocol | Minecraft | Key differences from previous |
|---|---|---|
| 47 | 1.8 | VarInt keepalive; string UUID in LoginSuccess |
| 340 | 1.12.2 | int64 keepalive; ClientSettings (0x04); some ID shifts |
| 754 | 1.16.5 | UUID as 16 raw bytes; TeleportConfirm (0x00); more ID shifts |
| 762 | 1.19.4 | Signed chat; SystemChatMessage (0x60); keepalive 0x20/0x11 |
| 765 | 1.20.4 | Configuration state added; keepalive 0x24/0x15 |
| 769 | 1.21+ | keepalive 0x26/0x18; JoinGame 0x2B; serves as forward-compat fallback |
Versions not in this table automatically fall back to the closest older supported version (e.g. protocol 766 → 765, protocol 770 → 769).
Use connection.on_packet() or connection.add_handler():
import asyncio
from mindpy import Bot
from mindpy.protocol.versions.v769 import (
KeepAliveClientboundPacket,
KeepAliveServerboundPacket,
JoinGamePacket,
)
bot = Bot("localhost", protocol_version=769)
@bot.on("bot.connected")
async def on_connected(event) -> None:
conn = bot._connection
# Handler decorator
@conn.on_packet(JoinGamePacket)
async def handle_join(pkt: JoinGamePacket) -> None:
print(f"Entity ID: {pkt.entity_id}, Game Mode: {pkt.game_mode}")
# Programmatic registration
async def auto_keepalive(pkt: KeepAliveClientboundPacket) -> None:
await conn.send_packet(KeepAliveServerboundPacket(
keep_alive_id=pkt.keep_alive_id
))
conn.add_handler(KeepAliveClientboundPacket, auto_keepalive)
async with bot:
await bot.run()Note: The Bot class already registers KeepAlive, Position, Health, JoinGame, Chat, and Disconnect handlers automatically. You only need to add custom handlers for packets not covered by default.
from mindpy.protocol.versions.v769 import ChatMessageServerboundPacket
await conn.send_packet(ChatMessageServerboundPacket(message="Hello!"))Any Packet subclass can be sent. send_packet handles encryption and compression transparently.
from __future__ import annotations
from dataclasses import dataclass
from mindpy.protocol.packets import Packet, PacketDirection
from mindpy.protocol.states import ConnectionState
from mindpy.protocol.types import PacketBuffer
@dataclass
class MyCustomPacket(Packet):
packet_id = 0x42
state = ConnectionState.PLAY
direction = PacketDirection.CLIENTBOUND
some_int: int = 0
some_str: str = ""
def encode(self, buf: PacketBuffer, protocol_version: int) -> None:
buf.write_varint(self.some_int)
buf.write_string(self.some_str)
@classmethod
def decode(cls, buf: PacketBuffer, protocol_version: int) -> "MyCustomPacket":
return cls(
some_int=buf.read_varint(),
some_str=buf.read_string(),
)Register it in the relevant version registry:
from mindpy.protocol.codec import ProtocolRegistry
codec = ProtocolRegistry.get(769)
codec.registry.register(MyCustomPacket)Create mindpy/protocol/versions/vXXX.py:
"""Minecraft 1.22 protocol (version 800)."""
from __future__ import annotations
from mindpy.protocol.codec import ProtocolRegistry
from mindpy.protocol.packets import PacketRegistry
from mindpy.protocol.versions.v769 import * # inherit all v769 packets
PROTOCOL_VERSION = 800
_registry = PacketRegistry()
# Re-register unchanged packets
from mindpy.protocol.versions.v769 import (
LoginStartPacket, LoginSuccessPacket, # etc.
)
for _cls in [...]:
_registry.register(_cls)
# Override changed packets
from dataclasses import dataclass
from mindpy.protocol.packets import Packet, PacketDirection
from mindpy.protocol.states import ConnectionState
from mindpy.protocol.types import PacketBuffer
@dataclass
class KeepAliveClientboundPacket(Packet):
packet_id = 0x28 # new ID in v800
state = ConnectionState.PLAY
direction = PacketDirection.CLIENTBOUND
keep_alive_id: int = 0
def encode(self, buf: PacketBuffer, protocol_version: int) -> None:
buf.write_long(self.keep_alive_id)
@classmethod
def decode(cls, buf: PacketBuffer, protocol_version: int) -> "KeepAliveClientboundPacket":
return cls(keep_alive_id=buf.read_long())
_registry.register(KeepAliveClientboundPacket)
ProtocolRegistry.register(PROTOCOL_VERSION, _registry)Then add the import to mindpy/protocol/versions/__init__.py:
from mindpy.protocol.versions import v800 # noqa: F401| Write | Read | Type |
|---|---|---|
write_varint(n) |
read_varint() |
Variable-length int32 |
write_varlong(n) |
read_varlong() |
Variable-length int64 |
write_string(s, max_len=32767) |
read_string(max_len=32767) |
UTF-8 with VarInt length prefix |
write_bool(b) |
read_bool() |
0x00 / 0x01 |
write_byte(n) |
read_byte() |
Signed int8 |
write_ubyte(n) |
read_ubyte() |
Unsigned int8 |
write_short(n) |
read_short() |
Signed int16 |
write_ushort(n) |
read_ushort() |
Unsigned int16 |
write_int(n) |
read_int() |
Signed int32 |
write_long(n) |
read_long() |
Signed int64 |
write_float(f) |
read_float() |
IEEE 754 float32 |
write_double(d) |
read_double() |
IEEE 754 float64 |
write_bytes(b) |
read_bytes(n) |
Raw bytes |
write_uuid(u) |
read_uuid() |
uuid.UUID ↔ 16 bytes |
No authentication — the username is sent as-is. The server must have online-mode=false.
from mindpy.protocol.auth import MicrosoftAuth, AuthProfile
async with MicrosoftAuth() as auth:
profile = await auth.device_flow_auth()
# Prints:
# Visit https://microsoft.com/link
# Enter code: XXXX-XXXX
# (15-minute window)
print(profile.username) # Minecraft username
print(profile.uuid) # UUID string (with hyphens)
print(profile.access_token) # Minecraft access token
bot = Bot("play.example.com", auth_profile=profile, online_mode=True)The LoginOrchestrator handles the full RSA key exchange when the server sends EncryptionRequest:
- Generates a random 16-byte
shared_secret - Computes Minecraft's SHA1 server hash:
sha1(server_id + shared_secret + public_key_der) - POSTs to
https://sessionserver.mojang.com/session/minecraft/join - RSA-encrypts
shared_secretandverify_tokenwith server's public key - Sends
EncryptionResponse - Enables AES-128-CFB8 on both sides simultaneously
import asyncio
from mindpy.protocol.connection import MinecraftConnection
from mindpy.protocol.login import LoginOrchestrator
from mindpy.protocol.codec import ProtocolRegistry
import mindpy.protocol.versions
async def ping(host: str, port: int = 25565) -> None:
codec = ProtocolRegistry.get(769)
conn = MinecraftConnection(host, port, 769, codec.registry)
await conn.connect()
orch = LoginOrchestrator(conn, username="")
info = await orch.ping()
print(f"Version: {info.version_name} (protocol {info.protocol_version})")
print(f"Players: {info.online_players}/{info.max_players}")
print(f"MOTD: {info.description}")
print(f"Latency: {info.latency_ms:.1f} ms")
await conn.close()
asyncio.run(ping("mc.hypixel.net"))