godot-mcp-runtime 2.2.1 → 2.2.3

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.
@@ -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. 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.',
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 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.',
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 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}]',
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: { type: 'string', description: '[key] Key name (e.g. "W", "Space", "Escape", "Up")' },
241
- 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
+ },
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: { type: 'string', enum: ['left', 'right', 'middle'], description: '[mouse_button, click_element] Mouse button (default: left)' },
246
- x: { type: 'number', description: '[mouse_button, mouse_motion] X position in viewport pixels' },
247
- y: { type: 'number', description: '[mouse_button, mouse_motion] Y position in viewport pixels' },
248
- relative_x: { type: 'number', description: '[mouse_motion] Relative X movement in pixels' },
249
- relative_y: { type: 'number', description: '[mouse_motion] Relative Y movement in pixels' },
250
- double_click: { type: 'boolean', description: '[mouse_button, click_element] Double click' },
251
- 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.' },
252
- action: { type: 'string', description: '[action] Godot input action name (as defined in Project Settings > Input Map)' },
253
- strength: { type: 'number', description: '[action] Action strength (0–1, default 1.0)' },
254
- 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
+ },
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 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? }] }',
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: { type: 'string', description: 'Name of the autoload node (e.g. "MyManager")' },
318
- autoloadPath: { type: 'string', description: 'Path to the script or scene (e.g. "res://autoload/my_manager.gd" or "autoload/my_manager.gd")' },
319
- 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
+ },
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: '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.",
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: { type: 'number', description: 'Maximum recursion depth. -1 means unlimited (default: -1)' },
358
- 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
+ },
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: { 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
+ },
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: { 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
+ },
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: { 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
+ },
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}.`, ['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
+ ]);
406
476
  }
407
- if (runner.activeSessionMode === 'spawned' && (!runner.activeProcess || runner.activeProcess.hasExited)) {
408
- 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
+ ]);
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', ['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
+ ]);
486
562
  }
487
563
  if (!validatePath(args.projectPath)) {
488
- 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
+ ]);
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', ['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
+ ]);
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}`, ['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
+ ]);
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: [{ 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
+ ],
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}`, ['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
+ ]);
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', ['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
+ ]);
519
613
  }
520
614
  if (!validatePath(args.projectPath)) {
521
- 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
+ ]);
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}`, ['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
+ ]);
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
- '- Wait 2–3 seconds before calling take_screenshot, simulate_input, get_ui_elements, or run_script (bridge needs time to initialize)',
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}`, ['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
+ ]);
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', ['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
+ ]);
552
682
  }
553
683
  if (!validatePath(args.projectPath)) {
554
- 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
+ ]);
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}`, ['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
+ ]);
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 after this call so the injected McpBridge can initialize',
568
- '- 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',
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}`, ['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
+ ]);
578
742
  }
579
743
  }
580
744
  export function handleDetachProject(runner) {
581
745
  if (runner.activeSessionMode !== 'attached') {
582
- 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
+ ]);
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.', ['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
+ ]);
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.', ['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
+ ]);
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 = '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.';
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.', ['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
+ ]);
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', ['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
+ ]);
659
846
  }
660
847
  if (!validatePath(args.directory)) {
661
- 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
+ ]);
662
851
  }
663
852
  try {
664
853
  if (!existsSync(args.directory)) {
665
- 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
+ ]);
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}`, ['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
+ ]);
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', ['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
+ ]);
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}`, ['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
+ ]);
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}`, ['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
+ ]);
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}`, ['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
+ ]);
740
947
  }
741
948
  if (parsed.error) {
742
- 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
+ ]);
743
953
  }
744
954
  if (!parsed.path) {
745
- 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
+ ]);
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}`, ['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
+ ]);
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 — wait 2-3 seconds after starting, then check get_debug_output if the issue persists',
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', ['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
+ ]);
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' && 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') {
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}`, ['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
+ ]);
810
1033
  }
811
1034
  if (parsed.error) {
812
- 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
+ ]);
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 — wait 2-3 seconds after starting, then check get_debug_output if the issue persists',
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}`, ['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
+ ]);
856
1087
  }
857
1088
  if (parsed.error) {
858
- 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
+ ]);
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 — wait 2-3 seconds after starting, then check get_debug_output if the issue persists',
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', ['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
+ ]);
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}`, ['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
+ ]);
920
1160
  }
921
1161
  if (parsed.error) {
922
- 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
+ ]);
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}`, ['Check if project.godot is accessible']);
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', ['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
+ ]);
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`, ['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
+ ]);
1125
1380
  }
1126
1381
  const isSingleton = args.singleton !== false;
1127
1382
  addAutoloadEntry(projectFile, args.autoloadName, args.autoloadPath, isSingleton);
1128
1383
  return {
1129
- 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
+ ],
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}`, ['Check if project.godot is accessible']);
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', ['Provide the name of the autoload to remove']);
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`, ['Use list_autoloads to see existing autoloads']);
1413
+ return createErrorResponse(`Autoload '${args.autoloadName}' not found`, [
1414
+ 'Use list_autoloads to see existing autoloads',
1415
+ ]);
1150
1416
  }
1151
- return { content: [{ type: 'text', text: `Autoload '${args.autoloadName}' removed successfully.` }] };
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}`, ['Check if project.godot is accessible']);
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', ['Provide the name of the autoload to update']);
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`, ['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
+ ]);
1174
1449
  }
1175
- return { content: [{ type: 'text', text: `Autoload '${args.autoloadName}' updated successfully.` }] };
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}`, ['Check if project.godot is accessible']);
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}`, ['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
+ ]);
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}`, ['Check if the project directory is accessible']);
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', ['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
+ ]);
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}`, ['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
+ ]);
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 { content: [{ type: 'text', text: JSON.stringify({ scene: args.scenePath, dependencies }) }] };
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}`, ['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
+ ]);
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}`, ['Check if project.godot is accessible']);
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