@lzimul/minecraft-mcp 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +219 -179
- package/SECURITY.md +38 -21
- package/build/config/server-info.js +3 -2
- package/build/config/world-limits.js +4 -1
- package/build/data/entities.js +1581 -0
- package/build/enums/entity-kind.enum.js +5 -0
- package/build/enums/inventory-section.enum.js +5 -0
- package/build/index.js +5 -6
- package/build/interfaces/entity-inventory.interface.js +1 -0
- package/build/interfaces/entity-type.interface.js +1 -0
- package/build/interfaces/entity.interface.js +1 -0
- package/build/resources/dimension-resources.js +23 -6
- package/build/resources/entity-resources.js +17 -0
- package/build/resources/index.js +2 -0
- package/build/schemas/coordinate-schema.js +5 -0
- package/build/schemas/dimension-schema.js +1 -0
- package/build/schemas/entity-schema.js +95 -0
- package/build/tools/block-tools.js +14 -23
- package/build/tools/entity-tools.js +325 -0
- package/build/tools/index.js +4 -0
- package/build/tools/result.js +3 -5
- package/build/tools/world-tools.js +139 -0
- package/build/utils/logger.js +53 -0
- package/build/world/block-spec.js +6 -6
- package/build/world/block-states.js +23 -4
- package/build/world/entity-inventory.js +84 -0
- package/build/world/item-spec.js +25 -0
- package/build/world/simulated-dimension.js +187 -13
- package/build/world/stores/file-world-store.js +67 -20
- package/build/world/stores/memory-world-store.js +13 -5
- package/build/world/stores/serialized-block.js +9 -14
- package/build/world/stores/serialized-entity.js +74 -0
- package/build/world/world.js +33 -1
- package/lib/config/server-info.d.ts.map +1 -1
- package/lib/config/world-limits.d.ts +3 -0
- package/lib/config/world-limits.d.ts.map +1 -1
- package/lib/data/entities.d.ts +5 -0
- package/lib/data/entities.d.ts.map +1 -0
- package/lib/enums/entity-kind.enum.d.ts +5 -0
- package/lib/enums/entity-kind.enum.d.ts.map +1 -0
- package/lib/enums/inventory-section.enum.d.ts +5 -0
- package/lib/enums/inventory-section.enum.d.ts.map +1 -0
- package/lib/interfaces/entity-inventory.interface.d.ts +9 -0
- package/lib/interfaces/entity-inventory.interface.d.ts.map +1 -0
- package/lib/interfaces/entity-type.interface.d.ts +8 -0
- package/lib/interfaces/entity-type.interface.d.ts.map +1 -0
- package/lib/interfaces/entity.interface.d.ts +17 -0
- package/lib/interfaces/entity.interface.d.ts.map +1 -0
- package/lib/interfaces/world-store.interface.d.ts +7 -2
- package/lib/interfaces/world-store.interface.d.ts.map +1 -1
- package/lib/resources/dimension-resources.d.ts.map +1 -1
- package/lib/resources/entity-resources.d.ts +3 -0
- package/lib/resources/entity-resources.d.ts.map +1 -0
- package/lib/resources/index.d.ts.map +1 -1
- package/lib/schemas/coordinate-schema.d.ts +5 -0
- package/lib/schemas/coordinate-schema.d.ts.map +1 -1
- package/lib/schemas/dimension-schema.d.ts +1 -0
- package/lib/schemas/dimension-schema.d.ts.map +1 -1
- package/lib/schemas/entity-schema.d.ts +134 -0
- package/lib/schemas/entity-schema.d.ts.map +1 -0
- package/lib/tools/block-tools.d.ts.map +1 -1
- package/lib/tools/entity-tools.d.ts +4 -0
- package/lib/tools/entity-tools.d.ts.map +1 -0
- package/lib/tools/index.d.ts.map +1 -1
- package/lib/tools/result.d.ts +2 -6
- package/lib/tools/result.d.ts.map +1 -1
- package/lib/tools/world-tools.d.ts +4 -0
- package/lib/tools/world-tools.d.ts.map +1 -0
- package/lib/utils/logger.d.ts +3 -0
- package/lib/utils/logger.d.ts.map +1 -0
- package/lib/world/block-states.d.ts +2 -0
- package/lib/world/block-states.d.ts.map +1 -1
- package/lib/world/entity-inventory.d.ts +16 -0
- package/lib/world/entity-inventory.d.ts.map +1 -0
- package/lib/world/item-spec.d.ts +10 -0
- package/lib/world/item-spec.d.ts.map +1 -0
- package/lib/world/simulated-dimension.d.ts +29 -0
- package/lib/world/simulated-dimension.d.ts.map +1 -1
- package/lib/world/stores/file-world-store.d.ts +3 -3
- package/lib/world/stores/file-world-store.d.ts.map +1 -1
- package/lib/world/stores/memory-world-store.d.ts +3 -4
- package/lib/world/stores/memory-world-store.d.ts.map +1 -1
- package/lib/world/stores/serialized-block.d.ts.map +1 -1
- package/lib/world/stores/serialized-entity.d.ts +24 -0
- package/lib/world/stores/serialized-entity.d.ts.map +1 -0
- package/lib/world/world.d.ts +3 -0
- package/lib/world/world.d.ts.map +1 -1
- package/package.json +113 -109
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import * as z from 'zod/v4';
|
|
2
|
+
import { WORLD_LIMITS } from '../config/world-limits.js';
|
|
3
|
+
import { getEntityTypeByName } from '../data/entities.js';
|
|
4
|
+
import { EInventorySection } from '../enums/inventory-section.enum.js';
|
|
5
|
+
import { blockPositionShape } from '../schemas/coordinate-schema.js';
|
|
6
|
+
import { dimensionField } from '../schemas/dimension-schema.js';
|
|
7
|
+
import { entityInfoSchema, inventoryChangeSchema, inventoryViewSchema, toEntityInfo, toInventoryView, toItemStackInfo } from '../schemas/entity-schema.js';
|
|
8
|
+
import { toErrorMessage } from '../utils/error-message.js';
|
|
9
|
+
import { HOTBAR_SLOT_COUNT, INVENTORY_SLOT_COUNT, slotCount } from '../world/entity-inventory.js';
|
|
10
|
+
import { parseItemSpec } from '../world/item-spec.js';
|
|
11
|
+
import { coordinateRangeError, toolError } from './result.js';
|
|
12
|
+
const entityIdSchema = z
|
|
13
|
+
.string()
|
|
14
|
+
.trim()
|
|
15
|
+
.min(1)
|
|
16
|
+
.max(128)
|
|
17
|
+
.describe('Entity UUID returned by createEntity or createPlayer');
|
|
18
|
+
const targetSchema = z
|
|
19
|
+
.string()
|
|
20
|
+
.trim()
|
|
21
|
+
.min(1)
|
|
22
|
+
.max(128)
|
|
23
|
+
.describe('Entity UUID or player name (player names are case-insensitive)');
|
|
24
|
+
const playerNameSchema = z
|
|
25
|
+
.string()
|
|
26
|
+
.trim()
|
|
27
|
+
.min(1)
|
|
28
|
+
.max(WORLD_LIMITS.maxPlayerNameLength)
|
|
29
|
+
.regex(/^[A-Za-z0-9_]+$/u, 'Player names may only contain letters, digits and underscores')
|
|
30
|
+
.describe('Unique player name (letters, digits and underscores)');
|
|
31
|
+
const entityNameSchema = z
|
|
32
|
+
.string()
|
|
33
|
+
.trim()
|
|
34
|
+
.min(1)
|
|
35
|
+
.max(WORLD_LIMITS.maxEntityNameLength)
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('Optional display name; defaults to the entity type display name');
|
|
38
|
+
const itemIdSchema = z
|
|
39
|
+
.string()
|
|
40
|
+
.trim()
|
|
41
|
+
.min(1)
|
|
42
|
+
.max(256)
|
|
43
|
+
.describe('Item id, such as "minecraft:diamond_sword" or "diamond_sword". Use "minecraft:air" or count 0 to clear the slot.');
|
|
44
|
+
function resolveEntityType(input) {
|
|
45
|
+
const normalized = input.trim().toLowerCase();
|
|
46
|
+
return (getEntityTypeByName(normalized) ??
|
|
47
|
+
getEntityTypeByName(`minecraft:${normalized}`));
|
|
48
|
+
}
|
|
49
|
+
function resolveTarget(world, target) {
|
|
50
|
+
const normalized = target.trim();
|
|
51
|
+
return world.findEntity(normalized) ?? world.findPlayer(normalized);
|
|
52
|
+
}
|
|
53
|
+
function text(message) {
|
|
54
|
+
return { type: 'text', text: message };
|
|
55
|
+
}
|
|
56
|
+
export function registerEntityTools(server, world) {
|
|
57
|
+
server.registerTool('createEntity', {
|
|
58
|
+
title: 'Create Entity',
|
|
59
|
+
description: 'Create a non-player entity (mob, projectile, vehicle, etc.) in a dimension with an empty 9-slot hotbar and 27-slot backpack. Entity ids come from the generated registry; see the resource lzimul://minecraft/entities for the full list. Use createPlayer for players.',
|
|
60
|
+
inputSchema: z.object({
|
|
61
|
+
type: z
|
|
62
|
+
.string()
|
|
63
|
+
.trim()
|
|
64
|
+
.min(1)
|
|
65
|
+
.max(128)
|
|
66
|
+
.describe('Entity id, such as "minecraft:zombie" or "zombie"; see lzimul://minecraft/entities'),
|
|
67
|
+
...blockPositionShape,
|
|
68
|
+
dimension: dimensionField,
|
|
69
|
+
name: entityNameSchema
|
|
70
|
+
}),
|
|
71
|
+
outputSchema: entityInfoSchema,
|
|
72
|
+
annotations: {
|
|
73
|
+
destructiveHint: false,
|
|
74
|
+
idempotentHint: false,
|
|
75
|
+
openWorldHint: false
|
|
76
|
+
}
|
|
77
|
+
}, async ({ type: typeId, x, y, z, dimension: dimensionType, name }) => {
|
|
78
|
+
try {
|
|
79
|
+
const type = resolveEntityType(typeId);
|
|
80
|
+
if (!type) {
|
|
81
|
+
return toolError(`Unknown entity type "${typeId}". Use an id such as "minecraft:zombie"; see the resource lzimul://minecraft/entities for the full list.`);
|
|
82
|
+
}
|
|
83
|
+
if (type.name === 'player') {
|
|
84
|
+
return toolError('The minecraft:player type cannot be created with createEntity. Use createPlayer instead.');
|
|
85
|
+
}
|
|
86
|
+
const dimension = world.getDimension(dimensionType);
|
|
87
|
+
if (!dimension.contains(x, y, z)) {
|
|
88
|
+
return coordinateRangeError(dimension.info, x, y, z);
|
|
89
|
+
}
|
|
90
|
+
if (!dimension.canAddEntity()) {
|
|
91
|
+
return toolError(`The ${dimension.info.displayName} world already holds the maximum of ${WORLD_LIMITS.maxEntitiesPerDimension} entities.`);
|
|
92
|
+
}
|
|
93
|
+
const entity = dimension.createEntity(type, name ?? type.displayName, x, y, z);
|
|
94
|
+
try {
|
|
95
|
+
await dimension.flush();
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
return toolError(`Entity ${type.key} "${entity.name}" was created at (${x}, ${y}, ${z}) in ${dimension.info.displayName} but could not be persisted to the world store: ${toErrorMessage(error)}`);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
content: [
|
|
102
|
+
text(`Created ${entity.type.displayName} "${entity.name}" (${entity.type.key}) with id ${entity.id} at (${x}, ${y}, ${z}) in ${dimension.info.displayName}`)
|
|
103
|
+
],
|
|
104
|
+
structuredContent: toEntityInfo(entity)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
return toolError(`createEntity failed: ${toErrorMessage(error)}`);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
server.registerTool('getEntity', {
|
|
112
|
+
title: 'Get Entity',
|
|
113
|
+
description: 'Get an entity or player by UUID, including its type, position, dimension, creation time and full hotbar/backpack contents. Use getInventory or setInventory to change items.',
|
|
114
|
+
inputSchema: z.object({
|
|
115
|
+
id: entityIdSchema
|
|
116
|
+
}),
|
|
117
|
+
outputSchema: entityInfoSchema,
|
|
118
|
+
annotations: {
|
|
119
|
+
readOnlyHint: true,
|
|
120
|
+
idempotentHint: true,
|
|
121
|
+
openWorldHint: false
|
|
122
|
+
}
|
|
123
|
+
}, async ({ id }) => {
|
|
124
|
+
try {
|
|
125
|
+
const entity = world.findEntity(id);
|
|
126
|
+
if (!entity) {
|
|
127
|
+
return toolError(`Unknown entity "${id}". Use the id returned by createEntity or createPlayer; use getPlayer for a player name.`);
|
|
128
|
+
}
|
|
129
|
+
const info = toEntityInfo(entity);
|
|
130
|
+
return {
|
|
131
|
+
content: [
|
|
132
|
+
text(`${info.typeDisplayName} "${info.name}" (${info.type}, ${info.kind}) at (${info.x}, ${info.y}, ${info.z}) in ${info.dimension}; ${info.usedSlots}/${info.totalSlots} inventory slots used`)
|
|
133
|
+
],
|
|
134
|
+
structuredContent: info
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
return toolError(`getEntity failed: ${toErrorMessage(error)}`);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
server.registerTool('createPlayer', {
|
|
142
|
+
title: 'Create Player',
|
|
143
|
+
description: 'Create a uniquely named player in a dimension with an empty 9-slot hotbar and 27-slot backpack. Player names are case-insensitively unique across the whole world (all dimensions).',
|
|
144
|
+
inputSchema: z.object({
|
|
145
|
+
name: playerNameSchema,
|
|
146
|
+
...blockPositionShape,
|
|
147
|
+
dimension: dimensionField
|
|
148
|
+
}),
|
|
149
|
+
outputSchema: entityInfoSchema,
|
|
150
|
+
annotations: {
|
|
151
|
+
destructiveHint: false,
|
|
152
|
+
idempotentHint: false,
|
|
153
|
+
openWorldHint: false
|
|
154
|
+
}
|
|
155
|
+
}, async ({ name, x, y, z, dimension: dimensionType }) => {
|
|
156
|
+
try {
|
|
157
|
+
const existing = world.findPlayer(name);
|
|
158
|
+
if (existing) {
|
|
159
|
+
const where = world.getDimension(existing.dimension).info.displayName;
|
|
160
|
+
return toolError(`A player named "${name}" already exists in ${where}. Player names are case-insensitive and unique across all dimensions.`);
|
|
161
|
+
}
|
|
162
|
+
const dimension = world.getDimension(dimensionType);
|
|
163
|
+
if (!dimension.contains(x, y, z)) {
|
|
164
|
+
return coordinateRangeError(dimension.info, x, y, z);
|
|
165
|
+
}
|
|
166
|
+
if (!dimension.canAddEntity()) {
|
|
167
|
+
return toolError(`The ${dimension.info.displayName} world already holds the maximum of ${WORLD_LIMITS.maxEntitiesPerDimension} entities.`);
|
|
168
|
+
}
|
|
169
|
+
const player = dimension.createPlayer(name, x, y, z);
|
|
170
|
+
try {
|
|
171
|
+
await dimension.flush();
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
return toolError(`Player "${name}" was created at (${x}, ${y}, ${z}) in ${dimension.info.displayName} but could not be persisted to the world store: ${toErrorMessage(error)}`);
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
content: [
|
|
178
|
+
text(`Created player "${player.name}" with id ${player.id} at (${x}, ${y}, ${z}) in ${dimension.info.displayName}`)
|
|
179
|
+
],
|
|
180
|
+
structuredContent: toEntityInfo(player)
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
return toolError(`createPlayer failed: ${toErrorMessage(error)}`);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
server.registerTool('getPlayer', {
|
|
188
|
+
title: 'Get Player',
|
|
189
|
+
description: 'Get a player by name (case-insensitive), including position, dimension, creation time and full hotbar/backpack contents.',
|
|
190
|
+
inputSchema: z.object({
|
|
191
|
+
name: playerNameSchema
|
|
192
|
+
}),
|
|
193
|
+
outputSchema: entityInfoSchema,
|
|
194
|
+
annotations: {
|
|
195
|
+
readOnlyHint: true,
|
|
196
|
+
idempotentHint: true,
|
|
197
|
+
openWorldHint: false
|
|
198
|
+
}
|
|
199
|
+
}, async ({ name }) => {
|
|
200
|
+
try {
|
|
201
|
+
const player = world.findPlayer(name);
|
|
202
|
+
if (!player) {
|
|
203
|
+
return toolError(`Unknown player "${name}". Player names are case-insensitive; use createPlayer to create one.`);
|
|
204
|
+
}
|
|
205
|
+
const info = toEntityInfo(player);
|
|
206
|
+
return {
|
|
207
|
+
content: [
|
|
208
|
+
text(`Player "${info.name}" (${info.id}) at (${info.x}, ${info.y}, ${info.z}) in ${info.dimension}; ${info.usedSlots}/${info.totalSlots} inventory slots used`)
|
|
209
|
+
],
|
|
210
|
+
structuredContent: info
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
return toolError(`getPlayer failed: ${toErrorMessage(error)}`);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
server.registerTool('getInventory', {
|
|
218
|
+
title: 'Get Inventory',
|
|
219
|
+
description: 'Get the full inventory of an entity or player: the 9 hotbar slots and the 27 backpack slots, plus used-slot counts. Empty slots are null. The target is an entity UUID or a player name.',
|
|
220
|
+
inputSchema: z.object({
|
|
221
|
+
target: targetSchema
|
|
222
|
+
}),
|
|
223
|
+
outputSchema: inventoryViewSchema,
|
|
224
|
+
annotations: {
|
|
225
|
+
readOnlyHint: true,
|
|
226
|
+
idempotentHint: true,
|
|
227
|
+
openWorldHint: false
|
|
228
|
+
}
|
|
229
|
+
}, async ({ target }) => {
|
|
230
|
+
try {
|
|
231
|
+
const entity = resolveTarget(world, target);
|
|
232
|
+
if (!entity) {
|
|
233
|
+
return toolError(`Unknown entity or player "${target}". Use an entity UUID from createEntity/createPlayer or a player name.`);
|
|
234
|
+
}
|
|
235
|
+
const view = toInventoryView(entity);
|
|
236
|
+
return {
|
|
237
|
+
content: [
|
|
238
|
+
text(`${entity.name} (${entity.kind}, ${entity.type.key}): ${view.usedSlots}/${view.totalSlots} inventory slots used (hotbar ${view.hotbarUsed}/${HOTBAR_SLOT_COUNT}, backpack ${view.inventoryUsed}/${INVENTORY_SLOT_COUNT})`)
|
|
239
|
+
],
|
|
240
|
+
structuredContent: view
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
return toolError(`getInventory failed: ${toErrorMessage(error)}`);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
server.registerTool('setInventory', {
|
|
248
|
+
title: 'Set Inventory',
|
|
249
|
+
description: 'Set one inventory slot of an entity or player. section is "hotbar" (slots 0-8) or "inventory" (backpack, slots 0-26). The item is validated against the item registry and the count must fit the item stack size. Use "minecraft:air" or count 0 to clear a slot. The target is an entity UUID or a player name.',
|
|
250
|
+
inputSchema: z.object({
|
|
251
|
+
target: targetSchema,
|
|
252
|
+
section: z
|
|
253
|
+
.enum(EInventorySection)
|
|
254
|
+
.default(EInventorySection.Inventory)
|
|
255
|
+
.describe('"hotbar" for the 9 hotbar slots or "inventory" for the 27 backpack slots'),
|
|
256
|
+
slot: z
|
|
257
|
+
.number()
|
|
258
|
+
.int()
|
|
259
|
+
.min(0)
|
|
260
|
+
.max(INVENTORY_SLOT_COUNT - 1)
|
|
261
|
+
.describe('Slot index within the section (hotbar 0-8, backpack 0-26)'),
|
|
262
|
+
item: itemIdSchema,
|
|
263
|
+
count: z
|
|
264
|
+
.number()
|
|
265
|
+
.int()
|
|
266
|
+
.min(0)
|
|
267
|
+
.max(64)
|
|
268
|
+
.default(1)
|
|
269
|
+
.describe('Stack count (1 to the item stack size); 0 clears the slot')
|
|
270
|
+
}),
|
|
271
|
+
outputSchema: inventoryChangeSchema,
|
|
272
|
+
annotations: {
|
|
273
|
+
destructiveHint: true,
|
|
274
|
+
idempotentHint: true,
|
|
275
|
+
openWorldHint: false
|
|
276
|
+
}
|
|
277
|
+
}, async ({ target, section, slot, item, count }) => {
|
|
278
|
+
try {
|
|
279
|
+
const entity = resolveTarget(world, target);
|
|
280
|
+
if (!entity) {
|
|
281
|
+
return toolError(`Unknown entity or player "${target}". Use an entity UUID from createEntity/createPlayer or a player name.`);
|
|
282
|
+
}
|
|
283
|
+
const maxSlot = slotCount(section) - 1;
|
|
284
|
+
if (slot > maxSlot) {
|
|
285
|
+
return toolError(`Slot ${slot} is out of range for the ${section} section (0-${maxSlot}).`);
|
|
286
|
+
}
|
|
287
|
+
const spec = parseItemSpec(item);
|
|
288
|
+
if (!spec.ok) {
|
|
289
|
+
return toolError(spec.error);
|
|
290
|
+
}
|
|
291
|
+
const isAir = spec.item.key === 'minecraft:air';
|
|
292
|
+
let stack = null;
|
|
293
|
+
if (!isAir && count > 0) {
|
|
294
|
+
if (count > spec.item.stackSize) {
|
|
295
|
+
return toolError(`${spec.item.displayName} stacks up to ${spec.item.stackSize} per slot; received ${count}.`);
|
|
296
|
+
}
|
|
297
|
+
stack = { item: spec.item.key, count };
|
|
298
|
+
}
|
|
299
|
+
const dimension = world.getDimension(entity.dimension);
|
|
300
|
+
const { previous, entity: updated } = dimension.setInventorySlot(entity.id, section, slot, stack);
|
|
301
|
+
try {
|
|
302
|
+
await dimension.flush();
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
return toolError(`The ${section} slot ${slot} of "${updated.name}" was changed but could not be persisted to the world store: ${toErrorMessage(error)}`);
|
|
306
|
+
}
|
|
307
|
+
const message = stack
|
|
308
|
+
? `Set ${section} slot ${slot} of "${updated.name}" to ${spec.item.displayName} x${stack.count}`
|
|
309
|
+
: `Cleared ${section} slot ${slot} of "${updated.name}"`;
|
|
310
|
+
return {
|
|
311
|
+
content: [text(message)],
|
|
312
|
+
structuredContent: {
|
|
313
|
+
...toInventoryView(updated),
|
|
314
|
+
section,
|
|
315
|
+
slot,
|
|
316
|
+
previous: previous ? toItemStackInfo(previous) : null,
|
|
317
|
+
item: stack ? toItemStackInfo(stack) : null
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
return toolError(`setInventory failed: ${toErrorMessage(error)}`);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
package/build/tools/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { registerBlockTools } from './block-tools.js';
|
|
2
|
+
import { registerEntityTools } from './entity-tools.js';
|
|
2
3
|
import { registerItemTools } from './item-tools.js';
|
|
4
|
+
import { registerWorldTools } from './world-tools.js';
|
|
3
5
|
export function registerTools(server, world) {
|
|
4
6
|
registerBlockTools(server, world);
|
|
5
7
|
registerItemTools(server);
|
|
8
|
+
registerWorldTools(server, world);
|
|
9
|
+
registerEntityTools(server, world);
|
|
6
10
|
}
|
package/build/tools/result.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
export function toolText(text) {
|
|
2
|
-
return {
|
|
3
|
-
content: [{ type: 'text', text }]
|
|
4
|
-
};
|
|
5
|
-
}
|
|
6
1
|
export function toolError(message) {
|
|
7
2
|
return {
|
|
8
3
|
isError: true,
|
|
9
4
|
content: [{ type: 'text', text: message }]
|
|
10
5
|
};
|
|
11
6
|
}
|
|
7
|
+
export function coordinateRangeError(dimension, x, y, z) {
|
|
8
|
+
return toolError(`Coordinates (${x}, ${y}, ${z}) are out of range in ${dimension.displayName} (Y: ${dimension.minY} to ${dimension.maxY - 1}).`);
|
|
9
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as z from 'zod/v4';
|
|
2
|
+
import { dimensionField } from '../schemas/dimension-schema.js';
|
|
3
|
+
import { toErrorMessage } from '../utils/error-message.js';
|
|
4
|
+
import { toolError } from './result.js';
|
|
5
|
+
const blockStatesSchema = z.record(z.string(), z.string());
|
|
6
|
+
export function registerWorldTools(server, world) {
|
|
7
|
+
server.registerTool('getDimension', {
|
|
8
|
+
title: 'Get Dimension',
|
|
9
|
+
description: 'Get metadata and live state for one simulated dimension: id, display name, height range, modified block count, entity count and player count. Dimensions are "overworld", "the_nether" and "the_end"; each has its own persisted world state.',
|
|
10
|
+
inputSchema: z.object({
|
|
11
|
+
dimension: dimensionField
|
|
12
|
+
}),
|
|
13
|
+
outputSchema: z.object({
|
|
14
|
+
key: z.string(),
|
|
15
|
+
type: z.string(),
|
|
16
|
+
displayName: z.string(),
|
|
17
|
+
minY: z.number().int(),
|
|
18
|
+
maxY: z.number().int(),
|
|
19
|
+
hasSkylight: z.boolean(),
|
|
20
|
+
modifiedBlockCount: z.number().int(),
|
|
21
|
+
entityCount: z.number().int(),
|
|
22
|
+
playerCount: z.number().int(),
|
|
23
|
+
blocksUri: z.string(),
|
|
24
|
+
dimensionsUri: z.string()
|
|
25
|
+
}),
|
|
26
|
+
annotations: {
|
|
27
|
+
readOnlyHint: true,
|
|
28
|
+
idempotentHint: true,
|
|
29
|
+
openWorldHint: false
|
|
30
|
+
}
|
|
31
|
+
}, async ({ dimension: dimensionType }) => {
|
|
32
|
+
try {
|
|
33
|
+
const dimension = world.getDimension(dimensionType);
|
|
34
|
+
const info = dimension.info;
|
|
35
|
+
return {
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
type: 'text',
|
|
39
|
+
text: `${info.displayName} (${info.key}): ${dimension.modifiedBlockCount} modified blocks, ${dimension.entityCount} entities, ${dimension.playerCount} players; Y ${info.minY} to ${info.maxY - 1}`
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
structuredContent: {
|
|
43
|
+
key: info.key,
|
|
44
|
+
type: info.type,
|
|
45
|
+
displayName: info.displayName,
|
|
46
|
+
minY: info.minY,
|
|
47
|
+
maxY: info.maxY,
|
|
48
|
+
hasSkylight: info.hasSkylight,
|
|
49
|
+
modifiedBlockCount: dimension.modifiedBlockCount,
|
|
50
|
+
entityCount: dimension.entityCount,
|
|
51
|
+
playerCount: dimension.playerCount,
|
|
52
|
+
blocksUri: `lzimul://minecraft/dimensions/${info.type}`,
|
|
53
|
+
dimensionsUri: 'lzimul://minecraft/dimensions'
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return toolError(`getDimension failed: ${toErrorMessage(error)}`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
server.registerTool('getDimensionBlocks', {
|
|
62
|
+
title: 'Get Dimension Blocks',
|
|
63
|
+
description: 'List the placed (non-air) blocks of a dimension, sorted by coordinates. Results are paginated because a dimension can hold up to 250,000 modified blocks: use limit and offset to page through, and hasMore to continue. Unmodified coordinates are air and are not listed; use getBlock for a single coordinate. Each row contains x, y, z, block id, display name and block states.',
|
|
64
|
+
inputSchema: z.object({
|
|
65
|
+
dimension: dimensionField,
|
|
66
|
+
limit: z
|
|
67
|
+
.number()
|
|
68
|
+
.int()
|
|
69
|
+
.min(1)
|
|
70
|
+
.max(10_000)
|
|
71
|
+
.default(1_000)
|
|
72
|
+
.describe('Maximum number of blocks to return (1-10000, default 1000)'),
|
|
73
|
+
offset: z
|
|
74
|
+
.number()
|
|
75
|
+
.int()
|
|
76
|
+
.min(0)
|
|
77
|
+
.default(0)
|
|
78
|
+
.describe('Number of blocks to skip, for pagination (default 0)')
|
|
79
|
+
}),
|
|
80
|
+
outputSchema: z.object({
|
|
81
|
+
dimension: z.string(),
|
|
82
|
+
displayName: z.string(),
|
|
83
|
+
total: z.number().int(),
|
|
84
|
+
offset: z.number().int(),
|
|
85
|
+
limit: z.number().int(),
|
|
86
|
+
returned: z.number().int(),
|
|
87
|
+
hasMore: z.boolean(),
|
|
88
|
+
blocks: z.array(z.object({
|
|
89
|
+
x: z.number().int(),
|
|
90
|
+
y: z.number().int(),
|
|
91
|
+
z: z.number().int(),
|
|
92
|
+
block: z.string(),
|
|
93
|
+
displayName: z.string(),
|
|
94
|
+
states: blockStatesSchema
|
|
95
|
+
}))
|
|
96
|
+
}),
|
|
97
|
+
annotations: {
|
|
98
|
+
readOnlyHint: true,
|
|
99
|
+
idempotentHint: true,
|
|
100
|
+
openWorldHint: false
|
|
101
|
+
}
|
|
102
|
+
}, async ({ dimension: dimensionType, limit, offset }) => {
|
|
103
|
+
try {
|
|
104
|
+
const dimension = world.getDimension(dimensionType);
|
|
105
|
+
const total = dimension.modifiedBlockCount;
|
|
106
|
+
const page = dimension.sortedPlacedBlocks.slice(offset, offset + limit);
|
|
107
|
+
const returned = page.length;
|
|
108
|
+
const hasMore = offset + returned < total;
|
|
109
|
+
return {
|
|
110
|
+
content: [
|
|
111
|
+
{
|
|
112
|
+
type: 'text',
|
|
113
|
+
text: `Returned ${returned} of ${total} placed block(s) in ${dimension.info.displayName} (offset ${offset}, limit ${limit}${hasMore ? ', more available' : ''})`
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
structuredContent: {
|
|
117
|
+
dimension: dimension.info.key,
|
|
118
|
+
displayName: dimension.info.displayName,
|
|
119
|
+
total,
|
|
120
|
+
offset,
|
|
121
|
+
limit,
|
|
122
|
+
returned,
|
|
123
|
+
hasMore,
|
|
124
|
+
blocks: page.map(placed => ({
|
|
125
|
+
x: placed.x,
|
|
126
|
+
y: placed.y,
|
|
127
|
+
z: placed.z,
|
|
128
|
+
block: placed.block.key,
|
|
129
|
+
displayName: placed.block.displayName,
|
|
130
|
+
states: placed.states
|
|
131
|
+
}))
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
return toolError(`getDimensionBlocks failed: ${toErrorMessage(error)}`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { format } from 'node:util';
|
|
2
|
+
import { chalkStderr as chalk } from 'chalk';
|
|
3
|
+
import log4js from 'log4js';
|
|
4
|
+
const VALID_LOG_LEVELS = new Set([
|
|
5
|
+
'ALL',
|
|
6
|
+
'TRACE',
|
|
7
|
+
'DEBUG',
|
|
8
|
+
'INFO',
|
|
9
|
+
'WARN',
|
|
10
|
+
'ERROR',
|
|
11
|
+
'FATAL',
|
|
12
|
+
'OFF',
|
|
13
|
+
'MARK'
|
|
14
|
+
]);
|
|
15
|
+
const REQUESTED_LOG_LEVEL = process.env.MCP_LOG_LEVEL?.trim().toUpperCase();
|
|
16
|
+
const LOG_LEVEL = REQUESTED_LOG_LEVEL && VALID_LOG_LEVELS.has(REQUESTED_LOG_LEVEL)
|
|
17
|
+
? REQUESTED_LOG_LEVEL
|
|
18
|
+
: 'INFO';
|
|
19
|
+
const LOGGER_CATEGORY = 'lzimul-minecraft-mcp';
|
|
20
|
+
function chalkLayout(event) {
|
|
21
|
+
const line = `${event.startTime.toISOString()} [${event.level.levelStr}] ${event.categoryName} - ${format(...event.data)}`;
|
|
22
|
+
switch (event.level.levelStr) {
|
|
23
|
+
case 'FATAL':
|
|
24
|
+
case 'ERROR':
|
|
25
|
+
return chalk.red(line);
|
|
26
|
+
case 'WARN':
|
|
27
|
+
return chalk.yellow(line);
|
|
28
|
+
case 'INFO':
|
|
29
|
+
return chalk.green(line);
|
|
30
|
+
case 'DEBUG':
|
|
31
|
+
return chalk.cyan(line);
|
|
32
|
+
case 'TRACE':
|
|
33
|
+
return chalk.blue(line);
|
|
34
|
+
default:
|
|
35
|
+
return line;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
log4js.addLayout('chalk', () => chalkLayout);
|
|
39
|
+
log4js.configure({
|
|
40
|
+
appenders: {
|
|
41
|
+
stderr: {
|
|
42
|
+
type: 'stderr',
|
|
43
|
+
layout: { type: 'chalk' }
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
categories: {
|
|
47
|
+
default: {
|
|
48
|
+
appenders: ['stderr'],
|
|
49
|
+
level: LOG_LEVEL
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
export const logger = log4js.getLogger(LOGGER_CATEGORY);
|
|
@@ -62,12 +62,12 @@ export function parseBlockSpec(input) {
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
states[propertyName] = propertyValue;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
65
|
+
if (Object.keys(states).length > MAX_STATES) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
error: `Too many block states in "${input}" (max ${MAX_STATES}).`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
return { ok: true, block, states: normalizeStates(states) };
|
|
@@ -17,8 +17,25 @@ export function isSafeStateValue(value) {
|
|
|
17
17
|
normalized.length <= MAX_STATE_VALUE_LENGTH &&
|
|
18
18
|
SAFE_STATE_VALUE.test(normalized));
|
|
19
19
|
}
|
|
20
|
-
export function
|
|
20
|
+
export function compareStateNames(a, b) {
|
|
21
|
+
if (a === b) {
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
return a < b ? -1 : 1;
|
|
25
|
+
}
|
|
26
|
+
export function isEmptyStates(states) {
|
|
21
27
|
if (!states) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
for (const key in states) {
|
|
31
|
+
if (Object.hasOwn(states, key)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
export function normalizeStates(states) {
|
|
38
|
+
if (!states || isEmptyStates(states)) {
|
|
22
39
|
return EMPTY_BLOCK_STATES;
|
|
23
40
|
}
|
|
24
41
|
const entries = Object.entries(states)
|
|
@@ -26,8 +43,10 @@ export function normalizeStates(states) {
|
|
|
26
43
|
isSafeStateName(key) &&
|
|
27
44
|
isSafeStateValue(value))
|
|
28
45
|
.map(([key, value]) => [key.toLowerCase(), value.toLowerCase()])
|
|
29
|
-
.sort(([a], [b]) => a
|
|
30
|
-
return
|
|
46
|
+
.sort(([a], [b]) => compareStateNames(a, b));
|
|
47
|
+
return entries.length === 0
|
|
48
|
+
? EMPTY_BLOCK_STATES
|
|
49
|
+
: Object.fromEntries(entries);
|
|
31
50
|
}
|
|
32
51
|
export function sameStates(a, b) {
|
|
33
52
|
const entries = Object.entries(a);
|
|
@@ -35,7 +54,7 @@ export function sameStates(a, b) {
|
|
|
35
54
|
entries.every(([key, value]) => b[key] === value));
|
|
36
55
|
}
|
|
37
56
|
export function formatStates(states) {
|
|
38
|
-
const entries = Object.entries(states).sort(([a], [b]) => a
|
|
57
|
+
const entries = Object.entries(states).sort(([a], [b]) => compareStateNames(a, b));
|
|
39
58
|
if (entries.length === 0) {
|
|
40
59
|
return '';
|
|
41
60
|
}
|