godot-mcp-runtime 2.3.0 → 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 +105 -309
- 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 +106 -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 +55 -209
- 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 +33 -28
- 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 +57 -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,9 @@
|
|
|
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 for the human user. Use only when the user explicitly asks to "open the editor"; for any agent-driven work, use the headless scene/node tools (add_node, set_node_property, etc.) instead — the editor cannot be controlled programmatically. Returns immediately after spawning. Errors if projectPath has no project.godot.',
|
|
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: 'Spawn a Godot project as a child process with stdout/stderr captured. This is the preferred entry to runtime tools — use whenever MCP can launch the game itself. Required before take_screenshot, simulate_input, get_ui_elements, run_script, or get_debug_output. For a Godot process you launched yourself (debugger attached, custom flags, IDE run), use attach_project instead. Verifies MCP bridge readiness before returning success. Call stop_project when done. Errors if projectPath is not a Godot project or another run is already active.',
|
|
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 manually launched Godot process without spawning one. Use this only when the user is running Godot themselves (debugger attached, custom CLI flags, IDE run) — for the standard case, use run_project. Injects the McpBridge autoload and marks the project active. Call once before launching Godot, then again with waitForBridge:true after launch to confirm the bridge is listening (up to 15s). Use detach_project or stop_project when done. get_debug_output is unavailable in attached mode (stdout/stderr 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. Does NOT stop the manually launched Godot process — that stays running. Use after attach_project when you are done driving the game from MCP. For spawned sessions (run_project), use stop_project instead. No-op if no attached session exists.',
|
|
152
|
-
annotations: { destructiveHint: true, idempotentHint: true },
|
|
153
|
-
inputSchema: {
|
|
154
|
-
type: 'object',
|
|
155
|
-
properties: {},
|
|
156
|
-
required: [],
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
name: 'get_debug_output',
|
|
161
|
-
description: 'Get captured stdout/stderr from a spawned Godot project. Use whenever runtime tools fail unexpectedly — script errors, missing nodes, and crash backtraces all surface here. Requires run_project (not attach_project; attached mode does not capture output). Returns { output, errors, running, exitCode? } with the last `limit` lines (default 200, from the end). Reports attached-mode unavailability gracefully.',
|
|
162
|
-
annotations: { readOnlyHint: true },
|
|
163
|
-
inputSchema: {
|
|
164
|
-
type: 'object',
|
|
165
|
-
properties: {
|
|
166
|
-
limit: {
|
|
167
|
-
type: 'number',
|
|
168
|
-
description: 'Max lines to return (default: 200, from end of output)',
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
required: [],
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
{
|
|
175
|
-
name: 'stop_project',
|
|
176
|
-
description: 'Stop the spawned Godot project and clean up MCP bridge state. Always call when done with runtime testing — even after a crash — to free the single process slot so run_project can be called again. For attached sessions, this detaches without killing the externally launched process. No-op if no session is active.',
|
|
177
|
-
annotations: { destructiveHint: true, idempotentHint: true },
|
|
178
|
-
inputSchema: {
|
|
179
|
-
type: 'object',
|
|
180
|
-
properties: {},
|
|
181
|
-
required: [],
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
7
|
{
|
|
185
8
|
name: 'list_projects',
|
|
186
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.',
|
|
@@ -215,201 +38,9 @@ export const projectToolDefinitions = [
|
|
|
215
38
|
required: [],
|
|
216
39
|
},
|
|
217
40
|
},
|
|
218
|
-
{
|
|
219
|
-
name: 'take_screenshot',
|
|
220
|
-
description: 'Capture a PNG screenshot of the running Godot viewport. Use after simulate_input or run_script to verify visual changes. Requires an active runtime session (run_project or attach_project). Returns the image inline as base64. Also saved to .mcp/screenshots/ in the project directory for later reference. Errors if no session is active or the bridge does not respond within timeout (default 10000ms).',
|
|
221
|
-
annotations: { readOnlyHint: true },
|
|
222
|
-
inputSchema: {
|
|
223
|
-
type: 'object',
|
|
224
|
-
properties: {
|
|
225
|
-
timeout: {
|
|
226
|
-
type: 'number',
|
|
227
|
-
description: 'Timeout in milliseconds to wait for the screenshot (default: 10000)',
|
|
228
|
-
},
|
|
229
|
-
},
|
|
230
|
-
required: [],
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
name: 'simulate_input',
|
|
235
|
-
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}]\n\nReturns { success, actions_processed, warnings? }. Errors if no runtime session is active.',
|
|
236
|
-
inputSchema: {
|
|
237
|
-
type: 'object',
|
|
238
|
-
properties: {
|
|
239
|
-
actions: {
|
|
240
|
-
type: 'array',
|
|
241
|
-
description: 'Array of input actions to execute sequentially. Each object must have a "type" field.',
|
|
242
|
-
items: {
|
|
243
|
-
type: 'object',
|
|
244
|
-
properties: {
|
|
245
|
-
type: {
|
|
246
|
-
type: 'string',
|
|
247
|
-
enum: ['key', 'mouse_button', 'mouse_motion', 'click_element', 'action', 'wait'],
|
|
248
|
-
description: 'The type of input action',
|
|
249
|
-
},
|
|
250
|
-
key: {
|
|
251
|
-
type: 'string',
|
|
252
|
-
description: '[key] Key name (e.g. "W", "Space", "Escape", "Up")',
|
|
253
|
-
},
|
|
254
|
-
pressed: {
|
|
255
|
-
type: 'boolean',
|
|
256
|
-
description: '[key, mouse_button, action] Whether the input is pressed (true) or released (false)',
|
|
257
|
-
},
|
|
258
|
-
shift: { type: 'boolean', description: '[key] Shift modifier' },
|
|
259
|
-
ctrl: { type: 'boolean', description: '[key] Ctrl modifier' },
|
|
260
|
-
alt: { type: 'boolean', description: '[key] Alt modifier' },
|
|
261
|
-
button: {
|
|
262
|
-
type: 'string',
|
|
263
|
-
enum: ['left', 'right', 'middle'],
|
|
264
|
-
description: '[mouse_button, click_element] Mouse button (default: left)',
|
|
265
|
-
},
|
|
266
|
-
x: {
|
|
267
|
-
type: 'number',
|
|
268
|
-
description: '[mouse_button, mouse_motion] X position in viewport pixels',
|
|
269
|
-
},
|
|
270
|
-
y: {
|
|
271
|
-
type: 'number',
|
|
272
|
-
description: '[mouse_button, mouse_motion] Y position in viewport pixels',
|
|
273
|
-
},
|
|
274
|
-
relative_x: {
|
|
275
|
-
type: 'number',
|
|
276
|
-
description: '[mouse_motion] Relative X movement in pixels',
|
|
277
|
-
},
|
|
278
|
-
relative_y: {
|
|
279
|
-
type: 'number',
|
|
280
|
-
description: '[mouse_motion] Relative Y movement in pixels',
|
|
281
|
-
},
|
|
282
|
-
double_click: {
|
|
283
|
-
type: 'boolean',
|
|
284
|
-
description: '[mouse_button, click_element] Double click',
|
|
285
|
-
},
|
|
286
|
-
element: {
|
|
287
|
-
type: 'string',
|
|
288
|
-
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.',
|
|
289
|
-
},
|
|
290
|
-
action: {
|
|
291
|
-
type: 'string',
|
|
292
|
-
description: '[action] Godot input action name (as defined in Project Settings > Input Map)',
|
|
293
|
-
},
|
|
294
|
-
strength: {
|
|
295
|
-
type: 'number',
|
|
296
|
-
description: '[action] Action strength (0–1, default 1.0)',
|
|
297
|
-
},
|
|
298
|
-
ms: {
|
|
299
|
-
type: 'number',
|
|
300
|
-
description: '[wait] Duration in milliseconds to pause before the next action',
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
required: ['type'],
|
|
304
|
-
},
|
|
305
|
-
},
|
|
306
|
-
},
|
|
307
|
-
required: ['actions'],
|
|
308
|
-
},
|
|
309
|
-
},
|
|
310
|
-
{
|
|
311
|
-
name: 'get_ui_elements',
|
|
312
|
-
description: 'Walk the running scene tree and return all Control nodes with positions, sizes, types, and text content. Always call this before simulate_input click_element actions to discover valid element names and paths. Requires an active runtime session (run_project or attach_project). visibleOnly defaults true; pass false to include hidden Controls. filter narrows by class. Returns { elements: [{ name, path, type, rect, visible, text?, disabled?, tooltip? }] }.',
|
|
313
|
-
annotations: { readOnlyHint: true },
|
|
314
|
-
inputSchema: {
|
|
315
|
-
type: 'object',
|
|
316
|
-
properties: {
|
|
317
|
-
visibleOnly: {
|
|
318
|
-
type: 'boolean',
|
|
319
|
-
description: 'Only return nodes where Control.visible is true (default: true). Set false to include hidden elements.',
|
|
320
|
-
},
|
|
321
|
-
filter: {
|
|
322
|
-
type: 'string',
|
|
323
|
-
description: 'Filter by Control node type (e.g. "Button", "Label", "LineEdit")',
|
|
324
|
-
},
|
|
325
|
-
},
|
|
326
|
-
required: [],
|
|
327
|
-
},
|
|
328
|
-
},
|
|
329
|
-
{
|
|
330
|
-
name: 'run_script',
|
|
331
|
-
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.',
|
|
332
|
-
inputSchema: {
|
|
333
|
-
type: 'object',
|
|
334
|
-
properties: {
|
|
335
|
-
script: {
|
|
336
|
-
type: 'string',
|
|
337
|
-
description: 'GDScript source code. Must contain "extends RefCounted" and "func execute(scene_tree: SceneTree) -> Variant".',
|
|
338
|
-
},
|
|
339
|
-
timeout: {
|
|
340
|
-
type: 'number',
|
|
341
|
-
description: 'Timeout in ms (default: 30000). Increase for long-running scripts.',
|
|
342
|
-
},
|
|
343
|
-
},
|
|
344
|
-
required: ['script'],
|
|
345
|
-
},
|
|
346
|
-
},
|
|
347
|
-
{
|
|
348
|
-
name: 'list_autoloads',
|
|
349
|
-
description: 'List all registered autoloads in a project with paths and singleton status. Use first when diagnosing headless failures — broken autoloads crash all headless ops, so this tells you what is loaded. No Godot process required (reads project.godot directly). Returns { autoloads: [{ name, path, singleton }] }.',
|
|
350
|
-
annotations: { readOnlyHint: true },
|
|
351
|
-
inputSchema: {
|
|
352
|
-
type: 'object',
|
|
353
|
-
properties: {
|
|
354
|
-
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
355
|
-
},
|
|
356
|
-
required: ['projectPath'],
|
|
357
|
-
},
|
|
358
|
-
},
|
|
359
|
-
{
|
|
360
|
-
name: 'add_autoload',
|
|
361
|
-
description: 'Register a new autoload in a project. autoloadPath accepts "res://..." or a project-relative path (auto-prefixed). singleton defaults true (accessible globally by name). No Godot process required. Warning: autoloads initialize in headless mode — a broken script will crash every subsequent headless op; validate before adding. Errors if an autoload with the same name already exists; use update_autoload to modify.',
|
|
362
|
-
inputSchema: {
|
|
363
|
-
type: 'object',
|
|
364
|
-
properties: {
|
|
365
|
-
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
366
|
-
autoloadName: {
|
|
367
|
-
type: 'string',
|
|
368
|
-
description: 'Name of the autoload node (e.g. "MyManager")',
|
|
369
|
-
},
|
|
370
|
-
autoloadPath: {
|
|
371
|
-
type: 'string',
|
|
372
|
-
description: 'Path to the script or scene (e.g. "res://autoload/my_manager.gd" or "autoload/my_manager.gd")',
|
|
373
|
-
},
|
|
374
|
-
singleton: {
|
|
375
|
-
type: 'boolean',
|
|
376
|
-
description: 'Register as a globally accessible singleton by name (default: true)',
|
|
377
|
-
},
|
|
378
|
-
},
|
|
379
|
-
required: ['projectPath', 'autoloadName', 'autoloadPath'],
|
|
380
|
-
},
|
|
381
|
-
},
|
|
382
|
-
{
|
|
383
|
-
name: 'remove_autoload',
|
|
384
|
-
description: 'Unregister an autoload from a project by name. Use to recover from a broken autoload that is crashing headless ops. No Godot process required. Errors if no autoload with that name exists.',
|
|
385
|
-
annotations: { destructiveHint: true },
|
|
386
|
-
inputSchema: {
|
|
387
|
-
type: 'object',
|
|
388
|
-
properties: {
|
|
389
|
-
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
390
|
-
autoloadName: { type: 'string', description: 'Name of the autoload to remove' },
|
|
391
|
-
},
|
|
392
|
-
required: ['projectPath', 'autoloadName'],
|
|
393
|
-
},
|
|
394
|
-
},
|
|
395
|
-
{
|
|
396
|
-
name: 'update_autoload',
|
|
397
|
-
description: "Modify an existing autoload's path or singleton flag. Pass either or both — omitted fields keep their current value. Use instead of remove_autoload + add_autoload (single edit, no orphan window). No Godot process required. Errors if autoloadName is not registered.",
|
|
398
|
-
annotations: { idempotentHint: true },
|
|
399
|
-
inputSchema: {
|
|
400
|
-
type: 'object',
|
|
401
|
-
properties: {
|
|
402
|
-
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
403
|
-
autoloadName: { type: 'string', description: 'Name of the autoload to update' },
|
|
404
|
-
autoloadPath: { type: 'string', description: 'New path to the script or scene' },
|
|
405
|
-
singleton: { type: 'boolean', description: 'New singleton flag' },
|
|
406
|
-
},
|
|
407
|
-
required: ['projectPath', 'autoloadName'],
|
|
408
|
-
},
|
|
409
|
-
},
|
|
410
41
|
{
|
|
411
42
|
name: 'get_project_files',
|
|
412
|
-
description: 'Return a recursive file tree of a Godot project
|
|
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.',
|
|
413
44
|
annotations: { readOnlyHint: true },
|
|
414
45
|
inputSchema: {
|
|
415
46
|
type: 'object',
|
|
@@ -481,26 +112,12 @@ export const projectToolDefinitions = [
|
|
|
481
112
|
},
|
|
482
113
|
},
|
|
483
114
|
];
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
return createErrorResponse(`No active runtime session. A project must be running or attached to ${actionDescription}.`, [
|
|
487
|
-
'Use run_project to start a Godot project first',
|
|
488
|
-
'Or use attach_project before launching Godot manually',
|
|
489
|
-
]);
|
|
490
|
-
}
|
|
491
|
-
if (runner.activeSessionMode === 'spawned' &&
|
|
492
|
-
(!runner.activeProcess || runner.activeProcess.hasExited)) {
|
|
493
|
-
return createErrorResponse(`The spawned Godot process has exited and cannot ${actionDescription}.`, [
|
|
494
|
-
'Use get_debug_output to inspect the last captured logs',
|
|
495
|
-
'Call stop_project to clean up, then run_project again',
|
|
496
|
-
]);
|
|
497
|
-
}
|
|
498
|
-
return null;
|
|
499
|
-
}
|
|
115
|
+
// --- Helpers ---
|
|
116
|
+
const PROJECT_SCAN_BLACKLIST = new Set(['.git', '.godot', '.mcp', 'node_modules', '.svn', '.hg']);
|
|
500
117
|
function findGodotProjects(directory, recursive) {
|
|
501
118
|
const projects = [];
|
|
502
119
|
try {
|
|
503
|
-
const projectFile =
|
|
120
|
+
const projectFile = projectGodotPath(directory);
|
|
504
121
|
if (existsSync(projectFile)) {
|
|
505
122
|
projects.push({
|
|
506
123
|
path: directory,
|
|
@@ -509,10 +126,10 @@ function findGodotProjects(directory, recursive) {
|
|
|
509
126
|
}
|
|
510
127
|
const entries = readdirSync(directory, { withFileTypes: true });
|
|
511
128
|
for (const entry of entries) {
|
|
512
|
-
if (!entry.isDirectory() || entry.name
|
|
129
|
+
if (!entry.isDirectory() || PROJECT_SCAN_BLACKLIST.has(entry.name))
|
|
513
130
|
continue;
|
|
514
131
|
const subdir = join(directory, entry.name);
|
|
515
|
-
if (existsSync(
|
|
132
|
+
if (existsSync(projectGodotPath(subdir))) {
|
|
516
133
|
projects.push({ path: subdir, name: entry.name });
|
|
517
134
|
}
|
|
518
135
|
else if (recursive) {
|
|
@@ -544,7 +161,8 @@ function getProjectStructure(projectPath) {
|
|
|
544
161
|
scanDirectory(entryPath);
|
|
545
162
|
}
|
|
546
163
|
else if (entry.isFile()) {
|
|
547
|
-
const
|
|
164
|
+
const dotIdx = entry.name.lastIndexOf('.');
|
|
165
|
+
const ext = dotIdx >= 0 ? entry.name.slice(dotIdx + 1).toLowerCase() : '';
|
|
548
166
|
if (ext === 'tscn') {
|
|
549
167
|
structure.scenes++;
|
|
550
168
|
}
|
|
@@ -567,709 +185,47 @@ function getProjectStructure(projectPath) {
|
|
|
567
185
|
scanDirectory(projectPath);
|
|
568
186
|
return structure;
|
|
569
187
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
}
|
|
577
|
-
if (!validatePath(args.projectPath)) {
|
|
578
|
-
return createErrorResponse('Invalid project path', [
|
|
579
|
-
'Provide a valid path without ".." or other potentially unsafe characters',
|
|
580
|
-
]);
|
|
581
|
-
}
|
|
582
|
-
try {
|
|
583
|
-
if (!runner.getGodotPath()) {
|
|
584
|
-
await runner.detectGodotPath();
|
|
585
|
-
if (!runner.getGodotPath()) {
|
|
586
|
-
return createErrorResponse('Could not find a valid Godot executable path', [
|
|
587
|
-
'Ensure Godot is installed correctly',
|
|
588
|
-
'Set GODOT_PATH environment variable',
|
|
589
|
-
]);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
const projectFile = join(args.projectPath, 'project.godot');
|
|
593
|
-
if (!existsSync(projectFile)) {
|
|
594
|
-
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, [
|
|
595
|
-
'Ensure the path points to a directory containing a project.godot file',
|
|
596
|
-
'Use list_projects to find valid Godot projects',
|
|
597
|
-
]);
|
|
598
|
-
}
|
|
599
|
-
logDebug(`Launching Godot editor for project: ${args.projectPath}`);
|
|
600
|
-
const process = runner.launchEditor(args.projectPath);
|
|
601
|
-
process.on('error', (err) => {
|
|
602
|
-
console.error('Failed to start Godot editor:', err);
|
|
603
|
-
});
|
|
604
|
-
return {
|
|
605
|
-
content: [
|
|
606
|
-
{
|
|
607
|
-
type: 'text',
|
|
608
|
-
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.`,
|
|
609
|
-
},
|
|
610
|
-
],
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
catch (error) {
|
|
614
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
615
|
-
return createErrorResponse(`Failed to launch Godot editor: ${errorMessage}`, [
|
|
616
|
-
'Ensure Godot is installed correctly',
|
|
617
|
-
'Check if the GODOT_PATH environment variable is set correctly',
|
|
618
|
-
]);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
export async function handleRunProject(runner, args) {
|
|
622
|
-
args = normalizeParameters(args);
|
|
623
|
-
if (!args.projectPath) {
|
|
624
|
-
return createErrorResponse('Project path is required', [
|
|
625
|
-
'Provide a valid path to a Godot project directory',
|
|
626
|
-
]);
|
|
627
|
-
}
|
|
628
|
-
if (!validatePath(args.projectPath)) {
|
|
629
|
-
return createErrorResponse('Invalid project path', [
|
|
630
|
-
'Provide a valid path without ".." or other potentially unsafe characters',
|
|
631
|
-
]);
|
|
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;
|
|
632
194
|
}
|
|
195
|
+
const children = [];
|
|
633
196
|
try {
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const background = args.background === true;
|
|
642
|
-
runner.runProject(args.projectPath, args.scene, background);
|
|
643
|
-
const bridgeResult = await runner.waitForBridge();
|
|
644
|
-
if (!bridgeResult.ready) {
|
|
645
|
-
if (runner.activeProcess && runner.activeProcess.hasExited) {
|
|
646
|
-
return createErrorResponse(`Godot process exited before the MCP bridge could initialize.\n${bridgeResult.error || ''}`, [
|
|
647
|
-
'Check get_debug_output for runtime errors',
|
|
648
|
-
'Verify a display server is available (Wayland/X11)',
|
|
649
|
-
'Check for broken autoloads with list_autoloads',
|
|
650
|
-
'Call stop_project to clean up, then try again',
|
|
651
|
-
]);
|
|
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));
|
|
652
204
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
];
|
|
660
|
-
if (background) {
|
|
661
|
-
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 });
|
|
662
211
|
}
|
|
663
|
-
return createErrorResponse(lines.join('\n'), [
|
|
664
|
-
'Use get_debug_output to inspect the last captured logs',
|
|
665
|
-
'Check that UDP port 9900 is not occupied by another Godot process',
|
|
666
|
-
'Call stop_project to clean up, then run_project again',
|
|
667
|
-
]);
|
|
668
212
|
}
|
|
669
|
-
const lines = [
|
|
670
|
-
'Godot project started and MCP bridge is ready.',
|
|
671
|
-
'- Runtime tools (take_screenshot, simulate_input, get_ui_elements, run_script) are available now',
|
|
672
|
-
'- Use get_debug_output to check runtime output and errors',
|
|
673
|
-
'- Call stop_project when done',
|
|
674
|
-
];
|
|
675
|
-
if (background) {
|
|
676
|
-
lines.push('- Background mode: window hidden, physical input blocked');
|
|
677
|
-
}
|
|
678
|
-
return {
|
|
679
|
-
content: [{ type: 'text', text: lines.join('\n') }],
|
|
680
|
-
};
|
|
681
213
|
}
|
|
682
|
-
catch (
|
|
683
|
-
|
|
684
|
-
return createErrorResponse(`Failed to run Godot project: ${errorMessage}`, [
|
|
685
|
-
'Ensure Godot is installed correctly',
|
|
686
|
-
'Check if the GODOT_PATH environment variable is set correctly',
|
|
687
|
-
]);
|
|
214
|
+
catch (err) {
|
|
215
|
+
logDebug(`buildFilesystemTree error at ${currentPath}: ${err}`);
|
|
688
216
|
}
|
|
217
|
+
node.children = children;
|
|
218
|
+
return node;
|
|
689
219
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
'Provide a valid path without ".." or other potentially unsafe characters',
|
|
700
|
-
]);
|
|
701
|
-
}
|
|
702
|
-
try {
|
|
703
|
-
const projectFile = join(args.projectPath, 'project.godot');
|
|
704
|
-
if (!existsSync(projectFile)) {
|
|
705
|
-
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, [
|
|
706
|
-
'Ensure the path points to a directory containing a project.godot file',
|
|
707
|
-
'Use list_projects to find valid Godot projects',
|
|
708
|
-
]);
|
|
709
|
-
}
|
|
710
|
-
runner.attachProject(args.projectPath);
|
|
711
|
-
if (args.waitForBridge === true) {
|
|
712
|
-
const bridgeResult = await runner.waitForBridgeAttached();
|
|
713
|
-
if (!bridgeResult.ready) {
|
|
714
|
-
return createErrorResponse(`Project attached but the MCP bridge is not ready.\n${bridgeResult.error || ''}`, [
|
|
715
|
-
'Verify Godot is running with this project',
|
|
716
|
-
'The McpBridge autoload must be initialized and listening on UDP port 9900',
|
|
717
|
-
'Check that no other Godot project is occupying port 9900',
|
|
718
|
-
'Use detach_project or stop_project when done',
|
|
719
|
-
]);
|
|
720
|
-
}
|
|
721
|
-
return {
|
|
722
|
-
content: [
|
|
723
|
-
{
|
|
724
|
-
type: 'text',
|
|
725
|
-
text: [
|
|
726
|
-
'Project attached and MCP bridge is ready.',
|
|
727
|
-
'- Runtime tools (take_screenshot, simulate_input, get_ui_elements, run_script) are available now',
|
|
728
|
-
'- get_debug_output is unavailable in attached mode because MCP did not spawn the process',
|
|
729
|
-
'- Use detach_project or stop_project when done to clean up the injected bridge state',
|
|
730
|
-
].join('\n'),
|
|
731
|
-
},
|
|
732
|
-
],
|
|
733
|
-
};
|
|
734
|
-
}
|
|
735
|
-
return {
|
|
736
|
-
content: [
|
|
737
|
-
{
|
|
738
|
-
type: 'text',
|
|
739
|
-
text: [
|
|
740
|
-
'Project attached for manual runtime use.',
|
|
741
|
-
'- Launch Godot yourself, then call attach_project again with waitForBridge: true to confirm readiness',
|
|
742
|
-
'- Or use runtime tools directly — they will fail with a clear error if the bridge is not yet listening',
|
|
743
|
-
'- get_debug_output is unavailable in attached mode because MCP did not spawn the process',
|
|
744
|
-
'- Use detach_project or stop_project when done to clean up the injected bridge state',
|
|
745
|
-
].join('\n'),
|
|
746
|
-
},
|
|
747
|
-
],
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
|
-
catch (error) {
|
|
751
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
752
|
-
return createErrorResponse(`Failed to attach project: ${errorMessage}`, [
|
|
753
|
-
'Check if project.godot is accessible',
|
|
754
|
-
'Ensure MCP can write the bridge autoload into the project',
|
|
755
|
-
]);
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
export function handleDetachProject(runner) {
|
|
759
|
-
if (runner.activeSessionMode !== 'attached') {
|
|
760
|
-
return createErrorResponse('No attached project to detach.', [
|
|
761
|
-
'Use attach_project first for manual-launch workflows',
|
|
762
|
-
'If MCP launched the game, use stop_project instead',
|
|
763
|
-
]);
|
|
764
|
-
}
|
|
765
|
-
const result = runner.stopProject();
|
|
766
|
-
return {
|
|
767
|
-
content: [
|
|
768
|
-
{
|
|
769
|
-
type: 'text',
|
|
770
|
-
text: JSON.stringify({
|
|
771
|
-
message: 'Detached attached project and cleaned MCP bridge state',
|
|
772
|
-
externalProcessPreserved: result.externalProcessPreserved === true,
|
|
773
|
-
}),
|
|
774
|
-
},
|
|
775
|
-
],
|
|
776
|
-
};
|
|
777
|
-
}
|
|
778
|
-
export function handleGetDebugOutput(runner, args = {}) {
|
|
779
|
-
args = normalizeParameters(args);
|
|
780
|
-
if (!runner.activeSessionMode) {
|
|
781
|
-
return createErrorResponse('No active runtime session.', [
|
|
782
|
-
'Use run_project to start a Godot project first',
|
|
783
|
-
'Or use attach_project before launching Godot manually',
|
|
784
|
-
]);
|
|
785
|
-
}
|
|
786
|
-
if (runner.activeSessionMode === 'attached') {
|
|
787
|
-
return {
|
|
788
|
-
content: [
|
|
789
|
-
{
|
|
790
|
-
type: 'text',
|
|
791
|
-
text: JSON.stringify({
|
|
792
|
-
output: [],
|
|
793
|
-
errors: [],
|
|
794
|
-
running: null,
|
|
795
|
-
attached: true,
|
|
796
|
-
tip: 'Attached mode does not capture stdout/stderr because Godot was launched outside MCP.',
|
|
797
|
-
}),
|
|
798
|
-
},
|
|
799
|
-
],
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
const proc = runner.activeProcess;
|
|
803
|
-
if (!proc) {
|
|
804
|
-
return createErrorResponse('No active spawned process is available for debug output.', [
|
|
805
|
-
'Use run_project to start a Godot project first',
|
|
806
|
-
'Or use attach_project only when stdout/stderr capture is not needed',
|
|
807
|
-
]);
|
|
808
|
-
}
|
|
809
|
-
const limit = typeof args.limit === 'number' ? args.limit : 200;
|
|
810
|
-
const response = {
|
|
811
|
-
output: proc.output.slice(-limit),
|
|
812
|
-
errors: proc.errors.slice(-limit),
|
|
813
|
-
running: !proc.hasExited,
|
|
814
|
-
};
|
|
815
|
-
if (proc.hasExited) {
|
|
816
|
-
response.exitCode = proc.exitCode;
|
|
817
|
-
response.tip =
|
|
818
|
-
'Process has exited. Call stop_project to clean up the process slot before starting a new one.';
|
|
819
|
-
}
|
|
820
|
-
return {
|
|
821
|
-
content: [
|
|
822
|
-
{
|
|
823
|
-
type: 'text',
|
|
824
|
-
text: JSON.stringify(response),
|
|
825
|
-
},
|
|
826
|
-
],
|
|
827
|
-
};
|
|
828
|
-
}
|
|
829
|
-
export function handleStopProject(runner) {
|
|
830
|
-
const result = runner.stopProject();
|
|
831
|
-
if (!result) {
|
|
832
|
-
return createErrorResponse('No active Godot process to stop.', [
|
|
833
|
-
'Use run_project to start a Godot project first',
|
|
834
|
-
'The process may have already terminated',
|
|
835
|
-
]);
|
|
836
|
-
}
|
|
837
|
-
return {
|
|
838
|
-
content: [
|
|
839
|
-
{
|
|
840
|
-
type: 'text',
|
|
841
|
-
text: JSON.stringify({
|
|
842
|
-
message: result.mode === 'attached'
|
|
843
|
-
? 'Attached project detached and MCP bridge state cleaned up'
|
|
844
|
-
: 'Godot project stopped',
|
|
845
|
-
mode: result.mode,
|
|
846
|
-
externalProcessPreserved: result.externalProcessPreserved === true,
|
|
847
|
-
finalOutput: result.output.slice(-200),
|
|
848
|
-
finalErrors: result.errors.slice(-200),
|
|
849
|
-
}),
|
|
850
|
-
},
|
|
851
|
-
],
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
export async function handleListProjects(args) {
|
|
855
|
-
args = normalizeParameters(args);
|
|
856
|
-
if (!args.directory) {
|
|
857
|
-
return createErrorResponse('Directory is required', [
|
|
858
|
-
'Provide a valid directory path to search for Godot projects',
|
|
859
|
-
]);
|
|
860
|
-
}
|
|
861
|
-
if (!validatePath(args.directory)) {
|
|
862
|
-
return createErrorResponse('Invalid directory path', [
|
|
863
|
-
'Provide a valid path without ".." or other potentially unsafe characters',
|
|
864
|
-
]);
|
|
865
|
-
}
|
|
866
|
-
try {
|
|
867
|
-
if (!existsSync(args.directory)) {
|
|
868
|
-
return createErrorResponse(`Directory does not exist: ${args.directory}`, [
|
|
869
|
-
'Provide a valid directory path that exists on the system',
|
|
870
|
-
]);
|
|
871
|
-
}
|
|
872
|
-
const recursive = args.recursive === true;
|
|
873
|
-
const projects = findGodotProjects(args.directory, recursive);
|
|
874
|
-
return {
|
|
875
|
-
content: [{ type: 'text', text: JSON.stringify(projects) }],
|
|
876
|
-
};
|
|
877
|
-
}
|
|
878
|
-
catch (error) {
|
|
879
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
880
|
-
return createErrorResponse(`Failed to list projects: ${errorMessage}`, [
|
|
881
|
-
'Ensure the directory exists and is accessible',
|
|
882
|
-
'Check if you have permission to read the directory',
|
|
883
|
-
]);
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
export async function handleGetProjectInfo(runner, args) {
|
|
887
|
-
args = normalizeParameters(args);
|
|
888
|
-
try {
|
|
889
|
-
const version = await runner.getVersion();
|
|
890
|
-
// If no project path, return just the Godot version
|
|
891
|
-
if (!args.projectPath) {
|
|
892
|
-
return {
|
|
893
|
-
content: [{ type: 'text', text: JSON.stringify({ godotVersion: version }) }],
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
if (!validatePath(args.projectPath)) {
|
|
897
|
-
return createErrorResponse('Invalid project path', [
|
|
898
|
-
'Provide a valid path without ".." or other potentially unsafe characters',
|
|
899
|
-
]);
|
|
900
|
-
}
|
|
901
|
-
const projectFile = join(args.projectPath, 'project.godot');
|
|
902
|
-
if (!existsSync(projectFile)) {
|
|
903
|
-
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, [
|
|
904
|
-
'Ensure the path points to a directory containing a project.godot file',
|
|
905
|
-
'Use list_projects to find valid Godot projects',
|
|
906
|
-
]);
|
|
907
|
-
}
|
|
908
|
-
const projectStructure = getProjectStructure(args.projectPath);
|
|
909
|
-
let projectName = basename(args.projectPath);
|
|
910
|
-
try {
|
|
911
|
-
const projectFileContent = readFileSync(projectFile, 'utf8');
|
|
912
|
-
const configNameMatch = projectFileContent.match(/config\/name="([^"]+)"/);
|
|
913
|
-
if (configNameMatch && configNameMatch[1]) {
|
|
914
|
-
projectName = configNameMatch[1];
|
|
915
|
-
logDebug(`Found project name in config: ${projectName}`);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
catch (error) {
|
|
919
|
-
logDebug(`Error reading project file: ${error}`);
|
|
920
|
-
}
|
|
921
|
-
return {
|
|
922
|
-
content: [
|
|
923
|
-
{
|
|
924
|
-
type: 'text',
|
|
925
|
-
text: JSON.stringify({
|
|
926
|
-
name: projectName,
|
|
927
|
-
path: args.projectPath,
|
|
928
|
-
godotVersion: version,
|
|
929
|
-
structure: projectStructure,
|
|
930
|
-
}),
|
|
931
|
-
},
|
|
932
|
-
],
|
|
933
|
-
};
|
|
934
|
-
}
|
|
935
|
-
catch (error) {
|
|
936
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
937
|
-
return createErrorResponse(`Failed to get project info: ${errorMessage}`, [
|
|
938
|
-
'Ensure Godot is installed correctly',
|
|
939
|
-
'Check if the GODOT_PATH environment variable is set correctly',
|
|
940
|
-
]);
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
export async function handleTakeScreenshot(runner, args) {
|
|
944
|
-
args = normalizeParameters(args);
|
|
945
|
-
const sessionError = ensureRuntimeSession(runner, 'take a screenshot');
|
|
946
|
-
if (sessionError) {
|
|
947
|
-
return sessionError;
|
|
948
|
-
}
|
|
949
|
-
const timeout = typeof args.timeout === 'number' ? args.timeout : 10000;
|
|
950
|
-
try {
|
|
951
|
-
const { response: responseStr, runtimeErrors } = await runner.sendCommandWithErrors('screenshot', {}, timeout);
|
|
952
|
-
let parsed;
|
|
953
|
-
try {
|
|
954
|
-
parsed = JSON.parse(responseStr);
|
|
955
|
-
}
|
|
956
|
-
catch {
|
|
957
|
-
return createErrorResponse(`Invalid response from screenshot server: ${responseStr}`, [
|
|
958
|
-
'The game may not have fully initialized yet',
|
|
959
|
-
'Try again after a few seconds',
|
|
960
|
-
]);
|
|
961
|
-
}
|
|
962
|
-
if (parsed.error) {
|
|
963
|
-
return createErrorResponse(`Screenshot server error: ${parsed.error}`, [
|
|
964
|
-
'Ensure the game viewport is active',
|
|
965
|
-
'Try again after a moment',
|
|
966
|
-
]);
|
|
967
|
-
}
|
|
968
|
-
if (!parsed.path) {
|
|
969
|
-
return createErrorResponse('Screenshot server returned no file path', [
|
|
970
|
-
'Try again after a few seconds',
|
|
971
|
-
]);
|
|
972
|
-
}
|
|
973
|
-
// Normalize path for the local filesystem (forward slashes from GDScript)
|
|
974
|
-
const screenshotPath = sep === '\\' ? parsed.path.replace(/\//g, '\\') : parsed.path;
|
|
975
|
-
if (!existsSync(screenshotPath)) {
|
|
976
|
-
return createErrorResponse(`Screenshot file not found at: ${screenshotPath}`, [
|
|
977
|
-
'The screenshot may have failed to save',
|
|
978
|
-
'Check disk space and permissions',
|
|
979
|
-
]);
|
|
980
|
-
}
|
|
981
|
-
const imageBuffer = readFileSync(screenshotPath);
|
|
982
|
-
const base64Data = imageBuffer.toString('base64');
|
|
983
|
-
const content = [
|
|
984
|
-
{
|
|
985
|
-
type: 'image',
|
|
986
|
-
data: base64Data,
|
|
987
|
-
mimeType: 'image/png',
|
|
988
|
-
},
|
|
989
|
-
{
|
|
990
|
-
type: 'text',
|
|
991
|
-
text: `Screenshot saved to: ${parsed.path}`,
|
|
992
|
-
},
|
|
993
|
-
];
|
|
994
|
-
if (runtimeErrors.length > 0) {
|
|
995
|
-
content.push({
|
|
996
|
-
type: 'text',
|
|
997
|
-
text: JSON.stringify({
|
|
998
|
-
warnings: runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES),
|
|
999
|
-
}),
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
return { content };
|
|
1003
|
-
}
|
|
1004
|
-
catch (error) {
|
|
1005
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1006
|
-
return createErrorResponse(`Failed to take screenshot: ${errorMessage}`, [
|
|
1007
|
-
'Ensure the project is running (use run_project first)',
|
|
1008
|
-
'The bridge may not be ready yet — use get_debug_output to investigate',
|
|
1009
|
-
'Check that UDP port 9900 is not blocked',
|
|
1010
|
-
]);
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
export async function handleSimulateInput(runner, args) {
|
|
1014
|
-
args = normalizeParameters(args);
|
|
1015
|
-
const sessionError = ensureRuntimeSession(runner, 'simulate input');
|
|
1016
|
-
if (sessionError) {
|
|
1017
|
-
return sessionError;
|
|
1018
|
-
}
|
|
1019
|
-
const actions = args.actions;
|
|
1020
|
-
if (!Array.isArray(actions) || actions.length === 0) {
|
|
1021
|
-
return createErrorResponse('actions must be a non-empty array of input actions', [
|
|
1022
|
-
'Provide at least one action object with a "type" field',
|
|
1023
|
-
]);
|
|
1024
|
-
}
|
|
1025
|
-
// Calculate timeout: sum of all wait durations + 10s buffer
|
|
1026
|
-
let totalWaitMs = 0;
|
|
1027
|
-
for (const action of actions) {
|
|
1028
|
-
if (typeof action === 'object' &&
|
|
1029
|
-
action !== null &&
|
|
1030
|
-
action.type === 'wait' &&
|
|
1031
|
-
typeof action.ms === 'number') {
|
|
1032
|
-
totalWaitMs += action.ms;
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
const timeoutMs = totalWaitMs + 10000;
|
|
1036
|
-
try {
|
|
1037
|
-
const { response: responseStr, runtimeErrors } = await runner.sendCommandWithErrors('input', { actions }, timeoutMs);
|
|
1038
|
-
let parsed;
|
|
1039
|
-
try {
|
|
1040
|
-
parsed = JSON.parse(responseStr);
|
|
1041
|
-
}
|
|
1042
|
-
catch {
|
|
1043
|
-
return createErrorResponse(`Invalid response from bridge: ${responseStr}`, [
|
|
1044
|
-
'The game may not have fully initialized yet',
|
|
1045
|
-
'Try again after a few seconds',
|
|
1046
|
-
]);
|
|
1047
|
-
}
|
|
1048
|
-
if (parsed.error) {
|
|
1049
|
-
return createErrorResponse(`Input simulation error: ${parsed.error}`, [
|
|
1050
|
-
'Check action types and parameters',
|
|
1051
|
-
'Ensure key names are valid Godot key names',
|
|
1052
|
-
]);
|
|
1053
|
-
}
|
|
1054
|
-
const payload = {
|
|
1055
|
-
success: true,
|
|
1056
|
-
actions_processed: parsed.actions_processed,
|
|
1057
|
-
tip: 'Call take_screenshot to verify the input had the intended visual effect.',
|
|
1058
|
-
};
|
|
1059
|
-
if (runtimeErrors.length > 0) {
|
|
1060
|
-
payload.warnings = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES);
|
|
1061
|
-
}
|
|
1062
|
-
return {
|
|
1063
|
-
content: [
|
|
1064
|
-
{
|
|
1065
|
-
type: 'text',
|
|
1066
|
-
text: JSON.stringify(payload),
|
|
1067
|
-
},
|
|
1068
|
-
],
|
|
1069
|
-
};
|
|
1070
|
-
}
|
|
1071
|
-
catch (error) {
|
|
1072
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1073
|
-
return createErrorResponse(`Failed to simulate input: ${errorMessage}`, [
|
|
1074
|
-
'Ensure the project is running (use run_project first)',
|
|
1075
|
-
'The bridge may not be ready yet — use get_debug_output to investigate',
|
|
1076
|
-
'Check that UDP port 9900 is not blocked',
|
|
1077
|
-
]);
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
export async function handleGetUiElements(runner, args) {
|
|
1081
|
-
args = normalizeParameters(args);
|
|
1082
|
-
const sessionError = ensureRuntimeSession(runner, 'query UI elements');
|
|
1083
|
-
if (sessionError) {
|
|
1084
|
-
return sessionError;
|
|
1085
|
-
}
|
|
1086
|
-
const visibleOnly = args.visibleOnly !== false;
|
|
1087
|
-
try {
|
|
1088
|
-
const cmdParams = { visible_only: visibleOnly };
|
|
1089
|
-
if (args.filter)
|
|
1090
|
-
cmdParams.type_filter = args.filter;
|
|
1091
|
-
const { response: responseStr, runtimeErrors } = await runner.sendCommandWithErrors('get_ui_elements', cmdParams);
|
|
1092
|
-
let parsed;
|
|
1093
|
-
try {
|
|
1094
|
-
parsed = JSON.parse(responseStr);
|
|
1095
|
-
}
|
|
1096
|
-
catch {
|
|
1097
|
-
return createErrorResponse(`Invalid response from bridge: ${responseStr}`, [
|
|
1098
|
-
'The game may not have fully initialized yet',
|
|
1099
|
-
'Try again after a few seconds',
|
|
1100
|
-
]);
|
|
1101
|
-
}
|
|
1102
|
-
if (parsed.error) {
|
|
1103
|
-
return createErrorResponse(`UI element query error: ${parsed.error}`, [
|
|
1104
|
-
'Ensure the game has a UI with Control nodes',
|
|
1105
|
-
]);
|
|
1106
|
-
}
|
|
1107
|
-
const payload = {
|
|
1108
|
-
...parsed,
|
|
1109
|
-
tip: "Use simulate_input with type 'click_element' and a node_path or text value from this list to interact with these elements.",
|
|
1110
|
-
};
|
|
1111
|
-
if (runtimeErrors.length > 0) {
|
|
1112
|
-
payload.warnings = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES);
|
|
1113
|
-
}
|
|
1114
|
-
return {
|
|
1115
|
-
content: [
|
|
1116
|
-
{
|
|
1117
|
-
type: 'text',
|
|
1118
|
-
text: JSON.stringify(payload),
|
|
1119
|
-
},
|
|
1120
|
-
],
|
|
1121
|
-
};
|
|
1122
|
-
}
|
|
1123
|
-
catch (error) {
|
|
1124
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1125
|
-
return createErrorResponse(`Failed to get UI elements: ${errorMessage}`, [
|
|
1126
|
-
'Ensure the project is running (use run_project first)',
|
|
1127
|
-
'The bridge may not be ready yet — use get_debug_output to investigate',
|
|
1128
|
-
'Check that UDP port 9900 is not blocked',
|
|
1129
|
-
]);
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
export async function handleRunScript(runner, args) {
|
|
1133
|
-
args = normalizeParameters(args);
|
|
1134
|
-
const sessionError = ensureRuntimeSession(runner, 'execute scripts');
|
|
1135
|
-
if (sessionError) {
|
|
1136
|
-
return sessionError;
|
|
1137
|
-
}
|
|
1138
|
-
const script = args.script;
|
|
1139
|
-
if (typeof script !== 'string' || script.trim() === '') {
|
|
1140
|
-
return createErrorResponse('script is required and must be a non-empty string', [
|
|
1141
|
-
'Provide GDScript source code with extends RefCounted and func execute(scene_tree: SceneTree) -> Variant',
|
|
1142
|
-
]);
|
|
1143
|
-
}
|
|
1144
|
-
if (!script.includes('func execute')) {
|
|
1145
|
-
return createErrorResponse('Script must define func execute(scene_tree: SceneTree) -> Variant', ['Add a func execute(scene_tree: SceneTree) -> Variant method to your script']);
|
|
1146
|
-
}
|
|
1147
|
-
// Write script to .mcp/scripts/ for audit trail
|
|
1148
|
-
try {
|
|
1149
|
-
const projectPath = runner.activeProjectPath;
|
|
1150
|
-
if (projectPath) {
|
|
1151
|
-
const scriptsDir = join(projectPath, '.mcp', 'scripts');
|
|
1152
|
-
mkdirSync(scriptsDir, { recursive: true });
|
|
1153
|
-
const timestamp = Date.now();
|
|
1154
|
-
const scriptFile = join(scriptsDir, `${timestamp}.gd`);
|
|
1155
|
-
writeFileSync(scriptFile, script, 'utf8');
|
|
1156
|
-
logDebug(`Saved script to ${scriptFile}`);
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
catch (error) {
|
|
1160
|
-
logDebug(`Failed to save script for audit: ${error}`);
|
|
1161
|
-
}
|
|
1162
|
-
const timeout = typeof args.timeout === 'number' ? args.timeout : 30000;
|
|
1163
|
-
try {
|
|
1164
|
-
const { response: responseStr, runtimeErrors } = await runner.sendCommandWithErrors('run_script', { source: script }, timeout);
|
|
1165
|
-
let parsed;
|
|
1166
|
-
try {
|
|
1167
|
-
parsed = JSON.parse(responseStr);
|
|
1168
|
-
}
|
|
1169
|
-
catch {
|
|
1170
|
-
return createErrorResponse(`Invalid response from bridge: ${responseStr}`, [
|
|
1171
|
-
'The script may have produced non-JSON output',
|
|
1172
|
-
'Check get_debug_output for print() statements',
|
|
1173
|
-
]);
|
|
1174
|
-
}
|
|
1175
|
-
if (parsed.error) {
|
|
1176
|
-
return createErrorResponse(`Script execution error: ${parsed.error}`, [
|
|
1177
|
-
'Check your GDScript syntax',
|
|
1178
|
-
'Ensure the script extends RefCounted',
|
|
1179
|
-
'Check get_debug_output for details',
|
|
1180
|
-
]);
|
|
1181
|
-
}
|
|
1182
|
-
// Detect false-positive success: GDScript has no try-catch, so runtime errors
|
|
1183
|
-
// return null and the real error only appears in stderr.
|
|
1184
|
-
if (parsed.success && parsed.result === null && runner.activeSessionMode === 'spawned') {
|
|
1185
|
-
if (runtimeErrors.length > 0) {
|
|
1186
|
-
const errorContext = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES).join('\n');
|
|
1187
|
-
return createErrorResponse(`Script runtime error detected:\n${errorContext}`, [
|
|
1188
|
-
'Fix the GDScript error in your script and retry',
|
|
1189
|
-
'Use get_debug_output for full process output',
|
|
1190
|
-
]);
|
|
1191
|
-
}
|
|
1192
|
-
return {
|
|
1193
|
-
content: [
|
|
1194
|
-
{
|
|
1195
|
-
type: 'text',
|
|
1196
|
-
text: JSON.stringify({
|
|
1197
|
-
success: true,
|
|
1198
|
-
result: null,
|
|
1199
|
-
warning: 'Script returned null. If unexpected, check get_debug_output for runtime errors — GDScript does not propagate exceptions.',
|
|
1200
|
-
tip: 'Call take_screenshot to verify any visual changes, or get_debug_output to review print() output from your script.',
|
|
1201
|
-
}),
|
|
1202
|
-
},
|
|
1203
|
-
],
|
|
1204
|
-
};
|
|
1205
|
-
}
|
|
1206
|
-
const payload = {
|
|
1207
|
-
success: true,
|
|
1208
|
-
result: parsed.result,
|
|
1209
|
-
tip: 'Call take_screenshot to verify any visual changes, or get_debug_output to review print() output from your script.',
|
|
1210
|
-
};
|
|
1211
|
-
if (runtimeErrors.length > 0) {
|
|
1212
|
-
payload.warnings = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES);
|
|
1213
|
-
}
|
|
1214
|
-
return {
|
|
1215
|
-
content: [
|
|
1216
|
-
{
|
|
1217
|
-
type: 'text',
|
|
1218
|
-
text: JSON.stringify(payload),
|
|
1219
|
-
},
|
|
1220
|
-
],
|
|
1221
|
-
};
|
|
1222
|
-
}
|
|
1223
|
-
catch (error) {
|
|
1224
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1225
|
-
return createErrorResponse(`Failed to execute script: ${errorMessage}`, [
|
|
1226
|
-
'Ensure the project is running (use run_project first)',
|
|
1227
|
-
'The bridge may not be ready yet — wait 2-3 seconds after starting, then check get_debug_output if the issue persists',
|
|
1228
|
-
'Check that UDP port 9900 is not blocked',
|
|
1229
|
-
'For long-running scripts, increase the timeout parameter',
|
|
1230
|
-
]);
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
function buildFilesystemTree(currentPath, relativePath, maxDepth, currentDepth, extensions) {
|
|
1234
|
-
const name = basename(currentPath);
|
|
1235
|
-
const node = { name, type: 'dir', path: relativePath || '.' };
|
|
1236
|
-
if (maxDepth !== -1 && currentDepth >= maxDepth) {
|
|
1237
|
-
node.children = [];
|
|
1238
|
-
return node;
|
|
1239
|
-
}
|
|
1240
|
-
const children = [];
|
|
1241
|
-
try {
|
|
1242
|
-
const entries = readdirSync(currentPath, { withFileTypes: true });
|
|
1243
|
-
for (const entry of entries) {
|
|
1244
|
-
if (entry.name.startsWith('.'))
|
|
1245
|
-
continue;
|
|
1246
|
-
const childRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
1247
|
-
if (entry.isDirectory()) {
|
|
1248
|
-
children.push(buildFilesystemTree(join(currentPath, entry.name), childRelPath, maxDepth, currentDepth + 1, extensions));
|
|
1249
|
-
}
|
|
1250
|
-
else if (entry.isFile()) {
|
|
1251
|
-
const ext = entry.name.includes('.') ? entry.name.split('.').pop().toLowerCase() : '';
|
|
1252
|
-
if (extensions && !extensions.includes(ext))
|
|
1253
|
-
continue;
|
|
1254
|
-
children.push({ name: entry.name, type: 'file', path: childRelPath, extension: ext });
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
catch (err) {
|
|
1259
|
-
logDebug(`buildFilesystemTree error at ${currentPath}: ${err}`);
|
|
1260
|
-
}
|
|
1261
|
-
node.children = children;
|
|
1262
|
-
return node;
|
|
1263
|
-
}
|
|
1264
|
-
function searchInFiles(rootPath, pattern, fileTypes, caseSensitive, maxResults) {
|
|
1265
|
-
const matches = [];
|
|
1266
|
-
let truncated = false;
|
|
1267
|
-
const searchDir = (currentPath, relBase) => {
|
|
1268
|
-
if (truncated)
|
|
1269
|
-
return;
|
|
1270
|
-
let entries;
|
|
1271
|
-
try {
|
|
1272
|
-
entries = readdirSync(currentPath, { withFileTypes: true });
|
|
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 });
|
|
1273
229
|
}
|
|
1274
230
|
catch (err) {
|
|
1275
231
|
logDebug(`searchInFiles readdir error at ${currentPath}: ${err}`);
|
|
@@ -1286,7 +242,8 @@ function searchInFiles(rootPath, pattern, fileTypes, caseSensitive, maxResults)
|
|
|
1286
242
|
searchDir(fullPath, childRelPath);
|
|
1287
243
|
}
|
|
1288
244
|
else if (entry.isFile()) {
|
|
1289
|
-
const
|
|
245
|
+
const dotIdx = entry.name.lastIndexOf('.');
|
|
246
|
+
const ext = dotIdx >= 0 ? entry.name.slice(dotIdx + 1).toLowerCase() : '';
|
|
1290
247
|
if (!fileTypes.includes(ext))
|
|
1291
248
|
continue;
|
|
1292
249
|
let content;
|
|
@@ -1353,122 +310,83 @@ function parseProjectSettings(projectFilePath) {
|
|
|
1353
310
|
}
|
|
1354
311
|
return result;
|
|
1355
312
|
}
|
|
1356
|
-
|
|
313
|
+
// --- Handlers ---
|
|
314
|
+
export async function handleListProjects(args) {
|
|
1357
315
|
args = normalizeParameters(args);
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
try {
|
|
1362
|
-
const projectFile = join(v.projectPath, 'project.godot');
|
|
1363
|
-
const autoloads = parseAutoloads(projectFile);
|
|
1364
|
-
return { content: [{ type: 'text', text: JSON.stringify({ autoloads }) }] };
|
|
1365
|
-
}
|
|
1366
|
-
catch (error) {
|
|
1367
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1368
|
-
return createErrorResponse(`Failed to list autoloads: ${errorMessage}`, [
|
|
1369
|
-
'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',
|
|
1370
319
|
]);
|
|
1371
320
|
}
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
const v = validateProjectArgs(args);
|
|
1376
|
-
if ('isError' in v)
|
|
1377
|
-
return v;
|
|
1378
|
-
if (!args.autoloadName || !args.autoloadPath) {
|
|
1379
|
-
return createErrorResponse('autoloadName and autoloadPath are required', [
|
|
1380
|
-
'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',
|
|
1381
324
|
]);
|
|
1382
325
|
}
|
|
1383
|
-
if (!validatePath(args.autoloadPath)) {
|
|
1384
|
-
return createErrorResponse('Invalid autoload path', ['Provide a valid path without ".."']);
|
|
1385
|
-
}
|
|
1386
326
|
try {
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
return createErrorResponse(`Autoload '${args.autoloadName}' already exists`, [
|
|
1391
|
-
'Use update_autoload to modify it',
|
|
1392
|
-
'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',
|
|
1393
330
|
]);
|
|
1394
331
|
}
|
|
1395
|
-
const
|
|
1396
|
-
|
|
332
|
+
const recursive = args.recursive === true;
|
|
333
|
+
const projects = findGodotProjects(args.directory, recursive);
|
|
1397
334
|
return {
|
|
1398
|
-
content: [
|
|
1399
|
-
{
|
|
1400
|
-
type: 'text',
|
|
1401
|
-
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.`,
|
|
1402
|
-
},
|
|
1403
|
-
],
|
|
335
|
+
content: [{ type: 'text', text: JSON.stringify(projects) }],
|
|
1404
336
|
};
|
|
1405
337
|
}
|
|
1406
338
|
catch (error) {
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
'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',
|
|
1410
342
|
]);
|
|
1411
343
|
}
|
|
1412
344
|
}
|
|
1413
|
-
export async function
|
|
345
|
+
export async function handleGetProjectInfo(runner, args) {
|
|
1414
346
|
args = normalizeParameters(args);
|
|
1415
|
-
const v = validateProjectArgs(args);
|
|
1416
|
-
if ('isError' in v)
|
|
1417
|
-
return v;
|
|
1418
|
-
if (!args.autoloadName) {
|
|
1419
|
-
return createErrorResponse('autoloadName is required', [
|
|
1420
|
-
'Provide the name of the autoload to remove',
|
|
1421
|
-
]);
|
|
1422
|
-
}
|
|
1423
347
|
try {
|
|
1424
|
-
const
|
|
1425
|
-
|
|
1426
|
-
if (!
|
|
1427
|
-
return
|
|
1428
|
-
'
|
|
1429
|
-
|
|
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
|
+
};
|
|
1430
354
|
}
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
if (!args.autoloadName) {
|
|
1448
|
-
return createErrorResponse('autoloadName is required', [
|
|
1449
|
-
'Provide the name of the autoload to update',
|
|
1450
|
-
]);
|
|
1451
|
-
}
|
|
1452
|
-
if (args.autoloadPath && !validatePath(args.autoloadPath)) {
|
|
1453
|
-
return createErrorResponse('Invalid autoload path', ['Provide a valid path without ".."']);
|
|
1454
|
-
}
|
|
1455
|
-
try {
|
|
1456
|
-
const projectFile = join(v.projectPath, 'project.godot');
|
|
1457
|
-
const updated = updateAutoloadEntry(projectFile, args.autoloadName, args.autoloadPath, args.singleton);
|
|
1458
|
-
if (!updated) {
|
|
1459
|
-
return createErrorResponse(`Autoload '${args.autoloadName}' not found`, [
|
|
1460
|
-
'Use list_autoloads to see existing autoloads',
|
|
1461
|
-
'Use add_autoload to register a new one',
|
|
1462
|
-
]);
|
|
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}`);
|
|
1463
371
|
}
|
|
1464
372
|
return {
|
|
1465
|
-
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
|
+
],
|
|
1466
384
|
};
|
|
1467
385
|
}
|
|
1468
386
|
catch (error) {
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
'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',
|
|
1472
390
|
]);
|
|
1473
391
|
}
|
|
1474
392
|
}
|
|
@@ -1486,8 +404,7 @@ export async function handleGetProjectFiles(args) {
|
|
|
1486
404
|
return { content: [{ type: 'text', text: JSON.stringify(tree) }] };
|
|
1487
405
|
}
|
|
1488
406
|
catch (error) {
|
|
1489
|
-
|
|
1490
|
-
return createErrorResponse(`Failed to get project files: ${errorMessage}`, [
|
|
407
|
+
return createErrorResponse(`Failed to get project files: ${getErrorMessage(error)}`, [
|
|
1491
408
|
'Check if the project directory is accessible',
|
|
1492
409
|
]);
|
|
1493
410
|
}
|
|
@@ -1510,8 +427,7 @@ export async function handleSearchProject(args) {
|
|
|
1510
427
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
1511
428
|
}
|
|
1512
429
|
catch (error) {
|
|
1513
|
-
|
|
1514
|
-
return createErrorResponse(`Failed to search project: ${errorMessage}`, [
|
|
430
|
+
return createErrorResponse(`Failed to search project: ${getErrorMessage(error)}`, [
|
|
1515
431
|
'Check if the project directory is accessible',
|
|
1516
432
|
]);
|
|
1517
433
|
}
|
|
@@ -1562,8 +478,7 @@ export async function handleGetSceneDependencies(args) {
|
|
|
1562
478
|
};
|
|
1563
479
|
}
|
|
1564
480
|
catch (error) {
|
|
1565
|
-
|
|
1566
|
-
return createErrorResponse(`Failed to get scene dependencies: ${errorMessage}`, [
|
|
481
|
+
return createErrorResponse(`Failed to get scene dependencies: ${getErrorMessage(error)}`, [
|
|
1567
482
|
'Check if the scene file is accessible',
|
|
1568
483
|
]);
|
|
1569
484
|
}
|
|
@@ -1574,7 +489,7 @@ export async function handleGetProjectSettings(args) {
|
|
|
1574
489
|
if ('isError' in v)
|
|
1575
490
|
return v;
|
|
1576
491
|
try {
|
|
1577
|
-
const projectFile =
|
|
492
|
+
const projectFile = projectGodotPath(v.projectPath);
|
|
1578
493
|
const allSettings = parseProjectSettings(projectFile);
|
|
1579
494
|
if (args.section && typeof args.section === 'string') {
|
|
1580
495
|
const sectionData = allSettings[args.section] ?? {};
|
|
@@ -1583,8 +498,7 @@ export async function handleGetProjectSettings(args) {
|
|
|
1583
498
|
return { content: [{ type: 'text', text: JSON.stringify({ settings: allSettings }) }] };
|
|
1584
499
|
}
|
|
1585
500
|
catch (error) {
|
|
1586
|
-
|
|
1587
|
-
return createErrorResponse(`Failed to get project settings: ${errorMessage}`, [
|
|
501
|
+
return createErrorResponse(`Failed to get project settings: ${getErrorMessage(error)}`, [
|
|
1588
502
|
'Check if project.godot is accessible',
|
|
1589
503
|
]);
|
|
1590
504
|
}
|