@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.
@@ -0,0 +1,108 @@
1
+ import { z } from "zod";
2
+ import { unwrap } from "../client.js";
3
+ import { grants } from "../scopes.js";
4
+ import { tool } from "../tool.js";
5
+ import { item, OFFLINE_NOTE, SLOT_LAYOUT } from "./common.js";
6
+ export function itemTools(ctx) {
7
+ const { api, token } = ctx;
8
+ const players = grants(ctx.scopes, "players:write");
9
+ const blocks = grants(ctx.scopes, "world:write");
10
+ tool(ctx, {
11
+ name: "edit_items",
12
+ title: "Give, take, or clear items",
13
+ description: `Change what a player's inventory or ender chest holds (players:write; ${OFFLINE_NOTE}), or a chest, barrel, or other container block (world:write). Give either player (and container) or block. \`give\` adds an item where it fits: a player's overflow drops at their feet, a block's is lost and counted in \`dropped\`. \`set\` puts an item in a slot, replacing what was there. \`take\` removes amount items from a slot and says what it took. \`clear_slot\` empties one slot; \`clear\` empties everything, a player's armor and offhand included. Cleared and taken items are gone. ${SLOT_LAYOUT} A container's slots count from 0; a double chest has 54, the left half first.`,
14
+ scope: { any: ["players:write", "world:write"] },
15
+ effect: "destructive",
16
+ input: {
17
+ action: z.enum(["give", "set", "take", "clear_slot", "clear"]),
18
+ player: z
19
+ .string()
20
+ .min(1)
21
+ .optional()
22
+ .describe(`Player name or UUID${ctx.player ? `; ${ctx.player}, the user, when neither player nor block is given` : ""}`),
23
+ container: z.enum(["inventory", "enderchest"]).default("inventory").describe("For a player"),
24
+ block: z
25
+ .object({ world: z.string().default("world"), x: z.number().int(), y: z.number().int(), z: z.number().int() })
26
+ .optional()
27
+ .describe("A container block instead of a player"),
28
+ slot: z.number().int().min(0).optional().describe("For set, take, and clear_slot"),
29
+ item: item.optional().describe("For give and set"),
30
+ amount: z.number().int().min(1).optional().describe("For take: how many; the whole stack when left out"),
31
+ },
32
+ run: (args) => {
33
+ const needSlot = () => {
34
+ if (args.slot === undefined)
35
+ throw new Error(`${args.action} needs slot`);
36
+ return args.slot;
37
+ };
38
+ const needItem = () => {
39
+ if (!args.item)
40
+ throw new Error(`${args.action} needs item`);
41
+ return args.item;
42
+ };
43
+ const take = { amount: args.amount ?? 6400 };
44
+ if (args.block) {
45
+ if (!blocks)
46
+ throw new Error("Changing a container block needs world:write");
47
+ const { world, x, y, z: bz } = args.block;
48
+ const at = { params: { path: { world, x, y, z: bz } } };
49
+ const slot = (n) => ({ params: { path: { world, x, y, z: bz, slot: n } } });
50
+ switch (args.action) {
51
+ case "clear":
52
+ return unwrap(api.DELETE("/worlds/{world}/blocks/{x}/{y}/{z}/container", at), token);
53
+ case "give":
54
+ return unwrap(api.POST("/worlds/{world}/blocks/{x}/{y}/{z}/container/give", { ...at, body: { item: needItem() } }), token);
55
+ case "clear_slot":
56
+ return unwrap(api.DELETE("/worlds/{world}/blocks/{x}/{y}/{z}/container/slots/{slot}", slot(needSlot())), token);
57
+ case "take":
58
+ return unwrap(api.POST("/worlds/{world}/blocks/{x}/{y}/{z}/container/slots/{slot}/take", {
59
+ ...slot(needSlot()),
60
+ body: take,
61
+ }), token);
62
+ case "set":
63
+ return unwrap(api.PATCH("/worlds/{world}/blocks/{x}/{y}/{z}/container/slots/{slot}", {
64
+ ...slot(needSlot()),
65
+ body: needItem(),
66
+ }), token);
67
+ }
68
+ }
69
+ const name = args.player ?? ctx.player;
70
+ if (!name)
71
+ throw new Error("Give player or block");
72
+ if (!players)
73
+ throw new Error("Changing a player's items needs players:write");
74
+ const ender = args.container === "enderchest";
75
+ const who = { params: { path: { player: name } } };
76
+ const slot = (n) => ({ params: { path: { player: name, slot: n } } });
77
+ switch (args.action) {
78
+ case "clear":
79
+ return unwrap(ender ? api.DELETE("/players/{player}/enderchest", who) : api.DELETE("/players/{player}/inventory", who), token);
80
+ case "give": {
81
+ const body = { item: needItem() };
82
+ return unwrap(ender
83
+ ? api.POST("/players/{player}/enderchest/give", { ...who, body })
84
+ : api.POST("/players/{player}/inventory/give", { ...who, body }), token);
85
+ }
86
+ case "clear_slot": {
87
+ const at = slot(needSlot());
88
+ return unwrap(ender
89
+ ? api.DELETE("/players/{player}/enderchest/slots/{slot}", at)
90
+ : api.DELETE("/players/{player}/inventory/slots/{slot}", at), token);
91
+ }
92
+ case "take": {
93
+ const at = { ...slot(needSlot()), body: take };
94
+ return unwrap(ender
95
+ ? api.POST("/players/{player}/enderchest/slots/{slot}/take", at)
96
+ : api.POST("/players/{player}/inventory/slots/{slot}/take", at), token);
97
+ }
98
+ case "set": {
99
+ const at = { ...slot(needSlot()), body: needItem() };
100
+ return unwrap(ender
101
+ ? api.PATCH("/players/{player}/enderchest/slots/{slot}", at)
102
+ : api.PATCH("/players/{player}/inventory/slots/{slot}", at), token);
103
+ }
104
+ }
105
+ },
106
+ });
107
+ }
108
+ //# sourceMappingURL=items.js.map
@@ -1,92 +1,96 @@
1
1
  import { z } from "zod";
2
2
  import { unwrap } from "../client.js";
3
3
  import { tool } from "../tool.js";
4
- import { player as anyPlayer, cursor, format, ITEM_DETAIL, item, limit, location, OFFLINE_NOTE, playerOrMe, SLOT_LAYOUT, } from "./common.js";
5
- const container = z.enum(["inventory", "enderchest"]).default("inventory");
4
+ import { cursor, format, ITEM_DETAIL, limit, location, OFFLINE_NOTE, playerOrMe, SLOT_LAYOUT } from "./common.js";
6
5
  export function playerTools(ctx) {
7
6
  const { api, token } = ctx;
8
7
  const who = (name) => ({ params: { path: { player: name } } });
9
8
  const player = playerOrMe(ctx.player);
10
9
  tool(ctx, {
11
10
  name: "list_players",
11
+ untrusted: true,
12
12
  title: "Players",
13
- description: "Everyone who has played here: online state, first and last seen, playtime in seconds. limit 0 only counts them.",
13
+ description: "Everyone who has played here: online state, first and last seen, playtime in seconds; limit 0 only counts them. online true lists who is playing now instead, with world, game mode, health, food, level, ping, idle time, and location.",
14
14
  scope: "players:read",
15
15
  effect: "read",
16
16
  input: {
17
- online: z.boolean().optional(),
17
+ online: z.boolean().optional().describe("true: who is online now, with their state"),
18
18
  sort: z.enum(["name", "last_seen", "playtime"]).optional(),
19
19
  limit,
20
20
  cursor,
21
21
  },
22
- run: (args) => unwrap(api.GET("/players", { params: { query: args } }), token),
23
- });
24
- tool(ctx, {
25
- name: "list_online_players",
26
- untrusted: true,
27
- title: "Online players",
28
- description: "Who is playing now, with world, game mode, health, food, level, ping, idle time, and location.",
29
- scope: "players:read",
30
- effect: "read",
31
- input: {},
32
- run: () => unwrap(api.GET("/players/online"), token),
22
+ run: (args) => args.online
23
+ ? unwrap(api.GET("/players/online"), token)
24
+ : unwrap(api.GET("/players", { params: { query: args } }), token),
33
25
  });
34
26
  tool(ctx, {
35
27
  name: "get_player",
36
28
  untrusted: true,
37
29
  title: "Player",
38
- description: `A player's profile and, while online, full state: game mode, health, food, XP, location, flight, speed, ping, client. Offline players show their last location. include adds parts in the same call: \`inventory\` and \`enderchest\` (from the saved player file while offline; ${SLOT_LAYOUT}), \`effects\` (online only), \`deaths\` (the last death place and the newest 5 with causes and killers), \`sessions\` (the newest 5), \`statistics\` (the general ones; get_player_statistics has every category), and \`advancements\` (counts and the newest 5; get_player_advancements lists them all). Coordinates, inventories, and death places need players:private. ${ITEM_DETAIL}`,
30
+ description: `A player's profile and, while online, full state: game mode, health, food, XP, location, flight, speed, ping, client. Offline players show their last location. players reads up to 10 at once. include adds parts in the same call: \`inventory\` and \`enderchest\` (from the saved player file while offline; ${SLOT_LAYOUT}), \`effects\` (online only), \`deaths\` (the last death place and the newest 5 with causes and killers), \`sessions\` (the newest 5), \`statistics\` (the general ones), and \`advancements\` (counts and the newest 5). statistics gives every vanilla statistic, or one category (custom, mined, broken, crafted, used, picked_up, dropped, killed, killed_by; play_time in ticks, distances in centimeters). advancements lists every advancement with title, description, frame, and parent: \`todo\` is what's left, and with the parents it shows what to go for next. permission says whether the player is an operator and, if online, has a permission node (a server permission, such as \`worldedit.wand\`). Coordinates, inventories, and death places need players:private. ${ITEM_DETAIL}`,
39
31
  scope: "players:read",
40
32
  effect: "read",
41
33
  input: {
42
- player,
34
+ player: player.optional(),
35
+ players: z.array(z.string().min(1)).min(1).max(10).optional().describe("Several players instead of one"),
43
36
  include: z
44
37
  .array(z.enum(["inventory", "enderchest", "effects", "deaths", "sessions", "statistics", "advancements"]))
45
38
  .optional()
46
39
  .describe('Parts to add, such as ["inventory", "deaths"]'),
40
+ statistics: z.string().min(1).optional().describe("`all`, or one category such as `mined`"),
41
+ advancements: z.enum(["all", "done", "todo"]).optional(),
42
+ permission: z.string().min(1).optional().describe("A permission node to check"),
43
+ },
44
+ run: async (args) => {
45
+ const one = async (name) => {
46
+ const [view, statistics, advancements, permission] = await Promise.all([
47
+ unwrap(api.GET("/players/{player}", {
48
+ params: { path: { player: name }, query: { include: args.include?.join(",") } },
49
+ }), token),
50
+ args.statistics
51
+ ? unwrap(api.GET("/players/{player}/statistics", {
52
+ params: {
53
+ path: { player: name },
54
+ query: { category: args.statistics === "all" ? undefined : args.statistics },
55
+ },
56
+ }), token)
57
+ : undefined,
58
+ args.advancements
59
+ ? unwrap(api.GET("/players/{player}/advancements", {
60
+ params: {
61
+ path: { player: name },
62
+ query: { done: args.advancements === "all" ? undefined : args.advancements === "done" },
63
+ },
64
+ }), token)
65
+ : undefined,
66
+ args.permission
67
+ ? unwrap(api.GET("/players/{player}/permissions", {
68
+ params: { path: { player: name }, query: { node: args.permission } },
69
+ }), token)
70
+ : undefined,
71
+ ]);
72
+ return {
73
+ ...view,
74
+ ...(statistics && { allStatistics: statistics }),
75
+ ...(advancements && { allAdvancements: advancements }),
76
+ ...(permission && { permission }),
77
+ };
78
+ };
79
+ if (args.players)
80
+ return Promise.all(args.players.map((name) => one(name).catch((error) => ({
81
+ player: name,
82
+ error: error.code ?? error.message,
83
+ }))));
84
+ const name = args.player ?? ctx.player;
85
+ if (!name)
86
+ throw new Error("Give player or players");
87
+ return one(name);
47
88
  },
48
- run: (args) => unwrap(api.GET("/players/{player}", {
49
- params: { path: { player: args.player }, query: { include: args.include?.join(",") } },
50
- }), token),
51
- });
52
- tool(ctx, {
53
- name: "get_player_statistics",
54
- title: "Statistics",
55
- description: "Vanilla statistics by category (custom, mined, broken, crafted, used, picked_up, dropped, killed, killed_by); live while online. play_time is in ticks (20 per second); distances in centimeters.",
56
- scope: "players:read",
57
- effect: "read",
58
- input: { player, category: z.string().optional().describe("Only one category, such as `mined`") },
59
- run: (args) => unwrap(api.GET("/players/{player}/statistics", {
60
- params: { path: { player: args.player }, query: { category: args.category } },
61
- }), token),
62
- });
63
- tool(ctx, {
64
- name: "get_player_advancements",
65
- untrusted: true,
66
- title: "Advancements",
67
- description: "Every advancement on the server with its title, description, frame, and parent, done or not, and when it was finished. done false lists what's left: with the parents, it shows what to go for next.",
68
- scope: "players:read",
69
- effect: "read",
70
- input: { player, done: z.boolean().optional().describe("Only finished (true) or unfinished (false)") },
71
- run: (args) => unwrap(api.GET("/players/{player}/advancements", {
72
- params: { path: { player: args.player }, query: { done: args.done } },
73
- }), token),
74
- });
75
- tool(ctx, {
76
- name: "check_permission",
77
- title: "Permissions",
78
- description: "Whether a player is an operator and, if online, whether they have a permission node (a server permission, not a WorldAPI one).",
79
- scope: "players:read",
80
- effect: "read",
81
- input: { player, node: z.string().optional().describe("Such as `worldedit.wand`") },
82
- run: (args) => unwrap(api.GET("/players/{player}/permissions", {
83
- params: { path: { player: args.player }, query: { node: args.node } },
84
- }), token),
85
89
  });
86
90
  tool(ctx, {
87
91
  name: "teleport_player",
88
92
  title: "Teleport a player",
89
- description: `Teleport to coordinates, to another player, or to a world's spawn; give exactly one. Check get_highest_block first to land on solid ground. ${OFFLINE_NOTE}`,
93
+ description: `Teleport to coordinates, to another player, or to a world's spawn; give exactly one. Check the surface with get_block (no y) first to land on solid ground. ${OFFLINE_NOTE}`,
90
94
  scope: "players:write",
91
95
  effect: "write",
92
96
  input: {
@@ -130,71 +134,28 @@ export function playerTools(ctx) {
130
134
  });
131
135
  tool(ctx, {
132
136
  name: "send_message",
133
- title: "Message a player",
134
- description: `Send one player a chat message, title, or action bar text, optionally with a sound. ${OFFLINE_NOTE}`,
137
+ title: "Message players",
138
+ description: "Send a chat message, title, or action bar text to some players, or to everyone online when players is left out. A sound plays only for named players, who must be online.",
135
139
  scope: "players:write",
136
140
  effect: "write",
137
141
  input: {
138
- player,
142
+ players: z.array(z.string().min(1)).min(1).optional().describe("Names or UUIDs; everyone online when left out"),
139
143
  message: z.string().min(1),
140
144
  kind: z.enum(["chat", "title", "actionbar"]).default("chat"),
141
- subtitle: z.string().optional(),
145
+ subtitle: z.string().optional().describe("Only for titles"),
142
146
  sound: z.string().optional().describe("Such as `minecraft:entity.experience_orb.pickup`"),
143
147
  format,
144
148
  },
145
- run: ({ player: name, ...body }) => unwrap(api.POST("/players/{player}/message", { ...who(name), body }), token),
146
- });
147
- tool(ctx, {
148
- name: "kick_player",
149
- title: "Kick a player",
150
- description: "Disconnect a player with an optional reason. Needs players:moderate.",
151
- scope: "players:moderate",
152
- effect: "destructive",
153
- input: { player: anyPlayer, reason: z.string().optional(), format },
154
- run: ({ player: name, ...body }) => unwrap(api.POST("/players/{player}/kick", { ...who(name), body }), token),
155
- });
156
- tool(ctx, {
157
- name: "give_item",
158
- title: "Give an item",
159
- description: `Add an item to a player's inventory (what doesn't fit drops at their feet) or ender chest. ${OFFLINE_NOTE}`,
160
- scope: "players:write",
161
- effect: "write",
162
- input: { player, item, container },
163
- run: (args) => unwrap(args.container === "enderchest"
164
- ? api.POST("/players/{player}/enderchest/give", { ...who(args.player), body: { item: args.item } })
165
- : api.POST("/players/{player}/inventory/give", { ...who(args.player), body: { item: args.item } }), token),
166
- });
167
- tool(ctx, {
168
- name: "edit_inventory",
169
- title: "Edit an inventory",
170
- description: `Change a player's inventory or ender chest: \`set\` puts an item in a slot, replacing what was there; \`clear_slot\` empties one slot; \`clear\` empties everything, armor and offhand included. Cleared items are gone. To add items without picking a slot, use give_item. ${SLOT_LAYOUT} ${OFFLINE_NOTE}`,
171
- scope: "players:write",
172
- effect: "destructive",
173
- input: {
174
- player,
175
- action: z.enum(["set", "clear_slot", "clear"]),
176
- slot: z.number().int().min(0).optional().describe("For set and clear_slot"),
177
- item: item.optional().describe("For set"),
178
- container,
179
- },
180
- run: (args) => {
181
- const ender = args.container === "enderchest";
182
- if (args.action === "clear")
183
- return unwrap(ender
184
- ? api.DELETE("/players/{player}/enderchest", who(args.player))
185
- : api.DELETE("/players/{player}/inventory", who(args.player)), token);
186
- if (args.slot === undefined)
187
- throw new Error(`${args.action} needs slot`);
188
- const params = { params: { path: { player: args.player, slot: args.slot } } };
189
- if (args.action === "clear_slot")
190
- return unwrap(ender
191
- ? api.DELETE("/players/{player}/enderchest/slots/{slot}", params)
192
- : api.DELETE("/players/{player}/inventory/slots/{slot}", params), token);
193
- if (!args.item)
194
- throw new Error("set needs item");
195
- return unwrap(ender
196
- ? api.PATCH("/players/{player}/enderchest/slots/{slot}", { ...params, body: args.item })
197
- : api.PATCH("/players/{player}/inventory/slots/{slot}", { ...params, body: args.item }), token);
149
+ run: async ({ players, sound, ...body }) => {
150
+ if (!players)
151
+ return unwrap(api.POST("/server/broadcast", { body }), token);
152
+ const sent = [];
153
+ for (const name of players)
154
+ sent.push({
155
+ player: name,
156
+ ...(await unwrap(api.POST("/players/{player}/message", { ...who(name), body: { ...body, sound } }), token)),
157
+ });
158
+ return sent.length === 1 ? sent[0] : sent;
198
159
  },
199
160
  });
200
161
  const effect = z.object({
@@ -229,20 +190,31 @@ export function playerTools(ctx) {
229
190
  });
230
191
  tool(ctx, {
231
192
  name: "set_advancement",
232
- title: "Grant or revoke an advancement",
233
- description: `Complete every criterion of an advancement for a player (done true), or take them all back (done false). ${OFFLINE_NOTE}`,
193
+ title: "Grant or revoke advancements",
194
+ description: `Complete every criterion of advancements for a player (done true), or take them all back (done false). ${OFFLINE_NOTE}`,
234
195
  scope: "players:write",
235
196
  effect: "idempotent",
236
197
  input: {
237
198
  player,
238
- key: z.string().min(1).describe("Advancement key, such as `minecraft:story/mine_diamond`"),
199
+ keys: z
200
+ .array(z.string().min(1))
201
+ .min(1)
202
+ .max(50)
203
+ .describe('Advancement keys, such as ["minecraft:story/mine_diamond"]'),
239
204
  done: z.boolean(),
240
205
  },
241
- run: (args) => {
242
- const params = { params: { path: { player: args.player, key: args.key } } };
243
- return unwrap(args.done
244
- ? api.POST("/players/{player}/advancements/{key}", { ...params, body: {} })
245
- : api.DELETE("/players/{player}/advancements/{key}", params), token);
206
+ run: async (args) => {
207
+ const results = [];
208
+ for (const key of args.keys) {
209
+ const params = { params: { path: { player: args.player, key } } };
210
+ results.push({
211
+ key,
212
+ ...(await unwrap(args.done
213
+ ? api.POST("/players/{player}/advancements/{key}", { ...params, body: {} })
214
+ : api.DELETE("/players/{player}/advancements/{key}", params), token)),
215
+ });
216
+ }
217
+ return results.length === 1 ? results[0] : results;
246
218
  },
247
219
  });
248
220
  }
@@ -38,26 +38,19 @@ export function scriptTools(ctx) {
38
38
  });
39
39
  tool(ctx, {
40
40
  name: "list_java_scripts",
41
- title: "Loaded scripts",
42
- description: "Resident scripts with their listener and task counts and error count.",
43
- scope: "code",
44
- effect: "read",
45
- input: {},
46
- run: () => unwrap(api.GET("/scripts"), token),
47
- });
48
- tool(ctx, {
49
- name: "get_java_script",
50
41
  untrusted: true,
51
- title: "Script and its log",
52
- description: "One resident script with its source, plus the latest lines it wrote with ctx.log and errors from its handlers and tasks. logs false leaves the log out.",
42
+ title: "Loaded scripts",
43
+ description: "Resident scripts with their listener and task counts and error count. With id, that script with its source, plus the latest lines it wrote with ctx.log and errors from its handlers and tasks; logs false leaves the log out.",
53
44
  scope: "code",
54
45
  effect: "read",
55
46
  input: {
56
- id: z.string(),
47
+ id: z.string().optional().describe("One script in full"),
57
48
  logs: z.boolean().default(true),
58
49
  limit: z.number().int().min(1).max(10000).optional().describe("Log lines to return"),
59
50
  },
60
51
  run: async (args) => {
52
+ if (!args.id)
53
+ return unwrap(api.GET("/scripts"), token);
61
54
  const path = { id: args.id };
62
55
  const [script, logs] = await Promise.all([
63
56
  unwrap(api.GET("/scripts/{id}", { params: { path } }), token),