godot-mcp-runtime 2.2.0 → 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.
@@ -1,6 +1,7 @@
1
1
  import { join, basename, sep } from 'path';
2
2
  import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
3
3
  import { normalizeParameters, validatePath, validateProjectArgs, createErrorResponse, logDebug, } from '../utils/godot-runner.js';
4
+ const MAX_RUNTIME_ERROR_CONTEXT_LINES = 30;
4
5
  function parseAutoloads(projectFilePath) {
5
6
  const content = readFileSync(projectFilePath, 'utf8');
6
7
  const autoloads = [];
@@ -27,7 +28,7 @@ function addAutoloadEntry(projectFilePath, name, path, singleton) {
27
28
  const content = readFileSync(projectFilePath, 'utf8');
28
29
  const lines = content.split('\n');
29
30
  const entry = `${name}="${singleton ? '*' : ''}${normalizeAutoloadPath(path)}"`;
30
- const sectionIdx = lines.findIndex(l => l.trim() === '[autoload]');
31
+ const sectionIdx = lines.findIndex((l) => l.trim() === '[autoload]');
31
32
  if (sectionIdx === -1) {
32
33
  writeFileSync(projectFilePath, content.trimEnd() + '\n\n[autoload]\n' + entry + '\n', 'utf8');
33
34
  return;
@@ -44,7 +45,7 @@ function removeAutoloadEntry(projectFilePath, name) {
44
45
  const lines = content.split('\n');
45
46
  let inAutoloadSection = false;
46
47
  let removed = false;
47
- const newLines = lines.filter(line => {
48
+ const newLines = lines.filter((line) => {
48
49
  const trimmed = line.trim();
49
50
  if (trimmed.startsWith('[')) {
50
51
  inAutoloadSection = trimmed === '[autoload]';
@@ -68,7 +69,7 @@ function updateAutoloadEntry(projectFilePath, name, newPath, singleton) {
68
69
  const lines = content.split('\n');
69
70
  let inAutoloadSection = false;
70
71
  let updated = false;
71
- const newLines = lines.map(line => {
72
+ const newLines = lines.map((line) => {
72
73
  const trimmed = line.trim();
73
74
  if (trimmed.startsWith('[')) {
74
75
  inAutoloadSection = trimmed === '[autoload]';
@@ -107,7 +108,7 @@ export const projectToolDefinitions = [
107
108
  },
108
109
  {
109
110
  name: 'run_project',
110
- description: 'Run a Godot project in debug mode. 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. After starting, wait 2–3 seconds for the MCP bridge to initialize before using runtime tools. Call stop_project when done.',
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.',
111
112
  inputSchema: {
112
113
  type: 'object',
113
114
  properties: {
@@ -129,7 +130,7 @@ export const projectToolDefinitions = [
129
130
  },
130
131
  {
131
132
  name: 'attach_project',
132
- 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.',
133
134
  inputSchema: {
134
135
  type: 'object',
135
136
  properties: {
@@ -137,6 +138,10 @@ export const projectToolDefinitions = [
137
138
  type: 'string',
138
139
  description: 'Path to the Godot project directory',
139
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
+ },
140
145
  },
141
146
  required: ['projectPath'],
142
147
  },
@@ -207,7 +212,7 @@ export const projectToolDefinitions = [
207
212
  },
208
213
  {
209
214
  name: 'take_screenshot',
210
- description: 'Capture a PNG screenshot of the running Godot viewport. Requires run_project first; wait 2–3 seconds after starting for the bridge to initialize. Returns the image inline. Screenshots are also saved to .mcp/screenshots/ in the project directory.',
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.',
211
216
  inputSchema: {
212
217
  type: 'object',
213
218
  properties: {
@@ -221,7 +226,7 @@ export const projectToolDefinitions = [
221
226
  },
222
227
  {
223
228
  name: 'simulate_input',
224
- description: 'Simulate batched sequential input in a running Godot project. Requires run_project first; wait 2–3 seconds after starting. 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}]',
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}]',
225
230
  inputSchema: {
226
231
  type: 'object',
227
232
  properties: {
@@ -236,21 +241,58 @@ export const projectToolDefinitions = [
236
241
  enum: ['key', 'mouse_button', 'mouse_motion', 'click_element', 'action', 'wait'],
237
242
  description: 'The type of input action',
238
243
  },
239
- key: { type: 'string', description: '[key] Key name (e.g. "W", "Space", "Escape", "Up")' },
240
- pressed: { type: 'boolean', description: '[key, mouse_button, action] Whether the input is pressed (true) or released (false)' },
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
+ },
241
252
  shift: { type: 'boolean', description: '[key] Shift modifier' },
242
253
  ctrl: { type: 'boolean', description: '[key] Ctrl modifier' },
243
254
  alt: { type: 'boolean', description: '[key] Alt modifier' },
244
- button: { type: 'string', enum: ['left', 'right', 'middle'], description: '[mouse_button, click_element] Mouse button (default: left)' },
245
- x: { type: 'number', description: '[mouse_button, mouse_motion] X position in viewport pixels' },
246
- y: { type: 'number', description: '[mouse_button, mouse_motion] Y position in viewport pixels' },
247
- relative_x: { type: 'number', description: '[mouse_motion] Relative X movement in pixels' },
248
- relative_y: { type: 'number', description: '[mouse_motion] Relative Y movement in pixels' },
249
- double_click: { type: 'boolean', description: '[mouse_button, click_element] Double click' },
250
- element: { type: 'string', 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.' },
251
- action: { type: 'string', description: '[action] Godot input action name (as defined in Project Settings > Input Map)' },
252
- strength: { type: 'number', description: '[action] Action strength (0–1, default 1.0)' },
253
- ms: { type: 'number', description: '[wait] Duration in milliseconds to pause before the next action' },
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
+ },
254
296
  },
255
297
  required: ['type'],
256
298
  },
@@ -261,7 +303,7 @@ export const projectToolDefinitions = [
261
303
  },
262
304
  {
263
305
  name: 'get_ui_elements',
264
- description: 'Get Control nodes from a running Godot project with their positions, sizes, types, and text. Requires run_project first; wait 2–3 seconds after starting. 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? }] }',
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? }] }',
265
307
  inputSchema: {
266
308
  type: 'object',
267
309
  properties: {
@@ -279,7 +321,7 @@ export const projectToolDefinitions = [
279
321
  },
280
322
  {
281
323
  name: 'run_script',
282
- 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.',
324
+ description: 'Execute a custom GDScript in the live running project with full scene tree access. Requires run_project first. Script must extend RefCounted and define func execute(scene_tree: SceneTree) -> Variant. Return values are JSON-serialized (primitives, Vector2/3, Color, Dictionary, Array, and Node path strings are supported). Use print() for debug output — it appears in get_debug_output, not in the script result. In spawned mode, runtime errors emitted to stderr are detected and either escalated (when the script returns null) or surfaced as warnings. In attached mode a null result includes a caveat since stderr is not captured.',
283
325
  inputSchema: {
284
326
  type: 'object',
285
327
  properties: {
@@ -313,9 +355,18 @@ export const projectToolDefinitions = [
313
355
  type: 'object',
314
356
  properties: {
315
357
  projectPath: { type: 'string', description: 'Path to the Godot project directory' },
316
- autoloadName: { type: 'string', description: 'Name of the autoload node (e.g. "MyManager")' },
317
- autoloadPath: { type: 'string', description: 'Path to the script or scene (e.g. "res://autoload/my_manager.gd" or "autoload/my_manager.gd")' },
318
- singleton: { type: 'boolean', description: 'Register as a globally accessible singleton by name (default: true)' },
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
+ },
319
370
  },
320
371
  required: ['projectPath', 'autoloadName', 'autoloadPath'],
321
372
  },
@@ -334,7 +385,7 @@ export const projectToolDefinitions = [
334
385
  },
335
386
  {
336
387
  name: 'update_autoload',
337
- description: 'Modify an existing autoload\'s path or singleton flag. No Godot process required.',
388
+ description: "Modify an existing autoload's path or singleton flag. No Godot process required.",
338
389
  inputSchema: {
339
390
  type: 'object',
340
391
  properties: {
@@ -353,8 +404,15 @@ export const projectToolDefinitions = [
353
404
  type: 'object',
354
405
  properties: {
355
406
  projectPath: { type: 'string', description: 'Path to the Godot project directory' },
356
- maxDepth: { type: 'number', description: 'Maximum recursion depth. -1 means unlimited (default: -1)' },
357
- extensions: { type: 'array', items: { type: 'string' }, description: 'Filter to only these file extensions (e.g. ["gd", "tscn"]). Omit to include all.' },
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
+ },
358
416
  },
359
417
  required: ['projectPath'],
360
418
  },
@@ -367,7 +425,11 @@ export const projectToolDefinitions = [
367
425
  properties: {
368
426
  projectPath: { type: 'string', description: 'Path to the Godot project directory' },
369
427
  pattern: { type: 'string', description: 'Plain-text string to search for' },
370
- fileTypes: { type: 'array', items: { type: 'string' }, description: 'File extensions to search (default: ["gd", "tscn", "cs", "gdshader"])' },
428
+ fileTypes: {
429
+ type: 'array',
430
+ items: { type: 'string' },
431
+ description: 'File extensions to search (default: ["gd", "tscn", "cs", "gdshader"])',
432
+ },
371
433
  caseSensitive: { type: 'boolean', description: 'Case-sensitive search (default: false)' },
372
434
  maxResults: { type: 'number', description: 'Maximum matches to return (default: 100)' },
373
435
  },
@@ -381,7 +443,10 @@ export const projectToolDefinitions = [
381
443
  type: 'object',
382
444
  properties: {
383
445
  projectPath: { type: 'string', description: 'Path to the Godot project directory' },
384
- scenePath: { type: 'string', description: 'Path to the .tscn file relative to the project root (e.g. "scenes/main.tscn")' },
446
+ scenePath: {
447
+ type: 'string',
448
+ description: 'Path to the .tscn file relative to the project root (e.g. "scenes/main.tscn")',
449
+ },
385
450
  },
386
451
  required: ['projectPath', 'scenePath'],
387
452
  },
@@ -393,7 +458,10 @@ export const projectToolDefinitions = [
393
458
  type: 'object',
394
459
  properties: {
395
460
  projectPath: { type: 'string', description: 'Path to the Godot project directory' },
396
- section: { type: 'string', description: 'Filter to a specific INI section (e.g. "display", "application"). Omit for all sections.' },
461
+ section: {
462
+ type: 'string',
463
+ description: 'Filter to a specific INI section (e.g. "display", "application"). Omit for all sections.',
464
+ },
397
465
  },
398
466
  required: ['projectPath'],
399
467
  },
@@ -401,10 +469,17 @@ export const projectToolDefinitions = [
401
469
  ];
402
470
  function ensureRuntimeSession(runner, actionDescription) {
403
471
  if (!runner.activeSessionMode || !runner.activeProjectPath) {
404
- return createErrorResponse(`No active runtime session. A project must be running or attached to ${actionDescription}.`, ['Use run_project to start a Godot project first', 'Or use attach_project before launching Godot manually']);
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
+ ]);
405
476
  }
406
- if (runner.activeSessionMode === 'spawned' && (!runner.activeProcess || runner.activeProcess.hasExited)) {
407
- return createErrorResponse(`The spawned Godot process has exited and cannot ${actionDescription}.`, ['Use get_debug_output to inspect the last captured logs', 'Call stop_project to clean up, then run_project again']);
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
+ ]);
408
483
  }
409
484
  return null;
410
485
  }
@@ -481,21 +556,31 @@ function getProjectStructure(projectPath) {
481
556
  export async function handleLaunchEditor(runner, args) {
482
557
  args = normalizeParameters(args);
483
558
  if (!args.projectPath) {
484
- return createErrorResponse('Project path is required', ['Provide a valid path to a Godot project directory']);
559
+ return createErrorResponse('Project path is required', [
560
+ 'Provide a valid path to a Godot project directory',
561
+ ]);
485
562
  }
486
563
  if (!validatePath(args.projectPath)) {
487
- return createErrorResponse('Invalid project path', ['Provide a valid path without ".." or other potentially unsafe characters']);
564
+ return createErrorResponse('Invalid project path', [
565
+ 'Provide a valid path without ".." or other potentially unsafe characters',
566
+ ]);
488
567
  }
489
568
  try {
490
569
  if (!runner.getGodotPath()) {
491
570
  await runner.detectGodotPath();
492
571
  if (!runner.getGodotPath()) {
493
- return createErrorResponse('Could not find a valid Godot executable path', ['Ensure Godot is installed correctly', 'Set GODOT_PATH environment variable']);
572
+ return createErrorResponse('Could not find a valid Godot executable path', [
573
+ 'Ensure Godot is installed correctly',
574
+ 'Set GODOT_PATH environment variable',
575
+ ]);
494
576
  }
495
577
  }
496
578
  const projectFile = join(args.projectPath, 'project.godot');
497
579
  if (!existsSync(projectFile)) {
498
- return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, ['Ensure the path points to a directory containing a project.godot file', 'Use list_projects to find valid Godot projects']);
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
+ ]);
499
584
  }
500
585
  logDebug(`Launching Godot editor for project: ${args.projectPath}`);
501
586
  const process = runner.launchEditor(args.projectPath);
@@ -503,34 +588,75 @@ export async function handleLaunchEditor(runner, args) {
503
588
  console.error('Failed to start Godot editor:', err);
504
589
  });
505
590
  return {
506
- content: [{ type: 'text', 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.` }],
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
+ ],
507
597
  };
508
598
  }
509
599
  catch (error) {
510
600
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
511
- return createErrorResponse(`Failed to launch Godot editor: ${errorMessage}`, ['Ensure Godot is installed correctly', 'Check if the GODOT_PATH environment variable is set correctly']);
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
+ ]);
512
605
  }
513
606
  }
514
607
  export async function handleRunProject(runner, args) {
515
608
  args = normalizeParameters(args);
516
609
  if (!args.projectPath) {
517
- return createErrorResponse('Project path is required', ['Provide a valid path to a Godot project directory']);
610
+ return createErrorResponse('Project path is required', [
611
+ 'Provide a valid path to a Godot project directory',
612
+ ]);
518
613
  }
519
614
  if (!validatePath(args.projectPath)) {
520
- return createErrorResponse('Invalid project path', ['Provide a valid path without ".." or other potentially unsafe characters']);
615
+ return createErrorResponse('Invalid project path', [
616
+ 'Provide a valid path without ".." or other potentially unsafe characters',
617
+ ]);
521
618
  }
522
619
  try {
523
620
  const projectFile = join(args.projectPath, 'project.godot');
524
621
  if (!existsSync(projectFile)) {
525
- return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, ['Ensure the path points to a directory containing a project.godot file', 'Use list_projects to find valid Godot projects']);
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
+ ]);
526
626
  }
527
627
  const background = args.background === true;
528
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
+ }
529
655
  const lines = [
530
- 'Godot project started in debug mode.',
656
+ 'Godot project started and MCP bridge is ready.',
657
+ '- Runtime tools (take_screenshot, simulate_input, get_ui_elements, run_script) are available now',
531
658
  '- Use get_debug_output to check runtime output and errors',
532
- '- Wait 2–3 seconds before calling take_screenshot, simulate_input, get_ui_elements, or run_script (bridge needs time to initialize)',
533
- '- Always call stop_project when done — it terminates the process and cleans up the MCP bridge',
659
+ '- Call stop_project when done',
534
660
  ];
535
661
  if (background) {
536
662
  lines.push('- Background mode: window hidden, physical input blocked');
@@ -541,64 +667,112 @@ export async function handleRunProject(runner, args) {
541
667
  }
542
668
  catch (error) {
543
669
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
544
- return createErrorResponse(`Failed to run Godot project: ${errorMessage}`, ['Ensure Godot is installed correctly', 'Check if the GODOT_PATH environment variable is set correctly']);
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
+ ]);
545
674
  }
546
675
  }
547
676
  export async function handleAttachProject(runner, args) {
548
677
  args = normalizeParameters(args);
549
678
  if (!args.projectPath) {
550
- return createErrorResponse('Project path is required', ['Provide a valid path to a Godot project directory']);
679
+ return createErrorResponse('Project path is required', [
680
+ 'Provide a valid path to a Godot project directory',
681
+ ]);
551
682
  }
552
683
  if (!validatePath(args.projectPath)) {
553
- return createErrorResponse('Invalid project path', ['Provide a valid path without ".." or other potentially unsafe characters']);
684
+ return createErrorResponse('Invalid project path', [
685
+ 'Provide a valid path without ".." or other potentially unsafe characters',
686
+ ]);
554
687
  }
555
688
  try {
556
689
  const projectFile = join(args.projectPath, 'project.godot');
557
690
  if (!existsSync(projectFile)) {
558
- return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, ['Ensure the path points to a directory containing a project.godot file', 'Use list_projects to find valid Godot projects']);
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
+ ]);
559
695
  }
560
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
+ }
561
721
  return {
562
- content: [{
722
+ content: [
723
+ {
563
724
  type: 'text',
564
725
  text: [
565
726
  'Project attached for manual runtime use.',
566
- '- Launch Godot yourself after this call so the injected McpBridge can initialize',
567
- '- Wait 2–3 seconds after launch before calling take_screenshot, simulate_input, get_ui_elements, or run_script',
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',
568
729
  '- get_debug_output is unavailable in attached mode because MCP did not spawn the process',
569
730
  '- Use detach_project or stop_project when done to clean up the injected bridge state',
570
731
  ].join('\n'),
571
- }],
732
+ },
733
+ ],
572
734
  };
573
735
  }
574
736
  catch (error) {
575
737
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
576
- return createErrorResponse(`Failed to attach project: ${errorMessage}`, ['Check if project.godot is accessible', 'Ensure MCP can write the bridge autoload into the project']);
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
+ ]);
577
742
  }
578
743
  }
579
744
  export function handleDetachProject(runner) {
580
745
  if (runner.activeSessionMode !== 'attached') {
581
- return createErrorResponse('No attached project to detach.', ['Use attach_project first for manual-launch workflows', 'If MCP launched the game, use stop_project instead']);
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
+ ]);
582
750
  }
583
751
  const result = runner.stopProject();
584
752
  return {
585
- content: [{
753
+ content: [
754
+ {
586
755
  type: 'text',
587
756
  text: JSON.stringify({
588
757
  message: 'Detached attached project and cleaned MCP bridge state',
589
758
  externalProcessPreserved: result.externalProcessPreserved === true,
590
759
  }),
591
- }],
760
+ },
761
+ ],
592
762
  };
593
763
  }
594
764
  export function handleGetDebugOutput(runner, args = {}) {
595
765
  args = normalizeParameters(args);
596
766
  if (!runner.activeSessionMode) {
597
- return createErrorResponse('No active runtime session.', ['Use run_project to start a Godot project first', 'Or use attach_project before launching Godot manually']);
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
+ ]);
598
771
  }
599
772
  if (runner.activeSessionMode === 'attached') {
600
773
  return {
601
- content: [{
774
+ content: [
775
+ {
602
776
  type: 'text',
603
777
  text: JSON.stringify({
604
778
  output: [],
@@ -607,12 +781,16 @@ export function handleGetDebugOutput(runner, args = {}) {
607
781
  attached: true,
608
782
  tip: 'Attached mode does not capture stdout/stderr because Godot was launched outside MCP.',
609
783
  }),
610
- }],
784
+ },
785
+ ],
611
786
  };
612
787
  }
613
788
  const proc = runner.activeProcess;
614
789
  if (!proc) {
615
- return createErrorResponse('No active spawned process is available for debug output.', ['Use run_project to start a Godot project first', 'Or use attach_project only when stdout/stderr capture is not needed']);
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
+ ]);
616
794
  }
617
795
  const limit = typeof args.limit === 'number' ? args.limit : 200;
618
796
  const response = {
@@ -622,22 +800,29 @@ export function handleGetDebugOutput(runner, args = {}) {
622
800
  };
623
801
  if (proc.hasExited) {
624
802
  response.exitCode = proc.exitCode;
625
- response.tip = 'Process has exited. Call stop_project to clean up the process slot before starting a new one.';
803
+ response.tip =
804
+ 'Process has exited. Call stop_project to clean up the process slot before starting a new one.';
626
805
  }
627
806
  return {
628
- content: [{
807
+ content: [
808
+ {
629
809
  type: 'text',
630
810
  text: JSON.stringify(response),
631
- }],
811
+ },
812
+ ],
632
813
  };
633
814
  }
634
815
  export function handleStopProject(runner) {
635
816
  const result = runner.stopProject();
636
817
  if (!result) {
637
- return createErrorResponse('No active Godot process to stop.', ['Use run_project to start a Godot project first', 'The process may have already terminated']);
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
+ ]);
638
822
  }
639
823
  return {
640
- content: [{
824
+ content: [
825
+ {
641
826
  type: 'text',
642
827
  text: JSON.stringify({
643
828
  message: result.mode === 'attached'
@@ -648,20 +833,27 @@ export function handleStopProject(runner) {
648
833
  finalOutput: result.output.slice(-200),
649
834
  finalErrors: result.errors.slice(-200),
650
835
  }),
651
- }],
836
+ },
837
+ ],
652
838
  };
653
839
  }
654
840
  export async function handleListProjects(args) {
655
841
  args = normalizeParameters(args);
656
842
  if (!args.directory) {
657
- return createErrorResponse('Directory is required', ['Provide a valid directory path to search for Godot projects']);
843
+ return createErrorResponse('Directory is required', [
844
+ 'Provide a valid directory path to search for Godot projects',
845
+ ]);
658
846
  }
659
847
  if (!validatePath(args.directory)) {
660
- return createErrorResponse('Invalid directory path', ['Provide a valid path without ".." or other potentially unsafe characters']);
848
+ return createErrorResponse('Invalid directory path', [
849
+ 'Provide a valid path without ".." or other potentially unsafe characters',
850
+ ]);
661
851
  }
662
852
  try {
663
853
  if (!existsSync(args.directory)) {
664
- return createErrorResponse(`Directory does not exist: ${args.directory}`, ['Provide a valid directory path that exists on the system']);
854
+ return createErrorResponse(`Directory does not exist: ${args.directory}`, [
855
+ 'Provide a valid directory path that exists on the system',
856
+ ]);
665
857
  }
666
858
  const recursive = args.recursive === true;
667
859
  const projects = findGodotProjects(args.directory, recursive);
@@ -671,7 +863,10 @@ export async function handleListProjects(args) {
671
863
  }
672
864
  catch (error) {
673
865
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
674
- return createErrorResponse(`Failed to list projects: ${errorMessage}`, ['Ensure the directory exists and is accessible', 'Check if you have permission to read the directory']);
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
+ ]);
675
870
  }
676
871
  }
677
872
  export async function handleGetProjectInfo(runner, args) {
@@ -685,11 +880,16 @@ export async function handleGetProjectInfo(runner, args) {
685
880
  };
686
881
  }
687
882
  if (!validatePath(args.projectPath)) {
688
- return createErrorResponse('Invalid project path', ['Provide a valid path without ".." or other potentially unsafe characters']);
883
+ return createErrorResponse('Invalid project path', [
884
+ 'Provide a valid path without ".." or other potentially unsafe characters',
885
+ ]);
689
886
  }
690
887
  const projectFile = join(args.projectPath, 'project.godot');
691
888
  if (!existsSync(projectFile)) {
692
- return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, ['Ensure the path points to a directory containing a project.godot file', 'Use list_projects to find valid Godot projects']);
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
+ ]);
693
893
  }
694
894
  const projectStructure = getProjectStructure(args.projectPath);
695
895
  let projectName = basename(args.projectPath);
@@ -705,7 +905,8 @@ export async function handleGetProjectInfo(runner, args) {
705
905
  logDebug(`Error reading project file: ${error}`);
706
906
  }
707
907
  return {
708
- content: [{
908
+ content: [
909
+ {
709
910
  type: 'text',
710
911
  text: JSON.stringify({
711
912
  name: projectName,
@@ -713,12 +914,16 @@ export async function handleGetProjectInfo(runner, args) {
713
914
  godotVersion: version,
714
915
  structure: projectStructure,
715
916
  }),
716
- }],
917
+ },
918
+ ],
717
919
  };
718
920
  }
719
921
  catch (error) {
720
922
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
721
- return createErrorResponse(`Failed to get project info: ${errorMessage}`, ['Ensure Godot is installed correctly', 'Check if the GODOT_PATH environment variable is set correctly']);
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
+ ]);
722
927
  }
723
928
  }
724
929
  export async function handleTakeScreenshot(runner, args) {
@@ -729,46 +934,64 @@ export async function handleTakeScreenshot(runner, args) {
729
934
  }
730
935
  const timeout = typeof args.timeout === 'number' ? args.timeout : 10000;
731
936
  try {
732
- const responseStr = await runner.sendCommand('screenshot', {}, timeout);
937
+ const { response: responseStr, runtimeErrors } = await runner.sendCommandWithErrors('screenshot', {}, timeout);
733
938
  let parsed;
734
939
  try {
735
940
  parsed = JSON.parse(responseStr);
736
941
  }
737
942
  catch {
738
- return createErrorResponse(`Invalid response from screenshot server: ${responseStr}`, ['The game may not have fully initialized yet', 'Try again after a few seconds']);
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
+ ]);
739
947
  }
740
948
  if (parsed.error) {
741
- return createErrorResponse(`Screenshot server error: ${parsed.error}`, ['Ensure the game viewport is active', 'Try again after a moment']);
949
+ return createErrorResponse(`Screenshot server error: ${parsed.error}`, [
950
+ 'Ensure the game viewport is active',
951
+ 'Try again after a moment',
952
+ ]);
742
953
  }
743
954
  if (!parsed.path) {
744
- return createErrorResponse('Screenshot server returned no file path', ['Try again after a few seconds']);
955
+ return createErrorResponse('Screenshot server returned no file path', [
956
+ 'Try again after a few seconds',
957
+ ]);
745
958
  }
746
959
  // Normalize path for the local filesystem (forward slashes from GDScript)
747
960
  const screenshotPath = sep === '\\' ? parsed.path.replace(/\//g, '\\') : parsed.path;
748
961
  if (!existsSync(screenshotPath)) {
749
- return createErrorResponse(`Screenshot file not found at: ${screenshotPath}`, ['The screenshot may have failed to save', 'Check disk space and permissions']);
962
+ return createErrorResponse(`Screenshot file not found at: ${screenshotPath}`, [
963
+ 'The screenshot may have failed to save',
964
+ 'Check disk space and permissions',
965
+ ]);
750
966
  }
751
967
  const imageBuffer = readFileSync(screenshotPath);
752
968
  const base64Data = imageBuffer.toString('base64');
753
- return {
754
- content: [
755
- {
756
- type: 'image',
757
- data: base64Data,
758
- mimeType: 'image/png',
759
- },
760
- {
761
- type: 'text',
762
- text: `Screenshot saved to: ${parsed.path}`,
763
- },
764
- ],
765
- };
969
+ const content = [
970
+ {
971
+ type: 'image',
972
+ data: base64Data,
973
+ mimeType: 'image/png',
974
+ },
975
+ {
976
+ type: 'text',
977
+ text: `Screenshot saved to: ${parsed.path}`,
978
+ },
979
+ ];
980
+ if (runtimeErrors.length > 0) {
981
+ content.push({
982
+ type: 'text',
983
+ text: JSON.stringify({
984
+ warnings: runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES),
985
+ }),
986
+ });
987
+ }
988
+ return { content };
766
989
  }
767
990
  catch (error) {
768
991
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
769
992
  return createErrorResponse(`Failed to take screenshot: ${errorMessage}`, [
770
993
  'Ensure the project is running (use run_project first)',
771
- 'Wait 2-3 seconds after starting for the screenshot server to initialize',
994
+ 'The bridge may not be ready yet use get_debug_output to investigate',
772
995
  'Check that UDP port 9900 is not blocked',
773
996
  ]);
774
997
  }
@@ -781,44 +1004,61 @@ export async function handleSimulateInput(runner, args) {
781
1004
  }
782
1005
  const actions = args.actions;
783
1006
  if (!Array.isArray(actions) || actions.length === 0) {
784
- return createErrorResponse('actions must be a non-empty array of input actions', ['Provide at least one action object with a "type" field']);
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
+ ]);
785
1010
  }
786
1011
  // Calculate timeout: sum of all wait durations + 10s buffer
787
1012
  let totalWaitMs = 0;
788
1013
  for (const action of actions) {
789
- if (typeof action === 'object' && action !== null && action.type === 'wait' && typeof action.ms === 'number') {
1014
+ if (typeof action === 'object' &&
1015
+ action !== null &&
1016
+ action.type === 'wait' &&
1017
+ typeof action.ms === 'number') {
790
1018
  totalWaitMs += action.ms;
791
1019
  }
792
1020
  }
793
1021
  const timeoutMs = totalWaitMs + 10000;
794
1022
  try {
795
- const responseStr = await runner.sendCommand('input', { actions }, timeoutMs);
1023
+ const { response: responseStr, runtimeErrors } = await runner.sendCommandWithErrors('input', { actions }, timeoutMs);
796
1024
  let parsed;
797
1025
  try {
798
1026
  parsed = JSON.parse(responseStr);
799
1027
  }
800
1028
  catch {
801
- return createErrorResponse(`Invalid response from bridge: ${responseStr}`, ['The game may not have fully initialized yet', 'Try again after a few seconds']);
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
+ ]);
802
1033
  }
803
1034
  if (parsed.error) {
804
- return createErrorResponse(`Input simulation error: ${parsed.error}`, ['Check action types and parameters', 'Ensure key names are valid Godot key names']);
1035
+ return createErrorResponse(`Input simulation error: ${parsed.error}`, [
1036
+ 'Check action types and parameters',
1037
+ 'Ensure key names are valid Godot key names',
1038
+ ]);
1039
+ }
1040
+ const payload = {
1041
+ success: true,
1042
+ actions_processed: parsed.actions_processed,
1043
+ tip: 'Call take_screenshot to verify the input had the intended visual effect.',
1044
+ };
1045
+ if (runtimeErrors.length > 0) {
1046
+ payload.warnings = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES);
805
1047
  }
806
1048
  return {
807
- content: [{
1049
+ content: [
1050
+ {
808
1051
  type: 'text',
809
- text: JSON.stringify({
810
- success: true,
811
- actions_processed: parsed.actions_processed,
812
- tip: 'Call take_screenshot to verify the input had the intended visual effect.',
813
- }),
814
- }],
1052
+ text: JSON.stringify(payload),
1053
+ },
1054
+ ],
815
1055
  };
816
1056
  }
817
1057
  catch (error) {
818
1058
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
819
1059
  return createErrorResponse(`Failed to simulate input: ${errorMessage}`, [
820
1060
  'Ensure the project is running (use run_project first)',
821
- 'Wait 2-3 seconds after starting for the bridge to initialize',
1061
+ 'The bridge may not be ready yet use get_debug_output to investigate',
822
1062
  'Check that UDP port 9900 is not blocked',
823
1063
  ]);
824
1064
  }
@@ -834,32 +1074,43 @@ export async function handleGetUiElements(runner, args) {
834
1074
  const cmdParams = { visible_only: visibleOnly };
835
1075
  if (args.filter)
836
1076
  cmdParams.type_filter = args.filter;
837
- const responseStr = await runner.sendCommand('get_ui_elements', cmdParams);
1077
+ const { response: responseStr, runtimeErrors } = await runner.sendCommandWithErrors('get_ui_elements', cmdParams);
838
1078
  let parsed;
839
1079
  try {
840
1080
  parsed = JSON.parse(responseStr);
841
1081
  }
842
1082
  catch {
843
- return createErrorResponse(`Invalid response from bridge: ${responseStr}`, ['The game may not have fully initialized yet', 'Try again after a few seconds']);
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
+ ]);
844
1087
  }
845
1088
  if (parsed.error) {
846
- return createErrorResponse(`UI element query error: ${parsed.error}`, ['Ensure the game has a UI with Control nodes']);
1089
+ return createErrorResponse(`UI element query error: ${parsed.error}`, [
1090
+ 'Ensure the game has a UI with Control nodes',
1091
+ ]);
1092
+ }
1093
+ const payload = {
1094
+ ...parsed,
1095
+ tip: "Use simulate_input with type 'click_element' and a node_path or text value from this list to interact with these elements.",
1096
+ };
1097
+ if (runtimeErrors.length > 0) {
1098
+ payload.warnings = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES);
847
1099
  }
848
1100
  return {
849
- content: [{
1101
+ content: [
1102
+ {
850
1103
  type: 'text',
851
- text: JSON.stringify({
852
- ...parsed,
853
- tip: "Use simulate_input with type 'click_element' and a node_path or text value from this list to interact with these elements.",
854
- }),
855
- }],
1104
+ text: JSON.stringify(payload),
1105
+ },
1106
+ ],
856
1107
  };
857
1108
  }
858
1109
  catch (error) {
859
1110
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
860
1111
  return createErrorResponse(`Failed to get UI elements: ${errorMessage}`, [
861
1112
  'Ensure the project is running (use run_project first)',
862
- 'Wait 2-3 seconds after starting for the bridge to initialize',
1113
+ 'The bridge may not be ready yet use get_debug_output to investigate',
863
1114
  'Check that UDP port 9900 is not blocked',
864
1115
  ]);
865
1116
  }
@@ -872,7 +1123,9 @@ export async function handleRunScript(runner, args) {
872
1123
  }
873
1124
  const script = args.script;
874
1125
  if (typeof script !== 'string' || script.trim() === '') {
875
- return createErrorResponse('script is required and must be a non-empty string', ['Provide GDScript source code with extends RefCounted and func execute(scene_tree: SceneTree) -> Variant']);
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
+ ]);
876
1129
  }
877
1130
  if (!script.includes('func execute')) {
878
1131
  return createErrorResponse('Script must define func execute(scene_tree: SceneTree) -> Variant', ['Add a func execute(scene_tree: SceneTree) -> Variant method to your script']);
@@ -894,33 +1147,70 @@ export async function handleRunScript(runner, args) {
894
1147
  }
895
1148
  const timeout = typeof args.timeout === 'number' ? args.timeout : 30000;
896
1149
  try {
897
- const responseStr = await runner.sendCommand('run_script', { source: script }, timeout);
1150
+ const { response: responseStr, runtimeErrors } = await runner.sendCommandWithErrors('run_script', { source: script }, timeout);
898
1151
  let parsed;
899
1152
  try {
900
1153
  parsed = JSON.parse(responseStr);
901
1154
  }
902
1155
  catch {
903
- return createErrorResponse(`Invalid response from bridge: ${responseStr}`, ['The script may have produced non-JSON output', 'Check get_debug_output for print() statements']);
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
+ ]);
904
1160
  }
905
1161
  if (parsed.error) {
906
- return createErrorResponse(`Script execution error: ${parsed.error}`, ['Check your GDScript syntax', 'Ensure the script extends RefCounted', 'Check get_debug_output for details']);
1162
+ return createErrorResponse(`Script execution error: ${parsed.error}`, [
1163
+ 'Check your GDScript syntax',
1164
+ 'Ensure the script extends RefCounted',
1165
+ 'Check get_debug_output for details',
1166
+ ]);
1167
+ }
1168
+ // Detect false-positive success: GDScript has no try-catch, so runtime errors
1169
+ // return null and the real error only appears in stderr.
1170
+ if (parsed.success && parsed.result === null && runner.activeSessionMode === 'spawned') {
1171
+ if (runtimeErrors.length > 0) {
1172
+ const errorContext = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES).join('\n');
1173
+ return createErrorResponse(`Script runtime error detected:\n${errorContext}`, [
1174
+ 'Fix the GDScript error in your script and retry',
1175
+ 'Use get_debug_output for full process output',
1176
+ ]);
1177
+ }
1178
+ return {
1179
+ content: [
1180
+ {
1181
+ type: 'text',
1182
+ text: JSON.stringify({
1183
+ success: true,
1184
+ result: null,
1185
+ warning: 'Script returned null. If unexpected, check get_debug_output for runtime errors — GDScript does not propagate exceptions.',
1186
+ tip: 'Call take_screenshot to verify any visual changes, or get_debug_output to review print() output from your script.',
1187
+ }),
1188
+ },
1189
+ ],
1190
+ };
1191
+ }
1192
+ const payload = {
1193
+ success: true,
1194
+ result: parsed.result,
1195
+ tip: 'Call take_screenshot to verify any visual changes, or get_debug_output to review print() output from your script.',
1196
+ };
1197
+ if (runtimeErrors.length > 0) {
1198
+ payload.warnings = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES);
907
1199
  }
908
1200
  return {
909
- content: [{
1201
+ content: [
1202
+ {
910
1203
  type: 'text',
911
- text: JSON.stringify({
912
- success: true,
913
- result: parsed.result,
914
- tip: 'Call take_screenshot to verify any visual changes, or get_debug_output to review print() output from your script.',
915
- }),
916
- }],
1204
+ text: JSON.stringify(payload),
1205
+ },
1206
+ ],
917
1207
  };
918
1208
  }
919
1209
  catch (error) {
920
1210
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
921
1211
  return createErrorResponse(`Failed to execute script: ${errorMessage}`, [
922
1212
  'Ensure the project is running (use run_project first)',
923
- 'Wait 2-3 seconds after starting for the bridge to initialize',
1213
+ 'The bridge may not be ready yet — wait 2-3 seconds after starting, then check get_debug_output if the issue persists',
924
1214
  'Check that UDP port 9900 is not blocked',
925
1215
  'For long-running scripts, increase the timeout parameter',
926
1216
  ]);
@@ -1061,7 +1351,9 @@ export async function handleListAutoloads(args) {
1061
1351
  }
1062
1352
  catch (error) {
1063
1353
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1064
- return createErrorResponse(`Failed to list autoloads: ${errorMessage}`, ['Check if project.godot is accessible']);
1354
+ return createErrorResponse(`Failed to list autoloads: ${errorMessage}`, [
1355
+ 'Check if project.godot is accessible',
1356
+ ]);
1065
1357
  }
1066
1358
  }
1067
1359
  export async function handleAddAutoload(args) {
@@ -1070,7 +1362,9 @@ export async function handleAddAutoload(args) {
1070
1362
  if ('isError' in v)
1071
1363
  return v;
1072
1364
  if (!args.autoloadName || !args.autoloadPath) {
1073
- return createErrorResponse('autoloadName and autoloadPath are required', ['Provide the autoload node name and script/scene path']);
1365
+ return createErrorResponse('autoloadName and autoloadPath are required', [
1366
+ 'Provide the autoload node name and script/scene path',
1367
+ ]);
1074
1368
  }
1075
1369
  if (!validatePath(args.autoloadPath)) {
1076
1370
  return createErrorResponse('Invalid autoload path', ['Provide a valid path without ".."']);
@@ -1078,18 +1372,28 @@ export async function handleAddAutoload(args) {
1078
1372
  try {
1079
1373
  const projectFile = join(v.projectPath, 'project.godot');
1080
1374
  const existing = parseAutoloads(projectFile);
1081
- if (existing.some(a => a.name === args.autoloadName)) {
1082
- return createErrorResponse(`Autoload '${args.autoloadName}' already exists`, ['Use update_autoload to modify it', 'Use list_autoloads to see current autoloads']);
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
+ ]);
1083
1380
  }
1084
1381
  const isSingleton = args.singleton !== false;
1085
1382
  addAutoloadEntry(projectFile, args.autoloadName, args.autoloadPath, isSingleton);
1086
1383
  return {
1087
- content: [{ type: 'text', 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.` }],
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
+ ],
1088
1390
  };
1089
1391
  }
1090
1392
  catch (error) {
1091
1393
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1092
- return createErrorResponse(`Failed to add autoload: ${errorMessage}`, ['Check if project.godot is accessible']);
1394
+ return createErrorResponse(`Failed to add autoload: ${errorMessage}`, [
1395
+ 'Check if project.godot is accessible',
1396
+ ]);
1093
1397
  }
1094
1398
  }
1095
1399
  export async function handleRemoveAutoload(args) {
@@ -1098,19 +1402,27 @@ export async function handleRemoveAutoload(args) {
1098
1402
  if ('isError' in v)
1099
1403
  return v;
1100
1404
  if (!args.autoloadName) {
1101
- return createErrorResponse('autoloadName is required', ['Provide the name of the autoload to remove']);
1405
+ return createErrorResponse('autoloadName is required', [
1406
+ 'Provide the name of the autoload to remove',
1407
+ ]);
1102
1408
  }
1103
1409
  try {
1104
1410
  const projectFile = join(v.projectPath, 'project.godot');
1105
1411
  const removed = removeAutoloadEntry(projectFile, args.autoloadName);
1106
1412
  if (!removed) {
1107
- return createErrorResponse(`Autoload '${args.autoloadName}' not found`, ['Use list_autoloads to see existing autoloads']);
1413
+ return createErrorResponse(`Autoload '${args.autoloadName}' not found`, [
1414
+ 'Use list_autoloads to see existing autoloads',
1415
+ ]);
1108
1416
  }
1109
- return { content: [{ type: 'text', text: `Autoload '${args.autoloadName}' removed successfully.` }] };
1417
+ return {
1418
+ content: [{ type: 'text', text: `Autoload '${args.autoloadName}' removed successfully.` }],
1419
+ };
1110
1420
  }
1111
1421
  catch (error) {
1112
1422
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1113
- return createErrorResponse(`Failed to remove autoload: ${errorMessage}`, ['Check if project.godot is accessible']);
1423
+ return createErrorResponse(`Failed to remove autoload: ${errorMessage}`, [
1424
+ 'Check if project.godot is accessible',
1425
+ ]);
1114
1426
  }
1115
1427
  }
1116
1428
  export async function handleUpdateAutoload(args) {
@@ -1119,7 +1431,9 @@ export async function handleUpdateAutoload(args) {
1119
1431
  if ('isError' in v)
1120
1432
  return v;
1121
1433
  if (!args.autoloadName) {
1122
- return createErrorResponse('autoloadName is required', ['Provide the name of the autoload to update']);
1434
+ return createErrorResponse('autoloadName is required', [
1435
+ 'Provide the name of the autoload to update',
1436
+ ]);
1123
1437
  }
1124
1438
  if (args.autoloadPath && !validatePath(args.autoloadPath)) {
1125
1439
  return createErrorResponse('Invalid autoload path', ['Provide a valid path without ".."']);
@@ -1128,13 +1442,20 @@ export async function handleUpdateAutoload(args) {
1128
1442
  const projectFile = join(v.projectPath, 'project.godot');
1129
1443
  const updated = updateAutoloadEntry(projectFile, args.autoloadName, args.autoloadPath, args.singleton);
1130
1444
  if (!updated) {
1131
- return createErrorResponse(`Autoload '${args.autoloadName}' not found`, ['Use list_autoloads to see existing autoloads', 'Use add_autoload to register a new one']);
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
+ ]);
1132
1449
  }
1133
- return { content: [{ type: 'text', text: `Autoload '${args.autoloadName}' updated successfully.` }] };
1450
+ return {
1451
+ content: [{ type: 'text', text: `Autoload '${args.autoloadName}' updated successfully.` }],
1452
+ };
1134
1453
  }
1135
1454
  catch (error) {
1136
1455
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1137
- return createErrorResponse(`Failed to update autoload: ${errorMessage}`, ['Check if project.godot is accessible']);
1456
+ return createErrorResponse(`Failed to update autoload: ${errorMessage}`, [
1457
+ 'Check if project.godot is accessible',
1458
+ ]);
1138
1459
  }
1139
1460
  }
1140
1461
  export async function handleGetProjectFiles(args) {
@@ -1145,14 +1466,16 @@ export async function handleGetProjectFiles(args) {
1145
1466
  try {
1146
1467
  const maxDepth = typeof args.maxDepth === 'number' ? args.maxDepth : -1;
1147
1468
  const extensions = Array.isArray(args.extensions)
1148
- ? args.extensions.map(e => e.toLowerCase().replace(/^\./, ''))
1469
+ ? args.extensions.map((e) => e.toLowerCase().replace(/^\./, ''))
1149
1470
  : null;
1150
1471
  const tree = buildFilesystemTree(v.projectPath, '', maxDepth, 0, extensions);
1151
1472
  return { content: [{ type: 'text', text: JSON.stringify(tree) }] };
1152
1473
  }
1153
1474
  catch (error) {
1154
1475
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1155
- return createErrorResponse(`Failed to get project files: ${errorMessage}`, ['Check if the project directory is accessible']);
1476
+ return createErrorResponse(`Failed to get project files: ${errorMessage}`, [
1477
+ 'Check if the project directory is accessible',
1478
+ ]);
1156
1479
  }
1157
1480
  }
1158
1481
  export async function handleSearchProject(args) {
@@ -1165,7 +1488,7 @@ export async function handleSearchProject(args) {
1165
1488
  }
1166
1489
  try {
1167
1490
  const fileTypes = Array.isArray(args.fileTypes)
1168
- ? args.fileTypes.map(e => e.toLowerCase().replace(/^\./, ''))
1491
+ ? args.fileTypes.map((e) => e.toLowerCase().replace(/^\./, ''))
1169
1492
  : ['gd', 'tscn', 'cs', 'gdshader'];
1170
1493
  const caseSensitive = args.caseSensitive === true;
1171
1494
  const maxResults = typeof args.maxResults === 'number' ? args.maxResults : 100;
@@ -1174,7 +1497,9 @@ export async function handleSearchProject(args) {
1174
1497
  }
1175
1498
  catch (error) {
1176
1499
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1177
- return createErrorResponse(`Failed to search project: ${errorMessage}`, ['Check if the project directory is accessible']);
1500
+ return createErrorResponse(`Failed to search project: ${errorMessage}`, [
1501
+ 'Check if the project directory is accessible',
1502
+ ]);
1178
1503
  }
1179
1504
  }
1180
1505
  export async function handleGetSceneDependencies(args) {
@@ -1183,7 +1508,9 @@ export async function handleGetSceneDependencies(args) {
1183
1508
  if ('isError' in v)
1184
1509
  return v;
1185
1510
  if (!args.scenePath || typeof args.scenePath !== 'string') {
1186
- return createErrorResponse('scenePath is required', ['Provide a path relative to the project root, e.g. "scenes/main.tscn"']);
1511
+ return createErrorResponse('scenePath is required', [
1512
+ 'Provide a path relative to the project root, e.g. "scenes/main.tscn"',
1513
+ ]);
1187
1514
  }
1188
1515
  if (!validatePath(args.scenePath)) {
1189
1516
  return createErrorResponse('Invalid scenePath', ['Provide a valid path without ".."']);
@@ -1191,7 +1518,10 @@ export async function handleGetSceneDependencies(args) {
1191
1518
  try {
1192
1519
  const sceneFullPath = join(v.projectPath, args.scenePath);
1193
1520
  if (!existsSync(sceneFullPath)) {
1194
- return createErrorResponse(`Scene file not found: ${args.scenePath}`, ['Verify the path is relative to the project root', 'Use get_project_files to list available .tscn files']);
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
+ ]);
1195
1525
  }
1196
1526
  const sceneContent = readFileSync(sceneFullPath, 'utf8');
1197
1527
  const dependencies = [];
@@ -1213,11 +1543,15 @@ export async function handleGetSceneDependencies(args) {
1213
1543
  dependencies.push(dep);
1214
1544
  }
1215
1545
  }
1216
- return { content: [{ type: 'text', text: JSON.stringify({ scene: args.scenePath, dependencies }) }] };
1546
+ return {
1547
+ content: [{ type: 'text', text: JSON.stringify({ scene: args.scenePath, dependencies }) }],
1548
+ };
1217
1549
  }
1218
1550
  catch (error) {
1219
1551
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1220
- return createErrorResponse(`Failed to get scene dependencies: ${errorMessage}`, ['Check if the scene file is accessible']);
1552
+ return createErrorResponse(`Failed to get scene dependencies: ${errorMessage}`, [
1553
+ 'Check if the scene file is accessible',
1554
+ ]);
1221
1555
  }
1222
1556
  }
1223
1557
  export async function handleGetProjectSettings(args) {
@@ -1236,7 +1570,9 @@ export async function handleGetProjectSettings(args) {
1236
1570
  }
1237
1571
  catch (error) {
1238
1572
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1239
- return createErrorResponse(`Failed to get project settings: ${errorMessage}`, ['Check if project.godot is accessible']);
1573
+ return createErrorResponse(`Failed to get project settings: ${errorMessage}`, [
1574
+ 'Check if project.godot is accessible',
1575
+ ]);
1240
1576
  }
1241
1577
  }
1242
1578
  //# sourceMappingURL=project-tools.js.map