---
title: Your first client
description: Connect to a server, ping it, or log a bot in.
---

`Client` opens a TCP connection and speaks the protocol from the client side. Unlike `Server`, it does **not** automate login for you: you send `Intension`, `Hello`, and the acknowledgements yourself. The state machine still switches automatically as those packets go out.

## Options

```ts
import { Client } from "mc-protocol";

const client = new Client({
  host: "localhost", // default
  port: 25565,       // default
  username: "bot",   // default; not sent automatically — include it in Hello
  version: 776,      // default; protocol number for the packet tables and Intension
});
```

The socket connects in the constructor. Wait for `Socket:Ready` before sending.

`version` must be a built-in protocol number (only `776` today; unknown values throw a `RangeError`). It selects the packet tables for both directions and fills `protocolVersion` in `Intension` when you leave it out. If you send `Intension` with a different `protocolVersion` that is also built in, the client switches to that version's tables. An unknown number is sent as is and the tables stay on `version`, which is enough for pinging a server of another version.

## Ping a server

The shortest useful client. Ask for status, print it, leave.

```ts
client.on("Socket:Ready", async () => {
  await client.sendPacket("Intension", {
    // protocolVersion defaults to the client's `version` option
    serverHost: "localhost",
    serverPort: 25565,
    nextState: 1, // status
  });
  await client.sendPacket("StatusRequest");
});

client.on("Status/StatusResponse", (data) => {
  const status = JSON.parse(data.jsonResponse);
  console.log(status.version.name, `${status.players.online}/${status.players.max}`);
  client.disconnect();
});
```

Every parsed packet is emitted as `packet` with the full `{ id, name, data }`, under its own name with `data, packet`, and under a state-qualified name such as `Status/StatusResponse`. A listener such as `client.on("KeepAlive", …)` is state-agnostic and receives every incoming `KeepAlive`, including packets from both configuration and play. Use state-qualified listeners for lifecycle handling so similarly named packets from another state cannot reach the handler. Valid prefixes are `Handshake`, `Status`, `Login`, `Config`, and `Play`.

## Log a bot in

Logging in means answering three prompts from the server. Against an mc-protocol server with `autoInit`, this is the complete exchange:

```ts
client.on("Socket:Ready", async () => {
  await client.sendPacket("Intension", {
    protocolVersion: 776,
    serverHost: "localhost",
    serverPort: 25565,
    nextState: 2, // login
  });
  await client.sendPacket("Hello", {
    username: "bot",
    playerUUID: "00000000000000000000000000000000", // 32 hex chars, no dashes
  });
});

// login state
client.on("Login/LoginFinished", async () => {
  await client.sendPacket("LoginAcknowledged"); // -> config, emits "login"
});

// config state
client.on("Config/SelectKnownPacks", async (data) => {
  await client.sendPacket("SelectKnownPacks", { knownPacks: data.knownPacks });
});

client.on("Config/FinishConfiguration", async () => {
  await client.sendPacket("FinishConfiguration"); // -> play, emits "playerJoin"
});

// play state
client.on("Play/KeepAlive", async (data) => {
  await client.sendPacket("KeepAlive", { keepAliveId: data.keepAliveId });
});

client.on("playerJoin", (session) => {
  console.log(`in game as ${session.username}`);
});
```

Compression is handled for you: when `LoginCompression` arrives the client switches both directions and emits `compression`. There is no client-side threshold option; the server chooses it. A positive threshold compresses packets at or above that many bytes (packet ID plus payload), `0` compresses every packet, and a negative threshold disables compression. `client.compression` and the `compression` event report `null` when disabled. The switch also works when the announcement and the next packet arrive in the same TCP chunk.

> **Warning**
>
> There is no encryption support. If the server sends `Hello` (the clientbound encryption request), the connection is in online mode and this client cannot proceed.

## Wrapping an existing socket

`Client.fromSocket(socket, options?)` builds a `Client` around a `net.Socket` the **server** side already accepted. Its `side` is `"server"`, so lifecycle packets are interpreted from the server's point of view. `Server` uses this internally with `{ versions }`; you need it for a [proxy](/docs/proxy) or a custom listener. Without `versions`, only the `version` option (default `776`) is accepted at the handshake.

```ts
import net from "node:net";
import { Client } from "mc-protocol";

net.createServer((socket) => {
  const player = Client.fromSocket(socket);
  player.on("packet", (packet) => console.log(packet.name));
}).listen(25565);
```

## Session and state

| Property | Meaning |
| --- | --- |
| `client.side` | `"client"` or `"server"` |
| `client.version` | Protocol number the packet tables use. Server side: negotiated from `Intension` |
| `client.session` | `{ protocol?, serverAddress?, serverPort?, username?, uuid?, profile? }`. `Intension` fills the first three; `Hello` fills `username`/`uuid` from either write names (`username`/`playerUUID`) or parsed names (`name`/`uuid`); `LoginFinished` overwrites them from `gameProfile` |
| `client.incomingState` / `outgoingState` | Current [connection state](/docs/connection-states) per direction |
| `client.compression` | Threshold in bytes, or `null` when off |
| `client.loggedIn` | `true` after `LoginAcknowledged` |
| `client.joined` | `true` after entering play |

## Closing

`client.disconnect()` ends the socket politely. `client.destroy()` tears it down immediately. Both emit `Socket:Close`.

## Sending and intercepting packets

`sendPacket(name, data)` looks the packet up by name in the current outgoing state. You can also pass a numeric ID for that state: `sendPacket(0, handshake)` is the same as `sendPacket("Intension", handshake)` during handshake.

`addPacketHandler(handler)` runs before `packet` and named events. The handler receives `{ id, name, data }` and may be async. It returns an unsubscribe function. `Server` and `Proxy` use this internally so their work finishes before your listeners see the packet. You rarely need it in application code.

`AcknowledgeConfiguration` is accepted as an alias of `ConfigurationAcknowledged` when switching play → config.
