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.
- package/README.md +246 -242
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/scripts/mcp_bridge.gd +7 -0
- package/dist/tools/project-tools.d.ts +27 -10
- package/dist/tools/project-tools.d.ts.map +1 -1
- package/dist/tools/project-tools.js +197 -51
- package/dist/tools/project-tools.js.map +1 -1
- package/dist/utils/godot-runner.d.ts +20 -4
- package/dist/utils/godot-runner.d.ts.map +1 -1
- package/dist/utils/godot-runner.js +82 -3
- package/dist/utils/godot-runner.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
84
|
-
data: string;
|
|
85
|
-
mimeType: string;
|
|
86
|
-
text?: undefined;
|
|
87
|
-
} | {
|
|
106
|
+
content: {
|
|
107
|
+
[key: string]: unknown;
|
|
88
108
|
type: string;
|
|
89
|
-
|
|
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;
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
518
|
-
return createErrorResponse('No active
|
|
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:
|
|
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
|
-
|
|
627
|
-
|
|
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.
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
'
|
|
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
|
-
|
|
678
|
-
|
|
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.
|
|
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
|
-
'
|
|
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
|
-
|
|
727
|
-
|
|
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.
|
|
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
|
-
'
|
|
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
|
-
|
|
767
|
-
|
|
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.
|
|
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
|
-
'
|
|
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
|
]);
|