The long version of the README: how the pieces fit together, what every tool, condition and scenario step does, what the version and platform support claims rest on, how to build from source, and what the jars bundle.
Setup is in INSTALL.md, full usage in usage.md, contribution rules in CONTRIBUTING.md, and release steps in publishing.md.
Three jars, in three different places. Only the first is a Minecraft plugin.
your MCP client (Claude Code, Cursor, Codex, Gemini CLI, ...)
|
| stdio, or shared loopback HTTP
v
mcp-server.jar ---- HTTP(S) + token ----> VitaminMCP.jar <- the plugin, inside your server
| sees events, logs, exceptions, live state
| spawns
v
Node runner -------- Minecraft protocol -> the same server, on :25565
sees what a player's client was actually sent
| Runs | Role | |
|---|---|---|
VitaminMCP.jar |
in the server, as a plugin | Listens to every event, taps the log, and serves an authenticated MCP endpoint. The only piece with a view of server internals |
mcp-server.jar |
on your machine | Speaks stdio or shared loopback HTTP to clients, HTTP to the plugin, and owns the bots |
runner.mjs or a platform bot-runner-* asset |
on your machine, as a child of mcp-server |
Connects real clients over the real protocol — login, packets, GUIs and all |
The plugin sees server-side events, logs, permissions and state; the Node runner sees what a real client receives. Read-only mode is the default, and bots are optional.
Ask the agent to test a plugin, or pass a scenario to bot_run_scenario:
[
{"action":"spawn", "bot":"Tester1"},
{"action":"command", "bot":"Tester1", "command":"shop"},
{"action":"wait_for", "condition":"inventory_open", "name":"Tester1", "title":"Shop"},
{"action":"assert_inventory", "bot":"Tester1", "slots":[
{"slot":11, "material":"DIAMOND_SWORD", "name":"Diamond Sword"}
]}
]
Two groups. Session tools live in mcp-server and are always present. Agent tools are
proxied from the plugin, so which ones exist is decided by the server you connected to —
session_start returns their real definitions in agentTools.
session_start |
Connect to a server and its agent. Every other tool needs it. Several sessions can be open at once — one per backend of a proxied network. If bots cannot join the server, the session still opens for the agent tools and bots says why |
session_reset |
Disconnect every bot, keeping the connection. Use between independent tests. World state is not rolled back. close: true ends the session instead |
bot_spawn |
Connect an offline or Microsoft-authenticated bot and wait until it is standing in the world. Offline UUIDs derive from the name |
bot_inspect |
What the bot’s client was actually sent: menu contents, messages (chat, action bar, title, subtitle) with the millisecond each arrived and a cursor to read only what came after an action, boss bars, sidebar scoreboard, health, food, experience and active effects |
bot_view |
Open a localhost-only live world or inventory view for a bot. The inventory view needs nothing extra; the world view downloads an optional asset the first time it is asked for, published for Windows x64 |
bot_run_scenario |
Run a whole scenario. Stops at the first failure with evidence attached |
server_info |
Version, TPS, players online, installed plugins, capture statistics |
command_exec |
Run a command as the console or as a player, vanilla commands included. Changes the server — absent entirely unless read-only: false. When nothing takes the command it says why, rather than only that it did not |
state_query kind="player" |
Position, gamemode, op, IP, and any permission nodes you name |
state_query kind="block" |
The block at a coordinate |
state_query kind="inventory" |
The menu a player has open — the only place a plugin GUI’s contents exist |
state_query kind="plugin" |
A plugin’s commands, permissions and live config |
events_summary |
Counts by event type. Call this before events_query — it stays small however busy the server is |
events_query |
Individual events, filtered by type and player, paged by cursor |
logs_query |
Logs by minimum severity and regular expression |
exceptions_recent |
Distinct exceptions with occurrence counts and first-seen times. Pass hash for a stack trace |
wait_for blocks until a condition holds, checked every tick inside the server.
| Condition | |
|---|---|
inventory_open |
a menu opened, optionally matching a title |
inventory_contains |
an item reached a slot — for GUIs filled after they open |
event |
an event fired, optionally for one player |
player_online / player_offline |
a player joined or left |
player_state |
online / gameMode / op reached a value |
player_near |
a player came within a radius |
block_is / block_is_not |
a block became, or stopped being, a material |
log_matches |
a log line matched a regex — for async work that changes nothing observable |
ticks |
the server advanced N ticks |
There is no sleep, and there will not be one. A fixed wait is a guess about timing that is right
on an idle server and wrong on a busy one — that is the entire mechanism by which flaky tests are
made. On timeout, wait_for returns the events and logs from that moment.
Available inside bot_run_scenario.
spawn / despawn |
connect or disconnect a bot |
move_to |
walk to coordinates by default; use mode: "teleport" for fast setup placement. Optional timeoutMillis distinguishes a sealed route from a walk that did not arrive in time |
break_block / use_block |
break, or right-click a block — use_block is how you open a chest |
use_entity |
right-click an NPC, villager or armour stand, named by the coordinates it stands at |
attack_entity |
left-click the nearest NPC, mob or armour stand at coordinates |
hold_item / drop_item |
select a hotbar slot, or drop the held item/one held item |
place_block |
place the held item against a block face |
jump / sneak / sprint |
perform one jump, or set the movement state on/off |
look_at |
look at world coordinates directly |
assert_reachable |
ask whether a loaded path exists without moving; set reachable: false for sealed-region assertions |
click_slot |
click a slot: left, right, shift_left, shift_right |
close_menu |
close the open menu |
chat |
send player chat and wait for the bot’s AsyncChatEvent; accepts timeoutMillis |
command |
send a command as the bot; use assertions to verify its result |
console |
run a command as the console |
wait_for |
any condition above |
chat uses normal player chat, including messages consumed by a chat-input plugin. A cancelled
AsyncChatEvent still confirms delivery; use assert_message or state assertions to check the
plugin’s result. It does not use /say or require that command’s permission. Offline bots have no
Mojang chat signing key: a server requiring secure chat can refuse them. Such a refusal fails the
step and includes messages received during the attempt. Use Microsoft authentication when signed
chat is required; the tool does not change server settings.
Verification is the point, so this is where the surface is widest.
| Checks | |
|---|---|
assert_inventory |
per slot: material, name, amount, lore, customModelData, modelDataString, empty — plus the menu’s title and size |
assert_player |
online, gameMode, op. Waits rather than reads, because /op resolves asynchronously |
assert_block |
the material at a coordinate |
assert_event |
an event fired, optionally for one player, since the scenario began |
assert_message |
the server told this bot something containing a string |
Use bot_inspect for messages, screen state and effects; use state_query for server state. Pass
proxied parameters flat at the top level. Full parameters are in usage.md.
These are the requirements for using a prebuilt release:
| Minecraft server | Paper 1.21 or later (Purpur and other Paper forks work) |
| Java | 21 for the local MCP server. The Paper server has its own requirement: 21 for 1.21.x, 25 for 26.1 |
| Node | 18.17 or later, for npx |
| Minecraft version | Windows | Linux | macOS | Status |
|---|---|---|---|---|
| 1.18 – 1.20.6 | 🟡 | 🟡 | 🟡 | Planned; below the current agent floor (1.21) |
| 1.21 – 1.21.11 | 🟢 | 🟢 | 🟢 | Supported and live-tested |
| 26.1 – 26.1.2 | 🟢 | 🟢 | 🟢 | Supported and live-tested; the server needs Java 25 |
| 26.2 – 26.3, agent | 🟢 | 🟢 | 🟢 | Agent supported: logs, events, state and commands, checked by hand on Paper 26.2-134 and 26.3-200; the server needs Java 25 |
| 26.2 – 26.3, bots | 🔴 | 🔴 | 🔴 | Not yet: mineflayer stops at 26.1 (#4). session_start opens for the agent and says why bots cannot join |
| Later releases | 🟡 | 🟡 | 🟡 | Each needs a compatibility run before it is added |
| Operating system | Node source runner | Native runner asset | Meaning |
|---|---|---|---|
| Windows x64 | 🟢 | 🟢 | Published, and the platform the matrix is run on |
| Linux x64 / arm64 | 🟢 | 🟢 | Published since 3.0.0 |
| macOS Intel / Apple Silicon | 🟢 | 🟢 | Published since 3.0.0, ad-hoc signed |
Legend: 🟢 supported · 🟡 planned or requires the stated runtime · 🔴 unsupported.
1.21 through 26.1 are supported today, and every one of them runs in the matrix
(versions.yaml). 1.21.11 is where the 1.21 line ends — Minecraft moved to calendar versions
after it, so what follows 1.21.11 is 26.1 rather than a 1.21.12. 26.1, 26.1.1 and 26.1.2 share one
protocol and are covered together.
26.2 and 26.3 are agent-only for now. The agent was checked by hand on Paper 26.2-134 and
26.3-200 — it loads, captures events and logs, answers state queries and runs commands — but bots
cannot join either until mineflayer and minecraft-protocol speak their protocols (776 and 777), so
neither is in the matrix: versions.yaml is what bots are tested against. Adding one is still a
compatibility run against a real server plus a check that the runner’s bundled data covers it,
never an edit to versions.yaml alone.
Paper 26.1 runs on Java 25, where the 1.21 line ran on 21. That is Paper’s requirement, not this project’s: the jars here still need only Java 21, and the agent loads on either.
The world view (bot_view) is not offered on 26.1 yet; everything else is.
Where each platform’s claim comes from. The matrix is run on Windows, against Paper builds it downloads itself — so what it proves is the same on any host, because the server it talks to is the same server. Each release builds its native runner on the operating system that runner is for, never cross-built, and every one of them is started in CI and has to refuse its own entry point with the expected exit code before it is uploaded. The world view is the one piece that is still Windows-only, and it says so where it is offered.
You install one Node runner whatever the version. It follows the Minecraft version the agent
reports, checks it against a server-list ping, and selects the matching minecraft-data entry, so
there is no protocol-specific runner to choose. When the two disagree — a proxy that echoes the
ping request’s protocol, or ViaVersion with ViaBackwards, which answers the ping in the client’s
protocol — the agent’s version wins and session_start says so under bots.
session_start.minecraftProtocol overrides both.
Outside the supported range, things fail clearly rather than misbehaving: an older server declines to load the agent, and a server whose protocol has no minecraft-data entry is named at startup.
Agent support and bot support can also differ. The agent needs a compatible Paper API; bots need a
matching minecraft-data entry and a supported runner environment. So a server may be readable by
the agent before bots can join it — inspection, logs and events all still work without them.
session_start then opens the session for the agent alone and bots in its response says why;
the bot tools refuse with that reason, and session_reset tries the runner again.
Most users do not need this section. Contributors need JDK 21 and Node/npm:
./gradlew build
cd bot/bot-runner-node && npm ci && npm test
Native runners are built with npm run build:sea -- win32-x64, linux-x64, linux-arm64,
darwin-x64 or darwin-arm64. macOS assets receive an ad-hoc signature in the release workflow.
The same scenario can be run across every supported version in one pass. The matrix is
versions.yaml, not code — adding a version is a single block. Server jars are
downloaded from the PaperMC API and started natively (no Docker, no ViaProxy;
no extra translation layer).
The protocol is deliberately not in that file. The Node runner asks each server what it speaks and selects the matching minecraft-data entry, so a version needs nothing there beyond the build to download.
Versions beyond 26.1 — 26.2, 26.3 and whatever follows — require a compatibility run before they are added, and a check that the runner’s trimmed data covers them. The runner bundles every version minecraft-data ships from the floor up, selects the matching one from the server handshake, and refuses clearly rather than half-working when it has no entry.
MIT — see LICENSE.
The distributed jars bundle third-party code, relocated so it cannot collide with the server or other plugins:
| Bundled in | License | |
|---|---|---|
| Jackson | VitaminMCP.jar, mcp-server.jar |
Apache-2.0 |
| ClassGraph | VitaminMCP.jar |
MIT |
| mineflayer, minecraft-data, mineflayer-pathfinder | Node runner dependencies | MIT |
Their license and notice files travel inside the jars under META-INF/ — relocating a package
renames it, it does not lift the obligation to carry the notice.
The optional Windows service installer downloads WinSW 2.12.0 from its official release and accepts only the SHA-256 pinned in the installer. WinSW is MIT licensed and is not bundled in this repository or the npm package.
paper-api, log4j-core and the JetBrains annotations are compile-only and are not distributed.
The agent compiles against Paper’s API, which is LGPL-3.0; the jar does not contain it, and the
server already provides it. Nothing here touches paper-server (GPL-3.0) — the agent uses the
Bukkit/Paper API only, never NMS.