@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.
- package/LICENSE +21 -21
- package/README.md +126 -67
- package/SECURITY.md +38 -21
- package/build/config/server-info.js +5 -4
- package/build/config/storage.js +2 -1
- package/build/config/world-limits.js +6 -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/enums/move-direction.enum.js +9 -0
- package/build/index.js +4 -3
- 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 +24 -7
- 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 +638 -0
- package/build/tools/index.js +4 -0
- package/build/tools/result.js +3 -5
- package/build/tools/world-tools.js +526 -0
- package/build/utils/logger.js +53 -0
- package/build/world/block-spec.js +8 -6
- package/build/world/block-states.js +23 -4
- package/build/world/entity-inventory.js +101 -0
- package/build/world/item-spec.js +25 -0
- package/build/world/simulated-dimension.js +314 -14
- package/build/world/stores/file-world-store.js +84 -29
- package/build/world/stores/memory-world-store.js +18 -5
- package/build/world/stores/serialized-block.js +9 -14
- package/build/world/stores/serialized-entity.js +75 -0
- package/build/world/stores/serialized-snapshot.js +35 -0
- package/build/world/world.js +33 -1
- package/lib/app/server.d.ts.map +1 -1
- package/lib/config/dimensions.d.ts.map +1 -1
- package/lib/config/server-info.d.ts.map +1 -1
- package/lib/config/storage.d.ts.map +1 -1
- package/lib/config/world-limits.d.ts +5 -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/bounding-box.enum.d.ts.map +1 -1
- package/lib/enums/dimension-type.enum.d.ts.map +1 -1
- package/lib/enums/entity-kind.enum.d.ts +5 -0
- package/lib/enums/entity-kind.enum.d.ts.map +1 -0
- package/lib/enums/icon-theme.enum.d.ts.map +1 -1
- package/lib/enums/inventory-section.enum.d.ts +5 -0
- package/lib/enums/inventory-section.enum.d.ts.map +1 -0
- package/lib/enums/move-direction.enum.d.ts +9 -0
- package/lib/enums/move-direction.enum.d.ts.map +1 -0
- package/lib/interfaces/block.interface.d.ts.map +1 -1
- package/lib/interfaces/data-entry.interface.d.ts.map +1 -1
- package/lib/interfaces/dimension.interface.d.ts.map +1 -1
- 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/item.interface.d.ts.map +1 -1
- package/lib/interfaces/placed-block.interface.d.ts.map +1 -1
- package/lib/interfaces/world-store.interface.d.ts +8 -2
- package/lib/interfaces/world-store.interface.d.ts.map +1 -1
- package/lib/resources/block-resources.d.ts.map +1 -1
- package/lib/resources/data-resources.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/resources/item-resources.d.ts.map +1 -1
- package/lib/schemas/block-schema.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/schemas/item-schema.d.ts.map +1 -1
- 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/item-tools.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-spec.d.ts.map +1 -1
- 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 +54 -0
- package/lib/world/simulated-dimension.d.ts.map +1 -1
- package/lib/world/stores/file-world-store.d.ts +4 -4
- package/lib/world/stores/file-world-store.d.ts.map +1 -1
- package/lib/world/stores/index.d.ts.map +1 -1
- package/lib/world/stores/memory-world-store.d.ts +4 -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/stores/serialized-snapshot.d.ts +6 -0
- package/lib/world/stores/serialized-snapshot.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 +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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
}
|