@lzimul/minecraft-mcp 1.0.2 → 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.
Files changed (88) hide show
  1. package/README.md +219 -168
  2. package/SECURITY.md +38 -21
  3. package/build/config/server-info.js +3 -2
  4. package/build/config/world-limits.js +4 -1
  5. package/build/data/entities.js +1581 -0
  6. package/build/enums/entity-kind.enum.js +5 -0
  7. package/build/enums/inventory-section.enum.js +5 -0
  8. package/build/index.js +4 -3
  9. package/build/interfaces/entity-inventory.interface.js +1 -0
  10. package/build/interfaces/entity-type.interface.js +1 -0
  11. package/build/interfaces/entity.interface.js +1 -0
  12. package/build/resources/dimension-resources.js +23 -6
  13. package/build/resources/entity-resources.js +17 -0
  14. package/build/resources/index.js +2 -0
  15. package/build/schemas/coordinate-schema.js +5 -0
  16. package/build/schemas/dimension-schema.js +1 -0
  17. package/build/schemas/entity-schema.js +95 -0
  18. package/build/tools/block-tools.js +14 -23
  19. package/build/tools/entity-tools.js +325 -0
  20. package/build/tools/index.js +4 -0
  21. package/build/tools/result.js +3 -5
  22. package/build/tools/world-tools.js +139 -0
  23. package/build/utils/logger.js +53 -0
  24. package/build/world/block-spec.js +6 -6
  25. package/build/world/block-states.js +23 -4
  26. package/build/world/entity-inventory.js +84 -0
  27. package/build/world/item-spec.js +25 -0
  28. package/build/world/simulated-dimension.js +187 -13
  29. package/build/world/stores/file-world-store.js +67 -20
  30. package/build/world/stores/memory-world-store.js +13 -5
  31. package/build/world/stores/serialized-block.js +9 -14
  32. package/build/world/stores/serialized-entity.js +74 -0
  33. package/build/world/world.js +33 -1
  34. package/lib/config/server-info.d.ts.map +1 -1
  35. package/lib/config/world-limits.d.ts +3 -0
  36. package/lib/config/world-limits.d.ts.map +1 -1
  37. package/lib/data/entities.d.ts +5 -0
  38. package/lib/data/entities.d.ts.map +1 -0
  39. package/lib/enums/entity-kind.enum.d.ts +5 -0
  40. package/lib/enums/entity-kind.enum.d.ts.map +1 -0
  41. package/lib/enums/inventory-section.enum.d.ts +5 -0
  42. package/lib/enums/inventory-section.enum.d.ts.map +1 -0
  43. package/lib/interfaces/entity-inventory.interface.d.ts +9 -0
  44. package/lib/interfaces/entity-inventory.interface.d.ts.map +1 -0
  45. package/lib/interfaces/entity-type.interface.d.ts +8 -0
  46. package/lib/interfaces/entity-type.interface.d.ts.map +1 -0
  47. package/lib/interfaces/entity.interface.d.ts +17 -0
  48. package/lib/interfaces/entity.interface.d.ts.map +1 -0
  49. package/lib/interfaces/world-store.interface.d.ts +7 -2
  50. package/lib/interfaces/world-store.interface.d.ts.map +1 -1
  51. package/lib/resources/dimension-resources.d.ts.map +1 -1
  52. package/lib/resources/entity-resources.d.ts +3 -0
  53. package/lib/resources/entity-resources.d.ts.map +1 -0
  54. package/lib/resources/index.d.ts.map +1 -1
  55. package/lib/schemas/coordinate-schema.d.ts +5 -0
  56. package/lib/schemas/coordinate-schema.d.ts.map +1 -1
  57. package/lib/schemas/dimension-schema.d.ts +1 -0
  58. package/lib/schemas/dimension-schema.d.ts.map +1 -1
  59. package/lib/schemas/entity-schema.d.ts +134 -0
  60. package/lib/schemas/entity-schema.d.ts.map +1 -0
  61. package/lib/tools/block-tools.d.ts.map +1 -1
  62. package/lib/tools/entity-tools.d.ts +4 -0
  63. package/lib/tools/entity-tools.d.ts.map +1 -0
  64. package/lib/tools/index.d.ts.map +1 -1
  65. package/lib/tools/result.d.ts +2 -6
  66. package/lib/tools/result.d.ts.map +1 -1
  67. package/lib/tools/world-tools.d.ts +4 -0
  68. package/lib/tools/world-tools.d.ts.map +1 -0
  69. package/lib/utils/logger.d.ts +3 -0
  70. package/lib/utils/logger.d.ts.map +1 -0
  71. package/lib/world/block-states.d.ts +2 -0
  72. package/lib/world/block-states.d.ts.map +1 -1
  73. package/lib/world/entity-inventory.d.ts +16 -0
  74. package/lib/world/entity-inventory.d.ts.map +1 -0
  75. package/lib/world/item-spec.d.ts +10 -0
  76. package/lib/world/item-spec.d.ts.map +1 -0
  77. package/lib/world/simulated-dimension.d.ts +29 -0
  78. package/lib/world/simulated-dimension.d.ts.map +1 -1
  79. package/lib/world/stores/file-world-store.d.ts +3 -3
  80. package/lib/world/stores/file-world-store.d.ts.map +1 -1
  81. package/lib/world/stores/memory-world-store.d.ts +3 -4
  82. package/lib/world/stores/memory-world-store.d.ts.map +1 -1
  83. package/lib/world/stores/serialized-block.d.ts.map +1 -1
  84. package/lib/world/stores/serialized-entity.d.ts +24 -0
  85. package/lib/world/stores/serialized-entity.d.ts.map +1 -0
  86. package/lib/world/world.d.ts +3 -0
  87. package/lib/world/world.d.ts.map +1 -1
  88. 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
+ }
@@ -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
+ }
@@ -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
- if (Object.keys(states).length > MAX_STATES) {
67
- return {
68
- ok: false,
69
- error: `Too many block states in "${input}" (max ${MAX_STATES}).`
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 normalizeStates(states) {
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.localeCompare(b));
30
- return Object.fromEntries(entries);
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.localeCompare(b));
57
+ const entries = Object.entries(states).sort(([a], [b]) => compareStateNames(a, b));
39
58
  if (entries.length === 0) {
40
59
  return '';
41
60
  }