@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,526 @@
1
+ import * as z from 'zod/v4';
2
+ import { WORLD_LIMITS } from '../config/world-limits.js';
3
+ import { blockPositionShape } from '../schemas/coordinate-schema.js';
4
+ import { dimensionField } from '../schemas/dimension-schema.js';
5
+ import { entitySummarySchema, toEntitySummary } from '../schemas/entity-schema.js';
6
+ import { toErrorMessage } from '../utils/error-message.js';
7
+ import { formatStates, isEmptyStates, sameStates } from '../world/block-states.js';
8
+ import { parseBlockSpec } from '../world/block-spec.js';
9
+ import { SNAPSHOT_FORMAT } from '../world/stores/serialized-snapshot.js';
10
+ import { coordinateRangeError, toolError } from './result.js';
11
+ const blockStatesSchema = z.record(z.string(), z.string());
12
+ const positionSchema = z.object({
13
+ x: z.number().int(),
14
+ y: z.number().int(),
15
+ z: z.number().int()
16
+ });
17
+ const blockTypeSchema = z
18
+ .string()
19
+ .trim()
20
+ .min(1)
21
+ .max(256)
22
+ .describe('Block id, such as "minecraft:stone" or "stone"; states are optional, for example "oak_stairs[facing=south]"');
23
+ function normalizeArea(from, to) {
24
+ return {
25
+ from: {
26
+ x: Math.min(from.x, to.x),
27
+ y: Math.min(from.y, to.y),
28
+ z: Math.min(from.z, to.z)
29
+ },
30
+ to: {
31
+ x: Math.max(from.x, to.x),
32
+ y: Math.max(from.y, to.y),
33
+ z: Math.max(from.z, to.z)
34
+ }
35
+ };
36
+ }
37
+ export function registerWorldTools(server, world) {
38
+ server.registerTool('getDimension', {
39
+ title: 'Get Dimension',
40
+ 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.',
41
+ inputSchema: z.object({
42
+ dimension: dimensionField
43
+ }),
44
+ outputSchema: z.object({
45
+ key: z.string(),
46
+ type: z.string(),
47
+ displayName: z.string(),
48
+ minY: z.number().int(),
49
+ maxY: z.number().int(),
50
+ hasSkylight: z.boolean(),
51
+ modifiedBlockCount: z.number().int(),
52
+ entityCount: z.number().int(),
53
+ playerCount: z.number().int(),
54
+ blocksUri: z.string(),
55
+ dimensionsUri: z.string()
56
+ }),
57
+ annotations: {
58
+ readOnlyHint: true,
59
+ idempotentHint: true,
60
+ openWorldHint: false
61
+ }
62
+ }, async ({ dimension: dimensionType }) => {
63
+ try {
64
+ const dimension = world.getDimension(dimensionType);
65
+ const info = dimension.info;
66
+ return {
67
+ content: [
68
+ {
69
+ type: 'text',
70
+ text: `${info.displayName} (${info.key}): ${dimension.modifiedBlockCount} modified blocks, ${dimension.entityCount} entities, ${dimension.playerCount} players; Y ${info.minY} to ${info.maxY - 1}`
71
+ }
72
+ ],
73
+ structuredContent: {
74
+ key: info.key,
75
+ type: info.type,
76
+ displayName: info.displayName,
77
+ minY: info.minY,
78
+ maxY: info.maxY,
79
+ hasSkylight: info.hasSkylight,
80
+ modifiedBlockCount: dimension.modifiedBlockCount,
81
+ entityCount: dimension.entityCount,
82
+ playerCount: dimension.playerCount,
83
+ blocksUri: `lzimul://minecraft/dimensions/${info.type}`,
84
+ dimensionsUri: 'lzimul://minecraft/dimensions'
85
+ }
86
+ };
87
+ }
88
+ catch (error) {
89
+ return toolError(`getDimension failed: ${toErrorMessage(error)}`);
90
+ }
91
+ });
92
+ server.registerTool('getDimensionBlocks', {
93
+ title: 'Get Dimension Blocks',
94
+ 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.',
95
+ inputSchema: z.object({
96
+ dimension: dimensionField,
97
+ limit: z
98
+ .number()
99
+ .int()
100
+ .min(1)
101
+ .max(10_000)
102
+ .default(1_000)
103
+ .describe('Maximum number of blocks to return (1-10000, default 1000)'),
104
+ offset: z
105
+ .number()
106
+ .int()
107
+ .min(0)
108
+ .default(0)
109
+ .describe('Number of blocks to skip, for pagination (default 0)')
110
+ }),
111
+ outputSchema: z.object({
112
+ dimension: z.string(),
113
+ displayName: z.string(),
114
+ total: z.number().int(),
115
+ offset: z.number().int(),
116
+ limit: z.number().int(),
117
+ returned: z.number().int(),
118
+ hasMore: z.boolean(),
119
+ blocks: z.array(z.object({
120
+ x: z.number().int(),
121
+ y: z.number().int(),
122
+ z: z.number().int(),
123
+ block: z.string(),
124
+ displayName: z.string(),
125
+ states: blockStatesSchema
126
+ }))
127
+ }),
128
+ annotations: {
129
+ readOnlyHint: true,
130
+ idempotentHint: true,
131
+ openWorldHint: false
132
+ }
133
+ }, async ({ dimension: dimensionType, limit, offset }) => {
134
+ try {
135
+ const dimension = world.getDimension(dimensionType);
136
+ const total = dimension.modifiedBlockCount;
137
+ const page = dimension.sortedPlacedBlocks.slice(offset, offset + limit);
138
+ const returned = page.length;
139
+ const hasMore = offset + returned < total;
140
+ return {
141
+ content: [
142
+ {
143
+ type: 'text',
144
+ text: `Returned ${returned} of ${total} placed block(s) in ${dimension.info.displayName} (offset ${offset}, limit ${limit}${hasMore ? ', more available' : ''})`
145
+ }
146
+ ],
147
+ structuredContent: {
148
+ dimension: dimension.info.key,
149
+ displayName: dimension.info.displayName,
150
+ total,
151
+ offset,
152
+ limit,
153
+ returned,
154
+ hasMore,
155
+ blocks: page.map(placed => ({
156
+ x: placed.x,
157
+ y: placed.y,
158
+ z: placed.z,
159
+ block: placed.block.key,
160
+ displayName: placed.block.displayName,
161
+ states: placed.states
162
+ }))
163
+ }
164
+ };
165
+ }
166
+ catch (error) {
167
+ return toolError(`getDimensionBlocks failed: ${toErrorMessage(error)}`);
168
+ }
169
+ });
170
+ server.registerTool('getBlocksInArea', {
171
+ title: 'Get Blocks In Area',
172
+ description: 'List placed (non-air) blocks inside an inclusive box in one dimension, sorted by x, y and z. Corners may be supplied in any order. An optional blockType filter matches the block id and, when states are supplied, the exact state map. Results are paginated with limit and offset. Unmodified coordinates are air and are not listed.',
173
+ inputSchema: z.object({
174
+ dimension: dimensionField,
175
+ from: z.object(blockPositionShape),
176
+ to: z.object(blockPositionShape),
177
+ blockType: blockTypeSchema
178
+ .optional()
179
+ .describe('Optional block filter, such as "minecraft:oak_stairs" or "oak_stairs[facing=south]"'),
180
+ limit: z
181
+ .number()
182
+ .int()
183
+ .min(1)
184
+ .max(10_000)
185
+ .default(1_000)
186
+ .describe('Maximum number of matching blocks to return (1-10000, default 1000)'),
187
+ offset: z
188
+ .number()
189
+ .int()
190
+ .min(0)
191
+ .default(0)
192
+ .describe('Number of matching blocks to skip (default 0)')
193
+ }),
194
+ outputSchema: z.object({
195
+ dimension: z.string(),
196
+ displayName: z.string(),
197
+ from: positionSchema,
198
+ to: positionSchema,
199
+ blockType: z.string().nullable(),
200
+ total: z.number().int(),
201
+ offset: z.number().int(),
202
+ limit: z.number().int(),
203
+ returned: z.number().int(),
204
+ hasMore: z.boolean(),
205
+ blocks: z.array(z.object({
206
+ x: z.number().int(),
207
+ y: z.number().int(),
208
+ z: z.number().int(),
209
+ block: z.string(),
210
+ displayName: z.string(),
211
+ states: blockStatesSchema
212
+ }))
213
+ }),
214
+ annotations: {
215
+ readOnlyHint: true,
216
+ idempotentHint: true,
217
+ openWorldHint: false
218
+ }
219
+ }, async ({ dimension: dimensionType, from, to, blockType, limit, offset }) => {
220
+ try {
221
+ const dimension = world.getDimension(dimensionType);
222
+ if (!dimension.contains(from.x, from.y, from.z)) {
223
+ return coordinateRangeError(dimension.info, from.x, from.y, from.z);
224
+ }
225
+ if (!dimension.contains(to.x, to.y, to.z)) {
226
+ return coordinateRangeError(dimension.info, to.x, to.y, to.z);
227
+ }
228
+ const area = normalizeArea(from, to);
229
+ const spec = blockType ? parseBlockSpec(blockType) : null;
230
+ if (spec && !spec.ok) {
231
+ return toolError(spec.error);
232
+ }
233
+ const matches = dimension.sortedPlacedBlocks.filter(placed => {
234
+ if (placed.x < area.from.x ||
235
+ placed.x > area.to.x ||
236
+ placed.y < area.from.y ||
237
+ placed.y > area.to.y ||
238
+ placed.z < area.from.z ||
239
+ placed.z > area.to.z) {
240
+ return false;
241
+ }
242
+ if (!spec) {
243
+ return true;
244
+ }
245
+ return (placed.block.key === spec.block.key &&
246
+ (isEmptyStates(spec.states) ||
247
+ sameStates(placed.states, spec.states)));
248
+ });
249
+ const total = matches.length;
250
+ const page = matches.slice(offset, offset + limit);
251
+ const returned = page.length;
252
+ const hasMore = offset + returned < total;
253
+ const filter = spec
254
+ ? `${spec.block.key}${formatStates(spec.states)}`
255
+ : null;
256
+ return {
257
+ content: [
258
+ {
259
+ type: 'text',
260
+ text: `Returned ${returned} of ${total} placed block(s) in ${dimension.info.displayName} from (${area.from.x}, ${area.from.y}, ${area.from.z}) to (${area.to.x}, ${area.to.y}, ${area.to.z})${filter ? ` matching ${filter}` : ''} (offset ${offset}, limit ${limit}${hasMore ? ', more available' : ''})`
261
+ }
262
+ ],
263
+ structuredContent: {
264
+ dimension: dimension.info.key,
265
+ displayName: dimension.info.displayName,
266
+ from: area.from,
267
+ to: area.to,
268
+ blockType: filter,
269
+ total,
270
+ offset,
271
+ limit,
272
+ returned,
273
+ hasMore,
274
+ blocks: page.map(placed => ({
275
+ x: placed.x,
276
+ y: placed.y,
277
+ z: placed.z,
278
+ block: placed.block.key,
279
+ displayName: placed.block.displayName,
280
+ states: placed.states
281
+ }))
282
+ }
283
+ };
284
+ }
285
+ catch (error) {
286
+ return toolError(`getBlocksInArea failed: ${toErrorMessage(error)}`);
287
+ }
288
+ });
289
+ server.registerTool('fillBlocks', {
290
+ title: 'Fill Blocks',
291
+ description: 'Fill an inclusive box in one dimension with one block type and optional states. Corners may be supplied in any order. By default only air coordinates are filled; set overwrite to true to replace existing blocks. Use "minecraft:air" with overwrite to clear a region. The maximum volume per request is 16,384 blocks.',
292
+ inputSchema: z.object({
293
+ dimension: dimensionField,
294
+ from: z.object(blockPositionShape),
295
+ to: z.object(blockPositionShape),
296
+ blockType: blockTypeSchema,
297
+ overwrite: z
298
+ .boolean()
299
+ .default(false)
300
+ .describe('Replace existing non-air blocks when true; default false fills air only')
301
+ }),
302
+ outputSchema: z.object({
303
+ dimension: z.string(),
304
+ displayName: z.string(),
305
+ from: positionSchema,
306
+ to: positionSchema,
307
+ blockType: z.string(),
308
+ block: z.string(),
309
+ states: blockStatesSchema,
310
+ overwrite: z.boolean(),
311
+ volume: z.number().int(),
312
+ changed: z.number().int(),
313
+ placed: z.number().int(),
314
+ removed: z.number().int(),
315
+ skipped: z.number().int(),
316
+ modifiedBlockCount: z.number().int()
317
+ }),
318
+ annotations: {
319
+ destructiveHint: true,
320
+ idempotentHint: true,
321
+ openWorldHint: false
322
+ }
323
+ }, async ({ dimension: dimensionType, from, to, blockType, overwrite }) => {
324
+ try {
325
+ const dimension = world.getDimension(dimensionType);
326
+ if (!dimension.contains(from.x, from.y, from.z)) {
327
+ return coordinateRangeError(dimension.info, from.x, from.y, from.z);
328
+ }
329
+ if (!dimension.contains(to.x, to.y, to.z)) {
330
+ return coordinateRangeError(dimension.info, to.x, to.y, to.z);
331
+ }
332
+ const area = normalizeArea(from, to);
333
+ const volume = (area.to.x - area.from.x + 1) *
334
+ (area.to.y - area.from.y + 1) *
335
+ (area.to.z - area.from.z + 1);
336
+ if (!Number.isSafeInteger(volume) ||
337
+ volume > WORLD_LIMITS.maxFillVolume) {
338
+ return toolError(`The requested fill volume is too large (${volume}); the maximum is ${WORLD_LIMITS.maxFillVolume} blocks.`);
339
+ }
340
+ const spec = parseBlockSpec(blockType);
341
+ if (!spec.ok) {
342
+ return toolError(spec.error);
343
+ }
344
+ const isAirTarget = spec.block.key === 'minecraft:air';
345
+ if (isAirTarget && !isEmptyStates(spec.states)) {
346
+ return toolError('Air cannot carry block states. Use "minecraft:air" to remove blocks.');
347
+ }
348
+ let newBlocks = 0;
349
+ let skipped = 0;
350
+ for (let x = area.from.x; x <= area.to.x; x += 1) {
351
+ for (let y = area.from.y; y <= area.to.y; y += 1) {
352
+ for (let z = area.from.z; z <= area.to.z; z += 1) {
353
+ const existing = dimension.getBlockState(x, y, z);
354
+ const existingIsAir = existing.block.key === 'minecraft:air';
355
+ if (!overwrite && !existingIsAir) {
356
+ skipped += 1;
357
+ continue;
358
+ }
359
+ if (isAirTarget) {
360
+ if (existingIsAir) {
361
+ skipped += 1;
362
+ }
363
+ continue;
364
+ }
365
+ if (!existingIsAir &&
366
+ existing.block.key === spec.block.key &&
367
+ sameStates(existing.states, spec.states)) {
368
+ skipped += 1;
369
+ continue;
370
+ }
371
+ if (existingIsAir) {
372
+ newBlocks += 1;
373
+ }
374
+ }
375
+ }
376
+ }
377
+ if (dimension.modifiedBlockCount + newBlocks >
378
+ WORLD_LIMITS.maxModifiedBlocks) {
379
+ return toolError(`The ${dimension.info.displayName} world would exceed the maximum of ${WORLD_LIMITS.maxModifiedBlocks} modified blocks.`);
380
+ }
381
+ let placed = 0;
382
+ let removed = 0;
383
+ let changed = 0;
384
+ for (let x = area.from.x; x <= area.to.x; x += 1) {
385
+ for (let y = area.from.y; y <= area.to.y; y += 1) {
386
+ for (let z = area.from.z; z <= area.to.z; z += 1) {
387
+ const existing = dimension.getBlockState(x, y, z);
388
+ const existingIsAir = existing.block.key === 'minecraft:air';
389
+ if (!overwrite && !existingIsAir) {
390
+ continue;
391
+ }
392
+ if (isAirTarget) {
393
+ if (!existingIsAir) {
394
+ dimension.setBlock(x, y, z, spec.block, spec.states);
395
+ removed += 1;
396
+ changed += 1;
397
+ }
398
+ continue;
399
+ }
400
+ if (!existingIsAir &&
401
+ existing.block.key === spec.block.key &&
402
+ sameStates(existing.states, spec.states)) {
403
+ continue;
404
+ }
405
+ dimension.setBlock(x, y, z, spec.block, spec.states);
406
+ placed += 1;
407
+ changed += 1;
408
+ }
409
+ }
410
+ }
411
+ if (changed > 0) {
412
+ try {
413
+ await dimension.flush();
414
+ }
415
+ catch (error) {
416
+ return toolError(`The region was changed but could not be persisted to the world store: ${toErrorMessage(error)}`);
417
+ }
418
+ }
419
+ const normalizedType = `${spec.block.key}${formatStates(spec.states)}`;
420
+ return {
421
+ content: [
422
+ {
423
+ type: 'text',
424
+ text: `Filled ${changed} of ${volume} coordinate(s) in ${dimension.info.displayName} with ${normalizedType} (${placed} placed, ${removed} removed, ${skipped} skipped)`
425
+ }
426
+ ],
427
+ structuredContent: {
428
+ dimension: dimension.info.key,
429
+ displayName: dimension.info.displayName,
430
+ from: area.from,
431
+ to: area.to,
432
+ blockType: normalizedType,
433
+ block: spec.block.key,
434
+ states: spec.states,
435
+ overwrite,
436
+ volume,
437
+ changed,
438
+ placed,
439
+ removed,
440
+ skipped,
441
+ modifiedBlockCount: dimension.modifiedBlockCount
442
+ }
443
+ };
444
+ }
445
+ catch (error) {
446
+ return toolError(`fillBlocks failed: ${toErrorMessage(error)}`);
447
+ }
448
+ });
449
+ server.registerTool('getDimensionEntities', {
450
+ title: 'Get Dimension Entities',
451
+ description: 'List every entity and player in one dimension, sorted by UUID. Each row contains the entity UUID, kind, type, display name, name, position and dimension. Use getEntity or getInventory for one entity and its full inventory.',
452
+ inputSchema: z.object({
453
+ dimension: dimensionField
454
+ }),
455
+ outputSchema: z.object({
456
+ dimension: z.string(),
457
+ displayName: z.string(),
458
+ entityCount: z.number().int(),
459
+ playerCount: z.number().int(),
460
+ entities: z.array(entitySummarySchema)
461
+ }),
462
+ annotations: {
463
+ readOnlyHint: true,
464
+ idempotentHint: true,
465
+ openWorldHint: false
466
+ }
467
+ }, async ({ dimension: dimensionType }) => {
468
+ try {
469
+ const dimension = world.getDimension(dimensionType);
470
+ const entities = dimension.sortedPlacedEntities.map(toEntitySummary);
471
+ return {
472
+ content: [
473
+ {
474
+ type: 'text',
475
+ text: `${dimension.info.displayName} (${dimension.info.key}) holds ${entities.length} entities and ${dimension.playerCount} players`
476
+ }
477
+ ],
478
+ structuredContent: {
479
+ dimension: dimension.info.key,
480
+ displayName: dimension.info.displayName,
481
+ entityCount: entities.length,
482
+ playerCount: dimension.playerCount,
483
+ entities
484
+ }
485
+ };
486
+ }
487
+ catch (error) {
488
+ return toolError(`getDimensionEntities failed: ${toErrorMessage(error)}`);
489
+ }
490
+ });
491
+ server.registerTool('getDimensionRawFile', {
492
+ title: 'Get Dimension Raw File',
493
+ description: 'Return the raw JSON snapshot of one dimension, including its top-level blocks and entities arrays. The result is the on-disk JSON for file storage, or the equivalent serialized state for memory storage, and can be loaded directly by minecraft_world_viewer.html.',
494
+ inputSchema: z.object({
495
+ dimension: dimensionField
496
+ }),
497
+ outputSchema: z.object({
498
+ dimension: z.string(),
499
+ format: z.string(),
500
+ bytes: z.number().int(),
501
+ raw: z.string()
502
+ }),
503
+ annotations: {
504
+ readOnlyHint: true,
505
+ idempotentHint: true,
506
+ openWorldHint: false
507
+ }
508
+ }, async ({ dimension: dimensionType }) => {
509
+ try {
510
+ const dimension = world.getDimension(dimensionType);
511
+ const raw = await dimension.readRaw();
512
+ return {
513
+ content: [{ type: 'text', text: raw }],
514
+ structuredContent: {
515
+ dimension: dimension.info.key,
516
+ format: SNAPSHOT_FORMAT,
517
+ bytes: Buffer.byteLength(raw, 'utf8'),
518
+ raw
519
+ }
520
+ };
521
+ }
522
+ catch (error) {
523
+ return toolError(`getDimensionRawFile failed: ${toErrorMessage(error)}`);
524
+ }
525
+ });
526
+ }
@@ -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);
@@ -26,6 +26,7 @@ export function parseBlockSpec(input) {
26
26
  };
27
27
  }
28
28
  const states = {};
29
+ let stateCount = 0;
29
30
  if (typeof rawStates === 'string') {
30
31
  if (rawStates.trim().length === 0) {
31
32
  return {
@@ -62,12 +63,13 @@ export function parseBlockSpec(input) {
62
63
  };
63
64
  }
64
65
  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
- };
66
+ stateCount += 1;
67
+ if (stateCount > MAX_STATES) {
68
+ return {
69
+ ok: false,
70
+ error: `Too many block states in "${input}" (max ${MAX_STATES}).`
71
+ };
72
+ }
71
73
  }
72
74
  }
73
75
  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
  }