actors-mcp-server
Official
by apify
- src
- handlers
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,
};
}
}
}