@mairwunnx/minecraft-mcp 0.0.0-stage → 2.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/LICENSE +21 -0
- package/README.RU.md +205 -0
- package/README.md +204 -2
- package/dist/client.js +38 -0
- package/dist/config.js +25 -0
- package/dist/events.js +136 -0
- package/dist/generated/worldapi.js +6 -0
- package/dist/index.js +110 -0
- package/dist/prompts.js +70 -0
- package/dist/resources.js +37 -0
- package/dist/scopes.js +55 -0
- package/dist/server.js +48 -0
- package/dist/tool.js +48 -0
- package/dist/tools/blocks.js +115 -0
- package/dist/tools/common.js +49 -0
- package/dist/tools/entities.js +219 -0
- package/dist/tools/players.js +272 -0
- package/dist/tools/scripts.js +81 -0
- package/dist/tools/server.js +209 -0
- package/dist/tools/stats.js +51 -0
- package/dist/tools/worlds.js +172 -0
- package/dist/version.js +35 -0
- package/openapi/worldapi.json +12886 -0
- package/package.json +60 -3
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { unwrap } from "../client.js";
|
|
3
|
+
import { waitForEvent } from "../events.js";
|
|
4
|
+
import { tool } from "../tool.js";
|
|
5
|
+
import { cursor, format, limit, player } from "./common.js";
|
|
6
|
+
export function serverTools(ctx) {
|
|
7
|
+
const { api, token } = ctx;
|
|
8
|
+
tool(ctx, {
|
|
9
|
+
name: "get_server_status",
|
|
10
|
+
title: "Server status",
|
|
11
|
+
description: "Version, Paper build, MOTD, players online and max, uptime, difficulty, view distance, worlds, tick rate, and current load: TPS over 1, 5, and 15 minutes (20 is perfect), milliseconds per tick, memory used and max in MB, loaded chunks, and entities. detail adds tick time percentiles (under 50 ms keeps 20 TPS), CPU, and chunks, entities, and the most common entity types per world.",
|
|
12
|
+
scope: "read",
|
|
13
|
+
effect: "read",
|
|
14
|
+
input: { detail: z.boolean().optional().describe("Add percentiles, CPU, and per-world load") },
|
|
15
|
+
run: async (args) => {
|
|
16
|
+
const [status, performance] = await Promise.all([
|
|
17
|
+
unwrap(api.GET("/server"), token),
|
|
18
|
+
args.detail ? unwrap(api.GET("/server/performance"), token) : undefined,
|
|
19
|
+
]);
|
|
20
|
+
return performance ? { ...status, performance } : status;
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
tool(ctx, {
|
|
24
|
+
name: "list_plugins",
|
|
25
|
+
title: "Plugins",
|
|
26
|
+
description: "Installed plugins with version and whether they are enabled.",
|
|
27
|
+
scope: "read",
|
|
28
|
+
effect: "read",
|
|
29
|
+
input: {},
|
|
30
|
+
run: () => unwrap(api.GET("/server/plugins"), token),
|
|
31
|
+
});
|
|
32
|
+
tool(ctx, {
|
|
33
|
+
name: "get_events",
|
|
34
|
+
untrusted: true,
|
|
35
|
+
title: "Event history",
|
|
36
|
+
description: "Recorded events, newest first: player.join, player.leave, player.kick, player.death, player.advancement, player.chat (if the server records chat), world.night_skipped, entity.boss_killed, raid.start, raid.finish, server.status. Filter by type, player, and time.",
|
|
37
|
+
scope: "read",
|
|
38
|
+
effect: "read",
|
|
39
|
+
input: {
|
|
40
|
+
types: z.array(z.string()).optional().describe('Event types, such as ["player.death"]'),
|
|
41
|
+
player: z.string().optional().describe("Player name or UUID"),
|
|
42
|
+
from: z.string().optional().describe("ISO-8601 time; only events at or after it"),
|
|
43
|
+
to: z.string().optional().describe("ISO-8601 time; only events before it"),
|
|
44
|
+
order: z.enum(["newest", "oldest"]).optional(),
|
|
45
|
+
limit,
|
|
46
|
+
cursor,
|
|
47
|
+
},
|
|
48
|
+
run: (args) => unwrap(api.GET("/events", {
|
|
49
|
+
params: {
|
|
50
|
+
query: {
|
|
51
|
+
type: args.types?.join(","),
|
|
52
|
+
player: args.player,
|
|
53
|
+
from: args.from,
|
|
54
|
+
to: args.to,
|
|
55
|
+
order: args.order,
|
|
56
|
+
limit: args.limit ?? 50,
|
|
57
|
+
cursor: args.cursor,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
}), token),
|
|
61
|
+
});
|
|
62
|
+
tool(ctx, {
|
|
63
|
+
name: "wait_for_event",
|
|
64
|
+
untrusted: true,
|
|
65
|
+
title: "Wait for something to happen",
|
|
66
|
+
description: "Wait until an event happens live and get it, or time out. Types are WebSocket topics: player.join, player.leave, player.death, player.advancement, player.chat (if the server records chat), player.command (admin), player.respawn, player.level_up, player.sleep, player.world_change, player.kill, player.craft, block.break, block.place, world.weather, world.night_skipped, entity.boss_killed, raid.start, raid.finish, server.status, and more. Narrow it to one player or world. Use it for “tell me when Steve logs in” or “wait until night is skipped”. Keep timeoutSeconds within what your client lets a tool run.",
|
|
67
|
+
scope: "read",
|
|
68
|
+
effect: "read",
|
|
69
|
+
input: {
|
|
70
|
+
types: z.array(z.string().min(1)).min(1).describe('Such as ["player.join"]'),
|
|
71
|
+
player: player.optional().describe("Only this player's events"),
|
|
72
|
+
world: z.string().optional().describe("Only this world's events"),
|
|
73
|
+
timeoutSeconds: z.number().int().min(1).max(300).default(60),
|
|
74
|
+
},
|
|
75
|
+
run: (args) => waitForEvent({ url: ctx.url, token }, { types: args.types, player: args.player, world: args.world, timeoutMs: args.timeoutSeconds * 1000 }),
|
|
76
|
+
});
|
|
77
|
+
tool(ctx, {
|
|
78
|
+
name: "broadcast",
|
|
79
|
+
title: "Broadcast",
|
|
80
|
+
description: "Send a chat message, title, or action bar text to everyone online.",
|
|
81
|
+
scope: "admin",
|
|
82
|
+
effect: "write",
|
|
83
|
+
input: {
|
|
84
|
+
message: z.string().min(1),
|
|
85
|
+
kind: z.enum(["chat", "title", "actionbar"]).default("chat"),
|
|
86
|
+
subtitle: z.string().optional().describe("Only for titles"),
|
|
87
|
+
format,
|
|
88
|
+
},
|
|
89
|
+
run: (args) => unwrap(api.POST("/server/broadcast", { body: args }), token),
|
|
90
|
+
});
|
|
91
|
+
tool(ctx, {
|
|
92
|
+
name: "run_command",
|
|
93
|
+
untrusted: true,
|
|
94
|
+
title: "Run a console command",
|
|
95
|
+
description: "Run any console command as the server console, without the leading slash, and get its output. Powerful: prefer a specific tool when one exists. Every call is audit-logged.",
|
|
96
|
+
scope: "admin",
|
|
97
|
+
effect: "destructive",
|
|
98
|
+
input: { command: z.string().min(1).describe("Such as `weather clear` or `give Steve minecraft:bread 16`") },
|
|
99
|
+
run: (args) => unwrap(api.POST("/server/commands", { body: args }), token),
|
|
100
|
+
});
|
|
101
|
+
tool(ctx, {
|
|
102
|
+
name: "get_server_log",
|
|
103
|
+
untrusted: true,
|
|
104
|
+
title: "Console log",
|
|
105
|
+
description: "Recent console lines, oldest first: errors, warnings, plugin messages, joins, chat, and commands players ran. WorldAPI keeps them from its own start (1000 by default) and blanks out its tokens. Filter by the lowest level and by text, such as a player's name or a plugin's.",
|
|
106
|
+
scope: "admin",
|
|
107
|
+
effect: "read",
|
|
108
|
+
input: {
|
|
109
|
+
level: z
|
|
110
|
+
.enum(["trace", "debug", "info", "warn", "error", "fatal"])
|
|
111
|
+
.optional()
|
|
112
|
+
.describe("Only this level and above"),
|
|
113
|
+
grep: z.string().min(1).optional().describe("Only lines that contain this text, ignoring case"),
|
|
114
|
+
limit: z.number().int().min(1).optional().describe("Newest matching lines to return; 100 by default"),
|
|
115
|
+
},
|
|
116
|
+
run: (args) => unwrap(api.GET("/server/logs", { params: { query: args } }), token),
|
|
117
|
+
});
|
|
118
|
+
tool(ctx, {
|
|
119
|
+
name: "restart_server",
|
|
120
|
+
title: "Restart the server",
|
|
121
|
+
description: "Restart after a countdown in chat: players see warnings at set marks, then every world and player is saved and the server stops. Whatever runs the server must start it again (Docker's restart policy, a start script loop); otherwise it stays off. Scheduling again replaces a pending restart; cancel calls it off. Everyone online gets disconnected, so confirm with the user first.",
|
|
122
|
+
scope: "admin",
|
|
123
|
+
effect: "destructive",
|
|
124
|
+
input: {
|
|
125
|
+
countdownSeconds: z.number().int().min(0).max(3600).default(60),
|
|
126
|
+
message: z.string().optional().describe("Said with each warning, such as the reason"),
|
|
127
|
+
cancel: z.boolean().optional().describe("Call off a pending restart instead"),
|
|
128
|
+
},
|
|
129
|
+
run: (args) => args.cancel
|
|
130
|
+
? unwrap(api.DELETE("/server/restart"), token)
|
|
131
|
+
: unwrap(api.POST("/server/restart", { body: { countdownSeconds: args.countdownSeconds, message: args.message } }), token),
|
|
132
|
+
});
|
|
133
|
+
tool(ctx, {
|
|
134
|
+
name: "save_worlds",
|
|
135
|
+
title: "Save worlds",
|
|
136
|
+
description: "Save every world, and player data, to disk now.",
|
|
137
|
+
scope: "admin",
|
|
138
|
+
effect: "idempotent",
|
|
139
|
+
input: { players: z.boolean().default(true) },
|
|
140
|
+
run: (args) => unwrap(api.POST("/server/save", { body: args }), token),
|
|
141
|
+
});
|
|
142
|
+
tool(ctx, {
|
|
143
|
+
name: "get_access",
|
|
144
|
+
title: "Whitelist, operators, and bans",
|
|
145
|
+
description: "Who may play and who runs the server: whether the whitelist is on and enforced and who is on it, the operators, and banned players and IP addresses with reasons and expiry.",
|
|
146
|
+
scope: "admin",
|
|
147
|
+
effect: "read",
|
|
148
|
+
input: {},
|
|
149
|
+
run: async () => {
|
|
150
|
+
const [whitelist, operators, bans] = await Promise.all([
|
|
151
|
+
unwrap(api.GET("/server/whitelist"), token),
|
|
152
|
+
unwrap(api.GET("/server/ops"), token),
|
|
153
|
+
unwrap(api.GET("/server/bans"), token),
|
|
154
|
+
]);
|
|
155
|
+
return { whitelist, operators, bans };
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
tool(ctx, {
|
|
159
|
+
name: "manage_whitelist",
|
|
160
|
+
title: "Change the whitelist",
|
|
161
|
+
description: "Turn the whitelist on or off, enforce it, and add or remove players by name or UUID.",
|
|
162
|
+
scope: "admin",
|
|
163
|
+
effect: "idempotent",
|
|
164
|
+
input: {
|
|
165
|
+
enabled: z.boolean().optional(),
|
|
166
|
+
enforced: z.boolean().optional().describe("Kick online players who aren't on the list"),
|
|
167
|
+
add: z.array(z.string()).optional(),
|
|
168
|
+
remove: z.array(z.string()).optional(),
|
|
169
|
+
},
|
|
170
|
+
run: (args) => unwrap(api.PATCH("/server/whitelist", { body: { ...args, add: args.add ?? [], remove: args.remove ?? [] } }), token),
|
|
171
|
+
});
|
|
172
|
+
tool(ctx, {
|
|
173
|
+
name: "set_operator",
|
|
174
|
+
title: "Grant or revoke operator",
|
|
175
|
+
description: "Make a player an operator, or take it away.",
|
|
176
|
+
scope: "admin",
|
|
177
|
+
effect: "idempotent",
|
|
178
|
+
input: { player, op: z.boolean() },
|
|
179
|
+
run: (args) => unwrap(api.PATCH("/server/ops", {
|
|
180
|
+
body: args.op ? { add: [args.player], remove: [] } : { add: [], remove: [args.player] },
|
|
181
|
+
}), token),
|
|
182
|
+
});
|
|
183
|
+
const ban = z.object({
|
|
184
|
+
reason: z.string().optional(),
|
|
185
|
+
durationSeconds: z.number().int().positive().optional().describe("Leave out for a permanent ban"),
|
|
186
|
+
});
|
|
187
|
+
tool(ctx, {
|
|
188
|
+
name: "manage_bans",
|
|
189
|
+
title: "Ban or pardon",
|
|
190
|
+
description: "Ban players by name or UUID, or IP addresses; anyone online who is banned gets kicked. Pardon lifts a ban. Give any of the four lists in one call.",
|
|
191
|
+
scope: "admin",
|
|
192
|
+
effect: "destructive",
|
|
193
|
+
input: {
|
|
194
|
+
ban: z.array(ban.extend({ player: z.string().min(1) })).optional(),
|
|
195
|
+
pardon: z.array(z.string().min(1)).optional().describe("Player names or UUIDs"),
|
|
196
|
+
banIps: z.array(ban.extend({ ip: z.string().min(1) })).optional(),
|
|
197
|
+
pardonIps: z.array(z.string().min(1)).optional(),
|
|
198
|
+
},
|
|
199
|
+
run: (args) => unwrap(api.PATCH("/server/bans", {
|
|
200
|
+
body: {
|
|
201
|
+
ban: args.ban ?? [],
|
|
202
|
+
pardon: args.pardon ?? [],
|
|
203
|
+
banIps: args.banIps ?? [],
|
|
204
|
+
pardonIps: args.pardonIps ?? [],
|
|
205
|
+
},
|
|
206
|
+
}), token),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { unwrap } from "../client.js";
|
|
3
|
+
import { tool } from "../tool.js";
|
|
4
|
+
import { cursor, limit } from "./common.js";
|
|
5
|
+
export function statsTools(ctx) {
|
|
6
|
+
const { api, token } = ctx;
|
|
7
|
+
tool(ctx, {
|
|
8
|
+
name: "get_leaderboard",
|
|
9
|
+
title: "Leaderboards",
|
|
10
|
+
description: "Players ranked by a statistic; ties share a place. Presets: playtime (hours), diamonds, advancements, mobs, travel (km), deaths, blocks, fish, damage (hearts), netherite (ancient debris mined), experience (xp gained; a lower bound, since vanilla keeps no lifetime total). Or any statistic as `category:key`, such as `custom:jump`, `mined:diamond_ore`, `killed:*`. Without a statistic, lists the presets and their units.",
|
|
11
|
+
scope: "read",
|
|
12
|
+
effect: "read",
|
|
13
|
+
input: {
|
|
14
|
+
statistic: z.string().min(1).optional(),
|
|
15
|
+
limit: z.number().int().min(1).max(500).optional().describe("10 by default"),
|
|
16
|
+
},
|
|
17
|
+
run: (args) => args.statistic
|
|
18
|
+
? unwrap(api.GET("/leaderboards/{statistic}", {
|
|
19
|
+
params: { path: { statistic: args.statistic }, query: { limit: args.limit ?? 10 } },
|
|
20
|
+
}), token)
|
|
21
|
+
: unwrap(api.GET("/leaderboards"), token),
|
|
22
|
+
});
|
|
23
|
+
tool(ctx, {
|
|
24
|
+
name: "get_recipes",
|
|
25
|
+
title: "Recipes",
|
|
26
|
+
description: "How to make an item (`item`) or what an item is good for (`uses`), or both: crafting grids with their shape, furnace, smoker, blast furnace, and campfire cooking with time and experience, stonecutting, and smithing. Plugin recipes count too. Each ingredient lists the items that fit.",
|
|
27
|
+
scope: "read",
|
|
28
|
+
effect: "read",
|
|
29
|
+
input: {
|
|
30
|
+
item: z.string().min(1).optional().describe("What the recipe makes, such as `minecraft:torch`"),
|
|
31
|
+
uses: z.string().min(1).optional().describe("What the recipe takes, such as `minecraft:coal`"),
|
|
32
|
+
limit,
|
|
33
|
+
cursor,
|
|
34
|
+
},
|
|
35
|
+
run: (args) => {
|
|
36
|
+
if (!args.item && !args.uses)
|
|
37
|
+
throw new Error("Give item, uses, or both");
|
|
38
|
+
return unwrap(api.GET("/recipes", { params: { query: { ...args, limit: args.limit ?? 20 } } }), token);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
tool(ctx, {
|
|
42
|
+
name: "get_aggregates",
|
|
43
|
+
title: "Server totals",
|
|
44
|
+
description: "Everyone's statistics added up: hours played, blocks mined and the top block, mobs killed and the top mob, deaths, advancements, world day.",
|
|
45
|
+
scope: "read",
|
|
46
|
+
effect: "read",
|
|
47
|
+
input: {},
|
|
48
|
+
run: () => unwrap(api.GET("/aggregates"), token),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=stats.js.map
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { unwrap } from "../client.js";
|
|
3
|
+
import { tool } from "../tool.js";
|
|
4
|
+
import { player, world } from "./common.js";
|
|
5
|
+
export function worldTools(ctx) {
|
|
6
|
+
const { api, token } = ctx;
|
|
7
|
+
const path = (name) => ({ params: { path: { world: name } } });
|
|
8
|
+
tool(ctx, {
|
|
9
|
+
name: "list_worlds",
|
|
10
|
+
title: "Worlds",
|
|
11
|
+
description: "Loaded worlds with their dimension key and player count.",
|
|
12
|
+
scope: "read",
|
|
13
|
+
effect: "read",
|
|
14
|
+
input: {},
|
|
15
|
+
run: () => unwrap(api.GET("/worlds"), token),
|
|
16
|
+
});
|
|
17
|
+
tool(ctx, {
|
|
18
|
+
name: "get_world",
|
|
19
|
+
title: "World",
|
|
20
|
+
description: "Time of day (dayTime 0–23999: 0 sunrise, 6000 noon, 13000 night, 18000 midnight), day number, moon phase, ticks until night, whether beds work, weather, difficulty, PvP, spawn, the world border, game rule values, and load. ruleDetails adds each game rule's type and default; size adds chunks ever generated, explored area in km², and size on disk.",
|
|
21
|
+
scope: "read",
|
|
22
|
+
effect: "read",
|
|
23
|
+
input: {
|
|
24
|
+
world,
|
|
25
|
+
ruleDetails: z.boolean().optional().describe("Add every game rule with its type and default"),
|
|
26
|
+
size: z.boolean().optional().describe("Add explored area and size on disk"),
|
|
27
|
+
},
|
|
28
|
+
run: async (args) => {
|
|
29
|
+
const [view, gameRuleDetails, size] = await Promise.all([
|
|
30
|
+
unwrap(api.GET("/worlds/{world}", path(args.world)), token),
|
|
31
|
+
args.ruleDetails ? unwrap(api.GET("/worlds/{world}/gamerules", path(args.world)), token) : undefined,
|
|
32
|
+
args.size ? unwrap(api.GET("/worlds/{world}/stats", path(args.world)), token) : undefined,
|
|
33
|
+
]);
|
|
34
|
+
return { ...view, ...(gameRuleDetails && { gameRuleDetails }), ...(size && { size }) };
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
tool(ctx, {
|
|
38
|
+
name: "locate_structure",
|
|
39
|
+
title: "Find a structure",
|
|
40
|
+
description: "The nearest structure of a type, from a point or the world spawn. `type` is a key such as `stronghold`, `ancient_city`, `trial_chambers`, or a prefix such as `village` that matches every village kind. Searching blocks the server thread for a moment, so keep the radius modest.",
|
|
41
|
+
scope: "read",
|
|
42
|
+
effect: "read",
|
|
43
|
+
input: {
|
|
44
|
+
world,
|
|
45
|
+
type: z.string().min(1),
|
|
46
|
+
x: z.number().int().optional(),
|
|
47
|
+
z: z.number().int().optional(),
|
|
48
|
+
radius: z.number().int().min(1).optional().describe("Search radius in chunks"),
|
|
49
|
+
unexplored: z.boolean().optional().describe("Only structures nobody found yet"),
|
|
50
|
+
},
|
|
51
|
+
run: (args) => unwrap(api.GET("/worlds/{world}/structures/nearest", {
|
|
52
|
+
params: {
|
|
53
|
+
path: { world: args.world },
|
|
54
|
+
query: { type: args.type, x: args.x, z: args.z, radius: args.radius, unexplored: args.unexplored },
|
|
55
|
+
},
|
|
56
|
+
}), token),
|
|
57
|
+
});
|
|
58
|
+
tool(ctx, {
|
|
59
|
+
name: "find_nearby",
|
|
60
|
+
title: "Find nearby blocks or biomes",
|
|
61
|
+
description: "The nearest blocks of some types and the nearest biome, around a point or a player. Blocks take keys or tags: `minecraft:diamond_ore`, `#minecraft:logs`, `#minecraft:iron_ores`. Block search reads only loaded chunks (those near players), 32 blocks around by default and 64 at most, and says how many chunks in range it couldn't read. Biome search asks the world generator, so it reaches unexplored land, 1024 blocks around by default. Without x and z it searches around the player: the user's own when WORLDAPI_PLAYER is set.",
|
|
62
|
+
scope: "read",
|
|
63
|
+
effect: "read",
|
|
64
|
+
input: {
|
|
65
|
+
blocks: z.array(z.string().min(1)).optional().describe('Such as ["minecraft:diamond_ore", "#minecraft:logs"]'),
|
|
66
|
+
biomes: z.array(z.string().min(1)).optional().describe('Such as ["minecraft:cherry_grove"]; the nearest of any'),
|
|
67
|
+
world: z.string().optional().describe("World name; the player's world, or `world`, when left out"),
|
|
68
|
+
x: z.number().int().optional(),
|
|
69
|
+
y: z.number().int().optional().describe("Needed for blocks when x and z are given"),
|
|
70
|
+
z: z.number().int().optional(),
|
|
71
|
+
player: player.optional().describe("Search around this player instead of x, y, z"),
|
|
72
|
+
radius: z.number().int().min(1).optional().describe("Block search radius"),
|
|
73
|
+
biomeRadius: z.number().int().min(1).optional().describe("Biome search radius"),
|
|
74
|
+
limit: z.number().int().min(1).max(100).optional().describe("Blocks to return, nearest first; 10 by default"),
|
|
75
|
+
},
|
|
76
|
+
run: async (args) => {
|
|
77
|
+
if (!args.blocks?.length && !args.biomes?.length)
|
|
78
|
+
throw new Error("Give blocks, biomes, or both");
|
|
79
|
+
let { world: name, x, y, z } = args;
|
|
80
|
+
if (x === undefined || z === undefined) {
|
|
81
|
+
const who = args.player ?? ctx.player;
|
|
82
|
+
if (!who)
|
|
83
|
+
throw new Error("Give x and z, or a player");
|
|
84
|
+
const view = await unwrap(api.GET("/players/{player}", { params: { path: { player: who } } }), token);
|
|
85
|
+
const at = "location" in view ? view.location : undefined;
|
|
86
|
+
if (!at)
|
|
87
|
+
throw new Error(`${who}'s location isn't known`);
|
|
88
|
+
name ??= at.world;
|
|
89
|
+
x = Math.floor(at.x);
|
|
90
|
+
y = Math.floor(at.y);
|
|
91
|
+
z = Math.floor(at.z);
|
|
92
|
+
}
|
|
93
|
+
return unwrap(api.GET("/worlds/{world}/search", {
|
|
94
|
+
params: {
|
|
95
|
+
path: { world: name ?? "world" },
|
|
96
|
+
query: {
|
|
97
|
+
blocks: args.blocks?.join(","),
|
|
98
|
+
biome: args.biomes?.join(","),
|
|
99
|
+
x,
|
|
100
|
+
y,
|
|
101
|
+
z,
|
|
102
|
+
radius: args.radius,
|
|
103
|
+
biomeRadius: args.biomeRadius,
|
|
104
|
+
limit: args.limit,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
}), token);
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
tool(ctx, {
|
|
111
|
+
name: "set_world",
|
|
112
|
+
title: "Change a world",
|
|
113
|
+
description: "Change any of these at once; only what you give changes. The time of day (dayTime 0–23999: 0 sunrise, 1000 morning, 6000 noon, 13000 night, 18000 midnight), or move the clock forward with addTicks (24000 is a day). The weather (clear, rain, thunder), optionally for durationTicks (20 per second). The difficulty, PvP, and the world spawn.",
|
|
114
|
+
scope: "write:world",
|
|
115
|
+
effect: "write",
|
|
116
|
+
input: {
|
|
117
|
+
world,
|
|
118
|
+
dayTime: z.number().int().min(0).max(23999).optional(),
|
|
119
|
+
addTicks: z.number().int().optional().describe("Moves the clock forward; 24000 ticks are one day"),
|
|
120
|
+
weather: z.enum(["clear", "rain", "thunder"]).optional(),
|
|
121
|
+
durationTicks: z.number().int().positive().optional().describe("How long the new weather lasts"),
|
|
122
|
+
difficulty: z.enum(["peaceful", "easy", "normal", "hard"]).optional(),
|
|
123
|
+
pvp: z.boolean().optional().describe("Whether players can hurt each other"),
|
|
124
|
+
spawn: z
|
|
125
|
+
.object({ x: z.number().int(), y: z.number().int(), z: z.number().int(), angle: z.number().optional() })
|
|
126
|
+
.optional()
|
|
127
|
+
.describe("New world spawn point"),
|
|
128
|
+
},
|
|
129
|
+
run: (args) => unwrap(api.PATCH("/worlds/{world}", {
|
|
130
|
+
...path(args.world),
|
|
131
|
+
body: {
|
|
132
|
+
dayTime: args.dayTime,
|
|
133
|
+
addTime: args.addTicks,
|
|
134
|
+
weather: args.weather,
|
|
135
|
+
weatherDurationTicks: args.weather && args.weather !== "clear" ? args.durationTicks : undefined,
|
|
136
|
+
clearWeatherDurationTicks: args.weather === "clear" ? args.durationTicks : undefined,
|
|
137
|
+
difficulty: args.difficulty,
|
|
138
|
+
pvp: args.pvp,
|
|
139
|
+
spawn: args.spawn && { ...args.spawn, angle: args.spawn.angle ?? 0 },
|
|
140
|
+
},
|
|
141
|
+
}), token),
|
|
142
|
+
});
|
|
143
|
+
tool(ctx, {
|
|
144
|
+
name: "set_gamerule",
|
|
145
|
+
title: "Set game rules",
|
|
146
|
+
description: 'Set one or more game rules at once, such as { "keep_inventory": true, "players_sleeping_percentage": 50 }. get_world with ruleDetails lists the names and types.',
|
|
147
|
+
scope: "write:world",
|
|
148
|
+
effect: "idempotent",
|
|
149
|
+
input: { world, rules: z.record(z.string(), z.union([z.boolean(), z.number().int()])) },
|
|
150
|
+
run: (args) => unwrap(api.PATCH("/worlds/{world}/gamerules", { ...path(args.world), body: args.rules }), token),
|
|
151
|
+
});
|
|
152
|
+
tool(ctx, {
|
|
153
|
+
name: "set_world_border",
|
|
154
|
+
title: "Change the world border",
|
|
155
|
+
description: "Resize (optionally over time) or move the world border, or change its damage and warnings.",
|
|
156
|
+
scope: "write:world",
|
|
157
|
+
effect: "idempotent",
|
|
158
|
+
input: {
|
|
159
|
+
world,
|
|
160
|
+
size: z.number().positive().optional().describe("Width in blocks"),
|
|
161
|
+
transitionSeconds: z.number().int().min(0).optional(),
|
|
162
|
+
centerX: z.number().optional(),
|
|
163
|
+
centerZ: z.number().optional(),
|
|
164
|
+
damageAmount: z.number().min(0).optional(),
|
|
165
|
+
damageBuffer: z.number().min(0).optional(),
|
|
166
|
+
warningDistance: z.number().int().min(0).optional(),
|
|
167
|
+
warningTimeTicks: z.number().int().min(0).optional(),
|
|
168
|
+
},
|
|
169
|
+
run: ({ world: name, ...body }) => unwrap(api.PATCH("/worlds/{world}/border", { ...path(name), body }), token),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=worlds.js.map
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
/** This package's version, read from package.json so a release changes it in one place. */
|
|
3
|
+
export const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
4
|
+
/** The oldest WorldAPI whose endpoints match the generated client. */
|
|
5
|
+
export const MIN_WORLDAPI = "2.0.0";
|
|
6
|
+
/** Compares two x.y.z versions; anything that isn't one (a dev build) counts as new enough. */
|
|
7
|
+
export function olderThan(version, minimum) {
|
|
8
|
+
const parse = (text) => /^(\d+)\.(\d+)\.(\d+)/.exec(text)?.slice(1).map(Number);
|
|
9
|
+
const have = parse(version);
|
|
10
|
+
const need = parse(minimum);
|
|
11
|
+
if (!have || !need)
|
|
12
|
+
return false;
|
|
13
|
+
for (let i = 0; i < 3; i++) {
|
|
14
|
+
if (have[i] !== need[i])
|
|
15
|
+
return (have[i] ?? 0) < (need[i] ?? 0);
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Asks WorldAPI for its version and warns when it's older than this server expects. It never
|
|
21
|
+
* stops the start: most tools still work, and the warning says which side to update.
|
|
22
|
+
*/
|
|
23
|
+
export async function checkWorldApi(api, log) {
|
|
24
|
+
try {
|
|
25
|
+
const { data } = await api.GET("/server");
|
|
26
|
+
const version = data?.worldApiVersion;
|
|
27
|
+
if (version && olderThan(version, MIN_WORLDAPI))
|
|
28
|
+
log(`WorldAPI ${version} is older than ${MIN_WORLDAPI}, which this server expects; update the plugin, or some tools will fail`);
|
|
29
|
+
return version;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=version.js.map
|