godot-agent-tools-mcp 0.1.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 +19 -0
- package/server.mjs +551 -0
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "godot-agent-tools-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "MCP server that bridges coding agents to the Godot Agent Tools editor plugin.",
|
|
6
|
+
"main": "server.mjs",
|
|
7
|
+
"bin": { "godot-agent-tools-mcp": "server.mjs" },
|
|
8
|
+
"files": ["server.mjs", "README.md"],
|
|
9
|
+
"keywords": ["godot", "mcp", "model-context-protocol", "ai", "agent", "claude", "gamedev"],
|
|
10
|
+
"author": "Blake Bukowsky",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"homepage": "https://github.com/BlakeBukowsky/GodotTools",
|
|
13
|
+
"repository": { "type": "git", "url": "git+https://github.com/BlakeBukowsky/GodotTools.git" },
|
|
14
|
+
"bugs": { "url": "https://github.com/BlakeBukowsky/GodotTools/issues" },
|
|
15
|
+
"engines": { "node": ">=18" },
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/server.mjs
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// MCP server that bridges stdio tool calls to the agent_tools plugin running
|
|
3
|
+
// inside the Godot editor (TCP JSON-RPC on 127.0.0.1:9920 by default).
|
|
4
|
+
|
|
5
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import {
|
|
8
|
+
CallToolRequestSchema,
|
|
9
|
+
ListToolsRequestSchema,
|
|
10
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
+
import net from "node:net";
|
|
12
|
+
|
|
13
|
+
const HOST = process.env.GODOT_AGENT_HOST || "127.0.0.1";
|
|
14
|
+
const PORT = parseInt(process.env.GODOT_AGENT_PORT || "9920", 10);
|
|
15
|
+
const TIMEOUT_MS = parseInt(process.env.GODOT_AGENT_TIMEOUT_MS || "15000", 10);
|
|
16
|
+
|
|
17
|
+
const TOOLS = [
|
|
18
|
+
{
|
|
19
|
+
name: "scene_inspect",
|
|
20
|
+
method: "scene.inspect",
|
|
21
|
+
description:
|
|
22
|
+
"Return the node tree of a scene as JSON (name, class, node_path, script, children). Omit 'path' to inspect the currently-edited scene. Read-only.",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
path: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description:
|
|
29
|
+
"Scene resource path (e.g. 'res://Main.tscn'). If omitted, uses the currently-edited scene.",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "scene_new",
|
|
36
|
+
method: "scene.new",
|
|
37
|
+
description:
|
|
38
|
+
"Create a new .tscn file from scratch with a root node of the given type. Opens the scene in the editor by default so subsequent scene.* calls target it.",
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
required: ["path"],
|
|
42
|
+
properties: {
|
|
43
|
+
path: { type: "string", description: "'res://...' ending in .tscn" },
|
|
44
|
+
root_type: { type: "string", default: "Node", description: "Godot class name for the root node." },
|
|
45
|
+
root_name: { type: "string", description: "Root node name; defaults to the class name." },
|
|
46
|
+
overwrite: { type: "boolean", default: false },
|
|
47
|
+
open_after: { type: "boolean", default: true },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "scene_instance_packed",
|
|
53
|
+
method: "scene.instance_packed",
|
|
54
|
+
description:
|
|
55
|
+
"Add an existing .tscn as a sub-scene child of a node in the currently-edited scene. Refuses to instance a scene into itself.",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
required: ["scene_path"],
|
|
59
|
+
properties: {
|
|
60
|
+
scene_path: { type: "string", description: "Path to the .tscn to instance." },
|
|
61
|
+
parent_path: { type: "string", description: "Parent NodePath; '.' for root. Defaults to '.'.", default: "." },
|
|
62
|
+
name: { type: "string", description: "Instance node name; defaults to the scene's root name." },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "scene_add_node",
|
|
68
|
+
method: "scene.add_node",
|
|
69
|
+
description:
|
|
70
|
+
"Add a new node to the currently-edited scene. Sets owner so the node persists on save.",
|
|
71
|
+
inputSchema: {
|
|
72
|
+
type: "object",
|
|
73
|
+
required: ["type"],
|
|
74
|
+
properties: {
|
|
75
|
+
type: { type: "string", description: "Godot class name, e.g. 'Node2D', 'Sprite2D', 'Label'." },
|
|
76
|
+
name: { type: "string", description: "Node name; defaults to the class name." },
|
|
77
|
+
parent_path: {
|
|
78
|
+
type: "string",
|
|
79
|
+
description: "NodePath of parent relative to scene root. '.' means root. Defaults to '.'.",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "scene_remove_node",
|
|
86
|
+
method: "scene.remove_node",
|
|
87
|
+
description: "Remove a node (and its descendants) from the currently-edited scene. Cannot remove the root.",
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: "object",
|
|
90
|
+
required: ["node_path"],
|
|
91
|
+
properties: { node_path: { type: "string" } },
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "scene_reparent",
|
|
96
|
+
method: "scene.reparent",
|
|
97
|
+
description:
|
|
98
|
+
"Move a node under a new parent in the currently-edited scene. Preserves global transform by default; refuses cycles.",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: "object",
|
|
101
|
+
required: ["node_path", "new_parent_path"],
|
|
102
|
+
properties: {
|
|
103
|
+
node_path: { type: "string" },
|
|
104
|
+
new_parent_path: { type: "string" },
|
|
105
|
+
keep_global_transform: { type: "boolean", default: true },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "scene_set_property",
|
|
111
|
+
method: "scene.set_property",
|
|
112
|
+
description:
|
|
113
|
+
"Set a property on a node. Coerces JSON to Godot types: Vector2/3 from [x,y(,z)], Color from [r,g,b(,a)] or '#hex', NodePath from string.",
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: "object",
|
|
116
|
+
required: ["node_path", "property"],
|
|
117
|
+
properties: {
|
|
118
|
+
node_path: { type: "string" },
|
|
119
|
+
property: { type: "string" },
|
|
120
|
+
value: { description: "Value, JSON-native. See description for coercion rules." },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "scene_open",
|
|
126
|
+
method: "scene.open",
|
|
127
|
+
description: "Open a scene in the editor so subsequent scene.* calls target it.",
|
|
128
|
+
inputSchema: {
|
|
129
|
+
type: "object",
|
|
130
|
+
required: ["path"],
|
|
131
|
+
properties: { path: { type: "string" } },
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "scene_save",
|
|
136
|
+
method: "scene.save",
|
|
137
|
+
description:
|
|
138
|
+
"Save the currently-edited scene. Pass 'path' to save-as (rebinds the scene to that path).",
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: "object",
|
|
141
|
+
properties: { path: { type: "string", description: "Optional save-as target." } },
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "scene_current",
|
|
146
|
+
method: "scene.current",
|
|
147
|
+
description: "Describe the currently-edited scene, or return {open: false} if none is open.",
|
|
148
|
+
inputSchema: { type: "object", properties: {} },
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "signal_connect",
|
|
152
|
+
method: "signal.connect",
|
|
153
|
+
description:
|
|
154
|
+
"Connect a signal between two nodes in the currently-edited scene. Connection is persistent (serialized into the .tscn as a [connection] block). Validates signal name, method existence, and arity.",
|
|
155
|
+
inputSchema: {
|
|
156
|
+
type: "object",
|
|
157
|
+
required: ["from", "signal", "to", "method"],
|
|
158
|
+
properties: {
|
|
159
|
+
from: { type: "string", description: "Source node path." },
|
|
160
|
+
signal: { type: "string" },
|
|
161
|
+
to: { type: "string", description: "Target node path." },
|
|
162
|
+
method: { type: "string", description: "Method name on the target's script." },
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: "signal_disconnect",
|
|
168
|
+
method: "signal.disconnect",
|
|
169
|
+
description: "Disconnect a specific signal wiring from the currently-edited scene.",
|
|
170
|
+
inputSchema: {
|
|
171
|
+
type: "object",
|
|
172
|
+
required: ["from", "signal", "to", "method"],
|
|
173
|
+
properties: {
|
|
174
|
+
from: { type: "string" },
|
|
175
|
+
signal: { type: "string" },
|
|
176
|
+
to: { type: "string" },
|
|
177
|
+
method: { type: "string" },
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "signal_list",
|
|
183
|
+
method: "signal.list",
|
|
184
|
+
description:
|
|
185
|
+
"List outgoing signal connections on a node. Each entry includes flags and a 'persistent' boolean so you can filter editor-serialized connections from runtime ones.",
|
|
186
|
+
inputSchema: {
|
|
187
|
+
type: "object",
|
|
188
|
+
required: ["node_path"],
|
|
189
|
+
properties: {
|
|
190
|
+
node_path: { type: "string" },
|
|
191
|
+
persistent_only: { type: "boolean", default: false },
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: "script_create",
|
|
197
|
+
method: "script.create",
|
|
198
|
+
description:
|
|
199
|
+
"Create a new .gd file with 'extends' and optional 'class_name', and optionally attach it to a node in the currently-edited scene.",
|
|
200
|
+
inputSchema: {
|
|
201
|
+
type: "object",
|
|
202
|
+
required: ["path"],
|
|
203
|
+
properties: {
|
|
204
|
+
path: { type: "string", description: "Target path, e.g. 'res://scripts/Player.gd'." },
|
|
205
|
+
extends: { type: "string", default: "Node" },
|
|
206
|
+
class_name: { type: "string" },
|
|
207
|
+
attach_to_node: { type: "string", description: "NodePath in the currently-edited scene." },
|
|
208
|
+
overwrite: { type: "boolean", default: false },
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: "script_attach",
|
|
214
|
+
method: "script.attach",
|
|
215
|
+
description: "Attach an existing script to a node in the currently-edited scene.",
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: "object",
|
|
218
|
+
required: ["node_path", "script_path"],
|
|
219
|
+
properties: {
|
|
220
|
+
node_path: { type: "string" },
|
|
221
|
+
script_path: { type: "string" },
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: "resource_create",
|
|
227
|
+
method: "resource.create",
|
|
228
|
+
description:
|
|
229
|
+
"Create a new .tres file. Use 'type' for built-in Resource subclasses (StyleBoxFlat, Theme, Curve, etc.) or 'script' pointing at a custom Resource .gd file. Optional 'properties' are applied before saving.",
|
|
230
|
+
inputSchema: {
|
|
231
|
+
type: "object",
|
|
232
|
+
required: ["path"],
|
|
233
|
+
properties: {
|
|
234
|
+
path: { type: "string", description: "Target path, e.g. 'res://themes/main.tres'." },
|
|
235
|
+
type: { type: "string", description: "Built-in Resource class name." },
|
|
236
|
+
script: { type: "string", description: "Path to a custom Resource .gd (use instead of 'type')." },
|
|
237
|
+
properties: {
|
|
238
|
+
type: "object",
|
|
239
|
+
description: "Initial property values. Same coercion rules as scene_set_property.",
|
|
240
|
+
additionalProperties: true,
|
|
241
|
+
},
|
|
242
|
+
overwrite: { type: "boolean", default: false },
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: "resource_set_property",
|
|
248
|
+
method: "resource.set_property",
|
|
249
|
+
description: "Load a .tres, set one property, and save. Uses the same JSON-to-Godot coercion as scene_set_property.",
|
|
250
|
+
inputSchema: {
|
|
251
|
+
type: "object",
|
|
252
|
+
required: ["path", "property"],
|
|
253
|
+
properties: {
|
|
254
|
+
path: { type: "string" },
|
|
255
|
+
property: { type: "string" },
|
|
256
|
+
value: { description: "Value, JSON-native." },
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: "refs_validate_project",
|
|
262
|
+
method: "refs.validate_project",
|
|
263
|
+
description:
|
|
264
|
+
"Project-wide scan for broken references: unparseable scripts, missing ext_resources, dangling UIDs, signal connections pointing at missing nodes or methods.",
|
|
265
|
+
inputSchema: { type: "object", properties: {} },
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: "refs_find_usages",
|
|
269
|
+
method: "refs.find_usages",
|
|
270
|
+
description:
|
|
271
|
+
"Find every file referencing a resource. Accepts a path or a uid:// — searches for both forms so uid-indirected references are caught. Returns file path, line number, and matched text.",
|
|
272
|
+
inputSchema: {
|
|
273
|
+
type: "object",
|
|
274
|
+
required: ["target"],
|
|
275
|
+
properties: {
|
|
276
|
+
target: { type: "string", description: "'res://...' or 'uid://...'" },
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: "refs_rename",
|
|
282
|
+
method: "refs.rename",
|
|
283
|
+
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. Writes to multiple files — review dry_run output first for anything non-trivial.",
|
|
285
|
+
inputSchema: {
|
|
286
|
+
type: "object",
|
|
287
|
+
required: ["from", "to"],
|
|
288
|
+
properties: {
|
|
289
|
+
from: { type: "string" },
|
|
290
|
+
to: { type: "string" },
|
|
291
|
+
overwrite: { type: "boolean", default: false },
|
|
292
|
+
dry_run: { type: "boolean", default: false },
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
name: "project_get_setting",
|
|
298
|
+
method: "project.get_setting",
|
|
299
|
+
description: "Read a project.godot setting by key (e.g. 'application/config/name'). Returns {exists: false} if unset.",
|
|
300
|
+
inputSchema: {
|
|
301
|
+
type: "object",
|
|
302
|
+
required: ["key"],
|
|
303
|
+
properties: { key: { type: "string" } },
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: "project_set_setting",
|
|
308
|
+
method: "project.set_setting",
|
|
309
|
+
description:
|
|
310
|
+
"Write a project.godot setting and save. DESTRUCTIVE — mutates project.godot. Prefer specific tools (autoload_add, etc.) when available.",
|
|
311
|
+
inputSchema: {
|
|
312
|
+
type: "object",
|
|
313
|
+
required: ["key", "value"],
|
|
314
|
+
properties: {
|
|
315
|
+
key: { type: "string" },
|
|
316
|
+
value: { description: "JSON-native value." },
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
name: "autoload_add",
|
|
322
|
+
method: "autoload.add",
|
|
323
|
+
description: "Register an autoload. Singleton by default (adds the '*' prefix so the name is globally accessible).",
|
|
324
|
+
inputSchema: {
|
|
325
|
+
type: "object",
|
|
326
|
+
required: ["name", "path"],
|
|
327
|
+
properties: {
|
|
328
|
+
name: { type: "string", description: "Globally-accessible name, e.g. 'GameState'." },
|
|
329
|
+
path: { type: "string", description: "Path to a .gd or .tscn, e.g. 'res://autoload/game_state.gd'." },
|
|
330
|
+
singleton: { type: "boolean", default: true },
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: "autoload_remove",
|
|
336
|
+
method: "autoload.remove",
|
|
337
|
+
description: "Unregister an autoload by name.",
|
|
338
|
+
inputSchema: {
|
|
339
|
+
type: "object",
|
|
340
|
+
required: ["name"],
|
|
341
|
+
properties: { name: { type: "string" } },
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: "autoload_list",
|
|
346
|
+
method: "autoload.list",
|
|
347
|
+
description: "List all registered autoloads with their paths and singleton flags.",
|
|
348
|
+
inputSchema: { type: "object", properties: {} },
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: "editor_reload_filesystem",
|
|
352
|
+
method: "editor.reload_filesystem",
|
|
353
|
+
description:
|
|
354
|
+
"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
|
+
inputSchema: { type: "object", properties: {} },
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
name: "docs_class_ref",
|
|
359
|
+
method: "docs.class_ref",
|
|
360
|
+
description:
|
|
361
|
+
"Return the public API of a Godot class (methods, properties, signals, constants) so the agent can plan calls without guessing. Defaults to class-local members; pass include_inherited:true to include ancestors.",
|
|
362
|
+
inputSchema: {
|
|
363
|
+
type: "object",
|
|
364
|
+
required: ["class_name"],
|
|
365
|
+
properties: {
|
|
366
|
+
class_name: { type: "string", description: "Godot class name, e.g. 'Timer', 'AnimationPlayer'." },
|
|
367
|
+
include_inherited: { type: "boolean", default: false },
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "input_map_add_action",
|
|
373
|
+
method: "input_map.add_action",
|
|
374
|
+
description: "Register a new input action in project.godot.",
|
|
375
|
+
inputSchema: {
|
|
376
|
+
type: "object",
|
|
377
|
+
required: ["name"],
|
|
378
|
+
properties: {
|
|
379
|
+
name: { type: "string", description: "Action name, e.g. 'jump'." },
|
|
380
|
+
deadzone: { type: "number", default: 0.5 },
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
name: "input_map_add_event",
|
|
386
|
+
method: "input_map.add_event",
|
|
387
|
+
description:
|
|
388
|
+
"Attach an input event to an existing action. Event shapes: {type:'key', keycode:'A'|'Space'|...}, {type:'mouse_button', button_index:1|2|3}, {type:'joy_button', button_index:0..}.",
|
|
389
|
+
inputSchema: {
|
|
390
|
+
type: "object",
|
|
391
|
+
required: ["action", "event"],
|
|
392
|
+
properties: {
|
|
393
|
+
action: { type: "string" },
|
|
394
|
+
event: {
|
|
395
|
+
type: "object",
|
|
396
|
+
required: ["type"],
|
|
397
|
+
properties: {
|
|
398
|
+
type: { type: "string", enum: ["key", "mouse_button", "joy_button"] },
|
|
399
|
+
keycode: { description: "For type='key': 'A', 'Space', 'F1', or int keycode." },
|
|
400
|
+
button_index: { type: "integer", description: "For mouse/joy button events." },
|
|
401
|
+
physical: { type: "boolean", default: true, description: "For type='key': use physical keycode (recommended)." },
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
name: "input_map_list",
|
|
409
|
+
method: "input_map.list",
|
|
410
|
+
description:
|
|
411
|
+
"List input actions with their events. Defaults to user-defined actions only (filters out Godot's ~90 built-in ui_* defaults). Pass include_builtins:true for the full list.",
|
|
412
|
+
inputSchema: {
|
|
413
|
+
type: "object",
|
|
414
|
+
properties: {
|
|
415
|
+
include_builtins: { type: "boolean", default: false },
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
name: "input_map_remove_action",
|
|
421
|
+
method: "input_map.remove_action",
|
|
422
|
+
description: "Delete a user-registered input action.",
|
|
423
|
+
inputSchema: {
|
|
424
|
+
type: "object",
|
|
425
|
+
required: ["name"],
|
|
426
|
+
properties: { name: { type: "string" } },
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
name: "run_scene_headless",
|
|
431
|
+
method: "run.scene_headless",
|
|
432
|
+
description:
|
|
433
|
+
"Run a scene in a headless child Godot process and return exit code plus combined stdout/stderr. BLOCKS the editor for the duration — use small quit_after_seconds (1-3) for smoke tests.",
|
|
434
|
+
inputSchema: {
|
|
435
|
+
type: "object",
|
|
436
|
+
required: ["path"],
|
|
437
|
+
properties: {
|
|
438
|
+
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." },
|
|
440
|
+
extra_args: {
|
|
441
|
+
type: "array",
|
|
442
|
+
items: { type: "string" },
|
|
443
|
+
description: "Additional CLI args passed to the child godot process.",
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
];
|
|
449
|
+
|
|
450
|
+
const BY_NAME = Object.fromEntries(TOOLS.map((t) => [t.name, t]));
|
|
451
|
+
|
|
452
|
+
function callGodot(method, params) {
|
|
453
|
+
return new Promise((resolve, reject) => {
|
|
454
|
+
const socket = new net.Socket();
|
|
455
|
+
let buffer = "";
|
|
456
|
+
let settled = false;
|
|
457
|
+
|
|
458
|
+
const settle = (fn, arg) => {
|
|
459
|
+
if (settled) return;
|
|
460
|
+
settled = true;
|
|
461
|
+
clearTimeout(timer);
|
|
462
|
+
try {
|
|
463
|
+
socket.end();
|
|
464
|
+
} catch {}
|
|
465
|
+
fn(arg);
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const timer = setTimeout(
|
|
469
|
+
() => settle(reject, new Error(`Godot tool '${method}' timed out after ${TIMEOUT_MS}ms`)),
|
|
470
|
+
TIMEOUT_MS
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
socket.setEncoding("utf8");
|
|
474
|
+
|
|
475
|
+
socket.on("data", (data) => {
|
|
476
|
+
buffer += data;
|
|
477
|
+
const nl = buffer.indexOf("\n");
|
|
478
|
+
if (nl < 0) return;
|
|
479
|
+
const line = buffer.slice(0, nl);
|
|
480
|
+
try {
|
|
481
|
+
const resp = JSON.parse(line);
|
|
482
|
+
if (resp.error) {
|
|
483
|
+
settle(
|
|
484
|
+
reject,
|
|
485
|
+
new Error(`Godot error ${resp.error.code}: ${resp.error.message}`)
|
|
486
|
+
);
|
|
487
|
+
} else {
|
|
488
|
+
settle(resolve, resp.result);
|
|
489
|
+
}
|
|
490
|
+
} catch (e) {
|
|
491
|
+
settle(reject, e);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
socket.on("error", (e) => {
|
|
496
|
+
if (e.code === "ECONNREFUSED") {
|
|
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
|
+
);
|
|
503
|
+
} else {
|
|
504
|
+
settle(reject, e);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
socket.on("end", () => settle(reject, new Error("Godot closed the connection before responding")));
|
|
509
|
+
|
|
510
|
+
socket.connect(PORT, HOST, () => {
|
|
511
|
+
socket.write(JSON.stringify({ id: 1, method, params: params || {} }) + "\n");
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const server = new Server(
|
|
517
|
+
{ name: "godot-agent-tools", version: "0.1.0" },
|
|
518
|
+
{ capabilities: { tools: {} } }
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
522
|
+
tools: TOOLS.map(({ name, description, inputSchema }) => ({
|
|
523
|
+
name,
|
|
524
|
+
description,
|
|
525
|
+
inputSchema,
|
|
526
|
+
})),
|
|
527
|
+
}));
|
|
528
|
+
|
|
529
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
530
|
+
const tool = BY_NAME[req.params.name];
|
|
531
|
+
if (!tool) {
|
|
532
|
+
return {
|
|
533
|
+
isError: true,
|
|
534
|
+
content: [{ type: "text", text: `Unknown tool: ${req.params.name}` }],
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
const result = await callGodot(tool.method, req.params.arguments || {});
|
|
539
|
+
return {
|
|
540
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
541
|
+
};
|
|
542
|
+
} catch (e) {
|
|
543
|
+
return {
|
|
544
|
+
isError: true,
|
|
545
|
+
content: [{ type: "text", text: e.message }],
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
const transport = new StdioServerTransport();
|
|
551
|
+
await server.connect(transport);
|