#
tokens: 42684/50000 27/27 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | [![npm version](https://badge.fury.io/js/@gerred%2Fmcpmc.svg)](https://badge.fury.io/js/@gerred%2Fmcpmc)
 4 | [![npm downloads](https://img.shields.io/npm/dm/@gerred/mcpmc.svg)](https://www.npmjs.com/package/@gerred/mcpmc)
 5 | [![CI](https://github.com/gerred/mcpmc/workflows/CI/badge.svg)](https://github.com/gerred/mcpmc/actions?query=workflow%3ACI)
 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 | 
```