@lzimul/minecraft-mcp 1.0.3 → 1.0.5

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.
Files changed (115) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +126 -67
  3. package/SECURITY.md +38 -21
  4. package/build/config/server-info.js +5 -4
  5. package/build/config/storage.js +2 -1
  6. package/build/config/world-limits.js +6 -1
  7. package/build/data/entities.js +1581 -0
  8. package/build/enums/entity-kind.enum.js +5 -0
  9. package/build/enums/inventory-section.enum.js +5 -0
  10. package/build/enums/move-direction.enum.js +9 -0
  11. package/build/index.js +4 -3
  12. package/build/interfaces/entity-inventory.interface.js +1 -0
  13. package/build/interfaces/entity-type.interface.js +1 -0
  14. package/build/interfaces/entity.interface.js +1 -0
  15. package/build/resources/dimension-resources.js +24 -7
  16. package/build/resources/entity-resources.js +17 -0
  17. package/build/resources/index.js +2 -0
  18. package/build/schemas/coordinate-schema.js +5 -0
  19. package/build/schemas/dimension-schema.js +1 -0
  20. package/build/schemas/entity-schema.js +95 -0
  21. package/build/tools/block-tools.js +14 -23
  22. package/build/tools/entity-tools.js +638 -0
  23. package/build/tools/index.js +4 -0
  24. package/build/tools/result.js +3 -5
  25. package/build/tools/world-tools.js +526 -0
  26. package/build/utils/logger.js +53 -0
  27. package/build/world/block-spec.js +8 -6
  28. package/build/world/block-states.js +23 -4
  29. package/build/world/entity-inventory.js +101 -0
  30. package/build/world/item-spec.js +25 -0
  31. package/build/world/simulated-dimension.js +314 -14
  32. package/build/world/stores/file-world-store.js +84 -29
  33. package/build/world/stores/memory-world-store.js +18 -5
  34. package/build/world/stores/serialized-block.js +9 -14
  35. package/build/world/stores/serialized-entity.js +75 -0
  36. package/build/world/stores/serialized-snapshot.js +35 -0
  37. package/build/world/world.js +33 -1
  38. package/lib/app/server.d.ts.map +1 -1
  39. package/lib/config/dimensions.d.ts.map +1 -1
  40. package/lib/config/server-info.d.ts.map +1 -1
  41. package/lib/config/storage.d.ts.map +1 -1
  42. package/lib/config/world-limits.d.ts +5 -0
  43. package/lib/config/world-limits.d.ts.map +1 -1
  44. package/lib/data/entities.d.ts +5 -0
  45. package/lib/data/entities.d.ts.map +1 -0
  46. package/lib/enums/bounding-box.enum.d.ts.map +1 -1
  47. package/lib/enums/dimension-type.enum.d.ts.map +1 -1
  48. package/lib/enums/entity-kind.enum.d.ts +5 -0
  49. package/lib/enums/entity-kind.enum.d.ts.map +1 -0
  50. package/lib/enums/icon-theme.enum.d.ts.map +1 -1
  51. package/lib/enums/inventory-section.enum.d.ts +5 -0
  52. package/lib/enums/inventory-section.enum.d.ts.map +1 -0
  53. package/lib/enums/move-direction.enum.d.ts +9 -0
  54. package/lib/enums/move-direction.enum.d.ts.map +1 -0
  55. package/lib/interfaces/block.interface.d.ts.map +1 -1
  56. package/lib/interfaces/data-entry.interface.d.ts.map +1 -1
  57. package/lib/interfaces/dimension.interface.d.ts.map +1 -1
  58. package/lib/interfaces/entity-inventory.interface.d.ts +9 -0
  59. package/lib/interfaces/entity-inventory.interface.d.ts.map +1 -0
  60. package/lib/interfaces/entity-type.interface.d.ts +8 -0
  61. package/lib/interfaces/entity-type.interface.d.ts.map +1 -0
  62. package/lib/interfaces/entity.interface.d.ts +17 -0
  63. package/lib/interfaces/entity.interface.d.ts.map +1 -0
  64. package/lib/interfaces/item.interface.d.ts.map +1 -1
  65. package/lib/interfaces/placed-block.interface.d.ts.map +1 -1
  66. package/lib/interfaces/world-store.interface.d.ts +8 -2
  67. package/lib/interfaces/world-store.interface.d.ts.map +1 -1
  68. package/lib/resources/block-resources.d.ts.map +1 -1
  69. package/lib/resources/data-resources.d.ts.map +1 -1
  70. package/lib/resources/dimension-resources.d.ts.map +1 -1
  71. package/lib/resources/entity-resources.d.ts +3 -0
  72. package/lib/resources/entity-resources.d.ts.map +1 -0
  73. package/lib/resources/index.d.ts.map +1 -1
  74. package/lib/resources/item-resources.d.ts.map +1 -1
  75. package/lib/schemas/block-schema.d.ts.map +1 -1
  76. package/lib/schemas/coordinate-schema.d.ts +5 -0
  77. package/lib/schemas/coordinate-schema.d.ts.map +1 -1
  78. package/lib/schemas/dimension-schema.d.ts +1 -0
  79. package/lib/schemas/dimension-schema.d.ts.map +1 -1
  80. package/lib/schemas/entity-schema.d.ts +134 -0
  81. package/lib/schemas/entity-schema.d.ts.map +1 -0
  82. package/lib/schemas/item-schema.d.ts.map +1 -1
  83. package/lib/tools/block-tools.d.ts.map +1 -1
  84. package/lib/tools/entity-tools.d.ts +4 -0
  85. package/lib/tools/entity-tools.d.ts.map +1 -0
  86. package/lib/tools/index.d.ts.map +1 -1
  87. package/lib/tools/item-tools.d.ts.map +1 -1
  88. package/lib/tools/result.d.ts +2 -6
  89. package/lib/tools/result.d.ts.map +1 -1
  90. package/lib/tools/world-tools.d.ts +4 -0
  91. package/lib/tools/world-tools.d.ts.map +1 -0
  92. package/lib/utils/logger.d.ts +3 -0
  93. package/lib/utils/logger.d.ts.map +1 -0
  94. package/lib/world/block-spec.d.ts.map +1 -1
  95. package/lib/world/block-states.d.ts +2 -0
  96. package/lib/world/block-states.d.ts.map +1 -1
  97. package/lib/world/entity-inventory.d.ts +16 -0
  98. package/lib/world/entity-inventory.d.ts.map +1 -0
  99. package/lib/world/item-spec.d.ts +10 -0
  100. package/lib/world/item-spec.d.ts.map +1 -0
  101. package/lib/world/simulated-dimension.d.ts +54 -0
  102. package/lib/world/simulated-dimension.d.ts.map +1 -1
  103. package/lib/world/stores/file-world-store.d.ts +4 -4
  104. package/lib/world/stores/file-world-store.d.ts.map +1 -1
  105. package/lib/world/stores/index.d.ts.map +1 -1
  106. package/lib/world/stores/memory-world-store.d.ts +4 -4
  107. package/lib/world/stores/memory-world-store.d.ts.map +1 -1
  108. package/lib/world/stores/serialized-block.d.ts.map +1 -1
  109. package/lib/world/stores/serialized-entity.d.ts +24 -0
  110. package/lib/world/stores/serialized-entity.d.ts.map +1 -0
  111. package/lib/world/stores/serialized-snapshot.d.ts +6 -0
  112. package/lib/world/stores/serialized-snapshot.d.ts.map +1 -0
  113. package/lib/world/world.d.ts +3 -0
  114. package/lib/world/world.d.ts.map +1 -1
  115. package/package.json +16 -12
@@ -0,0 +1,638 @@
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 { EMoveDirection } from '../enums/move-direction.enum.js';
6
+ import { blockPositionShape } from '../schemas/coordinate-schema.js';
7
+ import { dimensionField } from '../schemas/dimension-schema.js';
8
+ import { entityInfoSchema, entitySummarySchema, inventoryChangeSchema, inventoryViewSchema, itemStackSchema, toEntityInfo, toEntitySummary, toInventoryView, toItemStackInfo } from '../schemas/entity-schema.js';
9
+ import { toErrorMessage } from '../utils/error-message.js';
10
+ import { HOTBAR_SLOT_COUNT, INVENTORY_SLOT_COUNT, slotCount } from '../world/entity-inventory.js';
11
+ import { parseItemSpec } from '../world/item-spec.js';
12
+ import { coordinateRangeError, toolError } from './result.js';
13
+ const entityIdSchema = z
14
+ .string()
15
+ .trim()
16
+ .min(1)
17
+ .max(WORLD_LIMITS.maxEntityIdLength)
18
+ .describe('Entity UUID returned by createEntity or createPlayer');
19
+ const targetSchema = z
20
+ .string()
21
+ .trim()
22
+ .min(1)
23
+ .max(WORLD_LIMITS.maxEntityIdLength)
24
+ .describe('Entity UUID or player name (player names are case-insensitive)');
25
+ const playerNameSchema = z
26
+ .string()
27
+ .trim()
28
+ .min(1)
29
+ .max(WORLD_LIMITS.maxPlayerNameLength)
30
+ .regex(/^[A-Za-z0-9_]+$/u, 'Player names may only contain letters, digits and underscores')
31
+ .describe('Unique player name (letters, digits and underscores)');
32
+ const entityNameSchema = z
33
+ .string()
34
+ .trim()
35
+ .min(1)
36
+ .max(WORLD_LIMITS.maxEntityNameLength)
37
+ .optional()
38
+ .describe('Optional display name; defaults to the entity type display name');
39
+ const itemIdSchema = z
40
+ .string()
41
+ .trim()
42
+ .min(1)
43
+ .max(256)
44
+ .describe('Item id, such as "minecraft:diamond_sword" or "diamond_sword". Use "minecraft:air" or count 0 to clear the slot.');
45
+ const entityPositionShape = {
46
+ x: z.number().int(),
47
+ y: z.number().int(),
48
+ z: z.number().int()
49
+ };
50
+ const entityPositionSchema = z.object({
51
+ id: z.string(),
52
+ type: z.string(),
53
+ name: z.string(),
54
+ ...entityPositionShape
55
+ });
56
+ const entityMovementSchema = entityPositionSchema.extend({
57
+ direction: z.enum(EMoveDirection),
58
+ previous: z.object(entityPositionShape)
59
+ });
60
+ const entityPositionChangeSchema = entityPositionSchema.extend({
61
+ previous: z.object(entityPositionShape)
62
+ });
63
+ const inventoryClearSchema = inventoryViewSchema.extend({
64
+ clearedSlots: z.number().int()
65
+ });
66
+ const inventorySwapSchema = inventoryViewSchema.extend({
67
+ fromSection: z.enum(EInventorySection),
68
+ fromSlot: z.number().int(),
69
+ toSection: z.enum(EInventorySection),
70
+ toSlot: z.number().int(),
71
+ previousFrom: itemStackSchema.nullable(),
72
+ previousTo: itemStackSchema.nullable()
73
+ });
74
+ function toEntityPosition(entity) {
75
+ return {
76
+ id: entity.id,
77
+ type: entity.type.key,
78
+ name: entity.name,
79
+ x: entity.x,
80
+ y: entity.y,
81
+ z: entity.z
82
+ };
83
+ }
84
+ function resolveEntityType(input) {
85
+ const normalized = input.trim().toLowerCase();
86
+ return (getEntityTypeByName(normalized) ??
87
+ getEntityTypeByName(`minecraft:${normalized}`));
88
+ }
89
+ function resolveTarget(world, target) {
90
+ const normalized = target.trim();
91
+ return world.findEntity(normalized) ?? world.findPlayer(normalized);
92
+ }
93
+ function unknownTargetError(target) {
94
+ return toolError(`Unknown entity or player "${target}". Use an entity UUID from createEntity/createPlayer or a player name.`);
95
+ }
96
+ function text(message) {
97
+ return { type: 'text', text: message };
98
+ }
99
+ export function registerEntityTools(server, world) {
100
+ server.registerTool('createEntity', {
101
+ title: 'Create Entity',
102
+ 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.',
103
+ inputSchema: z.object({
104
+ type: z
105
+ .string()
106
+ .trim()
107
+ .min(1)
108
+ .max(128)
109
+ .describe('Entity id, such as "minecraft:zombie" or "zombie"; see lzimul://minecraft/entities'),
110
+ ...blockPositionShape,
111
+ dimension: dimensionField,
112
+ name: entityNameSchema
113
+ }),
114
+ outputSchema: entityInfoSchema,
115
+ annotations: {
116
+ destructiveHint: false,
117
+ idempotentHint: false,
118
+ openWorldHint: false
119
+ }
120
+ }, async ({ type: typeId, x, y, z, dimension: dimensionType, name }) => {
121
+ try {
122
+ const type = resolveEntityType(typeId);
123
+ if (!type) {
124
+ return toolError(`Unknown entity type "${typeId}". Use an id such as "minecraft:zombie"; see the resource lzimul://minecraft/entities for the full list.`);
125
+ }
126
+ if (type.name === 'player') {
127
+ return toolError('The minecraft:player type cannot be created with createEntity. Use createPlayer instead.');
128
+ }
129
+ const dimension = world.getDimension(dimensionType);
130
+ if (!dimension.contains(x, y, z)) {
131
+ return coordinateRangeError(dimension.info, x, y, z);
132
+ }
133
+ if (!dimension.canAddEntity()) {
134
+ return toolError(`The ${dimension.info.displayName} world already holds the maximum of ${WORLD_LIMITS.maxEntitiesPerDimension} entities.`);
135
+ }
136
+ const entity = dimension.createEntity(type, name ?? type.displayName, x, y, z);
137
+ try {
138
+ await dimension.flush();
139
+ }
140
+ catch (error) {
141
+ 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)}`);
142
+ }
143
+ return {
144
+ content: [
145
+ text(`Created ${entity.type.displayName} "${entity.name}" (${entity.type.key}) with id ${entity.id} at (${x}, ${y}, ${z}) in ${dimension.info.displayName}`)
146
+ ],
147
+ structuredContent: toEntityInfo(entity)
148
+ };
149
+ }
150
+ catch (error) {
151
+ return toolError(`createEntity failed: ${toErrorMessage(error)}`);
152
+ }
153
+ });
154
+ server.registerTool('getEntity', {
155
+ title: 'Get Entity',
156
+ 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.',
157
+ inputSchema: z.object({
158
+ id: entityIdSchema
159
+ }),
160
+ outputSchema: entityInfoSchema,
161
+ annotations: {
162
+ readOnlyHint: true,
163
+ idempotentHint: true,
164
+ openWorldHint: false
165
+ }
166
+ }, async ({ id }) => {
167
+ try {
168
+ const entity = world.findEntity(id);
169
+ if (!entity) {
170
+ return toolError(`Unknown entity "${id}". Use the id returned by createEntity or createPlayer; use getPlayer for a player name.`);
171
+ }
172
+ const info = toEntityInfo(entity);
173
+ return {
174
+ content: [
175
+ 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`)
176
+ ],
177
+ structuredContent: info
178
+ };
179
+ }
180
+ catch (error) {
181
+ return toolError(`getEntity failed: ${toErrorMessage(error)}`);
182
+ }
183
+ });
184
+ server.registerTool('createPlayer', {
185
+ title: 'Create Player',
186
+ 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).',
187
+ inputSchema: z.object({
188
+ name: playerNameSchema,
189
+ ...blockPositionShape,
190
+ dimension: dimensionField
191
+ }),
192
+ outputSchema: entityInfoSchema,
193
+ annotations: {
194
+ destructiveHint: false,
195
+ idempotentHint: false,
196
+ openWorldHint: false
197
+ }
198
+ }, async ({ name, x, y, z, dimension: dimensionType }) => {
199
+ try {
200
+ const existing = world.findPlayer(name);
201
+ if (existing) {
202
+ const where = world.getDimension(existing.dimension).info.displayName;
203
+ return toolError(`A player named "${name}" already exists in ${where}. Player names are case-insensitive and unique across all dimensions.`);
204
+ }
205
+ const dimension = world.getDimension(dimensionType);
206
+ if (!dimension.contains(x, y, z)) {
207
+ return coordinateRangeError(dimension.info, x, y, z);
208
+ }
209
+ if (!dimension.canAddEntity()) {
210
+ return toolError(`The ${dimension.info.displayName} world already holds the maximum of ${WORLD_LIMITS.maxEntitiesPerDimension} entities.`);
211
+ }
212
+ const player = dimension.createPlayer(name, x, y, z);
213
+ try {
214
+ await dimension.flush();
215
+ }
216
+ catch (error) {
217
+ 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)}`);
218
+ }
219
+ return {
220
+ content: [
221
+ text(`Created player "${player.name}" with id ${player.id} at (${x}, ${y}, ${z}) in ${dimension.info.displayName}`)
222
+ ],
223
+ structuredContent: toEntityInfo(player)
224
+ };
225
+ }
226
+ catch (error) {
227
+ return toolError(`createPlayer failed: ${toErrorMessage(error)}`);
228
+ }
229
+ });
230
+ server.registerTool('getPlayer', {
231
+ title: 'Get Player',
232
+ description: 'Get a player by name (case-insensitive), including position, dimension, creation time and full hotbar/backpack contents.',
233
+ inputSchema: z.object({
234
+ name: playerNameSchema
235
+ }),
236
+ outputSchema: entityInfoSchema,
237
+ annotations: {
238
+ readOnlyHint: true,
239
+ idempotentHint: true,
240
+ openWorldHint: false
241
+ }
242
+ }, async ({ name }) => {
243
+ try {
244
+ const player = world.findPlayer(name);
245
+ if (!player) {
246
+ return toolError(`Unknown player "${name}". Player names are case-insensitive; use createPlayer to create one.`);
247
+ }
248
+ const info = toEntityInfo(player);
249
+ return {
250
+ content: [
251
+ text(`Player "${info.name}" (${info.id}) at (${info.x}, ${info.y}, ${info.z}) in ${info.dimension}; ${info.usedSlots}/${info.totalSlots} inventory slots used`)
252
+ ],
253
+ structuredContent: info
254
+ };
255
+ }
256
+ catch (error) {
257
+ return toolError(`getPlayer failed: ${toErrorMessage(error)}`);
258
+ }
259
+ });
260
+ server.registerTool('deleteEntity', {
261
+ title: 'Delete Entity',
262
+ description: 'Delete one entity or player by UUID or player name. The entity is removed from its dimension and its inventory is discarded. Player names become available again after deletion.',
263
+ inputSchema: z.object({
264
+ target: targetSchema
265
+ }),
266
+ outputSchema: entitySummarySchema,
267
+ annotations: {
268
+ destructiveHint: true,
269
+ idempotentHint: false,
270
+ openWorldHint: false
271
+ }
272
+ }, async ({ target }) => {
273
+ try {
274
+ const entity = resolveTarget(world, target);
275
+ if (!entity) {
276
+ return unknownTargetError(target);
277
+ }
278
+ const dimension = world.getDimension(entity.dimension);
279
+ const deleted = dimension.deleteEntity(entity.id) ?? entity;
280
+ try {
281
+ await dimension.flush();
282
+ }
283
+ catch (error) {
284
+ return toolError(`Entity "${deleted.name}" was deleted but could not be persisted to the world store: ${toErrorMessage(error)}`);
285
+ }
286
+ return {
287
+ content: [
288
+ text(`Deleted ${deleted.kind} "${deleted.name}" (${deleted.id}, ${deleted.type.key}) from ${dimension.info.displayName}`)
289
+ ],
290
+ structuredContent: toEntitySummary(deleted)
291
+ };
292
+ }
293
+ catch (error) {
294
+ return toolError(`deleteEntity failed: ${toErrorMessage(error)}`);
295
+ }
296
+ });
297
+ server.registerTool('moveEntity', {
298
+ title: 'Move Entity',
299
+ description: 'Move one entity or player exactly one block in a fixed direction: forward (+Z, south), back (-Z, north), left (-X, west), right (+X, east), up (+Y) or down (-Y). The destination must stay inside the entity dimension.',
300
+ inputSchema: z.object({
301
+ id: entityIdSchema,
302
+ direction: z
303
+ .enum(EMoveDirection)
304
+ .describe('One-block direction: forward (+Z), back (-Z), left (-X), right (+X), up (+Y), down (-Y)')
305
+ }),
306
+ outputSchema: entityMovementSchema,
307
+ annotations: {
308
+ destructiveHint: false,
309
+ idempotentHint: false,
310
+ openWorldHint: false
311
+ }
312
+ }, async ({ id, direction }) => {
313
+ try {
314
+ const entity = world.findEntity(id);
315
+ if (!entity) {
316
+ return toolError(`Unknown entity "${id}". Use an entity UUID returned by createEntity or createPlayer.`);
317
+ }
318
+ const dimension = world.getDimension(entity.dimension);
319
+ const { previous, entity: moved } = dimension.moveEntity(id, direction);
320
+ try {
321
+ await dimension.flush();
322
+ }
323
+ catch (error) {
324
+ return toolError(`Entity "${moved.name}" was moved but could not be persisted to the world store: ${toErrorMessage(error)}`);
325
+ }
326
+ return {
327
+ content: [
328
+ text(`Moved ${moved.name} (${moved.id}) ${direction} from (${previous.x}, ${previous.y}, ${previous.z}) to (${moved.x}, ${moved.y}, ${moved.z}) in ${dimension.info.displayName}`)
329
+ ],
330
+ structuredContent: {
331
+ ...toEntityPosition(moved),
332
+ direction,
333
+ previous
334
+ }
335
+ };
336
+ }
337
+ catch (error) {
338
+ return toolError(`moveEntity failed: ${toErrorMessage(error)}`);
339
+ }
340
+ });
341
+ server.registerTool('getEntityPosition', {
342
+ title: 'Get Entity Position',
343
+ description: 'Get the UUID, entity type, name and exact x/y/z position of one entity or player.',
344
+ inputSchema: z.object({
345
+ id: entityIdSchema
346
+ }),
347
+ outputSchema: entityPositionSchema,
348
+ annotations: {
349
+ readOnlyHint: true,
350
+ idempotentHint: true,
351
+ openWorldHint: false
352
+ }
353
+ }, async ({ id }) => {
354
+ try {
355
+ const entity = world.findEntity(id);
356
+ if (!entity) {
357
+ return toolError(`Unknown entity "${id}". Use an entity UUID returned by createEntity or createPlayer.`);
358
+ }
359
+ const position = toEntityPosition(entity);
360
+ return {
361
+ content: [
362
+ text(`${position.name} (${position.type}, ${position.id}) is at (${position.x}, ${position.y}, ${position.z})`)
363
+ ],
364
+ structuredContent: position
365
+ };
366
+ }
367
+ catch (error) {
368
+ return toolError(`getEntityPosition failed: ${toErrorMessage(error)}`);
369
+ }
370
+ });
371
+ server.registerTool('setEntityPosition', {
372
+ title: 'Set Entity Position',
373
+ description: 'Set the exact x/y/z position of one entity or player. The position must stay inside the entity dimension.',
374
+ inputSchema: z.object({
375
+ id: entityIdSchema,
376
+ position: z.object(blockPositionShape)
377
+ }),
378
+ outputSchema: entityPositionChangeSchema,
379
+ annotations: {
380
+ destructiveHint: false,
381
+ idempotentHint: true,
382
+ openWorldHint: false
383
+ }
384
+ }, async ({ id, position }) => {
385
+ try {
386
+ const entity = world.findEntity(id);
387
+ if (!entity) {
388
+ return toolError(`Unknown entity "${id}". Use an entity UUID returned by createEntity or createPlayer.`);
389
+ }
390
+ const dimension = world.getDimension(entity.dimension);
391
+ if (!dimension.contains(position.x, position.y, position.z)) {
392
+ return coordinateRangeError(dimension.info, position.x, position.y, position.z);
393
+ }
394
+ const { previous, entity: updated } = dimension.setEntityPosition(id, position.x, position.y, position.z);
395
+ try {
396
+ await dimension.flush();
397
+ }
398
+ catch (error) {
399
+ return toolError(`Entity "${updated.name}" was moved but could not be persisted to the world store: ${toErrorMessage(error)}`);
400
+ }
401
+ return {
402
+ content: [
403
+ text(`Set ${updated.name} (${updated.id}) position from (${previous.x}, ${previous.y}, ${previous.z}) to (${updated.x}, ${updated.y}, ${updated.z}) in ${dimension.info.displayName}`)
404
+ ],
405
+ structuredContent: {
406
+ ...toEntityPosition(updated),
407
+ previous
408
+ }
409
+ };
410
+ }
411
+ catch (error) {
412
+ return toolError(`setEntityPosition failed: ${toErrorMessage(error)}`);
413
+ }
414
+ });
415
+ server.registerTool('getInventory', {
416
+ title: 'Get Inventory',
417
+ 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.',
418
+ inputSchema: z.object({
419
+ target: targetSchema
420
+ }),
421
+ outputSchema: inventoryViewSchema,
422
+ annotations: {
423
+ readOnlyHint: true,
424
+ idempotentHint: true,
425
+ openWorldHint: false
426
+ }
427
+ }, async ({ target }) => {
428
+ try {
429
+ const entity = resolveTarget(world, target);
430
+ if (!entity) {
431
+ return unknownTargetError(target);
432
+ }
433
+ const view = toInventoryView(entity);
434
+ return {
435
+ content: [
436
+ 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})`)
437
+ ],
438
+ structuredContent: view
439
+ };
440
+ }
441
+ catch (error) {
442
+ return toolError(`getInventory failed: ${toErrorMessage(error)}`);
443
+ }
444
+ });
445
+ server.registerTool('clearInventory', {
446
+ title: 'Clear Inventory',
447
+ description: 'Remove every item from the 9 hotbar slots and 27 backpack slots of one entity or player. The target is an entity UUID or a player name. The cleared slot count is returned.',
448
+ inputSchema: z.object({
449
+ target: targetSchema
450
+ }),
451
+ outputSchema: inventoryClearSchema,
452
+ annotations: {
453
+ destructiveHint: true,
454
+ idempotentHint: true,
455
+ openWorldHint: false
456
+ }
457
+ }, async ({ target }) => {
458
+ try {
459
+ const entity = resolveTarget(world, target);
460
+ if (!entity) {
461
+ return unknownTargetError(target);
462
+ }
463
+ const dimension = world.getDimension(entity.dimension);
464
+ const { clearedSlots, entity: updated } = dimension.clearInventory(entity.id);
465
+ if (clearedSlots > 0) {
466
+ try {
467
+ await dimension.flush();
468
+ }
469
+ catch (error) {
470
+ return toolError(`The inventory of "${updated.name}" was cleared but could not be persisted to the world store: ${toErrorMessage(error)}`);
471
+ }
472
+ }
473
+ return {
474
+ content: [
475
+ text(clearedSlots > 0
476
+ ? `Cleared ${clearedSlots} inventory slot(s) of "${updated.name}"`
477
+ : `The inventory of "${updated.name}" was already empty`)
478
+ ],
479
+ structuredContent: {
480
+ ...toInventoryView(updated),
481
+ clearedSlots
482
+ }
483
+ };
484
+ }
485
+ catch (error) {
486
+ return toolError(`clearInventory failed: ${toErrorMessage(error)}`);
487
+ }
488
+ });
489
+ server.registerTool('swapInventorySlots', {
490
+ title: 'Swap Inventory Slots',
491
+ description: 'Swap two inventory slots of one entity or player. section is "hotbar" (slots 0-8) or "inventory" (backpack, slots 0-26). Both slots may be in the same or different sections. The target is an entity UUID or a player name.',
492
+ inputSchema: z.object({
493
+ target: targetSchema,
494
+ fromSection: z
495
+ .enum(EInventorySection)
496
+ .describe('"hotbar" or "inventory" for the first slot'),
497
+ fromSlot: z
498
+ .number()
499
+ .int()
500
+ .min(0)
501
+ .max(INVENTORY_SLOT_COUNT - 1)
502
+ .describe('First slot index (hotbar 0-8, backpack 0-26)'),
503
+ toSection: z
504
+ .enum(EInventorySection)
505
+ .describe('"hotbar" or "inventory" for the second slot'),
506
+ toSlot: z
507
+ .number()
508
+ .int()
509
+ .min(0)
510
+ .max(INVENTORY_SLOT_COUNT - 1)
511
+ .describe('Second slot index (hotbar 0-8, backpack 0-26)')
512
+ }),
513
+ outputSchema: inventorySwapSchema,
514
+ annotations: {
515
+ destructiveHint: false,
516
+ idempotentHint: false,
517
+ openWorldHint: false
518
+ }
519
+ }, async ({ target, fromSection, fromSlot, toSection, toSlot }) => {
520
+ try {
521
+ const entity = resolveTarget(world, target);
522
+ if (!entity) {
523
+ return unknownTargetError(target);
524
+ }
525
+ const fromMax = slotCount(fromSection) - 1;
526
+ if (fromSlot > fromMax) {
527
+ return toolError(`Slot ${fromSlot} is out of range for the ${fromSection} section (0-${fromMax}).`);
528
+ }
529
+ const toMax = slotCount(toSection) - 1;
530
+ if (toSlot > toMax) {
531
+ return toolError(`Slot ${toSlot} is out of range for the ${toSection} section (0-${toMax}).`);
532
+ }
533
+ const dimension = world.getDimension(entity.dimension);
534
+ const { previousFrom, previousTo, entity: updated } = dimension.swapInventorySlots(entity.id, fromSection, fromSlot, toSection, toSlot);
535
+ try {
536
+ await dimension.flush();
537
+ }
538
+ catch (error) {
539
+ return toolError(`The inventory slots of "${updated.name}" were swapped but could not be persisted to the world store: ${toErrorMessage(error)}`);
540
+ }
541
+ return {
542
+ content: [
543
+ text(`Swapped ${fromSection} slot ${fromSlot} with ${toSection} slot ${toSlot} of "${updated.name}"`)
544
+ ],
545
+ structuredContent: {
546
+ ...toInventoryView(updated),
547
+ fromSection,
548
+ fromSlot,
549
+ toSection,
550
+ toSlot,
551
+ previousFrom: previousFrom ? toItemStackInfo(previousFrom) : null,
552
+ previousTo: previousTo ? toItemStackInfo(previousTo) : null
553
+ }
554
+ };
555
+ }
556
+ catch (error) {
557
+ return toolError(`swapInventorySlots failed: ${toErrorMessage(error)}`);
558
+ }
559
+ });
560
+ server.registerTool('setInventory', {
561
+ title: 'Set Inventory',
562
+ 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.',
563
+ inputSchema: z.object({
564
+ target: targetSchema,
565
+ section: z
566
+ .enum(EInventorySection)
567
+ .default(EInventorySection.Inventory)
568
+ .describe('"hotbar" for the 9 hotbar slots or "inventory" for the 27 backpack slots'),
569
+ slot: z
570
+ .number()
571
+ .int()
572
+ .min(0)
573
+ .max(INVENTORY_SLOT_COUNT - 1)
574
+ .describe('Slot index within the section (hotbar 0-8, backpack 0-26)'),
575
+ item: itemIdSchema,
576
+ count: z
577
+ .number()
578
+ .int()
579
+ .min(0)
580
+ .max(64)
581
+ .default(1)
582
+ .describe('Stack count (1 to the item stack size); 0 clears the slot')
583
+ }),
584
+ outputSchema: inventoryChangeSchema,
585
+ annotations: {
586
+ destructiveHint: true,
587
+ idempotentHint: true,
588
+ openWorldHint: false
589
+ }
590
+ }, async ({ target, section, slot, item, count }) => {
591
+ try {
592
+ const entity = resolveTarget(world, target);
593
+ if (!entity) {
594
+ return unknownTargetError(target);
595
+ }
596
+ const maxSlot = slotCount(section) - 1;
597
+ if (slot > maxSlot) {
598
+ return toolError(`Slot ${slot} is out of range for the ${section} section (0-${maxSlot}).`);
599
+ }
600
+ const spec = parseItemSpec(item);
601
+ if (!spec.ok) {
602
+ return toolError(spec.error);
603
+ }
604
+ const isAir = spec.item.key === 'minecraft:air';
605
+ let stack = null;
606
+ if (!isAir && count > 0) {
607
+ if (count > spec.item.stackSize) {
608
+ return toolError(`${spec.item.displayName} stacks up to ${spec.item.stackSize} per slot; received ${count}.`);
609
+ }
610
+ stack = { item: spec.item.key, count };
611
+ }
612
+ const dimension = world.getDimension(entity.dimension);
613
+ const { previous, entity: updated } = dimension.setInventorySlot(entity.id, section, slot, stack);
614
+ try {
615
+ await dimension.flush();
616
+ }
617
+ catch (error) {
618
+ return toolError(`The ${section} slot ${slot} of "${updated.name}" was changed but could not be persisted to the world store: ${toErrorMessage(error)}`);
619
+ }
620
+ const message = stack
621
+ ? `Set ${section} slot ${slot} of "${updated.name}" to ${spec.item.displayName} x${stack.count}`
622
+ : `Cleared ${section} slot ${slot} of "${updated.name}"`;
623
+ return {
624
+ content: [text(message)],
625
+ structuredContent: {
626
+ ...toInventoryView(updated),
627
+ section,
628
+ slot,
629
+ previous: previous ? toItemStackInfo(previous) : null,
630
+ item: stack ? toItemStackInfo(stack) : null
631
+ }
632
+ };
633
+ }
634
+ catch (error) {
635
+ return toolError(`setInventory failed: ${toErrorMessage(error)}`);
636
+ }
637
+ });
638
+ }
@@ -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
  }
@@ -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
+ }