mcp-minecraft
by yuniko-software
Verified
#!/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';
// ========== 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();
}
// ========== Response Helpers ==========
function createResponse(text: string): McpResponse {
return {
content: [{ type: "text", text }]
};
}
function createErrorResponse(error: Error | string): McpResponse {
const errorMessage = typeof error === 'string' ? error : error.message;
console.error(`Error: ${errorMessage}`);
return {
content: [{ type: "text", text: `Failed: ${errorMessage}` }],
isError: true
};
}
// ========== Bot Setup ==========
function setupBot(argv: any) {
// Configure bot options based on command line arguments
const botOptions = {
host: argv.host,
port: argv.port,
username: argv.username,
plugins: { pathfinder }
};
// Log connection information
console.error(`Connecting to Minecraft server at ${argv.host}:${argv.port} as ${argv.username}`);
// Create a bot instance
const bot = mineflayer.createBot(botOptions);
// Set up the bot when it spawns
bot.once('spawn', async () => {
console.error('Bot has spawned in the world');
// Set up pathfinder movements
const mcData = minecraftData(bot.version);
const defaultMove = new Movements(bot, mcData);
bot.pathfinder.setMovements(defaultMove);
bot.chat('Claude-powered bot ready to receive instructions!');
});
// Register common event handlers
bot.on('chat', (username, message) => {
if (username === bot.username) return;
console.error(`[CHAT] ${username}: ${message}`);
});
bot.on('kicked', (reason) => {
console.error(`Bot was kicked: ${reason}`);
});
bot.on('error', (err) => {
console.error(`Bot error: ${err.message}`);
});
return bot;
}
// ========== MCP Server Configuration ==========
function createMcpServer(bot: any) {
const server = new McpServer({
name: "minecraft-bot",
version: "1.0.0",
});
// Register all tool categories
registerPositionTools(server, bot);
registerInventoryTools(server, bot);
registerBlockTools(server, bot);
registerEntityTools(server, bot);
registerChatTools(server, bot);
return server;
}
// ========== Position and Movement Tools ==========
function registerPositionTools(server: McpServer, bot: any) {
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: any) {
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: any) {
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) {
console.error(`Failed to place using ${face.direction} face: ${(placeError as Error).message}`);
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: any) {
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 Tool ==========
function registerChatTools(server: McpServer, bot: any) {
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);
}
}
);
}
// ========== 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 Claude Desktop is closed
process.stdin.on('end', () => {
console.error("Claude has disconnected. Shutting down...");
if (bot) {
bot.quit();
}
process.exit(0);
});
// Connect to the transport
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Minecraft MCP Server running on stdio");
} catch (error) {
console.error("Failed to start server:", error);
if (bot) bot.quit();
process.exit(1);
}
}
// Start the application
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});