# Directory Structure
```
├── .cursorrules
├── .github
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── pull_request_template.md
│ └── workflows
│ └── ci.yml
├── .gitignore
├── .husky
│ └── pre-commit
├── .npmignore
├── bun.lockb
├── CONTRIBUTING.md
├── jest.config.js
├── knowledge.md
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── __tests__
│ │ ├── bot.test.ts
│ │ ├── mocks
│ │ │ └── mockBot.ts
│ │ ├── server.test.ts
│ │ └── setup.ts
│ ├── cli.ts
│ ├── core
│ │ └── bot.ts
│ ├── handlers
│ │ ├── resources.ts
│ │ └── tools.ts
│ ├── index.ts
│ ├── schemas.ts
│ ├── server.ts
│ ├── tools
│ │ └── index.ts
│ └── types
│ ├── minecraft.ts
│ └── tools.ts
├── tsconfig.json
└── yarn.lock
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
node_modules/
build/
coverage/
.env
.env.*
*.log
.DS_Store
.vscode/
.idea/
*.tsbuildinfo
```
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
```
src/__tests__/
coverage/
.github/
.vscode/
.idea/
*.test.ts
*.spec.ts
tsconfig.json
jest.config.js
.eslintrc
.prettierrc
```
--------------------------------------------------------------------------------
/.cursorrules:
--------------------------------------------------------------------------------
```
# .cursorrules
## ⚠️ IMPORTANT: JSON-RPC Warning
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:
1. Implement the actual Minecraft/Mineflayer functionality
2. Use the provided bot methods and APIs
3. Let the framework handle all communication
Never:
- Write JSON-RPC messages directly
- Handle stdio yourself
- Implement protocol-level error codes
- Create custom notification systems
## Overview
This project uses the Model Context Protocol (MCP) to bridge interactions between a Minecraft bot (powered by Mineflayer) and an LLM-based client.
The essential flow is:
1. The server starts up ("MinecraftServer") and connects to a Minecraft server automatically (via the Mineflayer bot).
2. The MCP server is exposed through standard JSON-RPC over stdio.
3. MCP "tools" correspond to actionable commands in Minecraft (e.g., "dig_area", "navigate_to", etc.).
4. MCP "resources" correspond to read-only data from Minecraft (e.g., "minecraft://inventory").
When an MCP client issues requests, the server routes these to either:
• The "toolHandler" (for effectful actions such as "dig_block")
• The "resourceHandler" (for returning game state like position, health, etc.)
## MCP Types and Imports
When working with MCP types:
1. Import types from the correct SDK paths:
- Transport: "@modelcontextprotocol/sdk/shared/transport.js"
- JSONRPCMessage and other core types: "@modelcontextprotocol/sdk/types.js"
2. Always check for optional fields using type guards (e.g., 'id' in message)
3. Follow existing implementations in example servers when unsure
4. Never modify working type imports - MCP has specific paths that must be used
## Progress Callbacks
For long-running operations like navigation and digging:
1. Use progress callbacks to report status to MCP clients
2. Include a progressToken in \_meta for tracking
3. Send notifications via "tool/progress" with:
- token: unique identifier
- progress: 0-100 percentage
- status: "in_progress" or "complete"
- message: human-readable progress
## API Compatibility and Alternatives
When working with Mineflayer's API:
1. Always check the actual API implementation before assuming method availability
2. When encountering type/compatibility issues:
- Look for alternative methods in the API (e.g., moveSlotItem instead of click)
- Consider type casting with 'unknown' when necessary (e.g., `as unknown as Furnace`)
- Add proper type annotations to parameters to avoid implicit any
3. For container operations:
- Prefer high-level methods like moveSlotItem over low-level ones
- Always handle cleanup (close containers) in finally blocks
- Cast specialized containers (like Furnace) appropriately
4. Error handling:
- Wrap all API calls in try/catch blocks
- Use wrapError for consistent error reporting
- Include specific error messages that help diagnose issues
## File Layout
- src/types/minecraft.ts
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.).
- src/core/bot.ts
Contains the main "MineflayerBot" class, an implementation of "MinecraftBot" using a real Mineflayer bot with pathfinding, digging, etc.
- src/handlers/tools.ts
Implements "ToolHandler" functions that receive tool requests and execute them against the MinecraftBot methods (e.g., "handleDigArea").
- src/handlers/resources.ts
Implements "ResourceHandler" for read-only data fetches (position, inventory, weather, etc.).
- src/core/server.ts (and src/server.ts in some setups)
Main MCP server that sets up request handlers, ties in the "MineflayerBot" instance, and starts listening for JSON-RPC calls over stdio.
- src/**tests**/\*
Contains Jest tests and "MockMinecraftBot" (a simplified implementation of "MinecraftBot" for testing).
## Tools and Technologies
- Model Context Protocol - an API for clients and servers to expose tools, resources, and prompts.
- Mineflayer
- Prismarine
## Code
- Write modern TypeScript against 2024 standards and expectations. Cleanly use async/await where possible.
- Use bun for CLI commands
## Error Handling
- All errors MUST be properly formatted as JSON-RPC responses over stdio
- Never throw errors directly as this will crash MCP clients
- Use the ToolResponse interface with isError: true for error cases
- Ensure all error messages are properly stringified JSON objects
## Logging Rules
- DO NOT use console.log, console.error, or any other console methods for logging
- All communication MUST be through JSON-RPC responses over stdio
- For error conditions, use proper JSON-RPC error response format
- For debug/info messages, include them in the response data structure
- Status updates should be sent as proper JSON-RPC notifications
- Never write directly to stdout/stderr as it will corrupt the JSON-RPC stream
## Commit Rules
Commits must follow the Conventional Commits specification (https://www.conventionalcommits.org/):
1. Format: `<type>(<scope>): <description>`
- `<type>`: The type of change being made:
- feat: A new feature
- fix: A bug fix
- docs: Documentation only changes
- style: Changes that do not affect the meaning of the code
- refactor: A code change that neither fixes a bug nor adds a feature
- perf: A code change that improves performance
- test: Adding missing tests or correcting existing tests
- chore: Changes to the build process or auxiliary tools
- ci: Changes to CI configuration files and scripts
- `<scope>`: Optional, indicates section of codebase (e.g., bot, server, tools)
- `<description>`: Clear, concise description in present tense
2. Examples:
- feat(bot): add block placement functionality
- fix(server): resolve reconnection loop issue
- docs(api): update tool documentation
- refactor(core): simplify connection handling
3. Breaking Changes:
- Include BREAKING CHANGE: in the commit footer
- Example: feat(api)!: change tool response format
4. Body and Footer:
- Optional but recommended for complex changes
- Separated from header by blank line
- Use bullet points for multiple changes
## Tool Handler Implementation Rules
The MinecraftToolHandler bridges Mineflayer's bot capabilities to MCP tools. Each handler maps directly to bot functionality:
1. Navigation & Movement
- `handleNavigateTo/handleNavigateRelative`: Uses Mineflayer pathfinding
- Always provide progress callbacks for pathfinding operations
- Handles coordinate translation between absolute/relative positions
- Uses goals.GoalBlock/goals.GoalXZ from mineflayer-pathfinder
2. Block Interaction
- `handleDigBlock/handleDigBlockRelative`: Direct block breaking
- `handleDigArea`: Area excavation with progress tracking
- `handlePlaceBlock`: Block placement with item selection
- `handleInspectBlock`: Block state inspection
- Uses Vec3 for position handling
3. Entity Interaction
- `handleFollowPlayer`: Player tracking with pathfinding
- `handleAttackEntity`: Combat with entity targeting
- Uses entity.position and entity.type from Mineflayer
4. Inventory Management
- `handleInspectInventory`: Inventory querying
- `handleCraftItem`: Crafting with/without tables
- `handleSmeltItem`: Furnace operations
- `handleEquipItem`: Equipment management
- `handleDepositItem/handleWithdrawItem`: Container interactions
- Uses window.items and container APIs
5. World Interaction
- `handleChat`: In-game communication
- `handleFindBlocks`: Block finding with constraints
- `handleFindEntities`: Entity detection
- `handleCheckPath`: Path validation
Key Bot Methods Used:
```typescript
// Core Movement
bot.pathfinder.goto(goal: goals.Goal)
bot.navigate.to(x: number, y: number, z: number)
// Block Operations
bot.dig(block: Block)
bot.placeBlock(referenceBlock: Block, faceVector: Vec3)
// Entity Interaction
bot.attack(entity: Entity)
bot.lookAt(position: Vec3)
// Inventory
bot.equip(item: Item, destination: string)
bot.craft(recipe: Recipe, count: number, craftingTable: Block)
// World Interaction
bot.findBlocks(options: FindBlocksOptions)
bot.blockAt(position: Vec3)
bot.chat(message: string)
```
Testing Focus:
- Test each bot method integration
- Verify coordinate systems (absolute vs relative)
- Check entity targeting and tracking
- Validate inventory operations
- Test pathfinding edge cases
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.
## JSON Response Formatting
When implementing tool handlers that return structured data:
1. Avoid using `type: "json"` with `JSON.stringify` for nested objects
2. Instead, format complex data as human-readable text
3. Use template literals and proper formatting for nested structures
4. For lists of items, use bullet points or numbered lists
5. Include relevant units and round numbers appropriately
6. Make responses both machine-parseable and human-readable
Examples:
✅ Good: `Found 3 blocks: \n- Stone at (10, 64, -30), distance: 5.2\n- Dirt at (11, 64, -30), distance: 5.5`
❌ Bad: `{"blocks":[{"name":"stone","position":{"x":10,"y":64,"z":-30}}]}`
## Building and Construction
When implementing building functionality:
1. Always check inventory before attempting to place blocks
2. Use find_blocks to locate suitable building locations and materials
3. Combine digging and building operations for complete structures
4. Follow a clear building pattern:
- Clear the area if needed (dig_area_relative)
- Place foundation blocks first
- Build walls from bottom to top
- Add details like doors and windows last
5. Consider the bot's position and reachability:
- Stay within reach distance (typically 4 blocks)
- Move to new positions as needed
- Ensure stable ground for the bot to stand on
6. Handle errors gracefully:
- Check for block placement success
- Have fallback positions for block placement
- Log unreachable or problematic areas
Example building sequence:
1. Survey area with find_blocks
2. Clear space with dig_area_relative
3. Check inventory for materials
4. Place foundation blocks
5. Build walls and roof
6. Add finishing touches
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# MCPMC (Minecraft Model Context Protocol)
[](https://badge.fury.io/js/@gerred%2Fmcpmc)
[](https://www.npmjs.com/package/@gerred/mcpmc)
[](https://github.com/gerred/mcpmc/actions?query=workflow%3ACI)
[](https://opensource.org/licenses/MIT)
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.
## Features
- Full MCP compatibility for AI agent integration
- Built on Mineflayer for reliable Minecraft interaction
- Supports navigation, block manipulation, inventory management, and more
- Real-time game state monitoring
- Type-safe API with TypeScript support
## Installation
```bash
# Using npm
npm install @gerred/mcpmc
# Using yarn
yarn add @gerred/mcpmc
# Using bun
bun add @gerred/mcpmc
```
## Usage
```bash
# Start the MCP server
mcpmc
```
The server communicates via stdin/stdout using the Model Context Protocol. For detailed API documentation, use the MCP inspector:
```bash
bun run inspector
```
## Development
```bash
# Install dependencies
bun install
# Run tests
bun test
# Build the project
bun run build
# Watch mode during development
bun run watch
# Run MCP inspector
bun run inspector
```
## Contributing
Contributions are welcome! Please follow these steps:
1. Fork the repository
2. Create a new branch for your feature
3. Write tests for your changes
4. Make your changes
5. Run tests and ensure they pass
6. Submit a pull request
Please make sure to update tests as appropriate and adhere to the existing coding style.
## License
MIT License
Copyright (c) 2024 Gerred Dillon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
```markdown
# Contributing to MCPMC
We love your input! We want to make contributing to MCPMC as easy and transparent as possible, whether it's:
- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- Proposing new features
- Becoming a maintainer
## We Develop with GitHub
We use GitHub to host code, to track issues and feature requests, as well as accept pull requests.
## We Use [GitHub Flow](https://guides.github.com/introduction/flow/index.html)
Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests:
1. Fork the repo and create your branch from `main`.
2. If you've added code that should be tested, add tests.
3. If you've changed APIs, update the documentation.
4. Ensure the test suite passes.
5. Make sure your code lints.
6. Issue that pull request!
## Any contributions you make will be under the MIT Software License
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.
## Report bugs using GitHub's [issue tracker](https://github.com/gerred/mcpmc/issues)
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!
## Write bug reports with detail, background, and sample code
**Great Bug Reports** tend to have:
- A quick summary and/or background
- Steps to reproduce
- Be specific!
- Give sample code if you can.
- What you expected would happen
- What actually happens
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
## Use a Consistent Coding Style
- Use TypeScript strict mode
- 2 spaces for indentation rather than tabs
- You can try running `bun test` for style unification
## License
By contributing, you agree that your contributions will be licensed under its MIT License.
```
--------------------------------------------------------------------------------
/src/__tests__/setup.ts:
--------------------------------------------------------------------------------
```typescript
import { jest } from "@jest/globals";
// Make jest available globally
(global as any).jest = jest;
jest.mock("mineflayer", () => ({
createBot: jest.fn(),
}));
```
--------------------------------------------------------------------------------
/src/types/tools.ts:
--------------------------------------------------------------------------------
```typescript
export interface ToolResponse {
_meta?: {
progressToken?: string | number;
};
content: Array<{
type: string;
text: string;
}>;
isError?: boolean;
}
```
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
```javascript
export default {
preset: 'ts-jest/presets/default-esm',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.test.ts'],
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
},
],
},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
transformIgnorePatterns: [
'node_modules/(?!@modelcontextprotocol)'
],
setupFilesAfterEnv: ['<rootDir>/src/__tests__/setup.ts']
};
```
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
```markdown
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE] "
labels: enhancement
assignees: ""
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
```
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
```markdown
---
name: Bug report
about: Create a report to help us improve
title: "[BUG] "
labels: bug
assignees: ""
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Start server with '...'
2. Run command '....'
3. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Environment (please complete the following information):**
- OS: [e.g. macOS, Windows]
- Node.js version: [e.g. 18.0.0]
- Bun version: [e.g. 1.0.0]
- Minecraft version: [e.g. 1.20.4]
- Package version: [e.g. 0.0.1]
**Additional context**
Add any other context about the problem here.
```
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
```yaml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Run tests
run: bun test
- name: Build
run: bun run build
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Type check
run: bun run tsc --noEmit
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Updated module resolution
"moduleResolution": "node",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"resolveJsonModule": true,
"esModuleInterop": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
},
"exclude": [
"node_modules",
"dist",
"build",
"coverage"
]
}
```
--------------------------------------------------------------------------------
/knowledge.md:
--------------------------------------------------------------------------------
```markdown
# CLI Publishing
Package is configured as a CLI tool:
- Binary name: `mcpmc`
- Executable: `build/index.js`
- Global install: `npm install -g @gerred/mcpmc`
- Required files included in npm package:
- build/index.js (executable)
- README.md
- LICENSE
- package.json
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.
# Publishing Process
1. Run tests and build: `bun test && bun run build`
2. Bump version: `npm version patch|minor|major`
3. Push changes: `git push && git push --tags`
4. Publish: `npm publish --otp=<code>`
- Requires 2FA authentication
- Get OTP code from authenticator app
- Package will be published to npm registry with public access
```
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
export const cliSchema = z.object({
host: z.string().default("localhost"),
port: z.number().int().min(1).max(65535).default(25565),
username: z.string().min(1).default("Claude"),
});
export type CLIArgs = z.infer<typeof cliSchema>;
export function parseArgs(args: string[]): CLIArgs {
const parsedArgs: Record<string, string | number> = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith("--")) {
const key = arg.slice(2);
const value = args[i + 1];
if (value && !value.startsWith("--")) {
parsedArgs[key] = key === "port" ? parseInt(value, 10) : value;
i++; // Skip the value in next iteration
}
}
}
// Parse with schema and get defaults
return cliSchema.parse({
host: parsedArgs.host || undefined,
port: parsedArgs.port || undefined,
username: parsedArgs.username || undefined,
});
}
```
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
```markdown
## Description
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context.
Fixes # (issue)
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce.
- [ ] Test A
- [ ] Test B
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
// Only if this file is run directly or via npx, create and start the server
if (
import.meta.url === `file://${process.argv[1]}` ||
process.argv[1]?.includes("mcpmc")
) {
const { MinecraftServer } = await import("./server.js");
const { parseArgs } = await import("./cli.js");
try {
const connectionParams = parseArgs(process.argv.slice(2));
const server = new MinecraftServer(connectionParams);
// Suppress deprecation warnings
process.removeAllListeners("warning");
process.on("warning", (warning) => {
if (warning.name !== "DeprecationWarning") {
process.stderr.write(
JSON.stringify({
jsonrpc: "2.0",
method: "system.warning",
params: {
message: warning.toString(),
type: "warning",
},
}) + "\n"
);
}
});
await server.start();
} catch (error: unknown) {
throw {
code: -32000,
message: "Server startup failed",
data: {
error: error instanceof Error ? error.message : String(error),
},
};
}
}
export * from "./server.js";
export * from "./schemas.js";
export * from "./tools/index.js";
export * from "./core/bot.js";
export * from "./handlers/tools.js";
export * from "./handlers/resources.js";
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@gerred/mcpmc",
"version": "0.0.9",
"description": "A MCP server for interacting with Minecraft via Mineflayer",
"private": false,
"type": "module",
"bin": {
"mcpmc": "./build/index.js"
},
"files": [
"build",
"README.md"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "bun build ./src/index.ts --outdir=build --target=node && chmod +x build/index.js",
"prepare": "husky install && bun run build",
"watch": "bun build ./src/index.ts --outdir=build --target=node --watch",
"inspector": "bunx @modelcontextprotocol/inspector build/index.js",
"start": "bun run build/index.js",
"test": "bun test",
"test:watch": "bun test --watch",
"test:coverage": "bun test --coverage"
},
"dependencies": {
"@modelcontextprotocol/inspector": "https://github.com/modelcontextprotocol/inspector.git#main",
"@modelcontextprotocol/sdk": "1.0.4",
"bunx": "^0.1.0",
"mineflayer": "^4.23.0",
"mineflayer-pathfinder": "^2.4.5",
"vec3": "^0.1.10",
"zod-to-json-schema": "^3.24.1"
},
"devDependencies": {
"@types/bun": "latest",
"@types/jest": "^29.5.14",
"husky": "^9.1.7",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"typescript": "^5.3.3"
},
"module": "index.ts",
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"repository": {
"type": "git",
"url": "git+https://github.com/gerred/mcpmc.git"
},
"keywords": [
"minecraft",
"mineflayer",
"ai",
"bot",
"mcp",
"claude"
],
"author": "Gerred Dillon",
"license": "MIT",
"bugs": {
"url": "https://github.com/gerred/mcpmc/issues"
},
"homepage": "https://github.com/gerred/mcpmc#readme"
}
```
--------------------------------------------------------------------------------
/src/__tests__/bot.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, beforeEach } from "@jest/globals";
import { MockMinecraftBot } from "./mocks/mockBot";
import type { MinecraftBot } from "../types/minecraft";
describe("MinecraftBot", () => {
let bot: MinecraftBot;
beforeEach(() => {
bot = new MockMinecraftBot({
host: "localhost",
port: 25565,
username: "testBot",
});
});
describe("connection", () => {
it("should initialize with default position", () => {
expect(bot.getPosition()).toMatchObject({ x: 0, y: 64, z: 0 });
});
it("should return position after initialization", () => {
const pos = bot.getPosition();
expect(pos).toMatchObject({ x: 0, y: 64, z: 0 });
});
it("should throw on operations when not connected", () => {
bot.disconnect();
expect(() => bot.getHealth()).toThrow("Not connected");
expect(() => bot.getInventory()).toThrow("Not connected");
expect(() => bot.getPlayers()).toThrow("Not connected");
});
});
describe("navigation", () => {
it("should update position after navigation", async () => {
await bot.navigateTo(100, 64, 100);
const pos = bot.getPosition();
expect(pos).toMatchObject({ x: 100, y: 64, z: 100 });
});
it("should update position after relative navigation", async () => {
await bot.navigateRelative(10, 0, 10);
const pos = bot.getPosition();
expect(pos).toMatchObject({ x: 10, y: 64, z: 10 });
});
});
describe("game state", () => {
it("should return health status", () => {
const health = bot.getHealthStatus();
expect(health).toMatchObject({
health: 20,
food: 20,
saturation: 5,
armor: 0,
});
});
it("should return weather status", () => {
const weather = bot.getWeather();
expect(weather).toMatchObject({
isRaining: false,
rainState: "clear",
thunderState: 0,
});
});
});
});
```
--------------------------------------------------------------------------------
/src/__tests__/server.test.ts:
--------------------------------------------------------------------------------
```typescript
import { MinecraftServer } from "../server";
import { MinecraftToolHandler } from "../handlers/tools";
import type { MinecraftBot } from "../types/minecraft";
describe("MinecraftServer", () => {
let server: MinecraftServer;
let toolHandler: MinecraftToolHandler;
let mockBot: MinecraftBot;
beforeEach(() => {
mockBot = {
chat: jest.fn(),
navigateRelative: jest.fn(),
digBlockRelative: jest.fn(),
digAreaRelative: jest.fn(),
} as unknown as MinecraftBot;
server = new MinecraftServer({
host: "localhost",
port: 25565,
username: "testBot",
});
toolHandler = new MinecraftToolHandler(mockBot);
});
describe("tool handling", () => {
it("should handle chat tool", async () => {
const result = await toolHandler.handleChat("hello");
expect(result).toBeDefined();
expect(result.content[0].text).toContain("hello");
expect(mockBot.chat).toHaveBeenCalledWith("hello");
});
it("should handle navigate_relative tool", async () => {
const result = await toolHandler.handleNavigateRelative(1, 0, 1);
expect(result).toBeDefined();
expect(result.content[0].text).toContain("Navigated relative");
expect(mockBot.navigateRelative).toHaveBeenCalledWith(1, 0, 1, expect.any(Function));
});
it("should handle dig_block_relative tool", async () => {
const result = await toolHandler.handleDigBlockRelative(1, 0, 1);
expect(result).toBeDefined();
expect(result.content[0].text).toContain("Dug block relative");
expect(mockBot.digBlockRelative).toHaveBeenCalledWith(1, 0, 1);
});
it("should handle dig_area_relative tool", async () => {
const result = await toolHandler.handleDigAreaRelative(
{ dx: 0, dy: 0, dz: 0 },
{ dx: 2, dy: 2, dz: 2 }
);
expect(result).toBeDefined();
expect(result.content[0].text).toContain("Successfully completed");
expect(mockBot.digAreaRelative).toHaveBeenCalledWith(
{ dx: 0, dy: 0, dz: 0 },
{ dx: 2, dy: 2, dz: 2 },
expect.any(Function)
);
});
});
});
```
--------------------------------------------------------------------------------
/src/handlers/resources.ts:
--------------------------------------------------------------------------------
```typescript
import type { MinecraftBot } from "../types/minecraft";
export interface ResourceResponse {
_meta?: {
progressToken?: string | number;
};
contents: Array<{
uri: string;
mimeType: string;
text: string;
}>;
}
export interface ResourceHandler {
handleGetPlayers(uri: string): Promise<ResourceResponse>;
handleGetPosition(uri: string): Promise<ResourceResponse>;
handleGetBlocksNearby(uri: string): Promise<ResourceResponse>;
handleGetEntitiesNearby(uri: string): Promise<ResourceResponse>;
handleGetInventory(uri: string): Promise<ResourceResponse>;
handleGetHealth(uri: string): Promise<ResourceResponse>;
handleGetWeather(uri: string): Promise<ResourceResponse>;
}
export class MinecraftResourceHandler implements ResourceHandler {
constructor(private bot: MinecraftBot) {}
async handleGetPlayers(uri: string): Promise<ResourceResponse> {
const players = this.bot.getPlayers();
return {
_meta: {},
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify(players, null, 2),
},
],
};
}
async handleGetPosition(uri: string): Promise<ResourceResponse> {
const position = this.bot.getPosition();
return {
_meta: {},
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify(position, null, 2),
},
],
};
}
async handleGetBlocksNearby(uri: string): Promise<ResourceResponse> {
const blocks = this.bot.getBlocksNearby();
return {
_meta: {},
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify(blocks, null, 2),
},
],
};
}
async handleGetEntitiesNearby(uri: string): Promise<ResourceResponse> {
const entities = this.bot.getEntitiesNearby();
return {
_meta: {},
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify(entities, null, 2),
},
],
};
}
async handleGetInventory(uri: string): Promise<ResourceResponse> {
const inventory = this.bot.getInventory();
return {
_meta: {},
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify(inventory, null, 2),
},
],
};
}
async handleGetHealth(uri: string): Promise<ResourceResponse> {
const health = this.bot.getHealthStatus();
return {
_meta: {},
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify(health, null, 2),
},
],
};
}
async handleGetWeather(uri: string): Promise<ResourceResponse> {
const weather = this.bot.getWeather();
return {
_meta: {},
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify(weather, null, 2),
},
],
};
}
}
```
--------------------------------------------------------------------------------
/src/schemas.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
// Base schemas
export const PositionSchema = z.object({
x: z.number(),
y: z.number(),
z: z.number(),
});
export const RelativePositionSchema = z.object({
dx: z.number(),
dy: z.number(),
dz: z.number(),
});
// Tool input schemas
export const ConnectSchema = z.object({
host: z.string(),
port: z.number().default(25565),
username: z.string(),
});
export const ChatSchema = z.object({
message: z.string(),
});
export const NavigateSchema = z.object({
x: z.number(),
y: z.number(),
z: z.number(),
});
export const NavigateRelativeSchema = z.object({
dx: z.number(),
dy: z.number(),
dz: z.number(),
});
export const DigBlockSchema = z.object({
x: z.number(),
y: z.number(),
z: z.number(),
});
export const DigBlockRelativeSchema = z.object({
dx: z.number(),
dy: z.number(),
dz: z.number(),
});
export const DigAreaSchema = z.object({
start: PositionSchema,
end: PositionSchema,
});
export const DigAreaRelativeSchema = z.object({
start: RelativePositionSchema,
end: RelativePositionSchema,
});
export const PlaceBlockSchema = z.object({
x: z.number(),
y: z.number(),
z: z.number(),
blockName: z.string(),
});
export const FollowPlayerSchema = z.object({
username: z.string(),
distance: z.number().default(2),
});
export const AttackEntitySchema = z.object({
entityName: z.string(),
maxDistance: z.number().default(5),
});
export const InspectBlockSchema = z.object({
position: PositionSchema,
includeState: z.boolean().default(true),
});
export const FindBlocksSchema = z.object({
blockTypes: z.union([
z.string(),
z.array(z.string()),
z.string().transform((str) => {
try {
// Handle string that looks like an array
if (str.startsWith("[") && str.endsWith("]")) {
const parsed = JSON.parse(str.replace(/'/g, '"'));
return Array.isArray(parsed) ? parsed : [str];
}
return [str];
} catch {
return [str];
}
}),
]),
maxDistance: z.number().default(32),
maxCount: z.number().default(1),
constraints: z
.object({
minY: z.number().optional(),
maxY: z.number().optional(),
requireReachable: z.boolean().default(false),
})
.optional(),
});
export const FindEntitiesSchema = z.object({
entityTypes: z.array(z.string()),
maxDistance: z.number().default(32),
maxCount: z.number().default(1),
constraints: z
.object({
mustBeVisible: z.boolean().default(false),
inFrontOnly: z.boolean().default(false),
minHealth: z.number().optional(),
maxHealth: z.number().optional(),
})
.optional(),
});
export const CheckPathSchema = z.object({
destination: PositionSchema,
dryRun: z.boolean().default(true),
includeObstacles: z.boolean().default(false),
});
// Response schemas
export const ToolResponseSchema = z.object({
_meta: z.object({}).optional(),
content: z.array(
z.object({
type: z.string(),
text: z.string(),
})
),
isError: z.boolean().optional(),
});
export type ToolResponse = z.infer<typeof ToolResponseSchema>;
export type Position = z.infer<typeof PositionSchema>;
export type RelativePosition = z.infer<typeof RelativePositionSchema>;
```
--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------
```typescript
import { zodToJsonSchema } from "zod-to-json-schema";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import {
ChatSchema,
NavigateRelativeSchema,
DigBlockRelativeSchema,
DigAreaRelativeSchema,
FollowPlayerSchema,
AttackEntitySchema,
FindBlocksSchema,
FindEntitiesSchema,
} from "../schemas.js";
type InputSchema = {
type: "object";
properties?: Record<string, unknown>;
[k: string]: unknown;
};
const toInputSchema = (schema: z.ZodType): InputSchema => ({
...zodToJsonSchema(schema),
type: "object",
});
const CraftItemSchema = z.object({
itemName: z.string(),
quantity: z.number().optional(),
useCraftingTable: z.boolean().optional(),
});
const SmeltItemSchema = z.object({
itemName: z.string(),
fuelName: z.string(),
quantity: z.number().optional(),
});
const EquipItemSchema = z.object({
itemName: z.string(),
destination: z.enum(["hand", "off-hand", "head", "torso", "legs", "feet"]),
});
const ContainerInteractionSchema = z.object({
containerPosition: z.object({
x: z.number(),
y: z.number(),
z: z.number(),
}),
itemName: z.string(),
quantity: z.number().optional(),
});
export const MINECRAFT_TOOLS: Tool[] = [
{
name: "chat",
description: "Send a chat message to the server",
inputSchema: toInputSchema(ChatSchema),
},
{
name: "navigate_relative",
description:
"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",
inputSchema: toInputSchema(NavigateRelativeSchema),
},
{
name: "dig_block_relative",
description:
"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",
inputSchema: toInputSchema(DigBlockRelativeSchema),
},
{
name: "dig_area_relative",
description:
"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.",
inputSchema: toInputSchema(DigAreaRelativeSchema),
},
{
name: "place_block",
description:
"Place a block from the bot's inventory at the specified position. Use this for building structures.",
inputSchema: toInputSchema(
z.object({
x: z.number(),
y: z.number(),
z: z.number(),
blockName: z.string(),
})
),
},
{
name: "find_blocks",
description:
"Find nearby blocks of specific types. Use this to locate building materials or identify terrain.",
inputSchema: toInputSchema(FindBlocksSchema),
},
{
name: "craft_item",
description:
"Craft items using materials in inventory. Can use a crafting table if specified.",
inputSchema: toInputSchema(CraftItemSchema),
},
{
name: "inspect_inventory",
description:
"Check the contents of the bot's inventory to see available materials.",
inputSchema: toInputSchema(
z.object({
itemType: z.string().optional(),
includeEquipment: z.boolean().optional(),
})
),
},
{
name: "follow_player",
description: "Make the bot follow a specific player",
inputSchema: toInputSchema(FollowPlayerSchema),
},
{
name: "attack_entity",
description: "Attack a specific entity near the bot",
inputSchema: toInputSchema(AttackEntitySchema),
},
];
```
--------------------------------------------------------------------------------
/src/types/minecraft.ts:
--------------------------------------------------------------------------------
```typescript
import type { Entity as PrismarineEntity } from "prismarine-entity";
import type { Block as PrismarineBlock } from "prismarine-block";
import type { Item as PrismarineItem } from "prismarine-item";
import { Vec3 } from "vec3";
export interface Position {
x: number;
y: number;
z: number;
}
export interface Block {
position: Vec3;
type: number;
name: string;
hardness: number;
}
export interface Entity {
name: string;
type: string;
position: Vec3;
velocity: Vec3;
health: number;
}
export interface InventoryItem {
name: string;
count: number;
slot: number;
}
export interface Player {
username: string;
uuid: string;
ping: number;
}
export interface HealthStatus {
health: number;
food: number;
saturation: number;
armor: number;
}
export interface Weather {
isRaining: boolean;
rainState: "clear" | "raining";
thunderState: number;
}
export interface Recipe {
name: string;
ingredients: { [itemName: string]: number };
requiresCraftingTable: boolean;
}
export interface Container {
type: "chest" | "furnace" | "crafting_table";
position: Position;
slots: { [slot: number]: InventoryItem | null };
}
/**
* Core interface for the bot. Each method is a single action
* that an LLM agent can call in multiple steps.
*/
export interface MinecraftBot {
// ---- Connection ----
connect(host: string, port: number, username: string): Promise<void>;
disconnect(): void;
// ---- Chat ----
chat(message: string): void;
// ---- State & Info ----
getPosition(): Position | null;
getHealth(): number;
getInventory(): InventoryItem[];
getPlayers(): Player[];
getBlocksNearby(maxDistance?: number, count?: number): Block[];
getEntitiesNearby(maxDistance?: number): Entity[];
getHealthStatus(): HealthStatus;
getWeather(): Weather;
// ---- Relative Movement & Actions ----
navigateRelative(
dx: number,
dy: number,
dz: number,
progressCallback?: (progress: number) => void
): Promise<void>;
navigateTo(x: number, y: number, z: number): Promise<void>;
digBlockRelative(dx: number, dy: number, dz: number): Promise<void>;
digAreaRelative(
start: { dx: number; dy: number; dz: number },
end: { dx: number; dy: number; dz: number },
progressCallback?: (
progress: number,
blocksDug: number,
totalBlocks: number
) => void
): Promise<void>;
placeBlock(x: number, y: number, z: number, blockName: string): Promise<void>;
// ---- Entity Interaction ----
followPlayer(username: string, distance?: number): Promise<void>;
attackEntity(entityName: string, maxDistance?: number): Promise<void>;
// ---- Block & Pathfinding Info ----
blockAt(position: Vec3): Block | null;
findBlocks(options: {
matching: (block: Block) => boolean;
maxDistance: number;
count: number;
point?: Vec3;
}): Vec3[];
getEquipmentDestSlot(destination: string): number;
canSeeEntity(entity: Entity): boolean;
// ---- Crafting & Item Management ----
craftItem(
itemName: string,
quantity?: number,
useCraftingTable?: boolean
): Promise<void>;
smeltItem(
itemName: string,
fuelName: string,
quantity?: number
): Promise<void>;
equipItem(
itemName: string,
destination: "hand" | "off-hand" | "head" | "torso" | "legs" | "feet"
): Promise<void>;
depositItem(
containerPosition: Position,
itemName: string,
quantity?: number
): Promise<void>;
withdrawItem(
containerPosition: Position,
itemName: string,
quantity?: number
): Promise<void>;
// ---- Expose underlying info for reference ----
readonly entity: {
position: Vec3;
velocity: Vec3;
yaw: number;
pitch: number;
};
readonly entities: { [id: string]: Entity };
readonly inventory: {
items: () => InventoryItem[];
slots: { [slot: string]: InventoryItem | null };
};
readonly pathfinder: any;
}
// Utility classes for type conversion between prismarine-xxx and your interfaces
export class TypeConverters {
static entity(entity: PrismarineEntity): Entity {
return {
name: entity.name || "unknown",
type: entity.type || "unknown",
position: entity.position,
velocity: entity.velocity,
health: entity.health || 0,
};
}
static block(block: PrismarineBlock): Block {
return {
position: block.position,
type: block.type,
name: block.name,
hardness: block.hardness || 0,
};
}
static item(item: PrismarineItem): InventoryItem {
return {
name: item.name,
count: item.count,
slot: item.slot,
};
}
}
export type { ToolResponse } from "./tools";
```
--------------------------------------------------------------------------------
/src/__tests__/mocks/mockBot.ts:
--------------------------------------------------------------------------------
```typescript
import { EventEmitter } from "events";
import type { MinecraftBot } from "../../types/minecraft";
import type {
Player,
InventoryItem,
Entity,
Block,
HealthStatus,
Weather,
Position,
Recipe,
Container,
} from "../../types/minecraft";
import { Vec3 } from "vec3";
import { goals, Movements } from "mineflayer-pathfinder";
interface ConnectionParams {
host: string;
port: number;
username: string;
}
export class MockMinecraftBot extends EventEmitter implements MinecraftBot {
private position = { x: 0, y: 64, z: 0 };
private isConnected = true;
private _inventory: { items: InventoryItem[] } = { items: [] };
private connectCount = 0;
private _blocks: { [key: string]: string } = {};
get entity() {
if (!this.isConnected) throw new Error("Not connected");
return {
position: new Vec3(this.position.x, this.position.y, this.position.z),
velocity: new Vec3(0, 0, 0),
yaw: 0,
pitch: 0,
};
}
get entities() {
if (!this.isConnected) throw new Error("Not connected");
return {};
}
get inventory() {
if (!this.isConnected) throw new Error("Not connected");
return {
items: () => this._inventory.items,
slots: {},
};
}
get pathfinder() {
if (!this.isConnected) throw new Error("Not connected");
return {
setMovements: () => {},
goto: () => Promise.resolve(),
getPathTo: () => Promise.resolve(null),
};
}
constructor(private connectionParams: ConnectionParams) {
super();
setTimeout(() => {
this.emit("spawn");
}, 0);
}
async connect(host: string, port: number, username: string): Promise<void> {
this.isConnected = true;
this.connectCount++;
setTimeout(() => {
this.emit("spawn");
}, 10);
return Promise.resolve();
}
disconnect(): void {
if (this.isConnected) {
this.isConnected = false;
this.emit("end");
}
}
chat(message: string): void {
if (!this.isConnected) throw new Error("Not connected");
}
getPosition() {
if (!this.isConnected) throw new Error("Not connected");
return { ...this.position };
}
getHealth() {
if (!this.isConnected) throw new Error("Not connected");
return 20;
}
getHealthStatus() {
if (!this.isConnected) throw new Error("Not connected");
return {
health: 20,
food: 20,
saturation: 5,
armor: 0,
};
}
getWeather(): Weather {
if (!this.isConnected) throw new Error("Not connected");
return {
isRaining: false,
rainState: "clear",
thunderState: 0,
};
}
getInventory() {
if (!this.isConnected) throw new Error("Not connected");
return this._inventory.items;
}
getPlayers() {
if (!this.isConnected) throw new Error("Not connected");
return [];
}
async navigateTo(x: number, y: number, z: number) {
if (!this.isConnected) throw new Error("Not connected");
this.position = { x, y, z };
}
async navigateRelative(dx: number, dy: number, dz: number) {
if (!this.isConnected) throw new Error("Not connected");
this.position = {
x: this.position.x + dx,
y: this.position.y + dy,
z: this.position.z + dz,
};
}
async digBlock(x: number, y: number, z: number) {
if (!this.isConnected) throw new Error("Not connected");
}
async digArea(start: any, end: any) {
if (!this.isConnected) throw new Error("Not connected");
}
async placeBlock(
x: number,
y: number,
z: number,
blockName: string
): Promise<void> {
if (!this.isConnected) throw new Error("Not connected");
this._blocks[`${x},${y},${z}`] = blockName;
}
async followPlayer(username: string, distance: number) {
if (!this.isConnected) throw new Error("Not connected");
}
async attackEntity(entityName: string, maxDistance: number) {
if (!this.isConnected) throw new Error("Not connected");
}
getEntitiesNearby(maxDistance?: number): Entity[] {
if (!this.isConnected) throw new Error("Not connected");
return [];
}
getBlocksNearby(maxDistance?: number, count?: number): Block[] {
if (!this.isConnected) throw new Error("Not connected");
return [];
}
async digBlockRelative(dx: number, dy: number, dz: number): Promise<void> {
if (!this.isConnected) throw new Error("Not connected");
}
async digAreaRelative(
start: { dx: number; dy: number; dz: number },
end: { dx: number; dy: number; dz: number },
progressCallback?: (
progress: number,
blocksDug: number,
totalBlocks: number
) => void
): Promise<void> {
if (!this.isConnected) throw new Error("Not connected");
if (progressCallback) {
progressCallback(100, 1, 1);
}
}
blockAt(position: Vec3): Block | null {
if (!this.isConnected) throw new Error("Not connected");
return null;
}
findBlocks(options: {
matching: ((block: Block) => boolean) | string | string[];
maxDistance: number;
count: number;
point?: Vec3;
}): Vec3[] {
if (!this.isConnected) throw new Error("Not connected");
return [];
}
getEquipmentDestSlot(destination: string): number {
if (!this.isConnected) throw new Error("Not connected");
return 0;
}
canSeeEntity(entity: Entity): boolean {
if (!this.isConnected) throw new Error("Not connected");
return false;
}
async craftItem(
itemName: string,
quantity?: number,
useCraftingTable?: boolean
): Promise<void> {
if (!this.isConnected) throw new Error("Not connected");
}
async equipItem(itemName: string, destination: string): Promise<void> {
if (!this.isConnected) throw new Error("Not connected");
}
async dropItem(itemName: string, quantity?: number): Promise<void> {
if (!this.isConnected) throw new Error("Not connected");
}
async openContainer(position: Position): Promise<Container> {
if (!this.isConnected) throw new Error("Not connected");
return {
type: "chest",
position,
slots: {},
};
}
closeContainer(): void {
if (!this.isConnected) throw new Error("Not connected");
}
getRecipe(itemName: string): Recipe | null {
if (!this.isConnected) throw new Error("Not connected");
return null;
}
listAvailableRecipes(): Recipe[] {
if (!this.isConnected) throw new Error("Not connected");
return [];
}
async smeltItem(
itemName: string,
fuelName: string,
quantity?: number
): Promise<void> {
if (!this.isConnected) throw new Error("Not connected");
}
async depositItem(
containerPosition: Position,
itemName: string,
quantity?: number
): Promise<void> {
if (!this.isConnected) throw new Error("Not connected");
}
async withdrawItem(
containerPosition: Position,
itemName: string,
quantity?: number
): Promise<void> {
if (!this.isConnected) throw new Error("Not connected");
}
canCraft(recipe: Recipe): boolean {
if (!this.isConnected) throw new Error("Not connected");
return false;
}
getConnectCount(): number {
return this.connectCount;
}
}
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { createBot } from "mineflayer";
import type { Bot } from "mineflayer";
import { pathfinder, goals, Movements } from "mineflayer-pathfinder";
import type { Pathfinder } from "mineflayer-pathfinder";
import { Vec3 } from "vec3";
import { MinecraftToolHandler } from "./handlers/tools.js";
import { MINECRAFT_TOOLS } from "./tools/index.js";
import * as schemas from "./schemas.js";
import { cliSchema } from "./cli.js";
import type { MinecraftBot } from "./types/minecraft.js";
import { MinecraftResourceHandler } from "./handlers/resources.js";
import type { ResourceHandler } from "./handlers/resources.js";
import type { ResourceResponse } from "./handlers/resources.js";
const MINECRAFT_RESOURCES = [
{
name: "players",
uri: "minecraft://players",
description:
"List of players currently on the server, including their usernames and connection info",
mimeType: "application/json",
},
{
name: "position",
uri: "minecraft://position",
description:
"Current position of the bot in the world (x, y, z coordinates)",
mimeType: "application/json",
},
{
name: "blocks/nearby",
uri: "minecraft://blocks/nearby",
description:
"List of blocks in the bot's vicinity, including their positions and types",
mimeType: "application/json",
},
{
name: "entities/nearby",
uri: "minecraft://entities/nearby",
description:
"List of entities (players, mobs, items) near the bot, including their positions and types",
mimeType: "application/json",
},
{
name: "inventory",
uri: "minecraft://inventory",
description:
"Current contents of the bot's inventory, including item names, counts, and slots",
mimeType: "application/json",
},
{
name: "health",
uri: "minecraft://health",
description: "Bot's current health, food, saturation, and armor status",
mimeType: "application/json",
},
{
name: "weather",
uri: "minecraft://weather",
description:
"Current weather conditions in the game (clear, raining, thundering)",
mimeType: "application/json",
},
];
interface ExtendedBot extends Bot {
pathfinder: Pathfinder & {
setMovements(movements: Movements): void;
goto(goal: goals.Goal): Promise<void>;
};
}
export class MinecraftServer {
private server: Server;
private bot: ExtendedBot | null = null;
private toolHandler!: MinecraftToolHandler;
private resourceHandler!: MinecraftResourceHandler;
private connectionParams: z.infer<typeof cliSchema>;
private isConnected: boolean = false;
private reconnectAttempts: number = 0;
private readonly maxReconnectAttempts: number = 3;
private readonly reconnectDelay: number = 5000; // 5 seconds
constructor(connectionParams: z.infer<typeof cliSchema>) {
this.connectionParams = connectionParams;
this.server = new Server(
{
name: "mineflayer-mcp-server",
version: "0.1.0",
},
{
capabilities: {
tools: {
enabled: true,
},
resources: {
enabled: true,
},
},
}
);
this.setupHandlers();
}
private sendJsonRpcNotification(method: string, params: any) {
this.server
.notification({
method,
params: JSON.parse(JSON.stringify(params)),
})
.catch((error) => {
console.error("Failed to send notification:", error);
});
}
private async connectBot(): Promise<void> {
if (this.bot) {
this.bot.end();
this.bot = null;
}
const bot = createBot({
host: this.connectionParams.host,
port: this.connectionParams.port,
username: this.connectionParams.username,
hideErrors: false,
}) as ExtendedBot;
bot.loadPlugin(pathfinder);
this.bot = bot;
// Create a wrapper that implements MinecraftBot interface
const wrapper: MinecraftBot = {
chat: (message: string) => bot.chat(message),
disconnect: () => bot.end(),
getPosition: () => {
const pos = bot.entity?.position;
return pos ? { x: pos.x, y: pos.y, z: pos.z } : null;
},
getHealth: () => bot.health,
getInventory: () =>
bot.inventory.items().map((item) => ({
name: item.name,
count: item.count,
slot: item.slot,
})),
getPlayers: () =>
Object.values(bot.players).map((player) => ({
username: player.username,
uuid: player.uuid,
ping: player.ping,
})),
navigateRelative: async (
dx: number,
dy: number,
dz: number,
progressCallback?: (progress: number) => void
) => {
const pos = bot.entity.position;
const yaw = bot.entity.yaw;
const sin = Math.sin(yaw);
const cos = Math.cos(yaw);
const worldDx = dx * cos - dz * sin;
const worldDz = dx * sin + dz * cos;
const goal = new goals.GoalNear(
pos.x + worldDx,
pos.y + dy,
pos.z + worldDz,
1
);
const startPos = bot.entity.position;
const targetPos = new Vec3(
pos.x + worldDx,
pos.y + dy,
pos.z + worldDz
);
const totalDistance = startPos.distanceTo(targetPos);
// Set up progress monitoring
const progressToken = Date.now().toString();
const checkProgress = () => {
if (!bot) return;
const currentPos = bot.entity.position;
const remainingDistance = currentPos.distanceTo(targetPos);
const progress = Math.min(
100,
((totalDistance - remainingDistance) / totalDistance) * 100
);
if (progressCallback) {
progressCallback(progress);
}
this.sendJsonRpcNotification("tool/progress", {
token: progressToken,
progress,
status: progress < 100 ? "in_progress" : "complete",
message: `Navigation progress: ${Math.round(progress)}%`,
});
};
const progressInterval = setInterval(checkProgress, 500);
try {
await bot.pathfinder.goto(goal);
} finally {
clearInterval(progressInterval);
// Send final progress
if (progressCallback) {
progressCallback(100);
}
this.sendJsonRpcNotification("tool/progress", {
token: progressToken,
progress: 100,
status: "complete",
message: "Navigation complete",
});
}
},
digBlockRelative: async (dx: number, dy: number, dz: number) => {
const pos = bot.entity.position;
const yaw = bot.entity.yaw;
const sin = Math.sin(yaw);
const cos = Math.cos(yaw);
const worldDx = dx * cos - dz * sin;
const worldDz = dx * sin + dz * cos;
const block = bot.blockAt(
new Vec3(
Math.floor(pos.x + worldDx),
Math.floor(pos.y + dy),
Math.floor(pos.z + worldDz)
)
);
if (!block) throw new Error("No block at relative position");
await bot.dig(block);
},
digAreaRelative: async (start, end, progressCallback) => {
const pos = bot.entity.position;
const yaw = bot.entity.yaw;
const sin = Math.sin(yaw);
const cos = Math.cos(yaw);
const transformPoint = (dx: number, dy: number, dz: number) => ({
x: Math.floor(pos.x + dx * cos - dz * sin),
y: Math.floor(pos.y + dy),
z: Math.floor(pos.z + dx * sin + dz * cos),
});
const absStart = transformPoint(start.dx, start.dy, start.dz);
const absEnd = transformPoint(end.dx, end.dy, end.dz);
const minX = Math.min(absStart.x, absEnd.x);
const maxX = Math.max(absStart.x, absEnd.x);
const minY = Math.min(absStart.y, absEnd.y);
const maxY = Math.max(absStart.y, absEnd.y);
const minZ = Math.min(absStart.z, absEnd.z);
const maxZ = Math.max(absStart.z, absEnd.z);
const totalBlocks =
(maxX - minX + 1) * (maxY - minY + 1) * (maxZ - minZ + 1);
let blocksDug = 0;
for (let y = maxY; y >= minY; y--) {
for (let x = minX; x <= maxX; x++) {
for (let z = minZ; z <= maxZ; z++) {
const block = bot.blockAt(new Vec3(x, y, z));
if (block && block.name !== "air") {
await bot.dig(block);
blocksDug++;
if (progressCallback) {
progressCallback(
(blocksDug / totalBlocks) * 100,
blocksDug,
totalBlocks
);
}
}
}
}
}
},
getBlocksNearby: () => {
const pos = bot.entity.position;
const radius = 4;
const blocks = [];
for (let x = -radius; x <= radius; x++) {
for (let y = -radius; y <= radius; y++) {
for (let z = -radius; z <= radius; z++) {
const block = bot.blockAt(
new Vec3(
Math.floor(pos.x + x),
Math.floor(pos.y + y),
Math.floor(pos.z + z)
)
);
if (block && block.name !== "air") {
blocks.push({
name: block.name,
position: {
x: Math.floor(pos.x + x),
y: Math.floor(pos.y + y),
z: Math.floor(pos.z + z),
},
});
}
}
}
}
return blocks;
},
getEntitiesNearby: () => {
return Object.values(bot.entities)
.filter((e) => e !== bot.entity && e.position)
.map((e) => ({
name: e.name || "unknown",
type: e.type,
position: {
x: e.position.x,
y: e.position.y,
z: e.position.z,
},
velocity: e.velocity,
health: e.health,
}));
},
getWeather: () => ({
isRaining: bot.isRaining,
rainState: bot.isRaining ? "raining" : "clear",
thunderState: bot.thunderState,
}),
} as MinecraftBot;
this.toolHandler = new MinecraftToolHandler(wrapper);
this.resourceHandler = new MinecraftResourceHandler(wrapper);
return new Promise((resolve, reject) => {
if (!this.bot) return reject(new Error("Bot not initialized"));
this.bot.once("spawn", () => {
this.isConnected = true;
this.reconnectAttempts = 0;
resolve();
});
this.bot.on("end", async () => {
this.isConnected = false;
try {
await this.server.notification({
method: "server/status",
params: {
type: "connection",
status: "disconnected",
host: this.connectionParams.host,
port: this.connectionParams.port,
},
});
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
await new Promise((resolve) =>
setTimeout(resolve, this.reconnectDelay)
);
await this.connectBot();
}
} catch (error) {
console.error("Failed to handle disconnection:", error);
}
});
this.bot.on("error", async (error) => {
try {
await this.server.notification({
method: "server/status",
params: {
type: "error",
error: error instanceof Error ? error.message : String(error),
},
});
} catch (notificationError) {
console.error(
"Failed to send error notification:",
notificationError
);
}
});
});
}
private setupHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: MINECRAFT_TOOLS,
}));
this.server.setRequestHandler(
ReadResourceRequestSchema,
async (request) => {
try {
if (!this.bot || !this.isConnected) {
throw new Error("Bot is not connected");
}
const { uri } = request.params;
let result: ResourceResponse;
switch (uri) {
case "minecraft://players":
result = await this.resourceHandler.handleGetPlayers(uri);
break;
case "minecraft://position":
result = await this.resourceHandler.handleGetPosition(uri);
break;
case "minecraft://blocks/nearby":
result = await this.resourceHandler.handleGetBlocksNearby(uri);
break;
case "minecraft://entities/nearby":
result = await this.resourceHandler.handleGetEntitiesNearby(uri);
break;
case "minecraft://inventory":
result = await this.resourceHandler.handleGetInventory(uri);
break;
case "minecraft://health":
result = await this.resourceHandler.handleGetHealth(uri);
break;
case "minecraft://weather":
result = await this.resourceHandler.handleGetWeather(uri);
break;
default:
throw new Error(`Resource not found: ${uri}`);
}
return {
contents: result.contents.map((content) => ({
uri: content.uri,
mimeType: content.mimeType || "application/json",
text:
typeof content.text === "string"
? content.text
: JSON.stringify(content.text),
})),
};
} catch (error) {
throw {
code: -32603,
message: error instanceof Error ? error.message : String(error),
};
}
}
);
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
if (!request.params.arguments) {
throw new Error("Arguments are required");
}
if (!this.bot || !this.isConnected) {
throw new Error("Bot is not connected");
}
let result;
switch (request.params.name) {
case "chat": {
const args = schemas.ChatSchema.parse(request.params.arguments);
result = await this.toolHandler.handleChat(args.message);
break;
}
case "navigate_relative": {
const args = schemas.NavigateRelativeSchema.parse(
request.params.arguments
);
result = await this.toolHandler.handleNavigateRelative(
args.dx,
args.dy,
args.dz
);
break;
}
case "dig_block_relative": {
const args = schemas.DigBlockRelativeSchema.parse(
request.params.arguments
);
result = await this.toolHandler.handleDigBlockRelative(
args.dx,
args.dy,
args.dz
);
break;
}
case "dig_area_relative": {
const args = schemas.DigAreaRelativeSchema.parse(
request.params.arguments
);
result = await this.toolHandler.handleDigAreaRelative(
args.start,
args.end
);
break;
}
default:
throw {
code: -32601,
message: `Unknown tool: ${request.params.name}`,
};
}
return {
content: result?.content || [{ type: "text", text: "Success" }],
_meta: result?._meta,
};
} catch (error) {
if (error instanceof z.ZodError) {
throw {
code: -32602,
message: "Invalid params",
data: {
errors: error.errors.map((e) => ({
path: e.path.join("."),
message: e.message,
})),
},
};
}
throw {
code: -32603,
message: error instanceof Error ? error.message : String(error),
};
}
});
}
async start(): Promise<void> {
try {
// Start MCP server first
const transport = new StdioServerTransport();
await this.server.connect(transport);
// Send startup status
await this.server.notification({
method: "server/status",
params: {
type: "startup",
status: "running",
transport: "stdio",
},
});
// Then connect bot
await this.connectBot();
// Keep process alive and handle termination
process.stdin.resume();
process.on("SIGINT", () => {
this.bot?.end();
process.exit(0);
});
process.on("SIGTERM", () => {
this.bot?.end();
process.exit(0);
});
} catch (error) {
throw {
code: -32000,
message: "Server startup failed",
data: {
error: error instanceof Error ? error.message : String(error),
},
};
}
}
}
```
--------------------------------------------------------------------------------
/src/handlers/tools.ts:
--------------------------------------------------------------------------------
```typescript
import type { MinecraftBot } from "../types/minecraft";
import { Vec3 } from "vec3";
import { goals } from "mineflayer-pathfinder";
import type { ToolResponse } from "../types/tools";
import type { Position } from "../types/minecraft";
export interface ToolHandler {
handleChat(message: string): Promise<ToolResponse>;
handleNavigateTo(x: number, y: number, z: number): Promise<ToolResponse>;
handleNavigateRelative(
dx: number,
dy: number,
dz: number
): Promise<ToolResponse>;
handleDigBlock(x: number, y: number, z: number): Promise<ToolResponse>;
handleDigBlockRelative(
dx: number,
dy: number,
dz: number
): Promise<ToolResponse>;
handleDigArea(
start: { x: number; y: number; z: number },
end: { x: number; y: number; z: number }
): Promise<ToolResponse>;
handleDigAreaRelative(
start: { dx: number; dy: number; dz: number },
end: { dx: number; dy: number; dz: number }
): Promise<ToolResponse>;
handlePlaceBlock(
x: number,
y: number,
z: number,
blockName: string
): Promise<ToolResponse>;
handleFollowPlayer(username: string, distance: number): Promise<ToolResponse>;
handleAttackEntity(
entityName: string,
maxDistance: number
): Promise<ToolResponse>;
handleInspectBlock(
position: { x: number; y: number; z: number },
includeState: boolean
): Promise<ToolResponse>;
handleFindBlocks(
blockTypes: string | string[],
maxDistance: number,
maxCount: number,
constraints?: {
minY?: number;
maxY?: number;
requireReachable?: boolean;
}
): Promise<ToolResponse>;
handleFindEntities(
entityTypes: string[],
maxDistance: number,
maxCount: number,
constraints?: {
mustBeVisible?: boolean;
inFrontOnly?: boolean;
minHealth?: number;
maxHealth?: number;
}
): Promise<ToolResponse>;
handleCheckPath(
destination: { x: number; y: number; z: number },
dryRun: boolean,
includeObstacles: boolean
): Promise<ToolResponse>;
handleInspectInventory(
itemType?: string,
includeEquipment?: boolean
): Promise<ToolResponse>;
handleCraftItem(
itemName: string,
quantity?: number,
useCraftingTable?: boolean
): Promise<ToolResponse>;
handleSmeltItem(
itemName: string,
fuelName: string,
quantity?: number
): Promise<ToolResponse>;
handleEquipItem(
itemName: string,
destination: "hand" | "off-hand" | "head" | "torso" | "legs" | "feet"
): Promise<ToolResponse>;
handleDepositItem(
containerPosition: Position,
itemName: string,
quantity?: number
): Promise<ToolResponse>;
handleWithdrawItem(
containerPosition: Position,
itemName: string,
quantity?: number
): Promise<ToolResponse>;
}
export class MinecraftToolHandler implements ToolHandler {
constructor(private bot: MinecraftBot) {}
private wrapError(error: unknown): ToolResponse {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
_meta: {},
isError: true,
content: [
{
type: "text",
text: `Error: ${errorMessage}`,
},
],
};
}
async handleChat(message: string): Promise<ToolResponse> {
this.bot.chat(message);
return {
_meta: {},
content: [
{
type: "text",
text: `Sent message: ${message}`,
},
],
};
}
async handleNavigateTo(
x: number,
y: number,
z: number
): Promise<ToolResponse> {
const progressToken = Date.now().toString();
const pos = this.bot.getPosition();
if (!pos) throw new Error("Bot position unknown");
await this.bot.navigateRelative(
x - pos.x,
y - pos.y,
z - pos.z,
(progress) => {
if (progress < 0 || progress > 100) return;
}
);
return {
_meta: {
progressToken,
},
content: [
{
type: "text",
text: `Navigated to ${x}, ${y}, ${z}`,
},
],
};
}
async handleNavigateRelative(
dx: number,
dy: number,
dz: number
): Promise<ToolResponse> {
const progressToken = Date.now().toString();
await this.bot.navigateRelative(dx, dy, dz, (progress) => {
if (progress < 0 || progress > 100) return;
});
return {
_meta: {
progressToken,
},
content: [
{
type: "text",
text: `Navigated relative to current position: ${dx} blocks right/left, ${dy} blocks up/down, ${dz} blocks forward/back`,
},
],
};
}
async handleDigBlock(x: number, y: number, z: number): Promise<ToolResponse> {
const pos = this.bot.getPosition();
if (!pos) throw new Error("Bot position unknown");
await this.bot.digBlockRelative(x - pos.x, y - pos.y, z - pos.z);
return {
content: [
{
type: "text",
text: `Dug block at ${x}, ${y}, ${z}`,
},
],
};
}
async handleDigBlockRelative(
dx: number,
dy: number,
dz: number
): Promise<ToolResponse> {
await this.bot.digBlockRelative(dx, dy, dz);
return {
_meta: {},
content: [
{
type: "text",
text: `Dug block relative to current position: ${dx} blocks right/left, ${dy} blocks up/down, ${dz} blocks forward/back`,
},
],
};
}
async handleDigArea(
start: Position,
end: Position,
progressCallback?: (
progress: number,
blocksDug: number,
totalBlocks: number
) => void
): Promise<ToolResponse> {
const pos = this.bot.getPosition();
if (!pos) throw new Error("Bot position unknown");
await this.bot.digAreaRelative(
{
dx: start.x - pos.x,
dy: start.y - pos.y,
dz: start.z - pos.z,
},
{
dx: end.x - pos.x,
dy: end.y - pos.y,
dz: end.z - pos.z,
},
progressCallback
);
return {
content: [
{
type: "text",
text: `Dug area from (${start.x}, ${start.y}, ${start.z}) to (${end.x}, ${end.y}, ${end.z})`,
},
],
};
}
async handleDigAreaRelative(
start: { dx: number; dy: number; dz: number },
end: { dx: number; dy: number; dz: number }
): Promise<ToolResponse> {
let progress = 0;
let blocksDug = 0;
let totalBlocks = 0;
try {
await this.bot.digAreaRelative(
start,
end,
(currentProgress, currentBlocksDug, currentTotalBlocks) => {
progress = currentProgress;
blocksDug = currentBlocksDug;
totalBlocks = currentTotalBlocks;
}
);
return {
_meta: {},
content: [
{
type: "text",
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.`,
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const progressMessage =
totalBlocks > 0
? `Progress before error: ${progress}% (${blocksDug}/${totalBlocks} blocks)`
: "";
return {
_meta: {},
content: [
{
type: "text",
text: `Failed to dig relative area: ${errorMessage}${
progressMessage ? `\n${progressMessage}` : ""
}`,
},
],
isError: true,
};
}
}
async handlePlaceBlock(
x: number,
y: number,
z: number,
blockName: string
): Promise<ToolResponse> {
await this.bot.placeBlock(x, y, z, blockName);
return {
_meta: {},
content: [
{
type: "text",
text: `Placed ${blockName} at ${x}, ${y}, ${z}`,
},
],
};
}
async handleFollowPlayer(
username: string,
distance: number
): Promise<ToolResponse> {
await this.bot.followPlayer(username, distance);
return {
_meta: {},
content: [
{
type: "text",
text: `Following player ${username}${
distance ? ` at distance ${distance}` : ""
}`,
},
],
};
}
async handleAttackEntity(
entityName: string,
maxDistance: number
): Promise<ToolResponse> {
await this.bot.attackEntity(entityName, maxDistance);
return {
_meta: {},
content: [
{
type: "text",
text: `Attacked ${entityName}`,
},
],
};
}
async handleInspectBlock(
position: { x: number; y: number; z: number },
includeState: boolean
): Promise<ToolResponse> {
const block = this.bot.blockAt(
new Vec3(position.x, position.y, position.z)
);
if (!block) {
return {
content: [
{ type: "text", text: "No block found at specified position" },
],
isError: true,
};
}
const blockInfo: any = {
name: block.name,
type: block.type,
position: position,
};
if (includeState && "metadata" in block) {
blockInfo.metadata = block.metadata;
blockInfo.stateId = (block as any).stateId;
blockInfo.light = (block as any).light;
blockInfo.skyLight = (block as any).skyLight;
blockInfo.boundingBox = (block as any).boundingBox;
}
return {
content: [
{
type: "text",
text: `Block at (${position.x}, ${position.y}, ${position.z}):`,
},
{
type: "json",
text: JSON.stringify(blockInfo, null, 2),
},
],
};
}
async handleFindBlocks(
blockTypes: string | string[],
maxDistance: number,
maxCount: number,
constraints?: {
minY?: number;
maxY?: number;
requireReachable?: boolean;
}
): Promise<ToolResponse> {
if (!this.bot) throw new Error("Not connected");
const blockTypesArray = Array.isArray(blockTypes)
? blockTypes
: [blockTypes];
const matches = this.bot.findBlocks({
matching: (block) => blockTypesArray.includes(block.name),
maxDistance,
count: maxCount,
point: this.bot.entity.position,
});
// Apply additional constraints
let filteredMatches = matches;
if (constraints) {
filteredMatches = matches.filter((pos) => {
if (constraints.minY !== undefined && pos.y < constraints.minY)
return false;
if (constraints.maxY !== undefined && pos.y > constraints.maxY)
return false;
if (constraints.requireReachable) {
// Check if we can actually reach this block
const goal = new goals.GoalGetToBlock(pos.x, pos.y, pos.z);
const result = this.bot.pathfinder.getPathTo(goal, maxDistance);
if (!result?.path?.length) return false;
}
return true;
});
}
const blocks = filteredMatches.map((pos) => {
const block = this.bot!.blockAt(pos);
return {
position: { x: pos.x, y: pos.y, z: pos.z },
name: block?.name || "unknown",
distance: pos.distanceTo(this.bot!.entity.position),
};
});
// Sort blocks by distance for better readability
blocks.sort((a, b) => a.distance - b.distance);
const summary = `Found ${
blocks.length
} matching blocks of types: ${blockTypesArray.join(", ")}`;
const details = blocks
.map(
(block) =>
`- ${block.name} at (${block.position.x}, ${block.position.y}, ${
block.position.z
}), ${block.distance.toFixed(1)} blocks away`
)
.join("\n");
return {
content: [
{
type: "text",
text: summary + (blocks.length > 0 ? "\n" + details : ""),
},
],
};
}
async handleFindEntities(
entityTypes: string[],
maxDistance: number,
maxCount: number,
constraints?: {
mustBeVisible?: boolean;
inFrontOnly?: boolean;
minHealth?: number;
maxHealth?: number;
}
): Promise<ToolResponse> {
if (!this.bot) throw new Error("Not connected");
let entities = Object.values(this.bot.entities)
.filter((entity) => {
if (!entity || !entity.position) return false;
if (!entityTypes.includes(entity.name || "")) return false;
const distance = entity.position.distanceTo(this.bot!.entity.position);
if (distance > maxDistance) return false;
if (constraints) {
if (
constraints.minHealth !== undefined &&
(entity.health || 0) < constraints.minHealth
)
return false;
if (
constraints.maxHealth !== undefined &&
(entity.health || 0) > constraints.maxHealth
)
return false;
if (constraints.mustBeVisible && !this.bot!.canSeeEntity(entity))
return false;
if (constraints.inFrontOnly) {
// Check if entity is in front of the bot using dot product
const botDir = this.bot!.entity.velocity;
const toEntity = entity.position.minus(this.bot!.entity.position);
const dot = botDir.dot(toEntity);
if (dot <= 0) return false;
}
}
return true;
})
.slice(0, maxCount)
.map((entity) => ({
name: entity.name || "unknown",
type: entity.type,
position: {
x: entity.position.x,
y: entity.position.y,
z: entity.position.z,
},
velocity: entity.velocity,
health: entity.health,
distance: entity.position.distanceTo(this.bot!.entity.position),
}));
return {
content: [
{
type: "text",
text: `Found ${entities.length} matching entities:`,
},
{
type: "json",
text: JSON.stringify(entities, null, 2),
},
],
};
}
async handleCheckPath(
destination: { x: number; y: number; z: number },
dryRun: boolean,
includeObstacles: boolean
): Promise<ToolResponse> {
if (!this.bot) throw new Error("Not connected");
const goal = new goals.GoalBlock(
destination.x,
destination.y,
destination.z
);
const pathResult = await this.bot.pathfinder.getPathTo(goal);
const response: any = {
pathExists: !!pathResult?.path?.length,
distance: pathResult?.path?.length || 0,
estimatedTime: (pathResult?.path?.length || 0) * 0.25, // Rough estimate: 4 blocks per second
};
if (!pathResult?.path?.length && includeObstacles) {
// Try to find what's blocking the path
const obstacles = [];
const line = this.getPointsOnLine(
this.bot.entity.position,
new Vec3(destination.x, destination.y, destination.z)
);
for (const point of line) {
const block = this.bot.blockAt(point);
if (block && (block as any).boundingBox !== "empty") {
obstacles.push({
position: { x: point.x, y: point.y, z: point.z },
block: block.name,
type: block.type,
});
if (obstacles.length >= 5) break; // Limit to first 5 obstacles
}
}
response.obstacles = obstacles;
}
if (!dryRun && pathResult?.path?.length) {
await this.bot.pathfinder.goto(goal);
response.status = "Reached destination";
}
return {
content: [
{
type: "text",
text: `Path check to (${destination.x}, ${destination.y}, ${destination.z}):`,
},
{
type: "json",
text: JSON.stringify(response, null, 2),
},
],
};
}
private getPointsOnLine(start: Vec3, end: Vec3): Vec3[] {
const points: Vec3[] = [];
const distance = start.distanceTo(end);
const steps = Math.ceil(distance);
for (let i = 0; i <= steps; i++) {
const t = i / steps;
points.push(start.scaled(1 - t).plus(end.scaled(t)));
}
return points;
}
async handleInspectInventory(
itemType?: string,
includeEquipment?: boolean
): Promise<ToolResponse> {
const inventory = this.bot.getInventory();
let items = inventory;
if (itemType) {
items = items.filter((item) => item.name === itemType);
}
const response = {
items,
totalCount: items.reduce((sum, item) => sum + item.count, 0),
uniqueItems: new Set(items.map((item) => item.name)).size,
};
return {
content: [
{
type: "text",
text: `Inventory contents${
itemType ? ` (filtered by ${itemType})` : ""
}:`,
},
{
type: "json",
text: JSON.stringify(response, null, 2),
},
],
};
}
async handleCraftItem(
itemName: string,
quantity?: number,
useCraftingTable?: boolean
): Promise<ToolResponse> {
try {
await this.bot.craftItem(itemName, quantity, useCraftingTable);
return {
content: [
{
type: "text",
text: `Successfully crafted ${quantity || 1}x ${itemName}${
useCraftingTable ? " using crafting table" : ""
}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to craft ${itemName}: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
isError: true,
};
}
}
async handleSmeltItem(
itemName: string,
fuelName: string,
quantity?: number
): Promise<ToolResponse> {
try {
await this.bot.smeltItem(itemName, fuelName, quantity);
return {
content: [
{
type: "text",
text: `Successfully smelted ${
quantity || 1
}x ${itemName} using ${fuelName} as fuel`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to smelt ${itemName}: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
isError: true,
};
}
}
async handleEquipItem(
itemName: string,
destination: "hand" | "off-hand" | "head" | "torso" | "legs" | "feet"
): Promise<ToolResponse> {
try {
await this.bot.equipItem(itemName, destination);
return {
content: [
{
type: "text",
text: `Successfully equipped ${itemName} to ${destination}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to equip ${itemName}: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
isError: true,
};
}
}
async handleDepositItem(
containerPosition: Position,
itemName: string,
quantity?: number
): Promise<ToolResponse> {
try {
await this.bot.depositItem(
containerPosition as Position,
itemName,
quantity
);
return {
content: [
{
type: "text",
text: `Successfully deposited ${
quantity || 1
}x ${itemName} into container at (${containerPosition.x}, ${
containerPosition.y
}, ${containerPosition.z})`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to deposit ${itemName}: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
isError: true,
};
}
}
async handleWithdrawItem(
containerPosition: Position,
itemName: string,
quantity?: number
): Promise<ToolResponse> {
try {
await this.bot.withdrawItem(
containerPosition as Position,
itemName,
quantity
);
return {
content: [
{
type: "text",
text: `Successfully withdrew ${
quantity || 1
}x ${itemName} from container at (${containerPosition.x}, ${
containerPosition.y
}, ${containerPosition.z})`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to withdraw ${itemName}: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
isError: true,
};
}
}
}
```
--------------------------------------------------------------------------------
/src/core/bot.ts:
--------------------------------------------------------------------------------
```typescript
import { createBot } from "mineflayer";
import type { Bot, Furnace } from "mineflayer";
import { Vec3 } from "vec3";
import { pathfinder, Movements, goals } from "mineflayer-pathfinder";
import type { Pathfinder } from "mineflayer-pathfinder";
import type {
Position,
MinecraftBot,
ToolResponse,
Player,
InventoryItem,
Entity as CustomEntity,
Block,
HealthStatus,
Weather,
Recipe,
Container,
} from "../types/minecraft";
import { TypeConverters } from "../types/minecraft";
import { Block as PrismarineBlock } from "prismarine-block";
import { Item } from "prismarine-item";
import { EventEmitter } from "events";
interface PrismarineBlockWithBoundingBox extends PrismarineBlock {
boundingBox: string;
}
type EquipmentDestination =
| "hand"
| "off-hand"
| "head"
| "torso"
| "legs"
| "feet";
interface ExtendedBot extends Bot {
pathfinder: Pathfinder & {
setMovements(movements: Movements): void;
goto(goal: goals.Goal): Promise<void>;
};
}
interface ConnectionParams {
host: string;
port: number;
username: string;
version?: string;
hideErrors?: boolean;
}
export class MineflayerBot extends EventEmitter implements MinecraftBot {
private bot: ExtendedBot | null = null;
private isConnected: boolean = false;
private isConnecting: boolean = false;
private reconnectAttempts: number = 0;
private readonly maxReconnectAttempts: number = 3;
private lastConnectionParams: ConnectionParams;
private movements: Movements | null = null;
constructor(connectionParams: ConnectionParams) {
super();
this.lastConnectionParams = connectionParams;
}
async connect(host: string, port: number, username: string): Promise<void> {
if (this.isConnecting) {
return;
}
this.isConnecting = true;
try {
const params: ConnectionParams = { host, port, username };
this.lastConnectionParams = params;
await this.setupBot();
} finally {
this.isConnecting = false;
}
}
private setupBot(): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
if (this.isConnecting) {
reject(new Error("Already connecting"));
return;
}
this.isConnecting = true;
if (this.bot) {
this.bot.end();
this.bot = null;
}
this.bot = createBot({
...this.lastConnectionParams,
hideErrors: false,
});
this.bot.loadPlugin(pathfinder);
this.bot.on("error", (error: Error) => {
this.logError("Bot error", error);
this.isConnecting = false;
reject(error);
});
this.bot.on("kicked", (reason: string, loggedIn: boolean) => {
this.logError("Bot kicked", { reason, loggedIn });
this.isConnecting = false;
this.handleDisconnect();
});
this.bot.once("spawn", () => {
this.logDebug("Bot spawned successfully");
this.isConnected = true;
this.isConnecting = false;
this.reconnectAttempts = 0;
this.setupMovements();
resolve();
});
this.bot.on("end", (reason: string) => {
this.logError("Bot connection ended", { reason });
this.isConnecting = false;
this.handleDisconnect();
});
} catch (error) {
this.logError("Bot setup error", error);
this.isConnecting = false;
this.sendJSONRPCError(-32001, "Failed to create bot", {
error: error instanceof Error ? error.message : String(error),
});
reject(error);
}
});
}
private setupMovements(): void {
if (!this.bot) return;
try {
this.movements = new Movements(this.bot);
this.movements.allowParkour = true;
this.movements.allowSprinting = true;
this.bot.pathfinder.setMovements(this.movements);
} catch (error) {
this.sendJSONRPCError(-32002, "Error setting up movements", {
error: error instanceof Error ? error.message : String(error),
});
}
}
private handleDisconnect(): void {
this.isConnected = false;
this.movements = null;
// Send a notification that the bot has disconnected
this.sendJsonRpcNotification("bot.disconnected", {
message: "Bot disconnected from server",
});
}
private sendJsonRpcNotification(method: string, params: any) {
process.stdout.write(
JSON.stringify({
jsonrpc: "2.0",
method,
params,
id: null,
}) + "\n"
);
}
private sendJSONRPCError(code: number, message: string, data?: any) {
process.stdout.write(
JSON.stringify({
jsonrpc: "2.0",
id: null,
error: {
code,
message,
data,
},
}) + "\n"
);
}
private logDebug(message: string, data?: any) {
this.sendJsonRpcNotification("bot.debug", { message, data });
}
private logWarning(message: string, data?: any) {
this.sendJsonRpcNotification("bot.warning", { message, data });
}
private logError(message: string, error?: any) {
this.sendJsonRpcNotification("bot.error", {
message,
error: String(error),
});
}
disconnect(): void {
if (this.bot) {
this.bot.end();
this.bot = null;
}
}
chat(message: string): void {
if (!this.bot) {
return this.wrapError("Not connected");
}
this.bot.chat(message);
}
getPosition(): Position | null {
if (!this.bot?.entity?.position) return null;
const pos = this.bot.entity.position;
return { x: pos.x, y: pos.y, z: pos.z };
}
getHealth(): number {
if (!this.bot) {
return this.wrapError("Not connected");
}
return this.bot.health;
}
getInventory(): InventoryItem[] {
if (!this.bot) {
return this.wrapError("Not connected");
}
return this.bot.inventory.items().map(TypeConverters.item);
}
getPlayers(): Player[] {
if (!this.bot) {
return this.wrapError("Not connected");
}
return Object.values(this.bot.players).map((player) => ({
username: player.username,
uuid: player.uuid,
ping: player.ping,
}));
}
async navigateTo(
x: number,
y: number,
z: number,
progressCallback?: (progress: number) => void
): Promise<void> {
if (!this.bot) return this.wrapError("Not connected");
const goal = new goals.GoalNear(x, y, z, 1);
try {
const startPos = this.bot.entity.position;
const targetPos = new Vec3(x, y, z);
const totalDistance = startPos.distanceTo(targetPos);
// Set up progress monitoring
const checkProgress = () => {
if (!this.bot) return;
const currentPos = this.bot.entity.position;
const remainingDistance = currentPos.distanceTo(targetPos);
const progress = Math.min(
100,
((totalDistance - remainingDistance) / totalDistance) * 100
);
progressCallback?.(progress);
};
const progressInterval = setInterval(checkProgress, 500);
try {
await this.bot.pathfinder.goto(goal);
} finally {
clearInterval(progressInterval);
// Send final progress
progressCallback?.(100);
}
} catch (error) {
return this.wrapError(
`Failed to navigate: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
async digBlock(x: number, y: number, z: number): Promise<void> {
if (!this.bot) {
return this.wrapError("Not connected");
}
const targetPos = new Vec3(x, y, z);
// Try to move close enough to dig if needed
try {
const goal = new goals.GoalNear(x, y, z, 3); // Stay within 3 blocks
await this.bot.pathfinder.goto(goal);
} catch (error) {
this.logWarning("Could not move closer to block for digging", error);
// Continue anyway - the block might still be reachable
}
while (true) {
const block = this.bot.blockAt(targetPos);
if (!block) {
// No block at all, so we're done
return;
}
if (block.name === "air") {
// The target is now air, so we're done
return;
}
// Skip bedrock and other indestructible blocks
if (block.hardness < 0) {
this.logWarning(
`Cannot dig indestructible block ${block.name} at ${x}, ${y}, ${z}`
);
return;
}
// Attempt to dig
try {
await this.bot.dig(block);
} catch (err) {
const error = err as Error;
// If it's a known "cannot dig" error, skip
if (
error.message?.includes("cannot be broken") ||
error.message?.includes("cannot dig") ||
error.message?.includes("unreachable")
) {
this.logWarning(
`Failed to dig block ${block.name} at ${x}, ${y}, ${z}: ${error.message}`
);
return;
}
// For other errors, wrap them
return this.wrapError(error.message || String(error));
}
// Small delay to avoid server spam
await new Promise((resolve) => setTimeout(resolve, 150));
}
}
async digArea(
start: Position,
end: Position,
progressCallback?: (
progress: number,
blocksDug: number,
totalBlocks: number
) => void
): Promise<void> {
if (!this.bot) {
return this.wrapError("Not connected");
}
const minX = Math.min(start.x, end.x);
const maxX = Math.max(start.x, end.x);
const minY = Math.min(start.y, end.y);
const maxY = Math.max(start.y, end.y);
const minZ = Math.min(start.z, end.z);
const maxZ = Math.max(start.z, end.z);
// Pre-scan the area to identify diggable blocks and create an efficient digging plan
const diggableBlocks: Vec3[] = [];
const undiggableBlocks: Vec3[] = [];
// Helper to check if a block is diggable
const isDiggable = (block: PrismarineBlock | null): boolean => {
if (!block) return false;
if (block.name === "air") return false;
if (block.hardness < 0) return false; // Bedrock and other unbreakable blocks
// Skip fluid blocks
if (
block.name.includes("water") ||
block.name.includes("lava") ||
block.name.includes("flowing")
) {
return false;
}
// Skip blocks that are known to be unbreakable or special
const unbreakableBlocks = [
"barrier",
"bedrock",
"end_portal",
"end_portal_frame",
];
if (unbreakableBlocks.includes(block.name)) return false;
return true;
};
// First pass: identify all diggable blocks
for (let y = maxY; y >= minY; y--) {
for (let x = minX; x <= maxX; x++) {
for (let z = minZ; z <= maxZ; z++) {
const pos = new Vec3(x, y, z);
const block = this.bot.blockAt(pos);
if (isDiggable(block)) {
diggableBlocks.push(pos);
} else if (block && block.name !== "air") {
undiggableBlocks.push(pos);
}
}
}
}
const totalBlocks = diggableBlocks.length;
let blocksDug = 0;
let lastProgressUpdate = Date.now();
// Set up disconnect handler
let disconnected = false;
const disconnectHandler = () => {
disconnected = true;
};
this.bot.once("end", disconnectHandler);
try {
// Group blocks into "slices" for more efficient digging
const sliceSize = 4; // Size of each work area
const slices: Vec3[][] = [];
// Group blocks into nearby clusters for efficient movement
for (let x = minX; x <= maxX; x += sliceSize) {
for (let z = minZ; z <= maxZ; z += sliceSize) {
const slice: Vec3[] = diggableBlocks.filter(
(pos) =>
pos.x >= x &&
pos.x < x + sliceSize &&
pos.z >= z &&
pos.z < z + sliceSize
);
if (slice.length > 0) {
// Sort the slice from top to bottom for safer digging
slice.sort((a, b) => b.y - a.y);
slices.push(slice);
}
}
}
// Process each slice
for (const slice of slices) {
if (disconnected) {
return this.wrapError("Disconnected while digging area");
}
// Find optimal position to dig this slice
const sliceCenter = slice
.reduce((acc, pos) => acc.plus(pos), new Vec3(0, 0, 0))
.scaled(1 / slice.length);
// Try to move to a good position for this slice
try {
// Position ourselves at a good vantage point for the slice
const standingPos = new Vec3(
sliceCenter.x - 1,
Math.max(sliceCenter.y, minY),
sliceCenter.z - 1
);
await this.navigateTo(standingPos.x, standingPos.y, standingPos.z);
} catch (error) {
this.logWarning(
"Could not reach optimal digging position for slice",
error
);
// Continue anyway - some blocks might still be reachable
}
// Process blocks in the slice from top to bottom
for (const pos of slice) {
if (disconnected) {
return this.wrapError("Disconnected while digging area");
}
try {
const block = this.bot.blockAt(pos);
if (!block || !isDiggable(block)) {
continue; // Skip if block changed or became undiggable
}
// Check if we need to move closer
const distance = pos.distanceTo(this.bot.entity.position);
if (distance > 4) {
try {
const goal = new goals.GoalNear(pos.x, pos.y, pos.z, 3);
await this.bot.pathfinder.goto(goal);
} catch (error) {
this.logWarning(
`Could not move closer to block at ${pos.x}, ${pos.y}, ${pos.z}:`,
error
);
continue; // Skip this block if we can't reach it
}
}
await this.digBlock(pos.x, pos.y, pos.z);
blocksDug++;
// Update progress every 500ms
const now = Date.now();
if (progressCallback && now - lastProgressUpdate >= 500) {
const progress = Math.floor((blocksDug / totalBlocks) * 100);
progressCallback(progress, blocksDug, totalBlocks);
lastProgressUpdate = now;
}
} catch (error) {
// Log the error but continue with other blocks
this.logWarning(
`Failed to dig block at ${pos.x}, ${pos.y}, ${pos.z}:`,
error
);
continue;
}
}
}
// Final progress update
if (progressCallback) {
progressCallback(100, blocksDug, totalBlocks);
}
// Log summary of undiggable blocks if any
if (undiggableBlocks.length > 0) {
this.logWarning(
`Completed digging with ${undiggableBlocks.length} undiggable blocks`,
undiggableBlocks.map((pos) => ({
position: pos,
type: this.bot?.blockAt(pos)?.name || "unknown",
}))
);
}
} finally {
// Clean up the disconnect handler
this.bot.removeListener("end", disconnectHandler);
}
}
async placeBlock(
x: number,
y: number,
z: number,
blockName: string
): Promise<void> {
if (!this.bot) return this.wrapError("Not connected");
const item = this.bot.inventory.items().find((i) => i.name === blockName);
if (!item) return this.wrapError(`No ${blockName} in inventory`);
try {
await this.bot.equip(item, "hand");
const targetPos = new Vec3(x, y, z);
const targetBlock = this.bot.blockAt(targetPos);
if (!targetBlock)
return this.wrapError("Invalid target position for placing block");
const faceVector = new Vec3(0, 1, 0);
await this.bot.placeBlock(targetBlock, faceVector);
} catch (error) {
return this.wrapError(
`Failed to place block: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
async followPlayer(username: string, distance: number = 2): Promise<void> {
if (!this.bot) return this.wrapError("Not connected");
const target = this.bot.players[username]?.entity;
if (!target) return this.wrapError(`Player ${username} not found`);
const goal = new goals.GoalFollow(target, distance);
try {
await this.bot.pathfinder.goto(goal);
} catch (error) {
return this.wrapError(
`Failed to follow player: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
async attackEntity(
entityName: string,
maxDistance: number = 5
): Promise<void> {
if (!this.bot) return this.wrapError("Not connected");
const entity = Object.values(this.bot.entities).find(
(e) =>
e.name === entityName &&
e.position.distanceTo(this.bot!.entity.position) <= maxDistance
);
if (!entity)
return this.wrapError(
`No ${entityName} found within ${maxDistance} blocks`
);
try {
await this.bot.attack(entity as any);
} catch (error) {
return this.wrapError(
`Failed to attack entity: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
getEntitiesNearby(maxDistance: number = 10): CustomEntity[] {
if (!this.bot) return this.wrapError("Not connected");
return Object.values(this.bot.entities)
.filter(
(e) => e.position.distanceTo(this.bot!.entity.position) <= maxDistance
)
.map(TypeConverters.entity);
}
getBlocksNearby(maxDistance: number = 10, count: number = 100): Block[] {
if (!this.bot) return this.wrapError("Not connected");
return this.bot
.findBlocks({
matching: () => true,
maxDistance,
count,
})
.map((pos) => {
const block = this.bot?.blockAt(pos);
return block ? TypeConverters.block(block) : null;
})
.filter((b): b is Block => b !== null);
}
getHealthStatus(): HealthStatus {
if (!this.bot) return this.wrapError("Not connected");
return {
health: this.bot.health,
food: this.bot.food,
saturation: this.bot.foodSaturation,
armor: this.bot.game.gameMode === "creative" ? 20 : 0,
};
}
getWeather(): Weather {
if (!this.bot) return this.wrapError("Not connected");
return {
isRaining: this.bot.isRaining,
rainState: this.bot.isRaining ? "raining" : "clear",
thunderState: this.bot.thunderState,
};
}
async navigateRelative(
dx: number,
dy: number,
dz: number,
progressCallback?: (progress: number) => void
): Promise<void> {
if (!this.bot) return this.wrapError("Not connected");
const currentPos = this.bot.entity.position;
const yaw = this.bot.entity.yaw;
const sin = Math.sin(yaw);
const cos = Math.cos(yaw);
const worldDx = dx * cos - dz * sin;
const worldDz = dx * sin + dz * cos;
try {
await this.navigateTo(
currentPos.x + worldDx,
currentPos.y + dy,
currentPos.z + worldDz,
progressCallback
);
} catch (error) {
return this.wrapError(
`Failed to navigate relatively: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
private relativeToAbsolute(
origin: Vec3,
dx: number,
dy: number,
dz: number
): Position {
const yaw = this.bot!.entity.yaw;
const sin = Math.sin(yaw);
const cos = Math.cos(yaw);
// For "forward/back" as +Z, "left/right" as ±X
const worldDx = dx * cos - dz * sin;
const worldDz = dx * sin + dz * cos;
return {
x: Math.floor(origin.x + worldDx),
y: Math.floor(origin.y + dy),
z: Math.floor(origin.z + worldDz),
};
}
async digBlockRelative(dx: number, dy: number, dz: number): Promise<void> {
if (!this.bot) throw new Error("Not connected");
const currentPos = this.bot.entity.position;
const { x, y, z } = this.relativeToAbsolute(currentPos, dx, dy, dz);
await this.digBlock(x, y, z);
}
async digAreaRelative(
start: { dx: number; dy: number; dz: number },
end: { dx: number; dy: number; dz: number },
progressCallback?: (
progress: number,
blocksDug: number,
totalBlocks: number
) => void
): Promise<void> {
if (!this.bot) throw new Error("Not connected");
const currentPos = this.bot.entity.position;
// Convert both corners to absolute coordinates
const absStart = this.relativeToAbsolute(
currentPos,
start.dx,
start.dy,
start.dz
);
const absEnd = this.relativeToAbsolute(currentPos, end.dx, end.dy, end.dz);
// Use the absolute digArea method
await this.digArea(absStart, absEnd, progressCallback);
}
get entity() {
if (!this.bot?.entity) return this.wrapError("Not connected");
return {
position: this.bot.entity.position,
velocity: this.bot.entity.velocity,
yaw: this.bot.entity.yaw,
pitch: this.bot.entity.pitch,
};
}
get entities() {
if (!this.bot) return this.wrapError("Not connected");
const converted: { [id: string]: CustomEntity } = {};
for (const [id, e] of Object.entries(this.bot.entities)) {
converted[id] = TypeConverters.entity(e);
}
return converted;
}
get inventory() {
if (!this.bot) return this.wrapError("Not connected");
return {
items: () => this.bot!.inventory.items().map(TypeConverters.item),
slots: Object.fromEntries(
Object.entries(this.bot!.inventory.slots).map(([slot, item]) => [
slot,
item ? TypeConverters.item(item) : null,
])
),
};
}
get pathfinder() {
if (!this.bot) return this.wrapError("Not connected");
if (!this.movements) {
this.movements = new Movements(this.bot as unknown as Bot);
}
const pf = this.bot.pathfinder;
const currentMovements = this.movements;
return {
setMovements: (movements: Movements) => {
this.movements = movements;
pf.setMovements(movements);
},
goto: (goal: goals.Goal) => pf.goto(goal),
getPathTo: async (goal: goals.Goal, timeout?: number) => {
if (!this.movements) return this.wrapError("Movements not initialized");
const path = await pf.getPathTo(this.movements, goal, timeout);
if (!path) return null;
return {
path: path.path.map((pos: any) => new Vec3(pos.x, pos.y, pos.z)),
};
},
};
}
blockAt(position: Vec3): Block | null {
if (!this.bot) return this.wrapError("Not connected");
const block = this.bot.blockAt(position);
return block ? TypeConverters.block(block) : null;
}
findBlocks(options: {
matching: ((block: Block) => boolean) | string | string[];
maxDistance: number;
count: number;
point?: Vec3;
}): Vec3[] {
if (!this.bot) return this.wrapError("Not connected");
// Convert string or string[] to matching function
let matchingFn: (block: PrismarineBlock) => boolean;
if (typeof options.matching === "string") {
const blockName = options.matching;
matchingFn = (b: PrismarineBlock) => b.name === blockName;
} else if (Array.isArray(options.matching)) {
const blockNames = options.matching;
matchingFn = (b: PrismarineBlock) => blockNames.includes(b.name);
} else {
const matchingFunc = options.matching;
matchingFn = (b: PrismarineBlock) =>
matchingFunc(TypeConverters.block(b));
}
return this.bot.findBlocks({
...options,
matching: matchingFn,
});
}
getEquipmentDestSlot(destination: string): number {
if (!this.bot) return this.wrapError("Not connected");
return this.bot.getEquipmentDestSlot(destination);
}
canSeeEntity(entity: CustomEntity): boolean {
if (!this.bot) return false;
const prismarineEntity = Object.values(this.bot.entities).find(
(e) =>
e.name === entity.name &&
e.position.equals(
new Vec3(entity.position.x, entity.position.y, entity.position.z)
)
);
if (!prismarineEntity) return false;
// Simple line-of-sight check
const distance = prismarineEntity.position.distanceTo(
this.bot.entity.position
);
return (
distance <= 32 &&
this.hasLineOfSight(this.bot.entity.position, prismarineEntity.position)
);
}
private hasLineOfSight(start: Vec3, end: Vec3): boolean {
if (!this.bot) return false;
const direction = end.minus(start).normalize();
const distance = start.distanceTo(end);
const steps = Math.ceil(distance);
for (let i = 1; i < steps; i++) {
const point = start.plus(direction.scaled(i));
const block = this.getPrismarineBlock(point);
if (block?.boundingBox !== "empty") {
return false;
}
}
return true;
}
private getPrismarineBlock(
position: Vec3
): PrismarineBlockWithBoundingBox | undefined {
if (!this.bot) return undefined;
const block = this.bot.blockAt(position);
if (!block) return undefined;
return block as PrismarineBlockWithBoundingBox;
}
async craftItem(
itemName: string,
quantity: number = 1,
useCraftingTable: boolean = false
): Promise<void> {
if (!this.bot) return this.wrapError("Not connected");
try {
// Find all available recipes
const itemById = this.bot.registry.itemsByName[itemName];
if (!itemById) return this.wrapError(`Unknown item: ${itemName}`);
const recipes = this.bot.recipesFor(itemById.id, 1, null, true);
const recipe = recipes[0]; // First matching recipe
if (!recipe) {
return this.wrapError(`No recipe found for ${itemName}`);
}
if (recipe.requiresTable && !useCraftingTable) {
return this.wrapError(`${itemName} requires a crafting table`);
}
// If we need a crafting table, find one nearby or place one
let craftingTableBlock = null;
if (useCraftingTable) {
const nearbyBlocks = this.findBlocks({
matching: (block) => block.name === "crafting_table",
maxDistance: 4,
count: 1,
});
if (nearbyBlocks.length > 0) {
craftingTableBlock = this.bot.blockAt(nearbyBlocks[0]);
} else {
// Try to place a crafting table
const tableItem = this.bot.inventory
.items()
.find((i) => i.name === "crafting_table");
if (!tableItem) {
return this.wrapError("No crafting table in inventory");
}
// Find a suitable position to place the table
const pos = this.bot.entity.position.offset(0, 0, 1);
await this.placeBlock(pos.x, pos.y, pos.z, "crafting_table");
craftingTableBlock = this.bot.blockAt(pos);
}
}
await this.bot.craft(recipe, quantity, craftingTableBlock || undefined);
} catch (error) {
return this.wrapError(
`Failed to craft ${itemName}: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
async equipItem(
itemName: string,
destination: EquipmentDestination
): Promise<void> {
if (!this.bot) return this.wrapError("Not connected");
const item = this.bot.inventory.items().find((i) => i.name === itemName);
if (!item) return this.wrapError(`No ${itemName} in inventory`);
try {
await this.bot.equip(item, destination);
} catch (error) {
return this.wrapError(
`Failed to equip ${itemName}: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
async dropItem(itemName: string, quantity: number = 1): Promise<void> {
if (!this.bot) return this.wrapError("Not connected");
const item = this.bot.inventory.items().find((i) => i.name === itemName);
if (!item) return this.wrapError(`No ${itemName} in inventory`);
try {
await this.bot.toss(item.type, quantity, null);
} catch (error) {
return this.wrapError(
`Failed to drop ${itemName}: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
async openContainer(position: Position): Promise<Container> {
if (!this.bot) return this.wrapError("Not connected");
const block = this.bot.blockAt(
new Vec3(position.x, position.y, position.z)
);
if (!block) return this.wrapError("No block at specified position");
try {
const container = await this.bot.openContainer(block);
return {
type: block.name as "chest" | "furnace" | "crafting_table",
position,
slots: Object.fromEntries(
Object.entries(container.slots).map(([slot, item]) => [
slot,
item ? TypeConverters.item(item as Item) : null,
])
),
};
} catch (error) {
return this.wrapError(
`Failed to open container: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
closeContainer(): void {
if (!this.bot?.currentWindow) return;
this.bot.closeWindow(this.bot.currentWindow);
}
getRecipe(itemName: string): Recipe | null {
if (!this.bot) return null;
const itemById = this.bot.registry.itemsByName[itemName];
if (!itemById) return null;
const recipes = this.bot.recipesFor(itemById.id, 1, null, true);
const recipe = recipes[0];
if (!recipe) return null;
return {
name: itemName,
ingredients: (recipe.ingredients as any[])
.filter((item) => item != null)
.reduce((acc: { [key: string]: number }, item) => {
const name = Object.entries(this.bot!.registry.itemsByName).find(
([_, v]) => v.id === item.id
)?.[0];
if (name) {
acc[name] = (acc[name] || 0) + 1;
}
return acc;
}, {}),
requiresCraftingTable: recipe.requiresTable,
};
}
listAvailableRecipes(): Recipe[] {
if (!this.bot) return [];
const recipes = new Set<string>();
// Get all item names from registry
Object.keys(this.bot.registry.itemsByName).forEach((name) => {
const recipe = this.getRecipe(name);
if (recipe) {
recipes.add(name);
}
});
return Array.from(recipes)
.map((name) => this.getRecipe(name))
.filter((recipe): recipe is Recipe => recipe !== null);
}
canCraft(recipe: Recipe): boolean {
if (!this.bot) return false;
// Check if we have all required ingredients
for (const [itemName, count] of Object.entries(recipe.ingredients)) {
const available = this.bot.inventory
.items()
.filter((item) => item.name === itemName)
.reduce((sum, item) => sum + item.count, 0);
if (available < count) return false;
}
// If it needs a crafting table, check if we have one or can reach one
if (recipe.requiresCraftingTable) {
const hasCraftingTable = this.bot.inventory
.items()
.some((item) => item.name === "crafting_table");
if (!hasCraftingTable) {
const nearbyCraftingTable = this.findBlocks({
matching: (block) => block.name === "crafting_table",
maxDistance: 4,
count: 1,
});
if (nearbyCraftingTable.length === 0) return false;
}
}
return true;
}
async smeltItem(
itemName: string,
fuelName: string,
quantity: number = 1
): Promise<void> {
if (!this.bot) return this.wrapError("Not connected");
try {
// Find a nearby furnace or place one
const nearbyBlocks = this.findBlocks({
matching: (block) => block.name === "furnace",
maxDistance: 4,
count: 1,
});
let furnaceBlock;
if (nearbyBlocks.length > 0) {
furnaceBlock = this.bot.blockAt(nearbyBlocks[0]);
} else {
// Try to place a furnace
const furnaceItem = this.bot.inventory
.items()
.find((i) => i.name === "furnace");
if (!furnaceItem) {
return this.wrapError("No furnace in inventory");
}
const pos = this.bot.entity.position.offset(0, 0, 1);
await this.placeBlock(pos.x, pos.y, pos.z, "furnace");
furnaceBlock = this.bot.blockAt(pos);
}
if (!furnaceBlock)
return this.wrapError("Could not find or place furnace");
// Open the furnace
const furnace = (await this.bot.openContainer(
furnaceBlock
)) as unknown as Furnace;
try {
// Add the item to smelt
const itemToSmelt = this.bot.inventory
.items()
.find((i) => i.name === itemName);
if (!itemToSmelt) return this.wrapError(`No ${itemName} in inventory`);
// Add the fuel
const fuelItem = this.bot.inventory
.items()
.find((i) => i.name === fuelName);
if (!fuelItem) return this.wrapError(`No ${fuelName} in inventory`);
// Put items in the furnace
await furnace.putInput(itemToSmelt.type, null, quantity);
await furnace.putFuel(fuelItem.type, null, quantity);
// Wait for smelting to complete
await new Promise((resolve) => {
const checkInterval = setInterval(() => {
if (furnace.fuel === 0 && furnace.progress === 0) {
clearInterval(checkInterval);
resolve(null);
}
}, 1000);
});
} finally {
// Always close the furnace when done
this.bot.closeWindow(furnace);
}
} catch (error) {
return this.wrapError(
`Failed to smelt ${itemName}: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
async depositItem(
containerPosition: Position,
itemName: string,
quantity: number = 1
): Promise<void> {
if (!this.bot) return this.wrapError("Not connected");
try {
const block = this.bot.blockAt(
new Vec3(containerPosition.x, containerPosition.y, containerPosition.z)
);
if (!block) return this.wrapError("No container at position");
const window = await this.bot.openContainer(block);
if (!window) return this.wrapError("Failed to open container");
try {
const item = this.bot.inventory.slots.find((i) => i?.name === itemName);
if (!item) return this.wrapError(`No ${itemName} in inventory`);
const emptySlot = window.slots.findIndex(
(slot: Item | null) => slot === null
);
if (emptySlot === -1) return this.wrapError("Container is full");
await this.bot.moveSlotItem(item.slot, emptySlot);
} finally {
this.bot.closeWindow(window);
}
} catch (error) {
return this.wrapError(
`Failed to deposit ${itemName}: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
async withdrawItem(
containerPosition: Position,
itemName: string,
quantity: number = 1
): Promise<void> {
if (!this.bot) return this.wrapError("Not connected");
try {
const block = this.bot.blockAt(
new Vec3(containerPosition.x, containerPosition.y, containerPosition.z)
);
if (!block) return this.wrapError("No container at position");
const window = await this.bot.openContainer(block);
if (!window) return this.wrapError("Failed to open container");
try {
const containerSlot = window.slots.findIndex(
(item: Item | null) => item?.name === itemName
);
if (containerSlot === -1)
return this.wrapError(`No ${itemName} in container`);
const emptySlot = this.bot.inventory.slots.findIndex(
(slot) => slot === null
);
if (emptySlot === -1) return this.wrapError("Inventory is full");
await this.bot.moveSlotItem(containerSlot, emptySlot);
} finally {
this.bot.closeWindow(window);
}
} catch (error) {
return this.wrapError(
`Failed to withdraw ${itemName}: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
private wrapError(message: string): never {
throw {
code: -32603,
message,
data: null,
};
}
}
```