# 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:
--------------------------------------------------------------------------------
```
1 | # Build output
2 | /dist
3 |
4 | # Dependencies
5 | /node_modules
6 | /libraries
7 |
8 | # Server files
9 | /versions
10 | /world
11 | /logs
12 | /minecraft-server
13 |
14 | # Configuration files
15 | banned-ips.json
16 | banned-players.json
17 | ops.json
18 | package-lock.json
19 | server.properties
20 | usercache.json
21 | whitelist.json
22 | eula.txt
23 |
24 | # IDE and system files
25 | .DS_Store
26 | .env
27 | *.log
28 |
29 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Minecraft MCP Integration
2 |
3 | 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.
4 |
5 | 
6 |
7 | ## Prerequisites
8 |
9 | 1. Minecraft Launcher
10 | 2. Node.js 18 or higher
11 | 3. Claude Desktop App
12 | 4. Java 21.0.5 (recommended)
13 |
14 | > ⚠️ Note: Currently only tested on macOS/Linux. Windows compatibility is not guaranteed.
15 |
16 | ## Important Note
17 |
18 | 1. **Use the F3+P Shortcut**:
19 | 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.
20 |
21 | 
22 |
23 | 2. **Connection Issues on Claude Restart**:
24 | 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.
25 |
26 | ## Installation Steps
27 |
28 | 1. **Download and Setup Minecraft Server**
29 | - Download Minecraft server v1.21 from [mcversions.net/1.21](https://mcversions.net/download/1.21)
30 | - Install Java 21.0.5 if not already installed (other versions are untested)
31 | - Create a dedicated directory (e.g., `~/minecraft-server/`)
32 | - Place the downloaded `server.jar` file in this directory
33 | - Note down the absolute path to your `server.jar` file
34 |
35 | 2. **Install and Configure MCP Integration**
36 |
37 | Quick Install (Recommended):
38 | ```bash
39 | npx -y @smithery/cli install mcp-minecraft --client claude
40 | ```
41 | Follow the CLI prompts to complete the setup.
42 |
43 | Or Manual Setup:
44 | - Navigate to `~/Library/Application Support/Claude/claude_desktop_config.json`
45 | - Add the MCP server configuration:
46 | ```json
47 | {
48 | "mcpServers": {
49 | "mcp-minecraft": {
50 | "command": "npx",
51 | "args": [
52 | "-y",
53 | "mcp-minecraft@latest",
54 | "--server-jar",
55 | "/absolute/path/to/minecraft-server/server.jar"
56 | ]
57 | }
58 | }
59 | }
60 | ```
61 | > ⚠️ Replace `/absolute/path/to/minecraft-server/server.jar` with your actual server.jar path
62 |
63 | 4. **Launch Claude Desktop**
64 | - Start Claude Desktop after completing the configuration
65 |
66 | 5. **Connect to Server**
67 | - Open Minecraft Launcher
68 | - Install and launch Minecraft Java Edition **v1.21**
69 | - Click "Play" and Select "Multiplayer"
70 | - Click "Add Server"
71 | - Enter server details:
72 | - Server Name: `Minecraft Server`
73 | - Server Address: `localhost:25565`
74 | - Click "Done"
75 |
76 | ## Features
77 |
78 | ### Resources
79 | The integration exposes these MCP resources:
80 |
81 | - `minecraft://bot/location` - Current bot position in the world
82 | - `minecraft://bot/status` - Bot connection status
83 |
84 | ### Tools
85 | Available MCP tools:
86 |
87 | - `chat` - Send chat messages to the server
88 | - `jump` - Make the bot jump
89 | - `moveForward` - Make the bot move forward
90 | - `moveBack` - Make the bot move backward
91 | - `turnLeft` - Make the bot turn left
92 | - `turnRight` - Make the bot turn right
93 | - `placeBlock` - Place a block at specified coordinates
94 | - `digBlock` - Break a block at specified coordinates
95 | - `getBlockInfo` - Get information about a block at specified coordinates
96 | - `selectSlot` - Select a hotbar slot (0-8)
97 | - `getInventory` - Get contents of bot's inventory
98 | - `equipItem` - Equip an item by name to specified destination
99 | - `getStatus` - Get bot's current status (health, food, position, etc.)
100 | - `getNearbyEntities` - Get list of nearby entities within range
101 | - `attack` - Attack a nearby entity by name
102 | - `useItem` - Use/activate the currently held item
103 | - `stopUsingItem` - Stop using/deactivate the current item
104 | - `lookAt` - Make the bot look at specific coordinates
105 | - `followPlayer` - Follow a specific player
106 | - `stopFollowing` - Stop following current target
107 | - `goToPosition` - Navigate to specific coordinates
108 |
109 | ## Technical Details
110 |
111 | - Server runs in offline mode for local development
112 | - Default memory allocation: 2GB
113 | - Default port: 25565
114 | - Bot username: MCPBot
115 |
116 | ## Troubleshooting
117 |
118 | ### Common Issues
119 |
120 | 1. **MCP Connection Failed**
121 | - Look for lingering Java processes
122 | - Terminate them manually:
123 | - Windows: Use Task Manager (untested)
124 | - Mac/Linux:
125 | - Go to 'Activity Monitor' and 'Force Quit' java
126 | - Restart computer if process termination fails
127 | - Note: Latest version should auto-resolve these issues
128 |
129 | 2. **Server Won't Start**
130 | - Verify Java is installed
131 | - Check server.jar path is correct
132 | - Ensure port 25565 is available
133 |
134 | 3. **Can't Connect to Server**
135 | - Verify server is running (check logs)
136 | - Confirm you're using "localhost" as server address
137 | - Check firewall settings
138 |
139 | ### Logs Location
140 | - Minecraft Server logs: Check the minecraft-server directory
141 | - Claude Desktop logs: `~/Library/Logs/Claude/mcp*.log`
142 |
143 | ## Contributing
144 |
145 | Contributions, big or small, are welcome!
146 |
147 | ## License
148 |
149 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
150 |
```
--------------------------------------------------------------------------------
/src/types/config.ts:
--------------------------------------------------------------------------------
```typescript
1 | export interface MinecraftServerConfig {
2 | maxPlayers: number;
3 | port: number;
4 | serverJarPath: string;
5 | memoryAllocation: string;
6 | username: string;
7 | version: string;
8 | }
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "outDir": "dist",
7 | "rootDir": "src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "resolveJsonModule": true
13 | },
14 | "ts-node": {
15 | "esm": true,
16 | "experimentalSpecifierResolution": "node"
17 | },
18 | "include": ["src/**/*.ts"],
19 | "exclude": ["node_modules"]
20 | }
21 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "mcp-minecraft",
3 | "version": "1.0.34",
4 | "type": "module",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "node --loader ts-node/esm src/index.ts",
9 | "dev": "ts-node-dev --respawn --transpile-only --esm src/index.ts",
10 | "build": "tsc"
11 | },
12 | "author": "",
13 | "license": "ISC",
14 | "description": "",
15 | "dependencies": {
16 | "@modelcontextprotocol/sdk": "latest",
17 | "@types/node": "^22.10.2",
18 | "minecraft-protocol": "^1.51.0",
19 | "mineflayer": "^4.23.0",
20 | "mineflayer-pathfinder": "^2.4.5",
21 | "ts-node": "^10.9.2",
22 | "typescript": "^5.7.2",
23 | "yargs": "^17.7.2"
24 | },
25 | "devDependencies": {
26 | "@types/yargs": "^17.0.33",
27 | "ts-node-dev": "^2.0.0"
28 | },
29 | "bin": {
30 | "minecraft-mcp": "./dist/index.js"
31 | }
32 | }
33 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import { Application } from './Application.js';
4 | import yargs from 'yargs';
5 | import { hideBin } from 'yargs/helpers';
6 | import * as fs from 'fs';
7 |
8 | const argv = yargs(hideBin(process.argv))
9 | .option('server-jar', {
10 | alias: 'j',
11 | type: 'string',
12 | description: 'Absolute path to the Minecraft server JAR file',
13 | demandOption: true
14 | })
15 | .scriptName('minecraft-mcp')
16 | .help()
17 | .parseSync();
18 |
19 | if (!fs.existsSync(argv.serverJar)) {
20 | process.exit(1);
21 | }
22 |
23 | const app = new Application({
24 | serverJarPath: argv.serverJar
25 | });
26 |
27 | const cleanup = async () => {
28 | try {
29 | await app.stop();
30 | process.exit(0);
31 | } catch (error) {
32 | process.exit(1);
33 | }
34 | };
35 |
36 | process.on('SIGINT', cleanup);
37 | process.on('SIGTERM', cleanup);
38 |
39 | process.on('uncaughtException', async (error) => {
40 | await cleanup();
41 | });
42 |
43 | process.on('unhandledRejection', async (error) => {
44 | await cleanup();
45 | });
46 |
47 | app.start().catch(() => {
48 | process.exit(1);
49 | });
50 |
```
--------------------------------------------------------------------------------
/src/Application.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2 | import { ServerManager } from "./ServerManager.js";
3 | import { ProtocolHandler } from "./ProtocolHandler.js";
4 | import { MCPHandler } from "./MCPHandler.js";
5 | import path from 'path';
6 |
7 | interface ApplicationConfig {
8 | serverJarPath: string;
9 | }
10 |
11 | export class Application {
12 | private serverManager: ServerManager;
13 | private protocolHandler: ProtocolHandler;
14 | private mcpHandler: MCPHandler;
15 | private transport: StdioServerTransport | null = null;
16 |
17 | constructor(config: ApplicationConfig) {
18 | const serverPath = path.resolve(config.serverJarPath);
19 |
20 | // Initialize with config from CLI
21 | this.serverManager = new ServerManager({
22 | maxPlayers: 10,
23 | port: 25565,
24 | serverJarPath: serverPath,
25 | memoryAllocation: '2G',
26 | username: 'MCPBot',
27 | version: '1.21'
28 | });
29 |
30 | this.protocolHandler = new ProtocolHandler({
31 | host: 'localhost',
32 | port: 25565,
33 | username: 'MCPBot',
34 | version: '1.21'
35 | });
36 |
37 | this.mcpHandler = new MCPHandler(this.protocolHandler);
38 |
39 | this.setupEventHandlers();
40 | }
41 |
42 | private setupEventHandlers(): void {
43 | this.serverManager.on('log', (message) => {
44 | });
45 |
46 | this.serverManager.on('error', (error) => {
47 | });
48 |
49 | this.protocolHandler.on('chat', ({ username, message }) => {
50 | });
51 |
52 | this.protocolHandler.on('error', (error) => {
53 | });
54 |
55 | process.on('SIGINT', async () => {
56 | await this.shutdown();
57 | process.exit(0);
58 | });
59 | }
60 |
61 | public async start(): Promise<void> {
62 | try {
63 | // Start MCP server first - use only stdout for MCP communication
64 | this.transport = new StdioServerTransport();
65 | await this.mcpHandler.getServer().connect(this.transport);
66 |
67 | // Start Minecraft server
68 | await this.serverManager.start();
69 |
70 | // Wait a bit for the server to initialize
71 | await new Promise(resolve => setTimeout(resolve, 5000));
72 |
73 | // Connect bot
74 | await this.protocolHandler.connect();
75 |
76 | } catch (error) {
77 | await this.shutdown();
78 | process.exit(1);
79 | }
80 | }
81 |
82 | public async shutdown(): Promise<void> {
83 | if (this.mcpHandler && this.transport) {
84 | this.transport = null;
85 | }
86 |
87 | await this.protocolHandler.disconnect();
88 | await this.serverManager.stop();
89 | }
90 |
91 | async stop(): Promise<void> {
92 | try {
93 | // Disconnect MCP server
94 | if (this.mcpHandler && this.transport) {
95 | await this.mcpHandler.getServer().close();
96 | this.transport = null;
97 | }
98 |
99 | // Disconnect bot
100 | await this.protocolHandler.disconnect();
101 |
102 | // Stop Minecraft server
103 | await this.serverManager.stop();
104 |
105 | } catch (error) {
106 | throw error;
107 | }
108 | }
109 | }
```
--------------------------------------------------------------------------------
/src/ServerManager.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { spawn, ChildProcess } from 'child_process';
2 | import { EventEmitter } from 'events';
3 | import { MinecraftServerConfig } from './types/config.js';
4 | import * as fs from 'fs';
5 | import path from 'path';
6 | import * as os from 'os';
7 |
8 | export class ServerManager extends EventEmitter {
9 | private process: ChildProcess | null = null;
10 | private config: MinecraftServerConfig;
11 | private isRunning: boolean = false;
12 |
13 | constructor(config: MinecraftServerConfig) {
14 | super();
15 | this.config = this.validateConfig(config);
16 |
17 | process.on('exit', () => {
18 | this.killProcess();
19 | });
20 |
21 | process.on('SIGTERM', () => {
22 | this.killProcess();
23 | });
24 |
25 | process.on('SIGINT', () => {
26 | this.killProcess();
27 | });
28 | }
29 |
30 | private killProcess(): void {
31 | if (this.process) {
32 | try {
33 | process.kill(-this.process.pid!, 'SIGKILL');
34 | } catch (error) {
35 | // Ignore errors during force kill
36 | }
37 | this.process = null;
38 | this.isRunning = false;
39 | }
40 | }
41 |
42 | private validateConfig(config: MinecraftServerConfig): MinecraftServerConfig {
43 | if (!config.serverJarPath) {
44 | throw new Error('Server JAR path is required');
45 | }
46 | if (!config.port || config.port < 1 || config.port > 65535) {
47 | throw new Error('Invalid port number');
48 | }
49 | return {
50 | maxPlayers: config.maxPlayers || 20,
51 | port: config.port,
52 | serverJarPath: config.serverJarPath,
53 | memoryAllocation: config.memoryAllocation || '2G',
54 | username: config.username || 'MCPBot',
55 | version: config.version || '1.21'
56 | };
57 | }
58 |
59 | private ensureEulaAccepted(): void {
60 | // Get the directory containing the server JAR
61 | const serverDir = path.dirname(this.config.serverJarPath);
62 | const eulaPath = path.join(serverDir, 'eula.txt');
63 |
64 | // Create or update eula.txt
65 | fs.writeFileSync(eulaPath, 'eula=true', 'utf8');
66 | }
67 |
68 | private ensureServerProperties(): void {
69 | const serverDir = path.dirname(this.config.serverJarPath);
70 | const propsPath = path.join(serverDir, 'server.properties');
71 |
72 | let properties = '';
73 | if (fs.existsSync(propsPath)) {
74 | properties = fs.readFileSync(propsPath, 'utf8');
75 | }
76 |
77 | // Define our server properties for a simple plains world
78 | const serverProperties = {
79 | 'online-mode': 'false',
80 | 'level-type': 'flat',
81 | 'spawn-protection': '0',
82 | 'difficulty': 'peaceful', // No hostile mobs
83 | 'spawn-monsters': 'false', // Disable monster spawning
84 | 'spawn-animals': 'true', // Enable animal spawning
85 | 'spawn-npcs': 'false', // Disable villagers
86 | 'generate-structures': 'false', // Disable structures (villages, temples, etc)
87 | 'allow-nether': 'false', // Disable nether
88 | 'gamemode': 'creative', // Set creative mode for easier building
89 | 'do-daylight-cycle': 'false',
90 | 'max-players': this.config.maxPlayers.toString(),
91 | 'server-port': this.config.port.toString(),
92 | 'motd': 'Peaceful Plains Server'
93 | };
94 |
95 | // Update or create each property
96 | for (const [key, value] of Object.entries(serverProperties)) {
97 | const regex = new RegExp(`^${key}=.*$`, 'm');
98 | if (properties.match(regex)) {
99 | properties = properties.replace(regex, `${key}=${value}`);
100 | } else {
101 | properties += `\n${key}=${value}`;
102 | }
103 | }
104 |
105 | fs.writeFileSync(propsPath, properties.trim(), 'utf8');
106 | }
107 |
108 | private normalizePath(p: string): string {
109 | return path.normalize(p).toLowerCase();
110 | }
111 |
112 | private expandHome(filepath: string): string {
113 | if (filepath.startsWith("~/") || filepath === "~") {
114 | return path.join(os.homedir(), filepath.slice(1));
115 | }
116 | return filepath;
117 | }
118 |
119 | private validateServerPath(): string {
120 | const expandedPath = this.expandHome(this.config.serverJarPath);
121 | const absolutePath = path.isAbsolute(expandedPath)
122 | ? path.resolve(expandedPath)
123 | : path.resolve(process.cwd(), expandedPath);
124 |
125 | if (!fs.existsSync(absolutePath)) {
126 | throw new Error(`Server JAR not found at path: ${absolutePath}`);
127 | }
128 |
129 | return absolutePath;
130 | }
131 |
132 | public async start(): Promise<void> {
133 | if (this.isRunning) {
134 | throw new Error('Server is already running');
135 | }
136 |
137 | return new Promise((resolve, reject) => {
138 | try {
139 | const serverJarPath = this.validateServerPath();
140 | const serverDir = path.dirname(serverJarPath);
141 |
142 | this.ensureEulaAccepted();
143 | this.ensureServerProperties();
144 |
145 | this.process = spawn('java', [
146 | `-Xmx${this.config.memoryAllocation}`,
147 | `-Xms${this.config.memoryAllocation}`,
148 | '-jar',
149 | serverJarPath,
150 | 'nogui'
151 | ], {
152 | cwd: serverDir,
153 | stdio: ['pipe', 'pipe', 'pipe'],
154 | detached: true,
155 | ...(process.platform !== 'win32' && { pid: true })
156 | });
157 |
158 | const timeout = setTimeout(() => {
159 | reject(new Error('Server startup timed out'));
160 | this.stop();
161 | }, 60000);
162 |
163 | this.process.stdout?.on('data', (data: Buffer) => {
164 | const message = data.toString();
165 |
166 | if (message.includes('Done')) {
167 | clearTimeout(timeout);
168 | this.isRunning = true;
169 | resolve();
170 | }
171 | });
172 |
173 | this.process.stderr?.on('data', (data: Buffer) => {
174 | const error = data.toString();
175 | if (error.includes('Error')) {
176 | reject(new Error(error));
177 | }
178 | });
179 |
180 | this.process.on('close', (code) => {
181 | this.isRunning = false;
182 | });
183 |
184 | this.process.on('error', (err) => {
185 | this.isRunning = false;
186 | reject(err);
187 | });
188 |
189 | } catch (error) {
190 | reject(error);
191 | }
192 | });
193 | }
194 |
195 | public async stop(): Promise<void> {
196 | if (!this.isRunning || !this.process) {
197 | return;
198 | }
199 |
200 | return new Promise((resolve) => {
201 | const forceKillTimeout = setTimeout(() => {
202 | this.killProcess();
203 | resolve();
204 | }, 10000);
205 |
206 | this.process?.once('close', () => {
207 | clearTimeout(forceKillTimeout);
208 | this.isRunning = false;
209 | this.process = null;
210 | resolve();
211 | });
212 |
213 | if (this.process?.stdin) {
214 | this.process.stdin.write('stop\n');
215 | } else {
216 | this.killProcess();
217 | resolve();
218 | }
219 | });
220 | }
221 |
222 | public isServerRunning(): boolean {
223 | return this.isRunning;
224 | }
225 | }
```
--------------------------------------------------------------------------------
/src/ProtocolHandler.ts:
--------------------------------------------------------------------------------
```typescript
1 | import * as mineflayer from 'mineflayer';
2 | import { EventEmitter } from 'events';
3 | import { Vec3 } from 'vec3';
4 | import pathfinderPkg from 'mineflayer-pathfinder';
5 | const { pathfinder, Movements, goals } = pathfinderPkg;
6 |
7 | export interface BotConfig {
8 | host: string;
9 | port: number;
10 | username: string;
11 | version: string;
12 | }
13 |
14 | export class ProtocolHandler extends EventEmitter {
15 | private bot: mineflayer.Bot | null = null;
16 | private config: BotConfig;
17 |
18 | constructor(config: BotConfig) {
19 | super();
20 | this.config = config;
21 | }
22 |
23 | public async connect(): Promise<void> {
24 | if (this.bot) {
25 | throw new Error('Bot is already connected');
26 | }
27 |
28 | return new Promise((resolve, reject) => {
29 | try {
30 | this.bot = mineflayer.createBot({
31 | host: this.config.host,
32 | port: this.config.port,
33 | username: this.config.username,
34 | version: this.config.version
35 | });
36 |
37 | this.bot.once('spawn', () => {
38 | this.bot?.loadPlugin(pathfinder);
39 |
40 | if (this.bot?.pathfinder) {
41 | this.bot.pathfinder.setMovements(new Movements(this.bot));
42 | }
43 |
44 | this.setupEventHandlers();
45 | this.emit('connected');
46 | resolve();
47 | });
48 |
49 | this.bot.on('error', (error) => {
50 | this.emit('error', error);
51 | reject(error);
52 | });
53 |
54 | } catch (error) {
55 | reject(error);
56 | }
57 | });
58 | }
59 |
60 | private setupEventHandlers(): void {
61 | if (!this.bot) return;
62 |
63 | this.bot.on('chat', (username, message) => {
64 | this.emit('chat', { username, message });
65 | });
66 |
67 | this.bot.on('kicked', (reason) => {
68 | this.emit('kicked', reason);
69 | });
70 |
71 | this.bot.on('error', (error) => {
72 | this.emit('error', error);
73 | });
74 | }
75 |
76 | public async sendChat(message: string): Promise<void> {
77 | if (!this.bot) throw new Error('Bot not connected');
78 | await this.bot.chat(message);
79 | }
80 |
81 | public async jump(): Promise<void> {
82 | if (!this.bot) throw new Error('Bot not connected');
83 | this.bot.setControlState('jump', true);
84 | setTimeout(() => {
85 | if (this.bot) this.bot.setControlState('jump', false);
86 | }, 500);
87 | }
88 |
89 | public getPosition(): Vec3 | null {
90 | if (!this.bot || !this.bot.entity) return null;
91 | return this.bot.entity.position;
92 | }
93 |
94 | public async disconnect(): Promise<void> {
95 | if (!this.bot) return;
96 |
97 | return new Promise((resolve) => {
98 | const bot = this.bot;
99 | if (!bot) {
100 | resolve();
101 | return;
102 | }
103 |
104 | bot.removeAllListeners();
105 |
106 | bot.once('end', () => {
107 | this.bot = null;
108 |
109 | setTimeout(() => {
110 | process.exit(0);
111 | }, 1000);
112 |
113 | resolve();
114 | });
115 |
116 | bot.end();
117 | });
118 | }
119 |
120 | public isConnected(): boolean {
121 | return this.bot !== null;
122 | }
123 |
124 | public async moveForward(): Promise<void> {
125 | if (!this.bot) throw new Error('Bot not connected');
126 | this.bot.setControlState('forward', true);
127 | await new Promise(resolve => setTimeout(resolve, 1000));
128 | this.bot.setControlState('forward', false);
129 | }
130 |
131 | public async moveBack(): Promise<void> {
132 | if (!this.bot) throw new Error('Bot not connected');
133 | this.bot.setControlState('back', true);
134 | await new Promise(resolve => setTimeout(resolve, 1000));
135 | this.bot.setControlState('back', false);
136 | }
137 |
138 | public async turnLeft(): Promise<void> {
139 | if (!this.bot) throw new Error('Bot not connected');
140 | this.bot.setControlState('left', true);
141 | await new Promise(resolve => setTimeout(resolve, 500));
142 | this.bot.setControlState('left', false);
143 | }
144 |
145 | public async turnRight(): Promise<void> {
146 | if (!this.bot) throw new Error('Bot not connected');
147 | this.bot.setControlState('right', true);
148 | await new Promise(resolve => setTimeout(resolve, 500));
149 | this.bot.setControlState('right', false);
150 | }
151 |
152 | public async placeBlock(x: number, y: number, z: number): Promise<void> {
153 | if (!this.bot) throw new Error('Bot not connected');
154 |
155 | try {
156 | const targetPos = new Vec3(x, y, z);
157 | const faceVector = new Vec3(0, 1, 0);
158 |
159 | const referenceBlock = await this.bot.blockAt(targetPos);
160 | if (!referenceBlock) throw new Error('No reference block found');
161 |
162 | await this.bot.placeBlock(referenceBlock, faceVector);
163 | } catch (error) {
164 | throw new Error(`Failed to place block: ${error}`);
165 | }
166 | }
167 |
168 | public async digBlock(x: number, y: number, z: number): Promise<void> {
169 | if (!this.bot) throw new Error('Bot not connected');
170 |
171 | try {
172 | const targetPos = new Vec3(x, y, z);
173 | const block = await this.bot.blockAt(targetPos);
174 |
175 | if (!block) throw new Error('No block at target position');
176 | if (block.name === 'air') throw new Error('Cannot dig air');
177 |
178 | await this.bot.dig(block);
179 | } catch (error) {
180 | throw new Error(`Failed to dig block: ${error}`);
181 | }
182 | }
183 |
184 | public async getBlockInfo(x: number, y: number, z: number): Promise<any> {
185 | if (!this.bot) throw new Error('Bot not connected');
186 |
187 | try {
188 | const targetPos = new Vec3(x, y, z);
189 | const block = await this.bot.blockAt(targetPos);
190 |
191 | if (!block) throw new Error('No block at target position');
192 |
193 | return {
194 | name: block.name,
195 | type: block.type,
196 | position: {
197 | x: block.position.x,
198 | y: block.position.y,
199 | z: block.position.z
200 | },
201 | hardness: block.hardness
202 | };
203 | } catch (error) {
204 | throw new Error(`Failed to get block info: ${error}`);
205 | }
206 | }
207 |
208 | public async selectSlot(slot: number): Promise<void> {
209 | if (!this.bot) throw new Error('Bot not connected');
210 | if (slot < 0 || slot > 8) throw new Error('Slot must be between 0 and 8');
211 |
212 | try {
213 | await this.bot.setQuickBarSlot(slot);
214 | } catch (error) {
215 | throw new Error(`Failed to select slot: ${error}`);
216 | }
217 | }
218 |
219 | public async getInventory(): Promise<any> {
220 | if (!this.bot) throw new Error('Bot not connected');
221 |
222 | const items = this.bot.inventory.items();
223 | return items.map(item => ({
224 | name: item.name,
225 | count: item.count,
226 | slot: item.slot,
227 | displayName: item.displayName
228 | }));
229 | }
230 |
231 | public async equipItem(itemName: string, destination?: string): Promise<void> {
232 | if (!this.bot) throw new Error('Bot not connected');
233 |
234 | try {
235 | const item = this.bot.inventory.items().find(item => item.name.includes(itemName));
236 | if (!item) throw new Error(`Item ${itemName} not found in inventory`);
237 |
238 | const equipDestination: mineflayer.EquipmentDestination | null = destination as mineflayer.EquipmentDestination || null;
239 | await this.bot.equip(item, equipDestination);
240 | } catch (error) {
241 | throw new Error(`Failed to equip item: ${error}`);
242 | }
243 | }
244 |
245 | public async getStatus(): Promise<any> {
246 | if (!this.bot) throw new Error('Bot not connected');
247 |
248 | return {
249 | health: this.bot.health,
250 | food: this.bot.food,
251 | gameMode: this.bot.game?.gameMode ?? 'unknown',
252 | position: this.getPosition(),
253 | isRaining: this.bot.isRaining,
254 | time: {
255 | timeOfDay: this.bot.time?.timeOfDay ?? 0,
256 | day: this.bot.time?.day ?? 0
257 | }
258 | };
259 | }
260 |
261 | public async getNearbyEntities(range: number = 10): Promise<any[]> {
262 | if (!this.bot) throw new Error('Bot not connected');
263 |
264 | return Object.values(this.bot.entities)
265 | .filter(entity => {
266 | if (!entity.position || !this.bot?.entity?.position) return false;
267 | return entity.position.distanceTo(this.bot.entity.position) <= range;
268 | })
269 | .map(entity => ({
270 | name: entity.name,
271 | type: entity.type,
272 | position: entity.position,
273 | distance: entity.position && this.bot?.entity?.position
274 | ? entity.position.distanceTo(this.bot.entity.position)
275 | : null // Handle the case where position might be null
276 | }));
277 | }
278 |
279 | public async attack(entityName: string): Promise<void> {
280 | if (!this.bot) throw new Error('Bot not connected');
281 |
282 | try {
283 | const entity = Object.values(this.bot.entities)
284 | .find(e => e.name === entityName &&
285 | e.position.distanceTo(this.bot!.entity.position) <= 4);
286 |
287 | if (!entity) throw new Error('Entity not found or too far');
288 | await this.bot.attack(entity);
289 | } catch (error) {
290 | throw new Error(`Failed to attack: ${error}`);
291 | }
292 | }
293 |
294 | public async useItem(hand: 'right' | 'left' = 'right'): Promise<void> {
295 | if (!this.bot) throw new Error('Bot not connected');
296 |
297 | try {
298 | await this.bot.activateItem(hand === 'right');
299 | } catch (error) {
300 | throw new Error(`Failed to use item: ${error}`);
301 | }
302 | }
303 |
304 | public async stopUsingItem(): Promise<void> {
305 | if (!this.bot) throw new Error('Bot not connected');
306 |
307 | try {
308 | await this.bot.deactivateItem();
309 | } catch (error) {
310 | throw new Error(`Failed to stop using item: ${error}`);
311 | }
312 | }
313 |
314 | public async lookAt(x: number, y: number, z: number): Promise<void> {
315 | if (!this.bot) throw new Error('Bot not connected');
316 |
317 | try {
318 | await this.bot.lookAt(new Vec3(x, y, z));
319 | } catch (error) {
320 | throw new Error(`Failed to look at position: ${error}`);
321 | }
322 | }
323 |
324 | public async followPlayer(playerName: string): Promise<void> {
325 | if (!this.bot) throw new Error('Bot not connected');
326 |
327 | try {
328 | const player = this.bot.players[playerName]?.entity;
329 | if (!player) throw new Error('Player not found');
330 |
331 | // Follow at 2 blocks distance
332 | await this.bot.pathfinder.goto(
333 | new goals.GoalFollow(player, 2)
334 | );
335 | } catch (error) {
336 | throw new Error(`Failed to follow player: ${error}`);
337 | }
338 | }
339 |
340 | public async stopFollowing(): Promise<void> {
341 | if (!this.bot) throw new Error('Bot not connected');
342 | this.bot.pathfinder.stop();
343 | }
344 |
345 | public async goToPosition(x: number, y: number, z: number): Promise<void> {
346 | if (!this.bot) throw new Error('Bot not connected');
347 |
348 | try {
349 | await this.bot.pathfinder.goto(
350 | new goals.GoalBlock(x, y, z)
351 | );
352 | } catch (error) {
353 | throw new Error(`Failed to go to position: ${error}`);
354 | }
355 | }
356 | }
```
--------------------------------------------------------------------------------
/src/MCPHandler.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2 | import {
3 | ListResourcesRequestSchema,
4 | ReadResourceRequestSchema,
5 | ListToolsRequestSchema,
6 | CallToolRequestSchema
7 | } from "@modelcontextprotocol/sdk/types.js";
8 | import { ProtocolHandler } from "./ProtocolHandler.js";
9 | import { ReadResourceRequest, CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
10 |
11 | export class MCPHandler {
12 | private server: Server;
13 | private protocolHandler: ProtocolHandler;
14 |
15 | constructor(protocolHandler: ProtocolHandler) {
16 | this.protocolHandler = protocolHandler;
17 | this.server = new Server({
18 | name: "minecraft-mcp-server",
19 | version: "1.0.0"
20 | }, {
21 | capabilities: {
22 | resources: {},
23 | tools: {}
24 | }
25 | });
26 |
27 | this.setupHandlers();
28 | }
29 |
30 | private setupHandlers(): void {
31 | this.setupResourceHandlers();
32 | this.setupToolHandlers();
33 | }
34 |
35 | private setupResourceHandlers(): void {
36 | // List available resources
37 | this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
38 | try {
39 | return {
40 | resources: [
41 | {
42 | uri: "minecraft://bot/location",
43 | name: "Bot Location",
44 | mimeType: "application/json",
45 | description: "Current bot location in the Minecraft world"
46 | },
47 | {
48 | uri: "minecraft://bot/status",
49 | name: "Bot Status",
50 | mimeType: "application/json",
51 | description: "Current status of the bot"
52 | }
53 | ]
54 | };
55 | } catch (error) {
56 | throw error;
57 | }
58 | });
59 |
60 | // Handle resource reading
61 | this.server.setRequestHandler(ReadResourceRequestSchema, async (request: ReadResourceRequest) => {
62 | try {
63 | switch (request.params.uri) {
64 | case "minecraft://bot/location": {
65 | const pos = this.protocolHandler.getPosition();
66 | if (!pos) throw new Error("Position not available");
67 |
68 | return {
69 | contents: [{
70 | uri: request.params.uri,
71 | mimeType: "application/json",
72 | text: JSON.stringify({
73 | x: Math.round(pos.x * 100) / 100,
74 | y: Math.round(pos.y * 100) / 100,
75 | z: Math.round(pos.z * 100) / 100
76 | })
77 | }]
78 | };
79 | }
80 |
81 | case "minecraft://bot/status": {
82 | return {
83 | contents: [{
84 | uri: request.params.uri,
85 | mimeType: "application/json",
86 | text: JSON.stringify({
87 | connected: this.protocolHandler.isConnected()
88 | })
89 | }]
90 | };
91 | }
92 |
93 | default:
94 | throw new Error(`Unknown resource: ${request.params.uri}`);
95 | }
96 | } catch (error) {
97 | throw error;
98 | }
99 | });
100 | }
101 |
102 | private setupToolHandlers(): void {
103 | // List available tools
104 | this.server.setRequestHandler(ListToolsRequestSchema, async () => {
105 | try {
106 | return {
107 | tools: [
108 | {
109 | name: "chat",
110 | description: "Send a chat message",
111 | inputSchema: {
112 | type: "object",
113 | properties: {
114 | message: { type: "string" }
115 | },
116 | required: ["message"]
117 | }
118 | },
119 | {
120 | name: "jump",
121 | description: "Make the bot jump",
122 | inputSchema: {
123 | type: "object",
124 | properties: {}
125 | }
126 | },
127 | {
128 | name: "moveForward",
129 | description: "Make the bot move forward",
130 | inputSchema: {
131 | type: "object",
132 | properties: {}
133 | }
134 | },
135 | {
136 | name: "moveBack",
137 | description: "Make the bot move backward",
138 | inputSchema: {
139 | type: "object",
140 | properties: {}
141 | }
142 | },
143 | {
144 | name: "turnLeft",
145 | description: "Make the bot turn left",
146 | inputSchema: {
147 | type: "object",
148 | properties: {}
149 | }
150 | },
151 | {
152 | name: "turnRight",
153 | description: "Make the bot turn right",
154 | inputSchema: {
155 | type: "object",
156 | properties: {}
157 | }
158 | },
159 | {
160 | name: "placeBlock",
161 | description: "Place a block at specified coordinates",
162 | inputSchema: {
163 | type: "object",
164 | properties: {
165 | x: { type: "number" },
166 | y: { type: "number" },
167 | z: { type: "number" }
168 | },
169 | required: ["x", "y", "z"]
170 | }
171 | },
172 | {
173 | name: "digBlock",
174 | description: "Break a block at specified coordinates",
175 | inputSchema: {
176 | type: "object",
177 | properties: {
178 | x: { type: "number" },
179 | y: { type: "number" },
180 | z: { type: "number" }
181 | },
182 | required: ["x", "y", "z"]
183 | }
184 | },
185 | {
186 | name: "getBlockInfo",
187 | description: "Get information about a block at specified coordinates",
188 | inputSchema: {
189 | type: "object",
190 | properties: {
191 | x: { type: "number" },
192 | y: { type: "number" },
193 | z: { type: "number" }
194 | },
195 | required: ["x", "y", "z"]
196 | }
197 | },
198 | {
199 | name: "selectSlot",
200 | description: "Select a hotbar slot (0-8)",
201 | inputSchema: {
202 | type: "object",
203 | properties: {
204 | slot: {
205 | type: "number",
206 | minimum: 0,
207 | maximum: 8
208 | }
209 | },
210 | required: ["slot"]
211 | }
212 | },
213 | {
214 | name: "getInventory",
215 | description: "Get contents of bot's inventory",
216 | inputSchema: {
217 | type: "object",
218 | properties: {}
219 | }
220 | },
221 | {
222 | name: "equipItem",
223 | description: "Equip an item by name",
224 | inputSchema: {
225 | type: "object",
226 | properties: {
227 | itemName: { type: "string" },
228 | destination: {
229 | type: "string",
230 | enum: ["hand", "head", "torso", "legs", "feet"]
231 | }
232 | },
233 | required: ["itemName"]
234 | }
235 | },
236 | {
237 | name: "getStatus",
238 | description: "Get bot's current status including health, food, position, etc.",
239 | inputSchema: {
240 | type: "object",
241 | properties: {}
242 | }
243 | },
244 | {
245 | name: "getNearbyEntities",
246 | description: "Get list of nearby entities within specified range",
247 | inputSchema: {
248 | type: "object",
249 | properties: {
250 | range: {
251 | type: "number",
252 | minimum: 1,
253 | maximum: 100,
254 | default: 10
255 | }
256 | }
257 | }
258 | },
259 | {
260 | name: "attack",
261 | description: "Attack a nearby entity by name",
262 | inputSchema: {
263 | type: "object",
264 | properties: {
265 | entityName: { type: "string" }
266 | },
267 | required: ["entityName"]
268 | }
269 | },
270 | {
271 | name: "useItem",
272 | description: "Use/activate the currently held item",
273 | inputSchema: {
274 | type: "object",
275 | properties: {
276 | hand: {
277 | type: "string",
278 | enum: ["right", "left"],
279 | default: "right"
280 | }
281 | }
282 | }
283 | },
284 | {
285 | name: "stopUsingItem",
286 | description: "Stop using/deactivate the current item",
287 | inputSchema: {
288 | type: "object",
289 | properties: {}
290 | }
291 | },
292 | {
293 | name: "lookAt",
294 | description: "Make the bot look at specific coordinates",
295 | inputSchema: {
296 | type: "object",
297 | properties: {
298 | x: { type: "number" },
299 | y: { type: "number" },
300 | z: { type: "number" }
301 | },
302 | required: ["x", "y", "z"]
303 | }
304 | },
305 | {
306 | name: "followPlayer",
307 | description: "Follow a specific player",
308 | inputSchema: {
309 | type: "object",
310 | properties: {
311 | playerName: { type: "string" }
312 | },
313 | required: ["playerName"]
314 | }
315 | },
316 | {
317 | name: "stopFollowing",
318 | description: "Stop following current target",
319 | inputSchema: {
320 | type: "object",
321 | properties: {}
322 | }
323 | },
324 | {
325 | name: "goToPosition",
326 | description: "Navigate to specific coordinates",
327 | inputSchema: {
328 | type: "object",
329 | properties: {
330 | x: { type: "number" },
331 | y: { type: "number" },
332 | z: { type: "number" }
333 | },
334 | required: ["x", "y", "z"]
335 | }
336 | }
337 | ]
338 | };
339 | } catch (error) {
340 | throw error;
341 | }
342 | });
343 |
344 | // Handle tool calls
345 | this.server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => {
346 | try {
347 | switch (request.params.name) {
348 | case "chat":
349 | if (request.params.arguments && request.params.arguments.message) {
350 | await this.protocolHandler.sendChat(request.params.arguments.message as string);
351 | return {
352 | content: [{
353 | type: "text",
354 | text: "Message sent"
355 | }]
356 | };
357 | } else {
358 | throw new Error("Invalid arguments for 'chat' tool");
359 | }
360 |
361 | case "jump":
362 | await this.protocolHandler.jump();
363 | return {
364 | content: [{
365 | type: "text",
366 | text: "Jumped!"
367 | }]
368 | };
369 |
370 | case "moveForward":
371 | await this.protocolHandler.moveForward();
372 | return {
373 | content: [{ type: "text", text: "Moved forward" }]
374 | };
375 |
376 | case "moveBack":
377 | await this.protocolHandler.moveBack();
378 | return {
379 | content: [{ type: "text", text: "Moved backward" }]
380 | };
381 |
382 | case "turnLeft":
383 | await this.protocolHandler.turnLeft();
384 | return {
385 | content: [{ type: "text", text: "Turned left" }]
386 | };
387 |
388 | case "turnRight":
389 | await this.protocolHandler.turnRight();
390 | return {
391 | content: [{ type: "text", text: "Turned right" }]
392 | };
393 |
394 | case "placeBlock": {
395 | const { x, y, z } = request.params.arguments as { x: number, y: number, z: number };
396 | await this.protocolHandler.placeBlock(x, y, z);
397 | return {
398 | content: [{ type: "text", text: `Placed block at (${x}, ${y}, ${z})` }]
399 | };
400 | }
401 |
402 | case "digBlock": {
403 | const { x, y, z } = request.params.arguments as { x: number, y: number, z: number };
404 | await this.protocolHandler.digBlock(x, y, z);
405 | return {
406 | content: [{ type: "text", text: `Broke block at (${x}, ${y}, ${z})` }]
407 | };
408 | }
409 |
410 | case "getBlockInfo": {
411 | const { x, y, z } = request.params.arguments as { x: number, y: number, z: number };
412 | const blockInfo = await this.protocolHandler.getBlockInfo(x, y, z);
413 | return {
414 | content: [{ type: "text", text: JSON.stringify(blockInfo, null, 2) }]
415 | };
416 | }
417 |
418 | case "selectSlot": {
419 | const { slot } = request.params.arguments as { slot: number };
420 | await this.protocolHandler.selectSlot(slot);
421 | return {
422 | content: [{ type: "text", text: `Selected slot ${slot}` }]
423 | };
424 | }
425 |
426 | case "getInventory": {
427 | const inventory = await this.protocolHandler.getInventory();
428 | return {
429 | content: [{ type: "text", text: JSON.stringify(inventory, null, 2) }]
430 | };
431 | }
432 |
433 | case "equipItem": {
434 | const { itemName, destination } = request.params.arguments as {
435 | itemName: string,
436 | destination?: string
437 | };
438 | await this.protocolHandler.equipItem(itemName, destination);
439 | return {
440 | content: [{
441 | type: "text",
442 | text: `Equipped ${itemName}${destination ? ` to ${destination}` : ''}`
443 | }]
444 | };
445 | }
446 |
447 | case "getStatus": {
448 | const status = await this.protocolHandler.getStatus();
449 | return {
450 | content: [{ type: "text", text: JSON.stringify(status, null, 2) }]
451 | };
452 | }
453 |
454 | case "getNearbyEntities": {
455 | const { range } = request.params.arguments as { range?: number };
456 | const entities = await this.protocolHandler.getNearbyEntities(range);
457 | return {
458 | content: [{ type: "text", text: JSON.stringify(entities, null, 2) }]
459 | };
460 | }
461 |
462 | case "attack": {
463 | const { entityName } = request.params.arguments as { entityName: string };
464 | await this.protocolHandler.attack(entityName);
465 | return {
466 | content: [{ type: "text", text: `Attacked entity: ${entityName}` }]
467 | };
468 | }
469 |
470 | case "useItem": {
471 | const { hand = 'right' } = request.params.arguments as { hand?: 'right' | 'left' };
472 | await this.protocolHandler.useItem(hand);
473 | return {
474 | content: [{ type: "text", text: `Used item in ${hand} hand` }]
475 | };
476 | }
477 |
478 | case "stopUsingItem": {
479 | await this.protocolHandler.stopUsingItem();
480 | return {
481 | content: [{ type: "text", text: "Stopped using item" }]
482 | };
483 | }
484 |
485 | case "lookAt": {
486 | const { x, y, z } = request.params.arguments as { x: number, y: number, z: number };
487 | await this.protocolHandler.lookAt(x, y, z);
488 | return {
489 | content: [{ type: "text", text: `Looking at position (${x}, ${y}, ${z})` }]
490 | };
491 | }
492 |
493 | case "followPlayer": {
494 | const { playerName } = request.params.arguments as { playerName: string };
495 | await this.protocolHandler.followPlayer(playerName);
496 | return {
497 | content: [{ type: "text", text: `Following player: ${playerName}` }]
498 | };
499 | }
500 |
501 | case "stopFollowing": {
502 | await this.protocolHandler.stopFollowing();
503 | return {
504 | content: [{ type: "text", text: "Stopped following" }]
505 | };
506 | }
507 |
508 | case "goToPosition": {
509 | const { x, y, z } = request.params.arguments as { x: number, y: number, z: number };
510 | await this.protocolHandler.goToPosition(x, y, z);
511 | return {
512 | content: [{ type: "text", text: `Moving to position (${x}, ${y}, ${z})` }]
513 | };
514 | }
515 |
516 | default:
517 | throw new Error(`Unknown tool: ${request.params.name}`);
518 | }
519 | } catch (error) {
520 | throw error;
521 | }
522 | });
523 | }
524 |
525 | public getServer(): Server {
526 | return this.server;
527 | }
528 | }
```