godot-mcp-runtime 1.1.0 → 2.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.
- package/README.md +36 -46
- package/dist/index.js +71 -10
- package/dist/index.js.map +1 -1
- package/dist/tools/node-tools.d.ts +121 -1
- package/dist/tools/node-tools.d.ts.map +1 -1
- package/dist/tools/node-tools.js +437 -277
- package/dist/tools/node-tools.js.map +1 -1
- package/dist/tools/project-tools.d.ts +85 -1
- package/dist/tools/project-tools.d.ts.map +1 -1
- package/dist/tools/project-tools.js +276 -192
- package/dist/tools/project-tools.js.map +1 -1
- package/dist/tools/scene-tools.d.ts +61 -1
- package/dist/tools/scene-tools.d.ts.map +1 -1
- package/dist/tools/scene-tools.js +246 -210
- package/dist/tools/scene-tools.js.map +1 -1
- package/dist/utils/godot-runner.d.ts +14 -0
- package/dist/utils/godot-runner.d.ts.map +1 -1
- package/dist/utils/godot-runner.js +59 -1
- package/dist/utils/godot-runner.js.map +1 -1
- package/package.json +1 -1
package/dist/tools/node-tools.js
CHANGED
|
@@ -1,89 +1,87 @@
|
|
|
1
|
-
import { join } from 'path';
|
|
2
1
|
import { existsSync } from 'fs';
|
|
3
|
-
import {
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { normalizeParameters, convertCamelToSnakeCase, validatePath, createErrorResponse, extractGdError, validateSceneArgs, } from '../utils/godot-runner.js';
|
|
4
|
+
// --- Tool definitions ---
|
|
4
5
|
export const nodeToolDefinitions = [
|
|
5
6
|
{
|
|
6
|
-
name: '
|
|
7
|
-
description: '
|
|
7
|
+
name: 'delete_node',
|
|
8
|
+
description: 'Remove a node from a Godot scene file. Saves automatically.',
|
|
8
9
|
inputSchema: {
|
|
9
10
|
type: 'object',
|
|
10
11
|
properties: {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
},
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
description: '[list] Scope results to this node path from scene root (e.g. "root/Player"). Defaults to the root node. Note: ignored by get_tree, which always starts from the scene root.',
|
|
42
|
-
},
|
|
43
|
-
changedOnly: {
|
|
44
|
-
type: 'boolean',
|
|
45
|
-
description: '[get_properties] Only return properties whose values differ from their class defaults (default: false)',
|
|
46
|
-
},
|
|
47
|
-
maxDepth: {
|
|
48
|
-
type: 'number',
|
|
49
|
-
description: '[get_tree] Maximum recursion depth. -1 for unlimited (default: -1). 1 returns only direct children.',
|
|
50
|
-
},
|
|
51
|
-
newName: {
|
|
52
|
-
type: 'string',
|
|
53
|
-
description: '[duplicate] Name for the duplicated node. Defaults to the original name + "2".',
|
|
54
|
-
},
|
|
55
|
-
targetParentPath: {
|
|
56
|
-
type: 'string',
|
|
57
|
-
description: '[duplicate] Node path of the parent to add the duplicate to. Defaults to the same parent as the original.',
|
|
58
|
-
},
|
|
59
|
-
signal: {
|
|
60
|
-
type: 'string',
|
|
61
|
-
description: '[connect_signal, disconnect_signal] Signal name on the source node (e.g. "pressed", "body_entered")',
|
|
62
|
-
},
|
|
63
|
-
targetNodePath: {
|
|
64
|
-
type: 'string',
|
|
65
|
-
description: '[connect_signal, disconnect_signal] Path of the target node that receives the signal (from scene root)',
|
|
66
|
-
},
|
|
67
|
-
method: {
|
|
68
|
-
type: 'string',
|
|
69
|
-
description: '[connect_signal, disconnect_signal] Method name on the target node to call when the signal fires',
|
|
70
|
-
},
|
|
12
|
+
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
13
|
+
scenePath: { type: 'string', description: 'Scene file path relative to the project (e.g. "scenes/main.tscn")' },
|
|
14
|
+
nodePath: { type: 'string', description: 'Node path from scene root (e.g. "root/Player/Sprite2D")' },
|
|
15
|
+
},
|
|
16
|
+
required: ['projectPath', 'scenePath', 'nodePath'],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'set_node_property',
|
|
21
|
+
description: 'Set a property on a node in a Godot scene file. Saves automatically. Primitives (string, number, boolean, array, object) are passed as-is. Vector2 ({"x","y"}), Vector3 ({"x","y","z"}), and Color ({"r","g","b","a"}) are automatically converted. Use run_script for other complex GDScript types.',
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
26
|
+
scenePath: { type: 'string', description: 'Scene file path relative to the project' },
|
|
27
|
+
nodePath: { type: 'string', description: 'Node path from scene root (e.g. "root/Player")' },
|
|
28
|
+
property: { type: 'string', description: 'GDScript property name in snake_case (e.g. "position", "modulate", "collision_layer"). Use get_node_properties to discover valid names.' },
|
|
29
|
+
value: { description: 'New property value' },
|
|
30
|
+
},
|
|
31
|
+
required: ['projectPath', 'scenePath', 'nodePath', 'property', 'value'],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'batch_set_node_properties',
|
|
36
|
+
description: 'Set multiple node properties in a single Godot process. Saves automatically. Returns { results: [{ nodePath, property, success?, error? }] }.',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
41
|
+
scenePath: { type: 'string', description: 'Scene file path relative to the project' },
|
|
71
42
|
updates: {
|
|
72
43
|
type: 'array',
|
|
73
|
-
description: '
|
|
44
|
+
description: 'Property updates to apply',
|
|
74
45
|
items: {
|
|
75
46
|
type: 'object',
|
|
76
47
|
properties: {
|
|
77
|
-
nodePath: { type: 'string', description: 'Node path from scene root
|
|
48
|
+
nodePath: { type: 'string', description: 'Node path from scene root' },
|
|
78
49
|
property: { type: 'string', description: 'GDScript property name in snake_case' },
|
|
79
50
|
value: { description: 'New property value' },
|
|
80
51
|
},
|
|
81
52
|
required: ['nodePath', 'property', 'value'],
|
|
82
53
|
},
|
|
83
54
|
},
|
|
55
|
+
abortOnError: { type: 'boolean', description: 'Stop processing on first error (default: false)' },
|
|
56
|
+
},
|
|
57
|
+
required: ['projectPath', 'scenePath', 'updates'],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'get_node_properties',
|
|
62
|
+
description: 'Read a node\'s current property values from a Godot scene file.',
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
67
|
+
scenePath: { type: 'string', description: 'Scene file path relative to the project' },
|
|
68
|
+
nodePath: { type: 'string', description: 'Node path from scene root (e.g. "root/Player")' },
|
|
69
|
+
changedOnly: { type: 'boolean', description: 'Only return properties whose values differ from their class defaults (default: false)' },
|
|
70
|
+
},
|
|
71
|
+
required: ['projectPath', 'scenePath', 'nodePath'],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'batch_get_node_properties',
|
|
76
|
+
description: 'Get properties from multiple nodes in a single Godot process. Returns { results: [{ nodePath, nodeType, properties?, error? }] }.',
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: {
|
|
80
|
+
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
81
|
+
scenePath: { type: 'string', description: 'Scene file path relative to the project' },
|
|
84
82
|
nodes: {
|
|
85
83
|
type: 'array',
|
|
86
|
-
description: '
|
|
84
|
+
description: 'Nodes to read properties from',
|
|
87
85
|
items: {
|
|
88
86
|
type: 'object',
|
|
89
87
|
properties: {
|
|
@@ -93,229 +91,391 @@ export const nodeToolDefinitions = [
|
|
|
93
91
|
required: ['nodePath'],
|
|
94
92
|
},
|
|
95
93
|
},
|
|
96
|
-
abortOnError: {
|
|
97
|
-
type: 'boolean',
|
|
98
|
-
description: '[update_property batch] Stop processing on first error (default: false)',
|
|
99
|
-
},
|
|
100
94
|
},
|
|
101
|
-
required: ['
|
|
95
|
+
required: ['projectPath', 'scenePath', 'nodes'],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'attach_script',
|
|
100
|
+
description: 'Attach a GDScript file to a node in a Godot scene. Saves automatically.',
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
105
|
+
scenePath: { type: 'string', description: 'Scene file path relative to the project' },
|
|
106
|
+
nodePath: { type: 'string', description: 'Node path from scene root (e.g. "root/Player")' },
|
|
107
|
+
scriptPath: { type: 'string', description: 'Path to the GDScript file relative to the project (e.g. "scripts/player.gd")' },
|
|
108
|
+
},
|
|
109
|
+
required: ['projectPath', 'scenePath', 'nodePath', 'scriptPath'],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'get_scene_tree',
|
|
114
|
+
description: 'Get the scene hierarchy as a tree structure. Use maxDepth: 1 for a shallow listing of direct children only.',
|
|
115
|
+
inputSchema: {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: {
|
|
118
|
+
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
119
|
+
scenePath: { type: 'string', description: 'Scene file path relative to the project' },
|
|
120
|
+
parentPath: { type: 'string', description: 'Scope to a subtree starting at this node path (e.g. "root/Player")' },
|
|
121
|
+
maxDepth: { type: 'number', description: 'Maximum recursion depth. -1 for unlimited (default: -1). 1 returns only direct children.' },
|
|
122
|
+
},
|
|
123
|
+
required: ['projectPath', 'scenePath'],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'duplicate_node',
|
|
128
|
+
description: 'Duplicate a node and its children in a Godot scene. Saves automatically.',
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: 'object',
|
|
131
|
+
properties: {
|
|
132
|
+
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
133
|
+
scenePath: { type: 'string', description: 'Scene file path relative to the project' },
|
|
134
|
+
nodePath: { type: 'string', description: 'Node path from scene root to duplicate' },
|
|
135
|
+
newName: { type: 'string', description: 'Name for the duplicated node (default: original name + "2")' },
|
|
136
|
+
targetParentPath: { type: 'string', description: 'Parent node path for the duplicate (default: same parent as original)' },
|
|
137
|
+
},
|
|
138
|
+
required: ['projectPath', 'scenePath', 'nodePath'],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'get_node_signals',
|
|
143
|
+
description: 'List all signals defined on a node and their current connections. Returns { nodePath, nodeType, signals: [{ name, connections: [{ signal, target, method }] }] }. Note: the target field uses Godot absolute path format (e.g. /root/Scene/Node) — convert to scene-root-relative (e.g. root/Node) before passing to connect_signal or disconnect_signal.',
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
properties: {
|
|
147
|
+
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
148
|
+
scenePath: { type: 'string', description: 'Scene file path relative to the project' },
|
|
149
|
+
nodePath: { type: 'string', description: 'Node path from scene root (e.g. "root/Button")' },
|
|
150
|
+
},
|
|
151
|
+
required: ['projectPath', 'scenePath', 'nodePath'],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'connect_signal',
|
|
156
|
+
description: 'Connect a signal from one node to a method on another node. Saves automatically. Errors if the signal does not exist on the source node or the method does not exist on the target node.',
|
|
157
|
+
inputSchema: {
|
|
158
|
+
type: 'object',
|
|
159
|
+
properties: {
|
|
160
|
+
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
161
|
+
scenePath: { type: 'string', description: 'Scene file path relative to the project' },
|
|
162
|
+
nodePath: { type: 'string', description: 'Source node path from scene root' },
|
|
163
|
+
signal: { type: 'string', description: 'Signal name on the source node (e.g. "pressed", "body_entered")' },
|
|
164
|
+
targetNodePath: { type: 'string', description: 'Target node path from scene root that receives the signal' },
|
|
165
|
+
method: { type: 'string', description: 'Method name on the target node to call when the signal fires' },
|
|
166
|
+
},
|
|
167
|
+
required: ['projectPath', 'scenePath', 'nodePath', 'signal', 'targetNodePath', 'method'],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'disconnect_signal',
|
|
172
|
+
description: 'Disconnect a signal connection between two nodes. Saves automatically. Errors if the connection does not exist — use get_node_signals first to verify.',
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: 'object',
|
|
175
|
+
properties: {
|
|
176
|
+
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
177
|
+
scenePath: { type: 'string', description: 'Scene file path relative to the project' },
|
|
178
|
+
nodePath: { type: 'string', description: 'Source node path from scene root' },
|
|
179
|
+
signal: { type: 'string', description: 'Signal name on the source node' },
|
|
180
|
+
targetNodePath: { type: 'string', description: 'Target node path from scene root' },
|
|
181
|
+
method: { type: 'string', description: 'Method name on the target node' },
|
|
182
|
+
},
|
|
183
|
+
required: ['projectPath', 'scenePath', 'nodePath', 'signal', 'targetNodePath', 'method'],
|
|
102
184
|
},
|
|
103
185
|
},
|
|
104
186
|
];
|
|
105
|
-
|
|
187
|
+
// --- Handlers ---
|
|
188
|
+
export async function handleDeleteNode(runner, args) {
|
|
106
189
|
args = normalizeParameters(args);
|
|
107
|
-
const
|
|
108
|
-
if (
|
|
109
|
-
return
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (!existsSync(projectFile)) {
|
|
119
|
-
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, ['Ensure the path points to a directory containing a project.godot file']);
|
|
120
|
-
}
|
|
121
|
-
const sceneFullPath = join(args.projectPath, args.scenePath);
|
|
122
|
-
if (!existsSync(sceneFullPath)) {
|
|
123
|
-
return createErrorResponse(`Scene file does not exist: ${args.scenePath}`, ['Ensure the scene path is correct']);
|
|
124
|
-
}
|
|
125
|
-
// Operations that require nodePath (skipped when batch params are provided)
|
|
126
|
-
const isBatchOp = (operation === 'update_property' && args.updates && Array.isArray(args.updates)) ||
|
|
127
|
-
(operation === 'get_properties' && args.nodes && Array.isArray(args.nodes));
|
|
128
|
-
const needsNodePath = ['delete', 'update_property', 'get_properties', 'attach_script', 'duplicate', 'get_signals', 'connect_signal', 'disconnect_signal'];
|
|
129
|
-
if (needsNodePath.includes(operation) && !isBatchOp) {
|
|
130
|
-
if (!args.nodePath) {
|
|
131
|
-
return createErrorResponse(`nodePath is required for ${operation}`, ['Provide the node path (e.g. "root/Player")']);
|
|
190
|
+
const v = validateSceneArgs(args);
|
|
191
|
+
if ('isError' in v)
|
|
192
|
+
return v;
|
|
193
|
+
if (!args.nodePath || !validatePath(args.nodePath)) {
|
|
194
|
+
return createErrorResponse('Valid nodePath is required', ['Provide the node path (e.g. "root/Player")']);
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const params = { scenePath: args.scenePath, nodePath: args.nodePath };
|
|
198
|
+
const { stdout, stderr } = await runner.executeOperation('delete_node', params, v.projectPath);
|
|
199
|
+
if (!stdout.trim()) {
|
|
200
|
+
return createErrorResponse(`Failed to delete node: ${extractGdError(stderr)}`, ['Check if the node path is correct']);
|
|
132
201
|
}
|
|
133
|
-
|
|
134
|
-
|
|
202
|
+
return { content: [{ type: 'text', text: stdout }] };
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
206
|
+
return createErrorResponse(`Failed to delete node: ${errorMessage}`, ['Ensure Godot is installed correctly']);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
export async function handleSetNodeProperty(runner, args) {
|
|
210
|
+
args = normalizeParameters(args);
|
|
211
|
+
const v = validateSceneArgs(args);
|
|
212
|
+
if ('isError' in v)
|
|
213
|
+
return v;
|
|
214
|
+
if (!args.nodePath || !validatePath(args.nodePath)) {
|
|
215
|
+
return createErrorResponse('Valid nodePath is required', ['Provide the node path (e.g. "root/Player")']);
|
|
216
|
+
}
|
|
217
|
+
if (!args.property || args.value === undefined) {
|
|
218
|
+
return createErrorResponse('property and value are required', ['Provide both property name and value']);
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const params = {
|
|
222
|
+
scenePath: args.scenePath,
|
|
223
|
+
nodePath: args.nodePath,
|
|
224
|
+
property: args.property,
|
|
225
|
+
value: args.value,
|
|
226
|
+
};
|
|
227
|
+
const { stdout, stderr } = await runner.executeOperation('update_node_property', params, v.projectPath);
|
|
228
|
+
if (!stdout.trim()) {
|
|
229
|
+
return createErrorResponse(`Failed to update property: ${extractGdError(stderr)}`, ['Check if the property name is valid for this node type']);
|
|
230
|
+
}
|
|
231
|
+
return { content: [{ type: 'text', text: stdout }] };
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
235
|
+
return createErrorResponse(`Failed to set node property: ${errorMessage}`, ['Ensure Godot is installed correctly']);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
export async function handleBatchSetNodeProperties(runner, args) {
|
|
239
|
+
args = normalizeParameters(args);
|
|
240
|
+
const v = validateSceneArgs(args);
|
|
241
|
+
if ('isError' in v)
|
|
242
|
+
return v;
|
|
243
|
+
if (!args.updates || !Array.isArray(args.updates)) {
|
|
244
|
+
return createErrorResponse('updates array is required', ['Provide an array of { nodePath, property, value }']);
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const snakeUpdates = args.updates.map(u => convertCamelToSnakeCase(u));
|
|
248
|
+
const params = {
|
|
249
|
+
scenePath: args.scenePath,
|
|
250
|
+
updates: snakeUpdates,
|
|
251
|
+
abortOnError: args.abortOnError ?? false,
|
|
252
|
+
};
|
|
253
|
+
const { stdout, stderr } = await runner.executeOperation('batch_update_node_properties', params, v.projectPath);
|
|
254
|
+
if (!stdout.trim()) {
|
|
255
|
+
return createErrorResponse(`Batch update failed: ${extractGdError(stderr)}`, ['Check node paths and property names']);
|
|
256
|
+
}
|
|
257
|
+
return { content: [{ type: 'text', text: stdout }] };
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
261
|
+
return createErrorResponse(`Batch set properties failed: ${errorMessage}`, ['Ensure Godot is installed correctly']);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
export async function handleGetNodeProperties(runner, args) {
|
|
265
|
+
args = normalizeParameters(args);
|
|
266
|
+
const v = validateSceneArgs(args);
|
|
267
|
+
if ('isError' in v)
|
|
268
|
+
return v;
|
|
269
|
+
if (!args.nodePath || !validatePath(args.nodePath)) {
|
|
270
|
+
return createErrorResponse('Valid nodePath is required', ['Provide the node path (e.g. "root/Player")']);
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
const params = { scenePath: args.scenePath, nodePath: args.nodePath };
|
|
274
|
+
if (args.changedOnly)
|
|
275
|
+
params.changedOnly = args.changedOnly;
|
|
276
|
+
const { stdout, stderr } = await runner.executeOperation('get_node_properties', params, v.projectPath);
|
|
277
|
+
if (!stdout.trim()) {
|
|
278
|
+
return createErrorResponse(`Failed to get properties: ${extractGdError(stderr)}`, ['Check if the node path is correct']);
|
|
279
|
+
}
|
|
280
|
+
return { content: [{ type: 'text', text: stdout }] };
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
284
|
+
return createErrorResponse(`Failed to get node properties: ${errorMessage}`, ['Ensure Godot is installed correctly']);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
export async function handleBatchGetNodeProperties(runner, args) {
|
|
288
|
+
args = normalizeParameters(args);
|
|
289
|
+
const v = validateSceneArgs(args);
|
|
290
|
+
if ('isError' in v)
|
|
291
|
+
return v;
|
|
292
|
+
if (!args.nodes || !Array.isArray(args.nodes)) {
|
|
293
|
+
return createErrorResponse('nodes array is required', ['Provide an array of { nodePath, changedOnly? }']);
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const snakeNodes = args.nodes.map(n => convertCamelToSnakeCase(n));
|
|
297
|
+
const params = { scenePath: args.scenePath, nodes: snakeNodes };
|
|
298
|
+
const { stdout, stderr } = await runner.executeOperation('batch_get_node_properties', params, v.projectPath);
|
|
299
|
+
if (!stdout.trim()) {
|
|
300
|
+
return createErrorResponse(`Batch get_properties failed: ${extractGdError(stderr)}`, ['Check node paths']);
|
|
301
|
+
}
|
|
302
|
+
return { content: [{ type: 'text', text: stdout }] };
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
306
|
+
return createErrorResponse(`Batch get properties failed: ${errorMessage}`, ['Ensure Godot is installed correctly']);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
export async function handleAttachScript(runner, args) {
|
|
310
|
+
args = normalizeParameters(args);
|
|
311
|
+
const v = validateSceneArgs(args);
|
|
312
|
+
if ('isError' in v)
|
|
313
|
+
return v;
|
|
314
|
+
if (!args.nodePath || !validatePath(args.nodePath)) {
|
|
315
|
+
return createErrorResponse('Valid nodePath is required', ['Provide the node path (e.g. "root/Player")']);
|
|
316
|
+
}
|
|
317
|
+
if (!args.scriptPath || !validatePath(args.scriptPath)) {
|
|
318
|
+
return createErrorResponse('Valid scriptPath is required', ['Provide the script path relative to the project']);
|
|
319
|
+
}
|
|
320
|
+
const scriptFullPath = join(v.projectPath, args.scriptPath);
|
|
321
|
+
if (!existsSync(scriptFullPath)) {
|
|
322
|
+
return createErrorResponse(`Script file does not exist: ${args.scriptPath}`, ['Create the script file first']);
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
const params = {
|
|
326
|
+
scenePath: args.scenePath,
|
|
327
|
+
nodePath: args.nodePath,
|
|
328
|
+
scriptPath: args.scriptPath,
|
|
329
|
+
};
|
|
330
|
+
const { stdout, stderr } = await runner.executeOperation('attach_script', params, v.projectPath);
|
|
331
|
+
if (!stdout.trim()) {
|
|
332
|
+
return createErrorResponse(`Failed to attach script: ${extractGdError(stderr)}`, ['Ensure the script is valid for this node type']);
|
|
333
|
+
}
|
|
334
|
+
return { content: [{ type: 'text', text: stdout }] };
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
338
|
+
return createErrorResponse(`Failed to attach script: ${errorMessage}`, ['Ensure Godot is installed correctly']);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
export async function handleGetSceneTree(runner, args) {
|
|
342
|
+
args = normalizeParameters(args);
|
|
343
|
+
const v = validateSceneArgs(args);
|
|
344
|
+
if ('isError' in v)
|
|
345
|
+
return v;
|
|
346
|
+
if (args.parentPath && !validatePath(args.parentPath)) {
|
|
347
|
+
return createErrorResponse('Invalid parentPath', ['Provide a valid path without ".."']);
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
const params = { scenePath: args.scenePath };
|
|
351
|
+
if (args.parentPath)
|
|
352
|
+
params.parentPath = args.parentPath;
|
|
353
|
+
if (typeof args.maxDepth === 'number')
|
|
354
|
+
params.maxDepth = args.maxDepth;
|
|
355
|
+
const { stdout, stderr } = await runner.executeOperation('get_scene_tree', params, v.projectPath);
|
|
356
|
+
if (!stdout.trim()) {
|
|
357
|
+
return createErrorResponse(`Failed to get scene tree: ${extractGdError(stderr)}`, ['Ensure the scene is valid']);
|
|
358
|
+
}
|
|
359
|
+
return { content: [{ type: 'text', text: stdout }] };
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
363
|
+
return createErrorResponse(`Failed to get scene tree: ${errorMessage}`, ['Ensure Godot is installed correctly']);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
export async function handleDuplicateNode(runner, args) {
|
|
367
|
+
args = normalizeParameters(args);
|
|
368
|
+
const v = validateSceneArgs(args);
|
|
369
|
+
if ('isError' in v)
|
|
370
|
+
return v;
|
|
371
|
+
if (!args.nodePath || !validatePath(args.nodePath)) {
|
|
372
|
+
return createErrorResponse('Valid nodePath is required', ['Provide the node path to duplicate']);
|
|
373
|
+
}
|
|
374
|
+
if (args.targetParentPath && !validatePath(args.targetParentPath)) {
|
|
375
|
+
return createErrorResponse('Invalid targetParentPath', ['Provide a valid path without ".."']);
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
const params = { scenePath: args.scenePath, nodePath: args.nodePath };
|
|
379
|
+
if (args.newName)
|
|
380
|
+
params.newName = args.newName;
|
|
381
|
+
if (args.targetParentPath)
|
|
382
|
+
params.targetParentPath = args.targetParentPath;
|
|
383
|
+
const { stdout, stderr } = await runner.executeOperation('duplicate_node', params, v.projectPath);
|
|
384
|
+
if (!stdout.trim()) {
|
|
385
|
+
return createErrorResponse(`Failed to duplicate node: ${extractGdError(stderr)}`, ['Check if the node path and target parent path are correct']);
|
|
386
|
+
}
|
|
387
|
+
return { content: [{ type: 'text', text: stdout }] };
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
391
|
+
return createErrorResponse(`Failed to duplicate node: ${errorMessage}`, ['Ensure Godot is installed correctly']);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
export async function handleGetNodeSignals(runner, args) {
|
|
395
|
+
args = normalizeParameters(args);
|
|
396
|
+
const v = validateSceneArgs(args);
|
|
397
|
+
if ('isError' in v)
|
|
398
|
+
return v;
|
|
399
|
+
if (!args.nodePath || !validatePath(args.nodePath)) {
|
|
400
|
+
return createErrorResponse('Valid nodePath is required', ['Provide the node path (e.g. "root/Button")']);
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
const params = { scenePath: args.scenePath, nodePath: args.nodePath };
|
|
404
|
+
const { stdout, stderr } = await runner.executeOperation('get_node_signals', params, v.projectPath);
|
|
405
|
+
if (!stdout.trim()) {
|
|
406
|
+
return createErrorResponse(`Failed to get signals: ${extractGdError(stderr)}`, ['Check if the node path is correct']);
|
|
135
407
|
}
|
|
408
|
+
return { content: [{ type: 'text', text: stdout }] };
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
412
|
+
return createErrorResponse(`Failed to get node signals: ${errorMessage}`, ['Ensure Godot is installed correctly']);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
export async function handleConnectSignal(runner, args) {
|
|
416
|
+
args = normalizeParameters(args);
|
|
417
|
+
const v = validateSceneArgs(args);
|
|
418
|
+
if ('isError' in v)
|
|
419
|
+
return v;
|
|
420
|
+
if (!args.nodePath || !validatePath(args.nodePath)) {
|
|
421
|
+
return createErrorResponse('Valid nodePath is required', ['Provide the source node path']);
|
|
422
|
+
}
|
|
423
|
+
if (!args.signal || !args.targetNodePath || !args.method) {
|
|
424
|
+
return createErrorResponse('signal, targetNodePath, and method are required', ['Provide all three parameters']);
|
|
425
|
+
}
|
|
426
|
+
if (!validatePath(args.targetNodePath)) {
|
|
427
|
+
return createErrorResponse('Invalid targetNodePath', ['Provide a valid path without ".."']);
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
const params = {
|
|
431
|
+
scenePath: args.scenePath,
|
|
432
|
+
nodePath: args.nodePath,
|
|
433
|
+
signal: args.signal,
|
|
434
|
+
targetNodePath: args.targetNodePath,
|
|
435
|
+
method: args.method,
|
|
436
|
+
};
|
|
437
|
+
const { stdout, stderr } = await runner.executeOperation('connect_node_signal', params, v.projectPath);
|
|
438
|
+
if (!stdout.trim()) {
|
|
439
|
+
return createErrorResponse(`Failed to connect signal: ${extractGdError(stderr)}`, ['Ensure the signal exists on the source node and the method exists on the target node']);
|
|
440
|
+
}
|
|
441
|
+
return { content: [{ type: 'text', text: stdout }] };
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
445
|
+
return createErrorResponse(`Failed to connect signal: ${errorMessage}`, ['Ensure Godot is installed correctly']);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
export async function handleDisconnectSignal(runner, args) {
|
|
449
|
+
args = normalizeParameters(args);
|
|
450
|
+
const v = validateSceneArgs(args);
|
|
451
|
+
if ('isError' in v)
|
|
452
|
+
return v;
|
|
453
|
+
if (!args.nodePath || !validatePath(args.nodePath)) {
|
|
454
|
+
return createErrorResponse('Valid nodePath is required', ['Provide the source node path']);
|
|
455
|
+
}
|
|
456
|
+
if (!args.signal || !args.targetNodePath || !args.method) {
|
|
457
|
+
return createErrorResponse('signal, targetNodePath, and method are required', ['Provide all three parameters']);
|
|
458
|
+
}
|
|
459
|
+
if (!validatePath(args.targetNodePath)) {
|
|
460
|
+
return createErrorResponse('Invalid targetNodePath', ['Provide a valid path without ".."']);
|
|
136
461
|
}
|
|
137
462
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// Batch mode: updates array
|
|
149
|
-
if (args.updates && Array.isArray(args.updates)) {
|
|
150
|
-
// Convert each item to snake_case for GDScript
|
|
151
|
-
// (convertCamelToSnakeCase doesn't recurse into arrays, so we map over items)
|
|
152
|
-
const snakeUpdates = args.updates.map(u => convertCamelToSnakeCase(u));
|
|
153
|
-
const params = {
|
|
154
|
-
scenePath: args.scenePath,
|
|
155
|
-
updates: snakeUpdates,
|
|
156
|
-
abortOnError: args.abortOnError ?? false,
|
|
157
|
-
};
|
|
158
|
-
const { stdout, stderr } = await runner.executeOperation('batch_update_node_properties', params, args.projectPath);
|
|
159
|
-
if (!stdout.trim()) {
|
|
160
|
-
return createErrorResponse(`Batch update failed: ${extractGdError(stderr)}`, ['Check node paths and property names']);
|
|
161
|
-
}
|
|
162
|
-
return { content: [{ type: 'text', text: stdout }] };
|
|
163
|
-
}
|
|
164
|
-
// Single-op
|
|
165
|
-
if (!args.property || args.value === undefined) {
|
|
166
|
-
return createErrorResponse('property and value are required for update_property', ['Provide both property name and value']);
|
|
167
|
-
}
|
|
168
|
-
const params = {
|
|
169
|
-
scenePath: args.scenePath,
|
|
170
|
-
nodePath: args.nodePath,
|
|
171
|
-
property: args.property,
|
|
172
|
-
value: args.value,
|
|
173
|
-
};
|
|
174
|
-
const { stdout, stderr } = await runner.executeOperation('update_node_property', params, args.projectPath);
|
|
175
|
-
if (!stdout.trim()) {
|
|
176
|
-
return createErrorResponse(`Failed to update property: ${extractGdError(stderr)}`, ['Check if the property name is valid for this node type']);
|
|
177
|
-
}
|
|
178
|
-
return { content: [{ type: 'text', text: stdout }] };
|
|
179
|
-
}
|
|
180
|
-
case 'get_properties': {
|
|
181
|
-
// Batch mode: nodes array
|
|
182
|
-
if (args.nodes && Array.isArray(args.nodes)) {
|
|
183
|
-
// Convert each item to snake_case for GDScript
|
|
184
|
-
const snakeNodes = args.nodes.map(n => convertCamelToSnakeCase(n));
|
|
185
|
-
const params = { scenePath: args.scenePath, nodes: snakeNodes };
|
|
186
|
-
const { stdout, stderr } = await runner.executeOperation('batch_get_node_properties', params, args.projectPath);
|
|
187
|
-
if (!stdout.trim()) {
|
|
188
|
-
return createErrorResponse(`Batch get_properties failed: ${extractGdError(stderr)}`, ['Check node paths']);
|
|
189
|
-
}
|
|
190
|
-
return { content: [{ type: 'text', text: stdout }] };
|
|
191
|
-
}
|
|
192
|
-
// Single-op
|
|
193
|
-
const params = { scenePath: args.scenePath, nodePath: args.nodePath };
|
|
194
|
-
if (args.changedOnly)
|
|
195
|
-
params.changedOnly = args.changedOnly;
|
|
196
|
-
const { stdout, stderr } = await runner.executeOperation('get_node_properties', params, args.projectPath);
|
|
197
|
-
if (!stdout.trim()) {
|
|
198
|
-
return createErrorResponse(`Failed to get properties: ${extractGdError(stderr)}`, ['Check if the node path is correct']);
|
|
199
|
-
}
|
|
200
|
-
return { content: [{ type: 'text', text: stdout }] };
|
|
201
|
-
}
|
|
202
|
-
case 'attach_script': {
|
|
203
|
-
if (!args.scriptPath) {
|
|
204
|
-
return createErrorResponse('scriptPath is required for attach_script', ['Provide the script path relative to the project']);
|
|
205
|
-
}
|
|
206
|
-
if (!validatePath(args.scriptPath)) {
|
|
207
|
-
return createErrorResponse('Invalid script path', ['Provide a valid path without ".."']);
|
|
208
|
-
}
|
|
209
|
-
const scriptFullPath = join(args.projectPath, args.scriptPath);
|
|
210
|
-
if (!existsSync(scriptFullPath)) {
|
|
211
|
-
return createErrorResponse(`Script file does not exist: ${args.scriptPath}`, ['Create the script file first']);
|
|
212
|
-
}
|
|
213
|
-
const params = {
|
|
214
|
-
scenePath: args.scenePath,
|
|
215
|
-
nodePath: args.nodePath,
|
|
216
|
-
scriptPath: args.scriptPath,
|
|
217
|
-
};
|
|
218
|
-
const { stdout, stderr } = await runner.executeOperation('attach_script', params, args.projectPath);
|
|
219
|
-
if (!stdout.trim()) {
|
|
220
|
-
return createErrorResponse(`Failed to attach script: ${extractGdError(stderr)}`, ['Ensure the script is valid for this node type']);
|
|
221
|
-
}
|
|
222
|
-
return { content: [{ type: 'text', text: stdout }] };
|
|
223
|
-
}
|
|
224
|
-
case 'list': {
|
|
225
|
-
if (args.parentPath && !validatePath(args.parentPath)) {
|
|
226
|
-
return createErrorResponse('Invalid parent path', ['Provide a valid path without ".."']);
|
|
227
|
-
}
|
|
228
|
-
const params = { scenePath: args.scenePath };
|
|
229
|
-
if (args.parentPath)
|
|
230
|
-
params.parentPath = args.parentPath;
|
|
231
|
-
const { stdout, stderr } = await runner.executeOperation('list_nodes', params, args.projectPath);
|
|
232
|
-
if (!stdout.trim()) {
|
|
233
|
-
return createErrorResponse(`Failed to list nodes: ${extractGdError(stderr)}`, ['Check if the parent path is correct']);
|
|
234
|
-
}
|
|
235
|
-
return { content: [{ type: 'text', text: stdout }] };
|
|
236
|
-
}
|
|
237
|
-
case 'get_tree': {
|
|
238
|
-
const params = { scenePath: args.scenePath };
|
|
239
|
-
if (args.parentPath)
|
|
240
|
-
params.parentPath = args.parentPath;
|
|
241
|
-
if (typeof args.maxDepth === 'number')
|
|
242
|
-
params.maxDepth = args.maxDepth;
|
|
243
|
-
const { stdout, stderr } = await runner.executeOperation('get_scene_tree', params, args.projectPath);
|
|
244
|
-
if (!stdout.trim()) {
|
|
245
|
-
return createErrorResponse(`Failed to get scene tree: ${extractGdError(stderr)}`, ['Ensure the scene is valid']);
|
|
246
|
-
}
|
|
247
|
-
return { content: [{ type: 'text', text: stdout }] };
|
|
248
|
-
}
|
|
249
|
-
case 'duplicate': {
|
|
250
|
-
if (args.targetParentPath && !validatePath(args.targetParentPath)) {
|
|
251
|
-
return createErrorResponse('Invalid targetParentPath', ['Provide a valid path without ".."']);
|
|
252
|
-
}
|
|
253
|
-
const params = { scenePath: args.scenePath, nodePath: args.nodePath };
|
|
254
|
-
if (args.newName)
|
|
255
|
-
params.newName = args.newName;
|
|
256
|
-
if (args.targetParentPath)
|
|
257
|
-
params.targetParentPath = args.targetParentPath;
|
|
258
|
-
const { stdout, stderr } = await runner.executeOperation('duplicate_node', params, args.projectPath);
|
|
259
|
-
if (!stdout.trim()) {
|
|
260
|
-
return createErrorResponse(`Failed to duplicate node: ${extractGdError(stderr)}`, ['Check if the node path and target parent path are correct']);
|
|
261
|
-
}
|
|
262
|
-
return { content: [{ type: 'text', text: stdout }] };
|
|
263
|
-
}
|
|
264
|
-
case 'get_signals': {
|
|
265
|
-
const params = { scenePath: args.scenePath, nodePath: args.nodePath };
|
|
266
|
-
const { stdout, stderr } = await runner.executeOperation('get_node_signals', params, args.projectPath);
|
|
267
|
-
if (!stdout.trim()) {
|
|
268
|
-
return createErrorResponse(`Failed to get signals: ${extractGdError(stderr)}`, ['Check if the node path is correct']);
|
|
269
|
-
}
|
|
270
|
-
return { content: [{ type: 'text', text: stdout }] };
|
|
271
|
-
}
|
|
272
|
-
case 'connect_signal': {
|
|
273
|
-
if (!args.signal || !args.targetNodePath || !args.method) {
|
|
274
|
-
return createErrorResponse('signal, targetNodePath, and method are required for connect_signal', ['Provide all three parameters']);
|
|
275
|
-
}
|
|
276
|
-
if (!validatePath(args.targetNodePath)) {
|
|
277
|
-
return createErrorResponse('Invalid targetNodePath', ['Provide a valid path without ".."']);
|
|
278
|
-
}
|
|
279
|
-
const params = {
|
|
280
|
-
scenePath: args.scenePath,
|
|
281
|
-
nodePath: args.nodePath,
|
|
282
|
-
signal: args.signal,
|
|
283
|
-
targetNodePath: args.targetNodePath,
|
|
284
|
-
method: args.method,
|
|
285
|
-
};
|
|
286
|
-
const { stdout, stderr } = await runner.executeOperation('connect_node_signal', params, args.projectPath);
|
|
287
|
-
if (!stdout.trim()) {
|
|
288
|
-
return createErrorResponse(`Failed to connect signal: ${extractGdError(stderr)}`, ['Ensure the signal exists on the source node and the method exists on the target node']);
|
|
289
|
-
}
|
|
290
|
-
return { content: [{ type: 'text', text: stdout }] };
|
|
291
|
-
}
|
|
292
|
-
case 'disconnect_signal': {
|
|
293
|
-
if (!args.signal || !args.targetNodePath || !args.method) {
|
|
294
|
-
return createErrorResponse('signal, targetNodePath, and method are required for disconnect_signal', ['Provide all three parameters']);
|
|
295
|
-
}
|
|
296
|
-
if (!validatePath(args.targetNodePath)) {
|
|
297
|
-
return createErrorResponse('Invalid targetNodePath', ['Provide a valid path without ".."']);
|
|
298
|
-
}
|
|
299
|
-
const params = {
|
|
300
|
-
scenePath: args.scenePath,
|
|
301
|
-
nodePath: args.nodePath,
|
|
302
|
-
signal: args.signal,
|
|
303
|
-
targetNodePath: args.targetNodePath,
|
|
304
|
-
method: args.method,
|
|
305
|
-
};
|
|
306
|
-
const { stdout, stderr } = await runner.executeOperation('disconnect_node_signal', params, args.projectPath);
|
|
307
|
-
if (!stdout.trim()) {
|
|
308
|
-
return createErrorResponse(`Failed to disconnect signal: ${extractGdError(stderr)}`, ['Ensure the signal connection exists before trying to disconnect it']);
|
|
309
|
-
}
|
|
310
|
-
return { content: [{ type: 'text', text: stdout }] };
|
|
311
|
-
}
|
|
312
|
-
default:
|
|
313
|
-
return createErrorResponse(`Unknown operation: ${operation}`, ['Use one of: delete, update_property, get_properties, attach_script, list, get_tree, duplicate, get_signals, connect_signal, disconnect_signal']);
|
|
463
|
+
const params = {
|
|
464
|
+
scenePath: args.scenePath,
|
|
465
|
+
nodePath: args.nodePath,
|
|
466
|
+
signal: args.signal,
|
|
467
|
+
targetNodePath: args.targetNodePath,
|
|
468
|
+
method: args.method,
|
|
469
|
+
};
|
|
470
|
+
const { stdout, stderr } = await runner.executeOperation('disconnect_node_signal', params, v.projectPath);
|
|
471
|
+
if (!stdout.trim()) {
|
|
472
|
+
return createErrorResponse(`Failed to disconnect signal: ${extractGdError(stderr)}`, ['Ensure the signal connection exists before trying to disconnect it']);
|
|
314
473
|
}
|
|
474
|
+
return { content: [{ type: 'text', text: stdout }] };
|
|
315
475
|
}
|
|
316
476
|
catch (error) {
|
|
317
477
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
318
|
-
return createErrorResponse(`Failed to
|
|
478
|
+
return createErrorResponse(`Failed to disconnect signal: ${errorMessage}`, ['Ensure Godot is installed correctly']);
|
|
319
479
|
}
|
|
320
480
|
}
|
|
321
481
|
//# sourceMappingURL=node-tools.js.map
|