Skip to main content
Glama

mcp-minecraft

#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import mineflayer from 'mineflayer'; import pathfinderPkg from 'mineflayer-pathfinder'; const { pathfinder, Movements, goals } = pathfinderPkg; import { Vec3 } from 'vec3'; import minecraftData from 'minecraft-data'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; // ========== Type Definitions ========== type TextContent = { type: "text"; text: string; }; type ContentItem = TextContent; type McpResponse = { content: ContentItem[]; _meta?: Record<string, unknown>; isError?: boolean; [key: string]: unknown; }; interface InventoryItem { name: string; count: number; slot: number; } interface FaceOption { direction: string; vector: Vec3; } type Direction = 'forward' | 'back' | 'left' | 'right'; type FaceDirection = 'up' | 'down' | 'north' | 'south' | 'east' | 'west'; interface StoredMessage { timestamp: number; username: string; content: string; } // ========== Command Line Argument Parsing ========== function parseCommandLineArgs() { return yargs(hideBin(process.argv)) .option('host', { type: 'string', description: 'Minecraft server host', default: 'localhost' }) .option('port', { type: 'number', description: 'Minecraft server port', default: 25565 }) .option('username', { type: 'string', description: 'Bot username', default: 'LLMBot' }) .help() .alias('help', 'h') .parseSync(); } // ========== Logging and Responding ========== type LogLevel = 'info' | 'warn' | 'error'; function log(level: LogLevel, message: string) { const timestamp = new Date().toISOString(); process.stderr.write(`${timestamp} [minecraft] [${level}] ${message}\n`); } function createResponse(text: string): McpResponse { return { content: [{ type: "text", text }] }; } function createErrorResponse(error: Error | string): McpResponse { const errorMessage = formatError(error); log('error', errorMessage); return { content: [{ type: "text", text: `Failed: ${errorMessage}` }], isError: true }; } function formatError(error: unknown): string { if (error instanceof Error) { return error.stack || error.message; } try { return JSON.stringify(error); } catch { return String(error); } } // ========== Message Storage ========== const MAX_STORED_MESSAGES = 100; class MessageStore { private messages: StoredMessage[] = []; private maxMessages = MAX_STORED_MESSAGES; addMessage(username: string, content: string) { const message: StoredMessage = { timestamp: Date.now(), username, content }; this.messages.push(message); if (this.messages.length > this.maxMessages) { this.messages.shift(); } } getRecentMessages(count: number = 10): StoredMessage[] { return this.messages.slice(-count); } } // Global message store instance const messageStore = new MessageStore(); // ========== Bot Setup ========== function setupBot(argv: any): mineflayer.Bot { // Configure bot options based on command line arguments const botOptions = { host: argv.host, port: argv.port, username: argv.username, plugins: { pathfinder }, }; // Create a bot instance const bot = mineflayer.createBot(botOptions); // Set up the bot when it spawns bot.once('spawn', async () => { // Set up pathfinder movements const mcData = minecraftData(bot.version); const defaultMove = new Movements(bot, mcData); bot.pathfinder.setMovements(defaultMove); bot.chat('LLM-powered bot ready to receive instructions!'); log('info', `Server started and connected successfully. Bot: ${argv.username} on ${argv.host}:${argv.port}`); }); // Register common event handlers bot.on('chat', (username, message) => { if (username === bot.username) return; messageStore.addMessage(username, message); }); bot.on('kicked', (reason) => { log('error', `Bot was kicked: ${formatError(reason)}`); bot.quit(); }); bot.on('error', (err) => { log('error', `Bot error: ${formatError(err)}`); }); return bot; } // ========== MCP Server Configuration ========== function createMcpServer(bot: mineflayer.Bot) { const server = new McpServer({ name: "minecraft-mcp-server", version: "1.2.0" }); // Register all tool categories registerPositionTools(server, bot); registerInventoryTools(server, bot); registerBlockTools(server, bot); registerEntityTools(server, bot); registerChatTools(server, bot); registerFlightTools(server, bot); registerGameStateTools(server, bot); return server; } // ========== Position and Movement Tools ========== function registerPositionTools(server: McpServer, bot: mineflayer.Bot) { server.tool( "get-position", "Get the current position of the bot", {}, async (): Promise<McpResponse> => { try { const position = bot.entity.position; const pos = { x: Math.floor(position.x), y: Math.floor(position.y), z: Math.floor(position.z) }; return createResponse(`Current position: (${pos.x}, ${pos.y}, ${pos.z})`); } catch (error) { return createErrorResponse(error as Error); } } ); server.tool( "move-to-position", "Move the bot to a specific position", { x: z.number().describe("X coordinate"), y: z.number().describe("Y coordinate"), z: z.number().describe("Z coordinate"), range: z.number().optional().describe("How close to get to the target (default: 1)") }, async ({ x, y, z, range = 1 }): Promise<McpResponse> => { try { const goal = new goals.GoalNear(x, y, z, range); await bot.pathfinder.goto(goal); return createResponse(`Successfully moved to position near (${x}, ${y}, ${z})`); } catch (error) { return createErrorResponse(error as Error); } } ); server.tool( "look-at", "Make the bot look at a specific position", { x: z.number().describe("X coordinate"), y: z.number().describe("Y coordinate"), z: z.number().describe("Z coordinate"), }, async ({ x, y, z }): Promise<McpResponse> => { try { await bot.lookAt(new Vec3(x, y, z), true); return createResponse(`Looking at position (${x}, ${y}, ${z})`); } catch (error) { return createErrorResponse(error as Error); } } ); server.tool( "jump", "Make the bot jump", {}, async (): Promise<McpResponse> => { try { bot.setControlState('jump', true); setTimeout(() => bot.setControlState('jump', false), 250); return createResponse("Successfully jumped"); } catch (error) { return createErrorResponse(error as Error); } } ); server.tool( "move-in-direction", "Move the bot in a specific direction for a duration", { direction: z.enum(['forward', 'back', 'left', 'right']).describe("Direction to move"), duration: z.number().optional().describe("Duration in milliseconds (default: 1000)") }, async ({ direction, duration = 1000 }: { direction: Direction, duration?: number }): Promise<McpResponse> => { return new Promise((resolve) => { try { bot.setControlState(direction, true); setTimeout(() => { bot.setControlState(direction, false); resolve(createResponse(`Moved ${direction} for ${duration}ms`)); }, duration); } catch (error) { bot.setControlState(direction, false); resolve(createErrorResponse(error as Error)); } }); } ); } // ========== Inventory Management Tools ========== function registerInventoryTools(server: McpServer, bot: mineflayer.Bot) { server.tool( "list-inventory", "List all items in the bot's inventory", {}, async (): Promise<McpResponse> => { try { const items = bot.inventory.items(); const itemList: InventoryItem[] = items.map((item: any) => ({ name: item.name, count: item.count, slot: item.slot })); if (items.length === 0) { return createResponse("Inventory is empty"); } let inventoryText = `Found ${items.length} items in inventory:\n\n`; itemList.forEach(item => { inventoryText += `- ${item.name} (x${item.count}) in slot ${item.slot}\n`; }); return createResponse(inventoryText); } catch (error) { return createErrorResponse(error as Error); } } ); server.tool( "find-item", "Find a specific item in the bot's inventory", { nameOrType: z.string().describe("Name or type of item to find") }, async ({ nameOrType }): Promise<McpResponse> => { try { const items = bot.inventory.items(); const item = items.find((item: any) => item.name.includes(nameOrType.toLowerCase()) ); if (item) { return createResponse(`Found ${item.count} ${item.name} in inventory (slot ${item.slot})`); } else { return createResponse(`Couldn't find any item matching '${nameOrType}' in inventory`); } } catch (error) { return createErrorResponse(error as Error); } } ); server.tool( "equip-item", "Equip a specific item", { itemName: z.string().describe("Name of the item to equip"), destination: z.string().optional().describe("Where to equip the item (default: 'hand')") }, async ({ itemName, destination = 'hand' }): Promise<McpResponse> => { try { const items = bot.inventory.items(); const item = items.find((item: any) => item.name.includes(itemName.toLowerCase()) ); if (!item) { return createResponse(`Couldn't find any item matching '${itemName}' in inventory`); } await bot.equip(item, destination as mineflayer.EquipmentDestination); return createResponse(`Equipped ${item.name} to ${destination}`); } catch (error) { return createErrorResponse(error as Error); } } ); } // ========== Block Interaction Tools ========== function registerBlockTools(server: McpServer, bot: mineflayer.Bot) { server.tool( "place-block", "Place a block at the specified position", { x: z.number().describe("X coordinate"), y: z.number().describe("Y coordinate"), z: z.number().describe("Z coordinate"), faceDirection: z.enum(['up', 'down', 'north', 'south', 'east', 'west']).optional().describe("Direction to place against (default: 'down')") }, async ({ x, y, z, faceDirection = 'down' }: { x: number, y: number, z: number, faceDirection?: FaceDirection }): Promise<McpResponse> => { try { const placePos = new Vec3(x, y, z); const blockAtPos = bot.blockAt(placePos); if (blockAtPos && blockAtPos.name !== 'air') { return createResponse(`There's already a block (${blockAtPos.name}) at (${x}, ${y}, ${z})`); } const possibleFaces: FaceOption[] = [ { direction: 'down', vector: new Vec3(0, -1, 0) }, { direction: 'north', vector: new Vec3(0, 0, -1) }, { direction: 'south', vector: new Vec3(0, 0, 1) }, { direction: 'east', vector: new Vec3(1, 0, 0) }, { direction: 'west', vector: new Vec3(-1, 0, 0) }, { direction: 'up', vector: new Vec3(0, 1, 0) } ]; // Prioritize the requested face direction if (faceDirection !== 'down') { const specificFace = possibleFaces.find(face => face.direction === faceDirection); if (specificFace) { possibleFaces.unshift(possibleFaces.splice(possibleFaces.indexOf(specificFace), 1)[0]); } } // Try each potential face for placing for (const face of possibleFaces) { const referencePos = placePos.plus(face.vector); const referenceBlock = bot.blockAt(referencePos); if (referenceBlock && referenceBlock.name !== 'air') { if (!bot.canSeeBlock(referenceBlock)) { // Try to move closer to see the block const goal = new goals.GoalNear(referencePos.x, referencePos.y, referencePos.z, 2); await bot.pathfinder.goto(goal); } await bot.lookAt(placePos, true); try { await bot.placeBlock(referenceBlock, face.vector.scaled(-1)); return createResponse(`Placed block at (${x}, ${y}, ${z}) using ${face.direction} face`); } catch (placeError) { log('warn', `Failed to place using ${face.direction} face: ${formatError(placeError)}`); continue; } } } return createResponse(`Failed to place block at (${x}, ${y}, ${z}): No suitable reference block found`); } catch (error) { return createErrorResponse(error as Error); } } ); server.tool( "dig-block", "Dig a block at the specified position", { x: z.number().describe("X coordinate"), y: z.number().describe("Y coordinate"), z: z.number().describe("Z coordinate"), }, async ({ x, y, z }): Promise<McpResponse> => { try { const blockPos = new Vec3(x, y, z); const block = bot.blockAt(blockPos); if (!block || block.name === 'air') { return createResponse(`No block found at position (${x}, ${y}, ${z})`); } if (!bot.canDigBlock(block) || !bot.canSeeBlock(block)) { // Try to move closer to dig the block const goal = new goals.GoalNear(x, y, z, 2); await bot.pathfinder.goto(goal); } await bot.dig(block); return createResponse(`Dug ${block.name} at (${x}, ${y}, ${z})`); } catch (error) { return createErrorResponse(error as Error); } } ); server.tool( "get-block-info", "Get information about a block at the specified position", { x: z.number().describe("X coordinate"), y: z.number().describe("Y coordinate"), z: z.number().describe("Z coordinate"), }, async ({ x, y, z }): Promise<McpResponse> => { try { const blockPos = new Vec3(x, y, z); const block = bot.blockAt(blockPos); if (!block) { return createResponse(`No block information found at position (${x}, ${y}, ${z})`); } return createResponse(`Found ${block.name} (type: ${block.type}) at position (${block.position.x}, ${block.position.y}, ${block.position.z})`); } catch (error) { return createErrorResponse(error as Error); } } ); server.tool( "find-block", "Find the nearest block of a specific type", { blockType: z.string().describe("Type of block to find"), maxDistance: z.number().optional().describe("Maximum search distance (default: 16)") }, async ({ blockType, maxDistance = 16 }): Promise<McpResponse> => { try { const mcData = minecraftData(bot.version); const blocksByName = mcData.blocksByName; if (!blocksByName[blockType]) { return createResponse(`Unknown block type: ${blockType}`); } const blockId = blocksByName[blockType].id; const block = bot.findBlock({ matching: blockId, maxDistance: maxDistance }); if (!block) { return createResponse(`No ${blockType} found within ${maxDistance} blocks`); } return createResponse(`Found ${blockType} at position (${block.position.x}, ${block.position.y}, ${block.position.z})`); } catch (error) { return createErrorResponse(error as Error); } } ); } // ========== Entity Interaction Tools ========== function registerEntityTools(server: McpServer, bot: mineflayer.Bot) { server.tool( "find-entity", "Find the nearest entity of a specific type", { type: z.string().optional().describe("Type of entity to find (empty for any entity)"), maxDistance: z.number().optional().describe("Maximum search distance (default: 16)") }, async ({ type = '', maxDistance = 16 }): Promise<McpResponse> => { try { const entityFilter = (entity: any) => { if (!type) return true; if (type === 'player') return entity.type === 'player'; if (type === 'mob') return entity.type === 'mob'; return entity.name && entity.name.includes(type.toLowerCase()); }; const entity = bot.nearestEntity(entityFilter); if (!entity || bot.entity.position.distanceTo(entity.position) > maxDistance) { return createResponse(`No ${type || 'entity'} found within ${maxDistance} blocks`); } return createResponse(`Found ${entity.name || (entity as any).username || entity.type} at position (${Math.floor(entity.position.x)}, ${Math.floor(entity.position.y)}, ${Math.floor(entity.position.z)})`); } catch (error) { return createErrorResponse(error as Error); } } ); } // ========== Chat Tools ========== function registerChatTools(server: McpServer, bot: mineflayer.Bot) { server.tool( "send-chat", "Send a chat message in-game", { message: z.string().describe("Message to send in chat") }, async ({ message }): Promise<McpResponse> => { try { bot.chat(message); return createResponse(`Sent message: "${message}"`); } catch (error) { return createErrorResponse(error as Error); } } ); server.tool( "read-chat", "Get recent chat messages from players", { count: z.number().optional().describe("Number of recent messages to retrieve (default: 10, max: 100)") }, async ({ count = 10 }): Promise<McpResponse> => { try { const maxCount = Math.min(count, MAX_STORED_MESSAGES); const messages = messageStore.getRecentMessages(maxCount); if (messages.length === 0) { return createResponse("No chat messages found"); } let output = `Found ${messages.length} chat message(s):\n\n`; messages.forEach((msg, index) => { const timestamp = new Date(msg.timestamp).toISOString(); output += `${index + 1}. ${timestamp} - ${msg.username}: ${msg.content}\n`; }); return createResponse(output); } catch (error) { return createErrorResponse(error as Error); } } ); } // ========== Flight Tools ========== function registerFlightTools(server: McpServer, bot: mineflayer.Bot) { server.tool( "fly-to", "Make the bot fly to a specific position", { x: z.number().describe("X coordinate"), y: z.number().describe("Y coordinate"), z: z.number().describe("Z coordinate") }, async ({ x, y, z }): Promise<McpResponse> => { if (!bot.creative) { return createResponse("Creative mode is not available. Cannot fly."); } const controller = new AbortController(); const FLIGHT_TIMEOUT_MS = 20000; const timeoutId = setTimeout(() => { if (!controller.signal.aborted) { controller.abort(); } }, FLIGHT_TIMEOUT_MS); try { const destination = new Vec3(x, y, z); await createCancellableFlightOperation(bot, destination, controller); return createResponse(`Successfully flew to position (${x}, ${y}, ${z}).`); } catch (error) { if (controller.signal.aborted) { const currentPosAfterTimeout = bot.entity.position; return createErrorResponse( `Flight timed out after ${FLIGHT_TIMEOUT_MS / 1000} seconds. The destination may be unreachable. ` + `Current position: (${Math.floor(currentPosAfterTimeout.x)}, ${Math.floor(currentPosAfterTimeout.y)}, ${Math.floor(currentPosAfterTimeout.z)})` ); } log('error', `Flight error: ${formatError(error)}`); return createErrorResponse(error as Error); } finally { clearTimeout(timeoutId); bot.creative.stopFlying(); } } ); } function createCancellableFlightOperation( bot: mineflayer.Bot, destination: Vec3, controller: AbortController ): Promise<boolean> { return new Promise((resolve, reject) => { let aborted = false; controller.signal.addEventListener('abort', () => { aborted = true; bot.creative.stopFlying(); reject(new Error("Flight operation cancelled")); }); bot.creative.flyTo(destination) .then(() => { if (!aborted) { resolve(true); } }) .catch((err: any) => { if (!aborted) { reject(err); } }); }); } // ========== Game State Tools ============ function registerGameStateTools(server: McpServer, bot: mineflayer.Bot) { server.tool( "detect-gamemode", "Detect the gamemode on game", {}, async (): Promise<McpResponse> => { try { return createResponse(`Bot gamemode: "${bot.game.gameMode}"`); } catch (error) { return createErrorResponse(error as Error); } } ); } // ========== Main Application ========== async function main() { let bot: mineflayer.Bot | undefined; try { // Parse command line arguments const argv = parseCommandLineArgs(); // Set up the Minecraft bot bot = setupBot(argv); // Create and configure MCP server const server = createMcpServer(bot); // Handle stdin end - this will detect when MCP Client is closed process.stdin.on('end', () => { if (bot) bot.quit(); log('info', 'MCP Client has disconnected. Shutting down...'); process.exit(0); }); // Connect to the transport const transport = new StdioServerTransport(); await server.connect(transport); } catch (error) { if (bot) bot.quit(); log('error', `Failed to start server: ${formatError(error)}`); process.exit(1); } } // Start the application main().catch((error) => { log('error', `Fatal error in main(): ${formatError(error)}`); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/yuniko-software/minecraft-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server