├── .cursorrules ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ └── ci.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bun.lockb ├── jest.config.js ├── knowledge.md ├── package-lock.json ├── package.json ├── 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 /.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: `(): ` 126 | 127 | - ``: 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 | - ``: Optional, indicates section of codebase (e.g., bot, server, tools) 138 | - ``: 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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 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/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 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/pull_request_template.md: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | coverage/ 4 | .env 5 | .env.* 6 | *.log 7 | .DS_Store 8 | .vscode/ 9 | .idea/ 10 | *.tsbuildinfo -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | bun test -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Gerred Dillon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerred/mcpmc/b2b0a32e1d1a3a6fa92e7630d9bc3608d9837650/bun.lockb -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: 'ts-jest/presets/default-esm', 3 | testEnvironment: 'node', 4 | roots: ['/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: ['/src/__tests__/setup.ts'] 21 | }; 22 | -------------------------------------------------------------------------------- /knowledge.md: -------------------------------------------------------------------------------- 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=` 21 | - Requires 2FA authentication 22 | - Get OTP code from authenticator app 23 | - Package will be published to npm registry with public access 24 | -------------------------------------------------------------------------------- /package.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": "yarn@1.22.22+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: -------------------------------------------------------------------------------- 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__/mocks/mockBot.ts: -------------------------------------------------------------------------------- 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 { 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 { 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 { 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 { 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 { 227 | if (!this.isConnected) throw new Error("Not connected"); 228 | } 229 | 230 | async equipItem(itemName: string, destination: string): Promise { 231 | if (!this.isConnected) throw new Error("Not connected"); 232 | } 233 | 234 | async dropItem(itemName: string, quantity?: number): Promise { 235 | if (!this.isConnected) throw new Error("Not connected"); 236 | } 237 | 238 | async openContainer(position: Position): Promise { 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 { 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 { 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 { 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/__tests__/server.test.ts: -------------------------------------------------------------------------------- 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/__tests__/setup.ts: -------------------------------------------------------------------------------- 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/cli.ts: -------------------------------------------------------------------------------- 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; 10 | 11 | export function parseArgs(args: string[]): CLIArgs { 12 | const parsedArgs: Record = {}; 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 | -------------------------------------------------------------------------------- /src/core/bot.ts: -------------------------------------------------------------------------------- 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; 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 { 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 { 80 | return new Promise((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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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(); 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 { 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 { 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 { 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 | -------------------------------------------------------------------------------- /src/handlers/resources.ts: -------------------------------------------------------------------------------- 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; 16 | handleGetPosition(uri: string): Promise; 17 | handleGetBlocksNearby(uri: string): Promise; 18 | handleGetEntitiesNearby(uri: string): Promise; 19 | handleGetInventory(uri: string): Promise; 20 | handleGetHealth(uri: string): Promise; 21 | handleGetWeather(uri: string): Promise; 22 | } 23 | 24 | export class MinecraftResourceHandler implements ResourceHandler { 25 | constructor(private bot: MinecraftBot) {} 26 | 27 | async handleGetPlayers(uri: string): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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/handlers/tools.ts: -------------------------------------------------------------------------------- 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; 9 | handleNavigateTo(x: number, y: number, z: number): Promise; 10 | handleNavigateRelative( 11 | dx: number, 12 | dy: number, 13 | dz: number 14 | ): Promise; 15 | handleDigBlock(x: number, y: number, z: number): Promise; 16 | handleDigBlockRelative( 17 | dx: number, 18 | dy: number, 19 | dz: number 20 | ): Promise; 21 | handleDigArea( 22 | start: { x: number; y: number; z: number }, 23 | end: { x: number; y: number; z: number } 24 | ): Promise; 25 | handleDigAreaRelative( 26 | start: { dx: number; dy: number; dz: number }, 27 | end: { dx: number; dy: number; dz: number } 28 | ): Promise; 29 | handlePlaceBlock( 30 | x: number, 31 | y: number, 32 | z: number, 33 | blockName: string 34 | ): Promise; 35 | handleFollowPlayer(username: string, distance: number): Promise; 36 | handleAttackEntity( 37 | entityName: string, 38 | maxDistance: number 39 | ): Promise; 40 | handleInspectBlock( 41 | position: { x: number; y: number; z: number }, 42 | includeState: boolean 43 | ): Promise; 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; 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; 65 | handleCheckPath( 66 | destination: { x: number; y: number; z: number }, 67 | dryRun: boolean, 68 | includeObstacles: boolean 69 | ): Promise; 70 | handleInspectInventory( 71 | itemType?: string, 72 | includeEquipment?: boolean 73 | ): Promise; 74 | handleCraftItem( 75 | itemName: string, 76 | quantity?: number, 77 | useCraftingTable?: boolean 78 | ): Promise; 79 | handleSmeltItem( 80 | itemName: string, 81 | fuelName: string, 82 | quantity?: number 83 | ): Promise; 84 | handleEquipItem( 85 | itemName: string, 86 | destination: "hand" | "off-hand" | "head" | "torso" | "legs" | "feet" 87 | ): Promise; 88 | handleDepositItem( 89 | containerPosition: Position, 90 | itemName: string, 91 | quantity?: number 92 | ): Promise; 93 | handleWithdrawItem( 94 | containerPosition: Position, 95 | itemName: string, 96 | quantity?: number 97 | ): Promise; 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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/index.ts: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /src/schemas.ts: -------------------------------------------------------------------------------- 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; 144 | export type Position = z.infer; 145 | export type RelativePosition = z.infer; 146 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 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; 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; 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) { 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 { 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 { 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/tools/index.ts: -------------------------------------------------------------------------------- 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; 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: -------------------------------------------------------------------------------- 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; 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; 93 | navigateTo(x: number, y: number, z: number): Promise; 94 | digBlockRelative(dx: number, dy: number, dz: number): Promise; 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; 104 | placeBlock(x: number, y: number, z: number, blockName: string): Promise; 105 | 106 | // ---- Entity Interaction ---- 107 | followPlayer(username: string, distance?: number): Promise; 108 | attackEntity(entityName: string, maxDistance?: number): Promise; 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; 127 | smeltItem( 128 | itemName: string, 129 | fuelName: string, 130 | quantity?: number 131 | ): Promise; 132 | equipItem( 133 | itemName: string, 134 | destination: "hand" | "off-hand" | "head" | "torso" | "legs" | "feet" 135 | ): Promise; 136 | depositItem( 137 | containerPosition: Position, 138 | itemName: string, 139 | quantity?: number 140 | ): Promise; 141 | withdrawItem( 142 | containerPosition: Position, 143 | itemName: string, 144 | quantity?: number 145 | ): Promise; 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/types/tools.ts: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /tsconfig.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 | --------------------------------------------------------------------------------