# Directory Structure
```
├── .cursorrules
├── .github
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── pull_request_template.md
│ └── workflows
│ └── ci.yml
├── .gitignore
├── .husky
│ └── pre-commit
├── .npmignore
├── bun.lockb
├── CONTRIBUTING.md
├── jest.config.js
├── knowledge.md
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── __tests__
│ │ ├── bot.test.ts
│ │ ├── mocks
│ │ │ └── mockBot.ts
│ │ ├── server.test.ts
│ │ └── setup.ts
│ ├── cli.ts
│ ├── core
│ │ └── bot.ts
│ ├── handlers
│ │ ├── resources.ts
│ │ └── tools.ts
│ ├── index.ts
│ ├── schemas.ts
│ ├── server.ts
│ ├── tools
│ │ └── index.ts
│ └── types
│ ├── minecraft.ts
│ └── tools.ts
├── tsconfig.json
└── yarn.lock
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | node_modules/
2 | build/
3 | coverage/
4 | .env
5 | .env.*
6 | *.log
7 | .DS_Store
8 | .vscode/
9 | .idea/
10 | *.tsbuildinfo
```
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
```
1 | src/__tests__/
2 | coverage/
3 | .github/
4 | .vscode/
5 | .idea/
6 | *.test.ts
7 | *.spec.ts
8 | tsconfig.json
9 | jest.config.js
10 | .eslintrc
11 | .prettierrc
```
--------------------------------------------------------------------------------
/.cursorrules:
--------------------------------------------------------------------------------
```
1 | # .cursorrules
2 |
3 | ## ⚠️ IMPORTANT: JSON-RPC Warning
4 |
5 | If you find yourself implementing JSON-RPC directly (e.g., writing JSON messages, handling protocol-level details, or dealing with stdio), STOP! You are going in the wrong direction. The MCP framework handles all protocol details. Your job is to:
6 |
7 | 1. Implement the actual Minecraft/Mineflayer functionality
8 | 2. Use the provided bot methods and APIs
9 | 3. Let the framework handle all communication
10 |
11 | Never:
12 |
13 | - Write JSON-RPC messages directly
14 | - Handle stdio yourself
15 | - Implement protocol-level error codes
16 | - Create custom notification systems
17 |
18 | ## Overview
19 |
20 | This project uses the Model Context Protocol (MCP) to bridge interactions between a Minecraft bot (powered by Mineflayer) and an LLM-based client.
21 |
22 | The essential flow is:
23 |
24 | 1. The server starts up ("MinecraftServer") and connects to a Minecraft server automatically (via the Mineflayer bot).
25 | 2. The MCP server is exposed through standard JSON-RPC over stdio.
26 | 3. MCP "tools" correspond to actionable commands in Minecraft (e.g., "dig_area", "navigate_to", etc.).
27 | 4. MCP "resources" correspond to read-only data from Minecraft (e.g., "minecraft://inventory").
28 |
29 | When an MCP client issues requests, the server routes these to either:
30 | • The "toolHandler" (for effectful actions such as "dig_block")
31 | • The "resourceHandler" (for returning game state like position, health, etc.)
32 |
33 | ## MCP Types and Imports
34 |
35 | When working with MCP types:
36 |
37 | 1. Import types from the correct SDK paths:
38 | - Transport: "@modelcontextprotocol/sdk/shared/transport.js"
39 | - JSONRPCMessage and other core types: "@modelcontextprotocol/sdk/types.js"
40 | 2. Always check for optional fields using type guards (e.g., 'id' in message)
41 | 3. Follow existing implementations in example servers when unsure
42 | 4. Never modify working type imports - MCP has specific paths that must be used
43 |
44 | ## Progress Callbacks
45 |
46 | For long-running operations like navigation and digging:
47 |
48 | 1. Use progress callbacks to report status to MCP clients
49 | 2. Include a progressToken in \_meta for tracking
50 | 3. Send notifications via "tool/progress" with:
51 | - token: unique identifier
52 | - progress: 0-100 percentage
53 | - status: "in_progress" or "complete"
54 | - message: human-readable progress
55 |
56 | ## API Compatibility and Alternatives
57 |
58 | When working with Mineflayer's API:
59 |
60 | 1. Always check the actual API implementation before assuming method availability
61 | 2. When encountering type/compatibility issues:
62 | - Look for alternative methods in the API (e.g., moveSlotItem instead of click)
63 | - Consider type casting with 'unknown' when necessary (e.g., `as unknown as Furnace`)
64 | - Add proper type annotations to parameters to avoid implicit any
65 | 3. For container operations:
66 | - Prefer high-level methods like moveSlotItem over low-level ones
67 | - Always handle cleanup (close containers) in finally blocks
68 | - Cast specialized containers (like Furnace) appropriately
69 | 4. Error handling:
70 | - Wrap all API calls in try/catch blocks
71 | - Use wrapError for consistent error reporting
72 | - Include specific error messages that help diagnose issues
73 |
74 | ## File Layout
75 |
76 | - src/types/minecraft.ts
77 | Type definitions for core Minecraft interfaces (Position, Block, Entity, etc.). Also includes the "MinecraftBot" interface, specifying the methods the bot should implement (like "digArea", "followPlayer", "attackEntity", etc.).
78 |
79 | - src/core/bot.ts
80 | Contains the main "MineflayerBot" class, an implementation of "MinecraftBot" using a real Mineflayer bot with pathfinding, digging, etc.
81 |
82 | - src/handlers/tools.ts
83 | Implements "ToolHandler" functions that receive tool requests and execute them against the MinecraftBot methods (e.g., "handleDigArea").
84 |
85 | - src/handlers/resources.ts
86 | Implements "ResourceHandler" for read-only data fetches (position, inventory, weather, etc.).
87 |
88 | - src/core/server.ts (and src/server.ts in some setups)
89 | Main MCP server that sets up request handlers, ties in the "MineflayerBot" instance, and starts listening for JSON-RPC calls over stdio.
90 |
91 | - src/**tests**/\*
92 | Contains Jest tests and "MockMinecraftBot" (a simplified implementation of "MinecraftBot" for testing).
93 |
94 | ## Tools and Technologies
95 |
96 | - Model Context Protocol - an API for clients and servers to expose tools, resources, and prompts.
97 | - Mineflayer
98 | - Prismarine
99 |
100 | ## Code
101 |
102 | - Write modern TypeScript against 2024 standards and expectations. Cleanly use async/await where possible.
103 | - Use bun for CLI commands
104 |
105 | ## Error Handling
106 |
107 | - All errors MUST be properly formatted as JSON-RPC responses over stdio
108 | - Never throw errors directly as this will crash MCP clients
109 | - Use the ToolResponse interface with isError: true for error cases
110 | - Ensure all error messages are properly stringified JSON objects
111 |
112 | ## Logging Rules
113 |
114 | - DO NOT use console.log, console.error, or any other console methods for logging
115 | - All communication MUST be through JSON-RPC responses over stdio
116 | - For error conditions, use proper JSON-RPC error response format
117 | - For debug/info messages, include them in the response data structure
118 | - Status updates should be sent as proper JSON-RPC notifications
119 | - Never write directly to stdout/stderr as it will corrupt the JSON-RPC stream
120 |
121 | ## Commit Rules
122 |
123 | Commits must follow the Conventional Commits specification (https://www.conventionalcommits.org/):
124 |
125 | 1. Format: `<type>(<scope>): <description>`
126 |
127 | - `<type>`: The type of change being made:
128 | - feat: A new feature
129 | - fix: A bug fix
130 | - docs: Documentation only changes
131 | - style: Changes that do not affect the meaning of the code
132 | - refactor: A code change that neither fixes a bug nor adds a feature
133 | - perf: A code change that improves performance
134 | - test: Adding missing tests or correcting existing tests
135 | - chore: Changes to the build process or auxiliary tools
136 | - ci: Changes to CI configuration files and scripts
137 | - `<scope>`: Optional, indicates section of codebase (e.g., bot, server, tools)
138 | - `<description>`: Clear, concise description in present tense
139 |
140 | 2. Examples:
141 |
142 | - feat(bot): add block placement functionality
143 | - fix(server): resolve reconnection loop issue
144 | - docs(api): update tool documentation
145 | - refactor(core): simplify connection handling
146 |
147 | 3. Breaking Changes:
148 |
149 | - Include BREAKING CHANGE: in the commit footer
150 | - Example: feat(api)!: change tool response format
151 |
152 | 4. Body and Footer:
153 | - Optional but recommended for complex changes
154 | - Separated from header by blank line
155 | - Use bullet points for multiple changes
156 |
157 | ## Tool Handler Implementation Rules
158 |
159 | The MinecraftToolHandler bridges Mineflayer's bot capabilities to MCP tools. Each handler maps directly to bot functionality:
160 |
161 | 1. Navigation & Movement
162 |
163 | - `handleNavigateTo/handleNavigateRelative`: Uses Mineflayer pathfinding
164 | - Always provide progress callbacks for pathfinding operations
165 | - Handles coordinate translation between absolute/relative positions
166 | - Uses goals.GoalBlock/goals.GoalXZ from mineflayer-pathfinder
167 |
168 | 2. Block Interaction
169 |
170 | - `handleDigBlock/handleDigBlockRelative`: Direct block breaking
171 | - `handleDigArea`: Area excavation with progress tracking
172 | - `handlePlaceBlock`: Block placement with item selection
173 | - `handleInspectBlock`: Block state inspection
174 | - Uses Vec3 for position handling
175 |
176 | 3. Entity Interaction
177 |
178 | - `handleFollowPlayer`: Player tracking with pathfinding
179 | - `handleAttackEntity`: Combat with entity targeting
180 | - Uses entity.position and entity.type from Mineflayer
181 |
182 | 4. Inventory Management
183 |
184 | - `handleInspectInventory`: Inventory querying
185 | - `handleCraftItem`: Crafting with/without tables
186 | - `handleSmeltItem`: Furnace operations
187 | - `handleEquipItem`: Equipment management
188 | - `handleDepositItem/handleWithdrawItem`: Container interactions
189 | - Uses window.items and container APIs
190 |
191 | 5. World Interaction
192 | - `handleChat`: In-game communication
193 | - `handleFindBlocks`: Block finding with constraints
194 | - `handleFindEntities`: Entity detection
195 | - `handleCheckPath`: Path validation
196 |
197 | Key Bot Methods Used:
198 |
199 | ```typescript
200 | // Core Movement
201 | bot.pathfinder.goto(goal: goals.Goal)
202 | bot.navigate.to(x: number, y: number, z: number)
203 |
204 | // Block Operations
205 | bot.dig(block: Block)
206 | bot.placeBlock(referenceBlock: Block, faceVector: Vec3)
207 |
208 | // Entity Interaction
209 | bot.attack(entity: Entity)
210 | bot.lookAt(position: Vec3)
211 |
212 | // Inventory
213 | bot.equip(item: Item, destination: string)
214 | bot.craft(recipe: Recipe, count: number, craftingTable: Block)
215 |
216 | // World Interaction
217 | bot.findBlocks(options: FindBlocksOptions)
218 | bot.blockAt(position: Vec3)
219 | bot.chat(message: string)
220 | ```
221 |
222 | Testing Focus:
223 |
224 | - Test each bot method integration
225 | - Verify coordinate systems (absolute vs relative)
226 | - Check entity targeting and tracking
227 | - Validate inventory operations
228 | - Test pathfinding edge cases
229 |
230 | Remember: Focus on Mineflayer's capabilities and proper bot method usage. The handler layer should cleanly map these capabilities to MCP tools while handling coordinate translations and progress tracking.
231 |
232 | ## JSON Response Formatting
233 |
234 | When implementing tool handlers that return structured data:
235 |
236 | 1. Avoid using `type: "json"` with `JSON.stringify` for nested objects
237 | 2. Instead, format complex data as human-readable text
238 | 3. Use template literals and proper formatting for nested structures
239 | 4. For lists of items, use bullet points or numbered lists
240 | 5. Include relevant units and round numbers appropriately
241 | 6. Make responses both machine-parseable and human-readable
242 |
243 | Examples:
244 | ✅ Good: `Found 3 blocks: \n- Stone at (10, 64, -30), distance: 5.2\n- Dirt at (11, 64, -30), distance: 5.5`
245 | ❌ Bad: `{"blocks":[{"name":"stone","position":{"x":10,"y":64,"z":-30}}]}`
246 |
247 | ## Building and Construction
248 |
249 | When implementing building functionality:
250 |
251 | 1. Always check inventory before attempting to place blocks
252 | 2. Use find_blocks to locate suitable building locations and materials
253 | 3. Combine digging and building operations for complete structures
254 | 4. Follow a clear building pattern:
255 | - Clear the area if needed (dig_area_relative)
256 | - Place foundation blocks first
257 | - Build walls from bottom to top
258 | - Add details like doors and windows last
259 | 5. Consider the bot's position and reachability:
260 | - Stay within reach distance (typically 4 blocks)
261 | - Move to new positions as needed
262 | - Ensure stable ground for the bot to stand on
263 | 6. Handle errors gracefully:
264 | - Check for block placement success
265 | - Have fallback positions for block placement
266 | - Log unreachable or problematic areas
267 |
268 | Example building sequence:
269 |
270 | 1. Survey area with find_blocks
271 | 2. Clear space with dig_area_relative
272 | 3. Check inventory for materials
273 | 4. Place foundation blocks
274 | 5. Build walls and roof
275 | 6. Add finishing touches
276 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # MCPMC (Minecraft Model Context Protocol)
2 |
3 | [](https://badge.fury.io/js/@gerred%2Fmcpmc)
4 | [](https://www.npmjs.com/package/@gerred/mcpmc)
5 | [](https://github.com/gerred/mcpmc/actions?query=workflow%3ACI)
6 | [](https://opensource.org/licenses/MIT)
7 |
8 | A Model Context Protocol (MCP) server for interacting with Minecraft via Mineflayer. This package enables AI agents to control Minecraft bots through a standardized JSON-RPC interface.
9 |
10 | ## Features
11 |
12 | - Full MCP compatibility for AI agent integration
13 | - Built on Mineflayer for reliable Minecraft interaction
14 | - Supports navigation, block manipulation, inventory management, and more
15 | - Real-time game state monitoring
16 | - Type-safe API with TypeScript support
17 |
18 | ## Installation
19 |
20 | ```bash
21 | # Using npm
22 | npm install @gerred/mcpmc
23 |
24 | # Using yarn
25 | yarn add @gerred/mcpmc
26 |
27 | # Using bun
28 | bun add @gerred/mcpmc
29 | ```
30 |
31 | ## Usage
32 |
33 | ```bash
34 | # Start the MCP server
35 | mcpmc
36 | ```
37 |
38 | The server communicates via stdin/stdout using the Model Context Protocol. For detailed API documentation, use the MCP inspector:
39 |
40 | ```bash
41 | bun run inspector
42 | ```
43 |
44 | ## Development
45 |
46 | ```bash
47 | # Install dependencies
48 | bun install
49 |
50 | # Run tests
51 | bun test
52 |
53 | # Build the project
54 | bun run build
55 |
56 | # Watch mode during development
57 | bun run watch
58 |
59 | # Run MCP inspector
60 | bun run inspector
61 | ```
62 |
63 | ## Contributing
64 |
65 | Contributions are welcome! Please follow these steps:
66 |
67 | 1. Fork the repository
68 | 2. Create a new branch for your feature
69 | 3. Write tests for your changes
70 | 4. Make your changes
71 | 5. Run tests and ensure they pass
72 | 6. Submit a pull request
73 |
74 | Please make sure to update tests as appropriate and adhere to the existing coding style.
75 |
76 | ## License
77 |
78 | MIT License
79 |
80 | Copyright (c) 2024 Gerred Dillon
81 |
82 | Permission is hereby granted, free of charge, to any person obtaining a copy
83 | of this software and associated documentation files (the "Software"), to deal
84 | in the Software without restriction, including without limitation the rights
85 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
86 | copies of the Software, and to permit persons to whom the Software is
87 | furnished to do so, subject to the following conditions:
88 |
89 | The above copyright notice and this permission notice shall be included in all
90 | copies or substantial portions of the Software.
91 |
92 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
93 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
94 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
95 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
96 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
97 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
98 | SOFTWARE.
99 |
```
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
```markdown
1 | # Contributing to MCPMC
2 |
3 | We love your input! We want to make contributing to MCPMC as easy and transparent as possible, whether it's:
4 |
5 | - Reporting a bug
6 | - Discussing the current state of the code
7 | - Submitting a fix
8 | - Proposing new features
9 | - Becoming a maintainer
10 |
11 | ## We Develop with GitHub
12 |
13 | We use GitHub to host code, to track issues and feature requests, as well as accept pull requests.
14 |
15 | ## We Use [GitHub Flow](https://guides.github.com/introduction/flow/index.html)
16 |
17 | Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests:
18 |
19 | 1. Fork the repo and create your branch from `main`.
20 | 2. If you've added code that should be tested, add tests.
21 | 3. If you've changed APIs, update the documentation.
22 | 4. Ensure the test suite passes.
23 | 5. Make sure your code lints.
24 | 6. Issue that pull request!
25 |
26 | ## Any contributions you make will be under the MIT Software License
27 |
28 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
29 |
30 | ## Report bugs using GitHub's [issue tracker](https://github.com/gerred/mcpmc/issues)
31 |
32 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/gerred/mcpmc/issues/new); it's that easy!
33 |
34 | ## Write bug reports with detail, background, and sample code
35 |
36 | **Great Bug Reports** tend to have:
37 |
38 | - A quick summary and/or background
39 | - Steps to reproduce
40 | - Be specific!
41 | - Give sample code if you can.
42 | - What you expected would happen
43 | - What actually happens
44 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
45 |
46 | ## Use a Consistent Coding Style
47 |
48 | - Use TypeScript strict mode
49 | - 2 spaces for indentation rather than tabs
50 | - You can try running `bun test` for style unification
51 |
52 | ## License
53 |
54 | By contributing, you agree that your contributions will be licensed under its MIT License.
55 |
```
--------------------------------------------------------------------------------
/src/__tests__/setup.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { jest } from "@jest/globals";
2 |
3 | // Make jest available globally
4 | (global as any).jest = jest;
5 |
6 | jest.mock("mineflayer", () => ({
7 | createBot: jest.fn(),
8 | }));
9 |
```
--------------------------------------------------------------------------------
/src/types/tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | export interface ToolResponse {
2 | _meta?: {
3 | progressToken?: string | number;
4 | };
5 | content: Array<{
6 | type: string;
7 | text: string;
8 | }>;
9 | isError?: boolean;
10 | }
11 |
```
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
```javascript
1 | export default {
2 | preset: 'ts-jest/presets/default-esm',
3 | testEnvironment: 'node',
4 | roots: ['<rootDir>/src'],
5 | testMatch: ['**/__tests__/**/*.test.ts'],
6 | transform: {
7 | '^.+\\.tsx?$': [
8 | 'ts-jest',
9 | {
10 | useESM: true,
11 | },
12 | ],
13 | },
14 | moduleNameMapper: {
15 | '^(\\.{1,2}/.*)\\.js$': '$1',
16 | },
17 | transformIgnorePatterns: [
18 | 'node_modules/(?!@modelcontextprotocol)'
19 | ],
20 | setupFilesAfterEnv: ['<rootDir>/src/__tests__/setup.ts']
21 | };
22 |
```
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "[FEATURE] "
5 | labels: enhancement
6 | assignees: ""
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
```
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[BUG] "
5 | labels: bug
6 | assignees: ""
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **To Reproduce**
13 | Steps to reproduce the behavior:
14 |
15 | 1. Start server with '...'
16 | 2. Run command '....'
17 | 3. See error
18 |
19 | **Expected behavior**
20 | A clear and concise description of what you expected to happen.
21 |
22 | **Environment (please complete the following information):**
23 |
24 | - OS: [e.g. macOS, Windows]
25 | - Node.js version: [e.g. 18.0.0]
26 | - Bun version: [e.g. 1.0.0]
27 | - Minecraft version: [e.g. 1.20.4]
28 | - Package version: [e.g. 0.0.1]
29 |
30 | **Additional context**
31 | Add any other context about the problem here.
32 |
```
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v4
15 |
16 | - name: Setup Bun
17 | uses: oven-sh/setup-bun@v1
18 | with:
19 | bun-version: latest
20 |
21 | - name: Install dependencies
22 | run: bun install
23 |
24 | - name: Run tests
25 | run: bun test
26 |
27 | - name: Build
28 | run: bun run build
29 |
30 | lint:
31 | runs-on: ubuntu-latest
32 |
33 | steps:
34 | - uses: actions/checkout@v4
35 |
36 | - name: Setup Bun
37 | uses: oven-sh/setup-bun@v1
38 | with:
39 | bun-version: latest
40 |
41 | - name: Install dependencies
42 | run: bun install
43 |
44 | - name: Type check
45 | run: bun run tsc --noEmit
46 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | // Enable latest features
4 | "lib": ["ESNext", "DOM"],
5 | "target": "ESNext",
6 | "module": "ESNext",
7 | "moduleDetection": "force",
8 | "jsx": "react-jsx",
9 | "allowJs": true,
10 |
11 | // Updated module resolution
12 | "moduleResolution": "node",
13 | "allowImportingTsExtensions": true,
14 | "verbatimModuleSyntax": true,
15 | "noEmit": true,
16 | "resolveJsonModule": true,
17 | "esModuleInterop": true,
18 |
19 | // Best practices
20 | "strict": true,
21 | "skipLibCheck": true,
22 | "noFallthroughCasesInSwitch": true,
23 |
24 | // Some stricter flags (disabled by default)
25 | "noUnusedLocals": false,
26 | "noUnusedParameters": false,
27 | "noPropertyAccessFromIndexSignature": false
28 | },
29 | "exclude": [
30 | "node_modules",
31 | "dist",
32 | "build",
33 | "coverage"
34 | ]
35 | }
36 |
```
--------------------------------------------------------------------------------
/knowledge.md:
--------------------------------------------------------------------------------
```markdown
1 | # CLI Publishing
2 |
3 | Package is configured as a CLI tool:
4 | - Binary name: `mcpmc`
5 | - Executable: `build/index.js`
6 | - Global install: `npm install -g @gerred/mcpmc`
7 | - Required files included in npm package:
8 | - build/index.js (executable)
9 | - README.md
10 | - LICENSE
11 | - package.json
12 |
13 | The build script makes the output file executable with `chmod +x`. The shebang line `#!/usr/bin/env node` ensures it runs with Node.js when installed globally.
14 |
15 | # Publishing Process
16 |
17 | 1. Run tests and build: `bun test && bun run build`
18 | 2. Bump version: `npm version patch|minor|major`
19 | 3. Push changes: `git push && git push --tags`
20 | 4. Publish: `npm publish --otp=<code>`
21 | - Requires 2FA authentication
22 | - Get OTP code from authenticator app
23 | - Package will be published to npm registry with public access
24 |
```
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 |
3 | export const cliSchema = z.object({
4 | host: z.string().default("localhost"),
5 | port: z.number().int().min(1).max(65535).default(25565),
6 | username: z.string().min(1).default("Claude"),
7 | });
8 |
9 | export type CLIArgs = z.infer<typeof cliSchema>;
10 |
11 | export function parseArgs(args: string[]): CLIArgs {
12 | const parsedArgs: Record<string, string | number> = {};
13 |
14 | for (let i = 0; i < args.length; i++) {
15 | const arg = args[i];
16 | if (arg.startsWith("--")) {
17 | const key = arg.slice(2);
18 | const value = args[i + 1];
19 | if (value && !value.startsWith("--")) {
20 | parsedArgs[key] = key === "port" ? parseInt(value, 10) : value;
21 | i++; // Skip the value in next iteration
22 | }
23 | }
24 | }
25 |
26 | // Parse with schema and get defaults
27 | return cliSchema.parse({
28 | host: parsedArgs.host || undefined,
29 | port: parsedArgs.port || undefined,
30 | username: parsedArgs.username || undefined,
31 | });
32 | }
33 |
```
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
```markdown
1 | ## Description
2 |
3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context.
4 |
5 | Fixes # (issue)
6 |
7 | ## Type of change
8 |
9 | Please delete options that are not relevant.
10 |
11 | - [ ] Bug fix (non-breaking change which fixes an issue)
12 | - [ ] New feature (non-breaking change which adds functionality)
13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
14 | - [ ] This change requires a documentation update
15 |
16 | ## How Has This Been Tested?
17 |
18 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce.
19 |
20 | - [ ] Test A
21 | - [ ] Test B
22 |
23 | ## Checklist:
24 |
25 | - [ ] My code follows the style guidelines of this project
26 | - [ ] I have performed a self-review of my own code
27 | - [ ] I have commented my code, particularly in hard-to-understand areas
28 | - [ ] I have made corresponding changes to the documentation
29 | - [ ] My changes generate no new warnings
30 | - [ ] I have added tests that prove my fix is effective or that my feature works
31 | - [ ] New and existing unit tests pass locally with my changes
32 | - [ ] Any dependent changes have been merged and published in downstream modules
33 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | // Only if this file is run directly or via npx, create and start the server
4 | if (
5 | import.meta.url === `file://${process.argv[1]}` ||
6 | process.argv[1]?.includes("mcpmc")
7 | ) {
8 | const { MinecraftServer } = await import("./server.js");
9 | const { parseArgs } = await import("./cli.js");
10 |
11 | try {
12 | const connectionParams = parseArgs(process.argv.slice(2));
13 | const server = new MinecraftServer(connectionParams);
14 |
15 | // Suppress deprecation warnings
16 | process.removeAllListeners("warning");
17 | process.on("warning", (warning) => {
18 | if (warning.name !== "DeprecationWarning") {
19 | process.stderr.write(
20 | JSON.stringify({
21 | jsonrpc: "2.0",
22 | method: "system.warning",
23 | params: {
24 | message: warning.toString(),
25 | type: "warning",
26 | },
27 | }) + "\n"
28 | );
29 | }
30 | });
31 |
32 | await server.start();
33 | } catch (error: unknown) {
34 | throw {
35 | code: -32000,
36 | message: "Server startup failed",
37 | data: {
38 | error: error instanceof Error ? error.message : String(error),
39 | },
40 | };
41 | }
42 | }
43 |
44 | export * from "./server.js";
45 | export * from "./schemas.js";
46 | export * from "./tools/index.js";
47 | export * from "./core/bot.js";
48 | export * from "./handlers/tools.js";
49 | export * from "./handlers/resources.js";
50 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@gerred/mcpmc",
3 | "version": "0.0.9",
4 | "description": "A MCP server for interacting with Minecraft via Mineflayer",
5 | "private": false,
6 | "type": "module",
7 | "bin": {
8 | "mcpmc": "./build/index.js"
9 | },
10 | "files": [
11 | "build",
12 | "README.md"
13 | ],
14 | "publishConfig": {
15 | "access": "public"
16 | },
17 | "scripts": {
18 | "build": "bun build ./src/index.ts --outdir=build --target=node && chmod +x build/index.js",
19 | "prepare": "husky install && bun run build",
20 | "watch": "bun build ./src/index.ts --outdir=build --target=node --watch",
21 | "inspector": "bunx @modelcontextprotocol/inspector build/index.js",
22 | "start": "bun run build/index.js",
23 | "test": "bun test",
24 | "test:watch": "bun test --watch",
25 | "test:coverage": "bun test --coverage"
26 | },
27 | "dependencies": {
28 | "@modelcontextprotocol/inspector": "https://github.com/modelcontextprotocol/inspector.git#main",
29 | "@modelcontextprotocol/sdk": "1.0.4",
30 | "bunx": "^0.1.0",
31 | "mineflayer": "^4.23.0",
32 | "mineflayer-pathfinder": "^2.4.5",
33 | "vec3": "^0.1.10",
34 | "zod-to-json-schema": "^3.24.1"
35 | },
36 | "devDependencies": {
37 | "@types/bun": "latest",
38 | "@types/jest": "^29.5.14",
39 | "husky": "^9.1.7",
40 | "jest": "^29.7.0",
41 | "ts-jest": "^29.2.5",
42 | "typescript": "^5.3.3"
43 | },
44 | "module": "index.ts",
45 | "packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
46 | "repository": {
47 | "type": "git",
48 | "url": "git+https://github.com/gerred/mcpmc.git"
49 | },
50 | "keywords": [
51 | "minecraft",
52 | "mineflayer",
53 | "ai",
54 | "bot",
55 | "mcp",
56 | "claude"
57 | ],
58 | "author": "Gerred Dillon",
59 | "license": "MIT",
60 | "bugs": {
61 | "url": "https://github.com/gerred/mcpmc/issues"
62 | },
63 | "homepage": "https://github.com/gerred/mcpmc#readme"
64 | }
65 |
```
--------------------------------------------------------------------------------
/src/__tests__/bot.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, beforeEach } from "@jest/globals";
2 | import { MockMinecraftBot } from "./mocks/mockBot";
3 | import type { MinecraftBot } from "../types/minecraft";
4 |
5 | describe("MinecraftBot", () => {
6 | let bot: MinecraftBot;
7 |
8 | beforeEach(() => {
9 | bot = new MockMinecraftBot({
10 | host: "localhost",
11 | port: 25565,
12 | username: "testBot",
13 | });
14 | });
15 |
16 | describe("connection", () => {
17 | it("should initialize with default position", () => {
18 | expect(bot.getPosition()).toMatchObject({ x: 0, y: 64, z: 0 });
19 | });
20 |
21 | it("should return position after initialization", () => {
22 | const pos = bot.getPosition();
23 | expect(pos).toMatchObject({ x: 0, y: 64, z: 0 });
24 | });
25 |
26 | it("should throw on operations when not connected", () => {
27 | bot.disconnect();
28 | expect(() => bot.getHealth()).toThrow("Not connected");
29 | expect(() => bot.getInventory()).toThrow("Not connected");
30 | expect(() => bot.getPlayers()).toThrow("Not connected");
31 | });
32 | });
33 |
34 | describe("navigation", () => {
35 | it("should update position after navigation", async () => {
36 | await bot.navigateTo(100, 64, 100);
37 | const pos = bot.getPosition();
38 | expect(pos).toMatchObject({ x: 100, y: 64, z: 100 });
39 | });
40 |
41 | it("should update position after relative navigation", async () => {
42 | await bot.navigateRelative(10, 0, 10);
43 | const pos = bot.getPosition();
44 | expect(pos).toMatchObject({ x: 10, y: 64, z: 10 });
45 | });
46 | });
47 |
48 | describe("game state", () => {
49 | it("should return health status", () => {
50 | const health = bot.getHealthStatus();
51 | expect(health).toMatchObject({
52 | health: 20,
53 | food: 20,
54 | saturation: 5,
55 | armor: 0,
56 | });
57 | });
58 |
59 | it("should return weather status", () => {
60 | const weather = bot.getWeather();
61 | expect(weather).toMatchObject({
62 | isRaining: false,
63 | rainState: "clear",
64 | thunderState: 0,
65 | });
66 | });
67 | });
68 | });
69 |
```
--------------------------------------------------------------------------------
/src/__tests__/server.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { MinecraftServer } from "../server";
2 | import { MinecraftToolHandler } from "../handlers/tools";
3 | import type { MinecraftBot } from "../types/minecraft";
4 |
5 | describe("MinecraftServer", () => {
6 | let server: MinecraftServer;
7 | let toolHandler: MinecraftToolHandler;
8 | let mockBot: MinecraftBot;
9 |
10 | beforeEach(() => {
11 | mockBot = {
12 | chat: jest.fn(),
13 | navigateRelative: jest.fn(),
14 | digBlockRelative: jest.fn(),
15 | digAreaRelative: jest.fn(),
16 | } as unknown as MinecraftBot;
17 |
18 | server = new MinecraftServer({
19 | host: "localhost",
20 | port: 25565,
21 | username: "testBot",
22 | });
23 |
24 | toolHandler = new MinecraftToolHandler(mockBot);
25 | });
26 |
27 | describe("tool handling", () => {
28 | it("should handle chat tool", async () => {
29 | const result = await toolHandler.handleChat("hello");
30 | expect(result).toBeDefined();
31 | expect(result.content[0].text).toContain("hello");
32 | expect(mockBot.chat).toHaveBeenCalledWith("hello");
33 | });
34 |
35 | it("should handle navigate_relative tool", async () => {
36 | const result = await toolHandler.handleNavigateRelative(1, 0, 1);
37 | expect(result).toBeDefined();
38 | expect(result.content[0].text).toContain("Navigated relative");
39 | expect(mockBot.navigateRelative).toHaveBeenCalledWith(1, 0, 1, expect.any(Function));
40 | });
41 |
42 | it("should handle dig_block_relative tool", async () => {
43 | const result = await toolHandler.handleDigBlockRelative(1, 0, 1);
44 | expect(result).toBeDefined();
45 | expect(result.content[0].text).toContain("Dug block relative");
46 | expect(mockBot.digBlockRelative).toHaveBeenCalledWith(1, 0, 1);
47 | });
48 |
49 | it("should handle dig_area_relative tool", async () => {
50 | const result = await toolHandler.handleDigAreaRelative(
51 | { dx: 0, dy: 0, dz: 0 },
52 | { dx: 2, dy: 2, dz: 2 }
53 | );
54 | expect(result).toBeDefined();
55 | expect(result.content[0].text).toContain("Successfully completed");
56 | expect(mockBot.digAreaRelative).toHaveBeenCalledWith(
57 | { dx: 0, dy: 0, dz: 0 },
58 | { dx: 2, dy: 2, dz: 2 },
59 | expect.any(Function)
60 | );
61 | });
62 | });
63 | });
64 |
```
--------------------------------------------------------------------------------
/src/handlers/resources.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { MinecraftBot } from "../types/minecraft";
2 |
3 | export interface ResourceResponse {
4 | _meta?: {
5 | progressToken?: string | number;
6 | };
7 | contents: Array<{
8 | uri: string;
9 | mimeType: string;
10 | text: string;
11 | }>;
12 | }
13 |
14 | export interface ResourceHandler {
15 | handleGetPlayers(uri: string): Promise<ResourceResponse>;
16 | handleGetPosition(uri: string): Promise<ResourceResponse>;
17 | handleGetBlocksNearby(uri: string): Promise<ResourceResponse>;
18 | handleGetEntitiesNearby(uri: string): Promise<ResourceResponse>;
19 | handleGetInventory(uri: string): Promise<ResourceResponse>;
20 | handleGetHealth(uri: string): Promise<ResourceResponse>;
21 | handleGetWeather(uri: string): Promise<ResourceResponse>;
22 | }
23 |
24 | export class MinecraftResourceHandler implements ResourceHandler {
25 | constructor(private bot: MinecraftBot) {}
26 |
27 | async handleGetPlayers(uri: string): Promise<ResourceResponse> {
28 | const players = this.bot.getPlayers();
29 | return {
30 | _meta: {},
31 | contents: [
32 | {
33 | uri,
34 | mimeType: "application/json",
35 | text: JSON.stringify(players, null, 2),
36 | },
37 | ],
38 | };
39 | }
40 |
41 | async handleGetPosition(uri: string): Promise<ResourceResponse> {
42 | const position = this.bot.getPosition();
43 | return {
44 | _meta: {},
45 | contents: [
46 | {
47 | uri,
48 | mimeType: "application/json",
49 | text: JSON.stringify(position, null, 2),
50 | },
51 | ],
52 | };
53 | }
54 |
55 | async handleGetBlocksNearby(uri: string): Promise<ResourceResponse> {
56 | const blocks = this.bot.getBlocksNearby();
57 | return {
58 | _meta: {},
59 | contents: [
60 | {
61 | uri,
62 | mimeType: "application/json",
63 | text: JSON.stringify(blocks, null, 2),
64 | },
65 | ],
66 | };
67 | }
68 |
69 | async handleGetEntitiesNearby(uri: string): Promise<ResourceResponse> {
70 | const entities = this.bot.getEntitiesNearby();
71 | return {
72 | _meta: {},
73 | contents: [
74 | {
75 | uri,
76 | mimeType: "application/json",
77 | text: JSON.stringify(entities, null, 2),
78 | },
79 | ],
80 | };
81 | }
82 |
83 | async handleGetInventory(uri: string): Promise<ResourceResponse> {
84 | const inventory = this.bot.getInventory();
85 | return {
86 | _meta: {},
87 | contents: [
88 | {
89 | uri,
90 | mimeType: "application/json",
91 | text: JSON.stringify(inventory, null, 2),
92 | },
93 | ],
94 | };
95 | }
96 |
97 | async handleGetHealth(uri: string): Promise<ResourceResponse> {
98 | const health = this.bot.getHealthStatus();
99 | return {
100 | _meta: {},
101 | contents: [
102 | {
103 | uri,
104 | mimeType: "application/json",
105 | text: JSON.stringify(health, null, 2),
106 | },
107 | ],
108 | };
109 | }
110 |
111 | async handleGetWeather(uri: string): Promise<ResourceResponse> {
112 | const weather = this.bot.getWeather();
113 | return {
114 | _meta: {},
115 | contents: [
116 | {
117 | uri,
118 | mimeType: "application/json",
119 | text: JSON.stringify(weather, null, 2),
120 | },
121 | ],
122 | };
123 | }
124 | }
125 |
```
--------------------------------------------------------------------------------
/src/schemas.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 |
3 | // Base schemas
4 | export const PositionSchema = z.object({
5 | x: z.number(),
6 | y: z.number(),
7 | z: z.number(),
8 | });
9 |
10 | export const RelativePositionSchema = z.object({
11 | dx: z.number(),
12 | dy: z.number(),
13 | dz: z.number(),
14 | });
15 |
16 | // Tool input schemas
17 | export const ConnectSchema = z.object({
18 | host: z.string(),
19 | port: z.number().default(25565),
20 | username: z.string(),
21 | });
22 |
23 | export const ChatSchema = z.object({
24 | message: z.string(),
25 | });
26 |
27 | export const NavigateSchema = z.object({
28 | x: z.number(),
29 | y: z.number(),
30 | z: z.number(),
31 | });
32 |
33 | export const NavigateRelativeSchema = z.object({
34 | dx: z.number(),
35 | dy: z.number(),
36 | dz: z.number(),
37 | });
38 |
39 | export const DigBlockSchema = z.object({
40 | x: z.number(),
41 | y: z.number(),
42 | z: z.number(),
43 | });
44 |
45 | export const DigBlockRelativeSchema = z.object({
46 | dx: z.number(),
47 | dy: z.number(),
48 | dz: z.number(),
49 | });
50 |
51 | export const DigAreaSchema = z.object({
52 | start: PositionSchema,
53 | end: PositionSchema,
54 | });
55 |
56 | export const DigAreaRelativeSchema = z.object({
57 | start: RelativePositionSchema,
58 | end: RelativePositionSchema,
59 | });
60 |
61 | export const PlaceBlockSchema = z.object({
62 | x: z.number(),
63 | y: z.number(),
64 | z: z.number(),
65 | blockName: z.string(),
66 | });
67 |
68 | export const FollowPlayerSchema = z.object({
69 | username: z.string(),
70 | distance: z.number().default(2),
71 | });
72 |
73 | export const AttackEntitySchema = z.object({
74 | entityName: z.string(),
75 | maxDistance: z.number().default(5),
76 | });
77 |
78 | export const InspectBlockSchema = z.object({
79 | position: PositionSchema,
80 | includeState: z.boolean().default(true),
81 | });
82 |
83 | export const FindBlocksSchema = z.object({
84 | blockTypes: z.union([
85 | z.string(),
86 | z.array(z.string()),
87 | z.string().transform((str) => {
88 | try {
89 | // Handle string that looks like an array
90 | if (str.startsWith("[") && str.endsWith("]")) {
91 | const parsed = JSON.parse(str.replace(/'/g, '"'));
92 | return Array.isArray(parsed) ? parsed : [str];
93 | }
94 | return [str];
95 | } catch {
96 | return [str];
97 | }
98 | }),
99 | ]),
100 | maxDistance: z.number().default(32),
101 | maxCount: z.number().default(1),
102 | constraints: z
103 | .object({
104 | minY: z.number().optional(),
105 | maxY: z.number().optional(),
106 | requireReachable: z.boolean().default(false),
107 | })
108 | .optional(),
109 | });
110 |
111 | export const FindEntitiesSchema = z.object({
112 | entityTypes: z.array(z.string()),
113 | maxDistance: z.number().default(32),
114 | maxCount: z.number().default(1),
115 | constraints: z
116 | .object({
117 | mustBeVisible: z.boolean().default(false),
118 | inFrontOnly: z.boolean().default(false),
119 | minHealth: z.number().optional(),
120 | maxHealth: z.number().optional(),
121 | })
122 | .optional(),
123 | });
124 |
125 | export const CheckPathSchema = z.object({
126 | destination: PositionSchema,
127 | dryRun: z.boolean().default(true),
128 | includeObstacles: z.boolean().default(false),
129 | });
130 |
131 | // Response schemas
132 | export const ToolResponseSchema = z.object({
133 | _meta: z.object({}).optional(),
134 | content: z.array(
135 | z.object({
136 | type: z.string(),
137 | text: z.string(),
138 | })
139 | ),
140 | isError: z.boolean().optional(),
141 | });
142 |
143 | export type ToolResponse = z.infer<typeof ToolResponseSchema>;
144 | export type Position = z.infer<typeof PositionSchema>;
145 | export type RelativePosition = z.infer<typeof RelativePositionSchema>;
146 |
```
--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { zodToJsonSchema } from "zod-to-json-schema";
2 | import type { Tool } from "@modelcontextprotocol/sdk/types.js";
3 | import { z } from "zod";
4 | import {
5 | ChatSchema,
6 | NavigateRelativeSchema,
7 | DigBlockRelativeSchema,
8 | DigAreaRelativeSchema,
9 | FollowPlayerSchema,
10 | AttackEntitySchema,
11 | FindBlocksSchema,
12 | FindEntitiesSchema,
13 | } from "../schemas.js";
14 |
15 | type InputSchema = {
16 | type: "object";
17 | properties?: Record<string, unknown>;
18 | [k: string]: unknown;
19 | };
20 |
21 | const toInputSchema = (schema: z.ZodType): InputSchema => ({
22 | ...zodToJsonSchema(schema),
23 | type: "object",
24 | });
25 |
26 | const CraftItemSchema = z.object({
27 | itemName: z.string(),
28 | quantity: z.number().optional(),
29 | useCraftingTable: z.boolean().optional(),
30 | });
31 |
32 | const SmeltItemSchema = z.object({
33 | itemName: z.string(),
34 | fuelName: z.string(),
35 | quantity: z.number().optional(),
36 | });
37 |
38 | const EquipItemSchema = z.object({
39 | itemName: z.string(),
40 | destination: z.enum(["hand", "off-hand", "head", "torso", "legs", "feet"]),
41 | });
42 |
43 | const ContainerInteractionSchema = z.object({
44 | containerPosition: z.object({
45 | x: z.number(),
46 | y: z.number(),
47 | z: z.number(),
48 | }),
49 | itemName: z.string(),
50 | quantity: z.number().optional(),
51 | });
52 |
53 | export const MINECRAFT_TOOLS: Tool[] = [
54 | {
55 | name: "chat",
56 | description: "Send a chat message to the server",
57 | inputSchema: toInputSchema(ChatSchema),
58 | },
59 | {
60 | name: "navigate_relative",
61 | description:
62 | "Make the bot walk relative to its current position. dx moves right(+)/left(-), dy moves up(+)/down(-), dz moves forward(+)/back(-) relative to bot's current position and orientation",
63 | inputSchema: toInputSchema(NavigateRelativeSchema),
64 | },
65 | {
66 | name: "dig_block_relative",
67 | description:
68 | "Dig a single block relative to the bot's current position. dx moves right(+)/left(-), dy moves up(+)/down(-), dz moves forward(+)/back(-) relative to bot's current position and orientation",
69 | inputSchema: toInputSchema(DigBlockRelativeSchema),
70 | },
71 | {
72 | name: "dig_area_relative",
73 | description:
74 | "Dig multiple blocks in an area relative to the bot's current position. Coordinates use the same relative system as dig_block_relative. Use this for clearing spaces.",
75 | inputSchema: toInputSchema(DigAreaRelativeSchema),
76 | },
77 | {
78 | name: "place_block",
79 | description:
80 | "Place a block from the bot's inventory at the specified position. Use this for building structures.",
81 | inputSchema: toInputSchema(
82 | z.object({
83 | x: z.number(),
84 | y: z.number(),
85 | z: z.number(),
86 | blockName: z.string(),
87 | })
88 | ),
89 | },
90 | {
91 | name: "find_blocks",
92 | description:
93 | "Find nearby blocks of specific types. Use this to locate building materials or identify terrain.",
94 | inputSchema: toInputSchema(FindBlocksSchema),
95 | },
96 | {
97 | name: "craft_item",
98 | description:
99 | "Craft items using materials in inventory. Can use a crafting table if specified.",
100 | inputSchema: toInputSchema(CraftItemSchema),
101 | },
102 | {
103 | name: "inspect_inventory",
104 | description:
105 | "Check the contents of the bot's inventory to see available materials.",
106 | inputSchema: toInputSchema(
107 | z.object({
108 | itemType: z.string().optional(),
109 | includeEquipment: z.boolean().optional(),
110 | })
111 | ),
112 | },
113 | {
114 | name: "follow_player",
115 | description: "Make the bot follow a specific player",
116 | inputSchema: toInputSchema(FollowPlayerSchema),
117 | },
118 | {
119 | name: "attack_entity",
120 | description: "Attack a specific entity near the bot",
121 | inputSchema: toInputSchema(AttackEntitySchema),
122 | },
123 | ];
124 |
```
--------------------------------------------------------------------------------
/src/types/minecraft.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { Entity as PrismarineEntity } from "prismarine-entity";
2 | import type { Block as PrismarineBlock } from "prismarine-block";
3 | import type { Item as PrismarineItem } from "prismarine-item";
4 | import { Vec3 } from "vec3";
5 |
6 | export interface Position {
7 | x: number;
8 | y: number;
9 | z: number;
10 | }
11 |
12 | export interface Block {
13 | position: Vec3;
14 | type: number;
15 | name: string;
16 | hardness: number;
17 | }
18 |
19 | export interface Entity {
20 | name: string;
21 | type: string;
22 | position: Vec3;
23 | velocity: Vec3;
24 | health: number;
25 | }
26 |
27 | export interface InventoryItem {
28 | name: string;
29 | count: number;
30 | slot: number;
31 | }
32 |
33 | export interface Player {
34 | username: string;
35 | uuid: string;
36 | ping: number;
37 | }
38 |
39 | export interface HealthStatus {
40 | health: number;
41 | food: number;
42 | saturation: number;
43 | armor: number;
44 | }
45 |
46 | export interface Weather {
47 | isRaining: boolean;
48 | rainState: "clear" | "raining";
49 | thunderState: number;
50 | }
51 |
52 | export interface Recipe {
53 | name: string;
54 | ingredients: { [itemName: string]: number };
55 | requiresCraftingTable: boolean;
56 | }
57 |
58 | export interface Container {
59 | type: "chest" | "furnace" | "crafting_table";
60 | position: Position;
61 | slots: { [slot: number]: InventoryItem | null };
62 | }
63 |
64 | /**
65 | * Core interface for the bot. Each method is a single action
66 | * that an LLM agent can call in multiple steps.
67 | */
68 | export interface MinecraftBot {
69 | // ---- Connection ----
70 | connect(host: string, port: number, username: string): Promise<void>;
71 | disconnect(): void;
72 |
73 | // ---- Chat ----
74 | chat(message: string): void;
75 |
76 | // ---- State & Info ----
77 | getPosition(): Position | null;
78 | getHealth(): number;
79 | getInventory(): InventoryItem[];
80 | getPlayers(): Player[];
81 | getBlocksNearby(maxDistance?: number, count?: number): Block[];
82 | getEntitiesNearby(maxDistance?: number): Entity[];
83 | getHealthStatus(): HealthStatus;
84 | getWeather(): Weather;
85 |
86 | // ---- Relative Movement & Actions ----
87 | navigateRelative(
88 | dx: number,
89 | dy: number,
90 | dz: number,
91 | progressCallback?: (progress: number) => void
92 | ): Promise<void>;
93 | navigateTo(x: number, y: number, z: number): Promise<void>;
94 | digBlockRelative(dx: number, dy: number, dz: number): Promise<void>;
95 | digAreaRelative(
96 | start: { dx: number; dy: number; dz: number },
97 | end: { dx: number; dy: number; dz: number },
98 | progressCallback?: (
99 | progress: number,
100 | blocksDug: number,
101 | totalBlocks: number
102 | ) => void
103 | ): Promise<void>;
104 | placeBlock(x: number, y: number, z: number, blockName: string): Promise<void>;
105 |
106 | // ---- Entity Interaction ----
107 | followPlayer(username: string, distance?: number): Promise<void>;
108 | attackEntity(entityName: string, maxDistance?: number): Promise<void>;
109 |
110 | // ---- Block & Pathfinding Info ----
111 | blockAt(position: Vec3): Block | null;
112 | findBlocks(options: {
113 | matching: (block: Block) => boolean;
114 | maxDistance: number;
115 | count: number;
116 | point?: Vec3;
117 | }): Vec3[];
118 | getEquipmentDestSlot(destination: string): number;
119 | canSeeEntity(entity: Entity): boolean;
120 |
121 | // ---- Crafting & Item Management ----
122 | craftItem(
123 | itemName: string,
124 | quantity?: number,
125 | useCraftingTable?: boolean
126 | ): Promise<void>;
127 | smeltItem(
128 | itemName: string,
129 | fuelName: string,
130 | quantity?: number
131 | ): Promise<void>;
132 | equipItem(
133 | itemName: string,
134 | destination: "hand" | "off-hand" | "head" | "torso" | "legs" | "feet"
135 | ): Promise<void>;
136 | depositItem(
137 | containerPosition: Position,
138 | itemName: string,
139 | quantity?: number
140 | ): Promise<void>;
141 | withdrawItem(
142 | containerPosition: Position,
143 | itemName: string,
144 | quantity?: number
145 | ): Promise<void>;
146 |
147 | // ---- Expose underlying info for reference ----
148 | readonly entity: {
149 | position: Vec3;
150 | velocity: Vec3;
151 | yaw: number;
152 | pitch: number;
153 | };
154 | readonly entities: { [id: string]: Entity };
155 | readonly inventory: {
156 | items: () => InventoryItem[];
157 | slots: { [slot: string]: InventoryItem | null };
158 | };
159 | readonly pathfinder: any;
160 | }
161 |
162 | // Utility classes for type conversion between prismarine-xxx and your interfaces
163 | export class TypeConverters {
164 | static entity(entity: PrismarineEntity): Entity {
165 | return {
166 | name: entity.name || "unknown",
167 | type: entity.type || "unknown",
168 | position: entity.position,
169 | velocity: entity.velocity,
170 | health: entity.health || 0,
171 | };
172 | }
173 |
174 | static block(block: PrismarineBlock): Block {
175 | return {
176 | position: block.position,
177 | type: block.type,
178 | name: block.name,
179 | hardness: block.hardness || 0,
180 | };
181 | }
182 |
183 | static item(item: PrismarineItem): InventoryItem {
184 | return {
185 | name: item.name,
186 | count: item.count,
187 | slot: item.slot,
188 | };
189 | }
190 | }
191 |
192 | export type { ToolResponse } from "./tools";
193 |
```
--------------------------------------------------------------------------------
/src/__tests__/mocks/mockBot.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { EventEmitter } from "events";
2 | import type { MinecraftBot } from "../../types/minecraft";
3 | import type {
4 | Player,
5 | InventoryItem,
6 | Entity,
7 | Block,
8 | HealthStatus,
9 | Weather,
10 | Position,
11 | Recipe,
12 | Container,
13 | } from "../../types/minecraft";
14 | import { Vec3 } from "vec3";
15 | import { goals, Movements } from "mineflayer-pathfinder";
16 |
17 | interface ConnectionParams {
18 | host: string;
19 | port: number;
20 | username: string;
21 | }
22 |
23 | export class MockMinecraftBot extends EventEmitter implements MinecraftBot {
24 | private position = { x: 0, y: 64, z: 0 };
25 | private isConnected = true;
26 | private _inventory: { items: InventoryItem[] } = { items: [] };
27 | private connectCount = 0;
28 | private _blocks: { [key: string]: string } = {};
29 |
30 | get entity() {
31 | if (!this.isConnected) throw new Error("Not connected");
32 | return {
33 | position: new Vec3(this.position.x, this.position.y, this.position.z),
34 | velocity: new Vec3(0, 0, 0),
35 | yaw: 0,
36 | pitch: 0,
37 | };
38 | }
39 |
40 | get entities() {
41 | if (!this.isConnected) throw new Error("Not connected");
42 | return {};
43 | }
44 |
45 | get inventory() {
46 | if (!this.isConnected) throw new Error("Not connected");
47 | return {
48 | items: () => this._inventory.items,
49 | slots: {},
50 | };
51 | }
52 |
53 | get pathfinder() {
54 | if (!this.isConnected) throw new Error("Not connected");
55 | return {
56 | setMovements: () => {},
57 | goto: () => Promise.resolve(),
58 | getPathTo: () => Promise.resolve(null),
59 | };
60 | }
61 |
62 | constructor(private connectionParams: ConnectionParams) {
63 | super();
64 | setTimeout(() => {
65 | this.emit("spawn");
66 | }, 0);
67 | }
68 |
69 | async connect(host: string, port: number, username: string): Promise<void> {
70 | this.isConnected = true;
71 | this.connectCount++;
72 | setTimeout(() => {
73 | this.emit("spawn");
74 | }, 10);
75 | return Promise.resolve();
76 | }
77 |
78 | disconnect(): void {
79 | if (this.isConnected) {
80 | this.isConnected = false;
81 | this.emit("end");
82 | }
83 | }
84 |
85 | chat(message: string): void {
86 | if (!this.isConnected) throw new Error("Not connected");
87 | }
88 |
89 | getPosition() {
90 | if (!this.isConnected) throw new Error("Not connected");
91 | return { ...this.position };
92 | }
93 |
94 | getHealth() {
95 | if (!this.isConnected) throw new Error("Not connected");
96 | return 20;
97 | }
98 |
99 | getHealthStatus() {
100 | if (!this.isConnected) throw new Error("Not connected");
101 | return {
102 | health: 20,
103 | food: 20,
104 | saturation: 5,
105 | armor: 0,
106 | };
107 | }
108 |
109 | getWeather(): Weather {
110 | if (!this.isConnected) throw new Error("Not connected");
111 | return {
112 | isRaining: false,
113 | rainState: "clear",
114 | thunderState: 0,
115 | };
116 | }
117 |
118 | getInventory() {
119 | if (!this.isConnected) throw new Error("Not connected");
120 | return this._inventory.items;
121 | }
122 |
123 | getPlayers() {
124 | if (!this.isConnected) throw new Error("Not connected");
125 | return [];
126 | }
127 |
128 | async navigateTo(x: number, y: number, z: number) {
129 | if (!this.isConnected) throw new Error("Not connected");
130 | this.position = { x, y, z };
131 | }
132 |
133 | async navigateRelative(dx: number, dy: number, dz: number) {
134 | if (!this.isConnected) throw new Error("Not connected");
135 | this.position = {
136 | x: this.position.x + dx,
137 | y: this.position.y + dy,
138 | z: this.position.z + dz,
139 | };
140 | }
141 |
142 | async digBlock(x: number, y: number, z: number) {
143 | if (!this.isConnected) throw new Error("Not connected");
144 | }
145 |
146 | async digArea(start: any, end: any) {
147 | if (!this.isConnected) throw new Error("Not connected");
148 | }
149 |
150 | async placeBlock(
151 | x: number,
152 | y: number,
153 | z: number,
154 | blockName: string
155 | ): Promise<void> {
156 | if (!this.isConnected) throw new Error("Not connected");
157 | this._blocks[`${x},${y},${z}`] = blockName;
158 | }
159 |
160 | async followPlayer(username: string, distance: number) {
161 | if (!this.isConnected) throw new Error("Not connected");
162 | }
163 |
164 | async attackEntity(entityName: string, maxDistance: number) {
165 | if (!this.isConnected) throw new Error("Not connected");
166 | }
167 |
168 | getEntitiesNearby(maxDistance?: number): Entity[] {
169 | if (!this.isConnected) throw new Error("Not connected");
170 | return [];
171 | }
172 |
173 | getBlocksNearby(maxDistance?: number, count?: number): Block[] {
174 | if (!this.isConnected) throw new Error("Not connected");
175 | return [];
176 | }
177 |
178 | async digBlockRelative(dx: number, dy: number, dz: number): Promise<void> {
179 | if (!this.isConnected) throw new Error("Not connected");
180 | }
181 |
182 | async digAreaRelative(
183 | start: { dx: number; dy: number; dz: number },
184 | end: { dx: number; dy: number; dz: number },
185 | progressCallback?: (
186 | progress: number,
187 | blocksDug: number,
188 | totalBlocks: number
189 | ) => void
190 | ): Promise<void> {
191 | if (!this.isConnected) throw new Error("Not connected");
192 | if (progressCallback) {
193 | progressCallback(100, 1, 1);
194 | }
195 | }
196 |
197 | blockAt(position: Vec3): Block | null {
198 | if (!this.isConnected) throw new Error("Not connected");
199 | return null;
200 | }
201 |
202 | findBlocks(options: {
203 | matching: ((block: Block) => boolean) | string | string[];
204 | maxDistance: number;
205 | count: number;
206 | point?: Vec3;
207 | }): Vec3[] {
208 | if (!this.isConnected) throw new Error("Not connected");
209 | return [];
210 | }
211 |
212 | getEquipmentDestSlot(destination: string): number {
213 | if (!this.isConnected) throw new Error("Not connected");
214 | return 0;
215 | }
216 |
217 | canSeeEntity(entity: Entity): boolean {
218 | if (!this.isConnected) throw new Error("Not connected");
219 | return false;
220 | }
221 |
222 | async craftItem(
223 | itemName: string,
224 | quantity?: number,
225 | useCraftingTable?: boolean
226 | ): Promise<void> {
227 | if (!this.isConnected) throw new Error("Not connected");
228 | }
229 |
230 | async equipItem(itemName: string, destination: string): Promise<void> {
231 | if (!this.isConnected) throw new Error("Not connected");
232 | }
233 |
234 | async dropItem(itemName: string, quantity?: number): Promise<void> {
235 | if (!this.isConnected) throw new Error("Not connected");
236 | }
237 |
238 | async openContainer(position: Position): Promise<Container> {
239 | if (!this.isConnected) throw new Error("Not connected");
240 | return {
241 | type: "chest",
242 | position,
243 | slots: {},
244 | };
245 | }
246 |
247 | closeContainer(): void {
248 | if (!this.isConnected) throw new Error("Not connected");
249 | }
250 |
251 | getRecipe(itemName: string): Recipe | null {
252 | if (!this.isConnected) throw new Error("Not connected");
253 | return null;
254 | }
255 |
256 | listAvailableRecipes(): Recipe[] {
257 | if (!this.isConnected) throw new Error("Not connected");
258 | return [];
259 | }
260 |
261 | async smeltItem(
262 | itemName: string,
263 | fuelName: string,
264 | quantity?: number
265 | ): Promise<void> {
266 | if (!this.isConnected) throw new Error("Not connected");
267 | }
268 |
269 | async depositItem(
270 | containerPosition: Position,
271 | itemName: string,
272 | quantity?: number
273 | ): Promise<void> {
274 | if (!this.isConnected) throw new Error("Not connected");
275 | }
276 |
277 | async withdrawItem(
278 | containerPosition: Position,
279 | itemName: string,
280 | quantity?: number
281 | ): Promise<void> {
282 | if (!this.isConnected) throw new Error("Not connected");
283 | }
284 |
285 | canCraft(recipe: Recipe): boolean {
286 | if (!this.isConnected) throw new Error("Not connected");
287 | return false;
288 | }
289 |
290 | getConnectCount(): number {
291 | return this.connectCount;
292 | }
293 | }
294 |
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3 | import {
4 | CallToolRequestSchema,
5 | ListToolsRequestSchema,
6 | ReadResourceRequestSchema,
7 | } from "@modelcontextprotocol/sdk/types.js";
8 | import { z } from "zod";
9 | import { createBot } from "mineflayer";
10 | import type { Bot } from "mineflayer";
11 | import { pathfinder, goals, Movements } from "mineflayer-pathfinder";
12 | import type { Pathfinder } from "mineflayer-pathfinder";
13 | import { Vec3 } from "vec3";
14 | import { MinecraftToolHandler } from "./handlers/tools.js";
15 | import { MINECRAFT_TOOLS } from "./tools/index.js";
16 | import * as schemas from "./schemas.js";
17 | import { cliSchema } from "./cli.js";
18 | import type { MinecraftBot } from "./types/minecraft.js";
19 | import { MinecraftResourceHandler } from "./handlers/resources.js";
20 | import type { ResourceHandler } from "./handlers/resources.js";
21 | import type { ResourceResponse } from "./handlers/resources.js";
22 |
23 | const MINECRAFT_RESOURCES = [
24 | {
25 | name: "players",
26 | uri: "minecraft://players",
27 | description:
28 | "List of players currently on the server, including their usernames and connection info",
29 | mimeType: "application/json",
30 | },
31 | {
32 | name: "position",
33 | uri: "minecraft://position",
34 | description:
35 | "Current position of the bot in the world (x, y, z coordinates)",
36 | mimeType: "application/json",
37 | },
38 | {
39 | name: "blocks/nearby",
40 | uri: "minecraft://blocks/nearby",
41 | description:
42 | "List of blocks in the bot's vicinity, including their positions and types",
43 | mimeType: "application/json",
44 | },
45 | {
46 | name: "entities/nearby",
47 | uri: "minecraft://entities/nearby",
48 | description:
49 | "List of entities (players, mobs, items) near the bot, including their positions and types",
50 | mimeType: "application/json",
51 | },
52 | {
53 | name: "inventory",
54 | uri: "minecraft://inventory",
55 | description:
56 | "Current contents of the bot's inventory, including item names, counts, and slots",
57 | mimeType: "application/json",
58 | },
59 | {
60 | name: "health",
61 | uri: "minecraft://health",
62 | description: "Bot's current health, food, saturation, and armor status",
63 | mimeType: "application/json",
64 | },
65 | {
66 | name: "weather",
67 | uri: "minecraft://weather",
68 | description:
69 | "Current weather conditions in the game (clear, raining, thundering)",
70 | mimeType: "application/json",
71 | },
72 | ];
73 |
74 | interface ExtendedBot extends Bot {
75 | pathfinder: Pathfinder & {
76 | setMovements(movements: Movements): void;
77 | goto(goal: goals.Goal): Promise<void>;
78 | };
79 | }
80 |
81 | export class MinecraftServer {
82 | private server: Server;
83 | private bot: ExtendedBot | null = null;
84 | private toolHandler!: MinecraftToolHandler;
85 | private resourceHandler!: MinecraftResourceHandler;
86 | private connectionParams: z.infer<typeof cliSchema>;
87 | private isConnected: boolean = false;
88 | private reconnectAttempts: number = 0;
89 | private readonly maxReconnectAttempts: number = 3;
90 | private readonly reconnectDelay: number = 5000; // 5 seconds
91 |
92 | constructor(connectionParams: z.infer<typeof cliSchema>) {
93 | this.connectionParams = connectionParams;
94 | this.server = new Server(
95 | {
96 | name: "mineflayer-mcp-server",
97 | version: "0.1.0",
98 | },
99 | {
100 | capabilities: {
101 | tools: {
102 | enabled: true,
103 | },
104 | resources: {
105 | enabled: true,
106 | },
107 | },
108 | }
109 | );
110 |
111 | this.setupHandlers();
112 | }
113 |
114 | private sendJsonRpcNotification(method: string, params: any) {
115 | this.server
116 | .notification({
117 | method,
118 | params: JSON.parse(JSON.stringify(params)),
119 | })
120 | .catch((error) => {
121 | console.error("Failed to send notification:", error);
122 | });
123 | }
124 |
125 | private async connectBot(): Promise<void> {
126 | if (this.bot) {
127 | this.bot.end();
128 | this.bot = null;
129 | }
130 |
131 | const bot = createBot({
132 | host: this.connectionParams.host,
133 | port: this.connectionParams.port,
134 | username: this.connectionParams.username,
135 | hideErrors: false,
136 | }) as ExtendedBot;
137 |
138 | bot.loadPlugin(pathfinder);
139 | this.bot = bot;
140 |
141 | // Create a wrapper that implements MinecraftBot interface
142 | const wrapper: MinecraftBot = {
143 | chat: (message: string) => bot.chat(message),
144 | disconnect: () => bot.end(),
145 | getPosition: () => {
146 | const pos = bot.entity?.position;
147 | return pos ? { x: pos.x, y: pos.y, z: pos.z } : null;
148 | },
149 | getHealth: () => bot.health,
150 | getInventory: () =>
151 | bot.inventory.items().map((item) => ({
152 | name: item.name,
153 | count: item.count,
154 | slot: item.slot,
155 | })),
156 | getPlayers: () =>
157 | Object.values(bot.players).map((player) => ({
158 | username: player.username,
159 | uuid: player.uuid,
160 | ping: player.ping,
161 | })),
162 | navigateRelative: async (
163 | dx: number,
164 | dy: number,
165 | dz: number,
166 | progressCallback?: (progress: number) => void
167 | ) => {
168 | const pos = bot.entity.position;
169 | const yaw = bot.entity.yaw;
170 | const sin = Math.sin(yaw);
171 | const cos = Math.cos(yaw);
172 | const worldDx = dx * cos - dz * sin;
173 | const worldDz = dx * sin + dz * cos;
174 |
175 | const goal = new goals.GoalNear(
176 | pos.x + worldDx,
177 | pos.y + dy,
178 | pos.z + worldDz,
179 | 1
180 | );
181 | const startPos = bot.entity.position;
182 | const targetPos = new Vec3(
183 | pos.x + worldDx,
184 | pos.y + dy,
185 | pos.z + worldDz
186 | );
187 | const totalDistance = startPos.distanceTo(targetPos);
188 |
189 | // Set up progress monitoring
190 | const progressToken = Date.now().toString();
191 | const checkProgress = () => {
192 | if (!bot) return;
193 | const currentPos = bot.entity.position;
194 | const remainingDistance = currentPos.distanceTo(targetPos);
195 | const progress = Math.min(
196 | 100,
197 | ((totalDistance - remainingDistance) / totalDistance) * 100
198 | );
199 |
200 | if (progressCallback) {
201 | progressCallback(progress);
202 | }
203 |
204 | this.sendJsonRpcNotification("tool/progress", {
205 | token: progressToken,
206 | progress,
207 | status: progress < 100 ? "in_progress" : "complete",
208 | message: `Navigation progress: ${Math.round(progress)}%`,
209 | });
210 | };
211 |
212 | const progressInterval = setInterval(checkProgress, 500);
213 |
214 | try {
215 | await bot.pathfinder.goto(goal);
216 | } finally {
217 | clearInterval(progressInterval);
218 | // Send final progress
219 | if (progressCallback) {
220 | progressCallback(100);
221 | }
222 | this.sendJsonRpcNotification("tool/progress", {
223 | token: progressToken,
224 | progress: 100,
225 | status: "complete",
226 | message: "Navigation complete",
227 | });
228 | }
229 | },
230 | digBlockRelative: async (dx: number, dy: number, dz: number) => {
231 | const pos = bot.entity.position;
232 | const yaw = bot.entity.yaw;
233 | const sin = Math.sin(yaw);
234 | const cos = Math.cos(yaw);
235 | const worldDx = dx * cos - dz * sin;
236 | const worldDz = dx * sin + dz * cos;
237 | const block = bot.blockAt(
238 | new Vec3(
239 | Math.floor(pos.x + worldDx),
240 | Math.floor(pos.y + dy),
241 | Math.floor(pos.z + worldDz)
242 | )
243 | );
244 | if (!block) throw new Error("No block at relative position");
245 | await bot.dig(block);
246 | },
247 | digAreaRelative: async (start, end, progressCallback) => {
248 | const pos = bot.entity.position;
249 | const yaw = bot.entity.yaw;
250 | const sin = Math.sin(yaw);
251 | const cos = Math.cos(yaw);
252 |
253 | const transformPoint = (dx: number, dy: number, dz: number) => ({
254 | x: Math.floor(pos.x + dx * cos - dz * sin),
255 | y: Math.floor(pos.y + dy),
256 | z: Math.floor(pos.z + dx * sin + dz * cos),
257 | });
258 |
259 | const absStart = transformPoint(start.dx, start.dy, start.dz);
260 | const absEnd = transformPoint(end.dx, end.dy, end.dz);
261 |
262 | const minX = Math.min(absStart.x, absEnd.x);
263 | const maxX = Math.max(absStart.x, absEnd.x);
264 | const minY = Math.min(absStart.y, absEnd.y);
265 | const maxY = Math.max(absStart.y, absEnd.y);
266 | const minZ = Math.min(absStart.z, absEnd.z);
267 | const maxZ = Math.max(absStart.z, absEnd.z);
268 |
269 | const totalBlocks =
270 | (maxX - minX + 1) * (maxY - minY + 1) * (maxZ - minZ + 1);
271 | let blocksDug = 0;
272 |
273 | for (let y = maxY; y >= minY; y--) {
274 | for (let x = minX; x <= maxX; x++) {
275 | for (let z = minZ; z <= maxZ; z++) {
276 | const block = bot.blockAt(new Vec3(x, y, z));
277 | if (block && block.name !== "air") {
278 | await bot.dig(block);
279 | blocksDug++;
280 | if (progressCallback) {
281 | progressCallback(
282 | (blocksDug / totalBlocks) * 100,
283 | blocksDug,
284 | totalBlocks
285 | );
286 | }
287 | }
288 | }
289 | }
290 | }
291 | },
292 | getBlocksNearby: () => {
293 | const pos = bot.entity.position;
294 | const radius = 4;
295 | const blocks = [];
296 |
297 | for (let x = -radius; x <= radius; x++) {
298 | for (let y = -radius; y <= radius; y++) {
299 | for (let z = -radius; z <= radius; z++) {
300 | const block = bot.blockAt(
301 | new Vec3(
302 | Math.floor(pos.x + x),
303 | Math.floor(pos.y + y),
304 | Math.floor(pos.z + z)
305 | )
306 | );
307 | if (block && block.name !== "air") {
308 | blocks.push({
309 | name: block.name,
310 | position: {
311 | x: Math.floor(pos.x + x),
312 | y: Math.floor(pos.y + y),
313 | z: Math.floor(pos.z + z),
314 | },
315 | });
316 | }
317 | }
318 | }
319 | }
320 | return blocks;
321 | },
322 | getEntitiesNearby: () => {
323 | return Object.values(bot.entities)
324 | .filter((e) => e !== bot.entity && e.position)
325 | .map((e) => ({
326 | name: e.name || "unknown",
327 | type: e.type,
328 | position: {
329 | x: e.position.x,
330 | y: e.position.y,
331 | z: e.position.z,
332 | },
333 | velocity: e.velocity,
334 | health: e.health,
335 | }));
336 | },
337 | getWeather: () => ({
338 | isRaining: bot.isRaining,
339 | rainState: bot.isRaining ? "raining" : "clear",
340 | thunderState: bot.thunderState,
341 | }),
342 | } as MinecraftBot;
343 |
344 | this.toolHandler = new MinecraftToolHandler(wrapper);
345 | this.resourceHandler = new MinecraftResourceHandler(wrapper);
346 |
347 | return new Promise((resolve, reject) => {
348 | if (!this.bot) return reject(new Error("Bot not initialized"));
349 |
350 | this.bot.once("spawn", () => {
351 | this.isConnected = true;
352 | this.reconnectAttempts = 0;
353 | resolve();
354 | });
355 |
356 | this.bot.on("end", async () => {
357 | this.isConnected = false;
358 | try {
359 | await this.server.notification({
360 | method: "server/status",
361 | params: {
362 | type: "connection",
363 | status: "disconnected",
364 | host: this.connectionParams.host,
365 | port: this.connectionParams.port,
366 | },
367 | });
368 |
369 | if (this.reconnectAttempts < this.maxReconnectAttempts) {
370 | this.reconnectAttempts++;
371 | await new Promise((resolve) =>
372 | setTimeout(resolve, this.reconnectDelay)
373 | );
374 | await this.connectBot();
375 | }
376 | } catch (error) {
377 | console.error("Failed to handle disconnection:", error);
378 | }
379 | });
380 |
381 | this.bot.on("error", async (error) => {
382 | try {
383 | await this.server.notification({
384 | method: "server/status",
385 | params: {
386 | type: "error",
387 | error: error instanceof Error ? error.message : String(error),
388 | },
389 | });
390 | } catch (notificationError) {
391 | console.error(
392 | "Failed to send error notification:",
393 | notificationError
394 | );
395 | }
396 | });
397 | });
398 | }
399 |
400 | private setupHandlers() {
401 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
402 | tools: MINECRAFT_TOOLS,
403 | }));
404 |
405 | this.server.setRequestHandler(
406 | ReadResourceRequestSchema,
407 | async (request) => {
408 | try {
409 | if (!this.bot || !this.isConnected) {
410 | throw new Error("Bot is not connected");
411 | }
412 |
413 | const { uri } = request.params;
414 | let result: ResourceResponse;
415 |
416 | switch (uri) {
417 | case "minecraft://players":
418 | result = await this.resourceHandler.handleGetPlayers(uri);
419 | break;
420 | case "minecraft://position":
421 | result = await this.resourceHandler.handleGetPosition(uri);
422 | break;
423 | case "minecraft://blocks/nearby":
424 | result = await this.resourceHandler.handleGetBlocksNearby(uri);
425 | break;
426 | case "minecraft://entities/nearby":
427 | result = await this.resourceHandler.handleGetEntitiesNearby(uri);
428 | break;
429 | case "minecraft://inventory":
430 | result = await this.resourceHandler.handleGetInventory(uri);
431 | break;
432 | case "minecraft://health":
433 | result = await this.resourceHandler.handleGetHealth(uri);
434 | break;
435 | case "minecraft://weather":
436 | result = await this.resourceHandler.handleGetWeather(uri);
437 | break;
438 | default:
439 | throw new Error(`Resource not found: ${uri}`);
440 | }
441 |
442 | return {
443 | contents: result.contents.map((content) => ({
444 | uri: content.uri,
445 | mimeType: content.mimeType || "application/json",
446 | text:
447 | typeof content.text === "string"
448 | ? content.text
449 | : JSON.stringify(content.text),
450 | })),
451 | };
452 | } catch (error) {
453 | throw {
454 | code: -32603,
455 | message: error instanceof Error ? error.message : String(error),
456 | };
457 | }
458 | }
459 | );
460 |
461 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
462 | try {
463 | if (!request.params.arguments) {
464 | throw new Error("Arguments are required");
465 | }
466 |
467 | if (!this.bot || !this.isConnected) {
468 | throw new Error("Bot is not connected");
469 | }
470 |
471 | let result;
472 | switch (request.params.name) {
473 | case "chat": {
474 | const args = schemas.ChatSchema.parse(request.params.arguments);
475 | result = await this.toolHandler.handleChat(args.message);
476 | break;
477 | }
478 | case "navigate_relative": {
479 | const args = schemas.NavigateRelativeSchema.parse(
480 | request.params.arguments
481 | );
482 | result = await this.toolHandler.handleNavigateRelative(
483 | args.dx,
484 | args.dy,
485 | args.dz
486 | );
487 | break;
488 | }
489 | case "dig_block_relative": {
490 | const args = schemas.DigBlockRelativeSchema.parse(
491 | request.params.arguments
492 | );
493 | result = await this.toolHandler.handleDigBlockRelative(
494 | args.dx,
495 | args.dy,
496 | args.dz
497 | );
498 | break;
499 | }
500 | case "dig_area_relative": {
501 | const args = schemas.DigAreaRelativeSchema.parse(
502 | request.params.arguments
503 | );
504 | result = await this.toolHandler.handleDigAreaRelative(
505 | args.start,
506 | args.end
507 | );
508 | break;
509 | }
510 | default:
511 | throw {
512 | code: -32601,
513 | message: `Unknown tool: ${request.params.name}`,
514 | };
515 | }
516 |
517 | return {
518 | content: result?.content || [{ type: "text", text: "Success" }],
519 | _meta: result?._meta,
520 | };
521 | } catch (error) {
522 | if (error instanceof z.ZodError) {
523 | throw {
524 | code: -32602,
525 | message: "Invalid params",
526 | data: {
527 | errors: error.errors.map((e) => ({
528 | path: e.path.join("."),
529 | message: e.message,
530 | })),
531 | },
532 | };
533 | }
534 | throw {
535 | code: -32603,
536 | message: error instanceof Error ? error.message : String(error),
537 | };
538 | }
539 | });
540 | }
541 |
542 | async start(): Promise<void> {
543 | try {
544 | // Start MCP server first
545 | const transport = new StdioServerTransport();
546 | await this.server.connect(transport);
547 |
548 | // Send startup status
549 | await this.server.notification({
550 | method: "server/status",
551 | params: {
552 | type: "startup",
553 | status: "running",
554 | transport: "stdio",
555 | },
556 | });
557 |
558 | // Then connect bot
559 | await this.connectBot();
560 |
561 | // Keep process alive and handle termination
562 | process.stdin.resume();
563 | process.on("SIGINT", () => {
564 | this.bot?.end();
565 | process.exit(0);
566 | });
567 | process.on("SIGTERM", () => {
568 | this.bot?.end();
569 | process.exit(0);
570 | });
571 | } catch (error) {
572 | throw {
573 | code: -32000,
574 | message: "Server startup failed",
575 | data: {
576 | error: error instanceof Error ? error.message : String(error),
577 | },
578 | };
579 | }
580 | }
581 | }
582 |
```
--------------------------------------------------------------------------------
/src/handlers/tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { MinecraftBot } from "../types/minecraft";
2 | import { Vec3 } from "vec3";
3 | import { goals } from "mineflayer-pathfinder";
4 | import type { ToolResponse } from "../types/tools";
5 | import type { Position } from "../types/minecraft";
6 |
7 | export interface ToolHandler {
8 | handleChat(message: string): Promise<ToolResponse>;
9 | handleNavigateTo(x: number, y: number, z: number): Promise<ToolResponse>;
10 | handleNavigateRelative(
11 | dx: number,
12 | dy: number,
13 | dz: number
14 | ): Promise<ToolResponse>;
15 | handleDigBlock(x: number, y: number, z: number): Promise<ToolResponse>;
16 | handleDigBlockRelative(
17 | dx: number,
18 | dy: number,
19 | dz: number
20 | ): Promise<ToolResponse>;
21 | handleDigArea(
22 | start: { x: number; y: number; z: number },
23 | end: { x: number; y: number; z: number }
24 | ): Promise<ToolResponse>;
25 | handleDigAreaRelative(
26 | start: { dx: number; dy: number; dz: number },
27 | end: { dx: number; dy: number; dz: number }
28 | ): Promise<ToolResponse>;
29 | handlePlaceBlock(
30 | x: number,
31 | y: number,
32 | z: number,
33 | blockName: string
34 | ): Promise<ToolResponse>;
35 | handleFollowPlayer(username: string, distance: number): Promise<ToolResponse>;
36 | handleAttackEntity(
37 | entityName: string,
38 | maxDistance: number
39 | ): Promise<ToolResponse>;
40 | handleInspectBlock(
41 | position: { x: number; y: number; z: number },
42 | includeState: boolean
43 | ): Promise<ToolResponse>;
44 | handleFindBlocks(
45 | blockTypes: string | string[],
46 | maxDistance: number,
47 | maxCount: number,
48 | constraints?: {
49 | minY?: number;
50 | maxY?: number;
51 | requireReachable?: boolean;
52 | }
53 | ): Promise<ToolResponse>;
54 | handleFindEntities(
55 | entityTypes: string[],
56 | maxDistance: number,
57 | maxCount: number,
58 | constraints?: {
59 | mustBeVisible?: boolean;
60 | inFrontOnly?: boolean;
61 | minHealth?: number;
62 | maxHealth?: number;
63 | }
64 | ): Promise<ToolResponse>;
65 | handleCheckPath(
66 | destination: { x: number; y: number; z: number },
67 | dryRun: boolean,
68 | includeObstacles: boolean
69 | ): Promise<ToolResponse>;
70 | handleInspectInventory(
71 | itemType?: string,
72 | includeEquipment?: boolean
73 | ): Promise<ToolResponse>;
74 | handleCraftItem(
75 | itemName: string,
76 | quantity?: number,
77 | useCraftingTable?: boolean
78 | ): Promise<ToolResponse>;
79 | handleSmeltItem(
80 | itemName: string,
81 | fuelName: string,
82 | quantity?: number
83 | ): Promise<ToolResponse>;
84 | handleEquipItem(
85 | itemName: string,
86 | destination: "hand" | "off-hand" | "head" | "torso" | "legs" | "feet"
87 | ): Promise<ToolResponse>;
88 | handleDepositItem(
89 | containerPosition: Position,
90 | itemName: string,
91 | quantity?: number
92 | ): Promise<ToolResponse>;
93 | handleWithdrawItem(
94 | containerPosition: Position,
95 | itemName: string,
96 | quantity?: number
97 | ): Promise<ToolResponse>;
98 | }
99 |
100 | export class MinecraftToolHandler implements ToolHandler {
101 | constructor(private bot: MinecraftBot) {}
102 |
103 | private wrapError(error: unknown): ToolResponse {
104 | const errorMessage = error instanceof Error ? error.message : String(error);
105 | return {
106 | _meta: {},
107 | isError: true,
108 | content: [
109 | {
110 | type: "text",
111 | text: `Error: ${errorMessage}`,
112 | },
113 | ],
114 | };
115 | }
116 |
117 | async handleChat(message: string): Promise<ToolResponse> {
118 | this.bot.chat(message);
119 | return {
120 | _meta: {},
121 | content: [
122 | {
123 | type: "text",
124 | text: `Sent message: ${message}`,
125 | },
126 | ],
127 | };
128 | }
129 | async handleNavigateTo(
130 | x: number,
131 | y: number,
132 | z: number
133 | ): Promise<ToolResponse> {
134 | const progressToken = Date.now().toString();
135 | const pos = this.bot.getPosition();
136 | if (!pos) throw new Error("Bot position unknown");
137 |
138 | await this.bot.navigateRelative(
139 | x - pos.x,
140 | y - pos.y,
141 | z - pos.z,
142 | (progress) => {
143 | if (progress < 0 || progress > 100) return;
144 | }
145 | );
146 |
147 | return {
148 | _meta: {
149 | progressToken,
150 | },
151 | content: [
152 | {
153 | type: "text",
154 | text: `Navigated to ${x}, ${y}, ${z}`,
155 | },
156 | ],
157 | };
158 | }
159 |
160 | async handleNavigateRelative(
161 | dx: number,
162 | dy: number,
163 | dz: number
164 | ): Promise<ToolResponse> {
165 | const progressToken = Date.now().toString();
166 |
167 | await this.bot.navigateRelative(dx, dy, dz, (progress) => {
168 | if (progress < 0 || progress > 100) return;
169 | });
170 |
171 | return {
172 | _meta: {
173 | progressToken,
174 | },
175 | content: [
176 | {
177 | type: "text",
178 | text: `Navigated relative to current position: ${dx} blocks right/left, ${dy} blocks up/down, ${dz} blocks forward/back`,
179 | },
180 | ],
181 | };
182 | }
183 |
184 | async handleDigBlock(x: number, y: number, z: number): Promise<ToolResponse> {
185 | const pos = this.bot.getPosition();
186 | if (!pos) throw new Error("Bot position unknown");
187 |
188 | await this.bot.digBlockRelative(x - pos.x, y - pos.y, z - pos.z);
189 |
190 | return {
191 | content: [
192 | {
193 | type: "text",
194 | text: `Dug block at ${x}, ${y}, ${z}`,
195 | },
196 | ],
197 | };
198 | }
199 |
200 | async handleDigBlockRelative(
201 | dx: number,
202 | dy: number,
203 | dz: number
204 | ): Promise<ToolResponse> {
205 | await this.bot.digBlockRelative(dx, dy, dz);
206 | return {
207 | _meta: {},
208 | content: [
209 | {
210 | type: "text",
211 | text: `Dug block relative to current position: ${dx} blocks right/left, ${dy} blocks up/down, ${dz} blocks forward/back`,
212 | },
213 | ],
214 | };
215 | }
216 |
217 | async handleDigArea(
218 | start: Position,
219 | end: Position,
220 | progressCallback?: (
221 | progress: number,
222 | blocksDug: number,
223 | totalBlocks: number
224 | ) => void
225 | ): Promise<ToolResponse> {
226 | const pos = this.bot.getPosition();
227 | if (!pos) throw new Error("Bot position unknown");
228 |
229 | await this.bot.digAreaRelative(
230 | {
231 | dx: start.x - pos.x,
232 | dy: start.y - pos.y,
233 | dz: start.z - pos.z,
234 | },
235 | {
236 | dx: end.x - pos.x,
237 | dy: end.y - pos.y,
238 | dz: end.z - pos.z,
239 | },
240 | progressCallback
241 | );
242 |
243 | return {
244 | content: [
245 | {
246 | type: "text",
247 | text: `Dug area from (${start.x}, ${start.y}, ${start.z}) to (${end.x}, ${end.y}, ${end.z})`,
248 | },
249 | ],
250 | };
251 | }
252 |
253 | async handleDigAreaRelative(
254 | start: { dx: number; dy: number; dz: number },
255 | end: { dx: number; dy: number; dz: number }
256 | ): Promise<ToolResponse> {
257 | let progress = 0;
258 | let blocksDug = 0;
259 | let totalBlocks = 0;
260 |
261 | try {
262 | await this.bot.digAreaRelative(
263 | start,
264 | end,
265 | (currentProgress, currentBlocksDug, currentTotalBlocks) => {
266 | progress = currentProgress;
267 | blocksDug = currentBlocksDug;
268 | totalBlocks = currentTotalBlocks;
269 | }
270 | );
271 |
272 | return {
273 | _meta: {},
274 | content: [
275 | {
276 | type: "text",
277 | text: `Successfully completed digging area relative to current position:\nFrom: ${start.dx} right/left, ${start.dy} up/down, ${start.dz} forward/back\nTo: ${end.dx} right/left, ${end.dy} up/down, ${end.dz} forward/back\nDug ${blocksDug} blocks.`,
278 | },
279 | ],
280 | };
281 | } catch (error) {
282 | const errorMessage =
283 | error instanceof Error ? error.message : String(error);
284 | const progressMessage =
285 | totalBlocks > 0
286 | ? `Progress before error: ${progress}% (${blocksDug}/${totalBlocks} blocks)`
287 | : "";
288 |
289 | return {
290 | _meta: {},
291 | content: [
292 | {
293 | type: "text",
294 | text: `Failed to dig relative area: ${errorMessage}${
295 | progressMessage ? `\n${progressMessage}` : ""
296 | }`,
297 | },
298 | ],
299 | isError: true,
300 | };
301 | }
302 | }
303 |
304 | async handlePlaceBlock(
305 | x: number,
306 | y: number,
307 | z: number,
308 | blockName: string
309 | ): Promise<ToolResponse> {
310 | await this.bot.placeBlock(x, y, z, blockName);
311 | return {
312 | _meta: {},
313 | content: [
314 | {
315 | type: "text",
316 | text: `Placed ${blockName} at ${x}, ${y}, ${z}`,
317 | },
318 | ],
319 | };
320 | }
321 |
322 | async handleFollowPlayer(
323 | username: string,
324 | distance: number
325 | ): Promise<ToolResponse> {
326 | await this.bot.followPlayer(username, distance);
327 | return {
328 | _meta: {},
329 | content: [
330 | {
331 | type: "text",
332 | text: `Following player ${username}${
333 | distance ? ` at distance ${distance}` : ""
334 | }`,
335 | },
336 | ],
337 | };
338 | }
339 |
340 | async handleAttackEntity(
341 | entityName: string,
342 | maxDistance: number
343 | ): Promise<ToolResponse> {
344 | await this.bot.attackEntity(entityName, maxDistance);
345 | return {
346 | _meta: {},
347 | content: [
348 | {
349 | type: "text",
350 | text: `Attacked ${entityName}`,
351 | },
352 | ],
353 | };
354 | }
355 |
356 | async handleInspectBlock(
357 | position: { x: number; y: number; z: number },
358 | includeState: boolean
359 | ): Promise<ToolResponse> {
360 | const block = this.bot.blockAt(
361 | new Vec3(position.x, position.y, position.z)
362 | );
363 | if (!block) {
364 | return {
365 | content: [
366 | { type: "text", text: "No block found at specified position" },
367 | ],
368 | isError: true,
369 | };
370 | }
371 |
372 | const blockInfo: any = {
373 | name: block.name,
374 | type: block.type,
375 | position: position,
376 | };
377 |
378 | if (includeState && "metadata" in block) {
379 | blockInfo.metadata = block.metadata;
380 | blockInfo.stateId = (block as any).stateId;
381 | blockInfo.light = (block as any).light;
382 | blockInfo.skyLight = (block as any).skyLight;
383 | blockInfo.boundingBox = (block as any).boundingBox;
384 | }
385 |
386 | return {
387 | content: [
388 | {
389 | type: "text",
390 | text: `Block at (${position.x}, ${position.y}, ${position.z}):`,
391 | },
392 | {
393 | type: "json",
394 | text: JSON.stringify(blockInfo, null, 2),
395 | },
396 | ],
397 | };
398 | }
399 |
400 | async handleFindBlocks(
401 | blockTypes: string | string[],
402 | maxDistance: number,
403 | maxCount: number,
404 | constraints?: {
405 | minY?: number;
406 | maxY?: number;
407 | requireReachable?: boolean;
408 | }
409 | ): Promise<ToolResponse> {
410 | if (!this.bot) throw new Error("Not connected");
411 |
412 | const blockTypesArray = Array.isArray(blockTypes)
413 | ? blockTypes
414 | : [blockTypes];
415 |
416 | const matches = this.bot.findBlocks({
417 | matching: (block) => blockTypesArray.includes(block.name),
418 | maxDistance,
419 | count: maxCount,
420 | point: this.bot.entity.position,
421 | });
422 |
423 | // Apply additional constraints
424 | let filteredMatches = matches;
425 | if (constraints) {
426 | filteredMatches = matches.filter((pos) => {
427 | if (constraints.minY !== undefined && pos.y < constraints.minY)
428 | return false;
429 | if (constraints.maxY !== undefined && pos.y > constraints.maxY)
430 | return false;
431 |
432 | if (constraints.requireReachable) {
433 | // Check if we can actually reach this block
434 | const goal = new goals.GoalGetToBlock(pos.x, pos.y, pos.z);
435 | const result = this.bot.pathfinder.getPathTo(goal, maxDistance);
436 | if (!result?.path?.length) return false;
437 | }
438 |
439 | return true;
440 | });
441 | }
442 |
443 | const blocks = filteredMatches.map((pos) => {
444 | const block = this.bot!.blockAt(pos);
445 | return {
446 | position: { x: pos.x, y: pos.y, z: pos.z },
447 | name: block?.name || "unknown",
448 | distance: pos.distanceTo(this.bot!.entity.position),
449 | };
450 | });
451 |
452 | // Sort blocks by distance for better readability
453 | blocks.sort((a, b) => a.distance - b.distance);
454 |
455 | const summary = `Found ${
456 | blocks.length
457 | } matching blocks of types: ${blockTypesArray.join(", ")}`;
458 | const details = blocks
459 | .map(
460 | (block) =>
461 | `- ${block.name} at (${block.position.x}, ${block.position.y}, ${
462 | block.position.z
463 | }), ${block.distance.toFixed(1)} blocks away`
464 | )
465 | .join("\n");
466 |
467 | return {
468 | content: [
469 | {
470 | type: "text",
471 | text: summary + (blocks.length > 0 ? "\n" + details : ""),
472 | },
473 | ],
474 | };
475 | }
476 |
477 | async handleFindEntities(
478 | entityTypes: string[],
479 | maxDistance: number,
480 | maxCount: number,
481 | constraints?: {
482 | mustBeVisible?: boolean;
483 | inFrontOnly?: boolean;
484 | minHealth?: number;
485 | maxHealth?: number;
486 | }
487 | ): Promise<ToolResponse> {
488 | if (!this.bot) throw new Error("Not connected");
489 |
490 | let entities = Object.values(this.bot.entities)
491 | .filter((entity) => {
492 | if (!entity || !entity.position) return false;
493 | if (!entityTypes.includes(entity.name || "")) return false;
494 |
495 | const distance = entity.position.distanceTo(this.bot!.entity.position);
496 | if (distance > maxDistance) return false;
497 |
498 | if (constraints) {
499 | if (
500 | constraints.minHealth !== undefined &&
501 | (entity.health || 0) < constraints.minHealth
502 | )
503 | return false;
504 | if (
505 | constraints.maxHealth !== undefined &&
506 | (entity.health || 0) > constraints.maxHealth
507 | )
508 | return false;
509 |
510 | if (constraints.mustBeVisible && !this.bot!.canSeeEntity(entity))
511 | return false;
512 |
513 | if (constraints.inFrontOnly) {
514 | // Check if entity is in front of the bot using dot product
515 | const botDir = this.bot!.entity.velocity;
516 | const toEntity = entity.position.minus(this.bot!.entity.position);
517 | const dot = botDir.dot(toEntity);
518 | if (dot <= 0) return false;
519 | }
520 | }
521 |
522 | return true;
523 | })
524 | .slice(0, maxCount)
525 | .map((entity) => ({
526 | name: entity.name || "unknown",
527 | type: entity.type,
528 | position: {
529 | x: entity.position.x,
530 | y: entity.position.y,
531 | z: entity.position.z,
532 | },
533 | velocity: entity.velocity,
534 | health: entity.health,
535 | distance: entity.position.distanceTo(this.bot!.entity.position),
536 | }));
537 |
538 | return {
539 | content: [
540 | {
541 | type: "text",
542 | text: `Found ${entities.length} matching entities:`,
543 | },
544 | {
545 | type: "json",
546 | text: JSON.stringify(entities, null, 2),
547 | },
548 | ],
549 | };
550 | }
551 |
552 | async handleCheckPath(
553 | destination: { x: number; y: number; z: number },
554 | dryRun: boolean,
555 | includeObstacles: boolean
556 | ): Promise<ToolResponse> {
557 | if (!this.bot) throw new Error("Not connected");
558 |
559 | const goal = new goals.GoalBlock(
560 | destination.x,
561 | destination.y,
562 | destination.z
563 | );
564 | const pathResult = await this.bot.pathfinder.getPathTo(goal);
565 |
566 | const response: any = {
567 | pathExists: !!pathResult?.path?.length,
568 | distance: pathResult?.path?.length || 0,
569 | estimatedTime: (pathResult?.path?.length || 0) * 0.25, // Rough estimate: 4 blocks per second
570 | };
571 |
572 | if (!pathResult?.path?.length && includeObstacles) {
573 | // Try to find what's blocking the path
574 | const obstacles = [];
575 | const line = this.getPointsOnLine(
576 | this.bot.entity.position,
577 | new Vec3(destination.x, destination.y, destination.z)
578 | );
579 |
580 | for (const point of line) {
581 | const block = this.bot.blockAt(point);
582 | if (block && (block as any).boundingBox !== "empty") {
583 | obstacles.push({
584 | position: { x: point.x, y: point.y, z: point.z },
585 | block: block.name,
586 | type: block.type,
587 | });
588 | if (obstacles.length >= 5) break; // Limit to first 5 obstacles
589 | }
590 | }
591 |
592 | response.obstacles = obstacles;
593 | }
594 |
595 | if (!dryRun && pathResult?.path?.length) {
596 | await this.bot.pathfinder.goto(goal);
597 | response.status = "Reached destination";
598 | }
599 |
600 | return {
601 | content: [
602 | {
603 | type: "text",
604 | text: `Path check to (${destination.x}, ${destination.y}, ${destination.z}):`,
605 | },
606 | {
607 | type: "json",
608 | text: JSON.stringify(response, null, 2),
609 | },
610 | ],
611 | };
612 | }
613 |
614 | private getPointsOnLine(start: Vec3, end: Vec3): Vec3[] {
615 | const points: Vec3[] = [];
616 | const distance = start.distanceTo(end);
617 | const steps = Math.ceil(distance);
618 |
619 | for (let i = 0; i <= steps; i++) {
620 | const t = i / steps;
621 | points.push(start.scaled(1 - t).plus(end.scaled(t)));
622 | }
623 |
624 | return points;
625 | }
626 |
627 | async handleInspectInventory(
628 | itemType?: string,
629 | includeEquipment?: boolean
630 | ): Promise<ToolResponse> {
631 | const inventory = this.bot.getInventory();
632 | let items = inventory;
633 |
634 | if (itemType) {
635 | items = items.filter((item) => item.name === itemType);
636 | }
637 |
638 | const response = {
639 | items,
640 | totalCount: items.reduce((sum, item) => sum + item.count, 0),
641 | uniqueItems: new Set(items.map((item) => item.name)).size,
642 | };
643 |
644 | return {
645 | content: [
646 | {
647 | type: "text",
648 | text: `Inventory contents${
649 | itemType ? ` (filtered by ${itemType})` : ""
650 | }:`,
651 | },
652 | {
653 | type: "json",
654 | text: JSON.stringify(response, null, 2),
655 | },
656 | ],
657 | };
658 | }
659 |
660 | async handleCraftItem(
661 | itemName: string,
662 | quantity?: number,
663 | useCraftingTable?: boolean
664 | ): Promise<ToolResponse> {
665 | try {
666 | await this.bot.craftItem(itemName, quantity, useCraftingTable);
667 | return {
668 | content: [
669 | {
670 | type: "text",
671 | text: `Successfully crafted ${quantity || 1}x ${itemName}${
672 | useCraftingTable ? " using crafting table" : ""
673 | }`,
674 | },
675 | ],
676 | };
677 | } catch (error) {
678 | return {
679 | content: [
680 | {
681 | type: "text",
682 | text: `Failed to craft ${itemName}: ${
683 | error instanceof Error ? error.message : String(error)
684 | }`,
685 | },
686 | ],
687 | isError: true,
688 | };
689 | }
690 | }
691 |
692 | async handleSmeltItem(
693 | itemName: string,
694 | fuelName: string,
695 | quantity?: number
696 | ): Promise<ToolResponse> {
697 | try {
698 | await this.bot.smeltItem(itemName, fuelName, quantity);
699 | return {
700 | content: [
701 | {
702 | type: "text",
703 | text: `Successfully smelted ${
704 | quantity || 1
705 | }x ${itemName} using ${fuelName} as fuel`,
706 | },
707 | ],
708 | };
709 | } catch (error) {
710 | return {
711 | content: [
712 | {
713 | type: "text",
714 | text: `Failed to smelt ${itemName}: ${
715 | error instanceof Error ? error.message : String(error)
716 | }`,
717 | },
718 | ],
719 | isError: true,
720 | };
721 | }
722 | }
723 |
724 | async handleEquipItem(
725 | itemName: string,
726 | destination: "hand" | "off-hand" | "head" | "torso" | "legs" | "feet"
727 | ): Promise<ToolResponse> {
728 | try {
729 | await this.bot.equipItem(itemName, destination);
730 | return {
731 | content: [
732 | {
733 | type: "text",
734 | text: `Successfully equipped ${itemName} to ${destination}`,
735 | },
736 | ],
737 | };
738 | } catch (error) {
739 | return {
740 | content: [
741 | {
742 | type: "text",
743 | text: `Failed to equip ${itemName}: ${
744 | error instanceof Error ? error.message : String(error)
745 | }`,
746 | },
747 | ],
748 | isError: true,
749 | };
750 | }
751 | }
752 |
753 | async handleDepositItem(
754 | containerPosition: Position,
755 | itemName: string,
756 | quantity?: number
757 | ): Promise<ToolResponse> {
758 | try {
759 | await this.bot.depositItem(
760 | containerPosition as Position,
761 | itemName,
762 | quantity
763 | );
764 | return {
765 | content: [
766 | {
767 | type: "text",
768 | text: `Successfully deposited ${
769 | quantity || 1
770 | }x ${itemName} into container at (${containerPosition.x}, ${
771 | containerPosition.y
772 | }, ${containerPosition.z})`,
773 | },
774 | ],
775 | };
776 | } catch (error) {
777 | return {
778 | content: [
779 | {
780 | type: "text",
781 | text: `Failed to deposit ${itemName}: ${
782 | error instanceof Error ? error.message : String(error)
783 | }`,
784 | },
785 | ],
786 | isError: true,
787 | };
788 | }
789 | }
790 |
791 | async handleWithdrawItem(
792 | containerPosition: Position,
793 | itemName: string,
794 | quantity?: number
795 | ): Promise<ToolResponse> {
796 | try {
797 | await this.bot.withdrawItem(
798 | containerPosition as Position,
799 | itemName,
800 | quantity
801 | );
802 | return {
803 | content: [
804 | {
805 | type: "text",
806 | text: `Successfully withdrew ${
807 | quantity || 1
808 | }x ${itemName} from container at (${containerPosition.x}, ${
809 | containerPosition.y
810 | }, ${containerPosition.z})`,
811 | },
812 | ],
813 | };
814 | } catch (error) {
815 | return {
816 | content: [
817 | {
818 | type: "text",
819 | text: `Failed to withdraw ${itemName}: ${
820 | error instanceof Error ? error.message : String(error)
821 | }`,
822 | },
823 | ],
824 | isError: true,
825 | };
826 | }
827 | }
828 | }
829 |
```
--------------------------------------------------------------------------------
/src/core/bot.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createBot } from "mineflayer";
2 | import type { Bot, Furnace } from "mineflayer";
3 | import { Vec3 } from "vec3";
4 | import { pathfinder, Movements, goals } from "mineflayer-pathfinder";
5 | import type { Pathfinder } from "mineflayer-pathfinder";
6 | import type {
7 | Position,
8 | MinecraftBot,
9 | ToolResponse,
10 | Player,
11 | InventoryItem,
12 | Entity as CustomEntity,
13 | Block,
14 | HealthStatus,
15 | Weather,
16 | Recipe,
17 | Container,
18 | } from "../types/minecraft";
19 | import { TypeConverters } from "../types/minecraft";
20 | import { Block as PrismarineBlock } from "prismarine-block";
21 | import { Item } from "prismarine-item";
22 | import { EventEmitter } from "events";
23 |
24 | interface PrismarineBlockWithBoundingBox extends PrismarineBlock {
25 | boundingBox: string;
26 | }
27 |
28 | type EquipmentDestination =
29 | | "hand"
30 | | "off-hand"
31 | | "head"
32 | | "torso"
33 | | "legs"
34 | | "feet";
35 |
36 | interface ExtendedBot extends Bot {
37 | pathfinder: Pathfinder & {
38 | setMovements(movements: Movements): void;
39 | goto(goal: goals.Goal): Promise<void>;
40 | };
41 | }
42 |
43 | interface ConnectionParams {
44 | host: string;
45 | port: number;
46 | username: string;
47 | version?: string;
48 | hideErrors?: boolean;
49 | }
50 |
51 | export class MineflayerBot extends EventEmitter implements MinecraftBot {
52 | private bot: ExtendedBot | null = null;
53 | private isConnected: boolean = false;
54 | private isConnecting: boolean = false;
55 | private reconnectAttempts: number = 0;
56 | private readonly maxReconnectAttempts: number = 3;
57 | private lastConnectionParams: ConnectionParams;
58 | private movements: Movements | null = null;
59 |
60 | constructor(connectionParams: ConnectionParams) {
61 | super();
62 | this.lastConnectionParams = connectionParams;
63 | }
64 |
65 | async connect(host: string, port: number, username: string): Promise<void> {
66 | if (this.isConnecting) {
67 | return;
68 | }
69 | this.isConnecting = true;
70 | try {
71 | const params: ConnectionParams = { host, port, username };
72 | this.lastConnectionParams = params;
73 | await this.setupBot();
74 | } finally {
75 | this.isConnecting = false;
76 | }
77 | }
78 |
79 | private setupBot(): Promise<void> {
80 | return new Promise<void>((resolve, reject) => {
81 | try {
82 | if (this.isConnecting) {
83 | reject(new Error("Already connecting"));
84 | return;
85 | }
86 |
87 | this.isConnecting = true;
88 |
89 | if (this.bot) {
90 | this.bot.end();
91 | this.bot = null;
92 | }
93 |
94 | this.bot = createBot({
95 | ...this.lastConnectionParams,
96 | hideErrors: false,
97 | });
98 |
99 | this.bot.loadPlugin(pathfinder);
100 |
101 | this.bot.on("error", (error: Error) => {
102 | this.logError("Bot error", error);
103 | this.isConnecting = false;
104 | reject(error);
105 | });
106 |
107 | this.bot.on("kicked", (reason: string, loggedIn: boolean) => {
108 | this.logError("Bot kicked", { reason, loggedIn });
109 | this.isConnecting = false;
110 | this.handleDisconnect();
111 | });
112 |
113 | this.bot.once("spawn", () => {
114 | this.logDebug("Bot spawned successfully");
115 | this.isConnected = true;
116 | this.isConnecting = false;
117 | this.reconnectAttempts = 0;
118 | this.setupMovements();
119 | resolve();
120 | });
121 |
122 | this.bot.on("end", (reason: string) => {
123 | this.logError("Bot connection ended", { reason });
124 | this.isConnecting = false;
125 | this.handleDisconnect();
126 | });
127 | } catch (error) {
128 | this.logError("Bot setup error", error);
129 | this.isConnecting = false;
130 | this.sendJSONRPCError(-32001, "Failed to create bot", {
131 | error: error instanceof Error ? error.message : String(error),
132 | });
133 | reject(error);
134 | }
135 | });
136 | }
137 |
138 | private setupMovements(): void {
139 | if (!this.bot) return;
140 |
141 | try {
142 | this.movements = new Movements(this.bot);
143 | this.movements.allowParkour = true;
144 | this.movements.allowSprinting = true;
145 | this.bot.pathfinder.setMovements(this.movements);
146 | } catch (error) {
147 | this.sendJSONRPCError(-32002, "Error setting up movements", {
148 | error: error instanceof Error ? error.message : String(error),
149 | });
150 | }
151 | }
152 |
153 | private handleDisconnect(): void {
154 | this.isConnected = false;
155 | this.movements = null;
156 |
157 | // Send a notification that the bot has disconnected
158 | this.sendJsonRpcNotification("bot.disconnected", {
159 | message: "Bot disconnected from server",
160 | });
161 | }
162 |
163 | private sendJsonRpcNotification(method: string, params: any) {
164 | process.stdout.write(
165 | JSON.stringify({
166 | jsonrpc: "2.0",
167 | method,
168 | params,
169 | id: null,
170 | }) + "\n"
171 | );
172 | }
173 |
174 | private sendJSONRPCError(code: number, message: string, data?: any) {
175 | process.stdout.write(
176 | JSON.stringify({
177 | jsonrpc: "2.0",
178 | id: null,
179 | error: {
180 | code,
181 | message,
182 | data,
183 | },
184 | }) + "\n"
185 | );
186 | }
187 |
188 | private logDebug(message: string, data?: any) {
189 | this.sendJsonRpcNotification("bot.debug", { message, data });
190 | }
191 |
192 | private logWarning(message: string, data?: any) {
193 | this.sendJsonRpcNotification("bot.warning", { message, data });
194 | }
195 |
196 | private logError(message: string, error?: any) {
197 | this.sendJsonRpcNotification("bot.error", {
198 | message,
199 | error: String(error),
200 | });
201 | }
202 |
203 | disconnect(): void {
204 | if (this.bot) {
205 | this.bot.end();
206 | this.bot = null;
207 | }
208 | }
209 |
210 | chat(message: string): void {
211 | if (!this.bot) {
212 | return this.wrapError("Not connected");
213 | }
214 | this.bot.chat(message);
215 | }
216 |
217 | getPosition(): Position | null {
218 | if (!this.bot?.entity?.position) return null;
219 | const pos = this.bot.entity.position;
220 | return { x: pos.x, y: pos.y, z: pos.z };
221 | }
222 |
223 | getHealth(): number {
224 | if (!this.bot) {
225 | return this.wrapError("Not connected");
226 | }
227 | return this.bot.health;
228 | }
229 |
230 | getInventory(): InventoryItem[] {
231 | if (!this.bot) {
232 | return this.wrapError("Not connected");
233 | }
234 | return this.bot.inventory.items().map(TypeConverters.item);
235 | }
236 |
237 | getPlayers(): Player[] {
238 | if (!this.bot) {
239 | return this.wrapError("Not connected");
240 | }
241 | return Object.values(this.bot.players).map((player) => ({
242 | username: player.username,
243 | uuid: player.uuid,
244 | ping: player.ping,
245 | }));
246 | }
247 |
248 | async navigateTo(
249 | x: number,
250 | y: number,
251 | z: number,
252 | progressCallback?: (progress: number) => void
253 | ): Promise<void> {
254 | if (!this.bot) return this.wrapError("Not connected");
255 | const goal = new goals.GoalNear(x, y, z, 1);
256 |
257 | try {
258 | const startPos = this.bot.entity.position;
259 | const targetPos = new Vec3(x, y, z);
260 | const totalDistance = startPos.distanceTo(targetPos);
261 |
262 | // Set up progress monitoring
263 | const checkProgress = () => {
264 | if (!this.bot) return;
265 | const currentPos = this.bot.entity.position;
266 | const remainingDistance = currentPos.distanceTo(targetPos);
267 | const progress = Math.min(
268 | 100,
269 | ((totalDistance - remainingDistance) / totalDistance) * 100
270 | );
271 | progressCallback?.(progress);
272 | };
273 |
274 | const progressInterval = setInterval(checkProgress, 500);
275 |
276 | try {
277 | await this.bot.pathfinder.goto(goal);
278 | } finally {
279 | clearInterval(progressInterval);
280 | // Send final progress
281 | progressCallback?.(100);
282 | }
283 | } catch (error) {
284 | return this.wrapError(
285 | `Failed to navigate: ${
286 | error instanceof Error ? error.message : String(error)
287 | }`
288 | );
289 | }
290 | }
291 |
292 | async digBlock(x: number, y: number, z: number): Promise<void> {
293 | if (!this.bot) {
294 | return this.wrapError("Not connected");
295 | }
296 |
297 | const targetPos = new Vec3(x, y, z);
298 |
299 | // Try to move close enough to dig if needed
300 | try {
301 | const goal = new goals.GoalNear(x, y, z, 3); // Stay within 3 blocks
302 | await this.bot.pathfinder.goto(goal);
303 | } catch (error) {
304 | this.logWarning("Could not move closer to block for digging", error);
305 | // Continue anyway - the block might still be reachable
306 | }
307 |
308 | while (true) {
309 | const block = this.bot.blockAt(targetPos);
310 | if (!block) {
311 | // No block at all, so we're done
312 | return;
313 | }
314 |
315 | if (block.name === "air") {
316 | // The target is now air, so we're done
317 | return;
318 | }
319 |
320 | // Skip bedrock and other indestructible blocks
321 | if (block.hardness < 0) {
322 | this.logWarning(
323 | `Cannot dig indestructible block ${block.name} at ${x}, ${y}, ${z}`
324 | );
325 | return;
326 | }
327 |
328 | // Attempt to dig
329 | try {
330 | await this.bot.dig(block);
331 | } catch (err) {
332 | const error = err as Error;
333 | // If it's a known "cannot dig" error, skip
334 | if (
335 | error.message?.includes("cannot be broken") ||
336 | error.message?.includes("cannot dig") ||
337 | error.message?.includes("unreachable")
338 | ) {
339 | this.logWarning(
340 | `Failed to dig block ${block.name} at ${x}, ${y}, ${z}: ${error.message}`
341 | );
342 | return;
343 | }
344 | // For other errors, wrap them
345 | return this.wrapError(error.message || String(error));
346 | }
347 |
348 | // Small delay to avoid server spam
349 | await new Promise((resolve) => setTimeout(resolve, 150));
350 | }
351 | }
352 |
353 | async digArea(
354 | start: Position,
355 | end: Position,
356 | progressCallback?: (
357 | progress: number,
358 | blocksDug: number,
359 | totalBlocks: number
360 | ) => void
361 | ): Promise<void> {
362 | if (!this.bot) {
363 | return this.wrapError("Not connected");
364 | }
365 |
366 | const minX = Math.min(start.x, end.x);
367 | const maxX = Math.max(start.x, end.x);
368 | const minY = Math.min(start.y, end.y);
369 | const maxY = Math.max(start.y, end.y);
370 | const minZ = Math.min(start.z, end.z);
371 | const maxZ = Math.max(start.z, end.z);
372 |
373 | // Pre-scan the area to identify diggable blocks and create an efficient digging plan
374 | const diggableBlocks: Vec3[] = [];
375 | const undiggableBlocks: Vec3[] = [];
376 |
377 | // Helper to check if a block is diggable
378 | const isDiggable = (block: PrismarineBlock | null): boolean => {
379 | if (!block) return false;
380 | if (block.name === "air") return false;
381 | if (block.hardness < 0) return false; // Bedrock and other unbreakable blocks
382 |
383 | // Skip fluid blocks
384 | if (
385 | block.name.includes("water") ||
386 | block.name.includes("lava") ||
387 | block.name.includes("flowing")
388 | ) {
389 | return false;
390 | }
391 |
392 | // Skip blocks that are known to be unbreakable or special
393 | const unbreakableBlocks = [
394 | "barrier",
395 | "bedrock",
396 | "end_portal",
397 | "end_portal_frame",
398 | ];
399 | if (unbreakableBlocks.includes(block.name)) return false;
400 |
401 | return true;
402 | };
403 |
404 | // First pass: identify all diggable blocks
405 | for (let y = maxY; y >= minY; y--) {
406 | for (let x = minX; x <= maxX; x++) {
407 | for (let z = minZ; z <= maxZ; z++) {
408 | const pos = new Vec3(x, y, z);
409 | const block = this.bot.blockAt(pos);
410 |
411 | if (isDiggable(block)) {
412 | diggableBlocks.push(pos);
413 | } else if (block && block.name !== "air") {
414 | undiggableBlocks.push(pos);
415 | }
416 | }
417 | }
418 | }
419 |
420 | const totalBlocks = diggableBlocks.length;
421 | let blocksDug = 0;
422 | let lastProgressUpdate = Date.now();
423 |
424 | // Set up disconnect handler
425 | let disconnected = false;
426 | const disconnectHandler = () => {
427 | disconnected = true;
428 | };
429 | this.bot.once("end", disconnectHandler);
430 |
431 | try {
432 | // Group blocks into "slices" for more efficient digging
433 | const sliceSize = 4; // Size of each work area
434 | const slices: Vec3[][] = [];
435 |
436 | // Group blocks into nearby clusters for efficient movement
437 | for (let x = minX; x <= maxX; x += sliceSize) {
438 | for (let z = minZ; z <= maxZ; z += sliceSize) {
439 | const slice: Vec3[] = diggableBlocks.filter(
440 | (pos) =>
441 | pos.x >= x &&
442 | pos.x < x + sliceSize &&
443 | pos.z >= z &&
444 | pos.z < z + sliceSize
445 | );
446 |
447 | if (slice.length > 0) {
448 | // Sort the slice from top to bottom for safer digging
449 | slice.sort((a, b) => b.y - a.y);
450 | slices.push(slice);
451 | }
452 | }
453 | }
454 |
455 | // Process each slice
456 | for (const slice of slices) {
457 | if (disconnected) {
458 | return this.wrapError("Disconnected while digging area");
459 | }
460 |
461 | // Find optimal position to dig this slice
462 | const sliceCenter = slice
463 | .reduce((acc, pos) => acc.plus(pos), new Vec3(0, 0, 0))
464 | .scaled(1 / slice.length);
465 |
466 | // Try to move to a good position for this slice
467 | try {
468 | // Position ourselves at a good vantage point for the slice
469 | const standingPos = new Vec3(
470 | sliceCenter.x - 1,
471 | Math.max(sliceCenter.y, minY),
472 | sliceCenter.z - 1
473 | );
474 | await this.navigateTo(standingPos.x, standingPos.y, standingPos.z);
475 | } catch (error) {
476 | this.logWarning(
477 | "Could not reach optimal digging position for slice",
478 | error
479 | );
480 | // Continue anyway - some blocks might still be reachable
481 | }
482 |
483 | // Process blocks in the slice from top to bottom
484 | for (const pos of slice) {
485 | if (disconnected) {
486 | return this.wrapError("Disconnected while digging area");
487 | }
488 |
489 | try {
490 | const block = this.bot.blockAt(pos);
491 | if (!block || !isDiggable(block)) {
492 | continue; // Skip if block changed or became undiggable
493 | }
494 |
495 | // Check if we need to move closer
496 | const distance = pos.distanceTo(this.bot.entity.position);
497 | if (distance > 4) {
498 | try {
499 | const goal = new goals.GoalNear(pos.x, pos.y, pos.z, 3);
500 | await this.bot.pathfinder.goto(goal);
501 | } catch (error) {
502 | this.logWarning(
503 | `Could not move closer to block at ${pos.x}, ${pos.y}, ${pos.z}:`,
504 | error
505 | );
506 | continue; // Skip this block if we can't reach it
507 | }
508 | }
509 |
510 | await this.digBlock(pos.x, pos.y, pos.z);
511 | blocksDug++;
512 |
513 | // Update progress every 500ms
514 | const now = Date.now();
515 | if (progressCallback && now - lastProgressUpdate >= 500) {
516 | const progress = Math.floor((blocksDug / totalBlocks) * 100);
517 | progressCallback(progress, blocksDug, totalBlocks);
518 | lastProgressUpdate = now;
519 | }
520 | } catch (error) {
521 | // Log the error but continue with other blocks
522 | this.logWarning(
523 | `Failed to dig block at ${pos.x}, ${pos.y}, ${pos.z}:`,
524 | error
525 | );
526 | continue;
527 | }
528 | }
529 | }
530 |
531 | // Final progress update
532 | if (progressCallback) {
533 | progressCallback(100, blocksDug, totalBlocks);
534 | }
535 |
536 | // Log summary of undiggable blocks if any
537 | if (undiggableBlocks.length > 0) {
538 | this.logWarning(
539 | `Completed digging with ${undiggableBlocks.length} undiggable blocks`,
540 | undiggableBlocks.map((pos) => ({
541 | position: pos,
542 | type: this.bot?.blockAt(pos)?.name || "unknown",
543 | }))
544 | );
545 | }
546 | } finally {
547 | // Clean up the disconnect handler
548 | this.bot.removeListener("end", disconnectHandler);
549 | }
550 | }
551 |
552 | async placeBlock(
553 | x: number,
554 | y: number,
555 | z: number,
556 | blockName: string
557 | ): Promise<void> {
558 | if (!this.bot) return this.wrapError("Not connected");
559 | const item = this.bot.inventory.items().find((i) => i.name === blockName);
560 | if (!item) return this.wrapError(`No ${blockName} in inventory`);
561 |
562 | try {
563 | await this.bot.equip(item, "hand");
564 | const targetPos = new Vec3(x, y, z);
565 | const targetBlock = this.bot.blockAt(targetPos);
566 | if (!targetBlock)
567 | return this.wrapError("Invalid target position for placing block");
568 | const faceVector = new Vec3(0, 1, 0);
569 | await this.bot.placeBlock(targetBlock, faceVector);
570 | } catch (error) {
571 | return this.wrapError(
572 | `Failed to place block: ${
573 | error instanceof Error ? error.message : String(error)
574 | }`
575 | );
576 | }
577 | }
578 |
579 | async followPlayer(username: string, distance: number = 2): Promise<void> {
580 | if (!this.bot) return this.wrapError("Not connected");
581 | const target = this.bot.players[username]?.entity;
582 | if (!target) return this.wrapError(`Player ${username} not found`);
583 | const goal = new goals.GoalFollow(target, distance);
584 | try {
585 | await this.bot.pathfinder.goto(goal);
586 | } catch (error) {
587 | return this.wrapError(
588 | `Failed to follow player: ${
589 | error instanceof Error ? error.message : String(error)
590 | }`
591 | );
592 | }
593 | }
594 |
595 | async attackEntity(
596 | entityName: string,
597 | maxDistance: number = 5
598 | ): Promise<void> {
599 | if (!this.bot) return this.wrapError("Not connected");
600 | const entity = Object.values(this.bot.entities).find(
601 | (e) =>
602 | e.name === entityName &&
603 | e.position.distanceTo(this.bot!.entity.position) <= maxDistance
604 | );
605 | if (!entity)
606 | return this.wrapError(
607 | `No ${entityName} found within ${maxDistance} blocks`
608 | );
609 | try {
610 | await this.bot.attack(entity as any);
611 | } catch (error) {
612 | return this.wrapError(
613 | `Failed to attack entity: ${
614 | error instanceof Error ? error.message : String(error)
615 | }`
616 | );
617 | }
618 | }
619 |
620 | getEntitiesNearby(maxDistance: number = 10): CustomEntity[] {
621 | if (!this.bot) return this.wrapError("Not connected");
622 | return Object.values(this.bot.entities)
623 | .filter(
624 | (e) => e.position.distanceTo(this.bot!.entity.position) <= maxDistance
625 | )
626 | .map(TypeConverters.entity);
627 | }
628 |
629 | getBlocksNearby(maxDistance: number = 10, count: number = 100): Block[] {
630 | if (!this.bot) return this.wrapError("Not connected");
631 | return this.bot
632 | .findBlocks({
633 | matching: () => true,
634 | maxDistance,
635 | count,
636 | })
637 | .map((pos) => {
638 | const block = this.bot?.blockAt(pos);
639 | return block ? TypeConverters.block(block) : null;
640 | })
641 | .filter((b): b is Block => b !== null);
642 | }
643 |
644 | getHealthStatus(): HealthStatus {
645 | if (!this.bot) return this.wrapError("Not connected");
646 | return {
647 | health: this.bot.health,
648 | food: this.bot.food,
649 | saturation: this.bot.foodSaturation,
650 | armor: this.bot.game.gameMode === "creative" ? 20 : 0,
651 | };
652 | }
653 |
654 | getWeather(): Weather {
655 | if (!this.bot) return this.wrapError("Not connected");
656 | return {
657 | isRaining: this.bot.isRaining,
658 | rainState: this.bot.isRaining ? "raining" : "clear",
659 | thunderState: this.bot.thunderState,
660 | };
661 | }
662 |
663 | async navigateRelative(
664 | dx: number,
665 | dy: number,
666 | dz: number,
667 | progressCallback?: (progress: number) => void
668 | ): Promise<void> {
669 | if (!this.bot) return this.wrapError("Not connected");
670 | const currentPos = this.bot.entity.position;
671 | const yaw = this.bot.entity.yaw;
672 | const sin = Math.sin(yaw);
673 | const cos = Math.cos(yaw);
674 |
675 | const worldDx = dx * cos - dz * sin;
676 | const worldDz = dx * sin + dz * cos;
677 |
678 | try {
679 | await this.navigateTo(
680 | currentPos.x + worldDx,
681 | currentPos.y + dy,
682 | currentPos.z + worldDz,
683 | progressCallback
684 | );
685 | } catch (error) {
686 | return this.wrapError(
687 | `Failed to navigate relatively: ${
688 | error instanceof Error ? error.message : String(error)
689 | }`
690 | );
691 | }
692 | }
693 |
694 | private relativeToAbsolute(
695 | origin: Vec3,
696 | dx: number,
697 | dy: number,
698 | dz: number
699 | ): Position {
700 | const yaw = this.bot!.entity.yaw;
701 | const sin = Math.sin(yaw);
702 | const cos = Math.cos(yaw);
703 |
704 | // For "forward/back" as +Z, "left/right" as ±X
705 | const worldDx = dx * cos - dz * sin;
706 | const worldDz = dx * sin + dz * cos;
707 |
708 | return {
709 | x: Math.floor(origin.x + worldDx),
710 | y: Math.floor(origin.y + dy),
711 | z: Math.floor(origin.z + worldDz),
712 | };
713 | }
714 |
715 | async digBlockRelative(dx: number, dy: number, dz: number): Promise<void> {
716 | if (!this.bot) throw new Error("Not connected");
717 | const currentPos = this.bot.entity.position;
718 | const { x, y, z } = this.relativeToAbsolute(currentPos, dx, dy, dz);
719 | await this.digBlock(x, y, z);
720 | }
721 |
722 | async digAreaRelative(
723 | start: { dx: number; dy: number; dz: number },
724 | end: { dx: number; dy: number; dz: number },
725 | progressCallback?: (
726 | progress: number,
727 | blocksDug: number,
728 | totalBlocks: number
729 | ) => void
730 | ): Promise<void> {
731 | if (!this.bot) throw new Error("Not connected");
732 | const currentPos = this.bot.entity.position;
733 |
734 | // Convert both corners to absolute coordinates
735 | const absStart = this.relativeToAbsolute(
736 | currentPos,
737 | start.dx,
738 | start.dy,
739 | start.dz
740 | );
741 | const absEnd = this.relativeToAbsolute(currentPos, end.dx, end.dy, end.dz);
742 |
743 | // Use the absolute digArea method
744 | await this.digArea(absStart, absEnd, progressCallback);
745 | }
746 |
747 | get entity() {
748 | if (!this.bot?.entity) return this.wrapError("Not connected");
749 | return {
750 | position: this.bot.entity.position,
751 | velocity: this.bot.entity.velocity,
752 | yaw: this.bot.entity.yaw,
753 | pitch: this.bot.entity.pitch,
754 | };
755 | }
756 |
757 | get entities() {
758 | if (!this.bot) return this.wrapError("Not connected");
759 | const converted: { [id: string]: CustomEntity } = {};
760 | for (const [id, e] of Object.entries(this.bot.entities)) {
761 | converted[id] = TypeConverters.entity(e);
762 | }
763 | return converted;
764 | }
765 |
766 | get inventory() {
767 | if (!this.bot) return this.wrapError("Not connected");
768 | return {
769 | items: () => this.bot!.inventory.items().map(TypeConverters.item),
770 | slots: Object.fromEntries(
771 | Object.entries(this.bot!.inventory.slots).map(([slot, item]) => [
772 | slot,
773 | item ? TypeConverters.item(item) : null,
774 | ])
775 | ),
776 | };
777 | }
778 |
779 | get pathfinder() {
780 | if (!this.bot) return this.wrapError("Not connected");
781 | if (!this.movements) {
782 | this.movements = new Movements(this.bot as unknown as Bot);
783 | }
784 | const pf = this.bot.pathfinder;
785 | const currentMovements = this.movements;
786 |
787 | return {
788 | setMovements: (movements: Movements) => {
789 | this.movements = movements;
790 | pf.setMovements(movements);
791 | },
792 | goto: (goal: goals.Goal) => pf.goto(goal),
793 | getPathTo: async (goal: goals.Goal, timeout?: number) => {
794 | if (!this.movements) return this.wrapError("Movements not initialized");
795 | const path = await pf.getPathTo(this.movements, goal, timeout);
796 | if (!path) return null;
797 | return {
798 | path: path.path.map((pos: any) => new Vec3(pos.x, pos.y, pos.z)),
799 | };
800 | },
801 | };
802 | }
803 |
804 | blockAt(position: Vec3): Block | null {
805 | if (!this.bot) return this.wrapError("Not connected");
806 | const block = this.bot.blockAt(position);
807 | return block ? TypeConverters.block(block) : null;
808 | }
809 |
810 | findBlocks(options: {
811 | matching: ((block: Block) => boolean) | string | string[];
812 | maxDistance: number;
813 | count: number;
814 | point?: Vec3;
815 | }): Vec3[] {
816 | if (!this.bot) return this.wrapError("Not connected");
817 |
818 | // Convert string or string[] to matching function
819 | let matchingFn: (block: PrismarineBlock) => boolean;
820 | if (typeof options.matching === "string") {
821 | const blockName = options.matching;
822 | matchingFn = (b: PrismarineBlock) => b.name === blockName;
823 | } else if (Array.isArray(options.matching)) {
824 | const blockNames = options.matching;
825 | matchingFn = (b: PrismarineBlock) => blockNames.includes(b.name);
826 | } else {
827 | const matchingFunc = options.matching;
828 | matchingFn = (b: PrismarineBlock) =>
829 | matchingFunc(TypeConverters.block(b));
830 | }
831 |
832 | return this.bot.findBlocks({
833 | ...options,
834 | matching: matchingFn,
835 | });
836 | }
837 |
838 | getEquipmentDestSlot(destination: string): number {
839 | if (!this.bot) return this.wrapError("Not connected");
840 | return this.bot.getEquipmentDestSlot(destination);
841 | }
842 |
843 | canSeeEntity(entity: CustomEntity): boolean {
844 | if (!this.bot) return false;
845 | const prismarineEntity = Object.values(this.bot.entities).find(
846 | (e) =>
847 | e.name === entity.name &&
848 | e.position.equals(
849 | new Vec3(entity.position.x, entity.position.y, entity.position.z)
850 | )
851 | );
852 | if (!prismarineEntity) return false;
853 |
854 | // Simple line-of-sight check
855 | const distance = prismarineEntity.position.distanceTo(
856 | this.bot.entity.position
857 | );
858 | return (
859 | distance <= 32 &&
860 | this.hasLineOfSight(this.bot.entity.position, prismarineEntity.position)
861 | );
862 | }
863 |
864 | private hasLineOfSight(start: Vec3, end: Vec3): boolean {
865 | if (!this.bot) return false;
866 | const direction = end.minus(start).normalize();
867 | const distance = start.distanceTo(end);
868 | const steps = Math.ceil(distance);
869 |
870 | for (let i = 1; i < steps; i++) {
871 | const point = start.plus(direction.scaled(i));
872 | const block = this.getPrismarineBlock(point);
873 | if (block?.boundingBox !== "empty") {
874 | return false;
875 | }
876 | }
877 | return true;
878 | }
879 |
880 | private getPrismarineBlock(
881 | position: Vec3
882 | ): PrismarineBlockWithBoundingBox | undefined {
883 | if (!this.bot) return undefined;
884 | const block = this.bot.blockAt(position);
885 | if (!block) return undefined;
886 | return block as PrismarineBlockWithBoundingBox;
887 | }
888 |
889 | async craftItem(
890 | itemName: string,
891 | quantity: number = 1,
892 | useCraftingTable: boolean = false
893 | ): Promise<void> {
894 | if (!this.bot) return this.wrapError("Not connected");
895 |
896 | try {
897 | // Find all available recipes
898 | const itemById = this.bot.registry.itemsByName[itemName];
899 | if (!itemById) return this.wrapError(`Unknown item: ${itemName}`);
900 | const recipes = this.bot.recipesFor(itemById.id, 1, null, true);
901 | const recipe = recipes[0]; // First matching recipe
902 |
903 | if (!recipe) {
904 | return this.wrapError(`No recipe found for ${itemName}`);
905 | }
906 |
907 | if (recipe.requiresTable && !useCraftingTable) {
908 | return this.wrapError(`${itemName} requires a crafting table`);
909 | }
910 |
911 | // If we need a crafting table, find one nearby or place one
912 | let craftingTableBlock = null;
913 | if (useCraftingTable) {
914 | const nearbyBlocks = this.findBlocks({
915 | matching: (block) => block.name === "crafting_table",
916 | maxDistance: 4,
917 | count: 1,
918 | });
919 |
920 | if (nearbyBlocks.length > 0) {
921 | craftingTableBlock = this.bot.blockAt(nearbyBlocks[0]);
922 | } else {
923 | // Try to place a crafting table
924 | const tableItem = this.bot.inventory
925 | .items()
926 | .find((i) => i.name === "crafting_table");
927 | if (!tableItem) {
928 | return this.wrapError("No crafting table in inventory");
929 | }
930 |
931 | // Find a suitable position to place the table
932 | const pos = this.bot.entity.position.offset(0, 0, 1);
933 | await this.placeBlock(pos.x, pos.y, pos.z, "crafting_table");
934 | craftingTableBlock = this.bot.blockAt(pos);
935 | }
936 | }
937 |
938 | await this.bot.craft(recipe, quantity, craftingTableBlock || undefined);
939 | } catch (error) {
940 | return this.wrapError(
941 | `Failed to craft ${itemName}: ${
942 | error instanceof Error ? error.message : String(error)
943 | }`
944 | );
945 | }
946 | }
947 |
948 | async equipItem(
949 | itemName: string,
950 | destination: EquipmentDestination
951 | ): Promise<void> {
952 | if (!this.bot) return this.wrapError("Not connected");
953 |
954 | const item = this.bot.inventory.items().find((i) => i.name === itemName);
955 | if (!item) return this.wrapError(`No ${itemName} in inventory`);
956 |
957 | try {
958 | await this.bot.equip(item, destination);
959 | } catch (error) {
960 | return this.wrapError(
961 | `Failed to equip ${itemName}: ${
962 | error instanceof Error ? error.message : String(error)
963 | }`
964 | );
965 | }
966 | }
967 |
968 | async dropItem(itemName: string, quantity: number = 1): Promise<void> {
969 | if (!this.bot) return this.wrapError("Not connected");
970 |
971 | const item = this.bot.inventory.items().find((i) => i.name === itemName);
972 | if (!item) return this.wrapError(`No ${itemName} in inventory`);
973 |
974 | try {
975 | await this.bot.toss(item.type, quantity, null);
976 | } catch (error) {
977 | return this.wrapError(
978 | `Failed to drop ${itemName}: ${
979 | error instanceof Error ? error.message : String(error)
980 | }`
981 | );
982 | }
983 | }
984 |
985 | async openContainer(position: Position): Promise<Container> {
986 | if (!this.bot) return this.wrapError("Not connected");
987 |
988 | const block = this.bot.blockAt(
989 | new Vec3(position.x, position.y, position.z)
990 | );
991 | if (!block) return this.wrapError("No block at specified position");
992 |
993 | try {
994 | const container = await this.bot.openContainer(block);
995 |
996 | return {
997 | type: block.name as "chest" | "furnace" | "crafting_table",
998 | position,
999 | slots: Object.fromEntries(
1000 | Object.entries(container.slots).map(([slot, item]) => [
1001 | slot,
1002 | item ? TypeConverters.item(item as Item) : null,
1003 | ])
1004 | ),
1005 | };
1006 | } catch (error) {
1007 | return this.wrapError(
1008 | `Failed to open container: ${
1009 | error instanceof Error ? error.message : String(error)
1010 | }`
1011 | );
1012 | }
1013 | }
1014 |
1015 | closeContainer(): void {
1016 | if (!this.bot?.currentWindow) return;
1017 | this.bot.closeWindow(this.bot.currentWindow);
1018 | }
1019 |
1020 | getRecipe(itemName: string): Recipe | null {
1021 | if (!this.bot) return null;
1022 |
1023 | const itemById = this.bot.registry.itemsByName[itemName];
1024 | if (!itemById) return null;
1025 | const recipes = this.bot.recipesFor(itemById.id, 1, null, true);
1026 | const recipe = recipes[0];
1027 | if (!recipe) return null;
1028 |
1029 | return {
1030 | name: itemName,
1031 | ingredients: (recipe.ingredients as any[])
1032 | .filter((item) => item != null)
1033 | .reduce((acc: { [key: string]: number }, item) => {
1034 | const name = Object.entries(this.bot!.registry.itemsByName).find(
1035 | ([_, v]) => v.id === item.id
1036 | )?.[0];
1037 | if (name) {
1038 | acc[name] = (acc[name] || 0) + 1;
1039 | }
1040 | return acc;
1041 | }, {}),
1042 | requiresCraftingTable: recipe.requiresTable,
1043 | };
1044 | }
1045 |
1046 | listAvailableRecipes(): Recipe[] {
1047 | if (!this.bot) return [];
1048 |
1049 | const recipes = new Set<string>();
1050 |
1051 | // Get all item names from registry
1052 | Object.keys(this.bot.registry.itemsByName).forEach((name) => {
1053 | const recipe = this.getRecipe(name);
1054 | if (recipe) {
1055 | recipes.add(name);
1056 | }
1057 | });
1058 |
1059 | return Array.from(recipes)
1060 | .map((name) => this.getRecipe(name))
1061 | .filter((recipe): recipe is Recipe => recipe !== null);
1062 | }
1063 |
1064 | canCraft(recipe: Recipe): boolean {
1065 | if (!this.bot) return false;
1066 |
1067 | // Check if we have all required ingredients
1068 | for (const [itemName, count] of Object.entries(recipe.ingredients)) {
1069 | const available = this.bot.inventory
1070 | .items()
1071 | .filter((item) => item.name === itemName)
1072 | .reduce((sum, item) => sum + item.count, 0);
1073 |
1074 | if (available < count) return false;
1075 | }
1076 |
1077 | // If it needs a crafting table, check if we have one or can reach one
1078 | if (recipe.requiresCraftingTable) {
1079 | const hasCraftingTable = this.bot.inventory
1080 | .items()
1081 | .some((item) => item.name === "crafting_table");
1082 |
1083 | if (!hasCraftingTable) {
1084 | const nearbyCraftingTable = this.findBlocks({
1085 | matching: (block) => block.name === "crafting_table",
1086 | maxDistance: 4,
1087 | count: 1,
1088 | });
1089 |
1090 | if (nearbyCraftingTable.length === 0) return false;
1091 | }
1092 | }
1093 |
1094 | return true;
1095 | }
1096 |
1097 | async smeltItem(
1098 | itemName: string,
1099 | fuelName: string,
1100 | quantity: number = 1
1101 | ): Promise<void> {
1102 | if (!this.bot) return this.wrapError("Not connected");
1103 |
1104 | try {
1105 | // Find a nearby furnace or place one
1106 | const nearbyBlocks = this.findBlocks({
1107 | matching: (block) => block.name === "furnace",
1108 | maxDistance: 4,
1109 | count: 1,
1110 | });
1111 |
1112 | let furnaceBlock;
1113 | if (nearbyBlocks.length > 0) {
1114 | furnaceBlock = this.bot.blockAt(nearbyBlocks[0]);
1115 | } else {
1116 | // Try to place a furnace
1117 | const furnaceItem = this.bot.inventory
1118 | .items()
1119 | .find((i) => i.name === "furnace");
1120 | if (!furnaceItem) {
1121 | return this.wrapError("No furnace in inventory");
1122 | }
1123 |
1124 | const pos = this.bot.entity.position.offset(0, 0, 1);
1125 | await this.placeBlock(pos.x, pos.y, pos.z, "furnace");
1126 | furnaceBlock = this.bot.blockAt(pos);
1127 | }
1128 |
1129 | if (!furnaceBlock)
1130 | return this.wrapError("Could not find or place furnace");
1131 |
1132 | // Open the furnace
1133 | const furnace = (await this.bot.openContainer(
1134 | furnaceBlock
1135 | )) as unknown as Furnace;
1136 |
1137 | try {
1138 | // Add the item to smelt
1139 | const itemToSmelt = this.bot.inventory
1140 | .items()
1141 | .find((i) => i.name === itemName);
1142 | if (!itemToSmelt) return this.wrapError(`No ${itemName} in inventory`);
1143 |
1144 | // Add the fuel
1145 | const fuelItem = this.bot.inventory
1146 | .items()
1147 | .find((i) => i.name === fuelName);
1148 | if (!fuelItem) return this.wrapError(`No ${fuelName} in inventory`);
1149 |
1150 | // Put items in the furnace
1151 | await furnace.putInput(itemToSmelt.type, null, quantity);
1152 | await furnace.putFuel(fuelItem.type, null, quantity);
1153 |
1154 | // Wait for smelting to complete
1155 | await new Promise((resolve) => {
1156 | const checkInterval = setInterval(() => {
1157 | if (furnace.fuel === 0 && furnace.progress === 0) {
1158 | clearInterval(checkInterval);
1159 | resolve(null);
1160 | }
1161 | }, 1000);
1162 | });
1163 | } finally {
1164 | // Always close the furnace when done
1165 | this.bot.closeWindow(furnace);
1166 | }
1167 | } catch (error) {
1168 | return this.wrapError(
1169 | `Failed to smelt ${itemName}: ${
1170 | error instanceof Error ? error.message : String(error)
1171 | }`
1172 | );
1173 | }
1174 | }
1175 |
1176 | async depositItem(
1177 | containerPosition: Position,
1178 | itemName: string,
1179 | quantity: number = 1
1180 | ): Promise<void> {
1181 | if (!this.bot) return this.wrapError("Not connected");
1182 |
1183 | try {
1184 | const block = this.bot.blockAt(
1185 | new Vec3(containerPosition.x, containerPosition.y, containerPosition.z)
1186 | );
1187 | if (!block) return this.wrapError("No container at position");
1188 |
1189 | const window = await this.bot.openContainer(block);
1190 | if (!window) return this.wrapError("Failed to open container");
1191 |
1192 | try {
1193 | const item = this.bot.inventory.slots.find((i) => i?.name === itemName);
1194 | if (!item) return this.wrapError(`No ${itemName} in inventory`);
1195 |
1196 | const emptySlot = window.slots.findIndex(
1197 | (slot: Item | null) => slot === null
1198 | );
1199 | if (emptySlot === -1) return this.wrapError("Container is full");
1200 |
1201 | await this.bot.moveSlotItem(item.slot, emptySlot);
1202 | } finally {
1203 | this.bot.closeWindow(window);
1204 | }
1205 | } catch (error) {
1206 | return this.wrapError(
1207 | `Failed to deposit ${itemName}: ${
1208 | error instanceof Error ? error.message : String(error)
1209 | }`
1210 | );
1211 | }
1212 | }
1213 |
1214 | async withdrawItem(
1215 | containerPosition: Position,
1216 | itemName: string,
1217 | quantity: number = 1
1218 | ): Promise<void> {
1219 | if (!this.bot) return this.wrapError("Not connected");
1220 |
1221 | try {
1222 | const block = this.bot.blockAt(
1223 | new Vec3(containerPosition.x, containerPosition.y, containerPosition.z)
1224 | );
1225 | if (!block) return this.wrapError("No container at position");
1226 |
1227 | const window = await this.bot.openContainer(block);
1228 | if (!window) return this.wrapError("Failed to open container");
1229 |
1230 | try {
1231 | const containerSlot = window.slots.findIndex(
1232 | (item: Item | null) => item?.name === itemName
1233 | );
1234 | if (containerSlot === -1)
1235 | return this.wrapError(`No ${itemName} in container`);
1236 |
1237 | const emptySlot = this.bot.inventory.slots.findIndex(
1238 | (slot) => slot === null
1239 | );
1240 | if (emptySlot === -1) return this.wrapError("Inventory is full");
1241 |
1242 | await this.bot.moveSlotItem(containerSlot, emptySlot);
1243 | } finally {
1244 | this.bot.closeWindow(window);
1245 | }
1246 | } catch (error) {
1247 | return this.wrapError(
1248 | `Failed to withdraw ${itemName}: ${
1249 | error instanceof Error ? error.message : String(error)
1250 | }`
1251 | );
1252 | }
1253 | }
1254 |
1255 | private wrapError(message: string): never {
1256 | throw {
1257 | code: -32603,
1258 | message,
1259 | data: null,
1260 | };
1261 | }
1262 | }
1263 |
```