# Directory Structure
```
├── .gitignore
├── LICENSE
├── package.json
├── public
│ ├── focus.png
│ ├── overview.jpeg
│ └── screenshot.png
├── README.md
├── src
│ ├── Application.ts
│ ├── index.ts
│ ├── MCPHandler.ts
│ ├── ProtocolHandler.ts
│ ├── ServerManager.ts
│ └── types
│ └── config.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Build output
/dist
# Dependencies
/node_modules
/libraries
# Server files
/versions
/world
/logs
/minecraft-server
# Configuration files
banned-ips.json
banned-players.json
ops.json
package-lock.json
server.properties
usercache.json
whitelist.json
eula.txt
# IDE and system files
.DS_Store
.env
*.log
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Minecraft MCP Integration
A Model Context Protocol (MCP) integration for Minecraft that enables AI assistants to interact with a Minecraft server. This integration allows AI models to observe and interact with the Minecraft world through a bot.

## Prerequisites
1. Minecraft Launcher
2. Node.js 18 or higher
3. Claude Desktop App
4. Java 21.0.5 (recommended)
> ⚠️ Note: Currently only tested on macOS/Linux. Windows compatibility is not guaranteed.
## Important Note
1. **Use the F3+P Shortcut**:
Press F3 + P together. This toggles the "Pause on Lost Focus" feature. Once turned off, you can switch to claude desktop and Minecraft will continue running without pausing.

2. **Connection Issues on Claude Restart**:
If you restart Claude while the Minecraft server is running, you may experience MCP connection issues on the next claude launch due to lingering java process. See [Troubleshooting: MCP Connection Failed](#common-issues) for resolution steps.
## Installation Steps
1. **Download and Setup Minecraft Server**
- Download Minecraft server v1.21 from [mcversions.net/1.21](https://mcversions.net/download/1.21)
- Install Java 21.0.5 if not already installed (other versions are untested)
- Create a dedicated directory (e.g., `~/minecraft-server/`)
- Place the downloaded `server.jar` file in this directory
- Note down the absolute path to your `server.jar` file
2. **Install and Configure MCP Integration**
Quick Install (Recommended):
```bash
npx -y @smithery/cli install mcp-minecraft --client claude
```
Follow the CLI prompts to complete the setup.
Or Manual Setup:
- Navigate to `~/Library/Application Support/Claude/claude_desktop_config.json`
- Add the MCP server configuration:
```json
{
"mcpServers": {
"mcp-minecraft": {
"command": "npx",
"args": [
"-y",
"mcp-minecraft@latest",
"--server-jar",
"/absolute/path/to/minecraft-server/server.jar"
]
}
}
}
```
> ⚠️ Replace `/absolute/path/to/minecraft-server/server.jar` with your actual server.jar path
4. **Launch Claude Desktop**
- Start Claude Desktop after completing the configuration
5. **Connect to Server**
- Open Minecraft Launcher
- Install and launch Minecraft Java Edition **v1.21**
- Click "Play" and Select "Multiplayer"
- Click "Add Server"
- Enter server details:
- Server Name: `Minecraft Server`
- Server Address: `localhost:25565`
- Click "Done"
## Features
### Resources
The integration exposes these MCP resources:
- `minecraft://bot/location` - Current bot position in the world
- `minecraft://bot/status` - Bot connection status
### Tools
Available MCP tools:
- `chat` - Send chat messages to the server
- `jump` - Make the bot jump
- `moveForward` - Make the bot move forward
- `moveBack` - Make the bot move backward
- `turnLeft` - Make the bot turn left
- `turnRight` - Make the bot turn right
- `placeBlock` - Place a block at specified coordinates
- `digBlock` - Break a block at specified coordinates
- `getBlockInfo` - Get information about a block at specified coordinates
- `selectSlot` - Select a hotbar slot (0-8)
- `getInventory` - Get contents of bot's inventory
- `equipItem` - Equip an item by name to specified destination
- `getStatus` - Get bot's current status (health, food, position, etc.)
- `getNearbyEntities` - Get list of nearby entities within range
- `attack` - Attack a nearby entity by name
- `useItem` - Use/activate the currently held item
- `stopUsingItem` - Stop using/deactivate the current item
- `lookAt` - Make the bot look at specific coordinates
- `followPlayer` - Follow a specific player
- `stopFollowing` - Stop following current target
- `goToPosition` - Navigate to specific coordinates
## Technical Details
- Server runs in offline mode for local development
- Default memory allocation: 2GB
- Default port: 25565
- Bot username: MCPBot
## Troubleshooting
### Common Issues
1. **MCP Connection Failed**
- Look for lingering Java processes
- Terminate them manually:
- Windows: Use Task Manager (untested)
- Mac/Linux:
- Go to 'Activity Monitor' and 'Force Quit' java
- Restart computer if process termination fails
- Note: Latest version should auto-resolve these issues
2. **Server Won't Start**
- Verify Java is installed
- Check server.jar path is correct
- Ensure port 25565 is available
3. **Can't Connect to Server**
- Verify server is running (check logs)
- Confirm you're using "localhost" as server address
- Check firewall settings
### Logs Location
- Minecraft Server logs: Check the minecraft-server directory
- Claude Desktop logs: `~/Library/Logs/Claude/mcp*.log`
## Contributing
Contributions, big or small, are welcome!
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
```
--------------------------------------------------------------------------------
/src/types/config.ts:
--------------------------------------------------------------------------------
```typescript
export interface MinecraftServerConfig {
maxPlayers: number;
port: number;
serverJarPath: string;
memoryAllocation: string;
username: string;
version: string;
}
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"ts-node": {
"esm": true,
"experimentalSpecifierResolution": "node"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "mcp-minecraft",
"version": "1.0.34",
"type": "module",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node --loader ts-node/esm src/index.ts",
"dev": "ts-node-dev --respawn --transpile-only --esm src/index.ts",
"build": "tsc"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@modelcontextprotocol/sdk": "latest",
"@types/node": "^22.10.2",
"minecraft-protocol": "^1.51.0",
"mineflayer": "^4.23.0",
"mineflayer-pathfinder": "^2.4.5",
"ts-node": "^10.9.2",
"typescript": "^5.7.2",
"yargs": "^17.7.2"
},
"devDependencies": {
"@types/yargs": "^17.0.33",
"ts-node-dev": "^2.0.0"
},
"bin": {
"minecraft-mcp": "./dist/index.js"
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { Application } from './Application.js';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import * as fs from 'fs';
const argv = yargs(hideBin(process.argv))
.option('server-jar', {
alias: 'j',
type: 'string',
description: 'Absolute path to the Minecraft server JAR file',
demandOption: true
})
.scriptName('minecraft-mcp')
.help()
.parseSync();
if (!fs.existsSync(argv.serverJar)) {
process.exit(1);
}
const app = new Application({
serverJarPath: argv.serverJar
});
const cleanup = async () => {
try {
await app.stop();
process.exit(0);
} catch (error) {
process.exit(1);
}
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
process.on('uncaughtException', async (error) => {
await cleanup();
});
process.on('unhandledRejection', async (error) => {
await cleanup();
});
app.start().catch(() => {
process.exit(1);
});
```
--------------------------------------------------------------------------------
/src/Application.ts:
--------------------------------------------------------------------------------
```typescript
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { ServerManager } from "./ServerManager.js";
import { ProtocolHandler } from "./ProtocolHandler.js";
import { MCPHandler } from "./MCPHandler.js";
import path from 'path';
interface ApplicationConfig {
serverJarPath: string;
}
export class Application {
private serverManager: ServerManager;
private protocolHandler: ProtocolHandler;
private mcpHandler: MCPHandler;
private transport: StdioServerTransport | null = null;
constructor(config: ApplicationConfig) {
const serverPath = path.resolve(config.serverJarPath);
// Initialize with config from CLI
this.serverManager = new ServerManager({
maxPlayers: 10,
port: 25565,
serverJarPath: serverPath,
memoryAllocation: '2G',
username: 'MCPBot',
version: '1.21'
});
this.protocolHandler = new ProtocolHandler({
host: 'localhost',
port: 25565,
username: 'MCPBot',
version: '1.21'
});
this.mcpHandler = new MCPHandler(this.protocolHandler);
this.setupEventHandlers();
}
private setupEventHandlers(): void {
this.serverManager.on('log', (message) => {
});
this.serverManager.on('error', (error) => {
});
this.protocolHandler.on('chat', ({ username, message }) => {
});
this.protocolHandler.on('error', (error) => {
});
process.on('SIGINT', async () => {
await this.shutdown();
process.exit(0);
});
}
public async start(): Promise<void> {
try {
// Start MCP server first - use only stdout for MCP communication
this.transport = new StdioServerTransport();
await this.mcpHandler.getServer().connect(this.transport);
// Start Minecraft server
await this.serverManager.start();
// Wait a bit for the server to initialize
await new Promise(resolve => setTimeout(resolve, 5000));
// Connect bot
await this.protocolHandler.connect();
} catch (error) {
await this.shutdown();
process.exit(1);
}
}
public async shutdown(): Promise<void> {
if (this.mcpHandler && this.transport) {
this.transport = null;
}
await this.protocolHandler.disconnect();
await this.serverManager.stop();
}
async stop(): Promise<void> {
try {
// Disconnect MCP server
if (this.mcpHandler && this.transport) {
await this.mcpHandler.getServer().close();
this.transport = null;
}
// Disconnect bot
await this.protocolHandler.disconnect();
// Stop Minecraft server
await this.serverManager.stop();
} catch (error) {
throw error;
}
}
}
```
--------------------------------------------------------------------------------
/src/ServerManager.ts:
--------------------------------------------------------------------------------
```typescript
import { spawn, ChildProcess } from 'child_process';
import { EventEmitter } from 'events';
import { MinecraftServerConfig } from './types/config.js';
import * as fs from 'fs';
import path from 'path';
import * as os from 'os';
export class ServerManager extends EventEmitter {
private process: ChildProcess | null = null;
private config: MinecraftServerConfig;
private isRunning: boolean = false;
constructor(config: MinecraftServerConfig) {
super();
this.config = this.validateConfig(config);
process.on('exit', () => {
this.killProcess();
});
process.on('SIGTERM', () => {
this.killProcess();
});
process.on('SIGINT', () => {
this.killProcess();
});
}
private killProcess(): void {
if (this.process) {
try {
process.kill(-this.process.pid!, 'SIGKILL');
} catch (error) {
// Ignore errors during force kill
}
this.process = null;
this.isRunning = false;
}
}
private validateConfig(config: MinecraftServerConfig): MinecraftServerConfig {
if (!config.serverJarPath) {
throw new Error('Server JAR path is required');
}
if (!config.port || config.port < 1 || config.port > 65535) {
throw new Error('Invalid port number');
}
return {
maxPlayers: config.maxPlayers || 20,
port: config.port,
serverJarPath: config.serverJarPath,
memoryAllocation: config.memoryAllocation || '2G',
username: config.username || 'MCPBot',
version: config.version || '1.21'
};
}
private ensureEulaAccepted(): void {
// Get the directory containing the server JAR
const serverDir = path.dirname(this.config.serverJarPath);
const eulaPath = path.join(serverDir, 'eula.txt');
// Create or update eula.txt
fs.writeFileSync(eulaPath, 'eula=true', 'utf8');
}
private ensureServerProperties(): void {
const serverDir = path.dirname(this.config.serverJarPath);
const propsPath = path.join(serverDir, 'server.properties');
let properties = '';
if (fs.existsSync(propsPath)) {
properties = fs.readFileSync(propsPath, 'utf8');
}
// Define our server properties for a simple plains world
const serverProperties = {
'online-mode': 'false',
'level-type': 'flat',
'spawn-protection': '0',
'difficulty': 'peaceful', // No hostile mobs
'spawn-monsters': 'false', // Disable monster spawning
'spawn-animals': 'true', // Enable animal spawning
'spawn-npcs': 'false', // Disable villagers
'generate-structures': 'false', // Disable structures (villages, temples, etc)
'allow-nether': 'false', // Disable nether
'gamemode': 'creative', // Set creative mode for easier building
'do-daylight-cycle': 'false',
'max-players': this.config.maxPlayers.toString(),
'server-port': this.config.port.toString(),
'motd': 'Peaceful Plains Server'
};
// Update or create each property
for (const [key, value] of Object.entries(serverProperties)) {
const regex = new RegExp(`^${key}=.*$`, 'm');
if (properties.match(regex)) {
properties = properties.replace(regex, `${key}=${value}`);
} else {
properties += `\n${key}=${value}`;
}
}
fs.writeFileSync(propsPath, properties.trim(), 'utf8');
}
private normalizePath(p: string): string {
return path.normalize(p).toLowerCase();
}
private expandHome(filepath: string): string {
if (filepath.startsWith("~/") || filepath === "~") {
return path.join(os.homedir(), filepath.slice(1));
}
return filepath;
}
private validateServerPath(): string {
const expandedPath = this.expandHome(this.config.serverJarPath);
const absolutePath = path.isAbsolute(expandedPath)
? path.resolve(expandedPath)
: path.resolve(process.cwd(), expandedPath);
if (!fs.existsSync(absolutePath)) {
throw new Error(`Server JAR not found at path: ${absolutePath}`);
}
return absolutePath;
}
public async start(): Promise<void> {
if (this.isRunning) {
throw new Error('Server is already running');
}
return new Promise((resolve, reject) => {
try {
const serverJarPath = this.validateServerPath();
const serverDir = path.dirname(serverJarPath);
this.ensureEulaAccepted();
this.ensureServerProperties();
this.process = spawn('java', [
`-Xmx${this.config.memoryAllocation}`,
`-Xms${this.config.memoryAllocation}`,
'-jar',
serverJarPath,
'nogui'
], {
cwd: serverDir,
stdio: ['pipe', 'pipe', 'pipe'],
detached: true,
...(process.platform !== 'win32' && { pid: true })
});
const timeout = setTimeout(() => {
reject(new Error('Server startup timed out'));
this.stop();
}, 60000);
this.process.stdout?.on('data', (data: Buffer) => {
const message = data.toString();
if (message.includes('Done')) {
clearTimeout(timeout);
this.isRunning = true;
resolve();
}
});
this.process.stderr?.on('data', (data: Buffer) => {
const error = data.toString();
if (error.includes('Error')) {
reject(new Error(error));
}
});
this.process.on('close', (code) => {
this.isRunning = false;
});
this.process.on('error', (err) => {
this.isRunning = false;
reject(err);
});
} catch (error) {
reject(error);
}
});
}
public async stop(): Promise<void> {
if (!this.isRunning || !this.process) {
return;
}
return new Promise((resolve) => {
const forceKillTimeout = setTimeout(() => {
this.killProcess();
resolve();
}, 10000);
this.process?.once('close', () => {
clearTimeout(forceKillTimeout);
this.isRunning = false;
this.process = null;
resolve();
});
if (this.process?.stdin) {
this.process.stdin.write('stop\n');
} else {
this.killProcess();
resolve();
}
});
}
public isServerRunning(): boolean {
return this.isRunning;
}
}
```
--------------------------------------------------------------------------------
/src/ProtocolHandler.ts:
--------------------------------------------------------------------------------
```typescript
import * as mineflayer from 'mineflayer';
import { EventEmitter } from 'events';
import { Vec3 } from 'vec3';
import pathfinderPkg from 'mineflayer-pathfinder';
const { pathfinder, Movements, goals } = pathfinderPkg;
export interface BotConfig {
host: string;
port: number;
username: string;
version: string;
}
export class ProtocolHandler extends EventEmitter {
private bot: mineflayer.Bot | null = null;
private config: BotConfig;
constructor(config: BotConfig) {
super();
this.config = config;
}
public async connect(): Promise<void> {
if (this.bot) {
throw new Error('Bot is already connected');
}
return new Promise((resolve, reject) => {
try {
this.bot = mineflayer.createBot({
host: this.config.host,
port: this.config.port,
username: this.config.username,
version: this.config.version
});
this.bot.once('spawn', () => {
this.bot?.loadPlugin(pathfinder);
if (this.bot?.pathfinder) {
this.bot.pathfinder.setMovements(new Movements(this.bot));
}
this.setupEventHandlers();
this.emit('connected');
resolve();
});
this.bot.on('error', (error) => {
this.emit('error', error);
reject(error);
});
} catch (error) {
reject(error);
}
});
}
private setupEventHandlers(): void {
if (!this.bot) return;
this.bot.on('chat', (username, message) => {
this.emit('chat', { username, message });
});
this.bot.on('kicked', (reason) => {
this.emit('kicked', reason);
});
this.bot.on('error', (error) => {
this.emit('error', error);
});
}
public async sendChat(message: string): Promise<void> {
if (!this.bot) throw new Error('Bot not connected');
await this.bot.chat(message);
}
public async jump(): Promise<void> {
if (!this.bot) throw new Error('Bot not connected');
this.bot.setControlState('jump', true);
setTimeout(() => {
if (this.bot) this.bot.setControlState('jump', false);
}, 500);
}
public getPosition(): Vec3 | null {
if (!this.bot || !this.bot.entity) return null;
return this.bot.entity.position;
}
public async disconnect(): Promise<void> {
if (!this.bot) return;
return new Promise((resolve) => {
const bot = this.bot;
if (!bot) {
resolve();
return;
}
bot.removeAllListeners();
bot.once('end', () => {
this.bot = null;
setTimeout(() => {
process.exit(0);
}, 1000);
resolve();
});
bot.end();
});
}
public isConnected(): boolean {
return this.bot !== null;
}
public async moveForward(): Promise<void> {
if (!this.bot) throw new Error('Bot not connected');
this.bot.setControlState('forward', true);
await new Promise(resolve => setTimeout(resolve, 1000));
this.bot.setControlState('forward', false);
}
public async moveBack(): Promise<void> {
if (!this.bot) throw new Error('Bot not connected');
this.bot.setControlState('back', true);
await new Promise(resolve => setTimeout(resolve, 1000));
this.bot.setControlState('back', false);
}
public async turnLeft(): Promise<void> {
if (!this.bot) throw new Error('Bot not connected');
this.bot.setControlState('left', true);
await new Promise(resolve => setTimeout(resolve, 500));
this.bot.setControlState('left', false);
}
public async turnRight(): Promise<void> {
if (!this.bot) throw new Error('Bot not connected');
this.bot.setControlState('right', true);
await new Promise(resolve => setTimeout(resolve, 500));
this.bot.setControlState('right', false);
}
public async placeBlock(x: number, y: number, z: number): Promise<void> {
if (!this.bot) throw new Error('Bot not connected');
try {
const targetPos = new Vec3(x, y, z);
const faceVector = new Vec3(0, 1, 0);
const referenceBlock = await this.bot.blockAt(targetPos);
if (!referenceBlock) throw new Error('No reference block found');
await this.bot.placeBlock(referenceBlock, faceVector);
} catch (error) {
throw new Error(`Failed to place block: ${error}`);
}
}
public async digBlock(x: number, y: number, z: number): Promise<void> {
if (!this.bot) throw new Error('Bot not connected');
try {
const targetPos = new Vec3(x, y, z);
const block = await this.bot.blockAt(targetPos);
if (!block) throw new Error('No block at target position');
if (block.name === 'air') throw new Error('Cannot dig air');
await this.bot.dig(block);
} catch (error) {
throw new Error(`Failed to dig block: ${error}`);
}
}
public async getBlockInfo(x: number, y: number, z: number): Promise<any> {
if (!this.bot) throw new Error('Bot not connected');
try {
const targetPos = new Vec3(x, y, z);
const block = await this.bot.blockAt(targetPos);
if (!block) throw new Error('No block at target position');
return {
name: block.name,
type: block.type,
position: {
x: block.position.x,
y: block.position.y,
z: block.position.z
},
hardness: block.hardness
};
} catch (error) {
throw new Error(`Failed to get block info: ${error}`);
}
}
public async selectSlot(slot: number): Promise<void> {
if (!this.bot) throw new Error('Bot not connected');
if (slot < 0 || slot > 8) throw new Error('Slot must be between 0 and 8');
try {
await this.bot.setQuickBarSlot(slot);
} catch (error) {
throw new Error(`Failed to select slot: ${error}`);
}
}
public async getInventory(): Promise<any> {
if (!this.bot) throw new Error('Bot not connected');
const items = this.bot.inventory.items();
return items.map(item => ({
name: item.name,
count: item.count,
slot: item.slot,
displayName: item.displayName
}));
}
public async equipItem(itemName: string, destination?: string): Promise<void> {
if (!this.bot) throw new Error('Bot not connected');
try {
const item = this.bot.inventory.items().find(item => item.name.includes(itemName));
if (!item) throw new Error(`Item ${itemName} not found in inventory`);
const equipDestination: mineflayer.EquipmentDestination | null = destination as mineflayer.EquipmentDestination || null;
await this.bot.equip(item, equipDestination);
} catch (error) {
throw new Error(`Failed to equip item: ${error}`);
}
}
public async getStatus(): Promise<any> {
if (!this.bot) throw new Error('Bot not connected');
return {
health: this.bot.health,
food: this.bot.food,
gameMode: this.bot.game?.gameMode ?? 'unknown',
position: this.getPosition(),
isRaining: this.bot.isRaining,
time: {
timeOfDay: this.bot.time?.timeOfDay ?? 0,
day: this.bot.time?.day ?? 0
}
};
}
public async getNearbyEntities(range: number = 10): Promise<any[]> {
if (!this.bot) throw new Error('Bot not connected');
return Object.values(this.bot.entities)
.filter(entity => {
if (!entity.position || !this.bot?.entity?.position) return false;
return entity.position.distanceTo(this.bot.entity.position) <= range;
})
.map(entity => ({
name: entity.name,
type: entity.type,
position: entity.position,
distance: entity.position && this.bot?.entity?.position
? entity.position.distanceTo(this.bot.entity.position)
: null // Handle the case where position might be null
}));
}
public async attack(entityName: string): Promise<void> {
if (!this.bot) throw new Error('Bot not connected');
try {
const entity = Object.values(this.bot.entities)
.find(e => e.name === entityName &&
e.position.distanceTo(this.bot!.entity.position) <= 4);
if (!entity) throw new Error('Entity not found or too far');
await this.bot.attack(entity);
} catch (error) {
throw new Error(`Failed to attack: ${error}`);
}
}
public async useItem(hand: 'right' | 'left' = 'right'): Promise<void> {
if (!this.bot) throw new Error('Bot not connected');
try {
await this.bot.activateItem(hand === 'right');
} catch (error) {
throw new Error(`Failed to use item: ${error}`);
}
}
public async stopUsingItem(): Promise<void> {
if (!this.bot) throw new Error('Bot not connected');
try {
await this.bot.deactivateItem();
} catch (error) {
throw new Error(`Failed to stop using item: ${error}`);
}
}
public async lookAt(x: number, y: number, z: number): Promise<void> {
if (!this.bot) throw new Error('Bot not connected');
try {
await this.bot.lookAt(new Vec3(x, y, z));
} catch (error) {
throw new Error(`Failed to look at position: ${error}`);
}
}
public async followPlayer(playerName: string): Promise<void> {
if (!this.bot) throw new Error('Bot not connected');
try {
const player = this.bot.players[playerName]?.entity;
if (!player) throw new Error('Player not found');
// Follow at 2 blocks distance
await this.bot.pathfinder.goto(
new goals.GoalFollow(player, 2)
);
} catch (error) {
throw new Error(`Failed to follow player: ${error}`);
}
}
public async stopFollowing(): Promise<void> {
if (!this.bot) throw new Error('Bot not connected');
this.bot.pathfinder.stop();
}
public async goToPosition(x: number, y: number, z: number): Promise<void> {
if (!this.bot) throw new Error('Bot not connected');
try {
await this.bot.pathfinder.goto(
new goals.GoalBlock(x, y, z)
);
} catch (error) {
throw new Error(`Failed to go to position: ${error}`);
}
}
}
```
--------------------------------------------------------------------------------
/src/MCPHandler.ts:
--------------------------------------------------------------------------------
```typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListToolsRequestSchema,
CallToolRequestSchema
} from "@modelcontextprotocol/sdk/types.js";
import { ProtocolHandler } from "./ProtocolHandler.js";
import { ReadResourceRequest, CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
export class MCPHandler {
private server: Server;
private protocolHandler: ProtocolHandler;
constructor(protocolHandler: ProtocolHandler) {
this.protocolHandler = protocolHandler;
this.server = new Server({
name: "minecraft-mcp-server",
version: "1.0.0"
}, {
capabilities: {
resources: {},
tools: {}
}
});
this.setupHandlers();
}
private setupHandlers(): void {
this.setupResourceHandlers();
this.setupToolHandlers();
}
private setupResourceHandlers(): void {
// List available resources
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
try {
return {
resources: [
{
uri: "minecraft://bot/location",
name: "Bot Location",
mimeType: "application/json",
description: "Current bot location in the Minecraft world"
},
{
uri: "minecraft://bot/status",
name: "Bot Status",
mimeType: "application/json",
description: "Current status of the bot"
}
]
};
} catch (error) {
throw error;
}
});
// Handle resource reading
this.server.setRequestHandler(ReadResourceRequestSchema, async (request: ReadResourceRequest) => {
try {
switch (request.params.uri) {
case "minecraft://bot/location": {
const pos = this.protocolHandler.getPosition();
if (!pos) throw new Error("Position not available");
return {
contents: [{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify({
x: Math.round(pos.x * 100) / 100,
y: Math.round(pos.y * 100) / 100,
z: Math.round(pos.z * 100) / 100
})
}]
};
}
case "minecraft://bot/status": {
return {
contents: [{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify({
connected: this.protocolHandler.isConnected()
})
}]
};
}
default:
throw new Error(`Unknown resource: ${request.params.uri}`);
}
} catch (error) {
throw error;
}
});
}
private setupToolHandlers(): void {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
try {
return {
tools: [
{
name: "chat",
description: "Send a chat message",
inputSchema: {
type: "object",
properties: {
message: { type: "string" }
},
required: ["message"]
}
},
{
name: "jump",
description: "Make the bot jump",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "moveForward",
description: "Make the bot move forward",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "moveBack",
description: "Make the bot move backward",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "turnLeft",
description: "Make the bot turn left",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "turnRight",
description: "Make the bot turn right",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "placeBlock",
description: "Place a block at specified coordinates",
inputSchema: {
type: "object",
properties: {
x: { type: "number" },
y: { type: "number" },
z: { type: "number" }
},
required: ["x", "y", "z"]
}
},
{
name: "digBlock",
description: "Break a block at specified coordinates",
inputSchema: {
type: "object",
properties: {
x: { type: "number" },
y: { type: "number" },
z: { type: "number" }
},
required: ["x", "y", "z"]
}
},
{
name: "getBlockInfo",
description: "Get information about a block at specified coordinates",
inputSchema: {
type: "object",
properties: {
x: { type: "number" },
y: { type: "number" },
z: { type: "number" }
},
required: ["x", "y", "z"]
}
},
{
name: "selectSlot",
description: "Select a hotbar slot (0-8)",
inputSchema: {
type: "object",
properties: {
slot: {
type: "number",
minimum: 0,
maximum: 8
}
},
required: ["slot"]
}
},
{
name: "getInventory",
description: "Get contents of bot's inventory",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "equipItem",
description: "Equip an item by name",
inputSchema: {
type: "object",
properties: {
itemName: { type: "string" },
destination: {
type: "string",
enum: ["hand", "head", "torso", "legs", "feet"]
}
},
required: ["itemName"]
}
},
{
name: "getStatus",
description: "Get bot's current status including health, food, position, etc.",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "getNearbyEntities",
description: "Get list of nearby entities within specified range",
inputSchema: {
type: "object",
properties: {
range: {
type: "number",
minimum: 1,
maximum: 100,
default: 10
}
}
}
},
{
name: "attack",
description: "Attack a nearby entity by name",
inputSchema: {
type: "object",
properties: {
entityName: { type: "string" }
},
required: ["entityName"]
}
},
{
name: "useItem",
description: "Use/activate the currently held item",
inputSchema: {
type: "object",
properties: {
hand: {
type: "string",
enum: ["right", "left"],
default: "right"
}
}
}
},
{
name: "stopUsingItem",
description: "Stop using/deactivate the current item",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "lookAt",
description: "Make the bot look at specific coordinates",
inputSchema: {
type: "object",
properties: {
x: { type: "number" },
y: { type: "number" },
z: { type: "number" }
},
required: ["x", "y", "z"]
}
},
{
name: "followPlayer",
description: "Follow a specific player",
inputSchema: {
type: "object",
properties: {
playerName: { type: "string" }
},
required: ["playerName"]
}
},
{
name: "stopFollowing",
description: "Stop following current target",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "goToPosition",
description: "Navigate to specific coordinates",
inputSchema: {
type: "object",
properties: {
x: { type: "number" },
y: { type: "number" },
z: { type: "number" }
},
required: ["x", "y", "z"]
}
}
]
};
} catch (error) {
throw error;
}
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => {
try {
switch (request.params.name) {
case "chat":
if (request.params.arguments && request.params.arguments.message) {
await this.protocolHandler.sendChat(request.params.arguments.message as string);
return {
content: [{
type: "text",
text: "Message sent"
}]
};
} else {
throw new Error("Invalid arguments for 'chat' tool");
}
case "jump":
await this.protocolHandler.jump();
return {
content: [{
type: "text",
text: "Jumped!"
}]
};
case "moveForward":
await this.protocolHandler.moveForward();
return {
content: [{ type: "text", text: "Moved forward" }]
};
case "moveBack":
await this.protocolHandler.moveBack();
return {
content: [{ type: "text", text: "Moved backward" }]
};
case "turnLeft":
await this.protocolHandler.turnLeft();
return {
content: [{ type: "text", text: "Turned left" }]
};
case "turnRight":
await this.protocolHandler.turnRight();
return {
content: [{ type: "text", text: "Turned right" }]
};
case "placeBlock": {
const { x, y, z } = request.params.arguments as { x: number, y: number, z: number };
await this.protocolHandler.placeBlock(x, y, z);
return {
content: [{ type: "text", text: `Placed block at (${x}, ${y}, ${z})` }]
};
}
case "digBlock": {
const { x, y, z } = request.params.arguments as { x: number, y: number, z: number };
await this.protocolHandler.digBlock(x, y, z);
return {
content: [{ type: "text", text: `Broke block at (${x}, ${y}, ${z})` }]
};
}
case "getBlockInfo": {
const { x, y, z } = request.params.arguments as { x: number, y: number, z: number };
const blockInfo = await this.protocolHandler.getBlockInfo(x, y, z);
return {
content: [{ type: "text", text: JSON.stringify(blockInfo, null, 2) }]
};
}
case "selectSlot": {
const { slot } = request.params.arguments as { slot: number };
await this.protocolHandler.selectSlot(slot);
return {
content: [{ type: "text", text: `Selected slot ${slot}` }]
};
}
case "getInventory": {
const inventory = await this.protocolHandler.getInventory();
return {
content: [{ type: "text", text: JSON.stringify(inventory, null, 2) }]
};
}
case "equipItem": {
const { itemName, destination } = request.params.arguments as {
itemName: string,
destination?: string
};
await this.protocolHandler.equipItem(itemName, destination);
return {
content: [{
type: "text",
text: `Equipped ${itemName}${destination ? ` to ${destination}` : ''}`
}]
};
}
case "getStatus": {
const status = await this.protocolHandler.getStatus();
return {
content: [{ type: "text", text: JSON.stringify(status, null, 2) }]
};
}
case "getNearbyEntities": {
const { range } = request.params.arguments as { range?: number };
const entities = await this.protocolHandler.getNearbyEntities(range);
return {
content: [{ type: "text", text: JSON.stringify(entities, null, 2) }]
};
}
case "attack": {
const { entityName } = request.params.arguments as { entityName: string };
await this.protocolHandler.attack(entityName);
return {
content: [{ type: "text", text: `Attacked entity: ${entityName}` }]
};
}
case "useItem": {
const { hand = 'right' } = request.params.arguments as { hand?: 'right' | 'left' };
await this.protocolHandler.useItem(hand);
return {
content: [{ type: "text", text: `Used item in ${hand} hand` }]
};
}
case "stopUsingItem": {
await this.protocolHandler.stopUsingItem();
return {
content: [{ type: "text", text: "Stopped using item" }]
};
}
case "lookAt": {
const { x, y, z } = request.params.arguments as { x: number, y: number, z: number };
await this.protocolHandler.lookAt(x, y, z);
return {
content: [{ type: "text", text: `Looking at position (${x}, ${y}, ${z})` }]
};
}
case "followPlayer": {
const { playerName } = request.params.arguments as { playerName: string };
await this.protocolHandler.followPlayer(playerName);
return {
content: [{ type: "text", text: `Following player: ${playerName}` }]
};
}
case "stopFollowing": {
await this.protocolHandler.stopFollowing();
return {
content: [{ type: "text", text: "Stopped following" }]
};
}
case "goToPosition": {
const { x, y, z } = request.params.arguments as { x: number, y: number, z: number };
await this.protocolHandler.goToPosition(x, y, z);
return {
content: [{ type: "text", text: `Moving to position (${x}, ${y}, ${z})` }]
};
}
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
} catch (error) {
throw error;
}
});
}
public getServer(): Server {
return this.server;
}
}
```