Skip to content

Latest commit

 

History

15 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Minecraft LLM Companion

Connects a large language model to a Minecraft server through mineflayer, so the bot can accompany a player: it reads requests in chat, carries them out as multi-step tasks, and answers in chat.

Status

The bot joins a Minecraft 26.1 server, reads chat, asks a model for a decision, runs tools against the live world, and answers in chat. It can walk after a player, walk to a place, pick up a dropped item, hold and use an item, kill a creature, eat food it carries, be told to stand still, keep a plan for a request that spans several turns, and be stopped mid-request. A trusted operator can have it run a server command.

Still missing: the bot says nothing unless spoken to, so it never comments on what it sees; it remembers nothing between requests beyond the current task list; and it cannot find a player who is out of entity range, so "come to me" only works while the player is in view.

Requirements

  • Deno 2.9.7 or newer.
  • A Minecraft server at version 26.1 with online-mode=false, because the bot uses offline authentication.
  • Java 25 or newer, for the dev server that the integration scenarios run against.
  • An OpenAI-compatible chat completions endpoint.

Running

deno task start
Task Purpose
deno task start run the bot
deno task dev run with the file watcher
deno task check type check
deno task test unit tests
deno task lint lint
deno task fmt format
deno task demo:01 connectivity scenario, against the dev server
deno task demo:02 task runner and turn budgets, no server needed
deno task stub:model stand-in OpenAI-compatible endpoint, for runs without an API key
deno task probe:chat joins as a second player and drives the bot through chat
deno task compile build a self-contained binary into dist/

Configuration is read from $MCLLM_CONFIG_DIR/config.json and overridden by MCLLM_* environment variables. Data is written under $MCLLM_DATA_DIR. Every setting, its environment variable and its default are declared in one table in src/config.ts, which is the only place a tunable is written down; the sections below name the ones an operator is likely to change.

The model is reached at MCLLM_LLM_BASE_URL with MCLLM_LLM_MODEL, and the key is read from MCLLM_API_KEY and never written to a config file. Without a key the bot still joins and answers its chat commands.

A full run without a real model:

scripts/dev-server.sh start
MCLLM_LLM_BASE_URL=http://127.0.0.1:18080/v1 MCLLM_LLM_MODEL=stub \
  MCLLM_API_KEY=stub deno task start &
deno task stub:model &
deno task probe:chat

A run against Ollama's hosted models, which speak the OpenAI wire format at https://ollama.com/v1 and need no local weights:

export MCLLM_LLM_BASE_URL=https://ollama.com/v1
# Any model the account may use. Tool calling is required.
export MCLLM_LLM_MODEL=gemma4:cloud
export MCLLM_API_KEY=...   # from ollama.com/settings
MCLLM_SERVER_PORT=25567 deno task start

A model outside the account's plan answers HTTP 402 with a link to the pricing page. deno task start logs the model it selected at startup, so a wrong name shows up before the first question.

Personas

The system message carries a persona read from personas/, chosen per world:

MCLLM_PERSONA=companion deno task start

personas/companion.md ships with the repository. A name with no matching file is not an error: the bot runs without a persona, and the startup line reports "persona":"none". MCLLM_PERSONA_DIR moves the lookup to another directory, which is how a compiled binary finds them.

Chat commands

Command Effect
!state read the bot's own state out loud
!tasks read the plan it is working through
!phase report which stage of the build the code is at
!stop stop everything it is doing, without waiting for a model turn
!help list the commands

Everything else is a request for the model.

A request that asks the bot to follow a player starts the pathfinder, which keeps running after the turn ends. !stop or "stop following me" ends it.

While it follows, the bot keeps its head on the player, so it looks at whoever it is walking behind rather than at the horizon. It looks along its path while it walks, because mineflayer steers the bot with the same rotation it uses for the head. If the followed player leaves the world, the follow ends there instead of walking on to where they last stood.

A follow keeps up rather than trailing behind. Where the ground between the bot and the player is walkable and the player is within thirty-two blocks, it runs straight at them, sprinting and jumping, which is about a quarter faster than sprinting alone; the pathfinder cannot sprint-jump, because it decides its gait against the next path node and those are one block apart on a straight run. Everywhere else the pathfinder walks the bot as before, and it takes over again the moment the straight line stops being walkable. MCLLM_FOLLOW_RUN_STRAIGHT=false turns that off.

Multi-step requests

The bot keeps a task list for a request that spans more than one turn. It writes the plan with mc_setTaskList, the plan is carried into every later request, and !tasks reads it back:

follow me home and eat something when we arrive
> Follow Alex home and eat something. | 0 of 2 steps done | next: Follow Alex

!stop cancels whatever is running and drops the plan, and says what it reached:

!stop
> stopped follow Alex; dropped the task list

A stop reaches a turn that is already running, including a model request still waiting on the server, so it does not have to wait for the current step to end. The plan is capped by MCLLM_TASKS_MAX_ITEMS, MCLLM_TASKS_MAX_ITEM_CHARS and MCLLM_TASKS_MAX_GOAL_CHARS, because it is paid for on every request.

A request that runs past MCLLM_MAX_STEPS_PER_TURN or MCLLM_TURN_WALL_CLOCK_MS stops there, says which limit it hit, and cancels what it started. The plan survives that, so the next request can pick it up.

The pathfinder never breaks blocks or builds a tower to reach a player. It reports that it found no path instead, so a follow across terrain the bot cannot walk stops rather than edits the world.

Items and fighting

Tool Effect
mc_goTo walk to a player or a coordinate
mc_pickupItems walk to a dropped item and pick it up
mc_equipItem hold a named item in the main hand
mc_useItem use the held item; eating waits for the meal
mc_attack chase a creature and swing until it dies

There is no tool for reading the inventory, because every state snapshot already carries the whole inventory and the held item. Such a tool would only repeat what the model has, and every extra choice is a chance to choose wrong.

mc_attack refuses a player whatever the request says, and refuses before it starts chasing when health is already at the floor it fights down to (MCLLM_FLEE_HEALTH, default 6). Both are policy, not capability: bot.attack would send the packet either way.

A walk or a fight replaces a follow that is still running, because the bot walks one way at a time. Each of those tools closes the follow's cancellation scope first, so a request that wants both asks for the follow again afterwards. A fight is also reachable by !stop, like any other long-running work.

Server commands

The bot can run a server command when a player asks it to. Two gates must both be open:

  1. MCLLM_CHEATS=true — the world allows it, and the bot holds operator status.
  2. The player who asked is on MCLLM_CHEAT_WHITELIST, a comma-separated list of names or UUIDs.
MCLLM_CHEATS=true MCLLM_CHEAT_WHITELIST=Alex deno task start

An empty whitelist trusts nobody, so the default is a bot that refuses every command. A refused request is logged as chat.untrusted and answered in chat, and the refusal reaches the model as not permitted: <reason> so it cannot mistake it for a success.

mineflayer does not expose whether the bot is an operator, so MCLLM_CHEATS is the operator's assertion and the server has the last word: a command the bot has no permission for comes back as "Unknown or incomplete command", and the tool reports that as a failure instead of a result.

One command runs at a time, and a command carrying a second line is refused, so a command cannot smuggle a second one past the model.

Joining a server newer than the client speaks

mineflayer speaks up to Minecraft 26.1. For a 26.2 or 26.3 server, put ViaProxy in front of it and point the bot at the proxy:

java -jar ViaProxy.jar cli \
  --bind-address 127.0.0.1:25568 \
  --target-address 127.0.0.1:25565 \
  --auth-method NONE \
  --proxy-online-mode false

MCLLM_SERVER_HOST=127.0.0.1 MCLLM_SERVER_PORT=25568 deno task start

The server must have online-mode=false: the bot authenticates offline, and a server that requires a real account rejects it during login.

ViaProxy translates the protocol only. It does not add mod registries, so it cannot make a modded Fabric server reachable. A Fabric server running fabric-registry-sync-v0 disconnects a client that cannot speak Fabric networking unless every registry the mods want to sync is marked optional by the mod that adds it. A vanilla-protocol client such as mineflayer is such a client, and no server-side setting lifts that.

Dev server

scripts/dev-server.sh downloads and runs a vanilla server for the integration scenarios. Everything it writes stays outside the repository, under $MCLLM_DEV_SERVER_DIR.

scripts/dev-server.sh start     # download if needed, start, wait until ready
scripts/dev-server.sh stop      # stop the server, letting it save the world
scripts/dev-server.sh status    # report whether it is running
Variable Default Meaning
MCLLM_MC_VERSION 26.1 Minecraft version
MCLLM_SERVER_PORT 25565 listening port
MCLLM_DEV_SERVER_DIR ~/.cache/minecraft-llm/dev-server data directory
MCLLM_DEV_SERVER_JAVA java java binary, needed when the default is older than 25
MCLLM_DEV_SERVER_MEMORY 1G heap size
MCLLM_LEVEL_TYPE normal vanilla level type

Starting the server accepts the Minecraft EULA. An example run:

MCLLM_DEV_SERVER_JAVA=/usr/lib/jvm/java-25-openjdk/bin/java \
  scripts/dev-server.sh start
MCLLM_SERVER_PORT=25565 deno task demo:01

Self-contained binary

deno task compile

Writes dist/mcllm, an executable that needs no Deno install, no node_modules and no source tree beside it. Permissions are baked in at build time, so it runs with no flags. personas/ is copied to dist/personas/, and MCLLM_PERSONA_DIR moves it.

minecraft-data carries the world data for every version it knows, which is 430 MB of JSON a bot speaking one version can never read, and a plain deno compile produces a 507 MB binary. scripts/compile.ts keeps the files the shipped version's entry in data.js reaches and hands the rest to --exclude, which brings the binary to 133 MB. That entry is not self-contained — the 26.1 entry reuses files from five other version directories — so what is dropped is a closure over the entry rather than a list of versions. MCLLM_MC_VERSION selects the version the binary is built for.

Contributing

scripts/README.md covers the pre-commit hook. Commit subjects follow the scoped convention: <scope>: <description>, with the rationale in the body.

License

ISC. See LICENSE.

AI usage

The code, this document and the test suite are written with AI assistance, under human review. Claims about behaviour are backed by a check that can be run rather than asserted: deno task test covers the units, deno task demo:01 and deno task demo:02 cover the integration paths without a model, and the figures quoted above were measured on a live server.

About

LLM-driven Minecraft companion bot: reads chat, runs multi-step tasks through mineflayer, answers in chat

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages