godot-agent-tools-mcp 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server.mjs +470 -60
package/package.json
CHANGED
package/server.mjs
CHANGED
|
@@ -91,6 +91,21 @@ const TOOLS = [
|
|
|
91
91
|
properties: { node_path: { type: "string" } },
|
|
92
92
|
},
|
|
93
93
|
},
|
|
94
|
+
{
|
|
95
|
+
name: "scene_duplicate_node",
|
|
96
|
+
method: "scene.duplicate_node",
|
|
97
|
+
description:
|
|
98
|
+
"Clone a node (with descendants) in the currently-edited scene. Owner is set recursively so the duplicated subtree serializes. Defaults to adding under the source's parent; override with parent_path.",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: "object",
|
|
101
|
+
required: ["node_path"],
|
|
102
|
+
properties: {
|
|
103
|
+
node_path: { type: "string", description: "Source node to clone." },
|
|
104
|
+
new_name: { type: "string", description: "Name for the copy; defaults to auto-generated '<name>2' style." },
|
|
105
|
+
parent_path: { type: "string", description: "Destination parent; defaults to the source's parent." },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
94
109
|
{
|
|
95
110
|
name: "scene_reparent",
|
|
96
111
|
method: "scene.reparent",
|
|
@@ -110,7 +125,22 @@ const TOOLS = [
|
|
|
110
125
|
name: "scene_set_property",
|
|
111
126
|
method: "scene.set_property",
|
|
112
127
|
description:
|
|
113
|
-
"Set a property on a node.
|
|
128
|
+
"Set a property on a node. JSON-to-Godot coercion: " +
|
|
129
|
+
"primitives (bool/int/float/String/StringName) pass through; " +
|
|
130
|
+
"NodePath from a string; " +
|
|
131
|
+
"Vector2/2i/3/3i/4/4i from [x, y(, z(, w))]; " +
|
|
132
|
+
"Rect2/2i from [x, y, width, height]; " +
|
|
133
|
+
"Quaternion from [x, y, z, w]; " +
|
|
134
|
+
"Color from [r, g, b(, a)] or '#rrggbb(aa)'; " +
|
|
135
|
+
"Transform2D from {origin: [x,y], rotation: radians, scale: [x,y], skew?}; " +
|
|
136
|
+
"Transform3D from {origin: [x,y,z], rotation: [x,y,z] (euler rad), scale: [x,y,z]}; " +
|
|
137
|
+
"Basis from {rotation, scale} (same shape as Transform3D minus origin); " +
|
|
138
|
+
"AABB from {position: [x,y,z], size: [x,y,z]}; " +
|
|
139
|
+
"Plane from {normal: [x,y,z], d: float}; " +
|
|
140
|
+
"Resource-typed properties (e.g. a CollisionShape2D's 'shape') auto-load from 'res://...' or 'uid://...' path strings; " +
|
|
141
|
+
"Packed{String,Int32,Int64,Float32,Float64,Vector2,Vector3,Color}Array from plain JSON arrays (element-wise coerced); " +
|
|
142
|
+
"other types pass through as-is. " +
|
|
143
|
+
"Returns an error if Godot drops the assignment (e.g. type mismatch) instead of echoing a misleading null.",
|
|
114
144
|
inputSchema: {
|
|
115
145
|
type: "object",
|
|
116
146
|
required: ["node_path", "property"],
|
|
@@ -121,6 +151,70 @@ const TOOLS = [
|
|
|
121
151
|
},
|
|
122
152
|
},
|
|
123
153
|
},
|
|
154
|
+
{
|
|
155
|
+
name: "scene_get_property",
|
|
156
|
+
method: "scene.get_property",
|
|
157
|
+
description:
|
|
158
|
+
"Read a property from a node in the currently-edited scene. Mirror of scene_set_property — useful for 'what's the current value?' queries without dumping the whole tree.",
|
|
159
|
+
inputSchema: {
|
|
160
|
+
type: "object",
|
|
161
|
+
required: ["node_path", "property"],
|
|
162
|
+
properties: {
|
|
163
|
+
node_path: { type: "string" },
|
|
164
|
+
property: { type: "string" },
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "scene_build_tree",
|
|
170
|
+
method: "scene.build_tree",
|
|
171
|
+
description:
|
|
172
|
+
"Build a subtree in the currently-edited scene in one call — recursive spec instead of dozens of scene_add_node + scene_set_property + script_attach round trips. " +
|
|
173
|
+
"Each tree entry: {type: 'ClassName' (required), name?: string, properties?: {propname: value, ...}, script?: 'res://...', children?: [entry, ...]}. " +
|
|
174
|
+
"Properties are coerced via the same rules as scene_set_property (Vectors from arrays, Resources auto-loaded from res:// paths, Transforms from {origin,rotation,scale}, etc.). " +
|
|
175
|
+
"Script is attached before properties so script-exported vars are settable in the same call. " +
|
|
176
|
+
"Atomic: if any entry fails (unknown type, missing property, coercion error, read-only slot, silent-null assignment), every node created during this call is rolled back — the scene is left in its pre-call state.",
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: "object",
|
|
179
|
+
required: ["nodes"],
|
|
180
|
+
properties: {
|
|
181
|
+
parent_path: { type: "string", default: ".", description: "Where to attach the new subtree. '.' = scene root." },
|
|
182
|
+
nodes: {
|
|
183
|
+
type: "array",
|
|
184
|
+
description: "Top-level tree entries (each may have arbitrarily nested children).",
|
|
185
|
+
items: {
|
|
186
|
+
type: "object",
|
|
187
|
+
required: ["type"],
|
|
188
|
+
properties: {
|
|
189
|
+
type: { type: "string", description: "Godot class name (e.g. 'PanelContainer', 'Button')." },
|
|
190
|
+
name: { type: "string" },
|
|
191
|
+
script: { type: "string", description: "Path to a .gd to attach before properties are applied." },
|
|
192
|
+
properties: { type: "object", additionalProperties: true, description: "Property → value pairs; values use the scene_set_property coercion rules." },
|
|
193
|
+
children: { type: "array", description: "Nested entries using the same shape as this one." },
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "scene_call_method",
|
|
202
|
+
method: "scene.call_method",
|
|
203
|
+
description:
|
|
204
|
+
"Invoke a method on a node in the currently-edited scene (e.g. a custom helper, or Godot built-ins like 'queue_free', 'add_to_group'). Args are coerced against the method's declared parameter types using the same rules as scene_set_property — including 'res://...' → Resource auto-load. Return value is JSON-native. Useful when properties alone can't express what you need — e.g. calling a script method with multiple args.",
|
|
205
|
+
inputSchema: {
|
|
206
|
+
type: "object",
|
|
207
|
+
required: ["node_path", "method"],
|
|
208
|
+
properties: {
|
|
209
|
+
node_path: { type: "string" },
|
|
210
|
+
method: { type: "string" },
|
|
211
|
+
args: {
|
|
212
|
+
type: "array",
|
|
213
|
+
description: "Method arguments in order. Each is coerced based on the method's declared parameter type.",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
124
218
|
{
|
|
125
219
|
name: "scene_open",
|
|
126
220
|
method: "scene.open",
|
|
@@ -135,10 +229,11 @@ const TOOLS = [
|
|
|
135
229
|
name: "scene_save",
|
|
136
230
|
method: "scene.save",
|
|
137
231
|
description:
|
|
138
|
-
"Save the currently-edited scene. Pass 'path' to save-as (rebinds the scene to that path)."
|
|
232
|
+
"Save the currently-edited scene. Pass 'path' to save-as (rebinds the scene to that path). " +
|
|
233
|
+
"REQUIRED for fresh scenes: if the scene has never been saved (no backing file), the tool returns an error rather than triggering Godot's native Save-As dialog (which would block the editor waiting for a human click). Always pass 'path' when you built the scene from scratch via scene.build_tree / scene.add_node without going through scene.new.",
|
|
139
234
|
inputSchema: {
|
|
140
235
|
type: "object",
|
|
141
|
-
properties: { path: { type: "string", description: "
|
|
236
|
+
properties: { path: { type: "string", description: "Save-as target, required when the scene has no existing file." } },
|
|
142
237
|
},
|
|
143
238
|
},
|
|
144
239
|
{
|
|
@@ -147,6 +242,21 @@ const TOOLS = [
|
|
|
147
242
|
description: "Describe the currently-edited scene, or return {open: false} if none is open.",
|
|
148
243
|
inputSchema: { type: "object", properties: {} },
|
|
149
244
|
},
|
|
245
|
+
{
|
|
246
|
+
name: "scene_capture_screenshot",
|
|
247
|
+
method: "scene.capture_screenshot",
|
|
248
|
+
description:
|
|
249
|
+
"Save a PNG of the editor viewport for the open scene (2D or 3D selected automatically). Clean capture — no editor grid/gizmos. Empty scenes render as the viewport background color. Default output: res://.godot/agent_tools/<scene-name>.png.",
|
|
250
|
+
inputSchema: {
|
|
251
|
+
type: "object",
|
|
252
|
+
properties: {
|
|
253
|
+
output: {
|
|
254
|
+
type: "string",
|
|
255
|
+
description: "Optional output path (res://...). Defaults to res://.godot/agent_tools/<scene>.png.",
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
150
260
|
{
|
|
151
261
|
name: "signal_connect",
|
|
152
262
|
method: "signal.connect",
|
|
@@ -246,7 +356,8 @@ const TOOLS = [
|
|
|
246
356
|
{
|
|
247
357
|
name: "resource_set_property",
|
|
248
358
|
method: "resource.set_property",
|
|
249
|
-
description:
|
|
359
|
+
description:
|
|
360
|
+
"Load a .tres, set one property, and save. Same JSON-to-Godot coercion as scene_set_property: primitives, NodePath, Vector2/2i/3/3i/4/4i, Rect2/2i, Quaternion, Color, Transform2D/3D, Basis, AABB, Plane, Resource auto-load from 'res://'/'uid://' paths, and all Packed*Array variants. Errors on silently-dropped assignments.",
|
|
250
361
|
inputSchema: {
|
|
251
362
|
type: "object",
|
|
252
363
|
required: ["path", "property"],
|
|
@@ -257,6 +368,22 @@ const TOOLS = [
|
|
|
257
368
|
},
|
|
258
369
|
},
|
|
259
370
|
},
|
|
371
|
+
{
|
|
372
|
+
name: "resource_call_method",
|
|
373
|
+
method: "resource.call_method",
|
|
374
|
+
description:
|
|
375
|
+
"Load a .tres, invoke a method on it, save, return the method's result. Rounds out what set_property can't express — e.g. StyleBoxFlat.set_border_width_all(4), set_corner_radius_all(14), Curve.add_point(...). Args coerce via the same rules as resource_set_property. Pass save:false to call without persisting (read-only method calls).",
|
|
376
|
+
inputSchema: {
|
|
377
|
+
type: "object",
|
|
378
|
+
required: ["path", "method"],
|
|
379
|
+
properties: {
|
|
380
|
+
path: { type: "string" },
|
|
381
|
+
method: { type: "string" },
|
|
382
|
+
args: { type: "array", description: "Method arguments in order, coerced per the method's parameter types." },
|
|
383
|
+
save: { type: "boolean", default: true, description: "Save the resource after the call. Set false for read-only method calls." },
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
},
|
|
260
387
|
{
|
|
261
388
|
name: "refs_validate_project",
|
|
262
389
|
method: "refs.validate_project",
|
|
@@ -281,7 +408,7 @@ const TOOLS = [
|
|
|
281
408
|
name: "refs_rename",
|
|
282
409
|
method: "refs.rename",
|
|
283
410
|
description:
|
|
284
|
-
"Move a file and rewrite every path-form reference to it. The .uid and .import sidecars are moved too so UID-form references keep resolving. Supports dry_run to preview changes without touching disk.
|
|
411
|
+
"Move a file and rewrite every path-form reference to it. The .uid and .import sidecars are moved too so UID-form references keep resolving. Supports dry_run to preview changes without touching disk.",
|
|
285
412
|
inputSchema: {
|
|
286
413
|
type: "object",
|
|
287
414
|
required: ["from", "to"],
|
|
@@ -293,6 +420,21 @@ const TOOLS = [
|
|
|
293
420
|
},
|
|
294
421
|
},
|
|
295
422
|
},
|
|
423
|
+
{
|
|
424
|
+
name: "refs_rename_class",
|
|
425
|
+
method: "refs.rename_class",
|
|
426
|
+
description:
|
|
427
|
+
"Rename 'class_name X' to 'class_name Y' across the project — updates the defining script and every word-boundary reference in .gd / .tscn / .tres files. Best-effort (won't distinguish an X that happens to be a local variable); use dry_run first.",
|
|
428
|
+
inputSchema: {
|
|
429
|
+
type: "object",
|
|
430
|
+
required: ["from", "to"],
|
|
431
|
+
properties: {
|
|
432
|
+
from: { type: "string" },
|
|
433
|
+
to: { type: "string" },
|
|
434
|
+
dry_run: { type: "boolean", default: false },
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
},
|
|
296
438
|
{
|
|
297
439
|
name: "project_get_setting",
|
|
298
440
|
method: "project.get_setting",
|
|
@@ -354,6 +496,12 @@ const TOOLS = [
|
|
|
354
496
|
"Trigger an editor filesystem rescan. Call this after creating/moving/deleting files via tools that bypass the editor, so load() and the FileSystem dock see the changes.",
|
|
355
497
|
inputSchema: { type: "object", properties: {} },
|
|
356
498
|
},
|
|
499
|
+
{
|
|
500
|
+
name: "editor_save_all_scenes",
|
|
501
|
+
method: "editor.save_all_scenes",
|
|
502
|
+
description: "Save every currently-open scene in the editor.",
|
|
503
|
+
inputSchema: { type: "object", properties: {} },
|
|
504
|
+
},
|
|
357
505
|
{
|
|
358
506
|
name: "docs_class_ref",
|
|
359
507
|
method: "docs.class_ref",
|
|
@@ -385,7 +533,11 @@ const TOOLS = [
|
|
|
385
533
|
name: "input_map_add_event",
|
|
386
534
|
method: "input_map.add_event",
|
|
387
535
|
description:
|
|
388
|
-
"Attach an input event to an existing action.
|
|
536
|
+
"Attach an input event to an existing action. Every event accepts an optional 'device' field (default -1 = all devices; set 0, 1, etc. for local-multiplayer device-specific bindings). Event shapes: " +
|
|
537
|
+
"{type:'key', keycode:'A'|'Space'|...}; " +
|
|
538
|
+
"{type:'mouse_button', button_index:1|2|3}; " +
|
|
539
|
+
"{type:'joy_button', button_index:0..}; " +
|
|
540
|
+
"{type:'joy_motion', axis:0..5 or 'left_x'|'left_y'|'right_x'|'right_y'|'trigger_left'|'trigger_right', axis_value:-1.0..1.0} (axis_value sign picks direction).",
|
|
389
541
|
inputSchema: {
|
|
390
542
|
type: "object",
|
|
391
543
|
required: ["action", "event"],
|
|
@@ -395,10 +547,13 @@ const TOOLS = [
|
|
|
395
547
|
type: "object",
|
|
396
548
|
required: ["type"],
|
|
397
549
|
properties: {
|
|
398
|
-
type: { type: "string", enum: ["key", "mouse_button", "joy_button"] },
|
|
550
|
+
type: { type: "string", enum: ["key", "mouse_button", "joy_button", "joy_motion"] },
|
|
551
|
+
device: { type: "integer", default: -1, description: "-1 = all devices (default); 0, 1, etc. bind a specific controller for local multiplayer." },
|
|
399
552
|
keycode: { description: "For type='key': 'A', 'Space', 'F1', or int keycode." },
|
|
400
|
-
button_index: { type: "integer", description: "For mouse/joy button events." },
|
|
401
553
|
physical: { type: "boolean", default: true, description: "For type='key': use physical keycode (recommended)." },
|
|
554
|
+
button_index: { type: "integer", description: "For mouse/joy button events." },
|
|
555
|
+
axis: { description: "For type='joy_motion': int 0..5 or 'left_x'|'left_y'|'right_x'|'right_y'|'trigger_left'|'trigger_right'." },
|
|
556
|
+
axis_value: { type: "number", description: "For type='joy_motion': -1.0 to 1.0; sign selects direction that triggers the action." },
|
|
402
557
|
},
|
|
403
558
|
},
|
|
404
559
|
},
|
|
@@ -416,6 +571,20 @@ const TOOLS = [
|
|
|
416
571
|
},
|
|
417
572
|
},
|
|
418
573
|
},
|
|
574
|
+
{
|
|
575
|
+
name: "input_map_remove_event",
|
|
576
|
+
method: "input_map.remove_event",
|
|
577
|
+
description:
|
|
578
|
+
"Remove an event from an action by index. Call input_map_list first to see indices (events are listed in add order).",
|
|
579
|
+
inputSchema: {
|
|
580
|
+
type: "object",
|
|
581
|
+
required: ["action", "event_index"],
|
|
582
|
+
properties: {
|
|
583
|
+
action: { type: "string" },
|
|
584
|
+
event_index: { type: "integer" },
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
},
|
|
419
588
|
{
|
|
420
589
|
name: "input_map_remove_action",
|
|
421
590
|
method: "input_map.remove_action",
|
|
@@ -430,18 +599,189 @@ const TOOLS = [
|
|
|
430
599
|
name: "run_scene_headless",
|
|
431
600
|
method: "run.scene_headless",
|
|
432
601
|
description:
|
|
433
|
-
"Run a scene in a
|
|
602
|
+
"Run a scene in a child Godot process with structured output. " +
|
|
603
|
+
"MODES: BARE (default) runs --headless — fast, no window, good for 'does _ready not crash' checks. " +
|
|
604
|
+
"DRIVEN (anything beyond path + quit_after_seconds) runs under a wrapper driver that can inject scripted input, capture screenshots at multiple frames, dump final scene state as JSON, and use a deterministic RNG seed. " +
|
|
605
|
+
"SCREENSHOTS: when screenshot(s) requested the subprocess drops --headless and runs with a real (offscreen) window because Godot 4.6's headless mode uses a dummy renderer. Expect a brief window flash. " +
|
|
606
|
+
"STRUCTURED RESULTS: tool parses stdout for ERROR: / USER ERROR: / SCRIPT ERROR: / WARNING: / USER WARNING: lines and returns them as result.errors and result.warnings arrays so the agent doesn't have to regex the raw output. state_dump:true adds result.final_state with the scene tree + common properties. " +
|
|
607
|
+
"Event types for input_script: " +
|
|
608
|
+
"{frame, type: 'action_tap', action}; " +
|
|
609
|
+
"{frame, type: 'action_press', action, strength?}; " +
|
|
610
|
+
"{frame, type: 'action_release', action}; " +
|
|
611
|
+
"{frame, type: 'key', keycode: 'Space'|int, pressed?: true}; " +
|
|
612
|
+
"{frame, type: 'mouse_click', position: [x, y], button?: 1}; " +
|
|
613
|
+
"{frame, type: 'mouse_motion', position: [x, y]}. " +
|
|
614
|
+
"BLOCKS the editor for the duration — use small quit_after_seconds (1-5).",
|
|
434
615
|
inputSchema: {
|
|
435
616
|
type: "object",
|
|
436
617
|
required: ["path"],
|
|
437
618
|
properties: {
|
|
438
619
|
path: { type: "string", description: "Scene to run, e.g. 'res://Main.tscn'." },
|
|
439
|
-
quit_after_seconds: { type: "number", default: 2, description: "Converted to frames assuming 60 fps." },
|
|
620
|
+
quit_after_seconds: { type: "number", default: 2, description: "Converted to frames assuming 60 fps. In DRIVEN mode this is also the quit_frame the driver targets." },
|
|
440
621
|
extra_args: {
|
|
441
622
|
type: "array",
|
|
442
623
|
items: { type: "string" },
|
|
443
624
|
description: "Additional CLI args passed to the child godot process.",
|
|
444
625
|
},
|
|
626
|
+
input_script: {
|
|
627
|
+
type: "array",
|
|
628
|
+
description: "Optional: enters DRIVEN mode. Array of event specs keyed by frame. See description for event type shapes.",
|
|
629
|
+
items: {
|
|
630
|
+
type: "object",
|
|
631
|
+
required: ["frame", "type"],
|
|
632
|
+
properties: {
|
|
633
|
+
frame: { type: "integer", description: "0-based frame to fire on." },
|
|
634
|
+
type: { type: "string", enum: ["action_press", "action_release", "action_tap", "key", "mouse_click", "mouse_motion"] },
|
|
635
|
+
action: { type: "string" },
|
|
636
|
+
strength: { type: "number", default: 1.0 },
|
|
637
|
+
keycode: { description: "For type='key': 'A', 'Space', 'F1', or int keycode." },
|
|
638
|
+
pressed: { type: "boolean" },
|
|
639
|
+
button: { type: "integer", description: "Mouse button (1=left, 2=right, 3=middle)." },
|
|
640
|
+
position: { type: "array", description: "[x, y] viewport coordinates." },
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
screenshot: {
|
|
645
|
+
type: "string",
|
|
646
|
+
description: "Shorthand: one PNG saved at the final frame. Equivalent to screenshots:[{frame: quit_frame, path: ...}]. Triggers offscreen-windowed subprocess (brief window flash).",
|
|
647
|
+
},
|
|
648
|
+
screenshots: {
|
|
649
|
+
type: "array",
|
|
650
|
+
description: "Capture PNGs at multiple specific frames during the run — useful for verifying state transitions (spawn at frame 30, mid-animation at 60, final at 120).",
|
|
651
|
+
items: {
|
|
652
|
+
type: "object",
|
|
653
|
+
required: ["frame", "path"],
|
|
654
|
+
properties: {
|
|
655
|
+
frame: { type: "integer" },
|
|
656
|
+
path: { type: "string" },
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
resolution: {
|
|
661
|
+
type: "string",
|
|
662
|
+
default: "320x240",
|
|
663
|
+
description: "Window resolution 'WxH' when screenshots are captured. Default is tiny to minimize offscreen footprint; bump to 1280x720 or similar for UI verification.",
|
|
664
|
+
},
|
|
665
|
+
state_dump: {
|
|
666
|
+
type: "boolean",
|
|
667
|
+
default: false,
|
|
668
|
+
description: "When true, driver writes a JSON snapshot of the final scene tree (name, class, node_path, script, children, plus common props like visible/position/text/value/modulate). Returned as result.final_state. Lets agents verify end state programmatically instead of eyeballing the screenshot.",
|
|
669
|
+
},
|
|
670
|
+
seed: {
|
|
671
|
+
type: "integer",
|
|
672
|
+
description: "Optional RNG seed. Set before the target scene is instanced so randi/randf are reproducible. Useful for deterministic tests.",
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
name: "user_fs_read",
|
|
679
|
+
method: "user_fs.read",
|
|
680
|
+
description:
|
|
681
|
+
"Read a text file from the user:// data directory — where games persist save files, custom-level JSON, settings, etc. Separate from fs_list (which is res://-only) because user:// is runtime-written state.",
|
|
682
|
+
inputSchema: {
|
|
683
|
+
type: "object",
|
|
684
|
+
required: ["path"],
|
|
685
|
+
properties: {
|
|
686
|
+
path: { type: "string", description: "Must begin with 'user://'." },
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
name: "user_fs_list",
|
|
692
|
+
method: "user_fs.list",
|
|
693
|
+
description:
|
|
694
|
+
"List files and subdirectories under a user:// directory. Optionally recursive.",
|
|
695
|
+
inputSchema: {
|
|
696
|
+
type: "object",
|
|
697
|
+
properties: {
|
|
698
|
+
dir: { type: "string", default: "user://", description: "Must begin with 'user://'." },
|
|
699
|
+
recursive: { type: "boolean", default: false },
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
name: "fs_list",
|
|
705
|
+
method: "fs.list",
|
|
706
|
+
description:
|
|
707
|
+
"Enumerate project files by type with optional glob filter. Types: all | scene | script | resource | shader | image | audio. Skips the agent_tools addon by default.",
|
|
708
|
+
inputSchema: {
|
|
709
|
+
type: "object",
|
|
710
|
+
properties: {
|
|
711
|
+
type: {
|
|
712
|
+
type: "string",
|
|
713
|
+
enum: ["all", "scene", "script", "resource", "shader", "image", "audio"],
|
|
714
|
+
default: "all",
|
|
715
|
+
},
|
|
716
|
+
glob: { type: "string", description: "Optional case-insensitive glob, e.g. 'res://scenes/**/*.tscn'." },
|
|
717
|
+
include_addons: { type: "boolean", default: false },
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
name: "animation_list",
|
|
723
|
+
method: "animation.list",
|
|
724
|
+
description: "List animations on an AnimationPlayer node with their tracks.",
|
|
725
|
+
inputSchema: {
|
|
726
|
+
type: "object",
|
|
727
|
+
required: ["node_path"],
|
|
728
|
+
properties: { node_path: { type: "string" } },
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
name: "animation_add_animation",
|
|
733
|
+
method: "animation.add_animation",
|
|
734
|
+
description: "Create an empty animation in the player's library. Use animation_add_value_track to populate it.",
|
|
735
|
+
inputSchema: {
|
|
736
|
+
type: "object",
|
|
737
|
+
required: ["node_path", "name"],
|
|
738
|
+
properties: {
|
|
739
|
+
node_path: { type: "string" },
|
|
740
|
+
name: { type: "string" },
|
|
741
|
+
length: { type: "number", default: 1.0 },
|
|
742
|
+
library: { type: "string", default: "", description: "Library name; '' is the default library." },
|
|
743
|
+
},
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
name: "animation_remove_animation",
|
|
748
|
+
method: "animation.remove_animation",
|
|
749
|
+
description: "Delete an animation from an AnimationPlayer.",
|
|
750
|
+
inputSchema: {
|
|
751
|
+
type: "object",
|
|
752
|
+
required: ["node_path", "name"],
|
|
753
|
+
properties: {
|
|
754
|
+
node_path: { type: "string" },
|
|
755
|
+
name: { type: "string" },
|
|
756
|
+
library: { type: "string", default: "" },
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
name: "animation_add_value_track",
|
|
762
|
+
method: "animation.add_value_track",
|
|
763
|
+
description:
|
|
764
|
+
"Add a value track to an animation that animates a property on a target node. target_node is resolved relative to the AnimationPlayer's root. Auto-extends the animation's length if keyframes go past it.",
|
|
765
|
+
inputSchema: {
|
|
766
|
+
type: "object",
|
|
767
|
+
required: ["node_path", "animation", "target_node", "property", "keyframes"],
|
|
768
|
+
properties: {
|
|
769
|
+
node_path: { type: "string", description: "AnimationPlayer node path." },
|
|
770
|
+
animation: { type: "string", description: "Animation name — use 'lib/anim' for non-default libraries." },
|
|
771
|
+
target_node: { type: "string", description: "NodePath to animated node, relative to the player's root." },
|
|
772
|
+
property: { type: "string", description: "Property name to animate." },
|
|
773
|
+
keyframes: {
|
|
774
|
+
type: "array",
|
|
775
|
+
items: {
|
|
776
|
+
type: "object",
|
|
777
|
+
required: ["time", "value"],
|
|
778
|
+
properties: {
|
|
779
|
+
time: { type: "number" },
|
|
780
|
+
value: { description: "Property value at this time." },
|
|
781
|
+
easing: { type: "number", default: 1.0, description: "Transition curve; 1.0 = linear." },
|
|
782
|
+
},
|
|
783
|
+
},
|
|
784
|
+
},
|
|
445
785
|
},
|
|
446
786
|
},
|
|
447
787
|
},
|
|
@@ -449,72 +789,142 @@ const TOOLS = [
|
|
|
449
789
|
|
|
450
790
|
const BY_NAME = Object.fromEntries(TOOLS.map((t) => [t.name, t]));
|
|
451
791
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
792
|
+
// Persistent Godot TCP client. One socket reused across tool calls; outstanding
|
|
793
|
+
// requests are tracked by id so multiple in-flight calls don't interleave data.
|
|
794
|
+
class GodotClient {
|
|
795
|
+
constructor(host, port) {
|
|
796
|
+
this.host = host;
|
|
797
|
+
this.port = port;
|
|
798
|
+
this.socket = null;
|
|
799
|
+
this.buffer = "";
|
|
800
|
+
this.pending = new Map(); // id -> { resolve, reject, timer }
|
|
801
|
+
this.nextId = 0;
|
|
802
|
+
this.connecting = null;
|
|
803
|
+
}
|
|
457
804
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
clearTimeout(timer);
|
|
462
|
-
try {
|
|
463
|
-
socket.end();
|
|
464
|
-
} catch {}
|
|
465
|
-
fn(arg);
|
|
466
|
-
};
|
|
805
|
+
async _ensureConnected() {
|
|
806
|
+
if (this.socket && !this.socket.destroyed) return;
|
|
807
|
+
if (this.connecting) return this.connecting;
|
|
467
808
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
809
|
+
this.connecting = new Promise((resolve, reject) => {
|
|
810
|
+
const s = new net.Socket();
|
|
811
|
+
s.setEncoding("utf8");
|
|
812
|
+
s.setNoDelay(true);
|
|
472
813
|
|
|
473
|
-
|
|
814
|
+
const onConnect = () => {
|
|
815
|
+
s.removeListener("error", onErrorPreConnect);
|
|
816
|
+
this.socket = s;
|
|
817
|
+
this.connecting = null;
|
|
818
|
+
resolve();
|
|
819
|
+
};
|
|
474
820
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
if (resp.error) {
|
|
483
|
-
settle(
|
|
484
|
-
reject,
|
|
485
|
-
new Error(`Godot error ${resp.error.code}: ${resp.error.message}`)
|
|
486
|
-
);
|
|
821
|
+
const onErrorPreConnect = (e) => {
|
|
822
|
+
this.connecting = null;
|
|
823
|
+
if (e.code === "ECONNREFUSED") {
|
|
824
|
+
reject(new Error(
|
|
825
|
+
`Godot editor not reachable on ${this.host}:${this.port}. ` +
|
|
826
|
+
`Open the project in the Godot editor with the 'Agent Tools' plugin enabled.`
|
|
827
|
+
));
|
|
487
828
|
} else {
|
|
488
|
-
|
|
829
|
+
reject(e);
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
s.once("connect", onConnect);
|
|
834
|
+
s.once("error", onErrorPreConnect);
|
|
835
|
+
|
|
836
|
+
s.on("data", (data) => this._onData(data));
|
|
837
|
+
s.on("error", (e) => this._onFatalError(e));
|
|
838
|
+
s.on("close", () => this._onClose());
|
|
839
|
+
|
|
840
|
+
s.connect(this.port, this.host);
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
return this.connecting;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
call(method, params) {
|
|
847
|
+
const id = ++this.nextId;
|
|
848
|
+
return new Promise(async (resolve, reject) => {
|
|
849
|
+
const timer = setTimeout(() => {
|
|
850
|
+
if (this.pending.has(id)) {
|
|
851
|
+
this.pending.delete(id);
|
|
852
|
+
reject(new Error(`Godot tool '${method}' timed out after ${TIMEOUT_MS}ms`));
|
|
489
853
|
}
|
|
854
|
+
}, TIMEOUT_MS);
|
|
855
|
+
|
|
856
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
857
|
+
|
|
858
|
+
try {
|
|
859
|
+
await this._ensureConnected();
|
|
490
860
|
} catch (e) {
|
|
491
|
-
|
|
861
|
+
clearTimeout(timer);
|
|
862
|
+
this.pending.delete(id);
|
|
863
|
+
reject(e);
|
|
864
|
+
return;
|
|
492
865
|
}
|
|
866
|
+
|
|
867
|
+
const line = JSON.stringify({ id, method, params: params || {} }) + "\n";
|
|
868
|
+
this.socket.write(line);
|
|
493
869
|
});
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
_onData(data) {
|
|
873
|
+
this.buffer += data;
|
|
874
|
+
while (true) {
|
|
875
|
+
const nl = this.buffer.indexOf("\n");
|
|
876
|
+
if (nl < 0) break;
|
|
877
|
+
const line = this.buffer.slice(0, nl);
|
|
878
|
+
this.buffer = this.buffer.slice(nl + 1);
|
|
879
|
+
if (!line) continue;
|
|
494
880
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
881
|
+
let msg;
|
|
882
|
+
try {
|
|
883
|
+
msg = JSON.parse(line);
|
|
884
|
+
} catch {
|
|
885
|
+
continue; // ignore malformed lines — shouldn't happen
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const pending = this.pending.get(msg.id);
|
|
889
|
+
if (!pending) continue;
|
|
890
|
+
this.pending.delete(msg.id);
|
|
891
|
+
clearTimeout(pending.timer);
|
|
892
|
+
|
|
893
|
+
if (msg.error) {
|
|
894
|
+
pending.reject(new Error(`Godot error ${msg.error.code}: ${msg.error.message}`));
|
|
503
895
|
} else {
|
|
504
|
-
|
|
896
|
+
pending.resolve(msg.result);
|
|
505
897
|
}
|
|
506
|
-
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
507
900
|
|
|
508
|
-
|
|
901
|
+
_onFatalError(e) {
|
|
902
|
+
for (const { reject, timer } of this.pending.values()) {
|
|
903
|
+
clearTimeout(timer);
|
|
904
|
+
reject(e);
|
|
905
|
+
}
|
|
906
|
+
this.pending.clear();
|
|
907
|
+
if (this.socket) {
|
|
908
|
+
this.socket.destroy();
|
|
909
|
+
this.socket = null;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
509
912
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
913
|
+
_onClose() {
|
|
914
|
+
for (const { reject, timer } of this.pending.values()) {
|
|
915
|
+
clearTimeout(timer);
|
|
916
|
+
reject(new Error("Godot closed the connection"));
|
|
917
|
+
}
|
|
918
|
+
this.pending.clear();
|
|
919
|
+
this.socket = null;
|
|
920
|
+
this.buffer = "";
|
|
921
|
+
}
|
|
514
922
|
}
|
|
515
923
|
|
|
924
|
+
const client = new GodotClient(HOST, PORT);
|
|
925
|
+
|
|
516
926
|
const server = new Server(
|
|
517
|
-
{ name: "godot-agent-tools", version: "0.
|
|
927
|
+
{ name: "godot-agent-tools", version: "0.2.0" },
|
|
518
928
|
{ capabilities: { tools: {} } }
|
|
519
929
|
);
|
|
520
930
|
|
|
@@ -535,7 +945,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
535
945
|
};
|
|
536
946
|
}
|
|
537
947
|
try {
|
|
538
|
-
const result = await
|
|
948
|
+
const result = await client.call(tool.method, req.params.arguments || {});
|
|
539
949
|
return {
|
|
540
950
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
541
951
|
};
|