Base URL: http://127.0.0.1:25568 (override with --api).
All JSON error responses use {"error": "<message>"}.
Returns proxy session state. Always 200 — check the active field.
No active session:
{ "active": false }Active session:
{
"active": true,
"upstream": "127.0.0.1:25565",
"state": "play",
"compression": 256,
"packets_c2s": 142,
"packets_s2c": 389,
"started_at": 1722000000000
}| Field | Type | Notes |
|---|---|---|
active |
bool | Whether a client is connected |
upstream |
string | Upstream server address |
state |
string | handshake / status / login / configuration / play |
compression |
int | null | null = disabled, 0 = enabled (no threshold), N = threshold in bytes |
packets_c2s |
int | Total client→server packets relayed this session |
packets_s2c |
int | Total server→client packets relayed this session |
started_at |
int | Unix milliseconds when the session started |
Returns all packet definitions, keyed by protocol state.
{
"play": [
{
"name": "swing_arm",
"id": 27,
"fields": [
{ "name": "hand", "type": "varint" }
]
}
],
"login": [...]
}Reloads the registry JSON from disk without restarting the proxy. No request body.
{ "ok": true, "packets": 12 }Returns 500 if the file cannot be read or parsed.
Adds a new packet definition to the registry, or replaces an existing one with the same name in the same state. Persists to the registry JSON file and updates the in-memory registry atomically.
Request:
{
"state": "play",
"name": "swing_arm",
"id": 27,
"dir": "c2s",
"fields": [
{ "name": "hand", "type": "varint" }
]
}dir is optional. Omit for C2S (client→server) entries; set to "s2c" for
server→client entries. Existing registry files with no dir field are
treated as C2S.
Response:
{ "ok": true, "packets": 13 }Errors:
- 400 —
nameis empty or is the reserved name"raw" - 500 — could not write the registry file to disk
Valid field types: bool, i8, u8, i16, u16, i32, u32,
i64, f32, f64, varint, string, uuid, hexbytes.
Injects a packet by numeric ID and a raw hex payload into the active session's client→server stream.
Request:
{ "id": 27, "hex": "01" }hex may be space-separated bytes ("01 00 ff") or a contiguous string
("0100ff"). id is the packet ID as a decimal integer (e.g. 27 for
0x1b).
Response:
{ "ok": true, "bytes": 1 }Errors:
- 400 —
hexis malformed - 503 — no active session
Looks up a packet definition by name from the registry, encodes the supplied field values, and injects the result.
Request:
{
"name": "swing_arm",
"fields": { "hand": "0" },
"state": "play"
}state is optional. When omitted, the current live protocol state is
used.
Response:
{ "ok": true, "id": 27, "bytes": 1 }Errors:
- 400 — a field value cannot be encoded to its declared type
- 404 — no packet with that name in the given state
- 503 — no active session
Run a McFabScript program. If a script is already running, it is stopped
first. Returns immediately — the script runs asynchronously; subscribe to
/ws/events for output.
Request:
{ "code": "log(\"hello\"); sleep(500); log(\"world\");" }Response:
{ "ok": true }Errors:
- 400 — script has a syntax or parse error (error message in body)
Stop the currently running script (no-op if none is running).
Response:
{ "ok": true }Returns whether a script is currently running.
{ "running": false }Upgrade to WebSocket at ws://127.0.0.1:25568/ws/events to receive a
live stream of proxy events. All messages are JSON objects with a "type"
discriminator field.
Sent for every packet relayed in either direction:
{
"type": "packet",
"direction": "c2s",
"state": "play",
"id": 27,
"data": [1],
"ts": 1722000000123,
"fields": { "hand": "0" }
}| Field | Type | Notes |
|---|---|---|
type |
string | Always "packet" |
direction |
string | c2s (client→server) or s2c (server→client) |
state |
string | Protocol state at capture time |
id |
int | Packet ID |
data |
int[] | Raw packet payload bytes (excluding framing and ID) |
ts |
int | Capture timestamp, Unix milliseconds |
fields |
object | Decoded field values keyed by name. Omitted when no matching registry entry exists or decoding fails — raw data is always present as a fallback. |
Emitted when a running script calls log() or encounters an error:
{
"type": "script_log",
"level": "info",
"msg": "hello world"
}| Field | Type | Notes |
|---|---|---|
type |
string | Always "script_log" |
level |
string | info, warn, or error |
msg |
string | The log message |
Emitted when a script starts or finishes:
{
"type": "script_status",
"running": true
}| Field | Type | Notes |
|---|---|---|
type |
string | Always "script_status" |
running |
bool | true when a script started, false when it finished or was stopped |
Sent when the broadcast channel drops events due to a slow consumer. The connection stays open — no reconnect needed:
{ "type": "gap", "count": 42 }count is the number of events dropped. The consumer should note the
discontinuity and continue.
The server sends no explicit close on session end. If the WebSocket closes, reconnect — the channel persists across proxy sessions.
All paths not matching /api/* or /ws/* are served from web/dist/
with an index.html fallback for client-side routing. The web UI is
available at http://127.0.0.1:25568/.