Skip to content
mc-protocol
Esc
↑↓navigate↵open⌘Jpreview
On this page

Commands

Declare the command tree the client uses for tab completion and syntax highlighting, without hand-numbering nodes.

The Commands packet tells the client which commands exist and how each argument parses. It is a flat array of nodes that reference each other by index, with flags packed into a byte and a parser ID plus parser-specific properties per argument. helpers.createCommandTree takes a nested description and produces that array.

import { helpers } from "mc-protocol";
const { literal, argument, createCommandTree } = helpers;

await client.sendPacket("Commands", createCommandTree([
  literal("ping", { executable: true }),
  literal("tp", {
    children: [argument("target", "entity", { single: true, playersOnly: true, executable: true })],
  }),
  literal("say", {
    children: [argument("message", "string", { type: "greedy", executable: true, suggestions: "ask_server" })],
  }),
  literal("run", { redirect: "root" }),
]));

This declares the syntax. Executing commands is still yours: listen for the serverbound ChatCommand packet and parse data.command.

createCommandTree(commands)

Takes the root’s children and returns { nodes, rootIndex: 0 }, ready for sendPacket("Commands", …). Nodes are numbered breadth-first from the root, so the order of your array is the order of the root’s children. A node object that appears under several parents is written once and shared.

Errors are thrown before anything is encoded when:

  • a redirect points at a node that is not reachable from the root (RangeError)
  • two children of the same node share a name (RangeError)
  • children form a cycle; loops such as /execute run … must use redirect (RangeError)
  • an element is not a node from literal() or argument() (TypeError)

literal(name, options?)

A fixed keyword. Options:

Option Type Wire effect
children CommandNode[] child indices
executable boolean flag 0x04; the path up to this node is a complete command
restricted boolean flag 0x20; requires permission level above 0
redirect CommandNode | "root" flag 0x08 and the target’s index

argument(name, parser, options?)

A typed argument. parser is any 26.2 parser name, with or without its prefix: "integer" and "brigadier:integer" are the same, as are "entity" and "minecraft:entity". Unknown names throw; the parser’s numeric ID is resolved by the wire codec. Argument nodes take the literal options plus suggestions, a suggestion provider: "ask_server", "all_recipes", "available_sounds", "summonable_entities", or a namespaced identifier.

Parsers with properties take them in the same options object. TypeScript narrows the accepted keys by parser name.

Parser Properties Notes
float, double, integer min?, max? Only the bounds you pass are written. integer bounds must be 32-bit integers.
long min?, max? number or bigint, 64-bit signed.
string type? "word" (default), "phrase" (quotable), or "greedy" (rest of the line).
entity single?, playersOnly? Both default to false.
score_holder multiple? Defaults to false.
time min? Minimum ticks, defaults to 0.
resource, resource_key, resource_or_tag, resource_or_tag_key, resource_selector registry Required. "entity_type" becomes "minecraft:entity_type".

Every other parser (block_pos, vec3, game_profile, uuid, component, …) takes no properties. Any key a parser does not take throws, so a typo like mni is caught. The full list is helpers.COMMAND_PARSERS. The wiki’s minecraft:nbt_compound_tag is spelled minecraft:nbt in the bundled codec; nbt_compound_tag, nbt and their prefixed forms are all accepted and encode as parser 21.

argument("amount", "integer", { min: 1, max: 64, executable: true });
argument("delay", "time", { min: 20 });
argument("biome", "resource_or_tag", { registry: "worldgen/biome", executable: true });
argument("mode", "gamemode", { executable: true });

Sharing and redirects

Nodes are plain objects. Keep a reference to share a subtree or to point a redirect at it:

const amount = argument("amount", "integer", { min: 1, executable: true });
const give = literal("give", { children: [amount] });
const take = literal("take", { children: [amount] });
const again = literal("again", { redirect: give }); // continue parsing at /give

createCommandTree([give, take, again]); // 5 nodes: root, give, take, again, amount

redirect: "root" is how vanilla builds /execute run <command>: after run, parsing restarts at the root.

What you get back

createCommandTree([literal("ping", { executable: true })]);
// {
//   nodes: [
//     { flags: { unused: 0, allows_restricted: 0, has_custom_suggestions: 0, has_redirect_node: 0, has_command: 0, command_node_type: 0 }, children: [1] },
//     { flags: { …, has_command: 1, command_node_type: 1 }, children: [], extraNodeData: { name: "ping" } },
//   ],
//   rootIndex: 0,
// }

The flags object uses the codec’s bit-field names. Argument nodes carry extraNodeData: { name, parser, properties, suggestionType }. Decoded packets return enums as { value, translation }, so a node read from the wire has parser: { value: 6, translation: "minecraft:entity" } rather than the bare string.

Types

All exported through helpers: CommandNode, CommandNodeWire, CommandsPacket, CommandNodeOptions, ArgumentNodeOptions, CommandParser, CommandParserName, ParserProperties<P>, CommandSuggestions.

Was this page helpful?