MCP server for testing Minecraft plugins: drives a real Paper server and real protocol bots from an AI agent.
VitaminMCP demonstrates solid tool design with clear, domain-specific naming and comprehensive descriptions. All 14 tools have non-empty descriptions (avg ~180 chars, within baseline 194). Input schemas are present and mostly well-typed. However, several tools lack explicit output schema documentation, and some parameter descriptions could be more prescriptive about constraints and error recovery. Tool composition is excellent, each tool has a single responsibility and chains well (e.g., events_summary → events_query, session_start → bot_spawn). Security is well-handled with no exposed credentials. The server targets a specialized domain (Minecraft testing) with sophisticated parameter relationships (e.g., state_query's 'kind' enum determines which other params apply), which is documented but could be more explicit about mutual exclusivity.
Execute an action on the bot's behalf. The response is what the server answered, or an error if the action was refused.
What the bot's client was told, which the server cannot always be asked: a plugin drawing its GUI with packets leaves the server-side inventory empty, and a refusal like 'you lack permission' goes to the player and never reaches the console. 'items' IS THE OPEN MENU'S CONTENTS AND NOTHING ELSE — null when no menu is open, never the player's own inventory (that is state_query kind='inventory' which='player'). 'messages' records carry sequence, timestamp (epoch milliseconds of arrival) and text; Chat text is returned as received, and action bar / title / subtitle text is prefixed with where it appeared. At most 100 messages are retained per bot. To isolate one action's reply, call this before the command, save 'messageCursor', and pass it back as 'cursor'; the cursor is opaque, belongs to this bot connection, and is rejected from another session, runner, or same-named replacement. A nonzero 'messagesDropped' means the answer is incomplete. Also reports health, food, experience, effects, and 'bossBars' and 'scoreboard' — persistent on-screen state, where a server's live view of a player (timers, money, quest progress) is usually drawn.
Connect an offline or Microsoft-authenticated bot and wait until it is standing in the world. Rejected while the connected agent is read-only. An offline bot's UUID derives from its name, so the same name is the same player every run — which means THE SERVER REMEMBERS IT: inventory, position and plugin data survive from earlier runs, so 'it has the item' may be left over rather than just granted. Use an unused offline name to test a first join, and clear what you leave behind. A successful spawn means the CLIENT is ready, not that the server will act yet — Paper and plugins drop or refuse a joining player's interactions for a few seconds, and a refused action says so in its answer, so read what an action answered rather than assuming spawn means ready.
state_query and wait_for lack explicit output schema documentation. Callers cannot predict response structure for complex queries (e.g., what fields does a player state return? what does inventory_contains return?). This forces LLMs to guess or make discovery calls.
state_query's 'kind' parameter has conditional sub-parameters (target required for player/inventory/plugin; x,y,z required for block; world optional for block). This dependency is documented in the description but not formalized as mutually-exclusive parameter groups, risking LLM confusion.
bot_action's 'action' parameter is a free-form string with no enum or format constraint. The description does not list valid action types (e.g., 'move', 'attack', 'use', 'chat'). LLMs will hallucinate invalid actions.
Inferred effective spec: <=2025-11-25.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | C | 68 | <=2025-11-25 | v2 |
Start or reuse a localhost-only live view for one bot; the same bot always reuses its URL. Pass stop:true to close it. The world viewer is an optional download, fetched on first use.
Execute a console command. Rejected while read-only. The response is the command's output, or an error if the command was refused.
Reads individual captured events. High-frequency types (PlayerMoveEvent, BlockPhysicsEvent, ChunkLoadEvent, entity movement) are excluded unless you name them in 'types' explicitly. Page with 'cursor'.
Counts captured events by type over a time window. ALWAYS call this before events_query — it stays small however busy the server is, and says which types are worth querying in detail.
Distinct exceptions, most recently seen first, collapsed with occurrence counts and first-seen times; stack traces are omitted — pass 'hash' to fetch one. THIS COVERS THE CURRENT SERVER RUN ONLY: the record starts empty at every boot, so an empty answer means 'not since this server started', never 'this has never happened'.
Searches captured server logs by severity and regular expression; there is no 'last N lines'. The buffer starts when the agent attaches, so a startup line written before that may be absent.
Server implementation, version, TPS, online players, installed plugins, and capture statistics. Start here.
Disconnect every bot, keeping the connection. Use between independent tests so one does not inherit the other's players. Pass close:true to end the session instead — the only way to release one.
Connect to a Minecraft server and its VitaminMCP agent. Call this first — every other tool needs it. Several sessions can be open at once (one per backend of a proxied network); starting one never disturbs the others, and naming an open session's server and agent replaces it. The response includes the server details, the real agent tool definitions and the current session roster; sessions whose runner process has exited are removed from that roster.
Reads current server state. kind='player' needs 'target', optionally 'permissions'. kind='block' needs x, y, z, optionally 'world'. kind='inventory' needs 'target' and reads the menu that player has open — THE ONLY PLACE A PLUGIN GUI'S CONTENTS EXIST. Slots report material, amount, name, lore and customModelData (what a resource pack draws — same-material buttons can still be different icons); empty slots are omitted; a 'view' of CRAFTING or CREATIVE means no menu is open. kind='plugin' needs 'target' and answers its commands with the permission gating each, its declared permissions, and its LIVE loaded config — often not what the file in a repository says; secret-looking values read '(redacted)'. A command often carries no 'permission' because its plugin checks a node in code instead — read the 'permissions' list too before concluding a command is ungated.
Blocks until a condition holds, then returns. USE THIS INSTEAD OF WAITING OR RETRYING YOURSELF — a fixed wait is right on an idle server and wrong on a busy one. On timeout the response carries the events and log lines from that moment. Wait for inventory_open before reading a menu — opening one is not synchronous with the command that caused it; use log_matches for async work that changes nothing observable. Chat, action bar and title NEVER reach the server-side agent, so no condition here can see them — assert those on the bot side (assert_message, bot_inspect).
Error handling descriptions are minimal. Tools like command_exec, bot_spawn, and bot_action state they return 'an error if refused' but do not guide recovery (e.g., 'If permission denied, check the player's role with state_query'). This leaves LLMs without actionable next steps.
session_start's 'minecraftProtocol' and 'tlsFingerprint' parameters lack numeric/format constraints. LLMs may pass invalid protocol numbers or malformed fingerprints. Descriptions should specify valid ranges (e.g., protocol 340 - 772 for MC 1.12 - 1.21) and format (SHA-256 hex string).