---
title: Building a proxy
description: Forward packets between players and an upstream server, with optional packet handlers.
---

`Proxy` accepts connections from players and opens a separate connection to the upstream server for each player. It forwards packets in both directions by name and re-encodes them for the receiving connection.

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

const proxy = new Proxy({
  listen: { host: "0.0.0.0", port: 25566 },
  upstream: { host: "play.example.net", port: 25565 },
  versions: [776], // optional; protocol numbers accepted from players
});

proxy.Client.on("ChatCommand", async (player, data, next) => {
  console.log(`${player.session.username} ran /${data.command}`);
  await next(data);
});

proxy.Server.on("SetPlayerInventory", (_server, data, next) => {
  if (shouldHideInventory(data)) return next(null);
  return next(data);
});
```

`proxy.Client` handles packets sent by players toward the upstream server. Its first argument is the player connection. `proxy.Server` handles packets sent by the upstream server toward a player. Its first argument is the upstream connection. Both connections expose `sendPacket(name, data)` for sending additional packets to that endpoint.

Call `next(data)` to forward a packet, optionally with changed data. Call `next(null)` to cancel it. If a handler does not call `next`, the packet is also stopped. Multiple handlers for the same packet run in registration order, so later handlers receive the data passed by the previous handler. For asynchronous work, await or return `next(...)`.

Use `proxy.Client.once(name, handler)` or `proxy.Server.once(name, handler)` when a handler should process only the next matching packet.

The proxy forwards login, configuration, compression, and keep-alive packets to the real endpoints. `proxy.server` exposes the underlying `Server`, `proxy.connections` is a `Map` from player to upstream, and `proxy.close()` closes the listener and its connections.

## Events

| Event | Arguments | When |
| --- | --- | --- |
| `Proxy:Ready` | – | The player-facing listener is open. |
| `Proxy:Error` | `error` | The player-facing listener failed. |
| `Proxy:Close` | – | The player-facing listener closed. |
| `connection` | `player, upstream` | A player connected and an upstream `Client` was created. The upstream socket may not be ready yet. |
| `disconnect` | `player, upstream` | The player socket closed. The upstream connection is destroyed. |
| `Upstream:Error` | `player, error` | The upstream socket or packet parse failed. A socket error also destroys the player. |
| `Client:Error` | `player, error` | The player connection failed to parse a packet. |

Packet interception is `proxy.Client.on` / `proxy.Server.on`, not these events. The same table lives on [Events](/docs/events).

The player-facing `Server` accepts the protocols in `versions` (default: the built-in default). The upstream `Client` adopts the player's negotiated protocol when the forwarded `Intension` is sent, so both sides of a connection use the same packet tables.

Compression is negotiated independently on each connection. By default, forwarding `LoginCompression` gives the player the upstream server's threshold. The proxy decodes each incoming packet and re-encodes it with the destination connection's compression settings, so handlers continue to receive ordinary packet data.

You can rewrite the player-facing threshold without changing the upstream connection:

```ts
proxy.Server.on("LoginCompression", (_server, _data, next) => {
  return next({ threshold: 64 });
});
```

Dropping the announcement with `next(null)` leaves the player's compression unchanged (disabled initially); the upstream connection still adopts the original threshold. The proxy's internal server has `autoInit: false`, so it does not send its own announcement.

> **Info**
>
> Online-mode servers send an encryption request that this proxy cannot answer. Proxying works against offline-mode servers only. See [Limitations](/docs/limitations).
