godot-mcp-runtime 2.1.0 → 2.2.1

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.
@@ -24,6 +24,30 @@ export declare function handleRunProject(runner: GodotRunner, args: OperationPar
24
24
  text: string;
25
25
  }[];
26
26
  }>;
27
+ export declare function handleAttachProject(runner: GodotRunner, args: OperationParams): Promise<{
28
+ content: Array<{
29
+ type: "text";
30
+ text: string;
31
+ }>;
32
+ isError: boolean;
33
+ } | {
34
+ content: {
35
+ type: string;
36
+ text: string;
37
+ }[];
38
+ }>;
39
+ export declare function handleDetachProject(runner: GodotRunner): {
40
+ content: Array<{
41
+ type: "text";
42
+ text: string;
43
+ }>;
44
+ isError: boolean;
45
+ } | {
46
+ content: {
47
+ type: string;
48
+ text: string;
49
+ }[];
50
+ };
27
51
  export declare function handleGetDebugOutput(runner: GodotRunner, args?: OperationParams): {
28
52
  content: Array<{
29
53
  type: "text";
@@ -79,17 +103,10 @@ export declare function handleTakeScreenshot(runner: GodotRunner, args: Operatio
79
103
  }>;
80
104
  isError: boolean;
81
105
  } | {
82
- content: ({
83
- type: string;
84
- data: string;
85
- mimeType: string;
86
- text?: undefined;
87
- } | {
106
+ content: {
107
+ [key: string]: unknown;
88
108
  type: string;
89
- text: string;
90
- data?: undefined;
91
- mimeType?: undefined;
92
- })[];
109
+ }[];
93
110
  }>;
94
111
  export declare function handleSimulateInput(runner: GodotRunner, args: OperationParams): Promise<{
95
112
  content: Array<{
@@ -1 +1 @@
1
- {"version":3,"file":"project-tools.d.ts","sourceRoot":"","sources":["../../src/tools/project-tools.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,WAAW,EAMX,eAAe,EACf,cAAc,EACf,MAAM,0BAA0B,CAAC;AAoGlC,eAAO,MAAM,sBAAsB,EAAE,cAAc,EA6RlD,CAAC;AAgFF,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe;;;;;;;;;;;GAqDlF;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe;;;;;;;;;;;GAiDhF;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,GAAE,eAAoB;;;;;;;;;;;EAmCnF;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW;;;;;;;;;;;EAoBpD;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GAsC7D;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe;;;;;;;;;;;GA4DpF;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe;;;;;;;;;;;;;;;;;;GA4EpF;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe;;;;;;;;;;;GAoEnF;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe;;;;;;;;;;;GAsDnF;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe;;;;;;;;;;;GAoF/E;AAyJD,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GAa9D;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GA8B5D;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GAoB/D;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GA+B/D;AAED,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GAgBhE;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GAqB9D;AAED,wBAAsB,0BAA0B,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GA4CrE;AAED,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GAiBnE"}
1
+ {"version":3,"file":"project-tools.d.ts","sourceRoot":"","sources":["../../src/tools/project-tools.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,WAAW,EAMX,eAAe,EACf,cAAc,EACf,MAAM,0BAA0B,CAAC;AAsGlC,eAAO,MAAM,sBAAsB,EAAE,cAAc,EAoTlD,CAAC;AAkGF,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe;;;;;;;;;;;GAqDlF;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe;;;;;;;;;;;GAiDhF;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe;;;;;;;;;;;GA+CnF;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW;;;;;;;;;;;EAmBtD;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,GAAE,eAAoB;;;;;;;;;;;EAyDnF;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW;;;;;;;;;;;EAwBpD;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GAsC7D;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe;;;;;;;;;;;GA4DpF;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe;;;;;;;;;cAkDpD,MAAM;;GAiCtC;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe;;;;;;;;;;;GAuEnF;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe;;;;;;;;;;;GAyDnF;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe;;;;;;;;;;;GAkH/E;AAyJD,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GAa9D;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GA8B5D;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GAoB/D;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GA+B/D;AAED,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GAgBhE;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GAqB9D;AAED,wBAAsB,0BAA0B,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GA4CrE;AAED,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GAiBnE"}
@@ -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 = [];
@@ -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. Required before calling take_screenshot, simulate_input, get_ui_elements, run_script, or get_debug_output. After starting, wait 2–3 seconds for the MCP bridge to initialize before using those 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. After starting, wait 2–3 seconds for the MCP bridge to initialize before using runtime tools. Call stop_project when done.',
111
112
  inputSchema: {
112
113
  type: 'object',
113
114
  properties: {
@@ -127,9 +128,32 @@ export const projectToolDefinitions = [
127
128
  required: ['projectPath'],
128
129
  },
129
130
  },
131
+ {
132
+ name: 'attach_project',
133
+ description: 'Attach runtime MCP tools to a project without spawning Godot. This injects the McpBridge autoload and marks the project as the active runtime session so you can launch the game manually from your own shell, then use take_screenshot, simulate_input, get_ui_elements, and run_script against that running game. Use detach_project or stop_project when done. get_debug_output is not available in attached mode because stdout/stderr are not captured.',
134
+ inputSchema: {
135
+ type: 'object',
136
+ properties: {
137
+ projectPath: {
138
+ type: 'string',
139
+ description: 'Path to the Godot project directory',
140
+ },
141
+ },
142
+ required: ['projectPath'],
143
+ },
144
+ },
145
+ {
146
+ name: 'detach_project',
147
+ description: 'Clear attached-mode runtime state and remove the injected McpBridge autoload without claiming that the manually launched Godot process was stopped.',
148
+ inputSchema: {
149
+ type: 'object',
150
+ properties: {},
151
+ required: [],
152
+ },
153
+ },
130
154
  {
131
155
  name: 'get_debug_output',
132
- description: 'Get stdout/stderr output from the running Godot project. Requires run_project first. Returns the last N lines of output and errors, a running flag, and an exit code if the process has ended. Use this to check GDScript errors, print() calls, and crash messages.',
156
+ description: 'Get stdout/stderr output from the running Godot project. Requires run_project first. Returns the last N lines of output and errors, a running flag, and an exit code if the process has ended. In attached mode, this reports that stdout/stderr capture is unavailable because Godot was launched outside MCP.',
133
157
  inputSchema: {
134
158
  type: 'object',
135
159
  properties: {
@@ -256,7 +280,7 @@ export const projectToolDefinitions = [
256
280
  },
257
281
  {
258
282
  name: 'run_script',
259
- 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.',
283
+ 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.',
260
284
  inputSchema: {
261
285
  type: 'object',
262
286
  properties: {
@@ -376,6 +400,15 @@ export const projectToolDefinitions = [
376
400
  },
377
401
  },
378
402
  ];
403
+ function ensureRuntimeSession(runner, actionDescription) {
404
+ 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']);
406
+ }
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']);
409
+ }
410
+ return null;
411
+ }
379
412
  function findGodotProjects(directory, recursive) {
380
413
  const projects = [];
381
414
  try {
@@ -495,7 +528,7 @@ export async function handleRunProject(runner, args) {
495
528
  const background = args.background === true;
496
529
  runner.runProject(args.projectPath, args.scene, background);
497
530
  const lines = [
498
- 'Godot project started in debug mode.',
531
+ 'Godot project started.',
499
532
  '- Use get_debug_output to check runtime output and errors',
500
533
  '- Wait 2–3 seconds before calling take_screenshot, simulate_input, get_ui_elements, or run_script (bridge needs time to initialize)',
501
534
  '- Always call stop_project when done — it terminates the process and cleans up the MCP bridge',
@@ -512,13 +545,77 @@ export async function handleRunProject(runner, args) {
512
545
  return createErrorResponse(`Failed to run Godot project: ${errorMessage}`, ['Ensure Godot is installed correctly', 'Check if the GODOT_PATH environment variable is set correctly']);
513
546
  }
514
547
  }
548
+ export async function handleAttachProject(runner, args) {
549
+ args = normalizeParameters(args);
550
+ if (!args.projectPath) {
551
+ return createErrorResponse('Project path is required', ['Provide a valid path to a Godot project directory']);
552
+ }
553
+ if (!validatePath(args.projectPath)) {
554
+ return createErrorResponse('Invalid project path', ['Provide a valid path without ".." or other potentially unsafe characters']);
555
+ }
556
+ try {
557
+ const projectFile = join(args.projectPath, 'project.godot');
558
+ 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']);
560
+ }
561
+ runner.attachProject(args.projectPath);
562
+ return {
563
+ content: [{
564
+ type: 'text',
565
+ text: [
566
+ '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',
569
+ '- get_debug_output is unavailable in attached mode because MCP did not spawn the process',
570
+ '- Use detach_project or stop_project when done to clean up the injected bridge state',
571
+ ].join('\n'),
572
+ }],
573
+ };
574
+ }
575
+ catch (error) {
576
+ 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']);
578
+ }
579
+ }
580
+ export function handleDetachProject(runner) {
581
+ 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']);
583
+ }
584
+ const result = runner.stopProject();
585
+ return {
586
+ content: [{
587
+ type: 'text',
588
+ text: JSON.stringify({
589
+ message: 'Detached attached project and cleaned MCP bridge state',
590
+ externalProcessPreserved: result.externalProcessPreserved === true,
591
+ }),
592
+ }],
593
+ };
594
+ }
515
595
  export function handleGetDebugOutput(runner, args = {}) {
516
596
  args = normalizeParameters(args);
517
- if (!runner.activeProcess) {
518
- return createErrorResponse('No active Godot process.', ['Use run_project to start a Godot project first', 'Check if the Godot process crashed unexpectedly']);
597
+ 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']);
599
+ }
600
+ if (runner.activeSessionMode === 'attached') {
601
+ return {
602
+ content: [{
603
+ type: 'text',
604
+ text: JSON.stringify({
605
+ output: [],
606
+ errors: [],
607
+ running: null,
608
+ attached: true,
609
+ tip: 'Attached mode does not capture stdout/stderr because Godot was launched outside MCP.',
610
+ }),
611
+ }],
612
+ };
519
613
  }
520
- const limit = typeof args.limit === 'number' ? args.limit : 200;
521
614
  const proc = runner.activeProcess;
615
+ 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']);
617
+ }
618
+ const limit = typeof args.limit === 'number' ? args.limit : 200;
522
619
  const response = {
523
620
  output: proc.output.slice(-limit),
524
621
  errors: proc.errors.slice(-limit),
@@ -544,7 +641,11 @@ export function handleStopProject(runner) {
544
641
  content: [{
545
642
  type: 'text',
546
643
  text: JSON.stringify({
547
- message: 'Godot project stopped',
644
+ message: result.mode === 'attached'
645
+ ? 'Attached project detached and MCP bridge state cleaned up'
646
+ : 'Godot project stopped',
647
+ mode: result.mode,
648
+ externalProcessPreserved: result.externalProcessPreserved === true,
548
649
  finalOutput: result.output.slice(-200),
549
650
  finalErrors: result.errors.slice(-200),
550
651
  }),
@@ -623,12 +724,13 @@ export async function handleGetProjectInfo(runner, args) {
623
724
  }
624
725
  export async function handleTakeScreenshot(runner, args) {
625
726
  args = normalizeParameters(args);
626
- if (!runner.activeProcess || runner.activeProcess.hasExited) {
627
- return createErrorResponse('No active Godot process. A project must be running to take a screenshot.', ['Use run_project to start a Godot project first', 'Wait a few seconds after starting for the game to initialize']);
727
+ const sessionError = ensureRuntimeSession(runner, 'take a screenshot');
728
+ if (sessionError) {
729
+ return sessionError;
628
730
  }
629
731
  const timeout = typeof args.timeout === 'number' ? args.timeout : 10000;
630
732
  try {
631
- const responseStr = await runner.sendCommand('screenshot', {}, timeout);
733
+ const { response: responseStr, runtimeErrors } = await runner.sendCommandWithErrors('screenshot', {}, timeout);
632
734
  let parsed;
633
735
  try {
634
736
  parsed = JSON.parse(responseStr);
@@ -649,33 +751,41 @@ export async function handleTakeScreenshot(runner, args) {
649
751
  }
650
752
  const imageBuffer = readFileSync(screenshotPath);
651
753
  const base64Data = imageBuffer.toString('base64');
652
- return {
653
- content: [
654
- {
655
- type: 'image',
656
- data: base64Data,
657
- mimeType: 'image/png',
658
- },
659
- {
660
- type: 'text',
661
- text: `Screenshot saved to: ${parsed.path}`,
662
- },
663
- ],
664
- };
754
+ const 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
+ if (runtimeErrors.length > 0) {
766
+ content.push({
767
+ type: 'text',
768
+ text: JSON.stringify({
769
+ warnings: runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES),
770
+ }),
771
+ });
772
+ }
773
+ return { content };
665
774
  }
666
775
  catch (error) {
667
776
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
668
777
  return createErrorResponse(`Failed to take screenshot: ${errorMessage}`, [
669
778
  'Ensure the project is running (use run_project first)',
670
- 'Wait 2-3 seconds after starting for the screenshot server to initialize',
779
+ 'The bridge may not be ready yet — wait 2-3 seconds after starting, then check get_debug_output if the issue persists',
671
780
  'Check that UDP port 9900 is not blocked',
672
781
  ]);
673
782
  }
674
783
  }
675
784
  export async function handleSimulateInput(runner, args) {
676
785
  args = normalizeParameters(args);
677
- if (!runner.activeProcess || runner.activeProcess.hasExited) {
678
- return createErrorResponse('No active Godot process. A project must be running to simulate input.', ['Use run_project to start a Godot project first', 'Wait a few seconds after starting for the game to initialize']);
786
+ const sessionError = ensureRuntimeSession(runner, 'simulate input');
787
+ if (sessionError) {
788
+ return sessionError;
679
789
  }
680
790
  const actions = args.actions;
681
791
  if (!Array.isArray(actions) || actions.length === 0) {
@@ -690,7 +800,7 @@ export async function handleSimulateInput(runner, args) {
690
800
  }
691
801
  const timeoutMs = totalWaitMs + 10000;
692
802
  try {
693
- const responseStr = await runner.sendCommand('input', { actions }, timeoutMs);
803
+ const { response: responseStr, runtimeErrors } = await runner.sendCommandWithErrors('input', { actions }, timeoutMs);
694
804
  let parsed;
695
805
  try {
696
806
  parsed = JSON.parse(responseStr);
@@ -701,14 +811,18 @@ export async function handleSimulateInput(runner, args) {
701
811
  if (parsed.error) {
702
812
  return createErrorResponse(`Input simulation error: ${parsed.error}`, ['Check action types and parameters', 'Ensure key names are valid Godot key names']);
703
813
  }
814
+ const payload = {
815
+ success: true,
816
+ actions_processed: parsed.actions_processed,
817
+ tip: 'Call take_screenshot to verify the input had the intended visual effect.',
818
+ };
819
+ if (runtimeErrors.length > 0) {
820
+ payload.warnings = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES);
821
+ }
704
822
  return {
705
823
  content: [{
706
824
  type: 'text',
707
- text: JSON.stringify({
708
- success: true,
709
- actions_processed: parsed.actions_processed,
710
- tip: 'Call take_screenshot to verify the input had the intended visual effect.',
711
- }),
825
+ text: JSON.stringify(payload),
712
826
  }],
713
827
  };
714
828
  }
@@ -716,22 +830,23 @@ export async function handleSimulateInput(runner, args) {
716
830
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
717
831
  return createErrorResponse(`Failed to simulate input: ${errorMessage}`, [
718
832
  'Ensure the project is running (use run_project first)',
719
- 'Wait 2-3 seconds after starting for the bridge to initialize',
833
+ 'The bridge may not be ready yet — wait 2-3 seconds after starting, then check get_debug_output if the issue persists',
720
834
  'Check that UDP port 9900 is not blocked',
721
835
  ]);
722
836
  }
723
837
  }
724
838
  export async function handleGetUiElements(runner, args) {
725
839
  args = normalizeParameters(args);
726
- if (!runner.activeProcess || runner.activeProcess.hasExited) {
727
- return createErrorResponse('No active Godot process. A project must be running to query UI elements.', ['Use run_project to start a Godot project first', 'Wait a few seconds after starting for the game to initialize']);
840
+ const sessionError = ensureRuntimeSession(runner, 'query UI elements');
841
+ if (sessionError) {
842
+ return sessionError;
728
843
  }
729
844
  const visibleOnly = args.visibleOnly !== false;
730
845
  try {
731
846
  const cmdParams = { visible_only: visibleOnly };
732
847
  if (args.filter)
733
848
  cmdParams.type_filter = args.filter;
734
- const responseStr = await runner.sendCommand('get_ui_elements', cmdParams);
849
+ const { response: responseStr, runtimeErrors } = await runner.sendCommandWithErrors('get_ui_elements', cmdParams);
735
850
  let parsed;
736
851
  try {
737
852
  parsed = JSON.parse(responseStr);
@@ -742,13 +857,17 @@ export async function handleGetUiElements(runner, args) {
742
857
  if (parsed.error) {
743
858
  return createErrorResponse(`UI element query error: ${parsed.error}`, ['Ensure the game has a UI with Control nodes']);
744
859
  }
860
+ const payload = {
861
+ ...parsed,
862
+ tip: "Use simulate_input with type 'click_element' and a node_path or text value from this list to interact with these elements.",
863
+ };
864
+ if (runtimeErrors.length > 0) {
865
+ payload.warnings = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES);
866
+ }
745
867
  return {
746
868
  content: [{
747
869
  type: 'text',
748
- text: JSON.stringify({
749
- ...parsed,
750
- tip: "Use simulate_input with type 'click_element' and a node_path or text value from this list to interact with these elements.",
751
- }),
870
+ text: JSON.stringify(payload),
752
871
  }],
753
872
  };
754
873
  }
@@ -756,15 +875,16 @@ export async function handleGetUiElements(runner, args) {
756
875
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
757
876
  return createErrorResponse(`Failed to get UI elements: ${errorMessage}`, [
758
877
  'Ensure the project is running (use run_project first)',
759
- 'Wait 2-3 seconds after starting for the bridge to initialize',
878
+ 'The bridge may not be ready yet — wait 2-3 seconds after starting, then check get_debug_output if the issue persists',
760
879
  'Check that UDP port 9900 is not blocked',
761
880
  ]);
762
881
  }
763
882
  }
764
883
  export async function handleRunScript(runner, args) {
765
884
  args = normalizeParameters(args);
766
- if (!runner.activeProcess || runner.activeProcess.hasExited) {
767
- return createErrorResponse('No active Godot process. A project must be running to execute scripts.', ['Use run_project to start a Godot project first', 'Wait a few seconds after starting for the game to initialize']);
885
+ const sessionError = ensureRuntimeSession(runner, 'execute scripts');
886
+ if (sessionError) {
887
+ return sessionError;
768
888
  }
769
889
  const script = args.script;
770
890
  if (typeof script !== 'string' || script.trim() === '') {
@@ -790,7 +910,7 @@ export async function handleRunScript(runner, args) {
790
910
  }
791
911
  const timeout = typeof args.timeout === 'number' ? args.timeout : 30000;
792
912
  try {
793
- const responseStr = await runner.sendCommand('run_script', { source: script }, timeout);
913
+ const { response: responseStr, runtimeErrors } = await runner.sendCommandWithErrors('run_script', { source: script }, timeout);
794
914
  let parsed;
795
915
  try {
796
916
  parsed = JSON.parse(responseStr);
@@ -801,14 +921,40 @@ export async function handleRunScript(runner, args) {
801
921
  if (parsed.error) {
802
922
  return createErrorResponse(`Script execution error: ${parsed.error}`, ['Check your GDScript syntax', 'Ensure the script extends RefCounted', 'Check get_debug_output for details']);
803
923
  }
924
+ // Detect false-positive success: GDScript has no try-catch, so runtime errors
925
+ // return null and the real error only appears in stderr.
926
+ if (parsed.success && parsed.result === null && runner.activeSessionMode === 'spawned') {
927
+ if (runtimeErrors.length > 0) {
928
+ const errorContext = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES).join('\n');
929
+ return createErrorResponse(`Script runtime error detected:\n${errorContext}`, [
930
+ 'Fix the GDScript error in your script and retry',
931
+ 'Use get_debug_output for full process output',
932
+ ]);
933
+ }
934
+ return {
935
+ content: [{
936
+ type: 'text',
937
+ text: JSON.stringify({
938
+ success: true,
939
+ result: null,
940
+ warning: 'Script returned null. If unexpected, check get_debug_output for runtime errors — GDScript does not propagate exceptions.',
941
+ tip: 'Call take_screenshot to verify any visual changes, or get_debug_output to review print() output from your script.',
942
+ }),
943
+ }],
944
+ };
945
+ }
946
+ const payload = {
947
+ success: true,
948
+ result: parsed.result,
949
+ tip: 'Call take_screenshot to verify any visual changes, or get_debug_output to review print() output from your script.',
950
+ };
951
+ if (runtimeErrors.length > 0) {
952
+ payload.warnings = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES);
953
+ }
804
954
  return {
805
955
  content: [{
806
956
  type: 'text',
807
- text: JSON.stringify({
808
- success: true,
809
- result: parsed.result,
810
- tip: 'Call take_screenshot to verify any visual changes, or get_debug_output to review print() output from your script.',
811
- }),
957
+ text: JSON.stringify(payload),
812
958
  }],
813
959
  };
814
960
  }
@@ -816,7 +962,7 @@ export async function handleRunScript(runner, args) {
816
962
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
817
963
  return createErrorResponse(`Failed to execute script: ${errorMessage}`, [
818
964
  'Ensure the project is running (use run_project first)',
819
- 'Wait 2-3 seconds after starting for the bridge to initialize',
965
+ 'The bridge may not be ready yet — wait 2-3 seconds after starting, then check get_debug_output if the issue persists',
820
966
  'Check that UDP port 9900 is not blocked',
821
967
  'For long-running scripts, increase the timeout parameter',
822
968
  ]);