@mairwunnx/minecraft-mcp 3.1.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.RU.md +2 -2
- package/README.md +2 -2
- package/dist/prompts.js +8 -8
- package/dist/scopes.js +1 -0
- package/dist/server.js +3 -1
- package/dist/tools/blocks.js +88 -98
- package/dist/tools/entities.js +19 -26
- package/dist/tools/files.js +38 -46
- package/dist/tools/items.js +108 -0
- package/dist/tools/players.js +92 -120
- package/dist/tools/scripts.js +5 -12
- package/dist/tools/server.js +115 -122
- package/dist/tools/stats.js +22 -18
- package/dist/tools/worlds.js +93 -86
- package/dist/version.js +1 -1
- package/openapi/worldapi.json +242 -1
- package/package.json +1 -1
package/README.RU.md
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
## Что вы получите
|
|
12
12
|
|
|
13
|
-
- 🧰 **
|
|
13
|
+
- 🧰 **46 инструментов** для сервера, миров, блоков, сундуков, сущностей, игроков, команд, рейтингов, рецептов, конфигов, логов и Java-скриптов. Большинство принимает сразу несколько целей, поэтому агент делает меньше вызовов.
|
|
14
14
|
- 🎒 **Инвентари в подробностях**: чары, прочность и эффекты каждого предмета, а ещё то, что лежит в шалкерах и мешках.
|
|
15
15
|
- 📜 **Конфиги и логи**: агент читает консоль и любой конфиг, а с подходящим токеном меняет значение и сохраняет комментарии.
|
|
16
|
-
- 🔑 **Только то, что позволяет токен**: с токеном `viewer` агент видит
|
|
16
|
+
- 🔑 **Только то, что позволяет токен**: с токеном `viewer` агент видит 17 инструментов, которые только смотрят, а режим «только чтение» прячет всё, что меняет сервер.
|
|
17
17
|
- 🙋 **«Я» — это вы**: укажите свой ник, и вопрос «где я?» просто сработает.
|
|
18
18
|
- ⏳ **Живые события**: «скажи, когда Стив зайдёт».
|
|
19
19
|
- 🛡️ **Безопасность с самого начала**: текст из игры помечен как данные, инструменты предупреждают, когда что-то разрушают, а токен никуда не утекает.
|
package/README.md
CHANGED
|
@@ -10,10 +10,10 @@ It runs next to your AI client—Claude Desktop, Claude Code, Cursor, VS Code, o
|
|
|
10
10
|
|
|
11
11
|
## What you get
|
|
12
12
|
|
|
13
|
-
- 🧰 **
|
|
13
|
+
- 🧰 **46 tools** for the server, worlds, blocks, chests, entities, players, commands, leaderboards, recipes, configs, logs, and Java scripts. Most take several targets at once, so the agent makes fewer calls.
|
|
14
14
|
- 🎒 **Inventories in depth**: every item’s enchantments, durability, and potion effects, and what’s inside shulker boxes and bundles.
|
|
15
15
|
- 📜 **Configs and logs**: the agent reads the console and any config, and with the right token changes a value and keeps its comments.
|
|
16
|
-
- 🔑 **Only what your token allows**: a viewer token shows
|
|
16
|
+
- 🔑 **Only what your token allows**: a viewer token shows 17 tools that only look, and read-only mode hides every write.
|
|
17
17
|
- 🙋 **“Me” means you**: set your player name, and “where am I?” just works.
|
|
18
18
|
- ⏳ **Live events**: “tell me when Steve joins”.
|
|
19
19
|
- 🛡️ **Safe by design**: game text is fenced as data, tools say when they destroy something, and the token never leaks.
|
package/dist/prompts.js
CHANGED
|
@@ -11,13 +11,13 @@ export function registerPrompts(server) {
|
|
|
11
11
|
}, ({ player }) => ask([
|
|
12
12
|
`Where is ${player} in the Minecraft world, and what is near them?`,
|
|
13
13
|
"Use get_player for their world, coordinates, health, and game mode. If they are offline, say when they were last seen and where they logged out.",
|
|
14
|
-
"If they are online, use
|
|
14
|
+
"If they are online, use get_block at their feet (it names the biome), get_entity_summary within 32 blocks, and locate with kind structure for a village nearby.",
|
|
15
15
|
"Answer in a few sentences a friend could act on: the dimension, rough coordinates, biome, threats or animals around, and the nearest landmark.",
|
|
16
16
|
]));
|
|
17
17
|
server.registerPrompt("server_health_check", { title: "Server health check", description: "Check performance, load, and recent trouble" }, () => ask([
|
|
18
18
|
"Check the health of the Minecraft server.",
|
|
19
|
-
"Use get_server_status with
|
|
20
|
-
"Use
|
|
19
|
+
"Use get_server_status with include performance (and host, if the token has system:read): TPS should be near 20 and the p95 tick time under 50 ms; heap use near the max and long garbage collection times are warning signs. With host, check free disk, RAM, and the container's memory limit.",
|
|
20
|
+
"Use get_world without a world, then with include size, to see which world carries the most chunks and entities, and get_events for recent server.status changes, kicks, and deaths.",
|
|
21
21
|
"If get_server_log is there, read its warnings and errors (level warn).",
|
|
22
22
|
"Report what is fine, what needs attention, and one concrete next step for each problem. Don't change anything.",
|
|
23
23
|
]));
|
|
@@ -27,8 +27,8 @@ export function registerPrompts(server) {
|
|
|
27
27
|
argsSchema: { world: z.string().optional().describe("Look at this world only") },
|
|
28
28
|
}, ({ world }) => ask([
|
|
29
29
|
`Find out why the Minecraft server lags${world ? ` in ${world}` : ""}, and where.`,
|
|
30
|
-
"1. get_server_status with
|
|
31
|
-
`2. get_entity_summary for ${world ?? "the busiest world"}: counts by type, items on the ground, villagers, and the ten busiest chunks with their hoppers. Then list_chunks with sort tileEntities. Look closer at the top three chunks with
|
|
30
|
+
"1. get_server_status with include performance: TPS below 20 or a p95 tick time over 50 ms means the server can't keep up. Note which world has the most entities and block entities, and its most common entity types.",
|
|
31
|
+
`2. get_entity_summary for ${world ?? "the busiest world"}: counts by type, items on the ground, villagers, and the ten busiest chunks with their hoppers. Then list_chunks with sort tileEntities. Look closer at the top three chunks with list_chunks and chunk: what entities and block entities sit there, whether the chunk is force-loaded, how long players stayed.`,
|
|
32
32
|
"3. If get_server_log is there, search it for “Can't keep up”, warnings, and errors, and for plugins that show up often.",
|
|
33
33
|
"4. Name the likely causes, such as dropped items piling up, a mob farm without a kill chamber, crowds of villagers, hopper chains, or force-loaded chunks nobody needs, with the chunk coordinates (block coordinates are chunk × 16) and who was near, if list_entities with the chunk or get_player tells.",
|
|
34
34
|
"Suggest fixes, cheapest first. Change nothing yourself: before any remove_entity, show its dryRun and ask the user.",
|
|
@@ -39,8 +39,8 @@ export function registerPrompts(server) {
|
|
|
39
39
|
argsSchema: { player: z.string().describe("Player name") },
|
|
40
40
|
}, ({ player }) => ask([
|
|
41
41
|
`Write ${player} a short, friendly guide to what to do next on this Minecraft server.`,
|
|
42
|
-
"1. get_player with include statistics and deaths for where they are, their state, how long they have played, and how they died;
|
|
43
|
-
"2.
|
|
42
|
+
"1. get_player with include statistics and deaths for where they are, their state, how long they have played, and how they died; statistics `mined` and `killed` for what they mined and killed.",
|
|
43
|
+
"2. get_player with advancements todo: pick three next goals whose parent they already finished, and say how to get each.",
|
|
44
44
|
"3. locate around them: blocks such as logs and coal and iron ores, a biome worth a visit, and the nearest village. get_world for how soon night falls and whether they can sleep now.",
|
|
45
45
|
"4. get_recipes for anything the goals need that they may not know how to make, such as a bed, a shield, or a furnace.",
|
|
46
46
|
"5. If the server runs plugins players use (list_plugins), mention the ones that help newcomers, such as land claims.",
|
|
@@ -56,7 +56,7 @@ export function registerPrompts(server) {
|
|
|
56
56
|
}, ({ player, where }) => ask([
|
|
57
57
|
`Help find out who damaged ${where ? `the build at ${where}` : "a build"}${player ? `, and whether ${player} had a part in it` : ""}. Be fair: collect evidence, don't accuse.`,
|
|
58
58
|
"WorldAPI doesn't log who broke or placed which block in the past; it only streams block.break and block.place live. Say so if the answer needs that history, and suggest a block logger such as CoreProtect.",
|
|
59
|
-
"1. get_events for joins, leaves, and deaths around the time: they show who was online when.
|
|
59
|
+
"1. get_events for joins, leaves, and deaths around the time: they show who was online when. get_events with player.chat, if the token reads chat, for what people said.",
|
|
60
60
|
"2. If get_server_log is there, search it for the players' names: it holds the commands they ran and what protection plugins logged, such as claims and denied actions. For days before the server's last start, read_file the logs under logs/ with grep.",
|
|
61
61
|
where
|
|
62
62
|
? "3. At the place: find_containers around it and get_block on chests and signs (contents and text), get_entity_summary and list_entities around it for dropped items, TNT, or mobs, and get_player for who logged out nearby."
|
package/dist/scopes.js
CHANGED
package/dist/server.js
CHANGED
|
@@ -6,6 +6,7 @@ import { blockTools } from "./tools/blocks.js";
|
|
|
6
6
|
import { entityTools } from "./tools/entities.js";
|
|
7
7
|
import { extensionTools } from "./tools/extensions.js";
|
|
8
8
|
import { fileTools } from "./tools/files.js";
|
|
9
|
+
import { itemTools } from "./tools/items.js";
|
|
9
10
|
import { playerTools } from "./tools/players.js";
|
|
10
11
|
import { scriptTools } from "./tools/scripts.js";
|
|
11
12
|
import { serverTools } from "./tools/server.js";
|
|
@@ -17,7 +18,7 @@ import { VERSION } from "./version.js";
|
|
|
17
18
|
const MISSING = [
|
|
18
19
|
[
|
|
19
20
|
"players:write",
|
|
20
|
-
"send chat messages, titles, or sounds (
|
|
21
|
+
"send chat messages, titles, or sounds (send_message), teleport players, or change their inventories and effects",
|
|
21
22
|
],
|
|
22
23
|
["world:write", "change time, weather, game rules, blocks, containers, or entities"],
|
|
23
24
|
["server:console", "run console commands"],
|
|
@@ -61,6 +62,7 @@ export function buildServer(config, scopes, feed, fetchImpl) {
|
|
|
61
62
|
blockTools(ctx);
|
|
62
63
|
entityTools(ctx);
|
|
63
64
|
playerTools(ctx);
|
|
65
|
+
itemTools(ctx);
|
|
64
66
|
statsTools(ctx);
|
|
65
67
|
fileTools(ctx);
|
|
66
68
|
scriptTools(ctx);
|
package/dist/tools/blocks.js
CHANGED
|
@@ -1,103 +1,124 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { unwrap } from "../client.js";
|
|
3
3
|
import { tool } from "../tool.js";
|
|
4
|
-
import { blockPosition, cursor, ITEM_DETAIL,
|
|
4
|
+
import { blockPosition, cursor, ITEM_DETAIL, limit, world } from "./common.js";
|
|
5
5
|
const xyz = { x: z.number().int(), y: z.number().int(), z: z.number().int() };
|
|
6
6
|
export function blockTools(ctx) {
|
|
7
7
|
const { api, token } = ctx;
|
|
8
|
-
const at = (args) => ({
|
|
9
|
-
params: { path: { world: args.world, x: args.x, y: args.y, z: args.z } },
|
|
10
|
-
});
|
|
11
8
|
tool(ctx, {
|
|
12
9
|
name: "get_block",
|
|
13
10
|
untrusted: true,
|
|
14
|
-
title: "
|
|
15
|
-
description: `
|
|
16
|
-
scope: "server:read",
|
|
17
|
-
effect: "read",
|
|
18
|
-
input: { world, ...xyz },
|
|
19
|
-
run: (args) => unwrap(api.GET("/worlds/{world}/blocks/{x}/{y}/{z}", at(args)), token),
|
|
20
|
-
});
|
|
21
|
-
tool(ctx, {
|
|
22
|
-
name: "get_biome",
|
|
23
|
-
title: "Biome",
|
|
24
|
-
description: "The biome at a block, with temperature and humidity.",
|
|
25
|
-
scope: "server:read",
|
|
26
|
-
effect: "read",
|
|
27
|
-
input: { world, ...xyz },
|
|
28
|
-
run: (args) => unwrap(api.GET("/worlds/{world}/biomes/{x}/{y}/{z}", at(args)), token),
|
|
29
|
-
});
|
|
30
|
-
tool(ctx, {
|
|
31
|
-
name: "get_highest_block",
|
|
32
|
-
title: "Surface height",
|
|
33
|
-
description: "The highest block in a column; handy to find safe ground before teleporting.",
|
|
11
|
+
title: "Blocks",
|
|
12
|
+
description: `Blocks at up to 16 positions in one call: type, full state, light, biome, redstone power, and what it holds: sign text, spawner settings, and a container's slots (chests, barrels, shulker boxes, hoppers, furnaces, brewing stands, crafters, copper chests, chiseled bookshelves; a double chest shows both halves as one, left half first). Leave out y for the highest block of the column instead, the surface to land on before a teleport (heightmap picks which). Container contents need players:private. ${ITEM_DETAIL} Chunks that were never generated give \`chunk_not_generated\`.`,
|
|
34
13
|
scope: "server:read",
|
|
35
14
|
effect: "read",
|
|
36
15
|
input: {
|
|
37
16
|
world,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
17
|
+
positions: z
|
|
18
|
+
.array(z.object({ x: z.number().int(), y: z.number().int().optional(), z: z.number().int() }))
|
|
19
|
+
.min(1)
|
|
20
|
+
.max(16)
|
|
21
|
+
.describe("Such as [{ x: 10, y: 64, z: -3 }]; without y, the column's highest block"),
|
|
22
|
+
heightmap: z
|
|
23
|
+
.enum(["motion_blocking", "motion_blocking_no_leaves", "ocean_floor", "world_surface"])
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("For positions without y"),
|
|
26
|
+
},
|
|
27
|
+
run: async (args) => {
|
|
28
|
+
const read = (at) => at.y === undefined
|
|
29
|
+
? unwrap(api.GET("/worlds/{world}/highest/{x}/{z}", {
|
|
30
|
+
params: { path: { world: args.world, x: at.x, z: at.z }, query: { heightmap: args.heightmap } },
|
|
31
|
+
}), token)
|
|
32
|
+
: unwrap(api.GET("/worlds/{world}/blocks/{x}/{y}/{z}", {
|
|
33
|
+
params: { path: { world: args.world, x: at.x, y: at.y, z: at.z } },
|
|
34
|
+
}), token);
|
|
35
|
+
if (args.positions.length === 1 && args.positions[0])
|
|
36
|
+
return read(args.positions[0]);
|
|
37
|
+
// One failed position, such as an ungenerated chunk, doesn't sink the rest.
|
|
38
|
+
return Promise.all(args.positions.map((at) => read(at).catch((error) => ({ ...at, error: error.code ?? error.message }))));
|
|
41
39
|
},
|
|
42
|
-
run: (args) => unwrap(api.GET("/worlds/{world}/highest/{x}/{z}", {
|
|
43
|
-
params: { path: { world: args.world, x: args.x, z: args.z }, query: { heightmap: args.heightmap } },
|
|
44
|
-
}), token),
|
|
45
40
|
});
|
|
46
41
|
tool(ctx, {
|
|
47
42
|
name: "list_chunks",
|
|
48
|
-
title: "
|
|
49
|
-
description: "Loaded chunks with entity and block entity counts. sort `entities` or `tileEntities` puts the busiest first, which is where lag usually sits. A chunk is 16×16 blocks; chunk X is block X divided by 16, rounded down.",
|
|
43
|
+
title: "Chunks",
|
|
44
|
+
description: "Loaded chunks with entity and block entity counts. sort `entities` or `tileEntities` puts the busiest first, which is where lag usually sits. With chunk, that one chunk in detail: entities and block entities by type, how long players spent there, slime chunk, force-loaded. A chunk is 16×16 blocks; chunk X is block X divided by 16, rounded down.",
|
|
50
45
|
scope: "server:read",
|
|
51
46
|
effect: "read",
|
|
52
47
|
input: {
|
|
53
48
|
world,
|
|
49
|
+
chunk: z
|
|
50
|
+
.object({ x: z.number().int(), z: z.number().int() })
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("One chunk, by chunk coordinates"),
|
|
54
53
|
sort: z.enum(["coords", "entities", "tileEntities"]).optional().describe("`coords` by default"),
|
|
55
54
|
limit,
|
|
56
55
|
cursor,
|
|
57
56
|
},
|
|
58
|
-
run: (args) =>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
input: { world, chunkX: z.number().int(), chunkZ: z.number().int() },
|
|
69
|
-
run: (args) => unwrap(api.GET("/worlds/{world}/chunks/{chunkX}/{chunkZ}", {
|
|
70
|
-
params: { path: { world: args.world, chunkX: args.chunkX, chunkZ: args.chunkZ } },
|
|
71
|
-
}), token),
|
|
72
|
-
});
|
|
73
|
-
tool(ctx, {
|
|
74
|
-
name: "set_block",
|
|
75
|
-
title: "Set a block",
|
|
76
|
-
description: "Place one block. `block` is a key or full state: `stone`, `minecraft:oak_stairs[facing=east]`. physics updates neighbors (sand falls, water flows).",
|
|
77
|
-
scope: "world:write",
|
|
78
|
-
effect: "idempotent",
|
|
79
|
-
input: { world, ...xyz, block: z.string().min(1), physics: z.boolean().default(false) },
|
|
80
|
-
run: (args) => unwrap(api.PUT("/worlds/{world}/blocks/{x}/{y}/{z}", {
|
|
81
|
-
...at(args),
|
|
82
|
-
body: { block: args.block, physics: args.physics },
|
|
83
|
-
}), token),
|
|
57
|
+
run: (args) => args.chunk
|
|
58
|
+
? unwrap(api.GET("/worlds/{world}/chunks/{chunkX}/{chunkZ}", {
|
|
59
|
+
params: { path: { world: args.world, chunkX: args.chunk.x, chunkZ: args.chunk.z } },
|
|
60
|
+
}), token)
|
|
61
|
+
: unwrap(api.GET("/worlds/{world}/chunks", {
|
|
62
|
+
params: {
|
|
63
|
+
path: { world: args.world },
|
|
64
|
+
query: { sort: args.sort, limit: args.limit, cursor: args.cursor },
|
|
65
|
+
},
|
|
66
|
+
}), token),
|
|
84
67
|
});
|
|
85
68
|
tool(ctx, {
|
|
86
|
-
name: "
|
|
87
|
-
title: "
|
|
88
|
-
description: "
|
|
69
|
+
name: "set_blocks",
|
|
70
|
+
title: "Place blocks",
|
|
71
|
+
description: "Place up to 64 single blocks (`blocks`), or fill a box between two corners, inclusive (`box`). A block is a key or full state: `stone`, `minecraft:oak_stairs[facing=east]`. Box modes: `replace` sets everything, `keep` only fills air, `replace_type` only changes blocks of type `replace`; the server caps the volume (32768 blocks by default). physics updates neighbors (sand falls, water flows). What was there is gone, so confirm big changes with the user.",
|
|
89
72
|
scope: "world:write",
|
|
90
73
|
effect: "destructive",
|
|
91
74
|
input: {
|
|
92
75
|
world,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
76
|
+
blocks: z
|
|
77
|
+
.array(z.object({ ...xyz, block: z.string().min(1) }))
|
|
78
|
+
.min(1)
|
|
79
|
+
.max(64)
|
|
80
|
+
.optional(),
|
|
81
|
+
box: z
|
|
82
|
+
.object({
|
|
83
|
+
from: blockPosition,
|
|
84
|
+
to: blockPosition,
|
|
85
|
+
block: z.string().min(1),
|
|
86
|
+
mode: z.enum(["replace", "keep", "replace_type"]).default("replace"),
|
|
87
|
+
replace: z.string().optional().describe("For replace_type: the block type to change"),
|
|
88
|
+
})
|
|
89
|
+
.optional(),
|
|
98
90
|
physics: z.boolean().default(false),
|
|
99
91
|
},
|
|
100
|
-
run: (
|
|
92
|
+
run: async (args) => {
|
|
93
|
+
if (!args.blocks && !args.box)
|
|
94
|
+
throw new Error("Give blocks or box");
|
|
95
|
+
const done = {};
|
|
96
|
+
if (args.box)
|
|
97
|
+
done.box = await unwrap(api.POST("/worlds/{world}/blocks/fill", {
|
|
98
|
+
params: { path: { world: args.world } },
|
|
99
|
+
body: { ...args.box, physics: args.physics },
|
|
100
|
+
}), token);
|
|
101
|
+
if (args.blocks) {
|
|
102
|
+
let placed = 0;
|
|
103
|
+
const failed = [];
|
|
104
|
+
for (const { x, y, z: bz, block } of args.blocks) {
|
|
105
|
+
try {
|
|
106
|
+
await unwrap(api.PUT("/worlds/{world}/blocks/{x}/{y}/{z}", {
|
|
107
|
+
params: { path: { world: args.world, x, y, z: bz } },
|
|
108
|
+
body: { block, physics: args.physics },
|
|
109
|
+
}), token);
|
|
110
|
+
placed++;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
failed.push({ x, y, z: bz, error: error.code ?? error.message });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
done.placed = placed;
|
|
117
|
+
if (failed.length)
|
|
118
|
+
done.failed = failed;
|
|
119
|
+
}
|
|
120
|
+
return done;
|
|
121
|
+
},
|
|
101
122
|
});
|
|
102
123
|
tool(ctx, {
|
|
103
124
|
name: "force_load_chunk",
|
|
@@ -144,36 +165,5 @@ export function blockTools(ctx) {
|
|
|
144
165
|
},
|
|
145
166
|
}), token),
|
|
146
167
|
});
|
|
147
|
-
tool(ctx, {
|
|
148
|
-
name: "edit_container",
|
|
149
|
-
title: "Edit a container",
|
|
150
|
-
description: "Change what a chest, barrel, or other container block holds: `give` adds an item where it fits (what doesn't fit is lost and counted in `dropped`); `set` puts an item in a slot, replacing what was there; `clear_slot` empties one slot; `clear` empties the whole container. Slots count from 0; a double chest has 54, the left half first. Cleared items are gone. Needs world:write.",
|
|
151
|
-
scope: "world:write",
|
|
152
|
-
effect: "destructive",
|
|
153
|
-
input: {
|
|
154
|
-
world,
|
|
155
|
-
...xyz,
|
|
156
|
-
action: z.enum(["give", "set", "clear_slot", "clear"]),
|
|
157
|
-
slot: z.number().int().min(0).optional().describe("For set and clear_slot"),
|
|
158
|
-
item: item.optional().describe("For give and set"),
|
|
159
|
-
},
|
|
160
|
-
run: (args) => {
|
|
161
|
-
if (args.action === "clear")
|
|
162
|
-
return unwrap(api.DELETE("/worlds/{world}/blocks/{x}/{y}/{z}/container", at(args)), token);
|
|
163
|
-
if (args.action === "give") {
|
|
164
|
-
if (!args.item)
|
|
165
|
-
throw new Error("give needs item");
|
|
166
|
-
return unwrap(api.POST("/worlds/{world}/blocks/{x}/{y}/{z}/container/give", { ...at(args), body: { item: args.item } }), token);
|
|
167
|
-
}
|
|
168
|
-
if (args.slot === undefined)
|
|
169
|
-
throw new Error(`${args.action} needs slot`);
|
|
170
|
-
const params = { params: { path: { world: args.world, x: args.x, y: args.y, z: args.z, slot: args.slot } } };
|
|
171
|
-
if (args.action === "clear_slot")
|
|
172
|
-
return unwrap(api.DELETE("/worlds/{world}/blocks/{x}/{y}/{z}/container/slots/{slot}", params), token);
|
|
173
|
-
if (!args.item)
|
|
174
|
-
throw new Error("set needs item");
|
|
175
|
-
return unwrap(api.PATCH("/worlds/{world}/blocks/{x}/{y}/{z}/container/slots/{slot}", { ...params, body: args.item }), token);
|
|
176
|
-
},
|
|
177
|
-
});
|
|
178
168
|
}
|
|
179
169
|
//# sourceMappingURL=blocks.js.map
|
package/dist/tools/entities.js
CHANGED
|
@@ -31,11 +31,12 @@ export function entityTools(ctx) {
|
|
|
31
31
|
name: "list_entities",
|
|
32
32
|
untrusted: true,
|
|
33
33
|
title: "Entities",
|
|
34
|
-
description:
|
|
34
|
+
description: `Loaded entities in a world, 25 to a page, filtered by type, by a radius around x, y, z (nearest first), by name, or by chunk. Only loaded chunks count: entities far from players aren't listed. For a crowded world, get_entity_summary first; limit 0 only counts. With uuid, that one entity in detail: position, health, equipment, effects, owner, tags, leash holder, spawn reason, whether it despawns far from players or came from a spawner, a villager's profession, level, and trades, and a dropped item's stack, age, and pickup delay. ${ITEM_DETAIL}`,
|
|
35
35
|
scope: "server:read",
|
|
36
36
|
effect: "read",
|
|
37
37
|
input: {
|
|
38
38
|
world,
|
|
39
|
+
uuid: z.string().optional().describe("One entity in detail; the other filters don't apply"),
|
|
39
40
|
types: z.array(z.string()).optional().describe('Entity keys, such as ["minecraft:zombie"]'),
|
|
40
41
|
x: z.number().optional(),
|
|
41
42
|
y: z.number().optional(),
|
|
@@ -53,32 +54,24 @@ export function entityTools(ctx) {
|
|
|
53
54
|
limit,
|
|
54
55
|
cursor,
|
|
55
56
|
},
|
|
56
|
-
run: (args) =>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
run: (args) => args.uuid
|
|
58
|
+
? unwrap(api.GET("/worlds/{world}/entities/{uuid}", one({ world: args.world, uuid: args.uuid })), token)
|
|
59
|
+
: unwrap(api.GET("/worlds/{world}/entities", {
|
|
60
|
+
params: {
|
|
61
|
+
path: { world: args.world },
|
|
62
|
+
query: {
|
|
63
|
+
type: args.types?.join(","),
|
|
64
|
+
x: args.x,
|
|
65
|
+
y: args.y,
|
|
66
|
+
z: args.z,
|
|
67
|
+
radius: args.radius,
|
|
68
|
+
name: args.name,
|
|
69
|
+
chunk: args.chunk && `${args.chunk.x},${args.chunk.z}`,
|
|
70
|
+
limit: args.limit,
|
|
71
|
+
cursor: args.cursor,
|
|
72
|
+
},
|
|
69
73
|
},
|
|
70
|
-
},
|
|
71
|
-
}), token),
|
|
72
|
-
});
|
|
73
|
-
tool(ctx, {
|
|
74
|
-
name: "get_entity",
|
|
75
|
-
untrusted: true,
|
|
76
|
-
title: "Entity",
|
|
77
|
-
description: `One entity in detail: position, health, equipment, effects, owner, tags, leash holder, spawn reason, whether it despawns far from players or came from a spawner, a villager's profession, level, and trades, and a dropped item's stack, age, and pickup delay. ${ITEM_DETAIL}`,
|
|
78
|
-
scope: "server:read",
|
|
79
|
-
effect: "read",
|
|
80
|
-
input: { world, uuid: z.string() },
|
|
81
|
-
run: (args) => unwrap(api.GET("/worlds/{world}/entities/{uuid}", one(args)), token),
|
|
74
|
+
}), token),
|
|
82
75
|
});
|
|
83
76
|
tool(ctx, {
|
|
84
77
|
name: "spawn_entity",
|
package/dist/tools/files.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { unwrap } from "../client.js";
|
|
3
3
|
import { tool } from "../tool.js";
|
|
4
4
|
const PATHS = "Paths are relative to the server folder, with forward slashes, such as `server.properties`, `config/paper-global.yml`, or `plugins/TAB/config.yml`. Files under logs/ need logs:read; the rest need files:read. WorldAPI's own folder, hidden files, keys, databases, and jars are never shared.";
|
|
5
|
-
const WRITES = "Needs files:write, which only WorldAPI's owner group has unless someone adds it. Confirm the change with the user first. WorldAPI keeps the previous version in plugins/WorldAPI/backups. The server reads its configs at start, and a plugin at start or on its own reload command, so a change takes effect only after a restart or that plugin's reload. Servers whose configs come from elsewhere (a Docker image or a repository that copies them on start) may undo the change on the next start. Logs and the lists the server keeps in memory (ops.json, whitelist.json, banned-*.json, usercache.json) can't be written:
|
|
5
|
+
const WRITES = "Needs files:write, which only WorldAPI's owner group has unless someone adds it. Confirm the change with the user first. WorldAPI keeps the previous version in plugins/WorldAPI/backups. The server reads its configs at start, and a plugin at start or on its own reload command, so a change takes effect only after a restart or that plugin's reload. Servers whose configs come from elsewhere (a Docker image or a repository that copies them on start) may undo the change on the next start. Logs and the lists the server keeps in memory (ops.json, whitelist.json, banned-*.json, usercache.json) can't be written: manage_access changes those. Pass the sha256 from read_file as expectedSha256, so the write fails if the file changed meanwhile.";
|
|
6
6
|
const expectedSha256 = z
|
|
7
7
|
.string()
|
|
8
8
|
.regex(/^[0-9a-f]{64}$/)
|
|
@@ -30,24 +30,20 @@ export function formatFile(file, grep) {
|
|
|
30
30
|
}
|
|
31
31
|
export function fileTools(ctx) {
|
|
32
32
|
const { api, token } = ctx;
|
|
33
|
-
tool(ctx, {
|
|
34
|
-
name: "list_configs",
|
|
35
|
-
title: "Config files",
|
|
36
|
-
description: `Where the server's own configs are (server.properties, bukkit.yml, spigot.yml, Paper's, and more) and each plugin's config files, as paths for read_file. Needs files:read.`,
|
|
37
|
-
scope: "files:read",
|
|
38
|
-
effect: "read",
|
|
39
|
-
input: {},
|
|
40
|
-
run: () => unwrap(api.GET("/server/configs"), token),
|
|
41
|
-
});
|
|
42
33
|
tool(ctx, {
|
|
43
34
|
name: "list_files",
|
|
44
35
|
untrusted: true,
|
|
45
36
|
title: "Folder",
|
|
46
|
-
description: `A folder of the server: names, folders first, sizes, modification times, and whether read_file and
|
|
37
|
+
description: `A folder of the server: names, folders first, sizes, modification times, and whether read_file and edit_file accept each. ${PATHS} \`logs\` lists the console logs: latest.log and older ones like 2026-10-19-1.log.gz. configs true answers instead where the server's own configs are (server.properties, bukkit.yml, spigot.yml, Paper's, and more) and each plugin's config files (needs files:read).`,
|
|
47
38
|
scope: { any: ["files:read", "logs:read"] },
|
|
48
39
|
effect: "read",
|
|
49
|
-
input: {
|
|
50
|
-
|
|
40
|
+
input: {
|
|
41
|
+
path: z.string().optional().describe("The folder; the server folder itself when left out"),
|
|
42
|
+
configs: z.boolean().optional().describe("List the server's and plugins' config files instead"),
|
|
43
|
+
},
|
|
44
|
+
run: (args) => args.configs
|
|
45
|
+
? unwrap(api.GET("/server/configs"), token)
|
|
46
|
+
: unwrap(api.GET("/files", { params: { query: { path: args.path } } }), token),
|
|
51
47
|
});
|
|
52
48
|
tool(ctx, {
|
|
53
49
|
name: "read_file",
|
|
@@ -78,48 +74,44 @@ export function fileTools(ctx) {
|
|
|
78
74
|
});
|
|
79
75
|
tool(ctx, {
|
|
80
76
|
name: "edit_file",
|
|
81
|
-
title: "
|
|
82
|
-
description: `
|
|
83
|
-
scope: "files:write",
|
|
84
|
-
effect: "destructive",
|
|
85
|
-
input: {
|
|
86
|
-
path: z.string().min(1),
|
|
87
|
-
oldText: z.string().min(1),
|
|
88
|
-
newText: z.string(),
|
|
89
|
-
replaceAll: z.boolean().optional().describe("Replace every occurrence"),
|
|
90
|
-
expectedSha256,
|
|
91
|
-
},
|
|
92
|
-
run: (args) => unwrap(api.POST("/files/edit", {
|
|
93
|
-
body: {
|
|
94
|
-
path: args.path,
|
|
95
|
-
edits: [{ oldText: args.oldText, newText: args.newText, replaceAll: args.replaceAll ?? false }],
|
|
96
|
-
expectedSha256: args.expectedSha256,
|
|
97
|
-
},
|
|
98
|
-
}), token),
|
|
99
|
-
});
|
|
100
|
-
tool(ctx, {
|
|
101
|
-
name: "set_config_values",
|
|
102
|
-
title: "Set config values",
|
|
103
|
-
description: `Set keys in a YAML, .properties, or JSON config by dotted path, such as { "chunks.auto-save-interval": 6000 } in config/paper-world-defaults.yml or { "view-distance": 8 } in server.properties; sections are created on the way, and null removes a key. YAML keeps its comments, though spacing and quotes may change. ${WRITES}`,
|
|
77
|
+
title: "Change a file",
|
|
78
|
+
description: `Change a server or plugin config in one of three ways; give exactly one. \`edits\` replaces exact text, in order: each oldText must appear exactly once (copy it from read_file without the line numbers and the tab), unless replaceAll. \`values\` sets keys in a YAML, .properties, or JSON config by dotted path, such as { "chunks.auto-save-interval": 6000 } in config/paper-world-defaults.yml or { "view-distance": 8 } in server.properties; sections are created on the way, null removes a key, and YAML keeps its comments, though spacing and quotes may change. \`content\` creates the file or replaces it whole; prefer the other two for a change inside a file, since they can't lose the rest of it. ${WRITES}`,
|
|
104
79
|
scope: "files:write",
|
|
105
80
|
effect: "destructive",
|
|
106
81
|
input: {
|
|
107
82
|
path: z.string().min(1),
|
|
83
|
+
edits: z
|
|
84
|
+
.array(z.object({
|
|
85
|
+
oldText: z.string().min(1),
|
|
86
|
+
newText: z.string(),
|
|
87
|
+
replaceAll: z.boolean().optional().describe("Replace every occurrence"),
|
|
88
|
+
}))
|
|
89
|
+
.min(1)
|
|
90
|
+
.optional(),
|
|
108
91
|
values: z
|
|
109
92
|
.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.unknown())]))
|
|
93
|
+
.optional()
|
|
110
94
|
.describe("Dotted path to the new value"),
|
|
95
|
+
content: z.string().optional().describe("The whole new file"),
|
|
111
96
|
expectedSha256,
|
|
112
97
|
},
|
|
113
|
-
run: (args) =>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
98
|
+
run: (args) => {
|
|
99
|
+
const given = [args.edits, args.values, args.content].filter((part) => part !== undefined).length;
|
|
100
|
+
if (given !== 1)
|
|
101
|
+
throw new Error("Give exactly one of edits, values, or content");
|
|
102
|
+
const { path, expectedSha256: sha } = args;
|
|
103
|
+
if (args.edits)
|
|
104
|
+
return unwrap(api.POST("/files/edit", {
|
|
105
|
+
body: {
|
|
106
|
+
path,
|
|
107
|
+
edits: args.edits.map((edit) => ({ ...edit, replaceAll: edit.replaceAll ?? false })),
|
|
108
|
+
expectedSha256: sha,
|
|
109
|
+
},
|
|
110
|
+
}), token);
|
|
111
|
+
if (args.values)
|
|
112
|
+
return unwrap(api.POST("/files/set", { body: { path, values: args.values, expectedSha256: sha } }), token);
|
|
113
|
+
return unwrap(api.PUT("/files/write", { body: { path, content: args.content ?? "", expectedSha256: sha } }), token);
|
|
114
|
+
},
|
|
123
115
|
});
|
|
124
116
|
}
|
|
125
117
|
//# sourceMappingURL=files.js.map
|