breakpoint-mcp 1.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,1919 @@
1
+ import { z } from "zod";
2
+ import { gate } from "../confirm.js";
3
+ import { ok } from "./lsp-common.js";
4
+ /**
5
+ * Editor-bridge tools. Each forwards to a method on the in-editor addon over
6
+ * TCP and returns the result as both human-readable text and structuredContent.
7
+ * A friendly, actionable message is returned (isError) when the editor/bridge
8
+ * is not reachable, instead of throwing an opaque protocol error.
9
+ */
10
+ function fail(err) {
11
+ const be = err;
12
+ const code = be?.code ?? "error";
13
+ const message = be?.message ?? String(err);
14
+ return {
15
+ isError: true,
16
+ content: [{ type: "text", text: `Bridge error [${code}]: ${message}` }],
17
+ };
18
+ }
19
+ export function registerEditorTools(server, bridge) {
20
+ const call = async (method, params = {}) => {
21
+ try {
22
+ const result = await bridge.request(method, params);
23
+ return ok(result);
24
+ }
25
+ catch (err) {
26
+ return fail(err);
27
+ }
28
+ };
29
+ server.registerTool("editor_ping", { title: "Ping editor bridge", description: "Check that the editor is running with the Breakpoint MCP plugin enabled.", inputSchema: {} }, async () => call("ping"));
30
+ server.registerTool("editor_get_state", { title: "Editor state", description: "Return the currently edited scene, its root type/path, and the current node selection.", inputSchema: {} }, async () => call("editor.get_state"));
31
+ server.registerTool("editor_undo", {
32
+ title: "Undo last edit",
33
+ description: "Undo the most recent edit in the editor's undo history (like Ctrl-Z). Defaults to the edited scene's history; pass scope='global' for the editor-wide history. Returns whether an action was undone, its name, and the remaining undo/redo depth.",
34
+ inputSchema: {
35
+ scope: z
36
+ .enum(["scene", "global"])
37
+ .optional()
38
+ .describe("Which history to step: 'scene' (default, the edited scene) or 'global' (editor-wide)"),
39
+ },
40
+ }, async ({ scope }) => call("edit.undo", scope ? { scope } : {}));
41
+ server.registerTool("editor_redo", {
42
+ title: "Redo last undone edit",
43
+ description: "Re-apply the most recently undone edit in the editor's undo history (like Ctrl-Shift-Z). Defaults to the edited scene's history; pass scope='global' for the editor-wide history.",
44
+ inputSchema: {
45
+ scope: z
46
+ .enum(["scene", "global"])
47
+ .optional()
48
+ .describe("Which history to step: 'scene' (default, the edited scene) or 'global' (editor-wide)"),
49
+ },
50
+ }, async ({ scope }) => call("edit.redo", scope ? { scope } : {}));
51
+ server.registerTool("project_get_info", { title: "Project info", description: "Return project name, main scene, project root path, Godot version, and feature tags.", inputSchema: {} }, async () => call("project.get_info"));
52
+ server.registerTool("project_get_setting", {
53
+ title: "Get project setting",
54
+ description: "Read a single ProjectSettings value by dotted key (e.g. application/config/name).",
55
+ inputSchema: { name: z.string().describe("ProjectSettings key") },
56
+ }, async ({ name }) => call("project.get_setting", { name }));
57
+ server.registerTool("project_set_setting", {
58
+ title: "Set project setting",
59
+ description: "Write a ProjectSettings value. Set save=true to persist to project.godot. DESTRUCTIVE — gated by confirmation.",
60
+ inputSchema: {
61
+ name: z.string().describe("ProjectSettings key"),
62
+ value: z.any().describe("New value; rich types use the {\"__type__\":...} tagging convention"),
63
+ save: z.boolean().optional().describe("Persist to disk (default false)"),
64
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
65
+ },
66
+ }, async ({ name, value, save, confirm }) => {
67
+ const blocked = await gate(server, confirm, `Set project setting "${name}"${save ? " and save project.godot" : ""}`);
68
+ if (blocked)
69
+ return blocked;
70
+ return call("project.set_setting", { name, value, save: save ?? false });
71
+ });
72
+ server.registerTool("scene_get_tree", {
73
+ title: "Get scene tree",
74
+ description: "Return the node tree of the currently edited scene (name, type, path, script, children).",
75
+ inputSchema: { max_depth: z.number().int().positive().optional().describe("Max recursion depth (default 64)") },
76
+ }, async ({ max_depth }) => call("scene.get_tree", max_depth ? { max_depth } : {}));
77
+ server.registerTool("scene_open", {
78
+ title: "Open scene",
79
+ description: "Open an existing scene in the editor by res:// path.",
80
+ inputSchema: { path: z.string().describe("Scene path, e.g. res://scenes/main.tscn") },
81
+ }, async ({ path }) => call("scene.open", { path }));
82
+ server.registerTool("scene_save", { title: "Save scene", description: "Save the currently edited scene to its existing path.", inputSchema: {} }, async () => call("scene.save"));
83
+ server.registerTool("scene_new", {
84
+ title: "New scene",
85
+ description: "Create a new scene with the given root class, save it to a path, and open it. DESTRUCTIVE (writes a file) — gated by confirmation.",
86
+ inputSchema: {
87
+ root_type: z.string().describe("Root node class, e.g. Node2D, Node3D, Control"),
88
+ path: z.string().describe("Where to save, e.g. res://scenes/new.tscn"),
89
+ name: z.string().optional().describe("Root node name (defaults to the class name)"),
90
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
91
+ },
92
+ }, async ({ root_type, path, name, confirm }) => {
93
+ const blocked = await gate(server, confirm, `Create and overwrite scene file "${path}"`);
94
+ if (blocked)
95
+ return blocked;
96
+ return call("scene.new", { root_type, path, name });
97
+ });
98
+ server.registerTool("scene_list_open", {
99
+ title: "List open scenes",
100
+ description: "List the res:// paths of all scenes open in the editor, which one is current, and which have unsaved changes. Read-only.",
101
+ inputSchema: {},
102
+ }, async () => call("scene.list_open"));
103
+ server.registerTool("scene_reload", {
104
+ title: "Reload scene from disk",
105
+ description: "Reload a scene from disk, discarding unsaved changes to it. DESTRUCTIVE — gated by confirmation. Defaults to the current scene.",
106
+ inputSchema: {
107
+ path: z.string().optional().describe("Scene res:// path; omitted = the current edited scene"),
108
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
109
+ },
110
+ }, async ({ path, confirm }) => {
111
+ const blocked = await gate(server, confirm, `Reload scene ${path ?? "(current)"} from disk, discarding unsaved changes`);
112
+ if (blocked)
113
+ return blocked;
114
+ return call("scene.reload", path !== undefined ? { path } : {});
115
+ });
116
+ server.registerTool("scene_close", {
117
+ title: "Close current scene",
118
+ description: "Close the current scene tab, discarding unsaved changes. DESTRUCTIVE — gated by confirmation. Only the current scene can be closed; pass its path to assert which one.",
119
+ inputSchema: {
120
+ path: z.string().optional().describe("Optional assertion: must equal the current edited scene's path"),
121
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
122
+ },
123
+ }, async ({ path, confirm }) => {
124
+ const blocked = await gate(server, confirm, "Close the current scene, discarding unsaved changes");
125
+ if (blocked)
126
+ return blocked;
127
+ return call("scene.close", path !== undefined ? { path } : {});
128
+ });
129
+ server.registerTool("scene_pack", {
130
+ title: "Pack branch as scene",
131
+ description: "Save a node branch (the node and its subtree) as a new PackedScene file. DESTRUCTIVE (writes a file) — gated by confirmation. Does not modify the edited scene.",
132
+ inputSchema: {
133
+ path: z.string().describe("Branch root node path relative to the scene root"),
134
+ to_path: z.string().describe("Where to save the PackedScene, e.g. res://scenes/branch.tscn"),
135
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
136
+ },
137
+ }, async ({ path, to_path, confirm }) => {
138
+ const blocked = await gate(server, confirm, `Pack branch "${path}" to ${to_path}`);
139
+ if (blocked)
140
+ return blocked;
141
+ return call("scene.pack", { path, to_path });
142
+ });
143
+ server.registerTool("scene_get_dependencies", {
144
+ title: "Scene dependencies",
145
+ description: "List the external resource dependencies of a scene file. Read-only. Defaults to the current scene.",
146
+ inputSchema: { path: z.string().optional().describe("Scene res:// path; omitted = the current edited scene") },
147
+ }, async ({ path }) => call("scene.get_dependencies", path !== undefined ? { path } : {}));
148
+ server.registerTool("scene_save_as", {
149
+ title: "Save scene as",
150
+ description: "Save the current scene to a new res:// path (Save As). DESTRUCTIVE (writes a file) — gated by confirmation.",
151
+ inputSchema: {
152
+ path: z.string().describe("Destination path, e.g. res://scenes/copy.tscn"),
153
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
154
+ },
155
+ }, async ({ path, confirm }) => {
156
+ const blocked = await gate(server, confirm, `Save the current scene to ${path}`);
157
+ if (blocked)
158
+ return blocked;
159
+ return call("scene.save_as", { path });
160
+ });
161
+ server.registerTool("node_add", {
162
+ title: "Add node",
163
+ description: "Instance a node of the given class under a parent path (undoable). Returns the new node's path.",
164
+ inputSchema: {
165
+ parent_path: z.string().describe("Parent node path relative to the scene root; \".\" for the root"),
166
+ type: z.string().describe("Node class to instance, e.g. Sprite2D, AudioStreamPlayer3D"),
167
+ name: z.string().optional().describe("Node name (defaults to the class name)"),
168
+ },
169
+ }, async ({ parent_path, type, name }) => call("node.add", { parent_path, type, name }));
170
+ server.registerTool("node_delete", {
171
+ title: "Delete node",
172
+ description: "Delete a node (undoable). DESTRUCTIVE — gated by confirmation. Refuses to delete the scene root.",
173
+ inputSchema: {
174
+ path: z.string().describe("Node path relative to the scene root"),
175
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
176
+ },
177
+ }, async ({ path, confirm }) => {
178
+ const blocked = await gate(server, confirm, `Delete node "${path}"`);
179
+ if (blocked)
180
+ return blocked;
181
+ return call("node.delete", { path });
182
+ });
183
+ server.registerTool("node_rename", {
184
+ title: "Rename node",
185
+ description: "Rename a node (undoable).",
186
+ inputSchema: {
187
+ path: z.string().describe("Node path relative to the scene root"),
188
+ new_name: z.string().describe("New node name"),
189
+ },
190
+ }, async ({ path, new_name }) => call("node.rename", { path, new_name }));
191
+ server.registerTool("node_reparent", {
192
+ title: "Reparent node",
193
+ description: "Move a node under a new parent (undoable).",
194
+ inputSchema: {
195
+ path: z.string().describe("Node path relative to the scene root"),
196
+ new_parent_path: z.string().describe("New parent path; \".\" for the root"),
197
+ keep_global_transform: z.boolean().optional().describe("Preserve global transform (default true)"),
198
+ },
199
+ }, async ({ path, new_parent_path, keep_global_transform }) => call("node.reparent", { path, new_parent_path, keep_global_transform: keep_global_transform ?? true }));
200
+ server.registerTool("node_set_property", {
201
+ title: "Set node property",
202
+ description: "Set a property on a node (undoable). Rich types use the {\"__type__\":\"Vector3\",\"x\":..} convention. " +
203
+ "Example value for position: {\"__type__\":\"Vector3\",\"x\":1,\"y\":0,\"z\":2}.",
204
+ inputSchema: {
205
+ path: z.string().describe("Node path relative to the scene root"),
206
+ property: z.string().describe("Property name, e.g. position, modulate, text"),
207
+ value: z.any().describe("New value (JSON scalar, array, object, or a __type__-tagged Variant)"),
208
+ },
209
+ }, async ({ path, property, value }) => call("node.set_property", { path, property, value }));
210
+ server.registerTool("node_get_property", {
211
+ title: "Get node property",
212
+ description: "Read a single property from a node. Rich types come back __type__-tagged.",
213
+ inputSchema: {
214
+ path: z.string().describe("Node path relative to the scene root"),
215
+ property: z.string().describe("Property name"),
216
+ },
217
+ }, async ({ path, property }) => call("node.get_property", { path, property }));
218
+ server.registerTool("node_duplicate", {
219
+ title: "Duplicate node",
220
+ description: "Duplicate a node and its children under the same parent (undoable). Returns the new node's path.",
221
+ inputSchema: {
222
+ path: z.string().describe("Node path relative to the scene root"),
223
+ name: z.string().optional().describe("Name for the duplicate (defaults to Godot's auto-numbered name)"),
224
+ },
225
+ }, async ({ path, name }) => call("node.duplicate", name !== undefined ? { path, name } : { path }));
226
+ server.registerTool("node_get_children", {
227
+ title: "Get node children",
228
+ description: "List the direct children of a node (name, type, path). Read-only.",
229
+ inputSchema: { path: z.string().describe("Node path relative to the scene root; \".\" for the root") },
230
+ }, async ({ path }) => call("node.get_children", { path }));
231
+ server.registerTool("node_find", {
232
+ title: "Find nodes",
233
+ description: "Search a node's descendants by class and/or name substring (case-insensitive). Read-only.",
234
+ inputSchema: {
235
+ root_path: z.string().optional().describe("Where to search from; \".\" or omitted for the scene root"),
236
+ type: z.string().optional().describe("Only match nodes of this class (is_class), e.g. Sprite2D"),
237
+ name_contains: z.string().optional().describe("Only match nodes whose name contains this substring"),
238
+ limit: z.number().int().positive().optional().describe("Max matches to return (default 200)"),
239
+ },
240
+ }, async ({ root_path, type, name_contains, limit }) => call("node.find", {
241
+ root_path: root_path ?? ".",
242
+ type: type ?? "",
243
+ name_contains: name_contains ?? "",
244
+ limit: limit ?? 200,
245
+ }));
246
+ server.registerTool("node_list_groups", {
247
+ title: "List node groups",
248
+ description: "List the groups a node belongs to. Read-only.",
249
+ inputSchema: { path: z.string().describe("Node path relative to the scene root") },
250
+ }, async ({ path }) => call("node.list_groups", { path }));
251
+ server.registerTool("node_add_to_group", {
252
+ title: "Add node to group",
253
+ description: "Add a node to a group (persistent, undoable). No-op if already a member.",
254
+ inputSchema: {
255
+ path: z.string().describe("Node path relative to the scene root"),
256
+ group: z.string().describe("Group name"),
257
+ },
258
+ }, async ({ path, group }) => call("node.add_to_group", { path, group }));
259
+ server.registerTool("node_remove_from_group", {
260
+ title: "Remove node from group",
261
+ description: "Remove a node from a group (undoable). No-op if not a member.",
262
+ inputSchema: {
263
+ path: z.string().describe("Node path relative to the scene root"),
264
+ group: z.string().describe("Group name"),
265
+ },
266
+ }, async ({ path, group }) => call("node.remove_from_group", { path, group }));
267
+ server.registerTool("node_instantiate_scene", {
268
+ title: "Instance scene under node",
269
+ description: "Instance an external PackedScene (res:// path) as an editable child of a parent node (undoable). Returns the new node's path.",
270
+ inputSchema: {
271
+ parent_path: z.string().describe("Parent node path relative to the scene root; \".\" for the root"),
272
+ scene_path: z.string().describe("Scene to instance, e.g. res://actors/enemy.tscn"),
273
+ name: z.string().optional().describe("Node name (defaults to the instanced scene's root name)"),
274
+ },
275
+ }, async ({ parent_path, scene_path, name }) => call("node.instantiate_scene", name !== undefined ? { parent_path, scene_path, name } : { parent_path, scene_path }));
276
+ server.registerTool("node_move_child", {
277
+ title: "Move child",
278
+ description: "Reorder a node among its siblings by index (undoable). Negative indices count from the end (-1 = last).",
279
+ inputSchema: {
280
+ path: z.string().describe("Node path relative to the scene root"),
281
+ to_index: z.number().int().describe("New sibling index (0-based; negative counts from the end)"),
282
+ },
283
+ }, async ({ path, to_index }) => call("node.move_child", { path, to_index }));
284
+ server.registerTool("node_change_type", {
285
+ title: "Change node type",
286
+ description: "Replace a node with a new node of a different class, carrying over compatible properties, children, and groups (undoable). Refuses the scene root.",
287
+ inputSchema: {
288
+ path: z.string().describe("Node path relative to the scene root"),
289
+ type: z.string().describe("New node class, e.g. Sprite2D, StaticBody3D"),
290
+ },
291
+ }, async ({ path, type }) => call("node.change_type", { path, type }));
292
+ server.registerTool("node_set_owner", {
293
+ title: "Set node owner",
294
+ description: "Set a node's owner (undoable). Owner must be an ancestor; \".\" or omitted sets the scene root. Ownership determines which scene a node is saved into.",
295
+ inputSchema: {
296
+ path: z.string().describe("Node path relative to the scene root"),
297
+ owner_path: z.string().optional().describe("Ancestor to own the node; \".\" or omitted for the scene root"),
298
+ },
299
+ }, async ({ path, owner_path }) => call("node.set_owner", owner_path !== undefined ? { path, owner_path } : { path }));
300
+ server.registerTool("node_call_method", {
301
+ title: "Call node method (edit-time)",
302
+ description: "Invoke a method on a node in the EDITED scene. DESTRUCTIVE (arbitrary invocation, not undoable) — gated by confirmation. " +
303
+ "Args use the tagged-Variant convention; the return value comes back tagged.",
304
+ inputSchema: {
305
+ path: z.string().describe("Node path relative to the scene root"),
306
+ method: z.string().describe("Method name"),
307
+ args: z.array(z.any()).optional().describe("Positional arguments (JSON scalars or __type__-tagged Variants)"),
308
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
309
+ },
310
+ }, async ({ path, method, args, confirm }) => {
311
+ const blocked = await gate(server, confirm, `Call ${path}.${method}() in the edited scene`);
312
+ if (blocked)
313
+ return blocked;
314
+ return call("node.call_method", { path, method, args: args ?? [] });
315
+ });
316
+ server.registerTool("node_get_path", {
317
+ title: "Get node path",
318
+ description: "Return a node's scene-relative path, class, sibling index, parent path, and child count. Read-only.",
319
+ inputSchema: { path: z.string().describe("Node path relative to the scene root; \".\" for the root") },
320
+ }, async ({ path }) => call("node.get_path", { path }));
321
+ server.registerTool("node_list_properties", {
322
+ title: "List node properties",
323
+ description: "List a node's inspector-visible properties (name, Variant type, class_name, usage flags). Read-only. Use node_get_property to read a value.",
324
+ inputSchema: { path: z.string().describe("Node path relative to the scene root") },
325
+ }, async ({ path }) => call("node.list_properties", { path }));
326
+ server.registerTool("signal_list", {
327
+ title: "List node signals",
328
+ description: "List the signals a node declares (name and argument names), including user signals. Read-only.",
329
+ inputSchema: { path: z.string().describe("Node path relative to the scene root") },
330
+ }, async ({ path }) => call("signal.list", { path }));
331
+ server.registerTool("signal_list_connections", {
332
+ title: "List signal connections",
333
+ description: "List a node's outgoing signal connections (signal, target node path, method, flags). Optionally filter to one signal. Read-only.",
334
+ inputSchema: {
335
+ path: z.string().describe("Node path relative to the scene root"),
336
+ signal: z.string().optional().describe("Only list connections for this signal"),
337
+ },
338
+ }, async ({ path, signal }) => call("signal.list_connections", signal !== undefined ? { path, signal } : { path }));
339
+ server.registerTool("signal_connect", {
340
+ title: "Connect signal",
341
+ description: "Connect a source node's signal to a target node's method (undoable). Persistent by default (flags=2, CONNECT_PERSIST) so it saves into the scene. No-op if already connected.",
342
+ inputSchema: {
343
+ path: z.string().describe("Source node path (emitter)"),
344
+ signal: z.string().describe("Signal name on the source"),
345
+ target_path: z.string().describe("Target node path (receiver)"),
346
+ method: z.string().describe("Method to call on the target"),
347
+ flags: z.number().int().optional().describe("Object.ConnectFlags bitmask (default 2 = CONNECT_PERSIST)"),
348
+ },
349
+ }, async ({ path, signal, target_path, method, flags }) => call("signal.connect", flags !== undefined ? { path, signal, target_path, method, flags } : { path, signal, target_path, method }));
350
+ server.registerTool("signal_disconnect", {
351
+ title: "Disconnect signal",
352
+ description: "Disconnect a source node's signal from a target node's method (undoable). No-op if not connected.",
353
+ inputSchema: {
354
+ path: z.string().describe("Source node path (emitter)"),
355
+ signal: z.string().describe("Signal name on the source"),
356
+ target_path: z.string().describe("Target node path (receiver)"),
357
+ method: z.string().describe("Connected method on the target"),
358
+ },
359
+ }, async ({ path, signal, target_path, method }) => call("signal.disconnect", { path, signal, target_path, method }));
360
+ server.registerTool("signal_add_user_signal", {
361
+ title: "Add user signal",
362
+ description: "Declare a new user signal on a node (undoable). Optional typed arguments. Errors if the signal already exists.",
363
+ inputSchema: {
364
+ path: z.string().describe("Node path relative to the scene root"),
365
+ signal: z.string().describe("New signal name"),
366
+ args: z
367
+ .array(z.object({ name: z.string(), type: z.number().int().optional() }))
368
+ .optional()
369
+ .describe("Signal parameters (name + Variant type int)"),
370
+ },
371
+ }, async ({ path, signal, args }) => call("signal.add_user_signal", args !== undefined ? { path, signal, args } : { path, signal }));
372
+ server.registerTool("signal_emit", {
373
+ title: "Emit signal (edit-time)",
374
+ description: "Emit a signal from a node in the EDITED scene, firing its connected callables now. DESTRUCTIVE (edit-time side effects) — gated by confirmation. Args use the tagged-Variant convention.",
375
+ inputSchema: {
376
+ path: z.string().describe("Node path relative to the scene root"),
377
+ signal: z.string().describe("Signal name"),
378
+ args: z.array(z.any()).optional().describe("Signal arguments (JSON scalars or __type__-tagged Variants)"),
379
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
380
+ },
381
+ }, async ({ path, signal, args, confirm }) => {
382
+ const blocked = await gate(server, confirm, `Emit signal "${signal}" from ${path} in the edited scene`);
383
+ if (blocked)
384
+ return blocked;
385
+ return call("signal.emit", { path, signal, args: args ?? [] });
386
+ });
387
+ server.registerTool("selection_get", { title: "Get selection", description: "Return the paths of the nodes currently selected in the editor.", inputSchema: {} }, async () => call("selection.get"));
388
+ server.registerTool("selection_set", {
389
+ title: "Set selection",
390
+ description: "Replace the editor's node selection with the given node paths.",
391
+ inputSchema: { paths: z.array(z.string()).describe("Node paths relative to the scene root") },
392
+ }, async ({ paths }) => call("selection.set", { paths }));
393
+ server.registerTool("classdb_get_class", {
394
+ title: "Introspect class",
395
+ description: "Return the parent class, methods, properties, and signals of an engine class via ClassDB.",
396
+ inputSchema: {
397
+ class_name: z.string().describe("Engine class name, e.g. AudioStreamPlayer3D"),
398
+ include_inherited: z.boolean().optional().describe("Include inherited members (default false)"),
399
+ },
400
+ }, async ({ class_name, include_inherited }) => call("classdb.get_class", { class_name, include_inherited: include_inherited ?? false }));
401
+ // ---- Group K: knowledge & search (ClassDB-backed; the host-side project index
402
+ // lives in tools/knowledge.ts) ----
403
+ server.registerTool("class_reference", {
404
+ title: "Class reference",
405
+ description: "Full engine-class reference via ClassDB: method SIGNATURES (return type + typed args), signal " +
406
+ "signatures, and typed properties — the detailed view classdb_get_class summarises as bare names. " +
407
+ "Read-only. Includes the canonical online docs URL. Pass member to filter to a single method/property/signal.",
408
+ inputSchema: {
409
+ class_name: z.string().describe("Engine class name, e.g. AudioStreamPlayer3D"),
410
+ include_inherited: z.boolean().optional().describe("Include inherited members (default false)"),
411
+ member: z.string().optional().describe("Only return members whose name contains this substring"),
412
+ },
413
+ }, async ({ class_name, include_inherited, member }) => call("classdb.reference", {
414
+ class_name,
415
+ include_inherited: include_inherited ?? false,
416
+ member: member ?? "",
417
+ }));
418
+ server.registerTool("docs_search", {
419
+ title: "Search the class reference",
420
+ description: "Search the Godot class reference (ClassDB) by keyword — matching class names and, unless a scope narrows " +
421
+ "it, their methods/properties/signals — and return each hit with its canonical docs URL. Read-only. " +
422
+ "Use kind to restrict to one member type, class_name to scope to a single class, and limit to bound results.",
423
+ inputSchema: {
424
+ query: z.string().describe("Case-insensitive substring to match against class / member names"),
425
+ kind: z.enum(["any", "class", "method", "property", "signal"]).optional().describe("Restrict to one result kind (default any)"),
426
+ class_name: z.string().optional().describe("Scope the member search to a single class (still returns class-name matches project-wide)"),
427
+ limit: z.number().int().positive().optional().describe("Max results before truncation (default 40)"),
428
+ deep: z.boolean().optional().describe("Also scan members, not just class names (default true)"),
429
+ },
430
+ }, async ({ query, kind, class_name, limit, deep }) => call("docs.search", {
431
+ query,
432
+ kind: kind ?? "any",
433
+ class_name: class_name ?? "",
434
+ limit: limit ?? 40,
435
+ deep: deep ?? true,
436
+ }));
437
+ server.registerTool("screenshot_editor", {
438
+ title: "Screenshot editor viewport",
439
+ description: "Capture the 2D or 3D editor viewport as a PNG and return it as image content so the assistant can see the scene. " +
440
+ "Requires the matching editor tab (2D/3D) to be active and rendered.",
441
+ inputSchema: { viewport: z.enum(["2d", "3d"]).optional().describe("Which viewport (default 3d)") },
442
+ }, async ({ viewport }) => {
443
+ try {
444
+ const r = (await bridge.request("screenshot.editor_viewport", { viewport: viewport ?? "3d" }));
445
+ return {
446
+ content: [
447
+ { type: "image", data: r.base64, mimeType: r.mime },
448
+ { type: "text", text: `Captured ${r.viewport} viewport (${r.width}x${r.height}).` },
449
+ ],
450
+ };
451
+ }
452
+ catch (err) {
453
+ return fail(err);
454
+ }
455
+ });
456
+ // ---- Group B: resources (operations.gd _resource_*) ----
457
+ server.registerTool("resource_create", {
458
+ title: "Create resource",
459
+ description: "Instantiate a Resource subclass and save it as a new file. DESTRUCTIVE (writes a file) — gated by confirmation. Optional initial properties use the tagged-Variant convention.",
460
+ inputSchema: {
461
+ class_name: z.string().describe("Resource subclass to instantiate, e.g. StyleBoxFlat, Theme, GDScript"),
462
+ to_path: z.string().describe("Destination res:// path, e.g. res://styles/panel.tres"),
463
+ properties: z.record(z.any()).optional().describe("Initial property values (JSON scalars or __type__-tagged Variants)"),
464
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
465
+ },
466
+ }, async ({ class_name, to_path, properties, confirm }) => {
467
+ const blocked = await gate(server, confirm, `Create ${class_name} resource at ${to_path}`);
468
+ if (blocked)
469
+ return blocked;
470
+ return call("resource.create", properties !== undefined ? { class_name, to_path, properties } : { class_name, to_path });
471
+ });
472
+ server.registerTool("resource_load", {
473
+ title: "Load resource",
474
+ description: "Load a resource file and return its class, resource_name, and inspector-visible property list. Read-only.",
475
+ inputSchema: { path: z.string().describe("Resource res:// path") },
476
+ }, async ({ path }) => call("resource.load", { path }));
477
+ server.registerTool("resource_save", {
478
+ title: "Save resource",
479
+ description: "Load a resource and (re-)save it, optionally to a new path and with ResourceSaver flags. DESTRUCTIVE (writes a file) — gated by confirmation. Shares subresources by reference; use resource_duplicate for an independent copy.",
480
+ inputSchema: {
481
+ from_path: z.string().describe("Source resource res:// path"),
482
+ to_path: z.string().optional().describe("Destination res:// path (default: overwrite from_path)"),
483
+ flags: z.number().int().optional().describe("ResourceSaver.SaverFlags bitmask (e.g. 32 = FLAG_COMPRESS)"),
484
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
485
+ },
486
+ }, async ({ from_path, to_path, flags, confirm }) => {
487
+ const blocked = await gate(server, confirm, `Save resource ${from_path}${to_path ? ` to ${to_path}` : ""}`);
488
+ if (blocked)
489
+ return blocked;
490
+ const params = { from_path };
491
+ if (to_path !== undefined)
492
+ params.to_path = to_path;
493
+ if (flags !== undefined)
494
+ params.flags = flags;
495
+ return call("resource.save", params);
496
+ });
497
+ server.registerTool("resource_duplicate", {
498
+ title: "Duplicate resource",
499
+ description: "Load a resource, duplicate it (optionally deep, cloning subresources), and save the copy to a new path. DESTRUCTIVE (writes a file) — gated by confirmation.",
500
+ inputSchema: {
501
+ path: z.string().describe("Source resource res:// path"),
502
+ to_path: z.string().describe("Destination res:// path for the copy"),
503
+ deep: z.boolean().optional().describe("Deep-duplicate subresources (default false)"),
504
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
505
+ },
506
+ }, async ({ path, to_path, deep, confirm }) => {
507
+ const blocked = await gate(server, confirm, `Duplicate resource ${path} to ${to_path}`);
508
+ if (blocked)
509
+ return blocked;
510
+ return call("resource.duplicate", deep !== undefined ? { path, to_path, deep } : { path, to_path });
511
+ });
512
+ server.registerTool("resource_get_property", {
513
+ title: "Get resource property",
514
+ description: "Read a single property of a resource file by name. Read-only. The value comes back tagged (Variant convention).",
515
+ inputSchema: {
516
+ path: z.string().describe("Resource res:// path"),
517
+ property: z.string().describe("Property name"),
518
+ },
519
+ }, async ({ path, property }) => call("resource.get_property", { path, property }));
520
+ server.registerTool("resource_set_property", {
521
+ title: "Set resource property",
522
+ description: "Set a single property on a resource file and save it. DESTRUCTIVE (writes a file) — gated by confirmation. The value uses the tagged-Variant convention.",
523
+ inputSchema: {
524
+ path: z.string().describe("Resource res:// path"),
525
+ property: z.string().describe("Property name"),
526
+ value: z.any().describe("New value (JSON scalar or __type__-tagged Variant)"),
527
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
528
+ },
529
+ }, async ({ path, property, value, confirm }) => {
530
+ const blocked = await gate(server, confirm, `Set ${property} on ${path}`);
531
+ if (blocked)
532
+ return blocked;
533
+ return call("resource.set_property", { path, property, value });
534
+ });
535
+ server.registerTool("resource_get_import_settings", {
536
+ title: "Get import settings",
537
+ description: "Read an asset's import metadata (.import): the importer and its parameters. Read-only. Returns imported=false when the asset has no .import sidecar.",
538
+ inputSchema: { path: z.string().describe("Asset res:// path (e.g. res://icon.png)") },
539
+ }, async ({ path }) => call("resource.get_import_settings", { path }));
540
+ server.registerTool("resource_set_import_settings", {
541
+ title: "Set import settings",
542
+ description: "Update import parameters in an asset's .import metadata and trigger a reimport. DESTRUCTIVE (rewrites metadata + reimports) — gated by confirmation. Errors if the asset has no .import sidecar.",
543
+ inputSchema: {
544
+ path: z.string().describe("Asset res:// path"),
545
+ settings: z.record(z.any()).describe("Import params to set (name -> JSON scalar or __type__-tagged Variant)"),
546
+ reimport: z.boolean().optional().describe("Reimport after writing (default true)"),
547
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
548
+ },
549
+ }, async ({ path, settings, reimport, confirm }) => {
550
+ const blocked = await gate(server, confirm, `Set import settings on ${path} and reimport`);
551
+ if (blocked)
552
+ return blocked;
553
+ return call("resource.set_import_settings", reimport !== undefined ? { path, settings, reimport } : { path, settings });
554
+ });
555
+ // ---- Group B: filesystem (operations.gd _filesystem_*) ----
556
+ server.registerTool("filesystem_list", {
557
+ title: "List project directory",
558
+ description: "List the subdirectories and files of a directory in the project filesystem (hidden entries like .godot are skipped). Read-only.",
559
+ inputSchema: { path: z.string().optional().describe("Directory res:// path (default res://)") },
560
+ }, async ({ path }) => call("filesystem.list", path !== undefined ? { path } : {}));
561
+ server.registerTool("filesystem_scan", {
562
+ title: "Rescan filesystem",
563
+ description: "Trigger an editor rescan of the project filesystem so newly added or externally-changed files are picked up. Read-only side effect.",
564
+ inputSchema: {},
565
+ }, async () => call("filesystem.scan"));
566
+ server.registerTool("filesystem_move", {
567
+ title: "Move / rename file",
568
+ description: "Move or rename a file or directory within the project (carrying its .import sidecar), then rescan. DESTRUCTIVE (moves on disk; does NOT remap references in other resources) — gated by confirmation.",
569
+ inputSchema: {
570
+ from_path: z.string().describe("Source res:// path (file or directory)"),
571
+ to_path: z.string().describe("Destination res:// path"),
572
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
573
+ },
574
+ }, async ({ from_path, to_path, confirm }) => {
575
+ const blocked = await gate(server, confirm, `Move ${from_path} to ${to_path}`);
576
+ if (blocked)
577
+ return blocked;
578
+ return call("filesystem.move", { from_path, to_path });
579
+ });
580
+ server.registerTool("filesystem_create_dir", {
581
+ title: "Create directory",
582
+ description: "Create a directory (recursively) in the project filesystem, then rescan. No-op if it already exists.",
583
+ inputSchema: { path: z.string().describe("Directory res:// path to create") },
584
+ }, async ({ path }) => call("filesystem.create_dir", { path }));
585
+ // ---- Group C: animation authoring (undoable in-scene mutations) ----
586
+ server.registerTool("anim_player_create", {
587
+ title: "Create AnimationPlayer",
588
+ description: "Add an AnimationPlayer node under a parent (undoable). Seeds an empty default animation library so anim_create works immediately.",
589
+ inputSchema: {
590
+ parent_path: z.string().describe("Parent node path relative to the scene root; \".\" for the root"),
591
+ name: z.string().optional().describe("Node name (default \"AnimationPlayer\")"),
592
+ },
593
+ }, async ({ parent_path, name }) => call("anim.player_create", { parent_path, name }));
594
+ server.registerTool("anim_create", {
595
+ title: "Create animation",
596
+ description: "Create an empty Animation in an AnimationPlayer's library (undoable). Creates the library if it does not exist yet.",
597
+ inputSchema: {
598
+ player_path: z.string().describe("AnimationPlayer node path relative to the scene root"),
599
+ name: z.string().describe("Animation name (unique within its library)"),
600
+ library: z.string().optional().describe("Animation library name (default \"\", the player's default library)"),
601
+ },
602
+ }, async ({ player_path, name, library }) => call("anim.create", { player_path, name, library: library ?? "" }));
603
+ server.registerTool("anim_delete", {
604
+ title: "Delete animation",
605
+ description: "Delete an Animation from an AnimationPlayer's library (undoable). DESTRUCTIVE — gated by confirmation.",
606
+ inputSchema: {
607
+ player_path: z.string().describe("AnimationPlayer node path relative to the scene root"),
608
+ name: z.string().describe("Animation name"),
609
+ library: z.string().optional().describe("Animation library name (default \"\")"),
610
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
611
+ },
612
+ }, async ({ player_path, name, library, confirm }) => {
613
+ const blocked = await gate(server, confirm, `Delete animation "${name}"`);
614
+ if (blocked)
615
+ return blocked;
616
+ return call("anim.delete", { player_path, name, library: library ?? "" });
617
+ });
618
+ server.registerTool("anim_add_track", {
619
+ title: "Add animation track",
620
+ description: "Add a track to an Animation and set its target path (undoable). Returns the new track index.",
621
+ inputSchema: {
622
+ player_path: z.string().describe("AnimationPlayer node path"),
623
+ name: z.string().describe("Animation name"),
624
+ path: z.string().describe("Track target: a node path, or \"Node:property\" for value tracks (e.g. \"Sprite2D:position\")"),
625
+ type: z
626
+ .string()
627
+ .optional()
628
+ .describe("Track type: value (default), position_3d, rotation_3d, scale_3d, blend_shape, method, bezier, audio, animation"),
629
+ library: z.string().optional().describe("Animation library name (default \"\")"),
630
+ },
631
+ }, async ({ player_path, name, path, type, library }) => call("anim.add_track", { player_path, name, path, type: type ?? "value", library: library ?? "" }));
632
+ server.registerTool("anim_insert_key", {
633
+ title: "Insert animation key",
634
+ description: "Insert a keyframe on a track at a given time (undoable). A key already at that exact time is overwritten.",
635
+ inputSchema: {
636
+ player_path: z.string().describe("AnimationPlayer node path"),
637
+ name: z.string().describe("Animation name"),
638
+ track: z.number().int().describe("Track index"),
639
+ time: z.number().describe("Key time in seconds"),
640
+ value: z.any().describe("Key value (JSON scalar, array, object, or a __type__-tagged Variant matching the track type)"),
641
+ transition: z.number().optional().describe("Transition curve exponent (default 1.0)"),
642
+ library: z.string().optional().describe("Animation library name (default \"\")"),
643
+ },
644
+ }, async ({ player_path, name, track, time, value, transition, library }) => call("anim.insert_key", { player_path, name, track, time, value, transition: transition ?? 1.0, library: library ?? "" }));
645
+ server.registerTool("anim_remove_key", {
646
+ title: "Remove animation key",
647
+ description: "Remove a keyframe by index from a track (undoable).",
648
+ inputSchema: {
649
+ player_path: z.string().describe("AnimationPlayer node path"),
650
+ name: z.string().describe("Animation name"),
651
+ track: z.number().int().describe("Track index"),
652
+ key: z.number().int().describe("Key index within the track"),
653
+ library: z.string().optional().describe("Animation library name (default \"\")"),
654
+ },
655
+ }, async ({ player_path, name, track, key, library }) => call("anim.remove_key", { player_path, name, track, key, library: library ?? "" }));
656
+ server.registerTool("anim_set_length", {
657
+ title: "Set animation length",
658
+ description: "Set an Animation's length in seconds (undoable).",
659
+ inputSchema: {
660
+ player_path: z.string().describe("AnimationPlayer node path"),
661
+ name: z.string().describe("Animation name"),
662
+ length: z.number().describe("New length in seconds (> 0)"),
663
+ library: z.string().optional().describe("Animation library name (default \"\")"),
664
+ },
665
+ }, async ({ player_path, name, length, library }) => call("anim.set_length", { player_path, name, length, library: library ?? "" }));
666
+ server.registerTool("anim_set_loop", {
667
+ title: "Set animation loop mode",
668
+ description: "Set an Animation's loop mode (undoable).",
669
+ inputSchema: {
670
+ player_path: z.string().describe("AnimationPlayer node path"),
671
+ name: z.string().describe("Animation name"),
672
+ mode: z.string().describe("Loop mode: none, linear, or pingpong"),
673
+ library: z.string().optional().describe("Animation library name (default \"\")"),
674
+ },
675
+ }, async ({ player_path, name, mode, library }) => call("anim.set_loop", { player_path, name, mode, library: library ?? "" }));
676
+ server.registerTool("anim_get_track_keys", {
677
+ title: "Get animation track keys",
678
+ description: "List all keyframes on a track (index, time, value, transition). Read-only.",
679
+ inputSchema: {
680
+ player_path: z.string().describe("AnimationPlayer node path"),
681
+ name: z.string().describe("Animation name"),
682
+ track: z.number().int().describe("Track index"),
683
+ library: z.string().optional().describe("Animation library name (default \"\")"),
684
+ },
685
+ }, async ({ player_path, name, track, library }) => call("anim.get_track_keys", { player_path, name, track, library: library ?? "" }));
686
+ server.registerTool("anim_list", {
687
+ title: "List animations",
688
+ description: "List all animations in an AnimationPlayer across its libraries, with length, loop mode, and track count. Read-only.",
689
+ inputSchema: {
690
+ player_path: z.string().describe("AnimationPlayer node path"),
691
+ },
692
+ }, async ({ player_path }) => call("anim.list", { player_path }));
693
+ // ---- Group C batch 2: AnimationTree + state machine (undoable in-scene) ----
694
+ server.registerTool("anim_tree_create", {
695
+ title: "Create AnimationTree",
696
+ description: "Add an AnimationTree node under a parent (undoable) with a fresh tree_root graph. root_type \"blend_tree\" (AnimationNodeBlendTree) or \"state_machine\" (AnimationNodeStateMachine). Created inactive by default; set anim_player_path to the AnimationPlayer it should drive.",
697
+ inputSchema: {
698
+ parent_path: z.string().describe("Parent node path relative to the scene root; \".\" for the root"),
699
+ name: z.string().optional().describe("Node name (default \"AnimationTree\")"),
700
+ root_type: z.enum(["blend_tree", "state_machine"]).optional().describe("tree_root graph type (default blend_tree)"),
701
+ anim_player_path: z.string().optional().describe("NodePath to the AnimationPlayer this tree drives, relative to the AnimationTree node"),
702
+ active: z.boolean().optional().describe("Whether the tree processes immediately (default false)"),
703
+ },
704
+ }, async ({ parent_path, name, root_type, anim_player_path, active }) => call("anim.tree_create", { parent_path, name, root_type: root_type ?? "blend_tree", anim_player_path: anim_player_path ?? "", active: active ?? false }));
705
+ server.registerTool("anim_tree_add_node", {
706
+ title: "Add AnimationTree graph node",
707
+ description: "Add a node to an AnimationTree's tree_root graph (AnimationNodeBlendTree or AnimationNodeStateMachine), undoable. node_type is any AnimationNode subclass (e.g. AnimationNodeAnimation, AnimationNodeBlend2, AnimationNodeStateMachine). For AnimationNodeAnimation, pass animation to bind a clip.",
708
+ inputSchema: {
709
+ tree_path: z.string().describe("AnimationTree node path relative to the scene root"),
710
+ node_name: z.string().describe("Unique node name within the graph"),
711
+ node_type: z.string().describe("AnimationNode subclass to instantiate (e.g. AnimationNodeAnimation)"),
712
+ animation: z.string().optional().describe("For AnimationNodeAnimation: the animation name to play"),
713
+ position: z.array(z.number()).optional().describe("Graph editor position [x, y] (default [0, 0])"),
714
+ },
715
+ }, async ({ tree_path, node_name, node_type, animation, position }) => call("anim.tree_add_node", { tree_path, node_name, node_type, animation, position }));
716
+ server.registerTool("anim_statemachine_add_state", {
717
+ title: "Add state machine state",
718
+ description: "Add a state to an AnimationNodeStateMachine (undoable). Targets the AnimationTree's tree_root when it is a state machine, or a nested state-machine node via state_machine. Defaults the state to an AnimationNodeAnimation; pass animation to bind a clip.",
719
+ inputSchema: {
720
+ tree_path: z.string().describe("AnimationTree node path relative to the scene root"),
721
+ state_name: z.string().describe("Unique state name within the state machine"),
722
+ animation: z.string().optional().describe("For an AnimationNodeAnimation state: the animation name to play"),
723
+ node_type: z.string().optional().describe("AnimationNode subclass for the state (default AnimationNodeAnimation)"),
724
+ state_machine: z.string().optional().describe("Name of a nested AnimationNodeStateMachine node within tree_root; omit to target tree_root itself"),
725
+ position: z.array(z.number()).optional().describe("Graph editor position [x, y] (default [0, 0])"),
726
+ },
727
+ }, async ({ tree_path, state_name, animation, node_type, state_machine, position }) => call("anim.statemachine_add_state", { tree_path, state_name, animation, node_type: node_type ?? "AnimationNodeAnimation", state_machine: state_machine ?? "", position }));
728
+ server.registerTool("anim_statemachine_add_transition", {
729
+ title: "Add state machine transition",
730
+ description: "Add a transition between two states in an AnimationNodeStateMachine (undoable). from_state/to_state must exist (or be the built-in \"Start\"/\"End\"). switch_mode: immediate|sync|at_end; advance_mode: disabled|enabled|auto.",
731
+ inputSchema: {
732
+ tree_path: z.string().describe("AnimationTree node path relative to the scene root"),
733
+ from_state: z.string().describe("Source state name (or \"Start\")"),
734
+ to_state: z.string().describe("Destination state name (or \"End\")"),
735
+ state_machine: z.string().optional().describe("Name of a nested AnimationNodeStateMachine node within tree_root; omit to target tree_root itself"),
736
+ xfade_time: z.number().optional().describe("Cross-fade time in seconds (default 0)"),
737
+ switch_mode: z.enum(["immediate", "sync", "at_end"]).optional().describe("Switch mode (default immediate)"),
738
+ advance_mode: z.enum(["disabled", "enabled", "auto"]).optional().describe("Advance mode (default enabled)"),
739
+ advance_condition: z.string().optional().describe("Advance condition parameter name (used with advance_mode auto)"),
740
+ priority: z.number().int().optional().describe("Transition priority (lower wins when multiple are valid)"),
741
+ },
742
+ }, async ({ tree_path, from_state, to_state, state_machine, xfade_time, switch_mode, advance_mode, advance_condition, priority }) => call("anim.statemachine_add_transition", { tree_path, from_state, to_state, state_machine: state_machine ?? "", xfade_time: xfade_time ?? 0.0, switch_mode: switch_mode ?? "immediate", advance_mode: advance_mode ?? "enabled", advance_condition: advance_condition ?? "", priority }));
743
+ // ---- Group D: TileSet (operations.gd _tileset_*; disk-backed .tres, gated writers) ----
744
+ server.registerTool("tileset_create", {
745
+ title: "Create TileSet",
746
+ description: "Instantiate a TileSet resource and save it as a new .tres file. DESTRUCTIVE (writes a file) — gated by confirmation. tile_size is the base grid cell size in pixels (default 16×16).",
747
+ inputSchema: {
748
+ to_path: z.string().describe("Destination res:// path, e.g. res://tiles/world.tres"),
749
+ tile_size: z.array(z.number().int()).optional().describe("Base tile grid size [x, y] in pixels (default [16, 16])"),
750
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
751
+ },
752
+ }, async ({ to_path, tile_size, confirm }) => {
753
+ const blocked = await gate(server, confirm, `Create TileSet resource at ${to_path}`);
754
+ if (blocked)
755
+ return blocked;
756
+ return call("tileset.create", tile_size !== undefined ? { to_path, tile_size } : { to_path });
757
+ });
758
+ server.registerTool("tileset_add_source", {
759
+ title: "Add TileSet atlas source",
760
+ description: "Add a TileSetAtlasSource (backed by a Texture2D) to a TileSet .tres and re-save. DESTRUCTIVE (writes a file) — gated by confirmation. texture_region_size defaults to the TileSet's tile_size; source_id -1 auto-assigns.",
761
+ inputSchema: {
762
+ tileset_path: z.string().describe("TileSet res:// .tres path"),
763
+ texture_path: z.string().describe("Texture2D res:// path used as the atlas image"),
764
+ texture_region_size: z.array(z.number().int()).optional().describe("Atlas cell size [x, y] in pixels (default = tile_size)"),
765
+ source_id: z.number().int().optional().describe("Explicit source id, or -1 to auto-assign (default -1)"),
766
+ margins: z.array(z.number().int()).optional().describe("Atlas margins [x, y] in pixels"),
767
+ separation: z.array(z.number().int()).optional().describe("Atlas separation [x, y] in pixels"),
768
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
769
+ },
770
+ }, async ({ tileset_path, texture_path, texture_region_size, source_id, margins, separation, confirm }) => {
771
+ const blocked = await gate(server, confirm, `Add atlas source (${texture_path}) to TileSet ${tileset_path}`);
772
+ if (blocked)
773
+ return blocked;
774
+ const params = { tileset_path, texture_path };
775
+ if (texture_region_size !== undefined)
776
+ params.texture_region_size = texture_region_size;
777
+ if (source_id !== undefined)
778
+ params.source_id = source_id;
779
+ if (margins !== undefined)
780
+ params.margins = margins;
781
+ if (separation !== undefined)
782
+ params.separation = separation;
783
+ return call("tileset.add_source", params);
784
+ });
785
+ server.registerTool("tileset_add_tile", {
786
+ title: "Add TileSet tile",
787
+ description: "Create a tile at atlas_coords in an atlas source of a TileSet .tres and re-save. DESTRUCTIVE (writes a file) — gated by confirmation. size is measured in atlas cells (default [1, 1]).",
788
+ inputSchema: {
789
+ tileset_path: z.string().describe("TileSet res:// .tres path"),
790
+ source_id: z.number().int().describe("Atlas source id within the TileSet"),
791
+ atlas_coords: z.array(z.number().int()).describe("Tile atlas coordinates [x, y] (in cells)"),
792
+ size: z.array(z.number().int()).optional().describe("Tile size in atlas cells [x, y] (default [1, 1])"),
793
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
794
+ },
795
+ }, async ({ tileset_path, source_id, atlas_coords, size, confirm }) => {
796
+ const blocked = await gate(server, confirm, `Add tile ${JSON.stringify(atlas_coords)} to source ${source_id} in ${tileset_path}`);
797
+ if (blocked)
798
+ return blocked;
799
+ const params = { tileset_path, source_id, atlas_coords };
800
+ if (size !== undefined)
801
+ params.size = size;
802
+ return call("tileset.add_tile", params);
803
+ });
804
+ server.registerTool("tileset_set_tile_collision", {
805
+ title: "Set tile collision polygon",
806
+ description: "Add a collision polygon to a tile on a TileSet physics layer and re-save. DESTRUCTIVE (writes a file) — gated by confirmation. Physics layers are created as needed. polygon is an array of [x, y] points (>= 3), tile-local pixels.",
807
+ inputSchema: {
808
+ tileset_path: z.string().describe("TileSet res:// .tres path"),
809
+ source_id: z.number().int().describe("Atlas source id within the TileSet"),
810
+ atlas_coords: z.array(z.number().int()).describe("Tile atlas coordinates [x, y] (in cells)"),
811
+ polygon: z.array(z.array(z.number())).describe("Collision polygon points [[x, y], ...] (>= 3), tile-local pixels"),
812
+ physics_layer: z.number().int().optional().describe("TileSet physics layer index (default 0; created if missing)"),
813
+ one_way: z.boolean().optional().describe("Mark the polygon as one-way collision"),
814
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
815
+ },
816
+ }, async ({ tileset_path, source_id, atlas_coords, polygon, physics_layer, one_way, confirm }) => {
817
+ const blocked = await gate(server, confirm, `Set collision on tile ${JSON.stringify(atlas_coords)} in ${tileset_path}`);
818
+ if (blocked)
819
+ return blocked;
820
+ const params = { tileset_path, source_id, atlas_coords, polygon };
821
+ if (physics_layer !== undefined)
822
+ params.physics_layer = physics_layer;
823
+ if (one_way !== undefined)
824
+ params.one_way = one_way;
825
+ return call("tileset.set_tile_collision", params);
826
+ });
827
+ // ---- Group D batch 2: TileMapLayer + cell painting (operations.gd _tilemap*; in-scene, undoable, ungated) ----
828
+ server.registerTool("tilemaplayer_create", {
829
+ title: "Create TileMapLayer",
830
+ description: "Add a TileMapLayer node under a parent in the edited scene (undoable), optionally binding a TileSet .tres so cells can be painted. In-scene and undoable — not gated.",
831
+ inputSchema: {
832
+ parent_path: z.string().describe("Parent node path relative to the scene root; \".\" for the root"),
833
+ name: z.string().optional().describe("Node name (default \"TileMapLayer\")"),
834
+ tileset_path: z.string().optional().describe("TileSet res:// .tres path to bind as tile_set (e.g. from tileset_create)"),
835
+ },
836
+ }, async ({ parent_path, name, tileset_path }) => {
837
+ const params = { parent_path };
838
+ if (name !== undefined)
839
+ params.name = name;
840
+ if (tileset_path !== undefined)
841
+ params.tileset_path = tileset_path;
842
+ return call("tilemaplayer.create", params);
843
+ });
844
+ server.registerTool("tilemap_set_cell", {
845
+ title: "Set TileMapLayer cell",
846
+ description: "Paint a single cell on a TileMapLayer (undoable). source_id -1 erases the cell (default). atlas_coords defaults to [0, 0]; alternative defaults to 0.",
847
+ inputSchema: {
848
+ path: z.string().describe("TileMapLayer node path relative to the scene root"),
849
+ coords: z.array(z.number().int()).describe("Cell coordinates [x, y] (in cells)"),
850
+ source_id: z.number().int().optional().describe("Atlas source id from the bound TileSet, or -1 to erase (default -1)"),
851
+ atlas_coords: z.array(z.number().int()).optional().describe("Tile atlas coordinates [x, y] within the source (default [0, 0])"),
852
+ alternative: z.number().int().optional().describe("Alternative tile id (default 0)"),
853
+ },
854
+ }, async ({ path, coords, source_id, atlas_coords, alternative }) => {
855
+ const params = { path, coords };
856
+ if (source_id !== undefined)
857
+ params.source_id = source_id;
858
+ if (atlas_coords !== undefined)
859
+ params.atlas_coords = atlas_coords;
860
+ if (alternative !== undefined)
861
+ params.alternative = alternative;
862
+ return call("tilemap.set_cell", params);
863
+ });
864
+ server.registerTool("tilemap_set_cells_rect", {
865
+ title: "Fill TileMapLayer cells (rect)",
866
+ description: "Paint every cell in a rectangular region of a TileMapLayer with one tile (undoable, one action). source_id -1 clears the region. Capped at 65536 cells; split larger fills across calls.",
867
+ inputSchema: {
868
+ path: z.string().describe("TileMapLayer node path relative to the scene root"),
869
+ rect: z.array(z.number().int()).describe("Region [x, y, width, height] in cells (width/height > 0)"),
870
+ source_id: z.number().int().optional().describe("Atlas source id from the bound TileSet, or -1 to clear (default -1)"),
871
+ atlas_coords: z.array(z.number().int()).optional().describe("Tile atlas coordinates [x, y] within the source (default [0, 0])"),
872
+ alternative: z.number().int().optional().describe("Alternative tile id (default 0)"),
873
+ },
874
+ }, async ({ path, rect, source_id, atlas_coords, alternative }) => {
875
+ const params = { path, rect };
876
+ if (source_id !== undefined)
877
+ params.source_id = source_id;
878
+ if (atlas_coords !== undefined)
879
+ params.atlas_coords = atlas_coords;
880
+ if (alternative !== undefined)
881
+ params.alternative = alternative;
882
+ return call("tilemap.set_cells_rect", params);
883
+ });
884
+ server.registerTool("tilemap_get_cell", {
885
+ title: "Get TileMapLayer cell",
886
+ description: "Read one cell of a TileMapLayer. An empty cell reads back as source_id -1, atlas_coords [-1, -1], alternative 0 (empty = true).",
887
+ inputSchema: {
888
+ path: z.string().describe("TileMapLayer node path relative to the scene root"),
889
+ coords: z.array(z.number().int()).describe("Cell coordinates [x, y] (in cells)"),
890
+ },
891
+ }, async ({ path, coords }) => call("tilemap.get_cell", { path, coords }));
892
+ server.registerTool("tilemap_clear", {
893
+ title: "Clear TileMapLayer",
894
+ description: "Remove every painted cell from a TileMapLayer (undoable — prior cells are restored on undo). Returns the number of cells cleared.",
895
+ inputSchema: {
896
+ path: z.string().describe("TileMapLayer node path relative to the scene root"),
897
+ },
898
+ }, async ({ path }) => call("tilemap.clear", { path }));
899
+ // ---- Group E: Physics & collision (operations.gd _body_*/_collisionshape_add; in-scene, undoable, ungated) ----
900
+ server.registerTool("body_create", {
901
+ title: "Create physics body",
902
+ description: "Add a physics body node under a parent in the edited scene (undoable): a StaticBody, RigidBody, CharacterBody, or Area, in 2D or 3D. In-scene and undoable — not gated. Attach collision shapes with collisionshape_add.",
903
+ inputSchema: {
904
+ parent_path: z.string().describe("Parent node path relative to the scene root; \".\" for the root"),
905
+ type: z.enum(["static", "rigid", "character", "area"]).describe("Body kind: static | rigid | character | area"),
906
+ dim: z.enum(["2d", "3d"]).optional().describe("Dimension: \"2d\" (default) or \"3d\""),
907
+ name: z.string().optional().describe("Node name (default the class name, e.g. \"StaticBody2D\")"),
908
+ },
909
+ }, async ({ parent_path, type, dim, name }) => {
910
+ const params = { parent_path, type };
911
+ if (dim !== undefined)
912
+ params.dim = dim;
913
+ if (name !== undefined)
914
+ params.name = name;
915
+ return call("body.create", params);
916
+ });
917
+ server.registerTool("collisionshape_add", {
918
+ title: "Add collision shape",
919
+ description: "Add a CollisionShape2D/3D node carrying a shape resource under a parent (usually a body) in the edited scene (undoable). shape is rect | circle | capsule | polygon; dim selects 2D (Rectangle/Circle/Capsule/ConvexPolygon 2D) or 3D (Box/Sphere/Capsule/ConvexPolygon 3D). In-scene and undoable — not gated.",
920
+ inputSchema: {
921
+ parent_path: z.string().describe("Parent node path (usually a body) relative to the scene root; \".\" for the root"),
922
+ shape: z.enum(["rect", "circle", "capsule", "polygon"]).describe("Shape kind: rect | circle | capsule | polygon"),
923
+ dim: z.enum(["2d", "3d"]).optional().describe("Dimension: \"2d\" (default) or \"3d\""),
924
+ name: z.string().optional().describe("Node name (default \"CollisionShape2D\"/\"CollisionShape3D\")"),
925
+ size: z.array(z.number()).optional().describe("rect: [w, h] (2D) or [w, h, d] (3D)"),
926
+ radius: z.number().optional().describe("circle/capsule radius"),
927
+ height: z.number().optional().describe("capsule height"),
928
+ points: z.array(z.array(z.number())).optional().describe("polygon: convex-hull points, [[x, y], …] (2D, ≥3) or [[x, y, z], …] (3D, ≥4)"),
929
+ },
930
+ }, async ({ parent_path, shape, dim, name, size, radius, height, points }) => {
931
+ const params = { parent_path, shape };
932
+ if (dim !== undefined)
933
+ params.dim = dim;
934
+ if (name !== undefined)
935
+ params.name = name;
936
+ if (size !== undefined)
937
+ params.size = size;
938
+ if (radius !== undefined)
939
+ params.radius = radius;
940
+ if (height !== undefined)
941
+ params.height = height;
942
+ if (points !== undefined)
943
+ params.points = points;
944
+ return call("collisionshape.add", params);
945
+ });
946
+ server.registerTool("body_set_collision_layer", {
947
+ title: "Set collision layer",
948
+ description: "Set the collision_layer bitmask on a physics body or area (CollisionObject2D/3D) in the edited scene (undoable). layer is the integer bitmask of layers this object occupies.",
949
+ inputSchema: {
950
+ path: z.string().describe("Body/Area node path relative to the scene root"),
951
+ layer: z.number().int().describe("collision_layer bitmask (non-negative integer)"),
952
+ },
953
+ }, async ({ path, layer }) => call("body.set_collision_layer", { path, layer }));
954
+ server.registerTool("body_set_collision_mask", {
955
+ title: "Set collision mask",
956
+ description: "Set the collision_mask bitmask on a physics body or area (CollisionObject2D/3D) in the edited scene (undoable). mask is the integer bitmask of layers this object scans for collisions.",
957
+ inputSchema: {
958
+ path: z.string().describe("Body/Area node path relative to the scene root"),
959
+ mask: z.number().int().describe("collision_mask bitmask (non-negative integer)"),
960
+ },
961
+ }, async ({ path, mask }) => call("body.set_collision_mask", { path, mask }));
962
+ // ---- Group E batch 2: areas, joints, collision polygons, rigidbody tuning, physics material (in-scene, undoable, ungated) + project gravity (gated) ----
963
+ server.registerTool("area_set_monitoring", {
964
+ title: "Set area monitoring",
965
+ description: "Set monitoring and/or monitorable on an Area2D/3D in the edited scene (undoable). monitoring = the area detects overlapping bodies/areas; monitorable = other areas can detect it. In-scene and undoable — not gated.",
966
+ inputSchema: {
967
+ path: z.string().describe("Area2D/3D node path relative to the scene root"),
968
+ monitoring: z.boolean().optional().describe("Whether the area detects overlapping bodies/areas"),
969
+ monitorable: z.boolean().optional().describe("Whether other areas can detect this area"),
970
+ },
971
+ }, async ({ path, monitoring, monitorable }) => {
972
+ const params = { path };
973
+ if (monitoring !== undefined)
974
+ params.monitoring = monitoring;
975
+ if (monitorable !== undefined)
976
+ params.monitorable = monitorable;
977
+ return call("area.set_monitoring", params);
978
+ });
979
+ server.registerTool("area_set_gravity", {
980
+ title: "Set area gravity",
981
+ description: "Set the local gravity override of an Area2D/3D in the edited scene (undoable): space_override mode, gravity magnitude, direction, and whether gravity points toward a center (point). In-scene and undoable — not gated.",
982
+ inputSchema: {
983
+ path: z.string().describe("Area2D/3D node path relative to the scene root"),
984
+ space_override: z.enum(["disabled", "combine", "combine_replace", "replace", "replace_combine"]).optional().describe("How this area's gravity combines with the global/other areas"),
985
+ gravity: z.number().optional().describe("Gravity magnitude (units/s^2)"),
986
+ direction: z.array(z.number()).optional().describe("Gravity direction [x, y] (2D) or [x, y, z] (3D)"),
987
+ point: z.boolean().optional().describe("If true, gravity pulls toward the area's gravity point instead of a direction"),
988
+ },
989
+ }, async ({ path, space_override, gravity, direction, point }) => {
990
+ const params = { path };
991
+ if (space_override !== undefined)
992
+ params.space_override = space_override;
993
+ if (gravity !== undefined)
994
+ params.gravity = gravity;
995
+ if (direction !== undefined)
996
+ params.direction = direction;
997
+ if (point !== undefined)
998
+ params.point = point;
999
+ return call("area.set_gravity", params);
1000
+ });
1001
+ server.registerTool("joint_create", {
1002
+ title: "Create physics joint",
1003
+ description: "Add a physics joint node under a parent in the edited scene (undoable). 2D types: pin | groove | spring (PinJoint2D/GrooveJoint2D/DampedSpringJoint2D); 3D types: pin | hinge | slider | cone_twist | generic6dof (PinJoint3D/HingeJoint3D/SliderJoint3D/ConeTwistJoint3D/Generic6DOFJoint3D). Optionally wire node_a/node_b to the two bodies. In-scene and undoable — not gated.",
1004
+ inputSchema: {
1005
+ parent_path: z.string().describe("Parent node path relative to the scene root; \".\" for the root"),
1006
+ type: z.enum(["pin", "groove", "spring", "hinge", "slider", "cone_twist", "generic6dof"]).describe("Joint kind; pin works in both dims, the rest are dim-specific"),
1007
+ dim: z.enum(["2d", "3d"]).optional().describe("Dimension: \"2d\" (default) or \"3d\""),
1008
+ name: z.string().optional().describe("Node name (default the class name, e.g. \"PinJoint2D\")"),
1009
+ node_a: z.string().optional().describe("NodePath (relative to the joint) of the first body"),
1010
+ node_b: z.string().optional().describe("NodePath (relative to the joint) of the second body"),
1011
+ },
1012
+ }, async ({ parent_path, type, dim, name, node_a, node_b }) => {
1013
+ const params = { parent_path, type };
1014
+ if (dim !== undefined)
1015
+ params.dim = dim;
1016
+ if (name !== undefined)
1017
+ params.name = name;
1018
+ if (node_a !== undefined)
1019
+ params.node_a = node_a;
1020
+ if (node_b !== undefined)
1021
+ params.node_b = node_b;
1022
+ return call("joint.create", params);
1023
+ });
1024
+ server.registerTool("joint_set_bodies", {
1025
+ title: "Set joint bodies",
1026
+ description: "Set node_a and/or node_b on an existing Joint2D/3D in the edited scene (undoable) — the NodePaths of the two bodies the joint connects, relative to the joint node. In-scene and undoable — not gated.",
1027
+ inputSchema: {
1028
+ path: z.string().describe("Joint2D/3D node path relative to the scene root"),
1029
+ node_a: z.string().optional().describe("NodePath (relative to the joint) of the first body"),
1030
+ node_b: z.string().optional().describe("NodePath (relative to the joint) of the second body"),
1031
+ },
1032
+ }, async ({ path, node_a, node_b }) => {
1033
+ const params = { path };
1034
+ if (node_a !== undefined)
1035
+ params.node_a = node_a;
1036
+ if (node_b !== undefined)
1037
+ params.node_b = node_b;
1038
+ return call("joint.set_bodies", params);
1039
+ });
1040
+ server.registerTool("collisionpolygon_add", {
1041
+ title: "Add collision polygon",
1042
+ description: "Add a CollisionPolygon2D/3D node carrying a polygon under a parent (usually a body) in the edited scene (undoable). points is a 2D outline [[x, y], …] (≥3); for 3D it is extruded along Z by depth. 2D build_mode is solids | segments. In-scene and undoable — not gated.",
1043
+ inputSchema: {
1044
+ parent_path: z.string().describe("Parent node path (usually a body) relative to the scene root; \".\" for the root"),
1045
+ points: z.array(z.array(z.number())).describe("Polygon outline points [[x, y], …] (≥3); a 2D outline even for 3D"),
1046
+ dim: z.enum(["2d", "3d"]).optional().describe("Dimension: \"2d\" (default) or \"3d\""),
1047
+ name: z.string().optional().describe("Node name (default \"CollisionPolygon2D\"/\"CollisionPolygon3D\")"),
1048
+ build_mode: z.enum(["solids", "segments"]).optional().describe("2D only: solids (default) or segments"),
1049
+ depth: z.number().optional().describe("3D only: extrusion depth along Z (default 1.0)"),
1050
+ },
1051
+ }, async ({ parent_path, points, dim, name, build_mode, depth }) => {
1052
+ const params = { parent_path, points };
1053
+ if (dim !== undefined)
1054
+ params.dim = dim;
1055
+ if (name !== undefined)
1056
+ params.name = name;
1057
+ if (build_mode !== undefined)
1058
+ params.build_mode = build_mode;
1059
+ if (depth !== undefined)
1060
+ params.depth = depth;
1061
+ return call("collisionpolygon.add", params);
1062
+ });
1063
+ server.registerTool("rigidbody_set_properties", {
1064
+ title: "Set rigidbody properties",
1065
+ description: "Tune a RigidBody2D/3D in the edited scene (undoable): mass (> 0), gravity_scale, linear_damp, angular_damp. Only the provided properties change. In-scene and undoable — not gated.",
1066
+ inputSchema: {
1067
+ path: z.string().describe("RigidBody2D/3D node path relative to the scene root"),
1068
+ mass: z.number().optional().describe("Body mass (> 0)"),
1069
+ gravity_scale: z.number().optional().describe("Multiplier on gravity (1 = normal, 0 = float)"),
1070
+ linear_damp: z.number().optional().describe("Linear damping (≥ 0)"),
1071
+ angular_damp: z.number().optional().describe("Angular damping (≥ 0)"),
1072
+ },
1073
+ }, async ({ path, mass, gravity_scale, linear_damp, angular_damp }) => {
1074
+ const params = { path };
1075
+ if (mass !== undefined)
1076
+ params.mass = mass;
1077
+ if (gravity_scale !== undefined)
1078
+ params.gravity_scale = gravity_scale;
1079
+ if (linear_damp !== undefined)
1080
+ params.linear_damp = linear_damp;
1081
+ if (angular_damp !== undefined)
1082
+ params.angular_damp = angular_damp;
1083
+ return call("rigidbody.set_properties", params);
1084
+ });
1085
+ server.registerTool("body_set_physics_material", {
1086
+ title: "Set body physics material",
1087
+ description: "Create a PhysicsMaterial and assign it as physics_material_override on a StaticBody/RigidBody (2D or 3D) in the edited scene (undoable): friction (default 1), bounce (default 0), rough, absorbent. In-scene and undoable — not gated.",
1088
+ inputSchema: {
1089
+ path: z.string().describe("StaticBody/RigidBody 2D/3D node path relative to the scene root"),
1090
+ friction: z.number().optional().describe("Surface friction 0..1 (default 1)"),
1091
+ bounce: z.number().optional().describe("Restitution/bounciness 0..1 (default 0)"),
1092
+ rough: z.boolean().optional().describe("Rough friction combine mode (default false)"),
1093
+ absorbent: z.boolean().optional().describe("Absorbent bounce combine mode (default false)"),
1094
+ },
1095
+ }, async ({ path, friction, bounce, rough, absorbent }) => {
1096
+ const params = { path };
1097
+ if (friction !== undefined)
1098
+ params.friction = friction;
1099
+ if (bounce !== undefined)
1100
+ params.bounce = bounce;
1101
+ if (rough !== undefined)
1102
+ params.rough = rough;
1103
+ if (absorbent !== undefined)
1104
+ params.absorbent = absorbent;
1105
+ return call("body.set_physics_material", params);
1106
+ });
1107
+ server.registerTool("physics_set_gravity", {
1108
+ title: "Set project gravity",
1109
+ description: "Write the project's default gravity for 2D or 3D (ProjectSettings physics/{2d,3d}/default_gravity + default_gravity_vector). Set save=true to persist to project.godot. DESTRUCTIVE (project-wide setting) — gated by confirmation.",
1110
+ inputSchema: {
1111
+ dim: z.enum(["2d", "3d"]).optional().describe("Which gravity to set: \"2d\" (default) or \"3d\""),
1112
+ magnitude: z.number().optional().describe("Gravity magnitude (default 2D 980, 3D 9.8)"),
1113
+ direction: z.array(z.number()).optional().describe("Gravity direction vector [x, y] (2D) or [x, y, z] (3D)"),
1114
+ save: z.boolean().optional().describe("Persist to project.godot (default false)"),
1115
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1116
+ },
1117
+ }, async ({ dim, magnitude, direction, save, confirm }) => {
1118
+ const blocked = await gate(server, confirm, `Set project ${dim ?? "2d"} gravity${save ? " and save project.godot" : ""}`);
1119
+ if (blocked)
1120
+ return blocked;
1121
+ const params = {};
1122
+ if (dim !== undefined)
1123
+ params.dim = dim;
1124
+ if (magnitude !== undefined)
1125
+ params.magnitude = magnitude;
1126
+ if (direction !== undefined)
1127
+ params.direction = direction;
1128
+ params.save = save ?? false;
1129
+ return call("physics.set_gravity", params);
1130
+ });
1131
+ server.registerTool("particles_create", {
1132
+ title: "Create particles",
1133
+ description: "Add a GPUParticles2D/3D node under a parent in the edited scene (undoable). Optional initial amount (> 0), lifetime (> 0), emitting. In-scene and undoable — not gated.",
1134
+ inputSchema: {
1135
+ parent_path: z.string().describe("Parent node path relative to the scene root; \".\" for the root"),
1136
+ dim: z.enum(["2d", "3d"]).optional().describe("Dimension: \"2d\" (default, GPUParticles2D) or \"3d\" (GPUParticles3D)"),
1137
+ name: z.string().optional().describe("Node name (default \"GPUParticles2D\"/\"GPUParticles3D\")"),
1138
+ amount: z.number().optional().describe("Number of particles (> 0, default 8)"),
1139
+ lifetime: z.number().optional().describe("Particle lifetime in seconds (> 0, default 1)"),
1140
+ emitting: z.boolean().optional().describe("Whether the system is emitting (default true)"),
1141
+ },
1142
+ }, async ({ parent_path, dim, name, amount, lifetime, emitting }) => {
1143
+ const params = { parent_path };
1144
+ if (dim !== undefined)
1145
+ params.dim = dim;
1146
+ if (name !== undefined)
1147
+ params.name = name;
1148
+ if (amount !== undefined)
1149
+ params.amount = amount;
1150
+ if (lifetime !== undefined)
1151
+ params.lifetime = lifetime;
1152
+ if (emitting !== undefined)
1153
+ params.emitting = emitting;
1154
+ return call("particles.create", params);
1155
+ });
1156
+ server.registerTool("particles_set_process_material", {
1157
+ title: "Set particles process material",
1158
+ description: "Create a ParticleProcessMaterial and assign it as process_material on a GPUParticles2D/3D in the edited scene (undoable). Optional knobs: gravity [x, y, z], direction [x, y, z], spread, initial_velocity_min/max, scale_min/max, color [r, g, b, a]. GPU particles need a process material to emit. In-scene and undoable — not gated.",
1159
+ inputSchema: {
1160
+ path: z.string().describe("GPUParticles2D/3D node path relative to the scene root"),
1161
+ gravity: z.array(z.number()).optional().describe("Gravity vector [x, y, z]"),
1162
+ direction: z.array(z.number()).optional().describe("Emission direction [x, y, z]"),
1163
+ spread: z.number().optional().describe("Emission spread in degrees"),
1164
+ initial_velocity_min: z.number().optional().describe("Minimum initial velocity"),
1165
+ initial_velocity_max: z.number().optional().describe("Maximum initial velocity"),
1166
+ scale_min: z.number().optional().describe("Minimum particle scale"),
1167
+ scale_max: z.number().optional().describe("Maximum particle scale"),
1168
+ color: z.array(z.number()).optional().describe("Base color [r, g, b] or [r, g, b, a] (0..1)"),
1169
+ },
1170
+ }, async ({ path, gravity, direction, spread, initial_velocity_min, initial_velocity_max, scale_min, scale_max, color }) => {
1171
+ const params = { path };
1172
+ if (gravity !== undefined)
1173
+ params.gravity = gravity;
1174
+ if (direction !== undefined)
1175
+ params.direction = direction;
1176
+ if (spread !== undefined)
1177
+ params.spread = spread;
1178
+ if (initial_velocity_min !== undefined)
1179
+ params.initial_velocity_min = initial_velocity_min;
1180
+ if (initial_velocity_max !== undefined)
1181
+ params.initial_velocity_max = initial_velocity_max;
1182
+ if (scale_min !== undefined)
1183
+ params.scale_min = scale_min;
1184
+ if (scale_max !== undefined)
1185
+ params.scale_max = scale_max;
1186
+ if (color !== undefined)
1187
+ params.color = color;
1188
+ return call("particles.set_process_material", params);
1189
+ });
1190
+ server.registerTool("particles_set_amount", {
1191
+ title: "Set particles amount",
1192
+ description: "Set the number of particles (amount, > 0) on a GPUParticles2D/3D in the edited scene (undoable). In-scene and undoable — not gated.",
1193
+ inputSchema: {
1194
+ path: z.string().describe("GPUParticles2D/3D node path relative to the scene root"),
1195
+ amount: z.number().describe("Number of particles (> 0)"),
1196
+ },
1197
+ }, async ({ path, amount }) => call("particles.set_amount", { path, amount }));
1198
+ server.registerTool("particles_set_lifetime", {
1199
+ title: "Set particles lifetime",
1200
+ description: "Set the particle lifetime in seconds (> 0) on a GPUParticles2D/3D in the edited scene (undoable). In-scene and undoable — not gated.",
1201
+ inputSchema: {
1202
+ path: z.string().describe("GPUParticles2D/3D node path relative to the scene root"),
1203
+ lifetime: z.number().describe("Lifetime in seconds (> 0)"),
1204
+ },
1205
+ }, async ({ path, lifetime }) => call("particles.set_lifetime", { path, lifetime }));
1206
+ server.registerTool("particles_set_emitting", {
1207
+ title: "Set particles emitting",
1208
+ description: "Toggle whether a GPUParticles2D/3D is emitting in the edited scene (undoable). In-scene and undoable — not gated.",
1209
+ inputSchema: {
1210
+ path: z.string().describe("GPUParticles2D/3D node path relative to the scene root"),
1211
+ emitting: z.boolean().describe("Whether the system is emitting"),
1212
+ },
1213
+ }, async ({ path, emitting }) => call("particles.set_emitting", { path, emitting }));
1214
+ server.registerTool("particles_set_texture", {
1215
+ title: "Set particles texture",
1216
+ description: "Load a Texture2D from a res:// path and assign it as texture on a GPUParticles2D in the edited scene (undoable). GPUParticles2D only — GPUParticles3D draws meshes and has no texture (returns unsupported). In-scene and undoable — not gated.",
1217
+ inputSchema: {
1218
+ path: z.string().describe("GPUParticles2D node path relative to the scene root"),
1219
+ texture_path: z.string().describe("res:// path to a Texture2D resource"),
1220
+ },
1221
+ }, async ({ path, texture_path }) => call("particles.set_texture", { path, texture_path }));
1222
+ server.registerTool("shader_create", {
1223
+ title: "Create shader",
1224
+ description: "Create a Shader resource with optional initial code and save it as a .gdshader file. DESTRUCTIVE (writes a file) — gated by confirmation.",
1225
+ inputSchema: {
1226
+ to_path: z.string().describe("Destination res:// path, e.g. res://shaders/glow.gdshader"),
1227
+ code: z.string().optional().describe("Initial GDShader source (e.g. \"shader_type canvas_item; ...\")"),
1228
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1229
+ },
1230
+ }, async ({ to_path, code, confirm }) => {
1231
+ const blocked = await gate(server, confirm, `Create shader at ${to_path}`);
1232
+ if (blocked)
1233
+ return blocked;
1234
+ return call("shader.create", code !== undefined ? { to_path, code } : { to_path });
1235
+ });
1236
+ server.registerTool("shader_set_code", {
1237
+ title: "Set shader code",
1238
+ description: "Replace the source code of an existing .gdshader resource and save it. DESTRUCTIVE (writes a file) — gated by confirmation.",
1239
+ inputSchema: {
1240
+ path: z.string().describe("Shader res:// path"),
1241
+ code: z.string().describe("New GDShader source"),
1242
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1243
+ },
1244
+ }, async ({ path, code, confirm }) => {
1245
+ const blocked = await gate(server, confirm, `Overwrite shader code at ${path}`);
1246
+ if (blocked)
1247
+ return blocked;
1248
+ return call("shader.set_code", { path, code });
1249
+ });
1250
+ server.registerTool("shadermaterial_create", {
1251
+ title: "Create shader material",
1252
+ description: "Create a ShaderMaterial and assign it to a node's material slot in the edited scene (undoable). Targets CanvasItem.material (2D / Control) or GeometryInstance3D.material_override (3D); other node types return unsupported. Optionally assign a Shader loaded from a res:// path. In-scene and undoable — not gated.",
1253
+ inputSchema: {
1254
+ path: z.string().describe("Node path relative to the scene root (a CanvasItem or GeometryInstance3D)"),
1255
+ shader_path: z.string().optional().describe("res:// path to a Shader to assign to the new material"),
1256
+ },
1257
+ }, async ({ path, shader_path }) => call("shadermaterial.create", shader_path !== undefined ? { path, shader_path } : { path }));
1258
+ server.registerTool("shadermaterial_set_shader", {
1259
+ title: "Set shader material shader",
1260
+ description: "Load a Shader from a res:// path and assign it to the ShaderMaterial on a node's material slot in the edited scene (undoable). The node must already have a ShaderMaterial. In-scene and undoable — not gated.",
1261
+ inputSchema: {
1262
+ path: z.string().describe("Node path relative to the scene root"),
1263
+ shader_path: z.string().describe("res:// path to a Shader resource"),
1264
+ },
1265
+ }, async ({ path, shader_path }) => call("shadermaterial.set_shader", { path, shader_path }));
1266
+ server.registerTool("shadermaterial_set_param", {
1267
+ title: "Set shader material parameter",
1268
+ description: "Set a shader uniform parameter on the ShaderMaterial of a node's material slot in the edited scene (undoable). The node must already have a ShaderMaterial. The value uses the tagged-Variant convention. In-scene and undoable — not gated.",
1269
+ inputSchema: {
1270
+ path: z.string().describe("Node path relative to the scene root"),
1271
+ param: z.string().describe("Shader uniform name"),
1272
+ value: z.any().describe("New value (JSON scalar or __type__-tagged Variant)"),
1273
+ },
1274
+ }, async ({ path, param, value }) => call("shadermaterial.set_param", { path, param, value }));
1275
+ server.registerTool("audio_player_create", {
1276
+ title: "Create audio player",
1277
+ description: "Add an AudioStreamPlayer / AudioStreamPlayer2D / AudioStreamPlayer3D node under a parent in the edited scene (undoable). `dim` selects \"none\" (default, non-positional AudioStreamPlayer), \"2d\", or \"3d\". Optional initial stream_path (res:// AudioStream), autoplay, volume_db, bus. In-scene and undoable — not gated.",
1278
+ inputSchema: {
1279
+ parent_path: z.string().describe("Parent node path relative to the scene root; \".\" for the root"),
1280
+ dim: z.enum(["none", "2d", "3d"]).optional().describe("Player kind: \"none\" (default, AudioStreamPlayer), \"2d\" (AudioStreamPlayer2D), or \"3d\" (AudioStreamPlayer3D)"),
1281
+ name: z.string().optional().describe("Node name (default matches the player class)"),
1282
+ stream_path: z.string().optional().describe("res:// path to an AudioStream to assign to the new player"),
1283
+ autoplay: z.boolean().optional().describe("Whether the player starts automatically on scene load"),
1284
+ volume_db: z.number().optional().describe("Player volume in dB"),
1285
+ bus: z.string().optional().describe("Target audio bus name (default \"Master\")"),
1286
+ },
1287
+ }, async ({ parent_path, dim, name, stream_path, autoplay, volume_db, bus }) => {
1288
+ const params = { parent_path };
1289
+ if (dim !== undefined)
1290
+ params.dim = dim;
1291
+ if (name !== undefined)
1292
+ params.name = name;
1293
+ if (stream_path !== undefined)
1294
+ params.stream_path = stream_path;
1295
+ if (autoplay !== undefined)
1296
+ params.autoplay = autoplay;
1297
+ if (volume_db !== undefined)
1298
+ params.volume_db = volume_db;
1299
+ if (bus !== undefined)
1300
+ params.bus = bus;
1301
+ return call("audio.player_create", params);
1302
+ });
1303
+ server.registerTool("audio_set_stream", {
1304
+ title: "Set audio stream",
1305
+ description: "Load an AudioStream from a res:// path and assign it as stream on an AudioStreamPlayer/2D/3D in the edited scene (undoable). In-scene and undoable — not gated.",
1306
+ inputSchema: {
1307
+ path: z.string().describe("AudioStreamPlayer/2D/3D node path relative to the scene root"),
1308
+ stream_path: z.string().describe("res:// path to an AudioStream resource"),
1309
+ },
1310
+ }, async ({ path, stream_path }) => call("audio.set_stream", { path, stream_path }));
1311
+ server.registerTool("audio_bus_add", {
1312
+ title: "Add audio bus",
1313
+ description: "Add a bus to the project's global AudioServer layout, optionally naming it, positioning it (at_position), and setting its send bus. DESTRUCTIVE (project-wide audio state) — gated by confirmation.",
1314
+ inputSchema: {
1315
+ name: z.string().optional().describe("Name for the new bus"),
1316
+ at_position: z.number().optional().describe("Insert index (default -1 = append at end)"),
1317
+ send: z.string().optional().describe("Name of the bus this bus sends its output to"),
1318
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1319
+ },
1320
+ }, async ({ name, at_position, send, confirm }) => {
1321
+ const blocked = await gate(server, confirm, `Add audio bus${name ? ` "${name}"` : ""}`);
1322
+ if (blocked)
1323
+ return blocked;
1324
+ const params = {};
1325
+ if (name !== undefined)
1326
+ params.name = name;
1327
+ if (at_position !== undefined)
1328
+ params.at_position = at_position;
1329
+ if (send !== undefined)
1330
+ params.send = send;
1331
+ return call("audio.bus_add", params);
1332
+ });
1333
+ server.registerTool("audio_bus_add_effect", {
1334
+ title: "Add audio bus effect",
1335
+ description: "Instantiate an AudioEffect subclass (by class name, e.g. \"AudioEffectReverb\") and add it to a named bus on the global AudioServer, optionally at a position in the bus's effect chain. DESTRUCTIVE (project-wide audio state) — gated by confirmation.",
1336
+ inputSchema: {
1337
+ bus: z.string().describe("Target bus name"),
1338
+ effect: z.string().describe("AudioEffect subclass name, e.g. \"AudioEffectReverb\", \"AudioEffectDelay\""),
1339
+ at_position: z.number().optional().describe("Insert index within the bus's effect chain (default -1 = append)"),
1340
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1341
+ },
1342
+ }, async ({ bus, effect, at_position, confirm }) => {
1343
+ const blocked = await gate(server, confirm, `Add ${effect} to audio bus "${bus}"`);
1344
+ if (blocked)
1345
+ return blocked;
1346
+ const params = { bus, effect };
1347
+ if (at_position !== undefined)
1348
+ params.at_position = at_position;
1349
+ return call("audio.bus_add_effect", params);
1350
+ });
1351
+ server.registerTool("audio_bus_set_volume", {
1352
+ title: "Set audio bus volume",
1353
+ description: "Set the volume (in dB) of a named bus on the global AudioServer. DESTRUCTIVE (project-wide audio state) — gated by confirmation.",
1354
+ inputSchema: {
1355
+ bus: z.string().describe("Bus name"),
1356
+ volume_db: z.number().describe("Volume in dB"),
1357
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1358
+ },
1359
+ }, async ({ bus, volume_db, confirm }) => {
1360
+ const blocked = await gate(server, confirm, `Set audio bus "${bus}" volume to ${volume_db} dB`);
1361
+ if (blocked)
1362
+ return blocked;
1363
+ return call("audio.bus_set_volume", { bus, volume_db });
1364
+ });
1365
+ server.registerTool("audio_set_bus_layout", {
1366
+ title: "Save audio bus layout",
1367
+ description: "Save the current AudioServer bus layout (buses, effects, volumes) to a .tres resource on disk (default res://default_bus_layout.tres) via generate_bus_layout + ResourceSaver.save. DESTRUCTIVE (writes a file) — gated by confirmation.",
1368
+ inputSchema: {
1369
+ to_path: z.string().optional().describe("Destination res:// path (default res://default_bus_layout.tres)"),
1370
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1371
+ },
1372
+ }, async ({ to_path, confirm }) => {
1373
+ const blocked = await gate(server, confirm, `Save audio bus layout to ${to_path ?? "res://default_bus_layout.tres"}`);
1374
+ if (blocked)
1375
+ return blocked;
1376
+ return call("audio.set_bus_layout", to_path !== undefined ? { to_path } : {});
1377
+ });
1378
+ // ---- Group G: UI / Control / theming --------------------------------------
1379
+ // control_* + container_add_child mutate the EDITED scene (Control nodes) and are
1380
+ // undoable via EditorUndoRedoManager and ungated — the node_* model. theme_* author a
1381
+ // Theme (or its entries) on disk via ResourceSaver, so — like resource_* / shader_create —
1382
+ // they are file-writers gated by confirmation, not scene-undoable.
1383
+ server.registerTool("control_create", {
1384
+ title: "Create control",
1385
+ description: "Instance a Control-derived UI node (Button, Label, Panel, VBoxContainer, TextureRect, …) under a parent (undoable). Refuses non-Control classes. Optional 'text' is applied only to controls that expose a 'text' property. Returns the new node's path.",
1386
+ inputSchema: {
1387
+ parent_path: z.string().describe("Parent node path relative to the scene root; \".\" for the root"),
1388
+ type: z.string().describe("Control subclass to instance, e.g. Button, Label, Panel, VBoxContainer, TextureRect"),
1389
+ name: z.string().optional().describe("Node name (defaults to the class name)"),
1390
+ text: z.string().optional().describe("Initial text; applied only if the control has a 'text' property"),
1391
+ },
1392
+ }, async ({ parent_path, type, name, text }) => {
1393
+ const params = { parent_path, type };
1394
+ if (name !== undefined)
1395
+ params.name = name;
1396
+ if (text !== undefined)
1397
+ params.text = text;
1398
+ return call("control.create", params);
1399
+ });
1400
+ server.registerTool("container_add_child", {
1401
+ title: "Add control to container",
1402
+ description: "Instance a Control-derived child under a Container node (VBoxContainer, GridContainer, MarginContainer, …) so it participates in the container's layout (undoable). Refuses a non-Container parent or a non-Control child. Returns the new node's path.",
1403
+ inputSchema: {
1404
+ container_path: z.string().describe("Container node path relative to the scene root"),
1405
+ type: z.string().describe("Control subclass to instance as the container's child, e.g. Button, Label"),
1406
+ name: z.string().optional().describe("Node name (defaults to the class name)"),
1407
+ },
1408
+ }, async ({ container_path, type, name }) => call("container.add_child", name !== undefined ? { container_path, type, name } : { container_path, type }));
1409
+ server.registerTool("control_set_anchors", {
1410
+ title: "Set control anchors",
1411
+ description: "Set one or more of a Control's anchors (left/top/right/bottom, each 0..1) directly (undoable). Only the sides you pass are changed; offsets are left as-is. Provide at least one side.",
1412
+ inputSchema: {
1413
+ path: z.string().describe("Control node path relative to the scene root"),
1414
+ left: z.number().optional().describe("anchor_left (0..1)"),
1415
+ top: z.number().optional().describe("anchor_top (0..1)"),
1416
+ right: z.number().optional().describe("anchor_right (0..1)"),
1417
+ bottom: z.number().optional().describe("anchor_bottom (0..1)"),
1418
+ },
1419
+ }, async ({ path, left, top, right, bottom }) => {
1420
+ const params = { path };
1421
+ if (left !== undefined)
1422
+ params.left = left;
1423
+ if (top !== undefined)
1424
+ params.top = top;
1425
+ if (right !== undefined)
1426
+ params.right = right;
1427
+ if (bottom !== undefined)
1428
+ params.bottom = bottom;
1429
+ return call("control.set_anchors", params);
1430
+ });
1431
+ server.registerTool("control_set_layout_preset", {
1432
+ title: "Apply control layout preset",
1433
+ description: "Apply a LayoutPreset to a Control via set_anchors_and_offsets_preset (undoable). 'preset' is a name (full_rect, center, top_left, center_top, left_wide, hcenter_wide, …) or the integer enum value. Optional resize_mode (0=min_size,1=keep_width,2=keep_height,3=keep_size) and margin.",
1434
+ inputSchema: {
1435
+ path: z.string().describe("Control node path relative to the scene root"),
1436
+ preset: z.union([z.string(), z.number()]).describe("LayoutPreset name or integer (0..15)"),
1437
+ resize_mode: z.number().int().optional().describe("LayoutPresetMode 0..3 (default 0 = min size)"),
1438
+ margin: z.number().int().optional().describe("Margin in pixels applied by the preset (default 0)"),
1439
+ },
1440
+ }, async ({ path, preset, resize_mode, margin }) => {
1441
+ const params = { path, preset };
1442
+ if (resize_mode !== undefined)
1443
+ params.resize_mode = resize_mode;
1444
+ if (margin !== undefined)
1445
+ params.margin = margin;
1446
+ return call("control.set_layout_preset", params);
1447
+ });
1448
+ server.registerTool("control_set_size_flags", {
1449
+ title: "Set control size flags",
1450
+ description: "Set a Control's container size flags and/or stretch ratio (undoable). 'horizontal'/'vertical' are SizeFlags bitmasks (1=fill, 2=expand, 3=expand_fill, 4=shrink_center, 8=shrink_end). Provide at least one of horizontal/vertical/stretch_ratio.",
1451
+ inputSchema: {
1452
+ path: z.string().describe("Control node path relative to the scene root"),
1453
+ horizontal: z.number().int().optional().describe("size_flags_horizontal bitmask"),
1454
+ vertical: z.number().int().optional().describe("size_flags_vertical bitmask"),
1455
+ stretch_ratio: z.number().optional().describe("size_flags_stretch_ratio (default 1.0)"),
1456
+ },
1457
+ }, async ({ path, horizontal, vertical, stretch_ratio }) => {
1458
+ const params = { path };
1459
+ if (horizontal !== undefined)
1460
+ params.horizontal = horizontal;
1461
+ if (vertical !== undefined)
1462
+ params.vertical = vertical;
1463
+ if (stretch_ratio !== undefined)
1464
+ params.stretch_ratio = stretch_ratio;
1465
+ return call("control.set_size_flags", params);
1466
+ });
1467
+ server.registerTool("control_set_theme", {
1468
+ title: "Set control theme",
1469
+ description: "Assign a Theme resource (res:// path) to a Control's 'theme' property so it and its children inherit it (undoable). Pass an empty theme_path to clear the override.",
1470
+ inputSchema: {
1471
+ path: z.string().describe("Control node path relative to the scene root"),
1472
+ theme_path: z.string().describe("Theme res:// path, or \"\" to clear the theme override"),
1473
+ },
1474
+ }, async ({ path, theme_path }) => call("control.set_theme", { path, theme_path }));
1475
+ server.registerTool("theme_create", {
1476
+ title: "Create theme",
1477
+ description: "Create a new empty Theme resource and save it to a res:// path. DESTRUCTIVE (writes a file) — gated by confirmation.",
1478
+ inputSchema: {
1479
+ to_path: z.string().describe("Destination res:// path, e.g. res://ui/main.theme or res://ui/main.tres"),
1480
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1481
+ },
1482
+ }, async ({ to_path, confirm }) => {
1483
+ const blocked = await gate(server, confirm, `Create Theme resource at ${to_path}`);
1484
+ if (blocked)
1485
+ return blocked;
1486
+ return call("theme.create", { to_path });
1487
+ });
1488
+ server.registerTool("theme_set_color", {
1489
+ title: "Set theme color",
1490
+ description: "Set a color entry on a Theme resource file and save it (e.g. font_color for the Button type). DESTRUCTIVE (writes a file) — gated by confirmation. 'color' is [r,g,b] or [r,g,b,a] with components 0..1.",
1491
+ inputSchema: {
1492
+ path: z.string().describe("Theme res:// path"),
1493
+ name: z.string().describe("Theme item name, e.g. font_color"),
1494
+ theme_type: z.string().describe("Theme type the item belongs to, e.g. Button, Label"),
1495
+ color: z.array(z.number()).describe("[r,g,b] or [r,g,b,a], components 0..1"),
1496
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1497
+ },
1498
+ }, async ({ path, name, theme_type, color, confirm }) => {
1499
+ const blocked = await gate(server, confirm, `Set theme color ${theme_type}/${name} in ${path}`);
1500
+ if (blocked)
1501
+ return blocked;
1502
+ return call("theme.set_color", { path, name, theme_type, color });
1503
+ });
1504
+ server.registerTool("theme_set_font", {
1505
+ title: "Set theme font",
1506
+ description: "Set a font entry on a Theme resource file (loading a Font from a res:// path) and save it. DESTRUCTIVE (writes a file) — gated by confirmation.",
1507
+ inputSchema: {
1508
+ path: z.string().describe("Theme res:// path"),
1509
+ name: z.string().describe("Theme item name, e.g. font"),
1510
+ theme_type: z.string().describe("Theme type the item belongs to, e.g. Button, Label"),
1511
+ font_path: z.string().describe("Font resource res:// path (FontFile / SystemFont / …)"),
1512
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1513
+ },
1514
+ }, async ({ path, name, theme_type, font_path, confirm }) => {
1515
+ const blocked = await gate(server, confirm, `Set theme font ${theme_type}/${name} in ${path}`);
1516
+ if (blocked)
1517
+ return blocked;
1518
+ return call("theme.set_font", { path, name, theme_type, font_path });
1519
+ });
1520
+ server.registerTool("theme_set_stylebox", {
1521
+ title: "Set theme stylebox",
1522
+ description: "Set a StyleBox entry on a Theme resource file (loading a StyleBox from a res:// path) and save it. DESTRUCTIVE (writes a file) — gated by confirmation.",
1523
+ inputSchema: {
1524
+ path: z.string().describe("Theme res:// path"),
1525
+ name: z.string().describe("Theme item name, e.g. normal, pressed, panel"),
1526
+ theme_type: z.string().describe("Theme type the item belongs to, e.g. Button, PanelContainer"),
1527
+ stylebox_path: z.string().describe("StyleBox resource res:// path (StyleBoxFlat / StyleBoxTexture / …)"),
1528
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1529
+ },
1530
+ }, async ({ path, name, theme_type, stylebox_path, confirm }) => {
1531
+ const blocked = await gate(server, confirm, `Set theme stylebox ${theme_type}/${name} in ${path}`);
1532
+ if (blocked)
1533
+ return blocked;
1534
+ return call("theme.set_stylebox", { path, name, theme_type, stylebox_path });
1535
+ });
1536
+ server.registerTool("theme_set_constant", {
1537
+ title: "Set theme constant",
1538
+ description: "Set an integer constant entry on a Theme resource file and save it (e.g. h_separation for a BoxContainer). DESTRUCTIVE (writes a file) — gated by confirmation.",
1539
+ inputSchema: {
1540
+ path: z.string().describe("Theme res:// path"),
1541
+ name: z.string().describe("Theme item name, e.g. h_separation, margin_left"),
1542
+ theme_type: z.string().describe("Theme type the item belongs to, e.g. HBoxContainer, MarginContainer"),
1543
+ value: z.number().int().describe("Integer constant value"),
1544
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1545
+ },
1546
+ }, async ({ path, name, theme_type, value, confirm }) => {
1547
+ const blocked = await gate(server, confirm, `Set theme constant ${theme_type}/${name} in ${path}`);
1548
+ if (blocked)
1549
+ return blocked;
1550
+ return call("theme.set_constant", { path, name, theme_type, value });
1551
+ });
1552
+ // ---- Group H: 3D & navigation -------------------------------------------
1553
+ // meshinstance/mesh/light/camera/csg/navregion/navagent mutate the EDITED scene (3D nodes) and are
1554
+ // undoable via EditorUndoRedoManager and ungated — the node_* model. primitive_mesh_create and the
1555
+ // two environment_* tools author a resource (PrimitiveMesh / Environment) on disk via ResourceSaver,
1556
+ // so — like resource_* / theme_create — they are file-writers gated by confirmation.
1557
+ server.registerTool("meshinstance_create", {
1558
+ title: "Create MeshInstance3D",
1559
+ description: "Add a MeshInstance3D under a parent (undoable). Optional 'mesh_path' loads a Mesh resource (e.g. a primitive_mesh_create output) and assigns it. Returns the new node's path.",
1560
+ inputSchema: {
1561
+ parent_path: z.string().describe("Parent node path relative to the scene root; \".\" for the root"),
1562
+ name: z.string().optional().describe("Node name (default MeshInstance3D)"),
1563
+ mesh_path: z.string().optional().describe("Mesh resource res:// path to assign to the instance's 'mesh'"),
1564
+ },
1565
+ }, async ({ parent_path, name, mesh_path }) => {
1566
+ const params = { parent_path };
1567
+ if (name !== undefined)
1568
+ params.name = name;
1569
+ if (mesh_path !== undefined)
1570
+ params.mesh_path = mesh_path;
1571
+ return call("meshinstance.create", params);
1572
+ });
1573
+ server.registerTool("mesh_set_surface_material", {
1574
+ title: "Set mesh surface material",
1575
+ description: "Assign a Material (res:// path) to a MeshInstance3D (undoable). Default surface -1 sets 'material_override' (whole instance); a surface index >= 0 sets that surface's override material (must be within the mesh's surface count). Refuses a non-MeshInstance3D node or a non-Material resource.",
1576
+ inputSchema: {
1577
+ path: z.string().describe("MeshInstance3D node path relative to the scene root"),
1578
+ material_path: z.string().describe("Material resource res:// path (StandardMaterial3D / ShaderMaterial / …)"),
1579
+ surface: z.number().int().optional().describe("Surface index, or -1 (default) for material_override"),
1580
+ },
1581
+ }, async ({ path, material_path, surface }) => {
1582
+ const params = { path, material_path };
1583
+ if (surface !== undefined)
1584
+ params.surface = surface;
1585
+ return call("mesh.set_surface_material", params);
1586
+ });
1587
+ server.registerTool("primitive_mesh_create", {
1588
+ title: "Create primitive mesh",
1589
+ description: "Create a PrimitiveMesh resource (box/sphere/cylinder/plane/capsule/prism/torus/quad) and save it to a res:// path. DESTRUCTIVE (writes a file) — gated by confirmation. Assign it with meshinstance_create's 'mesh_path'.",
1590
+ inputSchema: {
1591
+ to_path: z.string().describe("Destination res:// path, e.g. res://meshes/box.tres"),
1592
+ shape: z.string().optional().describe("box | sphere | cylinder | plane | capsule | prism | torus | quad (default box)"),
1593
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1594
+ },
1595
+ }, async ({ to_path, shape, confirm }) => {
1596
+ const blocked = await gate(server, confirm, `Create ${shape ?? "box"} PrimitiveMesh at ${to_path}`);
1597
+ if (blocked)
1598
+ return blocked;
1599
+ const params = { to_path };
1600
+ if (shape !== undefined)
1601
+ params.shape = shape;
1602
+ return call("primitive_mesh.create", params);
1603
+ });
1604
+ server.registerTool("light_create", {
1605
+ title: "Create Light3D",
1606
+ description: "Add a 3D light under a parent (undoable). 'kind' selects DirectionalLight3D (dir), OmniLight3D (omni), or SpotLight3D (spot). Returns the new node's path.",
1607
+ inputSchema: {
1608
+ parent_path: z.string().describe("Parent node path relative to the scene root; \".\" for the root"),
1609
+ kind: z.enum(["dir", "directional", "omni", "spot"]).optional().describe("Light kind: dir | omni | spot (default omni)"),
1610
+ name: z.string().optional().describe("Node name (defaults to the class name)"),
1611
+ },
1612
+ }, async ({ parent_path, kind, name }) => {
1613
+ const params = { parent_path };
1614
+ if (kind !== undefined)
1615
+ params.kind = kind;
1616
+ if (name !== undefined)
1617
+ params.name = name;
1618
+ return call("light.create", params);
1619
+ });
1620
+ server.registerTool("camera_create", {
1621
+ title: "Create Camera3D",
1622
+ description: "Add a Camera3D under a parent (undoable). Optional 'current' makes it the active camera. Returns the new node's path.",
1623
+ inputSchema: {
1624
+ parent_path: z.string().describe("Parent node path relative to the scene root; \".\" for the root"),
1625
+ name: z.string().optional().describe("Node name (default Camera3D)"),
1626
+ current: z.boolean().optional().describe("Make this the current/active camera (default false)"),
1627
+ },
1628
+ }, async ({ parent_path, name, current }) => {
1629
+ const params = { parent_path };
1630
+ if (name !== undefined)
1631
+ params.name = name;
1632
+ if (current !== undefined)
1633
+ params.current = current;
1634
+ return call("camera.create", params);
1635
+ });
1636
+ server.registerTool("csg_create", {
1637
+ title: "Create CSG node",
1638
+ description: "Add a CSG node under a parent (undoable). 'shape' selects CSGBox3D (box), CSGSphere3D (sphere), CSGCylinder3D (cylinder), CSGTorus3D (torus), CSGPolygon3D (polygon), CSGMesh3D (mesh), or CSGCombiner3D (combiner). Returns the new node's path.",
1639
+ inputSchema: {
1640
+ parent_path: z.string().describe("Parent node path relative to the scene root; \".\" for the root"),
1641
+ shape: z.string().optional().describe("box | sphere | cylinder | torus | polygon | mesh | combiner (default box)"),
1642
+ name: z.string().optional().describe("Node name (defaults to the class name)"),
1643
+ },
1644
+ }, async ({ parent_path, shape, name }) => {
1645
+ const params = { parent_path };
1646
+ if (shape !== undefined)
1647
+ params.shape = shape;
1648
+ if (name !== undefined)
1649
+ params.name = name;
1650
+ return call("csg.create", params);
1651
+ });
1652
+ server.registerTool("navregion_create", {
1653
+ title: "Create NavigationRegion3D",
1654
+ description: "Add a NavigationRegion3D under a parent (undoable). By default seeds a fresh empty NavigationMesh (set with_navmesh=false to skip). Returns the new node's path and whether a navmesh was attached.",
1655
+ inputSchema: {
1656
+ parent_path: z.string().describe("Parent node path relative to the scene root; \".\" for the root"),
1657
+ name: z.string().optional().describe("Node name (default NavigationRegion3D)"),
1658
+ with_navmesh: z.boolean().optional().describe("Seed a fresh empty NavigationMesh resource (default true)"),
1659
+ },
1660
+ }, async ({ parent_path, name, with_navmesh }) => {
1661
+ const params = { parent_path };
1662
+ if (name !== undefined)
1663
+ params.name = name;
1664
+ if (with_navmesh !== undefined)
1665
+ params.with_navmesh = with_navmesh;
1666
+ return call("navregion.create", params);
1667
+ });
1668
+ server.registerTool("navagent_configure", {
1669
+ title: "Add & configure NavigationAgent3D",
1670
+ description: "Add a NavigationAgent3D under a parent (undoable) and set any of its steering/avoidance properties. Returns the new node's path and the resulting config (radius, height, max_speed, path/target desired distances, avoidance_enabled).",
1671
+ inputSchema: {
1672
+ parent_path: z.string().describe("Parent node path relative to the scene root; \".\" for the root"),
1673
+ name: z.string().optional().describe("Node name (default NavigationAgent3D)"),
1674
+ radius: z.number().optional().describe("Agent radius"),
1675
+ height: z.number().optional().describe("Agent height"),
1676
+ max_speed: z.number().optional().describe("Maximum movement speed"),
1677
+ path_desired_distance: z.number().optional().describe("Distance to a path point before advancing"),
1678
+ target_desired_distance: z.number().optional().describe("Distance to the target that counts as arrived"),
1679
+ avoidance_enabled: z.boolean().optional().describe("Enable RVO avoidance"),
1680
+ },
1681
+ }, async ({ parent_path, name, radius, height, max_speed, path_desired_distance, target_desired_distance, avoidance_enabled }) => {
1682
+ const params = { parent_path };
1683
+ if (name !== undefined)
1684
+ params.name = name;
1685
+ if (radius !== undefined)
1686
+ params.radius = radius;
1687
+ if (height !== undefined)
1688
+ params.height = height;
1689
+ if (max_speed !== undefined)
1690
+ params.max_speed = max_speed;
1691
+ if (path_desired_distance !== undefined)
1692
+ params.path_desired_distance = path_desired_distance;
1693
+ if (target_desired_distance !== undefined)
1694
+ params.target_desired_distance = target_desired_distance;
1695
+ if (avoidance_enabled !== undefined)
1696
+ params.avoidance_enabled = avoidance_enabled;
1697
+ return call("navagent.configure", params);
1698
+ });
1699
+ server.registerTool("environment_create", {
1700
+ title: "Create Environment",
1701
+ description: "Create an Environment resource and save it to a res:// path. 'background' sets the mode (clear_color/color/sky/canvas, default clear_color); optional 'ambient_color' ([r,g,b(,a)], 0..1) sets the ambient light color. DESTRUCTIVE (writes a file) — gated by confirmation.",
1702
+ inputSchema: {
1703
+ to_path: z.string().describe("Destination res:// path, e.g. res://world.tres"),
1704
+ background: z.string().optional().describe("clear_color | color | sky | canvas (default clear_color)"),
1705
+ ambient_color: z.array(z.number()).optional().describe("Ambient light color [r,g,b] or [r,g,b,a], 0..1"),
1706
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1707
+ },
1708
+ }, async ({ to_path, background, ambient_color, confirm }) => {
1709
+ const blocked = await gate(server, confirm, `Create Environment at ${to_path}`);
1710
+ if (blocked)
1711
+ return blocked;
1712
+ const params = { to_path };
1713
+ if (background !== undefined)
1714
+ params.background = background;
1715
+ if (ambient_color !== undefined)
1716
+ params.ambient_color = ambient_color;
1717
+ return call("environment.create", params);
1718
+ });
1719
+ server.registerTool("environment_set_sky", {
1720
+ title: "Set Environment sky",
1721
+ description: "Attach a Sky to an existing Environment resource file (setting a ProceduralSkyMaterial, PhysicalSkyMaterial, or PanoramaSkyMaterial) and switch its background to SKY, then re-save. DESTRUCTIVE (writes a file) — gated by confirmation. Refuses a path that is not an Environment.",
1722
+ inputSchema: {
1723
+ path: z.string().describe("Environment res:// path"),
1724
+ sky_material: z.enum(["procedural", "physical", "panorama"]).optional().describe("Sky material kind (default procedural)"),
1725
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1726
+ },
1727
+ }, async ({ path, sky_material, confirm }) => {
1728
+ const blocked = await gate(server, confirm, `Set sky on Environment ${path}`);
1729
+ if (blocked)
1730
+ return blocked;
1731
+ const params = { path };
1732
+ if (sky_material !== undefined)
1733
+ params.sky_material = sky_material;
1734
+ return call("environment.set_sky", params);
1735
+ });
1736
+ // ---- Group I: input, project config & testing ---------------------------
1737
+ server.registerTool("inputmap_add_action", {
1738
+ title: "Add input action",
1739
+ description: "Define a project input action (ProjectSettings input/<name>) with an empty event list and a deadzone. Set save=true to persist to project.godot. DESTRUCTIVE (project-wide input map) — gated by confirmation.",
1740
+ inputSchema: {
1741
+ name: z.string().describe("Action name (without the input/ prefix)"),
1742
+ deadzone: z.number().optional().describe("Analog deadzone 0..1 (default 0.5)"),
1743
+ save: z.boolean().optional().describe("Persist to project.godot (default false)"),
1744
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1745
+ },
1746
+ }, async ({ name, deadzone, save, confirm }) => {
1747
+ const blocked = await gate(server, confirm, `Add input action "${name}"${save ? " and save project.godot" : ""}`);
1748
+ if (blocked)
1749
+ return blocked;
1750
+ const params = { name };
1751
+ if (deadzone !== undefined)
1752
+ params.deadzone = deadzone;
1753
+ if (save !== undefined)
1754
+ params.save = save;
1755
+ return call("inputmap.add_action", params);
1756
+ });
1757
+ server.registerTool("inputmap_add_event", {
1758
+ title: "Add input event to action",
1759
+ description: "Append an input event to an existing project input action. 'event' is { type: key|mouse_button|joy_button|joy_motion, ... } — key: keycode or physical_keycode as a name like \"A\"/\"Space\" or an int; mouse_button/joy_button: button_index; joy_motion: axis + axis_value. Set save=true to persist. DESTRUCTIVE — gated by confirmation.",
1760
+ inputSchema: {
1761
+ name: z.string().describe("Existing action name (without the input/ prefix)"),
1762
+ event: z
1763
+ .object({
1764
+ type: z.enum(["key", "mouse_button", "joy_button", "joy_motion"]).describe("Event type"),
1765
+ keycode: z.union([z.string(), z.number()]).optional().describe("Key name or code (type=key)"),
1766
+ physical_keycode: z.union([z.string(), z.number()]).optional().describe("Physical key name or code (type=key)"),
1767
+ button_index: z.number().optional().describe("Button index (mouse_button/joy_button)"),
1768
+ axis: z.number().optional().describe("Joypad axis (joy_motion)"),
1769
+ axis_value: z.number().optional().describe("Joypad axis value (joy_motion)"),
1770
+ })
1771
+ .describe("Input event descriptor"),
1772
+ save: z.boolean().optional().describe("Persist to project.godot (default false)"),
1773
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1774
+ },
1775
+ }, async ({ name, event, save, confirm }) => {
1776
+ const blocked = await gate(server, confirm, `Add input event to "${name}"`);
1777
+ if (blocked)
1778
+ return blocked;
1779
+ const params = { name, event };
1780
+ if (save !== undefined)
1781
+ params.save = save;
1782
+ return call("inputmap.add_event", params);
1783
+ });
1784
+ server.registerTool("inputmap_list", {
1785
+ title: "List input actions",
1786
+ description: "List all project-defined input actions (ProjectSettings input/*) with their deadzone and events (class + human-readable text). Read-only.",
1787
+ inputSchema: {},
1788
+ }, async () => call("inputmap.list"));
1789
+ server.registerTool("inputmap_erase_action", {
1790
+ title: "Erase input action",
1791
+ description: "Remove a project input action (ProjectSettings input/<name>). Set save=true to persist to project.godot. DESTRUCTIVE — gated by confirmation.",
1792
+ inputSchema: {
1793
+ name: z.string().describe("Action name (without the input/ prefix)"),
1794
+ save: z.boolean().optional().describe("Persist to project.godot (default false)"),
1795
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1796
+ },
1797
+ }, async ({ name, save, confirm }) => {
1798
+ const blocked = await gate(server, confirm, `Erase input action "${name}"${save ? " and save project.godot" : ""}`);
1799
+ if (blocked)
1800
+ return blocked;
1801
+ const params = { name };
1802
+ if (save !== undefined)
1803
+ params.save = save;
1804
+ return call("inputmap.erase_action", params);
1805
+ });
1806
+ server.registerTool("project_add_autoload", {
1807
+ title: "Add autoload singleton",
1808
+ description: "Register an autoload (ProjectSettings autoload/<name>) pointing at a res:// script or scene, enabled as a global singleton by default. Set save=true to persist to project.godot. DESTRUCTIVE — gated by confirmation.",
1809
+ inputSchema: {
1810
+ name: z.string().describe("Autoload / singleton name"),
1811
+ path: z.string().describe("res:// path to a .gd script or .tscn scene"),
1812
+ enabled: z.boolean().optional().describe("Enable as a global singleton (default true)"),
1813
+ save: z.boolean().optional().describe("Persist to project.godot (default false)"),
1814
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1815
+ },
1816
+ }, async ({ name, path, enabled, save, confirm }) => {
1817
+ const blocked = await gate(server, confirm, `Add autoload "${name}" -> ${path}${save ? " and save project.godot" : ""}`);
1818
+ if (blocked)
1819
+ return blocked;
1820
+ const params = { name, path };
1821
+ if (enabled !== undefined)
1822
+ params.enabled = enabled;
1823
+ if (save !== undefined)
1824
+ params.save = save;
1825
+ return call("project.add_autoload", params);
1826
+ });
1827
+ server.registerTool("project_remove_autoload", {
1828
+ title: "Remove autoload singleton",
1829
+ description: "Remove an autoload (ProjectSettings autoload/<name>). Set save=true to persist to project.godot. DESTRUCTIVE — gated by confirmation.",
1830
+ inputSchema: {
1831
+ name: z.string().describe("Autoload / singleton name"),
1832
+ save: z.boolean().optional().describe("Persist to project.godot (default false)"),
1833
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1834
+ },
1835
+ }, async ({ name, save, confirm }) => {
1836
+ const blocked = await gate(server, confirm, `Remove autoload "${name}"${save ? " and save project.godot" : ""}`);
1837
+ if (blocked)
1838
+ return blocked;
1839
+ const params = { name };
1840
+ if (save !== undefined)
1841
+ params.save = save;
1842
+ return call("project.remove_autoload", params);
1843
+ });
1844
+ server.registerTool("project_add_export_preset", {
1845
+ title: "Add export preset",
1846
+ description: "Append an export preset to res://export_presets.cfg for a platform (e.g. \"Windows Desktop\", \"Web\", \"Linux/X11\", \"macOS\"). Returns the preset's index. DESTRUCTIVE (writes export_presets.cfg) — gated by confirmation.",
1847
+ inputSchema: {
1848
+ name: z.string().describe("Preset display name"),
1849
+ platform: z.string().describe("Export platform name as shown in the editor"),
1850
+ runnable: z.boolean().optional().describe("Mark the preset runnable (default true)"),
1851
+ export_path: z.string().optional().describe("Default export output path"),
1852
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1853
+ },
1854
+ }, async ({ name, platform, runnable, export_path, confirm }) => {
1855
+ const blocked = await gate(server, confirm, `Add export preset "${name}" (${platform})`);
1856
+ if (blocked)
1857
+ return blocked;
1858
+ const params = { name, platform };
1859
+ if (runnable !== undefined)
1860
+ params.runnable = runnable;
1861
+ if (export_path !== undefined)
1862
+ params.export_path = export_path;
1863
+ return call("project.add_export_preset", params);
1864
+ });
1865
+ server.registerTool("project_set_main_scene", {
1866
+ title: "Set main scene",
1867
+ description: "Set the project's main scene (ProjectSettings application/run/main_scene) to an existing res:// .tscn/.scn. Set save=true to persist to project.godot. DESTRUCTIVE — gated by confirmation.",
1868
+ inputSchema: {
1869
+ path: z.string().describe("res:// path to the scene to run first"),
1870
+ save: z.boolean().optional().describe("Persist to project.godot (default false)"),
1871
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
1872
+ },
1873
+ }, async ({ path, save, confirm }) => {
1874
+ const blocked = await gate(server, confirm, `Set main scene to ${path}${save ? " and save project.godot" : ""}`);
1875
+ if (blocked)
1876
+ return blocked;
1877
+ const params = { path };
1878
+ if (save !== undefined)
1879
+ params.save = save;
1880
+ return call("project.set_main_scene", params);
1881
+ });
1882
+ server.registerTool("project_list_settings", {
1883
+ title: "List project settings",
1884
+ description: "List ProjectSettings keys and values, optionally filtered by a dotted-key prefix (e.g. \"input/\", \"autoload/\", \"application/\"). Read-only.",
1885
+ inputSchema: {
1886
+ prefix: z.string().optional().describe("Only return keys starting with this prefix (default: all)"),
1887
+ },
1888
+ }, async ({ prefix }) => call("project.list_settings", prefix !== undefined ? { prefix } : {}));
1889
+ server.registerTool("editorsettings_get_set", {
1890
+ title: "Get or set an editor setting",
1891
+ description: "Read an EditorSettings value by name; if 'value' is provided, write it instead (persists to the editor's global config). Reading is read-only; writing is DESTRUCTIVE (mutates the editor config) — gated by confirmation.",
1892
+ inputSchema: {
1893
+ name: z.string().describe("EditorSettings key, e.g. interface/editor/code_font_size"),
1894
+ value: z.any().optional().describe("If provided, set this value (rich types use the {\"__type__\":...} tagging convention)"),
1895
+ confirm: z.boolean().optional().describe("Auto-approve the write (skip the confirmation prompt)"),
1896
+ },
1897
+ }, async ({ name, value, confirm }) => {
1898
+ if (value !== undefined) {
1899
+ const blocked = await gate(server, confirm, `Set editor setting "${name}"`);
1900
+ if (blocked)
1901
+ return blocked;
1902
+ return call("editorsettings.get_set", { name, value });
1903
+ }
1904
+ return call("editorsettings.get_set", { name });
1905
+ });
1906
+ server.registerTool("test_detect", {
1907
+ title: "Detect test framework",
1908
+ description: "Detect an installed GDScript test framework (GUT or GdUnit4) in the project. Returns { framework: gut|gdunit4|none, path, version }. Read-only.",
1909
+ inputSchema: {},
1910
+ }, async () => call("test.detect"));
1911
+ server.registerTool("test_list", {
1912
+ title: "List test scripts",
1913
+ description: "List GDScript test scripts (files named test_*.gd or *_test.gd) under a directory (default res://test), searched recursively. Read-only.",
1914
+ inputSchema: {
1915
+ dir: z.string().optional().describe("Directory to search (default res://test)"),
1916
+ },
1917
+ }, async ({ dir }) => call("test.list", dir !== undefined ? { dir } : {}));
1918
+ }
1919
+ //# sourceMappingURL=editor.js.map