godot-mcp-runtime 2.2.1 → 2.2.2
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 +61 -64
- package/dist/dispatch.d.ts +26 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +70 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -118
- package/dist/index.js.map +1 -1
- package/dist/scripts/godot_operations.gd +1117 -1117
- package/dist/scripts/mcp_bridge.gd +3 -1
- package/dist/tools/node-tools.d.ts +1 -1
- package/dist/tools/node-tools.d.ts.map +1 -1
- package/dist/tools/node-tools.js +168 -56
- package/dist/tools/node-tools.js.map +1 -1
- package/dist/tools/project-tools.d.ts +1 -1
- package/dist/tools/project-tools.d.ts.map +1 -1
- package/dist/tools/project-tools.js +416 -122
- package/dist/tools/project-tools.js.map +1 -1
- package/dist/tools/scene-tools.d.ts +1 -1
- package/dist/tools/scene-tools.d.ts.map +1 -1
- package/dist/tools/scene-tools.js +168 -44
- package/dist/tools/scene-tools.js.map +1 -1
- package/dist/tools/validate-tools.d.ts +1 -1
- package/dist/tools/validate-tools.d.ts.map +1 -1
- package/dist/tools/validate-tools.js +44 -15
- package/dist/tools/validate-tools.js.map +1 -1
- package/dist/utils/godot-runner.d.ts +30 -1
- package/dist/utils/godot-runner.d.ts.map +1 -1
- package/dist/utils/godot-runner.js +172 -40
- package/dist/utils/godot-runner.js.map +1 -1
- package/package.json +22 -4
|
@@ -28,7 +28,7 @@ function addAutoloadEntry(projectFilePath, name, path, singleton) {
|
|
|
28
28
|
const content = readFileSync(projectFilePath, 'utf8');
|
|
29
29
|
const lines = content.split('\n');
|
|
30
30
|
const entry = `${name}="${singleton ? '*' : ''}${normalizeAutoloadPath(path)}"`;
|
|
31
|
-
const sectionIdx = lines.findIndex(l => l.trim() === '[autoload]');
|
|
31
|
+
const sectionIdx = lines.findIndex((l) => l.trim() === '[autoload]');
|
|
32
32
|
if (sectionIdx === -1) {
|
|
33
33
|
writeFileSync(projectFilePath, content.trimEnd() + '\n\n[autoload]\n' + entry + '\n', 'utf8');
|
|
34
34
|
return;
|
|
@@ -45,7 +45,7 @@ function removeAutoloadEntry(projectFilePath, name) {
|
|
|
45
45
|
const lines = content.split('\n');
|
|
46
46
|
let inAutoloadSection = false;
|
|
47
47
|
let removed = false;
|
|
48
|
-
const newLines = lines.filter(line => {
|
|
48
|
+
const newLines = lines.filter((line) => {
|
|
49
49
|
const trimmed = line.trim();
|
|
50
50
|
if (trimmed.startsWith('[')) {
|
|
51
51
|
inAutoloadSection = trimmed === '[autoload]';
|
|
@@ -69,7 +69,7 @@ function updateAutoloadEntry(projectFilePath, name, newPath, singleton) {
|
|
|
69
69
|
const lines = content.split('\n');
|
|
70
70
|
let inAutoloadSection = false;
|
|
71
71
|
let updated = false;
|
|
72
|
-
const newLines = lines.map(line => {
|
|
72
|
+
const newLines = lines.map((line) => {
|
|
73
73
|
const trimmed = line.trim();
|
|
74
74
|
if (trimmed.startsWith('[')) {
|
|
75
75
|
inAutoloadSection = trimmed === '[autoload]';
|
|
@@ -108,7 +108,7 @@ export const projectToolDefinitions = [
|
|
|
108
108
|
},
|
|
109
109
|
{
|
|
110
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.
|
|
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
112
|
inputSchema: {
|
|
113
113
|
type: 'object',
|
|
114
114
|
properties: {
|
|
@@ -130,7 +130,7 @@ export const projectToolDefinitions = [
|
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
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. Use detach_project or stop_project when done. get_debug_output is not available in attached mode because stdout/stderr are not captured.',
|
|
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
134
|
inputSchema: {
|
|
135
135
|
type: 'object',
|
|
136
136
|
properties: {
|
|
@@ -138,6 +138,10 @@ export const projectToolDefinitions = [
|
|
|
138
138
|
type: 'string',
|
|
139
139
|
description: 'Path to the Godot project directory',
|
|
140
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
|
+
},
|
|
141
145
|
},
|
|
142
146
|
required: ['projectPath'],
|
|
143
147
|
},
|
|
@@ -208,7 +212,7 @@ export const projectToolDefinitions = [
|
|
|
208
212
|
},
|
|
209
213
|
{
|
|
210
214
|
name: 'take_screenshot',
|
|
211
|
-
description: 'Capture a PNG screenshot of the running Godot viewport. Requires
|
|
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.',
|
|
212
216
|
inputSchema: {
|
|
213
217
|
type: 'object',
|
|
214
218
|
properties: {
|
|
@@ -222,7 +226,7 @@ export const projectToolDefinitions = [
|
|
|
222
226
|
},
|
|
223
227
|
{
|
|
224
228
|
name: 'simulate_input',
|
|
225
|
-
description: 'Simulate batched sequential input in a running Godot project. Requires
|
|
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}]',
|
|
226
230
|
inputSchema: {
|
|
227
231
|
type: 'object',
|
|
228
232
|
properties: {
|
|
@@ -237,21 +241,58 @@ export const projectToolDefinitions = [
|
|
|
237
241
|
enum: ['key', 'mouse_button', 'mouse_motion', 'click_element', 'action', 'wait'],
|
|
238
242
|
description: 'The type of input action',
|
|
239
243
|
},
|
|
240
|
-
key: {
|
|
241
|
-
|
|
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
|
+
},
|
|
242
252
|
shift: { type: 'boolean', description: '[key] Shift modifier' },
|
|
243
253
|
ctrl: { type: 'boolean', description: '[key] Ctrl modifier' },
|
|
244
254
|
alt: { type: 'boolean', description: '[key] Alt modifier' },
|
|
245
|
-
button: {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
+
},
|
|
255
296
|
},
|
|
256
297
|
required: ['type'],
|
|
257
298
|
},
|
|
@@ -262,7 +303,7 @@ export const projectToolDefinitions = [
|
|
|
262
303
|
},
|
|
263
304
|
{
|
|
264
305
|
name: 'get_ui_elements',
|
|
265
|
-
description: 'Get Control nodes from a running Godot project with their positions, sizes, types, and text. Requires
|
|
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? }] }',
|
|
266
307
|
inputSchema: {
|
|
267
308
|
type: 'object',
|
|
268
309
|
properties: {
|
|
@@ -314,9 +355,18 @@ export const projectToolDefinitions = [
|
|
|
314
355
|
type: 'object',
|
|
315
356
|
properties: {
|
|
316
357
|
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
317
|
-
autoloadName: {
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+
},
|
|
320
370
|
},
|
|
321
371
|
required: ['projectPath', 'autoloadName', 'autoloadPath'],
|
|
322
372
|
},
|
|
@@ -335,7 +385,7 @@ export const projectToolDefinitions = [
|
|
|
335
385
|
},
|
|
336
386
|
{
|
|
337
387
|
name: 'update_autoload',
|
|
338
|
-
description:
|
|
388
|
+
description: "Modify an existing autoload's path or singleton flag. No Godot process required.",
|
|
339
389
|
inputSchema: {
|
|
340
390
|
type: 'object',
|
|
341
391
|
properties: {
|
|
@@ -354,8 +404,15 @@ export const projectToolDefinitions = [
|
|
|
354
404
|
type: 'object',
|
|
355
405
|
properties: {
|
|
356
406
|
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
357
|
-
maxDepth: {
|
|
358
|
-
|
|
407
|
+
maxDepth: {
|
|
408
|
+
type: 'number',
|
|
409
|
+
description: 'Maximum recursion depth. -1 means unlimited (default: -1)',
|
|
410
|
+
},
|
|
411
|
+
extensions: {
|
|
412
|
+
type: 'array',
|
|
413
|
+
items: { type: 'string' },
|
|
414
|
+
description: 'Filter to only these file extensions (e.g. ["gd", "tscn"]). Omit to include all.',
|
|
415
|
+
},
|
|
359
416
|
},
|
|
360
417
|
required: ['projectPath'],
|
|
361
418
|
},
|
|
@@ -368,7 +425,11 @@ export const projectToolDefinitions = [
|
|
|
368
425
|
properties: {
|
|
369
426
|
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
370
427
|
pattern: { type: 'string', description: 'Plain-text string to search for' },
|
|
371
|
-
fileTypes: {
|
|
428
|
+
fileTypes: {
|
|
429
|
+
type: 'array',
|
|
430
|
+
items: { type: 'string' },
|
|
431
|
+
description: 'File extensions to search (default: ["gd", "tscn", "cs", "gdshader"])',
|
|
432
|
+
},
|
|
372
433
|
caseSensitive: { type: 'boolean', description: 'Case-sensitive search (default: false)' },
|
|
373
434
|
maxResults: { type: 'number', description: 'Maximum matches to return (default: 100)' },
|
|
374
435
|
},
|
|
@@ -382,7 +443,10 @@ export const projectToolDefinitions = [
|
|
|
382
443
|
type: 'object',
|
|
383
444
|
properties: {
|
|
384
445
|
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
385
|
-
scenePath: {
|
|
446
|
+
scenePath: {
|
|
447
|
+
type: 'string',
|
|
448
|
+
description: 'Path to the .tscn file relative to the project root (e.g. "scenes/main.tscn")',
|
|
449
|
+
},
|
|
386
450
|
},
|
|
387
451
|
required: ['projectPath', 'scenePath'],
|
|
388
452
|
},
|
|
@@ -394,7 +458,10 @@ export const projectToolDefinitions = [
|
|
|
394
458
|
type: 'object',
|
|
395
459
|
properties: {
|
|
396
460
|
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
|
|
397
|
-
section: {
|
|
461
|
+
section: {
|
|
462
|
+
type: 'string',
|
|
463
|
+
description: 'Filter to a specific INI section (e.g. "display", "application"). Omit for all sections.',
|
|
464
|
+
},
|
|
398
465
|
},
|
|
399
466
|
required: ['projectPath'],
|
|
400
467
|
},
|
|
@@ -402,10 +469,17 @@ export const projectToolDefinitions = [
|
|
|
402
469
|
];
|
|
403
470
|
function ensureRuntimeSession(runner, actionDescription) {
|
|
404
471
|
if (!runner.activeSessionMode || !runner.activeProjectPath) {
|
|
405
|
-
return createErrorResponse(`No active runtime session. A project must be running or attached to ${actionDescription}.`, [
|
|
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
|
+
]);
|
|
406
476
|
}
|
|
407
|
-
if (runner.activeSessionMode === 'spawned' &&
|
|
408
|
-
|
|
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
|
+
]);
|
|
409
483
|
}
|
|
410
484
|
return null;
|
|
411
485
|
}
|
|
@@ -482,21 +556,31 @@ function getProjectStructure(projectPath) {
|
|
|
482
556
|
export async function handleLaunchEditor(runner, args) {
|
|
483
557
|
args = normalizeParameters(args);
|
|
484
558
|
if (!args.projectPath) {
|
|
485
|
-
return createErrorResponse('Project path is required', [
|
|
559
|
+
return createErrorResponse('Project path is required', [
|
|
560
|
+
'Provide a valid path to a Godot project directory',
|
|
561
|
+
]);
|
|
486
562
|
}
|
|
487
563
|
if (!validatePath(args.projectPath)) {
|
|
488
|
-
return createErrorResponse('Invalid project path', [
|
|
564
|
+
return createErrorResponse('Invalid project path', [
|
|
565
|
+
'Provide a valid path without ".." or other potentially unsafe characters',
|
|
566
|
+
]);
|
|
489
567
|
}
|
|
490
568
|
try {
|
|
491
569
|
if (!runner.getGodotPath()) {
|
|
492
570
|
await runner.detectGodotPath();
|
|
493
571
|
if (!runner.getGodotPath()) {
|
|
494
|
-
return createErrorResponse('Could not find a valid Godot executable path', [
|
|
572
|
+
return createErrorResponse('Could not find a valid Godot executable path', [
|
|
573
|
+
'Ensure Godot is installed correctly',
|
|
574
|
+
'Set GODOT_PATH environment variable',
|
|
575
|
+
]);
|
|
495
576
|
}
|
|
496
577
|
}
|
|
497
578
|
const projectFile = join(args.projectPath, 'project.godot');
|
|
498
579
|
if (!existsSync(projectFile)) {
|
|
499
|
-
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, [
|
|
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
|
+
]);
|
|
500
584
|
}
|
|
501
585
|
logDebug(`Launching Godot editor for project: ${args.projectPath}`);
|
|
502
586
|
const process = runner.launchEditor(args.projectPath);
|
|
@@ -504,34 +588,75 @@ export async function handleLaunchEditor(runner, args) {
|
|
|
504
588
|
console.error('Failed to start Godot editor:', err);
|
|
505
589
|
});
|
|
506
590
|
return {
|
|
507
|
-
content: [
|
|
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
|
+
],
|
|
508
597
|
};
|
|
509
598
|
}
|
|
510
599
|
catch (error) {
|
|
511
600
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
512
|
-
return createErrorResponse(`Failed to launch Godot editor: ${errorMessage}`, [
|
|
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
|
+
]);
|
|
513
605
|
}
|
|
514
606
|
}
|
|
515
607
|
export async function handleRunProject(runner, args) {
|
|
516
608
|
args = normalizeParameters(args);
|
|
517
609
|
if (!args.projectPath) {
|
|
518
|
-
return createErrorResponse('Project path is required', [
|
|
610
|
+
return createErrorResponse('Project path is required', [
|
|
611
|
+
'Provide a valid path to a Godot project directory',
|
|
612
|
+
]);
|
|
519
613
|
}
|
|
520
614
|
if (!validatePath(args.projectPath)) {
|
|
521
|
-
return createErrorResponse('Invalid project path', [
|
|
615
|
+
return createErrorResponse('Invalid project path', [
|
|
616
|
+
'Provide a valid path without ".." or other potentially unsafe characters',
|
|
617
|
+
]);
|
|
522
618
|
}
|
|
523
619
|
try {
|
|
524
620
|
const projectFile = join(args.projectPath, 'project.godot');
|
|
525
621
|
if (!existsSync(projectFile)) {
|
|
526
|
-
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, [
|
|
622
|
+
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, [
|
|
623
|
+
'Ensure the path points to a directory containing a project.godot file',
|
|
624
|
+
'Use list_projects to find valid Godot projects',
|
|
625
|
+
]);
|
|
527
626
|
}
|
|
528
627
|
const background = args.background === true;
|
|
529
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
|
+
]);
|
|
638
|
+
}
|
|
639
|
+
const lines = [
|
|
640
|
+
'Godot process started, but the MCP bridge did not respond within 8 seconds.',
|
|
641
|
+
'- The process is running — bridge may still be initializing',
|
|
642
|
+
'- Use get_debug_output to investigate',
|
|
643
|
+
'- Runtime tools may work if you retry after a moment',
|
|
644
|
+
'- Call stop_project when done',
|
|
645
|
+
];
|
|
646
|
+
if (background) {
|
|
647
|
+
lines.push('- Background mode: window hidden, physical input blocked');
|
|
648
|
+
}
|
|
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
|
+
}
|
|
530
655
|
const lines = [
|
|
531
|
-
'Godot project started.',
|
|
656
|
+
'Godot project started and MCP bridge is ready.',
|
|
657
|
+
'- Runtime tools (take_screenshot, simulate_input, get_ui_elements, run_script) are available now',
|
|
532
658
|
'- Use get_debug_output to check runtime output and errors',
|
|
533
|
-
'-
|
|
534
|
-
'- Always call stop_project when done — it terminates the process and cleans up the MCP bridge',
|
|
659
|
+
'- Call stop_project when done',
|
|
535
660
|
];
|
|
536
661
|
if (background) {
|
|
537
662
|
lines.push('- Background mode: window hidden, physical input blocked');
|
|
@@ -542,64 +667,112 @@ export async function handleRunProject(runner, args) {
|
|
|
542
667
|
}
|
|
543
668
|
catch (error) {
|
|
544
669
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
545
|
-
return createErrorResponse(`Failed to run Godot project: ${errorMessage}`, [
|
|
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
|
+
]);
|
|
546
674
|
}
|
|
547
675
|
}
|
|
548
676
|
export async function handleAttachProject(runner, args) {
|
|
549
677
|
args = normalizeParameters(args);
|
|
550
678
|
if (!args.projectPath) {
|
|
551
|
-
return createErrorResponse('Project path is required', [
|
|
679
|
+
return createErrorResponse('Project path is required', [
|
|
680
|
+
'Provide a valid path to a Godot project directory',
|
|
681
|
+
]);
|
|
552
682
|
}
|
|
553
683
|
if (!validatePath(args.projectPath)) {
|
|
554
|
-
return createErrorResponse('Invalid project path', [
|
|
684
|
+
return createErrorResponse('Invalid project path', [
|
|
685
|
+
'Provide a valid path without ".." or other potentially unsafe characters',
|
|
686
|
+
]);
|
|
555
687
|
}
|
|
556
688
|
try {
|
|
557
689
|
const projectFile = join(args.projectPath, 'project.godot');
|
|
558
690
|
if (!existsSync(projectFile)) {
|
|
559
|
-
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, [
|
|
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
|
+
]);
|
|
560
695
|
}
|
|
561
696
|
runner.attachProject(args.projectPath);
|
|
697
|
+
if (args.waitForBridge === true) {
|
|
698
|
+
const bridgeResult = await runner.waitForBridgeAttached();
|
|
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
|
+
}
|
|
562
721
|
return {
|
|
563
|
-
content: [
|
|
722
|
+
content: [
|
|
723
|
+
{
|
|
564
724
|
type: 'text',
|
|
565
725
|
text: [
|
|
566
726
|
'Project attached for manual runtime use.',
|
|
567
|
-
'- Launch Godot yourself
|
|
568
|
-
'-
|
|
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',
|
|
569
729
|
'- get_debug_output is unavailable in attached mode because MCP did not spawn the process',
|
|
570
730
|
'- Use detach_project or stop_project when done to clean up the injected bridge state',
|
|
571
731
|
].join('\n'),
|
|
572
|
-
}
|
|
732
|
+
},
|
|
733
|
+
],
|
|
573
734
|
};
|
|
574
735
|
}
|
|
575
736
|
catch (error) {
|
|
576
737
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
577
|
-
return createErrorResponse(`Failed to attach project: ${errorMessage}`, [
|
|
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
|
+
]);
|
|
578
742
|
}
|
|
579
743
|
}
|
|
580
744
|
export function handleDetachProject(runner) {
|
|
581
745
|
if (runner.activeSessionMode !== 'attached') {
|
|
582
|
-
return createErrorResponse('No attached project to detach.', [
|
|
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
|
+
]);
|
|
583
750
|
}
|
|
584
751
|
const result = runner.stopProject();
|
|
585
752
|
return {
|
|
586
|
-
content: [
|
|
753
|
+
content: [
|
|
754
|
+
{
|
|
587
755
|
type: 'text',
|
|
588
756
|
text: JSON.stringify({
|
|
589
757
|
message: 'Detached attached project and cleaned MCP bridge state',
|
|
590
758
|
externalProcessPreserved: result.externalProcessPreserved === true,
|
|
591
759
|
}),
|
|
592
|
-
}
|
|
760
|
+
},
|
|
761
|
+
],
|
|
593
762
|
};
|
|
594
763
|
}
|
|
595
764
|
export function handleGetDebugOutput(runner, args = {}) {
|
|
596
765
|
args = normalizeParameters(args);
|
|
597
766
|
if (!runner.activeSessionMode) {
|
|
598
|
-
return createErrorResponse('No active runtime session.', [
|
|
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
|
+
]);
|
|
599
771
|
}
|
|
600
772
|
if (runner.activeSessionMode === 'attached') {
|
|
601
773
|
return {
|
|
602
|
-
content: [
|
|
774
|
+
content: [
|
|
775
|
+
{
|
|
603
776
|
type: 'text',
|
|
604
777
|
text: JSON.stringify({
|
|
605
778
|
output: [],
|
|
@@ -608,12 +781,16 @@ export function handleGetDebugOutput(runner, args = {}) {
|
|
|
608
781
|
attached: true,
|
|
609
782
|
tip: 'Attached mode does not capture stdout/stderr because Godot was launched outside MCP.',
|
|
610
783
|
}),
|
|
611
|
-
}
|
|
784
|
+
},
|
|
785
|
+
],
|
|
612
786
|
};
|
|
613
787
|
}
|
|
614
788
|
const proc = runner.activeProcess;
|
|
615
789
|
if (!proc) {
|
|
616
|
-
return createErrorResponse('No active spawned process is available for debug output.', [
|
|
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
|
+
]);
|
|
617
794
|
}
|
|
618
795
|
const limit = typeof args.limit === 'number' ? args.limit : 200;
|
|
619
796
|
const response = {
|
|
@@ -623,22 +800,29 @@ export function handleGetDebugOutput(runner, args = {}) {
|
|
|
623
800
|
};
|
|
624
801
|
if (proc.hasExited) {
|
|
625
802
|
response.exitCode = proc.exitCode;
|
|
626
|
-
response.tip =
|
|
803
|
+
response.tip =
|
|
804
|
+
'Process has exited. Call stop_project to clean up the process slot before starting a new one.';
|
|
627
805
|
}
|
|
628
806
|
return {
|
|
629
|
-
content: [
|
|
807
|
+
content: [
|
|
808
|
+
{
|
|
630
809
|
type: 'text',
|
|
631
810
|
text: JSON.stringify(response),
|
|
632
|
-
}
|
|
811
|
+
},
|
|
812
|
+
],
|
|
633
813
|
};
|
|
634
814
|
}
|
|
635
815
|
export function handleStopProject(runner) {
|
|
636
816
|
const result = runner.stopProject();
|
|
637
817
|
if (!result) {
|
|
638
|
-
return createErrorResponse('No active Godot process to stop.', [
|
|
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
|
+
]);
|
|
639
822
|
}
|
|
640
823
|
return {
|
|
641
|
-
content: [
|
|
824
|
+
content: [
|
|
825
|
+
{
|
|
642
826
|
type: 'text',
|
|
643
827
|
text: JSON.stringify({
|
|
644
828
|
message: result.mode === 'attached'
|
|
@@ -649,20 +833,27 @@ export function handleStopProject(runner) {
|
|
|
649
833
|
finalOutput: result.output.slice(-200),
|
|
650
834
|
finalErrors: result.errors.slice(-200),
|
|
651
835
|
}),
|
|
652
|
-
}
|
|
836
|
+
},
|
|
837
|
+
],
|
|
653
838
|
};
|
|
654
839
|
}
|
|
655
840
|
export async function handleListProjects(args) {
|
|
656
841
|
args = normalizeParameters(args);
|
|
657
842
|
if (!args.directory) {
|
|
658
|
-
return createErrorResponse('Directory is required', [
|
|
843
|
+
return createErrorResponse('Directory is required', [
|
|
844
|
+
'Provide a valid directory path to search for Godot projects',
|
|
845
|
+
]);
|
|
659
846
|
}
|
|
660
847
|
if (!validatePath(args.directory)) {
|
|
661
|
-
return createErrorResponse('Invalid directory path', [
|
|
848
|
+
return createErrorResponse('Invalid directory path', [
|
|
849
|
+
'Provide a valid path without ".." or other potentially unsafe characters',
|
|
850
|
+
]);
|
|
662
851
|
}
|
|
663
852
|
try {
|
|
664
853
|
if (!existsSync(args.directory)) {
|
|
665
|
-
return createErrorResponse(`Directory does not exist: ${args.directory}`, [
|
|
854
|
+
return createErrorResponse(`Directory does not exist: ${args.directory}`, [
|
|
855
|
+
'Provide a valid directory path that exists on the system',
|
|
856
|
+
]);
|
|
666
857
|
}
|
|
667
858
|
const recursive = args.recursive === true;
|
|
668
859
|
const projects = findGodotProjects(args.directory, recursive);
|
|
@@ -672,7 +863,10 @@ export async function handleListProjects(args) {
|
|
|
672
863
|
}
|
|
673
864
|
catch (error) {
|
|
674
865
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
675
|
-
return createErrorResponse(`Failed to list projects: ${errorMessage}`, [
|
|
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
|
+
]);
|
|
676
870
|
}
|
|
677
871
|
}
|
|
678
872
|
export async function handleGetProjectInfo(runner, args) {
|
|
@@ -686,11 +880,16 @@ export async function handleGetProjectInfo(runner, args) {
|
|
|
686
880
|
};
|
|
687
881
|
}
|
|
688
882
|
if (!validatePath(args.projectPath)) {
|
|
689
|
-
return createErrorResponse('Invalid project path', [
|
|
883
|
+
return createErrorResponse('Invalid project path', [
|
|
884
|
+
'Provide a valid path without ".." or other potentially unsafe characters',
|
|
885
|
+
]);
|
|
690
886
|
}
|
|
691
887
|
const projectFile = join(args.projectPath, 'project.godot');
|
|
692
888
|
if (!existsSync(projectFile)) {
|
|
693
|
-
return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, [
|
|
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
|
+
]);
|
|
694
893
|
}
|
|
695
894
|
const projectStructure = getProjectStructure(args.projectPath);
|
|
696
895
|
let projectName = basename(args.projectPath);
|
|
@@ -706,7 +905,8 @@ export async function handleGetProjectInfo(runner, args) {
|
|
|
706
905
|
logDebug(`Error reading project file: ${error}`);
|
|
707
906
|
}
|
|
708
907
|
return {
|
|
709
|
-
content: [
|
|
908
|
+
content: [
|
|
909
|
+
{
|
|
710
910
|
type: 'text',
|
|
711
911
|
text: JSON.stringify({
|
|
712
912
|
name: projectName,
|
|
@@ -714,12 +914,16 @@ export async function handleGetProjectInfo(runner, args) {
|
|
|
714
914
|
godotVersion: version,
|
|
715
915
|
structure: projectStructure,
|
|
716
916
|
}),
|
|
717
|
-
}
|
|
917
|
+
},
|
|
918
|
+
],
|
|
718
919
|
};
|
|
719
920
|
}
|
|
720
921
|
catch (error) {
|
|
721
922
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
722
|
-
return createErrorResponse(`Failed to get project info: ${errorMessage}`, [
|
|
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
|
+
]);
|
|
723
927
|
}
|
|
724
928
|
}
|
|
725
929
|
export async function handleTakeScreenshot(runner, args) {
|
|
@@ -736,18 +940,29 @@ export async function handleTakeScreenshot(runner, args) {
|
|
|
736
940
|
parsed = JSON.parse(responseStr);
|
|
737
941
|
}
|
|
738
942
|
catch {
|
|
739
|
-
return createErrorResponse(`Invalid response from screenshot server: ${responseStr}`, [
|
|
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
|
+
]);
|
|
740
947
|
}
|
|
741
948
|
if (parsed.error) {
|
|
742
|
-
return createErrorResponse(`Screenshot server error: ${parsed.error}`, [
|
|
949
|
+
return createErrorResponse(`Screenshot server error: ${parsed.error}`, [
|
|
950
|
+
'Ensure the game viewport is active',
|
|
951
|
+
'Try again after a moment',
|
|
952
|
+
]);
|
|
743
953
|
}
|
|
744
954
|
if (!parsed.path) {
|
|
745
|
-
return createErrorResponse('Screenshot server returned no file path', [
|
|
955
|
+
return createErrorResponse('Screenshot server returned no file path', [
|
|
956
|
+
'Try again after a few seconds',
|
|
957
|
+
]);
|
|
746
958
|
}
|
|
747
959
|
// Normalize path for the local filesystem (forward slashes from GDScript)
|
|
748
960
|
const screenshotPath = sep === '\\' ? parsed.path.replace(/\//g, '\\') : parsed.path;
|
|
749
961
|
if (!existsSync(screenshotPath)) {
|
|
750
|
-
return createErrorResponse(`Screenshot file not found at: ${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
|
+
]);
|
|
751
966
|
}
|
|
752
967
|
const imageBuffer = readFileSync(screenshotPath);
|
|
753
968
|
const base64Data = imageBuffer.toString('base64');
|
|
@@ -776,7 +991,7 @@ export async function handleTakeScreenshot(runner, args) {
|
|
|
776
991
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
777
992
|
return createErrorResponse(`Failed to take screenshot: ${errorMessage}`, [
|
|
778
993
|
'Ensure the project is running (use run_project first)',
|
|
779
|
-
'The bridge may not be ready yet —
|
|
994
|
+
'The bridge may not be ready yet — use get_debug_output to investigate',
|
|
780
995
|
'Check that UDP port 9900 is not blocked',
|
|
781
996
|
]);
|
|
782
997
|
}
|
|
@@ -789,12 +1004,17 @@ export async function handleSimulateInput(runner, args) {
|
|
|
789
1004
|
}
|
|
790
1005
|
const actions = args.actions;
|
|
791
1006
|
if (!Array.isArray(actions) || actions.length === 0) {
|
|
792
|
-
return createErrorResponse('actions must be a non-empty array of input actions', [
|
|
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
|
+
]);
|
|
793
1010
|
}
|
|
794
1011
|
// Calculate timeout: sum of all wait durations + 10s buffer
|
|
795
1012
|
let totalWaitMs = 0;
|
|
796
1013
|
for (const action of actions) {
|
|
797
|
-
if (typeof action === 'object' &&
|
|
1014
|
+
if (typeof action === 'object' &&
|
|
1015
|
+
action !== null &&
|
|
1016
|
+
action.type === 'wait' &&
|
|
1017
|
+
typeof action.ms === 'number') {
|
|
798
1018
|
totalWaitMs += action.ms;
|
|
799
1019
|
}
|
|
800
1020
|
}
|
|
@@ -806,10 +1026,16 @@ export async function handleSimulateInput(runner, args) {
|
|
|
806
1026
|
parsed = JSON.parse(responseStr);
|
|
807
1027
|
}
|
|
808
1028
|
catch {
|
|
809
|
-
return createErrorResponse(`Invalid response from bridge: ${responseStr}`, [
|
|
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
|
+
]);
|
|
810
1033
|
}
|
|
811
1034
|
if (parsed.error) {
|
|
812
|
-
return createErrorResponse(`Input simulation error: ${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
|
+
]);
|
|
813
1039
|
}
|
|
814
1040
|
const payload = {
|
|
815
1041
|
success: true,
|
|
@@ -820,17 +1046,19 @@ export async function handleSimulateInput(runner, args) {
|
|
|
820
1046
|
payload.warnings = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES);
|
|
821
1047
|
}
|
|
822
1048
|
return {
|
|
823
|
-
content: [
|
|
1049
|
+
content: [
|
|
1050
|
+
{
|
|
824
1051
|
type: 'text',
|
|
825
1052
|
text: JSON.stringify(payload),
|
|
826
|
-
}
|
|
1053
|
+
},
|
|
1054
|
+
],
|
|
827
1055
|
};
|
|
828
1056
|
}
|
|
829
1057
|
catch (error) {
|
|
830
1058
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
831
1059
|
return createErrorResponse(`Failed to simulate input: ${errorMessage}`, [
|
|
832
1060
|
'Ensure the project is running (use run_project first)',
|
|
833
|
-
'The bridge may not be ready yet —
|
|
1061
|
+
'The bridge may not be ready yet — use get_debug_output to investigate',
|
|
834
1062
|
'Check that UDP port 9900 is not blocked',
|
|
835
1063
|
]);
|
|
836
1064
|
}
|
|
@@ -852,10 +1080,15 @@ export async function handleGetUiElements(runner, args) {
|
|
|
852
1080
|
parsed = JSON.parse(responseStr);
|
|
853
1081
|
}
|
|
854
1082
|
catch {
|
|
855
|
-
return createErrorResponse(`Invalid response from bridge: ${responseStr}`, [
|
|
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
|
+
]);
|
|
856
1087
|
}
|
|
857
1088
|
if (parsed.error) {
|
|
858
|
-
return createErrorResponse(`UI element query error: ${parsed.error}`, [
|
|
1089
|
+
return createErrorResponse(`UI element query error: ${parsed.error}`, [
|
|
1090
|
+
'Ensure the game has a UI with Control nodes',
|
|
1091
|
+
]);
|
|
859
1092
|
}
|
|
860
1093
|
const payload = {
|
|
861
1094
|
...parsed,
|
|
@@ -865,17 +1098,19 @@ export async function handleGetUiElements(runner, args) {
|
|
|
865
1098
|
payload.warnings = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES);
|
|
866
1099
|
}
|
|
867
1100
|
return {
|
|
868
|
-
content: [
|
|
1101
|
+
content: [
|
|
1102
|
+
{
|
|
869
1103
|
type: 'text',
|
|
870
1104
|
text: JSON.stringify(payload),
|
|
871
|
-
}
|
|
1105
|
+
},
|
|
1106
|
+
],
|
|
872
1107
|
};
|
|
873
1108
|
}
|
|
874
1109
|
catch (error) {
|
|
875
1110
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
876
1111
|
return createErrorResponse(`Failed to get UI elements: ${errorMessage}`, [
|
|
877
1112
|
'Ensure the project is running (use run_project first)',
|
|
878
|
-
'The bridge may not be ready yet —
|
|
1113
|
+
'The bridge may not be ready yet — use get_debug_output to investigate',
|
|
879
1114
|
'Check that UDP port 9900 is not blocked',
|
|
880
1115
|
]);
|
|
881
1116
|
}
|
|
@@ -888,7 +1123,9 @@ export async function handleRunScript(runner, args) {
|
|
|
888
1123
|
}
|
|
889
1124
|
const script = args.script;
|
|
890
1125
|
if (typeof script !== 'string' || script.trim() === '') {
|
|
891
|
-
return createErrorResponse('script is required and must be a non-empty string', [
|
|
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
|
+
]);
|
|
892
1129
|
}
|
|
893
1130
|
if (!script.includes('func execute')) {
|
|
894
1131
|
return createErrorResponse('Script must define func execute(scene_tree: SceneTree) -> Variant', ['Add a func execute(scene_tree: SceneTree) -> Variant method to your script']);
|
|
@@ -916,10 +1153,17 @@ export async function handleRunScript(runner, args) {
|
|
|
916
1153
|
parsed = JSON.parse(responseStr);
|
|
917
1154
|
}
|
|
918
1155
|
catch {
|
|
919
|
-
return createErrorResponse(`Invalid response from bridge: ${responseStr}`, [
|
|
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
|
+
]);
|
|
920
1160
|
}
|
|
921
1161
|
if (parsed.error) {
|
|
922
|
-
return createErrorResponse(`Script execution error: ${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
|
+
]);
|
|
923
1167
|
}
|
|
924
1168
|
// Detect false-positive success: GDScript has no try-catch, so runtime errors
|
|
925
1169
|
// return null and the real error only appears in stderr.
|
|
@@ -932,7 +1176,8 @@ export async function handleRunScript(runner, args) {
|
|
|
932
1176
|
]);
|
|
933
1177
|
}
|
|
934
1178
|
return {
|
|
935
|
-
content: [
|
|
1179
|
+
content: [
|
|
1180
|
+
{
|
|
936
1181
|
type: 'text',
|
|
937
1182
|
text: JSON.stringify({
|
|
938
1183
|
success: true,
|
|
@@ -940,7 +1185,8 @@ export async function handleRunScript(runner, args) {
|
|
|
940
1185
|
warning: 'Script returned null. If unexpected, check get_debug_output for runtime errors — GDScript does not propagate exceptions.',
|
|
941
1186
|
tip: 'Call take_screenshot to verify any visual changes, or get_debug_output to review print() output from your script.',
|
|
942
1187
|
}),
|
|
943
|
-
}
|
|
1188
|
+
},
|
|
1189
|
+
],
|
|
944
1190
|
};
|
|
945
1191
|
}
|
|
946
1192
|
const payload = {
|
|
@@ -952,10 +1198,12 @@ export async function handleRunScript(runner, args) {
|
|
|
952
1198
|
payload.warnings = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES);
|
|
953
1199
|
}
|
|
954
1200
|
return {
|
|
955
|
-
content: [
|
|
1201
|
+
content: [
|
|
1202
|
+
{
|
|
956
1203
|
type: 'text',
|
|
957
1204
|
text: JSON.stringify(payload),
|
|
958
|
-
}
|
|
1205
|
+
},
|
|
1206
|
+
],
|
|
959
1207
|
};
|
|
960
1208
|
}
|
|
961
1209
|
catch (error) {
|
|
@@ -1103,7 +1351,9 @@ export async function handleListAutoloads(args) {
|
|
|
1103
1351
|
}
|
|
1104
1352
|
catch (error) {
|
|
1105
1353
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1106
|
-
return createErrorResponse(`Failed to list autoloads: ${errorMessage}`, [
|
|
1354
|
+
return createErrorResponse(`Failed to list autoloads: ${errorMessage}`, [
|
|
1355
|
+
'Check if project.godot is accessible',
|
|
1356
|
+
]);
|
|
1107
1357
|
}
|
|
1108
1358
|
}
|
|
1109
1359
|
export async function handleAddAutoload(args) {
|
|
@@ -1112,7 +1362,9 @@ export async function handleAddAutoload(args) {
|
|
|
1112
1362
|
if ('isError' in v)
|
|
1113
1363
|
return v;
|
|
1114
1364
|
if (!args.autoloadName || !args.autoloadPath) {
|
|
1115
|
-
return createErrorResponse('autoloadName and autoloadPath are required', [
|
|
1365
|
+
return createErrorResponse('autoloadName and autoloadPath are required', [
|
|
1366
|
+
'Provide the autoload node name and script/scene path',
|
|
1367
|
+
]);
|
|
1116
1368
|
}
|
|
1117
1369
|
if (!validatePath(args.autoloadPath)) {
|
|
1118
1370
|
return createErrorResponse('Invalid autoload path', ['Provide a valid path without ".."']);
|
|
@@ -1120,18 +1372,28 @@ export async function handleAddAutoload(args) {
|
|
|
1120
1372
|
try {
|
|
1121
1373
|
const projectFile = join(v.projectPath, 'project.godot');
|
|
1122
1374
|
const existing = parseAutoloads(projectFile);
|
|
1123
|
-
if (existing.some(a => a.name === args.autoloadName)) {
|
|
1124
|
-
return createErrorResponse(`Autoload '${args.autoloadName}' already exists`, [
|
|
1375
|
+
if (existing.some((a) => a.name === args.autoloadName)) {
|
|
1376
|
+
return createErrorResponse(`Autoload '${args.autoloadName}' already exists`, [
|
|
1377
|
+
'Use update_autoload to modify it',
|
|
1378
|
+
'Use list_autoloads to see current autoloads',
|
|
1379
|
+
]);
|
|
1125
1380
|
}
|
|
1126
1381
|
const isSingleton = args.singleton !== false;
|
|
1127
1382
|
addAutoloadEntry(projectFile, args.autoloadName, args.autoloadPath, isSingleton);
|
|
1128
1383
|
return {
|
|
1129
|
-
content: [
|
|
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
|
+
],
|
|
1130
1390
|
};
|
|
1131
1391
|
}
|
|
1132
1392
|
catch (error) {
|
|
1133
1393
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1134
|
-
return createErrorResponse(`Failed to add autoload: ${errorMessage}`, [
|
|
1394
|
+
return createErrorResponse(`Failed to add autoload: ${errorMessage}`, [
|
|
1395
|
+
'Check if project.godot is accessible',
|
|
1396
|
+
]);
|
|
1135
1397
|
}
|
|
1136
1398
|
}
|
|
1137
1399
|
export async function handleRemoveAutoload(args) {
|
|
@@ -1140,19 +1402,27 @@ export async function handleRemoveAutoload(args) {
|
|
|
1140
1402
|
if ('isError' in v)
|
|
1141
1403
|
return v;
|
|
1142
1404
|
if (!args.autoloadName) {
|
|
1143
|
-
return createErrorResponse('autoloadName is required', [
|
|
1405
|
+
return createErrorResponse('autoloadName is required', [
|
|
1406
|
+
'Provide the name of the autoload to remove',
|
|
1407
|
+
]);
|
|
1144
1408
|
}
|
|
1145
1409
|
try {
|
|
1146
1410
|
const projectFile = join(v.projectPath, 'project.godot');
|
|
1147
1411
|
const removed = removeAutoloadEntry(projectFile, args.autoloadName);
|
|
1148
1412
|
if (!removed) {
|
|
1149
|
-
return createErrorResponse(`Autoload '${args.autoloadName}' not found`, [
|
|
1413
|
+
return createErrorResponse(`Autoload '${args.autoloadName}' not found`, [
|
|
1414
|
+
'Use list_autoloads to see existing autoloads',
|
|
1415
|
+
]);
|
|
1150
1416
|
}
|
|
1151
|
-
return {
|
|
1417
|
+
return {
|
|
1418
|
+
content: [{ type: 'text', text: `Autoload '${args.autoloadName}' removed successfully.` }],
|
|
1419
|
+
};
|
|
1152
1420
|
}
|
|
1153
1421
|
catch (error) {
|
|
1154
1422
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1155
|
-
return createErrorResponse(`Failed to remove autoload: ${errorMessage}`, [
|
|
1423
|
+
return createErrorResponse(`Failed to remove autoload: ${errorMessage}`, [
|
|
1424
|
+
'Check if project.godot is accessible',
|
|
1425
|
+
]);
|
|
1156
1426
|
}
|
|
1157
1427
|
}
|
|
1158
1428
|
export async function handleUpdateAutoload(args) {
|
|
@@ -1161,7 +1431,9 @@ export async function handleUpdateAutoload(args) {
|
|
|
1161
1431
|
if ('isError' in v)
|
|
1162
1432
|
return v;
|
|
1163
1433
|
if (!args.autoloadName) {
|
|
1164
|
-
return createErrorResponse('autoloadName is required', [
|
|
1434
|
+
return createErrorResponse('autoloadName is required', [
|
|
1435
|
+
'Provide the name of the autoload to update',
|
|
1436
|
+
]);
|
|
1165
1437
|
}
|
|
1166
1438
|
if (args.autoloadPath && !validatePath(args.autoloadPath)) {
|
|
1167
1439
|
return createErrorResponse('Invalid autoload path', ['Provide a valid path without ".."']);
|
|
@@ -1170,13 +1442,20 @@ export async function handleUpdateAutoload(args) {
|
|
|
1170
1442
|
const projectFile = join(v.projectPath, 'project.godot');
|
|
1171
1443
|
const updated = updateAutoloadEntry(projectFile, args.autoloadName, args.autoloadPath, args.singleton);
|
|
1172
1444
|
if (!updated) {
|
|
1173
|
-
return createErrorResponse(`Autoload '${args.autoloadName}' not found`, [
|
|
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
|
+
]);
|
|
1174
1449
|
}
|
|
1175
|
-
return {
|
|
1450
|
+
return {
|
|
1451
|
+
content: [{ type: 'text', text: `Autoload '${args.autoloadName}' updated successfully.` }],
|
|
1452
|
+
};
|
|
1176
1453
|
}
|
|
1177
1454
|
catch (error) {
|
|
1178
1455
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1179
|
-
return createErrorResponse(`Failed to update autoload: ${errorMessage}`, [
|
|
1456
|
+
return createErrorResponse(`Failed to update autoload: ${errorMessage}`, [
|
|
1457
|
+
'Check if project.godot is accessible',
|
|
1458
|
+
]);
|
|
1180
1459
|
}
|
|
1181
1460
|
}
|
|
1182
1461
|
export async function handleGetProjectFiles(args) {
|
|
@@ -1187,14 +1466,16 @@ export async function handleGetProjectFiles(args) {
|
|
|
1187
1466
|
try {
|
|
1188
1467
|
const maxDepth = typeof args.maxDepth === 'number' ? args.maxDepth : -1;
|
|
1189
1468
|
const extensions = Array.isArray(args.extensions)
|
|
1190
|
-
? args.extensions.map(e => e.toLowerCase().replace(/^\./, ''))
|
|
1469
|
+
? args.extensions.map((e) => e.toLowerCase().replace(/^\./, ''))
|
|
1191
1470
|
: null;
|
|
1192
1471
|
const tree = buildFilesystemTree(v.projectPath, '', maxDepth, 0, extensions);
|
|
1193
1472
|
return { content: [{ type: 'text', text: JSON.stringify(tree) }] };
|
|
1194
1473
|
}
|
|
1195
1474
|
catch (error) {
|
|
1196
1475
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1197
|
-
return createErrorResponse(`Failed to get project files: ${errorMessage}`, [
|
|
1476
|
+
return createErrorResponse(`Failed to get project files: ${errorMessage}`, [
|
|
1477
|
+
'Check if the project directory is accessible',
|
|
1478
|
+
]);
|
|
1198
1479
|
}
|
|
1199
1480
|
}
|
|
1200
1481
|
export async function handleSearchProject(args) {
|
|
@@ -1207,7 +1488,7 @@ export async function handleSearchProject(args) {
|
|
|
1207
1488
|
}
|
|
1208
1489
|
try {
|
|
1209
1490
|
const fileTypes = Array.isArray(args.fileTypes)
|
|
1210
|
-
? args.fileTypes.map(e => e.toLowerCase().replace(/^\./, ''))
|
|
1491
|
+
? args.fileTypes.map((e) => e.toLowerCase().replace(/^\./, ''))
|
|
1211
1492
|
: ['gd', 'tscn', 'cs', 'gdshader'];
|
|
1212
1493
|
const caseSensitive = args.caseSensitive === true;
|
|
1213
1494
|
const maxResults = typeof args.maxResults === 'number' ? args.maxResults : 100;
|
|
@@ -1216,7 +1497,9 @@ export async function handleSearchProject(args) {
|
|
|
1216
1497
|
}
|
|
1217
1498
|
catch (error) {
|
|
1218
1499
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1219
|
-
return createErrorResponse(`Failed to search project: ${errorMessage}`, [
|
|
1500
|
+
return createErrorResponse(`Failed to search project: ${errorMessage}`, [
|
|
1501
|
+
'Check if the project directory is accessible',
|
|
1502
|
+
]);
|
|
1220
1503
|
}
|
|
1221
1504
|
}
|
|
1222
1505
|
export async function handleGetSceneDependencies(args) {
|
|
@@ -1225,7 +1508,9 @@ export async function handleGetSceneDependencies(args) {
|
|
|
1225
1508
|
if ('isError' in v)
|
|
1226
1509
|
return v;
|
|
1227
1510
|
if (!args.scenePath || typeof args.scenePath !== 'string') {
|
|
1228
|
-
return createErrorResponse('scenePath is required', [
|
|
1511
|
+
return createErrorResponse('scenePath is required', [
|
|
1512
|
+
'Provide a path relative to the project root, e.g. "scenes/main.tscn"',
|
|
1513
|
+
]);
|
|
1229
1514
|
}
|
|
1230
1515
|
if (!validatePath(args.scenePath)) {
|
|
1231
1516
|
return createErrorResponse('Invalid scenePath', ['Provide a valid path without ".."']);
|
|
@@ -1233,7 +1518,10 @@ export async function handleGetSceneDependencies(args) {
|
|
|
1233
1518
|
try {
|
|
1234
1519
|
const sceneFullPath = join(v.projectPath, args.scenePath);
|
|
1235
1520
|
if (!existsSync(sceneFullPath)) {
|
|
1236
|
-
return createErrorResponse(`Scene file not found: ${args.scenePath}`, [
|
|
1521
|
+
return createErrorResponse(`Scene file not found: ${args.scenePath}`, [
|
|
1522
|
+
'Verify the path is relative to the project root',
|
|
1523
|
+
'Use get_project_files to list available .tscn files',
|
|
1524
|
+
]);
|
|
1237
1525
|
}
|
|
1238
1526
|
const sceneContent = readFileSync(sceneFullPath, 'utf8');
|
|
1239
1527
|
const dependencies = [];
|
|
@@ -1255,11 +1543,15 @@ export async function handleGetSceneDependencies(args) {
|
|
|
1255
1543
|
dependencies.push(dep);
|
|
1256
1544
|
}
|
|
1257
1545
|
}
|
|
1258
|
-
return {
|
|
1546
|
+
return {
|
|
1547
|
+
content: [{ type: 'text', text: JSON.stringify({ scene: args.scenePath, dependencies }) }],
|
|
1548
|
+
};
|
|
1259
1549
|
}
|
|
1260
1550
|
catch (error) {
|
|
1261
1551
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1262
|
-
return createErrorResponse(`Failed to get scene dependencies: ${errorMessage}`, [
|
|
1552
|
+
return createErrorResponse(`Failed to get scene dependencies: ${errorMessage}`, [
|
|
1553
|
+
'Check if the scene file is accessible',
|
|
1554
|
+
]);
|
|
1263
1555
|
}
|
|
1264
1556
|
}
|
|
1265
1557
|
export async function handleGetProjectSettings(args) {
|
|
@@ -1278,7 +1570,9 @@ export async function handleGetProjectSettings(args) {
|
|
|
1278
1570
|
}
|
|
1279
1571
|
catch (error) {
|
|
1280
1572
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1281
|
-
return createErrorResponse(`Failed to get project settings: ${errorMessage}`, [
|
|
1573
|
+
return createErrorResponse(`Failed to get project settings: ${errorMessage}`, [
|
|
1574
|
+
'Check if project.godot is accessible',
|
|
1575
|
+
]);
|
|
1282
1576
|
}
|
|
1283
1577
|
}
|
|
1284
1578
|
//# sourceMappingURL=project-tools.js.map
|