godot-agent-tools-mcp 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server.mjs +285 -51
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",
|
|
@@ -121,6 +136,20 @@ const TOOLS = [
|
|
|
121
136
|
},
|
|
122
137
|
},
|
|
123
138
|
},
|
|
139
|
+
{
|
|
140
|
+
name: "scene_get_property",
|
|
141
|
+
method: "scene.get_property",
|
|
142
|
+
description:
|
|
143
|
+
"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.",
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: "object",
|
|
146
|
+
required: ["node_path", "property"],
|
|
147
|
+
properties: {
|
|
148
|
+
node_path: { type: "string" },
|
|
149
|
+
property: { type: "string" },
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
124
153
|
{
|
|
125
154
|
name: "scene_open",
|
|
126
155
|
method: "scene.open",
|
|
@@ -147,6 +176,21 @@ const TOOLS = [
|
|
|
147
176
|
description: "Describe the currently-edited scene, or return {open: false} if none is open.",
|
|
148
177
|
inputSchema: { type: "object", properties: {} },
|
|
149
178
|
},
|
|
179
|
+
{
|
|
180
|
+
name: "scene_capture_screenshot",
|
|
181
|
+
method: "scene.capture_screenshot",
|
|
182
|
+
description:
|
|
183
|
+
"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.",
|
|
184
|
+
inputSchema: {
|
|
185
|
+
type: "object",
|
|
186
|
+
properties: {
|
|
187
|
+
output: {
|
|
188
|
+
type: "string",
|
|
189
|
+
description: "Optional output path (res://...). Defaults to res://.godot/agent_tools/<scene>.png.",
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
150
194
|
{
|
|
151
195
|
name: "signal_connect",
|
|
152
196
|
method: "signal.connect",
|
|
@@ -281,7 +325,7 @@ const TOOLS = [
|
|
|
281
325
|
name: "refs_rename",
|
|
282
326
|
method: "refs.rename",
|
|
283
327
|
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.
|
|
328
|
+
"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
329
|
inputSchema: {
|
|
286
330
|
type: "object",
|
|
287
331
|
required: ["from", "to"],
|
|
@@ -293,6 +337,21 @@ const TOOLS = [
|
|
|
293
337
|
},
|
|
294
338
|
},
|
|
295
339
|
},
|
|
340
|
+
{
|
|
341
|
+
name: "refs_rename_class",
|
|
342
|
+
method: "refs.rename_class",
|
|
343
|
+
description:
|
|
344
|
+
"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.",
|
|
345
|
+
inputSchema: {
|
|
346
|
+
type: "object",
|
|
347
|
+
required: ["from", "to"],
|
|
348
|
+
properties: {
|
|
349
|
+
from: { type: "string" },
|
|
350
|
+
to: { type: "string" },
|
|
351
|
+
dry_run: { type: "boolean", default: false },
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
},
|
|
296
355
|
{
|
|
297
356
|
name: "project_get_setting",
|
|
298
357
|
method: "project.get_setting",
|
|
@@ -354,6 +413,12 @@ const TOOLS = [
|
|
|
354
413
|
"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
414
|
inputSchema: { type: "object", properties: {} },
|
|
356
415
|
},
|
|
416
|
+
{
|
|
417
|
+
name: "editor_save_all_scenes",
|
|
418
|
+
method: "editor.save_all_scenes",
|
|
419
|
+
description: "Save every currently-open scene in the editor.",
|
|
420
|
+
inputSchema: { type: "object", properties: {} },
|
|
421
|
+
},
|
|
357
422
|
{
|
|
358
423
|
name: "docs_class_ref",
|
|
359
424
|
method: "docs.class_ref",
|
|
@@ -416,6 +481,20 @@ const TOOLS = [
|
|
|
416
481
|
},
|
|
417
482
|
},
|
|
418
483
|
},
|
|
484
|
+
{
|
|
485
|
+
name: "input_map_remove_event",
|
|
486
|
+
method: "input_map.remove_event",
|
|
487
|
+
description:
|
|
488
|
+
"Remove an event from an action by index. Call input_map_list first to see indices (events are listed in add order).",
|
|
489
|
+
inputSchema: {
|
|
490
|
+
type: "object",
|
|
491
|
+
required: ["action", "event_index"],
|
|
492
|
+
properties: {
|
|
493
|
+
action: { type: "string" },
|
|
494
|
+
event_index: { type: "integer" },
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
},
|
|
419
498
|
{
|
|
420
499
|
name: "input_map_remove_action",
|
|
421
500
|
method: "input_map.remove_action",
|
|
@@ -445,76 +524,231 @@ const TOOLS = [
|
|
|
445
524
|
},
|
|
446
525
|
},
|
|
447
526
|
},
|
|
527
|
+
{
|
|
528
|
+
name: "fs_list",
|
|
529
|
+
method: "fs.list",
|
|
530
|
+
description:
|
|
531
|
+
"Enumerate project files by type with optional glob filter. Types: all | scene | script | resource | shader | image | audio. Skips the agent_tools addon by default.",
|
|
532
|
+
inputSchema: {
|
|
533
|
+
type: "object",
|
|
534
|
+
properties: {
|
|
535
|
+
type: {
|
|
536
|
+
type: "string",
|
|
537
|
+
enum: ["all", "scene", "script", "resource", "shader", "image", "audio"],
|
|
538
|
+
default: "all",
|
|
539
|
+
},
|
|
540
|
+
glob: { type: "string", description: "Optional case-insensitive glob, e.g. 'res://scenes/**/*.tscn'." },
|
|
541
|
+
include_addons: { type: "boolean", default: false },
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
name: "animation_list",
|
|
547
|
+
method: "animation.list",
|
|
548
|
+
description: "List animations on an AnimationPlayer node with their tracks.",
|
|
549
|
+
inputSchema: {
|
|
550
|
+
type: "object",
|
|
551
|
+
required: ["node_path"],
|
|
552
|
+
properties: { node_path: { type: "string" } },
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
name: "animation_add_animation",
|
|
557
|
+
method: "animation.add_animation",
|
|
558
|
+
description: "Create an empty animation in the player's library. Use animation_add_value_track to populate it.",
|
|
559
|
+
inputSchema: {
|
|
560
|
+
type: "object",
|
|
561
|
+
required: ["node_path", "name"],
|
|
562
|
+
properties: {
|
|
563
|
+
node_path: { type: "string" },
|
|
564
|
+
name: { type: "string" },
|
|
565
|
+
length: { type: "number", default: 1.0 },
|
|
566
|
+
library: { type: "string", default: "", description: "Library name; '' is the default library." },
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
name: "animation_remove_animation",
|
|
572
|
+
method: "animation.remove_animation",
|
|
573
|
+
description: "Delete an animation from an AnimationPlayer.",
|
|
574
|
+
inputSchema: {
|
|
575
|
+
type: "object",
|
|
576
|
+
required: ["node_path", "name"],
|
|
577
|
+
properties: {
|
|
578
|
+
node_path: { type: "string" },
|
|
579
|
+
name: { type: "string" },
|
|
580
|
+
library: { type: "string", default: "" },
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
name: "animation_add_value_track",
|
|
586
|
+
method: "animation.add_value_track",
|
|
587
|
+
description:
|
|
588
|
+
"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.",
|
|
589
|
+
inputSchema: {
|
|
590
|
+
type: "object",
|
|
591
|
+
required: ["node_path", "animation", "target_node", "property", "keyframes"],
|
|
592
|
+
properties: {
|
|
593
|
+
node_path: { type: "string", description: "AnimationPlayer node path." },
|
|
594
|
+
animation: { type: "string", description: "Animation name — use 'lib/anim' for non-default libraries." },
|
|
595
|
+
target_node: { type: "string", description: "NodePath to animated node, relative to the player's root." },
|
|
596
|
+
property: { type: "string", description: "Property name to animate." },
|
|
597
|
+
keyframes: {
|
|
598
|
+
type: "array",
|
|
599
|
+
items: {
|
|
600
|
+
type: "object",
|
|
601
|
+
required: ["time", "value"],
|
|
602
|
+
properties: {
|
|
603
|
+
time: { type: "number" },
|
|
604
|
+
value: { description: "Property value at this time." },
|
|
605
|
+
easing: { type: "number", default: 1.0, description: "Transition curve; 1.0 = linear." },
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
},
|
|
448
612
|
];
|
|
449
613
|
|
|
450
614
|
const BY_NAME = Object.fromEntries(TOOLS.map((t) => [t.name, t]));
|
|
451
615
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
616
|
+
// Persistent Godot TCP client. One socket reused across tool calls; outstanding
|
|
617
|
+
// requests are tracked by id so multiple in-flight calls don't interleave data.
|
|
618
|
+
class GodotClient {
|
|
619
|
+
constructor(host, port) {
|
|
620
|
+
this.host = host;
|
|
621
|
+
this.port = port;
|
|
622
|
+
this.socket = null;
|
|
623
|
+
this.buffer = "";
|
|
624
|
+
this.pending = new Map(); // id -> { resolve, reject, timer }
|
|
625
|
+
this.nextId = 0;
|
|
626
|
+
this.connecting = null;
|
|
627
|
+
}
|
|
457
628
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
clearTimeout(timer);
|
|
462
|
-
try {
|
|
463
|
-
socket.end();
|
|
464
|
-
} catch {}
|
|
465
|
-
fn(arg);
|
|
466
|
-
};
|
|
629
|
+
async _ensureConnected() {
|
|
630
|
+
if (this.socket && !this.socket.destroyed) return;
|
|
631
|
+
if (this.connecting) return this.connecting;
|
|
467
632
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
633
|
+
this.connecting = new Promise((resolve, reject) => {
|
|
634
|
+
const s = new net.Socket();
|
|
635
|
+
s.setEncoding("utf8");
|
|
636
|
+
s.setNoDelay(true);
|
|
472
637
|
|
|
473
|
-
|
|
638
|
+
const onConnect = () => {
|
|
639
|
+
s.removeListener("error", onErrorPreConnect);
|
|
640
|
+
this.socket = s;
|
|
641
|
+
this.connecting = null;
|
|
642
|
+
resolve();
|
|
643
|
+
};
|
|
474
644
|
|
|
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
|
-
);
|
|
645
|
+
const onErrorPreConnect = (e) => {
|
|
646
|
+
this.connecting = null;
|
|
647
|
+
if (e.code === "ECONNREFUSED") {
|
|
648
|
+
reject(new Error(
|
|
649
|
+
`Godot editor not reachable on ${this.host}:${this.port}. ` +
|
|
650
|
+
`Open the project in the Godot editor with the 'Agent Tools' plugin enabled.`
|
|
651
|
+
));
|
|
487
652
|
} else {
|
|
488
|
-
|
|
653
|
+
reject(e);
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
s.once("connect", onConnect);
|
|
658
|
+
s.once("error", onErrorPreConnect);
|
|
659
|
+
|
|
660
|
+
s.on("data", (data) => this._onData(data));
|
|
661
|
+
s.on("error", (e) => this._onFatalError(e));
|
|
662
|
+
s.on("close", () => this._onClose());
|
|
663
|
+
|
|
664
|
+
s.connect(this.port, this.host);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
return this.connecting;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
call(method, params) {
|
|
671
|
+
const id = ++this.nextId;
|
|
672
|
+
return new Promise(async (resolve, reject) => {
|
|
673
|
+
const timer = setTimeout(() => {
|
|
674
|
+
if (this.pending.has(id)) {
|
|
675
|
+
this.pending.delete(id);
|
|
676
|
+
reject(new Error(`Godot tool '${method}' timed out after ${TIMEOUT_MS}ms`));
|
|
489
677
|
}
|
|
678
|
+
}, TIMEOUT_MS);
|
|
679
|
+
|
|
680
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
681
|
+
|
|
682
|
+
try {
|
|
683
|
+
await this._ensureConnected();
|
|
490
684
|
} catch (e) {
|
|
491
|
-
|
|
685
|
+
clearTimeout(timer);
|
|
686
|
+
this.pending.delete(id);
|
|
687
|
+
reject(e);
|
|
688
|
+
return;
|
|
492
689
|
}
|
|
690
|
+
|
|
691
|
+
const line = JSON.stringify({ id, method, params: params || {} }) + "\n";
|
|
692
|
+
this.socket.write(line);
|
|
493
693
|
});
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
_onData(data) {
|
|
697
|
+
this.buffer += data;
|
|
698
|
+
while (true) {
|
|
699
|
+
const nl = this.buffer.indexOf("\n");
|
|
700
|
+
if (nl < 0) break;
|
|
701
|
+
const line = this.buffer.slice(0, nl);
|
|
702
|
+
this.buffer = this.buffer.slice(nl + 1);
|
|
703
|
+
if (!line) continue;
|
|
704
|
+
|
|
705
|
+
let msg;
|
|
706
|
+
try {
|
|
707
|
+
msg = JSON.parse(line);
|
|
708
|
+
} catch {
|
|
709
|
+
continue; // ignore malformed lines — shouldn't happen
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const pending = this.pending.get(msg.id);
|
|
713
|
+
if (!pending) continue;
|
|
714
|
+
this.pending.delete(msg.id);
|
|
715
|
+
clearTimeout(pending.timer);
|
|
494
716
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
settle(
|
|
498
|
-
reject,
|
|
499
|
-
new Error(
|
|
500
|
-
`Godot editor not reachable on ${HOST}:${PORT}. Open the project in the Godot editor with the 'Agent Tools' plugin enabled.`
|
|
501
|
-
)
|
|
502
|
-
);
|
|
717
|
+
if (msg.error) {
|
|
718
|
+
pending.reject(new Error(`Godot error ${msg.error.code}: ${msg.error.message}`));
|
|
503
719
|
} else {
|
|
504
|
-
|
|
720
|
+
pending.resolve(msg.result);
|
|
505
721
|
}
|
|
506
|
-
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
507
724
|
|
|
508
|
-
|
|
725
|
+
_onFatalError(e) {
|
|
726
|
+
for (const { reject, timer } of this.pending.values()) {
|
|
727
|
+
clearTimeout(timer);
|
|
728
|
+
reject(e);
|
|
729
|
+
}
|
|
730
|
+
this.pending.clear();
|
|
731
|
+
if (this.socket) {
|
|
732
|
+
this.socket.destroy();
|
|
733
|
+
this.socket = null;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
509
736
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
737
|
+
_onClose() {
|
|
738
|
+
for (const { reject, timer } of this.pending.values()) {
|
|
739
|
+
clearTimeout(timer);
|
|
740
|
+
reject(new Error("Godot closed the connection"));
|
|
741
|
+
}
|
|
742
|
+
this.pending.clear();
|
|
743
|
+
this.socket = null;
|
|
744
|
+
this.buffer = "";
|
|
745
|
+
}
|
|
514
746
|
}
|
|
515
747
|
|
|
748
|
+
const client = new GodotClient(HOST, PORT);
|
|
749
|
+
|
|
516
750
|
const server = new Server(
|
|
517
|
-
{ name: "godot-agent-tools", version: "0.
|
|
751
|
+
{ name: "godot-agent-tools", version: "0.2.0" },
|
|
518
752
|
{ capabilities: { tools: {} } }
|
|
519
753
|
);
|
|
520
754
|
|
|
@@ -535,7 +769,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
535
769
|
};
|
|
536
770
|
}
|
|
537
771
|
try {
|
|
538
|
-
const result = await
|
|
772
|
+
const result = await client.call(tool.method, req.params.arguments || {});
|
|
539
773
|
return {
|
|
540
774
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
541
775
|
};
|