godot-mcp-runtime 2.2.3 → 3.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 +30 -93
- package/dist/dispatch.d.ts +1 -11
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +7 -8
- package/dist/dispatch.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -10
- package/dist/index.js.map +1 -1
- package/dist/scripts/godot_operations.gd +134 -283
- package/dist/scripts/mcp_bridge.gd +210 -43
- package/dist/tools/autoload-tools.d.ts +51 -0
- package/dist/tools/autoload-tools.d.ts.map +1 -0
- package/dist/tools/autoload-tools.js +187 -0
- package/dist/tools/autoload-tools.js.map +1 -0
- package/dist/tools/node-tools.d.ts +9 -78
- package/dist/tools/node-tools.d.ts.map +1 -1
- package/dist/tools/node-tools.js +114 -310
- package/dist/tools/node-tools.js.map +1 -1
- package/dist/tools/project-tools.d.ts +0 -168
- package/dist/tools/project-tools.d.ts.map +1 -1
- package/dist/tools/project-tools.js +120 -1192
- package/dist/tools/project-tools.js.map +1 -1
- package/dist/tools/runtime-tools.d.ts +108 -0
- package/dist/tools/runtime-tools.d.ts.map +1 -0
- package/dist/tools/runtime-tools.js +808 -0
- package/dist/tools/runtime-tools.js.map +1 -0
- package/dist/tools/scene-tools.d.ts +6 -48
- package/dist/tools/scene-tools.d.ts.map +1 -1
- package/dist/tools/scene-tools.js +67 -211
- package/dist/tools/scene-tools.js.map +1 -1
- package/dist/tools/validate-tools.d.ts.map +1 -1
- package/dist/tools/validate-tools.js +35 -29
- package/dist/tools/validate-tools.js.map +1 -1
- package/dist/utils/autoload-ini.d.ts +32 -0
- package/dist/utils/autoload-ini.d.ts.map +1 -0
- package/dist/utils/autoload-ini.js +111 -0
- package/dist/utils/autoload-ini.js.map +1 -0
- package/dist/utils/bridge-manager.d.ts +29 -0
- package/dist/utils/bridge-manager.d.ts.map +1 -0
- package/dist/utils/bridge-manager.js +136 -0
- package/dist/utils/bridge-manager.js.map +1 -0
- package/dist/utils/bridge-protocol.d.ts +34 -0
- package/dist/utils/bridge-protocol.d.ts.map +1 -0
- package/dist/utils/bridge-protocol.js +65 -0
- package/dist/utils/bridge-protocol.js.map +1 -0
- package/dist/utils/godot-runner.d.ts +70 -15
- package/dist/utils/godot-runner.d.ts.map +1 -1
- package/dist/utils/godot-runner.js +309 -277
- package/dist/utils/godot-runner.js.map +1 -1
- package/dist/utils/handler-helpers.d.ts +34 -0
- package/dist/utils/handler-helpers.d.ts.map +1 -0
- package/dist/utils/handler-helpers.js +55 -0
- package/dist/utils/handler-helpers.js.map +1 -0
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +11 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +7 -4
|
@@ -1,186 +1,13 @@
|
|
|
1
|
-
import { join, basename
|
|
2
|
-
import { existsSync, readdirSync, readFileSync
|
|
3
|
-
import { normalizeParameters, validatePath, validateProjectArgs, createErrorResponse,
|
|
4
|
-
|
|
5
|
-
function parseAutoloads(projectFilePath) {
|
|
6
|
-
const content = readFileSync(projectFilePath, 'utf8');
|
|
7
|
-
const autoloads = [];
|
|
8
|
-
let inAutoloadSection = false;
|
|
9
|
-
for (const line of content.split('\n')) {
|
|
10
|
-
const trimmed = line.trim();
|
|
11
|
-
if (trimmed.startsWith('[')) {
|
|
12
|
-
inAutoloadSection = trimmed === '[autoload]';
|
|
13
|
-
continue;
|
|
14
|
-
}
|
|
15
|
-
if (!inAutoloadSection || trimmed === '' || trimmed.startsWith(';') || trimmed.startsWith('#'))
|
|
16
|
-
continue;
|
|
17
|
-
const match = trimmed.match(/^(\w+)="(\*?)([^"]*)"$/);
|
|
18
|
-
if (match) {
|
|
19
|
-
autoloads.push({ name: match[1], singleton: match[2] === '*', path: match[3] });
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return autoloads;
|
|
23
|
-
}
|
|
24
|
-
function normalizeAutoloadPath(p) {
|
|
25
|
-
return p.startsWith('res://') ? p : `res://${p}`;
|
|
26
|
-
}
|
|
27
|
-
function addAutoloadEntry(projectFilePath, name, path, singleton) {
|
|
28
|
-
const content = readFileSync(projectFilePath, 'utf8');
|
|
29
|
-
const lines = content.split('\n');
|
|
30
|
-
const entry = `${name}="${singleton ? '*' : ''}${normalizeAutoloadPath(path)}"`;
|
|
31
|
-
const sectionIdx = lines.findIndex((l) => l.trim() === '[autoload]');
|
|
32
|
-
if (sectionIdx === -1) {
|
|
33
|
-
writeFileSync(projectFilePath, content.trimEnd() + '\n\n[autoload]\n' + entry + '\n', 'utf8');
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
let insertIdx = sectionIdx + 1;
|
|
37
|
-
while (insertIdx < lines.length && !lines[insertIdx].trim().startsWith('[')) {
|
|
38
|
-
insertIdx++;
|
|
39
|
-
}
|
|
40
|
-
lines.splice(insertIdx, 0, entry);
|
|
41
|
-
writeFileSync(projectFilePath, lines.join('\n'), 'utf8');
|
|
42
|
-
}
|
|
43
|
-
function removeAutoloadEntry(projectFilePath, name) {
|
|
44
|
-
const content = readFileSync(projectFilePath, 'utf8');
|
|
45
|
-
const lines = content.split('\n');
|
|
46
|
-
let inAutoloadSection = false;
|
|
47
|
-
let removed = false;
|
|
48
|
-
const newLines = lines.filter((line) => {
|
|
49
|
-
const trimmed = line.trim();
|
|
50
|
-
if (trimmed.startsWith('[')) {
|
|
51
|
-
inAutoloadSection = trimmed === '[autoload]';
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
if (inAutoloadSection) {
|
|
55
|
-
const match = trimmed.match(/^(\w+)=/);
|
|
56
|
-
if (match && match[1] === name) {
|
|
57
|
-
removed = true;
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return true;
|
|
62
|
-
});
|
|
63
|
-
if (removed)
|
|
64
|
-
writeFileSync(projectFilePath, newLines.join('\n'), 'utf8');
|
|
65
|
-
return removed;
|
|
66
|
-
}
|
|
67
|
-
function updateAutoloadEntry(projectFilePath, name, newPath, singleton) {
|
|
68
|
-
const content = readFileSync(projectFilePath, 'utf8');
|
|
69
|
-
const lines = content.split('\n');
|
|
70
|
-
let inAutoloadSection = false;
|
|
71
|
-
let updated = false;
|
|
72
|
-
const newLines = lines.map((line) => {
|
|
73
|
-
const trimmed = line.trim();
|
|
74
|
-
if (trimmed.startsWith('[')) {
|
|
75
|
-
inAutoloadSection = trimmed === '[autoload]';
|
|
76
|
-
return line;
|
|
77
|
-
}
|
|
78
|
-
if (inAutoloadSection) {
|
|
79
|
-
const match = trimmed.match(/^(\w+)="(\*?)([^"]*)"$/);
|
|
80
|
-
if (match && match[1] === name) {
|
|
81
|
-
const effectiveSingleton = singleton !== undefined ? singleton : match[2] === '*';
|
|
82
|
-
const effectivePath = newPath !== undefined ? normalizeAutoloadPath(newPath) : match[3];
|
|
83
|
-
updated = true;
|
|
84
|
-
return `${name}="${effectiveSingleton ? '*' : ''}${effectivePath}"`;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return line;
|
|
88
|
-
});
|
|
89
|
-
if (updated)
|
|
90
|
-
writeFileSync(projectFilePath, newLines.join('\n'), 'utf8');
|
|
91
|
-
return updated;
|
|
92
|
-
}
|
|
1
|
+
import { join, basename } from 'path';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
3
|
+
import { normalizeParameters, validatePath, validateProjectArgs, createErrorResponse, getErrorMessage, projectGodotPath, } from '../utils/godot-runner.js';
|
|
4
|
+
import { logDebug } from '../utils/logger.js';
|
|
93
5
|
// --- Tool definitions ---
|
|
94
6
|
export const projectToolDefinitions = [
|
|
95
|
-
{
|
|
96
|
-
name: 'launch_editor',
|
|
97
|
-
description: 'Open the Godot editor GUI for a project. The editor is a display application — it cannot be controlled programmatically and returns immediately. For headless project modification, use the scene and node editing tools (add_node, set_node_property, etc.) instead.',
|
|
98
|
-
inputSchema: {
|
|
99
|
-
type: 'object',
|
|
100
|
-
properties: {
|
|
101
|
-
projectPath: {
|
|
102
|
-
type: 'string',
|
|
103
|
-
description: 'Path to the Godot project directory',
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
required: ['projectPath'],
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
name: 'run_project',
|
|
111
|
-
description: 'Run a Godot project with stdout/stderr captured. Preferred path for runtime tools. Required before calling take_screenshot, simulate_input, get_ui_elements, run_script, or get_debug_output unless you intentionally use attach_project for a manually launched game. Verifies MCP bridge readiness before returning success. Call stop_project when done.',
|
|
112
|
-
inputSchema: {
|
|
113
|
-
type: 'object',
|
|
114
|
-
properties: {
|
|
115
|
-
projectPath: {
|
|
116
|
-
type: 'string',
|
|
117
|
-
description: 'Path to the Godot project directory',
|
|
118
|
-
},
|
|
119
|
-
scene: {
|
|
120
|
-
type: 'string',
|
|
121
|
-
description: 'Scene to run (path relative to project, e.g. "scenes/main.tscn"). Omit to use the project\'s main scene.',
|
|
122
|
-
},
|
|
123
|
-
background: {
|
|
124
|
-
type: 'boolean',
|
|
125
|
-
description: 'If true, hides the Godot window off-screen and blocks all physical keyboard and mouse input, while keeping programmatic input (simulate_input, run_script) and screenshots fully active. Useful for automated agent-driven testing where the window should not be visible or interactive.',
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
required: ['projectPath'],
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
name: 'attach_project',
|
|
133
|
-
description: 'Attach runtime MCP tools to a project without spawning Godot. This injects the McpBridge autoload and marks the project as the active runtime session so you can launch the game manually from your own shell, then use take_screenshot, simulate_input, get_ui_elements, and run_script against that running game. Set waitForBridge to true after launching Godot to block until the bridge is confirmed ready. Use detach_project or stop_project when done. get_debug_output is not available in attached mode because stdout/stderr are not captured.',
|
|
134
|
-
inputSchema: {
|
|
135
|
-
type: 'object',
|
|
136
|
-
properties: {
|
|
137
|
-
projectPath: {
|
|
138
|
-
type: 'string',
|
|
139
|
-
description: 'Path to the Godot project directory',
|
|
140
|
-
},
|
|
141
|
-
waitForBridge: {
|
|
142
|
-
type: 'boolean',
|
|
143
|
-
description: 'If true, poll the bridge until it responds (up to 15 seconds). Use this after Godot is already running to confirm runtime tools are ready. Defaults to false.',
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
required: ['projectPath'],
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
name: 'detach_project',
|
|
151
|
-
description: 'Clear attached-mode runtime state and remove the injected McpBridge autoload without claiming that the manually launched Godot process was stopped.',
|
|
152
|
-
inputSchema: {
|
|
153
|
-
type: 'object',
|
|
154
|
-
properties: {},
|
|
155
|
-
required: [],
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
name: 'get_debug_output',
|
|
160
|
-
description: 'Get stdout/stderr output from the running Godot project. Requires run_project first. Returns the last N lines of output and errors, a running flag, and an exit code if the process has ended. In attached mode, this reports that stdout/stderr capture is unavailable because Godot was launched outside MCP.',
|
|
161
|
-
inputSchema: {
|
|
162
|
-
type: 'object',
|
|
163
|
-
properties: {
|
|
164
|
-
limit: {
|
|
165
|
-
type: 'number',
|
|
166
|
-
description: 'Max lines to return (default: 200, from end of output)',
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
required: [],
|
|
170
|
-
},
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
name: 'stop_project',
|
|
174
|
-
description: 'Stop the running Godot project and clean up the MCP bridge. Always call this when done with runtime testing — even if the game crashed — to free the process slot so run_project can be called again.',
|
|
175
|
-
inputSchema: {
|
|
176
|
-
type: 'object',
|
|
177
|
-
properties: {},
|
|
178
|
-
required: [],
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
7
|
{
|
|
182
8
|
name: 'list_projects',
|
|
183
|
-
description: '
|
|
9
|
+
description: 'Find Godot projects under a directory by locating project.godot files. Use to discover available projects when the user has not specified one; for inspecting a known project, use get_project_info. recursive:true descends into subdirectories (skipping hidden ones); default false checks only the directory itself and its immediate children. Returns { projects: [{ path, name }] }, empty array on no matches.',
|
|
10
|
+
annotations: { readOnlyHint: true },
|
|
184
11
|
inputSchema: {
|
|
185
12
|
type: 'object',
|
|
186
13
|
properties: {
|
|
@@ -198,7 +25,8 @@ export const projectToolDefinitions = [
|
|
|
198
25
|
},
|
|
199
26
|
{
|
|
200
27
|
name: 'get_project_info',
|
|
201
|
-
description: '
|
|
28
|
+
description: 'Get metadata about a Godot project: name, path, Godot version, and a structure summary (counts of scenes/scripts/assets/other). Omit projectPath to get just the Godot version (useful for capability checks). Returns { name, path, godotVersion, structure } or { godotVersion } when projectPath is omitted. Errors if projectPath is set but lacks project.godot.',
|
|
29
|
+
annotations: { readOnlyHint: true },
|
|
202
30
|
inputSchema: {
|
|
203
31
|
type: 'object',
|
|
204
32
|
properties: {
|
|
@@ -210,196 +38,10 @@ export const projectToolDefinitions = [
|
|
|
210
38
|
required: [],
|
|
211
39
|
},
|
|
212
40
|
},
|
|
213
|
-
{
|
|
214
|
-
name: 'take_screenshot',
|
|
215
|
-
description: 'Capture a PNG screenshot of the running Godot viewport. Requires an active runtime session (run_project or attach_project). Returns the image inline. Screenshots are also saved to .mcp/screenshots/ in the project directory.',
|
|
216
|
-
inputSchema: {
|
|
217
|
-
type: 'object',
|
|
218
|
-
properties: {
|
|
219
|
-
timeout: {
|
|
220
|
-
type: 'number',
|
|
221
|
-
description: 'Timeout in milliseconds to wait for the screenshot (default: 10000)',
|
|
222
|
-
},
|
|
223
|
-
},
|
|
224
|
-
required: [],
|
|
225
|
-
},
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
name: 'simulate_input',
|
|
229
|
-
description: 'Simulate batched sequential input in a running Godot project. Requires an active runtime session (run_project or attach_project). Use get_ui_elements first to discover element names and paths for click_element actions.\n\nEach action object requires a "type" field. Valid types and their specific fields:\n- key: keyboard event (key: string, pressed: bool, shift/ctrl/alt: bool)\n- mouse_button: click at coordinates (x, y: number, button: "left"|"right"|"middle", pressed: bool, double_click: bool)\n- mouse_motion: move cursor (x, y: number, relative_x, relative_y: number)\n- click_element: click a UI element by node path or node name (element: string, button, double_click)\n- action: fire a Godot input action (action: string, pressed: bool, strength: 0–1)\n- wait: pause between actions (ms: number)\n\nExamples:\n1. Press and release Space: [{type:"key",key:"Space",pressed:true},{type:"wait",ms:100},{type:"key",key:"Space",pressed:false}]\n2. Click a UI button (discover path with get_ui_elements first): [{type:"click_element",element:"StartButton"}]\n3. Left-click at viewport coordinates: [{type:"mouse_button",x:400,y:300,button:"left",pressed:true},{type:"mouse_button",x:400,y:300,button:"left",pressed:false}]\n4. Fire a Godot action: [{type:"action",action:"jump",pressed:true},{type:"wait",ms:200},{type:"action",action:"jump",pressed:false}]\n5. Type "hello": [{type:"key",key:"H",pressed:true},{type:"key",key:"H",pressed:false},{type:"key",key:"E",pressed:true},{type:"key",key:"E",pressed:false},{type:"key",key:"L",pressed:true},{type:"key",key:"L",pressed:false},{type:"key",key:"L",pressed:true},{type:"key",key:"L",pressed:false},{type:"key",key:"O",pressed:true},{type:"key",key:"O",pressed:false}]',
|
|
230
|
-
inputSchema: {
|
|
231
|
-
type: 'object',
|
|
232
|
-
properties: {
|
|
233
|
-
actions: {
|
|
234
|
-
type: 'array',
|
|
235
|
-
description: 'Array of input actions to execute sequentially. Each object must have a "type" field.',
|
|
236
|
-
items: {
|
|
237
|
-
type: 'object',
|
|
238
|
-
properties: {
|
|
239
|
-
type: {
|
|
240
|
-
type: 'string',
|
|
241
|
-
enum: ['key', 'mouse_button', 'mouse_motion', 'click_element', 'action', 'wait'],
|
|
242
|
-
description: 'The type of input action',
|
|
243
|
-
},
|
|
244
|
-
key: {
|
|
245
|
-
type: 'string',
|
|
246
|
-
description: '[key] Key name (e.g. "W", "Space", "Escape", "Up")',
|
|
247
|
-
},
|
|
248
|
-
pressed: {
|
|
249
|
-
type: 'boolean',
|
|
250
|
-
description: '[key, mouse_button, action] Whether the input is pressed (true) or released (false)',
|
|
251
|
-
},
|
|
252
|
-
shift: { type: 'boolean', description: '[key] Shift modifier' },
|
|
253
|
-
ctrl: { type: 'boolean', description: '[key] Ctrl modifier' },
|
|
254
|
-
alt: { type: 'boolean', description: '[key] Alt modifier' },
|
|
255
|
-
button: {
|
|
256
|
-
type: 'string',
|
|
257
|
-
enum: ['left', 'right', 'middle'],
|
|
258
|
-
description: '[mouse_button, click_element] Mouse button (default: left)',
|
|
259
|
-
},
|
|
260
|
-
x: {
|
|
261
|
-
type: 'number',
|
|
262
|
-
description: '[mouse_button, mouse_motion] X position in viewport pixels',
|
|
263
|
-
},
|
|
264
|
-
y: {
|
|
265
|
-
type: 'number',
|
|
266
|
-
description: '[mouse_button, mouse_motion] Y position in viewport pixels',
|
|
267
|
-
},
|
|
268
|
-
relative_x: {
|
|
269
|
-
type: 'number',
|
|
270
|
-
description: '[mouse_motion] Relative X movement in pixels',
|
|
271
|
-
},
|
|
272
|
-
relative_y: {
|
|
273
|
-
type: 'number',
|
|
274
|
-
description: '[mouse_motion] Relative Y movement in pixels',
|
|
275
|
-
},
|
|
276
|
-
double_click: {
|
|
277
|
-
type: 'boolean',
|
|
278
|
-
description: '[mouse_button, click_element] Double click',
|
|
279
|
-
},
|
|
280
|
-
element: {
|
|
281
|
-
type: 'string',
|
|
282
|
-
description: '[click_element] Identifies the UI element to click. Accepts: absolute node path (e.g. "/root/HUD/Button"), relative node path, or node name (BFS matched). Use get_ui_elements to discover valid names and paths.',
|
|
283
|
-
},
|
|
284
|
-
action: {
|
|
285
|
-
type: 'string',
|
|
286
|
-
description: '[action] Godot input action name (as defined in Project Settings > Input Map)',
|
|
287
|
-
},
|
|
288
|
-
strength: {
|
|
289
|
-
type: 'number',
|
|
290
|
-
description: '[action] Action strength (0–1, default 1.0)',
|
|
291
|
-
},
|
|
292
|
-
ms: {
|
|
293
|
-
type: 'number',
|
|
294
|
-
description: '[wait] Duration in milliseconds to pause before the next action',
|
|
295
|
-
},
|
|
296
|
-
},
|
|
297
|
-
required: ['type'],
|
|
298
|
-
},
|
|
299
|
-
},
|
|
300
|
-
},
|
|
301
|
-
required: ['actions'],
|
|
302
|
-
},
|
|
303
|
-
},
|
|
304
|
-
{
|
|
305
|
-
name: 'get_ui_elements',
|
|
306
|
-
description: 'Get Control nodes from a running Godot project with their positions, sizes, types, and text. Requires an active runtime session (run_project or attach_project). Call this before simulate_input with click_element to discover valid element names and paths. Returns: { elements: [{ name, path, type, rect: {x,y,width,height}, visible, text? (Button/Label/LineEdit/TextEdit), disabled? (buttons), tooltip? }] }',
|
|
307
|
-
inputSchema: {
|
|
308
|
-
type: 'object',
|
|
309
|
-
properties: {
|
|
310
|
-
visibleOnly: {
|
|
311
|
-
type: 'boolean',
|
|
312
|
-
description: 'Only return nodes where Control.visible is true (default: true). Set false to include hidden elements.',
|
|
313
|
-
},
|
|
314
|
-
filter: {
|
|
315
|
-
type: 'string',
|
|
316
|
-
description: 'Filter by Control node type (e.g. "Button", "Label", "LineEdit")',
|
|
317
|
-
},
|
|
318
|
-
},
|
|
319
|
-
required: [],
|
|
320
|
-
},
|
|
321
|
-
},
|
|
322
|
-
{
|
|
323
|
-
name: 'run_script',
|
|
324
|
-
description: 'Execute a custom GDScript in the live running project with full scene tree access. Requires run_project first. Script must extend RefCounted and define func execute(scene_tree: SceneTree) -> Variant. Return values are JSON-serialized (primitives, Vector2/3, Color, Dictionary, Array, and Node path strings are supported). Use print() for debug output — it appears in get_debug_output, not in the script result. In spawned mode, runtime errors emitted to stderr are detected and either escalated (when the script returns null) or surfaced as warnings. In attached mode a null result includes a caveat since stderr is not captured.',
|
|
325
|
-
inputSchema: {
|
|
326
|
-
type: 'object',
|
|
327
|
-
properties: {
|
|
328
|
-
script: {
|
|
329
|
-
type: 'string',
|
|
330
|
-
description: 'GDScript source code. Must contain "extends RefCounted" and "func execute(scene_tree: SceneTree) -> Variant".',
|
|
331
|
-
},
|
|
332
|
-
timeout: {
|
|
333
|
-
type: 'number',
|
|
334
|
-
description: 'Timeout in ms (default: 30000). Increase for long-running scripts.',
|
|
335
|
-
},
|
|
336
|
-
},
|
|
337
|
-
required: ['script'],
|
|
338
|
-
},
|
|
339
|
-
},
|
|
340
|
-
{
|
|
341
|
-
name: 'list_autoloads',
|
|
342
|
-
description: 'List all registered autoloads in a Godot project with paths and singleton status. No Godot process required — reads project.godot directly.',
|
|
343
|
-
inputSchema: {
|
|
344
|
-
type: 'object',
|
|
345
|
-
properties: {
|
|
346
|
-
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
347
|
-
},
|
|
348
|
-
required: ['projectPath'],
|
|
349
|
-
},
|
|
350
|
-
},
|
|
351
|
-
{
|
|
352
|
-
name: 'add_autoload',
|
|
353
|
-
description: 'Register a new autoload in a Godot project. No Godot process required. Warning: autoloads initialize in headless mode — if the script has errors, all headless operations will fail.',
|
|
354
|
-
inputSchema: {
|
|
355
|
-
type: 'object',
|
|
356
|
-
properties: {
|
|
357
|
-
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
358
|
-
autoloadName: {
|
|
359
|
-
type: 'string',
|
|
360
|
-
description: 'Name of the autoload node (e.g. "MyManager")',
|
|
361
|
-
},
|
|
362
|
-
autoloadPath: {
|
|
363
|
-
type: 'string',
|
|
364
|
-
description: 'Path to the script or scene (e.g. "res://autoload/my_manager.gd" or "autoload/my_manager.gd")',
|
|
365
|
-
},
|
|
366
|
-
singleton: {
|
|
367
|
-
type: 'boolean',
|
|
368
|
-
description: 'Register as a globally accessible singleton by name (default: true)',
|
|
369
|
-
},
|
|
370
|
-
},
|
|
371
|
-
required: ['projectPath', 'autoloadName', 'autoloadPath'],
|
|
372
|
-
},
|
|
373
|
-
},
|
|
374
|
-
{
|
|
375
|
-
name: 'remove_autoload',
|
|
376
|
-
description: 'Unregister an autoload from a Godot project by name. No Godot process required.',
|
|
377
|
-
inputSchema: {
|
|
378
|
-
type: 'object',
|
|
379
|
-
properties: {
|
|
380
|
-
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
381
|
-
autoloadName: { type: 'string', description: 'Name of the autoload to remove' },
|
|
382
|
-
},
|
|
383
|
-
required: ['projectPath', 'autoloadName'],
|
|
384
|
-
},
|
|
385
|
-
},
|
|
386
|
-
{
|
|
387
|
-
name: 'update_autoload',
|
|
388
|
-
description: "Modify an existing autoload's path or singleton flag. No Godot process required.",
|
|
389
|
-
inputSchema: {
|
|
390
|
-
type: 'object',
|
|
391
|
-
properties: {
|
|
392
|
-
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
393
|
-
autoloadName: { type: 'string', description: 'Name of the autoload to update' },
|
|
394
|
-
autoloadPath: { type: 'string', description: 'New path to the script or scene' },
|
|
395
|
-
singleton: { type: 'boolean', description: 'New singleton flag' },
|
|
396
|
-
},
|
|
397
|
-
required: ['projectPath', 'autoloadName'],
|
|
398
|
-
},
|
|
399
|
-
},
|
|
400
41
|
{
|
|
401
42
|
name: 'get_project_files',
|
|
402
|
-
description: 'Return a recursive file tree of a Godot project. Skips hidden (dot-prefixed) entries and the .mcp directory. Returns nested { name, type, path, extension?, children? }
|
|
43
|
+
description: 'Return a recursive file tree of a Godot project. Use to discover project structure when paths are unknown. Pass extensions to filter (e.g. ["gd","tscn"]); maxDepth caps recursion (-1 unlimited). Skips hidden (dot-prefixed) entries and the .mcp directory. Returns nested { name, type, path, extension?, children? } file tree.',
|
|
44
|
+
annotations: { readOnlyHint: true },
|
|
403
45
|
inputSchema: {
|
|
404
46
|
type: 'object',
|
|
405
47
|
properties: {
|
|
@@ -419,7 +61,8 @@ export const projectToolDefinitions = [
|
|
|
419
61
|
},
|
|
420
62
|
{
|
|
421
63
|
name: 'search_project',
|
|
422
|
-
description: 'Plain-text search across project files. Returns { matches: [{ file, lineNumber, line }], truncated }. Skips hidden entries and the .mcp directory.',
|
|
64
|
+
description: 'Plain-text (substring) search across project files. Use to find references, callers, or signatures across the codebase. Default fileTypes is ["gd","tscn","cs","gdshader"]; caseSensitive default false; maxResults default 100 (truncated:true if hit). Returns { matches: [{ file, lineNumber, line }], truncated }. Skips hidden entries and the .mcp directory.',
|
|
65
|
+
annotations: { readOnlyHint: true },
|
|
423
66
|
inputSchema: {
|
|
424
67
|
type: 'object',
|
|
425
68
|
properties: {
|
|
@@ -438,7 +81,8 @@ export const projectToolDefinitions = [
|
|
|
438
81
|
},
|
|
439
82
|
{
|
|
440
83
|
name: 'get_scene_dependencies',
|
|
441
|
-
description: 'Parse a .tscn file for ext_resource references (scripts, textures, subscenes). Returns { scene, dependencies: [{ path, type, uid? }] }.',
|
|
84
|
+
description: 'Parse a .tscn file for ext_resource references (scripts, textures, subscenes). Use to inspect what a scene depends on before refactoring or moving files. Returns { scene, dependencies: [{ path, type, uid? }] }. Errors if scene file does not exist.',
|
|
85
|
+
annotations: { readOnlyHint: true },
|
|
442
86
|
inputSchema: {
|
|
443
87
|
type: 'object',
|
|
444
88
|
properties: {
|
|
@@ -453,7 +97,8 @@ export const projectToolDefinitions = [
|
|
|
453
97
|
},
|
|
454
98
|
{
|
|
455
99
|
name: 'get_project_settings',
|
|
456
|
-
description: 'Parse project.godot into structured JSON. Returns { settings: { [section]: { [key]: value } } }. Complex Godot types are returned as raw strings
|
|
100
|
+
description: 'Parse project.godot into structured JSON. Use to inspect configured display, input, rendering, etc. settings without launching Godot. Pass section to filter to one INI section (e.g. "display", "application"). Returns { settings: { [section]: { [key]: value } } } or { settings: { [key]: value } } when section is given. Complex Godot types are returned as raw strings; keys outside any section appear under __global__.',
|
|
101
|
+
annotations: { readOnlyHint: true },
|
|
457
102
|
inputSchema: {
|
|
458
103
|
type: 'object',
|
|
459
104
|
properties: {
|
|
@@ -467,26 +112,12 @@ export const projectToolDefinitions = [
|
|
|
467
112
|
},
|
|
468
113
|
},
|
|
469
114
|
];
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
return createErrorResponse(`No active runtime session. A project must be running or attached to ${actionDescription}.`, [
|
|
473
|
-
'Use run_project to start a Godot project first',
|
|
474
|
-
'Or use attach_project before launching Godot manually',
|
|
475
|
-
]);
|
|
476
|
-
}
|
|
477
|
-
if (runner.activeSessionMode === 'spawned' &&
|
|
478
|
-
(!runner.activeProcess || runner.activeProcess.hasExited)) {
|
|
479
|
-
return createErrorResponse(`The spawned Godot process has exited and cannot ${actionDescription}.`, [
|
|
480
|
-
'Use get_debug_output to inspect the last captured logs',
|
|
481
|
-
'Call stop_project to clean up, then run_project again',
|
|
482
|
-
]);
|
|
483
|
-
}
|
|
484
|
-
return null;
|
|
485
|
-
}
|
|
115
|
+
// --- Helpers ---
|
|
116
|
+
const PROJECT_SCAN_BLACKLIST = new Set(['.git', '.godot', '.mcp', 'node_modules', '.svn', '.hg']);
|
|
486
117
|
function findGodotProjects(directory, recursive) {
|
|
487
118
|
const projects = [];
|
|
488
119
|
try {
|
|
489
|
-
const projectFile =
|
|
120
|
+
const projectFile = projectGodotPath(directory);
|
|
490
121
|
if (existsSync(projectFile)) {
|
|
491
122
|
projects.push({
|
|
492
123
|
path: directory,
|
|
@@ -495,10 +126,10 @@ function findGodotProjects(directory, recursive) {
|
|
|
495
126
|
}
|
|
496
127
|
const entries = readdirSync(directory, { withFileTypes: true });
|
|
497
128
|
for (const entry of entries) {
|
|
498
|
-
if (!entry.isDirectory() || entry.name
|
|
129
|
+
if (!entry.isDirectory() || PROJECT_SCAN_BLACKLIST.has(entry.name))
|
|
499
130
|
continue;
|
|
500
131
|
const subdir = join(directory, entry.name);
|
|
501
|
-
if (existsSync(
|
|
132
|
+
if (existsSync(projectGodotPath(subdir))) {
|
|
502
133
|
projects.push({ path: subdir, name: entry.name });
|
|
503
134
|
}
|
|
504
135
|
else if (recursive) {
|
|
@@ -530,7 +161,8 @@ function getProjectStructure(projectPath) {
|
|
|
530
161
|
scanDirectory(entryPath);
|
|
531
162
|
}
|
|
532
163
|
else if (entry.isFile()) {
|
|
533
|
-
const
|
|
164
|
+
const dotIdx = entry.name.lastIndexOf('.');
|
|
165
|
+
const ext = dotIdx >= 0 ? entry.name.slice(dotIdx + 1).toLowerCase() : '';
|
|
534
166
|
if (ext === 'tscn') {
|
|
535
167
|
structure.scenes++;
|
|
536
168
|
}
|
|
@@ -553,713 +185,51 @@ function getProjectStructure(projectPath) {
|
|
|
553
185
|
scanDirectory(projectPath);
|
|
554
186
|
return structure;
|
|
555
187
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
563
|
-
if (!validatePath(args.projectPath)) {
|
|
564
|
-
return createErrorResponse('Invalid project path', [
|
|
565
|
-
'Provide a valid path without ".." or other potentially unsafe characters',
|
|
566
|
-
]);
|
|
567
|
-
}
|
|
568
|
-
try {
|
|
569
|
-
if (!runner.getGodotPath()) {
|
|
570
|
-
await runner.detectGodotPath();
|
|
571
|
-
if (!runner.getGodotPath()) {
|
|
572
|
-
return createErrorResponse('Could not find a valid Godot executable path', [
|
|
573
|
-
'Ensure Godot is installed correctly',
|
|
574
|
-
'Set GODOT_PATH environment variable',
|
|
575
|
-
]);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
const projectFile = join(args.projectPath, 'project.godot');
|
|
579
|
-
if (!existsSync(projectFile)) {
|
|
580
|
-
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, [
|
|
581
|
-
'Ensure the path points to a directory containing a project.godot file',
|
|
582
|
-
'Use list_projects to find valid Godot projects',
|
|
583
|
-
]);
|
|
584
|
-
}
|
|
585
|
-
logDebug(`Launching Godot editor for project: ${args.projectPath}`);
|
|
586
|
-
const process = runner.launchEditor(args.projectPath);
|
|
587
|
-
process.on('error', (err) => {
|
|
588
|
-
console.error('Failed to start Godot editor:', err);
|
|
589
|
-
});
|
|
590
|
-
return {
|
|
591
|
-
content: [
|
|
592
|
-
{
|
|
593
|
-
type: 'text',
|
|
594
|
-
text: `Godot editor launched successfully for project at ${args.projectPath}.\nNote: the editor is a GUI application and cannot be controlled programmatically. Use the scene and node editing tools (add_node, set_node_property, etc.) to modify the project headlessly without the editor.`,
|
|
595
|
-
},
|
|
596
|
-
],
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
catch (error) {
|
|
600
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
601
|
-
return createErrorResponse(`Failed to launch Godot editor: ${errorMessage}`, [
|
|
602
|
-
'Ensure Godot is installed correctly',
|
|
603
|
-
'Check if the GODOT_PATH environment variable is set correctly',
|
|
604
|
-
]);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
export async function handleRunProject(runner, args) {
|
|
608
|
-
args = normalizeParameters(args);
|
|
609
|
-
if (!args.projectPath) {
|
|
610
|
-
return createErrorResponse('Project path is required', [
|
|
611
|
-
'Provide a valid path to a Godot project directory',
|
|
612
|
-
]);
|
|
613
|
-
}
|
|
614
|
-
if (!validatePath(args.projectPath)) {
|
|
615
|
-
return createErrorResponse('Invalid project path', [
|
|
616
|
-
'Provide a valid path without ".." or other potentially unsafe characters',
|
|
617
|
-
]);
|
|
188
|
+
function buildFilesystemTree(currentPath, relativePath, maxDepth, currentDepth, extensions) {
|
|
189
|
+
const name = basename(currentPath);
|
|
190
|
+
const node = { name, type: 'dir', path: relativePath || '.' };
|
|
191
|
+
if (maxDepth !== -1 && currentDepth >= maxDepth) {
|
|
192
|
+
node.children = [];
|
|
193
|
+
return node;
|
|
618
194
|
}
|
|
195
|
+
const children = [];
|
|
619
196
|
try {
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
const background = args.background === true;
|
|
628
|
-
runner.runProject(args.projectPath, args.scene, background);
|
|
629
|
-
const bridgeResult = await runner.waitForBridge();
|
|
630
|
-
if (!bridgeResult.ready) {
|
|
631
|
-
if (runner.activeProcess && runner.activeProcess.hasExited) {
|
|
632
|
-
return createErrorResponse(`Godot process exited before the MCP bridge could initialize.\n${bridgeResult.error || ''}`, [
|
|
633
|
-
'Check get_debug_output for runtime errors',
|
|
634
|
-
'Verify a display server is available (Wayland/X11)',
|
|
635
|
-
'Check for broken autoloads with list_autoloads',
|
|
636
|
-
'Call stop_project to clean up, then try again',
|
|
637
|
-
]);
|
|
197
|
+
const entries = readdirSync(currentPath, { withFileTypes: true });
|
|
198
|
+
for (const entry of entries) {
|
|
199
|
+
if (entry.name.startsWith('.'))
|
|
200
|
+
continue;
|
|
201
|
+
const childRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
202
|
+
if (entry.isDirectory()) {
|
|
203
|
+
children.push(buildFilesystemTree(join(currentPath, entry.name), childRelPath, maxDepth, currentDepth + 1, extensions));
|
|
638
204
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
];
|
|
646
|
-
if (background) {
|
|
647
|
-
lines.push('- Background mode: window hidden, physical input blocked');
|
|
205
|
+
else if (entry.isFile()) {
|
|
206
|
+
const dotIdx = entry.name.lastIndexOf('.');
|
|
207
|
+
const ext = dotIdx >= 0 ? entry.name.slice(dotIdx + 1).toLowerCase() : '';
|
|
208
|
+
if (extensions && !extensions.includes(ext))
|
|
209
|
+
continue;
|
|
210
|
+
children.push({ name: entry.name, type: 'file', path: childRelPath, extension: ext });
|
|
648
211
|
}
|
|
649
|
-
return createErrorResponse(lines.join('\n'), [
|
|
650
|
-
'Use get_debug_output to inspect the last captured logs',
|
|
651
|
-
'Check that UDP port 9900 is not occupied by another Godot process',
|
|
652
|
-
'Call stop_project to clean up, then run_project again',
|
|
653
|
-
]);
|
|
654
212
|
}
|
|
655
|
-
const lines = [
|
|
656
|
-
'Godot project started and MCP bridge is ready.',
|
|
657
|
-
'- Runtime tools (take_screenshot, simulate_input, get_ui_elements, run_script) are available now',
|
|
658
|
-
'- Use get_debug_output to check runtime output and errors',
|
|
659
|
-
'- Call stop_project when done',
|
|
660
|
-
];
|
|
661
|
-
if (background) {
|
|
662
|
-
lines.push('- Background mode: window hidden, physical input blocked');
|
|
663
|
-
}
|
|
664
|
-
return {
|
|
665
|
-
content: [{ type: 'text', text: lines.join('\n') }],
|
|
666
|
-
};
|
|
667
213
|
}
|
|
668
|
-
catch (
|
|
669
|
-
|
|
670
|
-
return createErrorResponse(`Failed to run Godot project: ${errorMessage}`, [
|
|
671
|
-
'Ensure Godot is installed correctly',
|
|
672
|
-
'Check if the GODOT_PATH environment variable is set correctly',
|
|
673
|
-
]);
|
|
214
|
+
catch (err) {
|
|
215
|
+
logDebug(`buildFilesystemTree error at ${currentPath}: ${err}`);
|
|
674
216
|
}
|
|
217
|
+
node.children = children;
|
|
218
|
+
return node;
|
|
675
219
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
'Provide a valid path without ".." or other potentially unsafe characters',
|
|
686
|
-
]);
|
|
687
|
-
}
|
|
688
|
-
try {
|
|
689
|
-
const projectFile = join(args.projectPath, 'project.godot');
|
|
690
|
-
if (!existsSync(projectFile)) {
|
|
691
|
-
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, [
|
|
692
|
-
'Ensure the path points to a directory containing a project.godot file',
|
|
693
|
-
'Use list_projects to find valid Godot projects',
|
|
694
|
-
]);
|
|
220
|
+
function searchInFiles(rootPath, pattern, fileTypes, caseSensitive, maxResults) {
|
|
221
|
+
const matches = [];
|
|
222
|
+
let truncated = false;
|
|
223
|
+
const searchDir = (currentPath, relBase) => {
|
|
224
|
+
if (truncated)
|
|
225
|
+
return;
|
|
226
|
+
let entries;
|
|
227
|
+
try {
|
|
228
|
+
entries = readdirSync(currentPath, { withFileTypes: true });
|
|
695
229
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
if (!bridgeResult.ready) {
|
|
700
|
-
return createErrorResponse(`Project attached but the MCP bridge is not ready.\n${bridgeResult.error || ''}`, [
|
|
701
|
-
'Verify Godot is running with this project',
|
|
702
|
-
'The McpBridge autoload must be initialized and listening on UDP port 9900',
|
|
703
|
-
'Check that no other Godot project is occupying port 9900',
|
|
704
|
-
'Use detach_project or stop_project when done',
|
|
705
|
-
]);
|
|
706
|
-
}
|
|
707
|
-
return {
|
|
708
|
-
content: [
|
|
709
|
-
{
|
|
710
|
-
type: 'text',
|
|
711
|
-
text: [
|
|
712
|
-
'Project attached and MCP bridge is ready.',
|
|
713
|
-
'- Runtime tools (take_screenshot, simulate_input, get_ui_elements, run_script) are available now',
|
|
714
|
-
'- get_debug_output is unavailable in attached mode because MCP did not spawn the process',
|
|
715
|
-
'- Use detach_project or stop_project when done to clean up the injected bridge state',
|
|
716
|
-
].join('\n'),
|
|
717
|
-
},
|
|
718
|
-
],
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
return {
|
|
722
|
-
content: [
|
|
723
|
-
{
|
|
724
|
-
type: 'text',
|
|
725
|
-
text: [
|
|
726
|
-
'Project attached for manual runtime use.',
|
|
727
|
-
'- Launch Godot yourself, then call attach_project again with waitForBridge: true to confirm readiness',
|
|
728
|
-
'- Or use runtime tools directly — they will fail with a clear error if the bridge is not yet listening',
|
|
729
|
-
'- get_debug_output is unavailable in attached mode because MCP did not spawn the process',
|
|
730
|
-
'- Use detach_project or stop_project when done to clean up the injected bridge state',
|
|
731
|
-
].join('\n'),
|
|
732
|
-
},
|
|
733
|
-
],
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
catch (error) {
|
|
737
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
738
|
-
return createErrorResponse(`Failed to attach project: ${errorMessage}`, [
|
|
739
|
-
'Check if project.godot is accessible',
|
|
740
|
-
'Ensure MCP can write the bridge autoload into the project',
|
|
741
|
-
]);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
export function handleDetachProject(runner) {
|
|
745
|
-
if (runner.activeSessionMode !== 'attached') {
|
|
746
|
-
return createErrorResponse('No attached project to detach.', [
|
|
747
|
-
'Use attach_project first for manual-launch workflows',
|
|
748
|
-
'If MCP launched the game, use stop_project instead',
|
|
749
|
-
]);
|
|
750
|
-
}
|
|
751
|
-
const result = runner.stopProject();
|
|
752
|
-
return {
|
|
753
|
-
content: [
|
|
754
|
-
{
|
|
755
|
-
type: 'text',
|
|
756
|
-
text: JSON.stringify({
|
|
757
|
-
message: 'Detached attached project and cleaned MCP bridge state',
|
|
758
|
-
externalProcessPreserved: result.externalProcessPreserved === true,
|
|
759
|
-
}),
|
|
760
|
-
},
|
|
761
|
-
],
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
export function handleGetDebugOutput(runner, args = {}) {
|
|
765
|
-
args = normalizeParameters(args);
|
|
766
|
-
if (!runner.activeSessionMode) {
|
|
767
|
-
return createErrorResponse('No active runtime session.', [
|
|
768
|
-
'Use run_project to start a Godot project first',
|
|
769
|
-
'Or use attach_project before launching Godot manually',
|
|
770
|
-
]);
|
|
771
|
-
}
|
|
772
|
-
if (runner.activeSessionMode === 'attached') {
|
|
773
|
-
return {
|
|
774
|
-
content: [
|
|
775
|
-
{
|
|
776
|
-
type: 'text',
|
|
777
|
-
text: JSON.stringify({
|
|
778
|
-
output: [],
|
|
779
|
-
errors: [],
|
|
780
|
-
running: null,
|
|
781
|
-
attached: true,
|
|
782
|
-
tip: 'Attached mode does not capture stdout/stderr because Godot was launched outside MCP.',
|
|
783
|
-
}),
|
|
784
|
-
},
|
|
785
|
-
],
|
|
786
|
-
};
|
|
787
|
-
}
|
|
788
|
-
const proc = runner.activeProcess;
|
|
789
|
-
if (!proc) {
|
|
790
|
-
return createErrorResponse('No active spawned process is available for debug output.', [
|
|
791
|
-
'Use run_project to start a Godot project first',
|
|
792
|
-
'Or use attach_project only when stdout/stderr capture is not needed',
|
|
793
|
-
]);
|
|
794
|
-
}
|
|
795
|
-
const limit = typeof args.limit === 'number' ? args.limit : 200;
|
|
796
|
-
const response = {
|
|
797
|
-
output: proc.output.slice(-limit),
|
|
798
|
-
errors: proc.errors.slice(-limit),
|
|
799
|
-
running: !proc.hasExited,
|
|
800
|
-
};
|
|
801
|
-
if (proc.hasExited) {
|
|
802
|
-
response.exitCode = proc.exitCode;
|
|
803
|
-
response.tip =
|
|
804
|
-
'Process has exited. Call stop_project to clean up the process slot before starting a new one.';
|
|
805
|
-
}
|
|
806
|
-
return {
|
|
807
|
-
content: [
|
|
808
|
-
{
|
|
809
|
-
type: 'text',
|
|
810
|
-
text: JSON.stringify(response),
|
|
811
|
-
},
|
|
812
|
-
],
|
|
813
|
-
};
|
|
814
|
-
}
|
|
815
|
-
export function handleStopProject(runner) {
|
|
816
|
-
const result = runner.stopProject();
|
|
817
|
-
if (!result) {
|
|
818
|
-
return createErrorResponse('No active Godot process to stop.', [
|
|
819
|
-
'Use run_project to start a Godot project first',
|
|
820
|
-
'The process may have already terminated',
|
|
821
|
-
]);
|
|
822
|
-
}
|
|
823
|
-
return {
|
|
824
|
-
content: [
|
|
825
|
-
{
|
|
826
|
-
type: 'text',
|
|
827
|
-
text: JSON.stringify({
|
|
828
|
-
message: result.mode === 'attached'
|
|
829
|
-
? 'Attached project detached and MCP bridge state cleaned up'
|
|
830
|
-
: 'Godot project stopped',
|
|
831
|
-
mode: result.mode,
|
|
832
|
-
externalProcessPreserved: result.externalProcessPreserved === true,
|
|
833
|
-
finalOutput: result.output.slice(-200),
|
|
834
|
-
finalErrors: result.errors.slice(-200),
|
|
835
|
-
}),
|
|
836
|
-
},
|
|
837
|
-
],
|
|
838
|
-
};
|
|
839
|
-
}
|
|
840
|
-
export async function handleListProjects(args) {
|
|
841
|
-
args = normalizeParameters(args);
|
|
842
|
-
if (!args.directory) {
|
|
843
|
-
return createErrorResponse('Directory is required', [
|
|
844
|
-
'Provide a valid directory path to search for Godot projects',
|
|
845
|
-
]);
|
|
846
|
-
}
|
|
847
|
-
if (!validatePath(args.directory)) {
|
|
848
|
-
return createErrorResponse('Invalid directory path', [
|
|
849
|
-
'Provide a valid path without ".." or other potentially unsafe characters',
|
|
850
|
-
]);
|
|
851
|
-
}
|
|
852
|
-
try {
|
|
853
|
-
if (!existsSync(args.directory)) {
|
|
854
|
-
return createErrorResponse(`Directory does not exist: ${args.directory}`, [
|
|
855
|
-
'Provide a valid directory path that exists on the system',
|
|
856
|
-
]);
|
|
857
|
-
}
|
|
858
|
-
const recursive = args.recursive === true;
|
|
859
|
-
const projects = findGodotProjects(args.directory, recursive);
|
|
860
|
-
return {
|
|
861
|
-
content: [{ type: 'text', text: JSON.stringify(projects) }],
|
|
862
|
-
};
|
|
863
|
-
}
|
|
864
|
-
catch (error) {
|
|
865
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
866
|
-
return createErrorResponse(`Failed to list projects: ${errorMessage}`, [
|
|
867
|
-
'Ensure the directory exists and is accessible',
|
|
868
|
-
'Check if you have permission to read the directory',
|
|
869
|
-
]);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
export async function handleGetProjectInfo(runner, args) {
|
|
873
|
-
args = normalizeParameters(args);
|
|
874
|
-
try {
|
|
875
|
-
const version = await runner.getVersion();
|
|
876
|
-
// If no project path, return just the Godot version
|
|
877
|
-
if (!args.projectPath) {
|
|
878
|
-
return {
|
|
879
|
-
content: [{ type: 'text', text: JSON.stringify({ godotVersion: version }) }],
|
|
880
|
-
};
|
|
881
|
-
}
|
|
882
|
-
if (!validatePath(args.projectPath)) {
|
|
883
|
-
return createErrorResponse('Invalid project path', [
|
|
884
|
-
'Provide a valid path without ".." or other potentially unsafe characters',
|
|
885
|
-
]);
|
|
886
|
-
}
|
|
887
|
-
const projectFile = join(args.projectPath, 'project.godot');
|
|
888
|
-
if (!existsSync(projectFile)) {
|
|
889
|
-
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, [
|
|
890
|
-
'Ensure the path points to a directory containing a project.godot file',
|
|
891
|
-
'Use list_projects to find valid Godot projects',
|
|
892
|
-
]);
|
|
893
|
-
}
|
|
894
|
-
const projectStructure = getProjectStructure(args.projectPath);
|
|
895
|
-
let projectName = basename(args.projectPath);
|
|
896
|
-
try {
|
|
897
|
-
const projectFileContent = readFileSync(projectFile, 'utf8');
|
|
898
|
-
const configNameMatch = projectFileContent.match(/config\/name="([^"]+)"/);
|
|
899
|
-
if (configNameMatch && configNameMatch[1]) {
|
|
900
|
-
projectName = configNameMatch[1];
|
|
901
|
-
logDebug(`Found project name in config: ${projectName}`);
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
catch (error) {
|
|
905
|
-
logDebug(`Error reading project file: ${error}`);
|
|
906
|
-
}
|
|
907
|
-
return {
|
|
908
|
-
content: [
|
|
909
|
-
{
|
|
910
|
-
type: 'text',
|
|
911
|
-
text: JSON.stringify({
|
|
912
|
-
name: projectName,
|
|
913
|
-
path: args.projectPath,
|
|
914
|
-
godotVersion: version,
|
|
915
|
-
structure: projectStructure,
|
|
916
|
-
}),
|
|
917
|
-
},
|
|
918
|
-
],
|
|
919
|
-
};
|
|
920
|
-
}
|
|
921
|
-
catch (error) {
|
|
922
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
923
|
-
return createErrorResponse(`Failed to get project info: ${errorMessage}`, [
|
|
924
|
-
'Ensure Godot is installed correctly',
|
|
925
|
-
'Check if the GODOT_PATH environment variable is set correctly',
|
|
926
|
-
]);
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
export async function handleTakeScreenshot(runner, args) {
|
|
930
|
-
args = normalizeParameters(args);
|
|
931
|
-
const sessionError = ensureRuntimeSession(runner, 'take a screenshot');
|
|
932
|
-
if (sessionError) {
|
|
933
|
-
return sessionError;
|
|
934
|
-
}
|
|
935
|
-
const timeout = typeof args.timeout === 'number' ? args.timeout : 10000;
|
|
936
|
-
try {
|
|
937
|
-
const { response: responseStr, runtimeErrors } = await runner.sendCommandWithErrors('screenshot', {}, timeout);
|
|
938
|
-
let parsed;
|
|
939
|
-
try {
|
|
940
|
-
parsed = JSON.parse(responseStr);
|
|
941
|
-
}
|
|
942
|
-
catch {
|
|
943
|
-
return createErrorResponse(`Invalid response from screenshot server: ${responseStr}`, [
|
|
944
|
-
'The game may not have fully initialized yet',
|
|
945
|
-
'Try again after a few seconds',
|
|
946
|
-
]);
|
|
947
|
-
}
|
|
948
|
-
if (parsed.error) {
|
|
949
|
-
return createErrorResponse(`Screenshot server error: ${parsed.error}`, [
|
|
950
|
-
'Ensure the game viewport is active',
|
|
951
|
-
'Try again after a moment',
|
|
952
|
-
]);
|
|
953
|
-
}
|
|
954
|
-
if (!parsed.path) {
|
|
955
|
-
return createErrorResponse('Screenshot server returned no file path', [
|
|
956
|
-
'Try again after a few seconds',
|
|
957
|
-
]);
|
|
958
|
-
}
|
|
959
|
-
// Normalize path for the local filesystem (forward slashes from GDScript)
|
|
960
|
-
const screenshotPath = sep === '\\' ? parsed.path.replace(/\//g, '\\') : parsed.path;
|
|
961
|
-
if (!existsSync(screenshotPath)) {
|
|
962
|
-
return createErrorResponse(`Screenshot file not found at: ${screenshotPath}`, [
|
|
963
|
-
'The screenshot may have failed to save',
|
|
964
|
-
'Check disk space and permissions',
|
|
965
|
-
]);
|
|
966
|
-
}
|
|
967
|
-
const imageBuffer = readFileSync(screenshotPath);
|
|
968
|
-
const base64Data = imageBuffer.toString('base64');
|
|
969
|
-
const content = [
|
|
970
|
-
{
|
|
971
|
-
type: 'image',
|
|
972
|
-
data: base64Data,
|
|
973
|
-
mimeType: 'image/png',
|
|
974
|
-
},
|
|
975
|
-
{
|
|
976
|
-
type: 'text',
|
|
977
|
-
text: `Screenshot saved to: ${parsed.path}`,
|
|
978
|
-
},
|
|
979
|
-
];
|
|
980
|
-
if (runtimeErrors.length > 0) {
|
|
981
|
-
content.push({
|
|
982
|
-
type: 'text',
|
|
983
|
-
text: JSON.stringify({
|
|
984
|
-
warnings: runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES),
|
|
985
|
-
}),
|
|
986
|
-
});
|
|
987
|
-
}
|
|
988
|
-
return { content };
|
|
989
|
-
}
|
|
990
|
-
catch (error) {
|
|
991
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
992
|
-
return createErrorResponse(`Failed to take screenshot: ${errorMessage}`, [
|
|
993
|
-
'Ensure the project is running (use run_project first)',
|
|
994
|
-
'The bridge may not be ready yet — use get_debug_output to investigate',
|
|
995
|
-
'Check that UDP port 9900 is not blocked',
|
|
996
|
-
]);
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
export async function handleSimulateInput(runner, args) {
|
|
1000
|
-
args = normalizeParameters(args);
|
|
1001
|
-
const sessionError = ensureRuntimeSession(runner, 'simulate input');
|
|
1002
|
-
if (sessionError) {
|
|
1003
|
-
return sessionError;
|
|
1004
|
-
}
|
|
1005
|
-
const actions = args.actions;
|
|
1006
|
-
if (!Array.isArray(actions) || actions.length === 0) {
|
|
1007
|
-
return createErrorResponse('actions must be a non-empty array of input actions', [
|
|
1008
|
-
'Provide at least one action object with a "type" field',
|
|
1009
|
-
]);
|
|
1010
|
-
}
|
|
1011
|
-
// Calculate timeout: sum of all wait durations + 10s buffer
|
|
1012
|
-
let totalWaitMs = 0;
|
|
1013
|
-
for (const action of actions) {
|
|
1014
|
-
if (typeof action === 'object' &&
|
|
1015
|
-
action !== null &&
|
|
1016
|
-
action.type === 'wait' &&
|
|
1017
|
-
typeof action.ms === 'number') {
|
|
1018
|
-
totalWaitMs += action.ms;
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
const timeoutMs = totalWaitMs + 10000;
|
|
1022
|
-
try {
|
|
1023
|
-
const { response: responseStr, runtimeErrors } = await runner.sendCommandWithErrors('input', { actions }, timeoutMs);
|
|
1024
|
-
let parsed;
|
|
1025
|
-
try {
|
|
1026
|
-
parsed = JSON.parse(responseStr);
|
|
1027
|
-
}
|
|
1028
|
-
catch {
|
|
1029
|
-
return createErrorResponse(`Invalid response from bridge: ${responseStr}`, [
|
|
1030
|
-
'The game may not have fully initialized yet',
|
|
1031
|
-
'Try again after a few seconds',
|
|
1032
|
-
]);
|
|
1033
|
-
}
|
|
1034
|
-
if (parsed.error) {
|
|
1035
|
-
return createErrorResponse(`Input simulation error: ${parsed.error}`, [
|
|
1036
|
-
'Check action types and parameters',
|
|
1037
|
-
'Ensure key names are valid Godot key names',
|
|
1038
|
-
]);
|
|
1039
|
-
}
|
|
1040
|
-
const payload = {
|
|
1041
|
-
success: true,
|
|
1042
|
-
actions_processed: parsed.actions_processed,
|
|
1043
|
-
tip: 'Call take_screenshot to verify the input had the intended visual effect.',
|
|
1044
|
-
};
|
|
1045
|
-
if (runtimeErrors.length > 0) {
|
|
1046
|
-
payload.warnings = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES);
|
|
1047
|
-
}
|
|
1048
|
-
return {
|
|
1049
|
-
content: [
|
|
1050
|
-
{
|
|
1051
|
-
type: 'text',
|
|
1052
|
-
text: JSON.stringify(payload),
|
|
1053
|
-
},
|
|
1054
|
-
],
|
|
1055
|
-
};
|
|
1056
|
-
}
|
|
1057
|
-
catch (error) {
|
|
1058
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1059
|
-
return createErrorResponse(`Failed to simulate input: ${errorMessage}`, [
|
|
1060
|
-
'Ensure the project is running (use run_project first)',
|
|
1061
|
-
'The bridge may not be ready yet — use get_debug_output to investigate',
|
|
1062
|
-
'Check that UDP port 9900 is not blocked',
|
|
1063
|
-
]);
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
export async function handleGetUiElements(runner, args) {
|
|
1067
|
-
args = normalizeParameters(args);
|
|
1068
|
-
const sessionError = ensureRuntimeSession(runner, 'query UI elements');
|
|
1069
|
-
if (sessionError) {
|
|
1070
|
-
return sessionError;
|
|
1071
|
-
}
|
|
1072
|
-
const visibleOnly = args.visibleOnly !== false;
|
|
1073
|
-
try {
|
|
1074
|
-
const cmdParams = { visible_only: visibleOnly };
|
|
1075
|
-
if (args.filter)
|
|
1076
|
-
cmdParams.type_filter = args.filter;
|
|
1077
|
-
const { response: responseStr, runtimeErrors } = await runner.sendCommandWithErrors('get_ui_elements', cmdParams);
|
|
1078
|
-
let parsed;
|
|
1079
|
-
try {
|
|
1080
|
-
parsed = JSON.parse(responseStr);
|
|
1081
|
-
}
|
|
1082
|
-
catch {
|
|
1083
|
-
return createErrorResponse(`Invalid response from bridge: ${responseStr}`, [
|
|
1084
|
-
'The game may not have fully initialized yet',
|
|
1085
|
-
'Try again after a few seconds',
|
|
1086
|
-
]);
|
|
1087
|
-
}
|
|
1088
|
-
if (parsed.error) {
|
|
1089
|
-
return createErrorResponse(`UI element query error: ${parsed.error}`, [
|
|
1090
|
-
'Ensure the game has a UI with Control nodes',
|
|
1091
|
-
]);
|
|
1092
|
-
}
|
|
1093
|
-
const payload = {
|
|
1094
|
-
...parsed,
|
|
1095
|
-
tip: "Use simulate_input with type 'click_element' and a node_path or text value from this list to interact with these elements.",
|
|
1096
|
-
};
|
|
1097
|
-
if (runtimeErrors.length > 0) {
|
|
1098
|
-
payload.warnings = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES);
|
|
1099
|
-
}
|
|
1100
|
-
return {
|
|
1101
|
-
content: [
|
|
1102
|
-
{
|
|
1103
|
-
type: 'text',
|
|
1104
|
-
text: JSON.stringify(payload),
|
|
1105
|
-
},
|
|
1106
|
-
],
|
|
1107
|
-
};
|
|
1108
|
-
}
|
|
1109
|
-
catch (error) {
|
|
1110
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1111
|
-
return createErrorResponse(`Failed to get UI elements: ${errorMessage}`, [
|
|
1112
|
-
'Ensure the project is running (use run_project first)',
|
|
1113
|
-
'The bridge may not be ready yet — use get_debug_output to investigate',
|
|
1114
|
-
'Check that UDP port 9900 is not blocked',
|
|
1115
|
-
]);
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
export async function handleRunScript(runner, args) {
|
|
1119
|
-
args = normalizeParameters(args);
|
|
1120
|
-
const sessionError = ensureRuntimeSession(runner, 'execute scripts');
|
|
1121
|
-
if (sessionError) {
|
|
1122
|
-
return sessionError;
|
|
1123
|
-
}
|
|
1124
|
-
const script = args.script;
|
|
1125
|
-
if (typeof script !== 'string' || script.trim() === '') {
|
|
1126
|
-
return createErrorResponse('script is required and must be a non-empty string', [
|
|
1127
|
-
'Provide GDScript source code with extends RefCounted and func execute(scene_tree: SceneTree) -> Variant',
|
|
1128
|
-
]);
|
|
1129
|
-
}
|
|
1130
|
-
if (!script.includes('func execute')) {
|
|
1131
|
-
return createErrorResponse('Script must define func execute(scene_tree: SceneTree) -> Variant', ['Add a func execute(scene_tree: SceneTree) -> Variant method to your script']);
|
|
1132
|
-
}
|
|
1133
|
-
// Write script to .mcp/scripts/ for audit trail
|
|
1134
|
-
try {
|
|
1135
|
-
const projectPath = runner.activeProjectPath;
|
|
1136
|
-
if (projectPath) {
|
|
1137
|
-
const scriptsDir = join(projectPath, '.mcp', 'scripts');
|
|
1138
|
-
mkdirSync(scriptsDir, { recursive: true });
|
|
1139
|
-
const timestamp = Date.now();
|
|
1140
|
-
const scriptFile = join(scriptsDir, `${timestamp}.gd`);
|
|
1141
|
-
writeFileSync(scriptFile, script, 'utf8');
|
|
1142
|
-
logDebug(`Saved script to ${scriptFile}`);
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
catch (error) {
|
|
1146
|
-
logDebug(`Failed to save script for audit: ${error}`);
|
|
1147
|
-
}
|
|
1148
|
-
const timeout = typeof args.timeout === 'number' ? args.timeout : 30000;
|
|
1149
|
-
try {
|
|
1150
|
-
const { response: responseStr, runtimeErrors } = await runner.sendCommandWithErrors('run_script', { source: script }, timeout);
|
|
1151
|
-
let parsed;
|
|
1152
|
-
try {
|
|
1153
|
-
parsed = JSON.parse(responseStr);
|
|
1154
|
-
}
|
|
1155
|
-
catch {
|
|
1156
|
-
return createErrorResponse(`Invalid response from bridge: ${responseStr}`, [
|
|
1157
|
-
'The script may have produced non-JSON output',
|
|
1158
|
-
'Check get_debug_output for print() statements',
|
|
1159
|
-
]);
|
|
1160
|
-
}
|
|
1161
|
-
if (parsed.error) {
|
|
1162
|
-
return createErrorResponse(`Script execution error: ${parsed.error}`, [
|
|
1163
|
-
'Check your GDScript syntax',
|
|
1164
|
-
'Ensure the script extends RefCounted',
|
|
1165
|
-
'Check get_debug_output for details',
|
|
1166
|
-
]);
|
|
1167
|
-
}
|
|
1168
|
-
// Detect false-positive success: GDScript has no try-catch, so runtime errors
|
|
1169
|
-
// return null and the real error only appears in stderr.
|
|
1170
|
-
if (parsed.success && parsed.result === null && runner.activeSessionMode === 'spawned') {
|
|
1171
|
-
if (runtimeErrors.length > 0) {
|
|
1172
|
-
const errorContext = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES).join('\n');
|
|
1173
|
-
return createErrorResponse(`Script runtime error detected:\n${errorContext}`, [
|
|
1174
|
-
'Fix the GDScript error in your script and retry',
|
|
1175
|
-
'Use get_debug_output for full process output',
|
|
1176
|
-
]);
|
|
1177
|
-
}
|
|
1178
|
-
return {
|
|
1179
|
-
content: [
|
|
1180
|
-
{
|
|
1181
|
-
type: 'text',
|
|
1182
|
-
text: JSON.stringify({
|
|
1183
|
-
success: true,
|
|
1184
|
-
result: null,
|
|
1185
|
-
warning: 'Script returned null. If unexpected, check get_debug_output for runtime errors — GDScript does not propagate exceptions.',
|
|
1186
|
-
tip: 'Call take_screenshot to verify any visual changes, or get_debug_output to review print() output from your script.',
|
|
1187
|
-
}),
|
|
1188
|
-
},
|
|
1189
|
-
],
|
|
1190
|
-
};
|
|
1191
|
-
}
|
|
1192
|
-
const payload = {
|
|
1193
|
-
success: true,
|
|
1194
|
-
result: parsed.result,
|
|
1195
|
-
tip: 'Call take_screenshot to verify any visual changes, or get_debug_output to review print() output from your script.',
|
|
1196
|
-
};
|
|
1197
|
-
if (runtimeErrors.length > 0) {
|
|
1198
|
-
payload.warnings = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES);
|
|
1199
|
-
}
|
|
1200
|
-
return {
|
|
1201
|
-
content: [
|
|
1202
|
-
{
|
|
1203
|
-
type: 'text',
|
|
1204
|
-
text: JSON.stringify(payload),
|
|
1205
|
-
},
|
|
1206
|
-
],
|
|
1207
|
-
};
|
|
1208
|
-
}
|
|
1209
|
-
catch (error) {
|
|
1210
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1211
|
-
return createErrorResponse(`Failed to execute script: ${errorMessage}`, [
|
|
1212
|
-
'Ensure the project is running (use run_project first)',
|
|
1213
|
-
'The bridge may not be ready yet — wait 2-3 seconds after starting, then check get_debug_output if the issue persists',
|
|
1214
|
-
'Check that UDP port 9900 is not blocked',
|
|
1215
|
-
'For long-running scripts, increase the timeout parameter',
|
|
1216
|
-
]);
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
function buildFilesystemTree(currentPath, relativePath, maxDepth, currentDepth, extensions) {
|
|
1220
|
-
const name = basename(currentPath);
|
|
1221
|
-
const node = { name, type: 'dir', path: relativePath || '.' };
|
|
1222
|
-
if (maxDepth !== -1 && currentDepth >= maxDepth) {
|
|
1223
|
-
node.children = [];
|
|
1224
|
-
return node;
|
|
1225
|
-
}
|
|
1226
|
-
const children = [];
|
|
1227
|
-
try {
|
|
1228
|
-
const entries = readdirSync(currentPath, { withFileTypes: true });
|
|
1229
|
-
for (const entry of entries) {
|
|
1230
|
-
if (entry.name.startsWith('.'))
|
|
1231
|
-
continue;
|
|
1232
|
-
const childRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
1233
|
-
if (entry.isDirectory()) {
|
|
1234
|
-
children.push(buildFilesystemTree(join(currentPath, entry.name), childRelPath, maxDepth, currentDepth + 1, extensions));
|
|
1235
|
-
}
|
|
1236
|
-
else if (entry.isFile()) {
|
|
1237
|
-
const ext = entry.name.includes('.') ? entry.name.split('.').pop().toLowerCase() : '';
|
|
1238
|
-
if (extensions && !extensions.includes(ext))
|
|
1239
|
-
continue;
|
|
1240
|
-
children.push({ name: entry.name, type: 'file', path: childRelPath, extension: ext });
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
catch (err) {
|
|
1245
|
-
logDebug(`buildFilesystemTree error at ${currentPath}: ${err}`);
|
|
1246
|
-
}
|
|
1247
|
-
node.children = children;
|
|
1248
|
-
return node;
|
|
1249
|
-
}
|
|
1250
|
-
function searchInFiles(rootPath, pattern, fileTypes, caseSensitive, maxResults) {
|
|
1251
|
-
const matches = [];
|
|
1252
|
-
let truncated = false;
|
|
1253
|
-
const searchDir = (currentPath, relBase) => {
|
|
1254
|
-
if (truncated)
|
|
1255
|
-
return;
|
|
1256
|
-
let entries;
|
|
1257
|
-
try {
|
|
1258
|
-
entries = readdirSync(currentPath, { withFileTypes: true });
|
|
1259
|
-
}
|
|
1260
|
-
catch (err) {
|
|
1261
|
-
logDebug(`searchInFiles readdir error at ${currentPath}: ${err}`);
|
|
1262
|
-
return;
|
|
230
|
+
catch (err) {
|
|
231
|
+
logDebug(`searchInFiles readdir error at ${currentPath}: ${err}`);
|
|
232
|
+
return;
|
|
1263
233
|
}
|
|
1264
234
|
for (const entry of entries) {
|
|
1265
235
|
if (truncated)
|
|
@@ -1272,7 +242,8 @@ function searchInFiles(rootPath, pattern, fileTypes, caseSensitive, maxResults)
|
|
|
1272
242
|
searchDir(fullPath, childRelPath);
|
|
1273
243
|
}
|
|
1274
244
|
else if (entry.isFile()) {
|
|
1275
|
-
const
|
|
245
|
+
const dotIdx = entry.name.lastIndexOf('.');
|
|
246
|
+
const ext = dotIdx >= 0 ? entry.name.slice(dotIdx + 1).toLowerCase() : '';
|
|
1276
247
|
if (!fileTypes.includes(ext))
|
|
1277
248
|
continue;
|
|
1278
249
|
let content;
|
|
@@ -1339,122 +310,83 @@ function parseProjectSettings(projectFilePath) {
|
|
|
1339
310
|
}
|
|
1340
311
|
return result;
|
|
1341
312
|
}
|
|
1342
|
-
|
|
313
|
+
// --- Handlers ---
|
|
314
|
+
export async function handleListProjects(args) {
|
|
1343
315
|
args = normalizeParameters(args);
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
try {
|
|
1348
|
-
const projectFile = join(v.projectPath, 'project.godot');
|
|
1349
|
-
const autoloads = parseAutoloads(projectFile);
|
|
1350
|
-
return { content: [{ type: 'text', text: JSON.stringify({ autoloads }) }] };
|
|
1351
|
-
}
|
|
1352
|
-
catch (error) {
|
|
1353
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1354
|
-
return createErrorResponse(`Failed to list autoloads: ${errorMessage}`, [
|
|
1355
|
-
'Check if project.godot is accessible',
|
|
316
|
+
if (!args.directory) {
|
|
317
|
+
return createErrorResponse('Directory is required', [
|
|
318
|
+
'Provide a valid directory path to search for Godot projects',
|
|
1356
319
|
]);
|
|
1357
320
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
const v = validateProjectArgs(args);
|
|
1362
|
-
if ('isError' in v)
|
|
1363
|
-
return v;
|
|
1364
|
-
if (!args.autoloadName || !args.autoloadPath) {
|
|
1365
|
-
return createErrorResponse('autoloadName and autoloadPath are required', [
|
|
1366
|
-
'Provide the autoload node name and script/scene path',
|
|
321
|
+
if (!validatePath(args.directory)) {
|
|
322
|
+
return createErrorResponse('Invalid directory path', [
|
|
323
|
+
'Provide a valid path without ".." or other potentially unsafe characters',
|
|
1367
324
|
]);
|
|
1368
325
|
}
|
|
1369
|
-
if (!validatePath(args.autoloadPath)) {
|
|
1370
|
-
return createErrorResponse('Invalid autoload path', ['Provide a valid path without ".."']);
|
|
1371
|
-
}
|
|
1372
326
|
try {
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
return createErrorResponse(`Autoload '${args.autoloadName}' already exists`, [
|
|
1377
|
-
'Use update_autoload to modify it',
|
|
1378
|
-
'Use list_autoloads to see current autoloads',
|
|
327
|
+
if (!existsSync(args.directory)) {
|
|
328
|
+
return createErrorResponse(`Directory does not exist: ${args.directory}`, [
|
|
329
|
+
'Provide a valid directory path that exists on the system',
|
|
1379
330
|
]);
|
|
1380
331
|
}
|
|
1381
|
-
const
|
|
1382
|
-
|
|
332
|
+
const recursive = args.recursive === true;
|
|
333
|
+
const projects = findGodotProjects(args.directory, recursive);
|
|
1383
334
|
return {
|
|
1384
|
-
content: [
|
|
1385
|
-
{
|
|
1386
|
-
type: 'text',
|
|
1387
|
-
text: `Autoload '${args.autoloadName}' registered at '${args.autoloadPath}' (singleton: ${isSingleton}).\nWarning: autoloads initialize in headless mode too. If this script has errors, all headless operations will fail. Verify by running get_scene_tree — if it fails, use remove_autoload to remove it.`,
|
|
1388
|
-
},
|
|
1389
|
-
],
|
|
335
|
+
content: [{ type: 'text', text: JSON.stringify(projects) }],
|
|
1390
336
|
};
|
|
1391
337
|
}
|
|
1392
338
|
catch (error) {
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
'Check if
|
|
339
|
+
return createErrorResponse(`Failed to list projects: ${getErrorMessage(error)}`, [
|
|
340
|
+
'Ensure the directory exists and is accessible',
|
|
341
|
+
'Check if you have permission to read the directory',
|
|
1396
342
|
]);
|
|
1397
343
|
}
|
|
1398
344
|
}
|
|
1399
|
-
export async function
|
|
345
|
+
export async function handleGetProjectInfo(runner, args) {
|
|
1400
346
|
args = normalizeParameters(args);
|
|
1401
|
-
const v = validateProjectArgs(args);
|
|
1402
|
-
if ('isError' in v)
|
|
1403
|
-
return v;
|
|
1404
|
-
if (!args.autoloadName) {
|
|
1405
|
-
return createErrorResponse('autoloadName is required', [
|
|
1406
|
-
'Provide the name of the autoload to remove',
|
|
1407
|
-
]);
|
|
1408
|
-
}
|
|
1409
347
|
try {
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
if (!
|
|
1413
|
-
return
|
|
1414
|
-
'
|
|
1415
|
-
|
|
348
|
+
const version = await runner.getVersion();
|
|
349
|
+
// If no project path, return just the Godot version
|
|
350
|
+
if (!args.projectPath) {
|
|
351
|
+
return {
|
|
352
|
+
content: [{ type: 'text', text: JSON.stringify({ godotVersion: version }) }],
|
|
353
|
+
};
|
|
1416
354
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
if (!args.autoloadName) {
|
|
1434
|
-
return createErrorResponse('autoloadName is required', [
|
|
1435
|
-
'Provide the name of the autoload to update',
|
|
1436
|
-
]);
|
|
1437
|
-
}
|
|
1438
|
-
if (args.autoloadPath && !validatePath(args.autoloadPath)) {
|
|
1439
|
-
return createErrorResponse('Invalid autoload path', ['Provide a valid path without ".."']);
|
|
1440
|
-
}
|
|
1441
|
-
try {
|
|
1442
|
-
const projectFile = join(v.projectPath, 'project.godot');
|
|
1443
|
-
const updated = updateAutoloadEntry(projectFile, args.autoloadName, args.autoloadPath, args.singleton);
|
|
1444
|
-
if (!updated) {
|
|
1445
|
-
return createErrorResponse(`Autoload '${args.autoloadName}' not found`, [
|
|
1446
|
-
'Use list_autoloads to see existing autoloads',
|
|
1447
|
-
'Use add_autoload to register a new one',
|
|
1448
|
-
]);
|
|
355
|
+
const v = validateProjectArgs(args);
|
|
356
|
+
if ('isError' in v)
|
|
357
|
+
return v;
|
|
358
|
+
const projectFile = projectGodotPath(v.projectPath);
|
|
359
|
+
const projectStructure = getProjectStructure(v.projectPath);
|
|
360
|
+
let projectName = basename(v.projectPath);
|
|
361
|
+
try {
|
|
362
|
+
const projectFileContent = readFileSync(projectFile, 'utf8');
|
|
363
|
+
const configNameMatch = projectFileContent.match(/config\/name="([^"]+)"/);
|
|
364
|
+
if (configNameMatch && configNameMatch[1]) {
|
|
365
|
+
projectName = configNameMatch[1];
|
|
366
|
+
logDebug(`Found project name in config: ${projectName}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
logDebug(`Error reading project file: ${error}`);
|
|
1449
371
|
}
|
|
1450
372
|
return {
|
|
1451
|
-
content: [
|
|
373
|
+
content: [
|
|
374
|
+
{
|
|
375
|
+
type: 'text',
|
|
376
|
+
text: JSON.stringify({
|
|
377
|
+
name: projectName,
|
|
378
|
+
path: v.projectPath,
|
|
379
|
+
godotVersion: version,
|
|
380
|
+
structure: projectStructure,
|
|
381
|
+
}),
|
|
382
|
+
},
|
|
383
|
+
],
|
|
1452
384
|
};
|
|
1453
385
|
}
|
|
1454
386
|
catch (error) {
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
'Check if
|
|
387
|
+
return createErrorResponse(`Failed to get project info: ${getErrorMessage(error)}`, [
|
|
388
|
+
'Ensure Godot is installed correctly',
|
|
389
|
+
'Check if the GODOT_PATH environment variable is set correctly',
|
|
1458
390
|
]);
|
|
1459
391
|
}
|
|
1460
392
|
}
|
|
@@ -1472,8 +404,7 @@ export async function handleGetProjectFiles(args) {
|
|
|
1472
404
|
return { content: [{ type: 'text', text: JSON.stringify(tree) }] };
|
|
1473
405
|
}
|
|
1474
406
|
catch (error) {
|
|
1475
|
-
|
|
1476
|
-
return createErrorResponse(`Failed to get project files: ${errorMessage}`, [
|
|
407
|
+
return createErrorResponse(`Failed to get project files: ${getErrorMessage(error)}`, [
|
|
1477
408
|
'Check if the project directory is accessible',
|
|
1478
409
|
]);
|
|
1479
410
|
}
|
|
@@ -1496,8 +427,7 @@ export async function handleSearchProject(args) {
|
|
|
1496
427
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
1497
428
|
}
|
|
1498
429
|
catch (error) {
|
|
1499
|
-
|
|
1500
|
-
return createErrorResponse(`Failed to search project: ${errorMessage}`, [
|
|
430
|
+
return createErrorResponse(`Failed to search project: ${getErrorMessage(error)}`, [
|
|
1501
431
|
'Check if the project directory is accessible',
|
|
1502
432
|
]);
|
|
1503
433
|
}
|
|
@@ -1548,8 +478,7 @@ export async function handleGetSceneDependencies(args) {
|
|
|
1548
478
|
};
|
|
1549
479
|
}
|
|
1550
480
|
catch (error) {
|
|
1551
|
-
|
|
1552
|
-
return createErrorResponse(`Failed to get scene dependencies: ${errorMessage}`, [
|
|
481
|
+
return createErrorResponse(`Failed to get scene dependencies: ${getErrorMessage(error)}`, [
|
|
1553
482
|
'Check if the scene file is accessible',
|
|
1554
483
|
]);
|
|
1555
484
|
}
|
|
@@ -1560,7 +489,7 @@ export async function handleGetProjectSettings(args) {
|
|
|
1560
489
|
if ('isError' in v)
|
|
1561
490
|
return v;
|
|
1562
491
|
try {
|
|
1563
|
-
const projectFile =
|
|
492
|
+
const projectFile = projectGodotPath(v.projectPath);
|
|
1564
493
|
const allSettings = parseProjectSettings(projectFile);
|
|
1565
494
|
if (args.section && typeof args.section === 'string') {
|
|
1566
495
|
const sectionData = allSettings[args.section] ?? {};
|
|
@@ -1569,8 +498,7 @@ export async function handleGetProjectSettings(args) {
|
|
|
1569
498
|
return { content: [{ type: 'text', text: JSON.stringify({ settings: allSettings }) }] };
|
|
1570
499
|
}
|
|
1571
500
|
catch (error) {
|
|
1572
|
-
|
|
1573
|
-
return createErrorResponse(`Failed to get project settings: ${errorMessage}`, [
|
|
501
|
+
return createErrorResponse(`Failed to get project settings: ${getErrorMessage(error)}`, [
|
|
1574
502
|
'Check if project.godot is accessible',
|
|
1575
503
|
]);
|
|
1576
504
|
}
|