godot-mcp-runtime 2.3.0 → 3.1.0

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.
Files changed (59) hide show
  1. package/README.md +46 -132
  2. package/dist/dispatch.d.ts +1 -11
  3. package/dist/dispatch.d.ts.map +1 -1
  4. package/dist/dispatch.js +32 -33
  5. package/dist/dispatch.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +12 -10
  9. package/dist/index.js.map +1 -1
  10. package/dist/scripts/godot_operations.gd +268 -382
  11. package/dist/scripts/mcp_bridge.gd +206 -44
  12. package/dist/tools/autoload-tools.d.ts +51 -0
  13. package/dist/tools/autoload-tools.d.ts.map +1 -0
  14. package/dist/tools/autoload-tools.js +191 -0
  15. package/dist/tools/autoload-tools.js.map +1 -0
  16. package/dist/tools/node-tools.d.ts +9 -78
  17. package/dist/tools/node-tools.d.ts.map +1 -1
  18. package/dist/tools/node-tools.js +188 -312
  19. package/dist/tools/node-tools.js.map +1 -1
  20. package/dist/tools/project-tools.d.ts +0 -168
  21. package/dist/tools/project-tools.d.ts.map +1 -1
  22. package/dist/tools/project-tools.js +191 -1240
  23. package/dist/tools/project-tools.js.map +1 -1
  24. package/dist/tools/runtime-tools.d.ts +108 -0
  25. package/dist/tools/runtime-tools.d.ts.map +1 -0
  26. package/dist/tools/runtime-tools.js +994 -0
  27. package/dist/tools/runtime-tools.js.map +1 -0
  28. package/dist/tools/scene-tools.d.ts +6 -48
  29. package/dist/tools/scene-tools.d.ts.map +1 -1
  30. package/dist/tools/scene-tools.js +76 -212
  31. package/dist/tools/scene-tools.js.map +1 -1
  32. package/dist/tools/validate-tools.d.ts.map +1 -1
  33. package/dist/tools/validate-tools.js +115 -51
  34. package/dist/tools/validate-tools.js.map +1 -1
  35. package/dist/utils/autoload-ini.d.ts +38 -0
  36. package/dist/utils/autoload-ini.d.ts.map +1 -0
  37. package/dist/utils/autoload-ini.js +124 -0
  38. package/dist/utils/autoload-ini.js.map +1 -0
  39. package/dist/utils/bridge-manager.d.ts +46 -0
  40. package/dist/utils/bridge-manager.d.ts.map +1 -0
  41. package/dist/utils/bridge-manager.js +186 -0
  42. package/dist/utils/bridge-manager.js.map +1 -0
  43. package/dist/utils/bridge-protocol.d.ts +37 -0
  44. package/dist/utils/bridge-protocol.d.ts.map +1 -0
  45. package/dist/utils/bridge-protocol.js +78 -0
  46. package/dist/utils/bridge-protocol.js.map +1 -0
  47. package/dist/utils/godot-runner.d.ts +102 -16
  48. package/dist/utils/godot-runner.d.ts.map +1 -1
  49. package/dist/utils/godot-runner.js +497 -284
  50. package/dist/utils/godot-runner.js.map +1 -1
  51. package/dist/utils/handler-helpers.d.ts +34 -0
  52. package/dist/utils/handler-helpers.d.ts.map +1 -0
  53. package/dist/utils/handler-helpers.js +55 -0
  54. package/dist/utils/handler-helpers.js.map +1 -0
  55. package/dist/utils/logger.d.ts +4 -0
  56. package/dist/utils/logger.d.ts.map +1 -0
  57. package/dist/utils/logger.js +11 -0
  58. package/dist/utils/logger.js.map +1 -0
  59. package/package.json +8 -4
@@ -1,189 +1,16 @@
1
- import { join, basename, sep } from 'path';
2
- import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
3
- import { normalizeParameters, validatePath, validateProjectArgs, createErrorResponse, logDebug, } from '../utils/godot-runner.js';
4
- const MAX_RUNTIME_ERROR_CONTEXT_LINES = 30;
5
- function parseAutoloads(projectFilePath) {
6
- const content = readFileSync(projectFilePath, 'utf8');
7
- const autoloads = [];
8
- let inAutoloadSection = false;
9
- for (const line of content.split('\n')) {
10
- const trimmed = line.trim();
11
- if (trimmed.startsWith('[')) {
12
- inAutoloadSection = trimmed === '[autoload]';
13
- continue;
14
- }
15
- if (!inAutoloadSection || trimmed === '' || trimmed.startsWith(';') || trimmed.startsWith('#'))
16
- continue;
17
- const match = trimmed.match(/^(\w+)="(\*?)([^"]*)"$/);
18
- if (match) {
19
- autoloads.push({ name: match[1], singleton: match[2] === '*', path: match[3] });
20
- }
21
- }
22
- return autoloads;
23
- }
24
- function normalizeAutoloadPath(p) {
25
- return p.startsWith('res://') ? p : `res://${p}`;
26
- }
27
- function addAutoloadEntry(projectFilePath, name, path, singleton) {
28
- const content = readFileSync(projectFilePath, 'utf8');
29
- const lines = content.split('\n');
30
- const entry = `${name}="${singleton ? '*' : ''}${normalizeAutoloadPath(path)}"`;
31
- const sectionIdx = lines.findIndex((l) => l.trim() === '[autoload]');
32
- if (sectionIdx === -1) {
33
- writeFileSync(projectFilePath, content.trimEnd() + '\n\n[autoload]\n' + entry + '\n', 'utf8');
34
- return;
35
- }
36
- let insertIdx = sectionIdx + 1;
37
- while (insertIdx < lines.length && !lines[insertIdx].trim().startsWith('[')) {
38
- insertIdx++;
39
- }
40
- lines.splice(insertIdx, 0, entry);
41
- writeFileSync(projectFilePath, lines.join('\n'), 'utf8');
42
- }
43
- function removeAutoloadEntry(projectFilePath, name) {
44
- const content = readFileSync(projectFilePath, 'utf8');
45
- const lines = content.split('\n');
46
- let inAutoloadSection = false;
47
- let removed = false;
48
- const newLines = lines.filter((line) => {
49
- const trimmed = line.trim();
50
- if (trimmed.startsWith('[')) {
51
- inAutoloadSection = trimmed === '[autoload]';
52
- return true;
53
- }
54
- if (inAutoloadSection) {
55
- const match = trimmed.match(/^(\w+)=/);
56
- if (match && match[1] === name) {
57
- removed = true;
58
- return false;
59
- }
60
- }
61
- return true;
62
- });
63
- if (removed)
64
- writeFileSync(projectFilePath, newLines.join('\n'), 'utf8');
65
- return removed;
66
- }
67
- function updateAutoloadEntry(projectFilePath, name, newPath, singleton) {
68
- const content = readFileSync(projectFilePath, 'utf8');
69
- const lines = content.split('\n');
70
- let inAutoloadSection = false;
71
- let updated = false;
72
- const newLines = lines.map((line) => {
73
- const trimmed = line.trim();
74
- if (trimmed.startsWith('[')) {
75
- inAutoloadSection = trimmed === '[autoload]';
76
- return line;
77
- }
78
- if (inAutoloadSection) {
79
- const match = trimmed.match(/^(\w+)="(\*?)([^"]*)"$/);
80
- if (match && match[1] === name) {
81
- const effectiveSingleton = singleton !== undefined ? singleton : match[2] === '*';
82
- const effectivePath = newPath !== undefined ? normalizeAutoloadPath(newPath) : match[3];
83
- updated = true;
84
- return `${name}="${effectiveSingleton ? '*' : ''}${effectivePath}"`;
85
- }
86
- }
87
- return line;
88
- });
89
- if (updated)
90
- writeFileSync(projectFilePath, newLines.join('\n'), 'utf8');
91
- return updated;
1
+ import { join, basename } from 'path';
2
+ import { existsSync, readdirSync, readFileSync } from 'fs';
3
+ import { normalizeParameters, validatePath, validateSubPath, validateProjectArgs, createErrorResponse, getErrorMessage, projectGodotPath, } from '../utils/godot-runner.js';
4
+ import { logDebug } from '../utils/logger.js';
5
+ function fileExtension(name) {
6
+ const dotIdx = name.lastIndexOf('.');
7
+ return dotIdx >= 0 ? name.slice(dotIdx + 1).toLowerCase() : '';
92
8
  }
93
9
  // --- Tool definitions ---
94
10
  export const projectToolDefinitions = [
95
- {
96
- name: 'launch_editor',
97
- description: 'Open the Godot editor GUI for a project for the human user. Use only when the user explicitly asks to "open the editor"; for any agent-driven work, use the headless scene/node tools (add_node, set_node_property, etc.) instead — the editor cannot be controlled programmatically. Returns immediately after spawning. Errors if projectPath has no project.godot.',
98
- inputSchema: {
99
- type: 'object',
100
- properties: {
101
- projectPath: {
102
- type: 'string',
103
- description: 'Path to the Godot project directory',
104
- },
105
- },
106
- required: ['projectPath'],
107
- },
108
- },
109
- {
110
- name: 'run_project',
111
- description: 'Spawn a Godot project as a child process with stdout/stderr captured. This is the preferred entry to runtime tools — use whenever MCP can launch the game itself. Required before take_screenshot, simulate_input, get_ui_elements, run_script, or get_debug_output. For a Godot process you launched yourself (debugger attached, custom flags, IDE run), use attach_project instead. Verifies MCP bridge readiness before returning success. Call stop_project when done. Errors if projectPath is not a Godot project or another run is already active.',
112
- inputSchema: {
113
- type: 'object',
114
- properties: {
115
- projectPath: {
116
- type: 'string',
117
- description: 'Path to the Godot project directory',
118
- },
119
- scene: {
120
- type: 'string',
121
- description: 'Scene to run (path relative to project, e.g. "scenes/main.tscn"). Omit to use the project\'s main scene.',
122
- },
123
- background: {
124
- type: 'boolean',
125
- description: 'If true, hides the Godot window off-screen and blocks all physical keyboard and mouse input, while keeping programmatic input (simulate_input, run_script) and screenshots fully active. Useful for automated agent-driven testing where the window should not be visible or interactive.',
126
- },
127
- },
128
- required: ['projectPath'],
129
- },
130
- },
131
- {
132
- name: 'attach_project',
133
- description: 'Attach runtime MCP tools to a manually launched Godot process without spawning one. Use this only when the user is running Godot themselves (debugger attached, custom CLI flags, IDE run) — for the standard case, use run_project. Injects the McpBridge autoload and marks the project active. Call once before launching Godot, then again with waitForBridge:true after launch to confirm the bridge is listening (up to 15s). Use detach_project or stop_project when done. get_debug_output is unavailable in attached mode (stdout/stderr not captured).',
134
- inputSchema: {
135
- type: 'object',
136
- properties: {
137
- projectPath: {
138
- type: 'string',
139
- description: 'Path to the Godot project directory',
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
- },
145
- },
146
- required: ['projectPath'],
147
- },
148
- },
149
- {
150
- name: 'detach_project',
151
- description: 'Clear attached-mode runtime state and remove the injected McpBridge autoload. Does NOT stop the manually launched Godot process — that stays running. Use after attach_project when you are done driving the game from MCP. For spawned sessions (run_project), use stop_project instead. No-op if no attached session exists.',
152
- annotations: { destructiveHint: true, idempotentHint: true },
153
- inputSchema: {
154
- type: 'object',
155
- properties: {},
156
- required: [],
157
- },
158
- },
159
- {
160
- name: 'get_debug_output',
161
- description: 'Get captured stdout/stderr from a spawned Godot project. Use whenever runtime tools fail unexpectedly — script errors, missing nodes, and crash backtraces all surface here. Requires run_project (not attach_project; attached mode does not capture output). Returns { output, errors, running, exitCode? } with the last `limit` lines (default 200, from the end). Reports attached-mode unavailability gracefully.',
162
- annotations: { readOnlyHint: true },
163
- inputSchema: {
164
- type: 'object',
165
- properties: {
166
- limit: {
167
- type: 'number',
168
- description: 'Max lines to return (default: 200, from end of output)',
169
- },
170
- },
171
- required: [],
172
- },
173
- },
174
- {
175
- name: 'stop_project',
176
- description: 'Stop the spawned Godot project and clean up MCP bridge state. Always call when done with runtime testing — even after a crash — to free the single process slot so run_project can be called again. For attached sessions, this detaches without killing the externally launched process. No-op if no session is active.',
177
- annotations: { destructiveHint: true, idempotentHint: true },
178
- inputSchema: {
179
- type: 'object',
180
- properties: {},
181
- required: [],
182
- },
183
- },
184
11
  {
185
12
  name: 'list_projects',
186
- description: 'Find Godot projects under a directory by locating project.godot files. Use to discover available projects when the user has not specified one; for inspecting a known project, use get_project_info. recursive:true descends into subdirectories (skipping hidden ones); default false checks only the directory itself and its immediate children. Returns { projects: [{ path, name }] }, empty array on no matches.',
13
+ description: 'Find Godot projects under a directory by locating project.godot files. Use to discover available projects when the user has not specified one; for inspecting a known project, use get_project_info. recursive:true descends into subdirectories (skipping hidden ones); default false checks only the directory itself and its immediate children. Returns: [{ path, name }], empty array on no matches.',
187
14
  annotations: { readOnlyHint: true },
188
15
  inputSchema: {
189
16
  type: 'object',
@@ -202,7 +29,7 @@ export const projectToolDefinitions = [
202
29
  },
203
30
  {
204
31
  name: 'get_project_info',
205
- description: 'Get metadata about a Godot project: name, path, Godot version, and a structure summary (counts of scenes/scripts/assets/other). Omit projectPath to get just the Godot version (useful for capability checks). Returns { name, path, godotVersion, structure } or { godotVersion } when projectPath is omitted. Errors if projectPath is set but lacks project.godot.',
32
+ description: 'Get metadata about a Godot project: name, path, Godot version, and a structure summary (counts of scenes/scripts/assets/other). Omit projectPath to get just the Godot version (useful for capability checks). Returns: { name, path, godotVersion, structure } or { godotVersion } when projectPath is omitted. Errors if projectPath is set but lacks project.godot.',
206
33
  annotations: { readOnlyHint: true },
207
34
  inputSchema: {
208
35
  type: 'object',
@@ -215,201 +42,9 @@ export const projectToolDefinitions = [
215
42
  required: [],
216
43
  },
217
44
  },
218
- {
219
- name: 'take_screenshot',
220
- description: 'Capture a PNG screenshot of the running Godot viewport. Use after simulate_input or run_script to verify visual changes. Requires an active runtime session (run_project or attach_project). Returns the image inline as base64. Also saved to .mcp/screenshots/ in the project directory for later reference. Errors if no session is active or the bridge does not respond within timeout (default 10000ms).',
221
- annotations: { readOnlyHint: true },
222
- inputSchema: {
223
- type: 'object',
224
- properties: {
225
- timeout: {
226
- type: 'number',
227
- description: 'Timeout in milliseconds to wait for the screenshot (default: 10000)',
228
- },
229
- },
230
- required: [],
231
- },
232
- },
233
- {
234
- name: 'simulate_input',
235
- 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}]\n\nReturns { success, actions_processed, warnings? }. Errors if no runtime session is active.',
236
- inputSchema: {
237
- type: 'object',
238
- properties: {
239
- actions: {
240
- type: 'array',
241
- description: 'Array of input actions to execute sequentially. Each object must have a "type" field.',
242
- items: {
243
- type: 'object',
244
- properties: {
245
- type: {
246
- type: 'string',
247
- enum: ['key', 'mouse_button', 'mouse_motion', 'click_element', 'action', 'wait'],
248
- description: 'The type of input action',
249
- },
250
- key: {
251
- type: 'string',
252
- description: '[key] Key name (e.g. "W", "Space", "Escape", "Up")',
253
- },
254
- pressed: {
255
- type: 'boolean',
256
- description: '[key, mouse_button, action] Whether the input is pressed (true) or released (false)',
257
- },
258
- shift: { type: 'boolean', description: '[key] Shift modifier' },
259
- ctrl: { type: 'boolean', description: '[key] Ctrl modifier' },
260
- alt: { type: 'boolean', description: '[key] Alt modifier' },
261
- button: {
262
- type: 'string',
263
- enum: ['left', 'right', 'middle'],
264
- description: '[mouse_button, click_element] Mouse button (default: left)',
265
- },
266
- x: {
267
- type: 'number',
268
- description: '[mouse_button, mouse_motion] X position in viewport pixels',
269
- },
270
- y: {
271
- type: 'number',
272
- description: '[mouse_button, mouse_motion] Y position in viewport pixels',
273
- },
274
- relative_x: {
275
- type: 'number',
276
- description: '[mouse_motion] Relative X movement in pixels',
277
- },
278
- relative_y: {
279
- type: 'number',
280
- description: '[mouse_motion] Relative Y movement in pixels',
281
- },
282
- double_click: {
283
- type: 'boolean',
284
- description: '[mouse_button, click_element] Double click',
285
- },
286
- element: {
287
- type: 'string',
288
- 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.',
289
- },
290
- action: {
291
- type: 'string',
292
- description: '[action] Godot input action name (as defined in Project Settings > Input Map)',
293
- },
294
- strength: {
295
- type: 'number',
296
- description: '[action] Action strength (0–1, default 1.0)',
297
- },
298
- ms: {
299
- type: 'number',
300
- description: '[wait] Duration in milliseconds to pause before the next action',
301
- },
302
- },
303
- required: ['type'],
304
- },
305
- },
306
- },
307
- required: ['actions'],
308
- },
309
- },
310
- {
311
- name: 'get_ui_elements',
312
- description: 'Walk the running scene tree and return all Control nodes with positions, sizes, types, and text content. Always call this before simulate_input click_element actions to discover valid element names and paths. Requires an active runtime session (run_project or attach_project). visibleOnly defaults true; pass false to include hidden Controls. filter narrows by class. Returns { elements: [{ name, path, type, rect, visible, text?, disabled?, tooltip? }] }.',
313
- annotations: { readOnlyHint: true },
314
- inputSchema: {
315
- type: 'object',
316
- properties: {
317
- visibleOnly: {
318
- type: 'boolean',
319
- description: 'Only return nodes where Control.visible is true (default: true). Set false to include hidden elements.',
320
- },
321
- filter: {
322
- type: 'string',
323
- description: 'Filter by Control node type (e.g. "Button", "Label", "LineEdit")',
324
- },
325
- },
326
- required: [],
327
- },
328
- },
329
- {
330
- name: 'run_script',
331
- 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.',
332
- inputSchema: {
333
- type: 'object',
334
- properties: {
335
- script: {
336
- type: 'string',
337
- description: 'GDScript source code. Must contain "extends RefCounted" and "func execute(scene_tree: SceneTree) -> Variant".',
338
- },
339
- timeout: {
340
- type: 'number',
341
- description: 'Timeout in ms (default: 30000). Increase for long-running scripts.',
342
- },
343
- },
344
- required: ['script'],
345
- },
346
- },
347
- {
348
- name: 'list_autoloads',
349
- description: 'List all registered autoloads in a project with paths and singleton status. Use first when diagnosing headless failures — broken autoloads crash all headless ops, so this tells you what is loaded. No Godot process required (reads project.godot directly). Returns { autoloads: [{ name, path, singleton }] }.',
350
- annotations: { readOnlyHint: true },
351
- inputSchema: {
352
- type: 'object',
353
- properties: {
354
- projectPath: { type: 'string', description: 'Path to the Godot project directory' },
355
- },
356
- required: ['projectPath'],
357
- },
358
- },
359
- {
360
- name: 'add_autoload',
361
- description: 'Register a new autoload in a project. autoloadPath accepts "res://..." or a project-relative path (auto-prefixed). singleton defaults true (accessible globally by name). No Godot process required. Warning: autoloads initialize in headless mode — a broken script will crash every subsequent headless op; validate before adding. Errors if an autoload with the same name already exists; use update_autoload to modify.',
362
- inputSchema: {
363
- type: 'object',
364
- properties: {
365
- projectPath: { type: 'string', description: 'Path to the Godot project directory' },
366
- autoloadName: {
367
- type: 'string',
368
- description: 'Name of the autoload node (e.g. "MyManager")',
369
- },
370
- autoloadPath: {
371
- type: 'string',
372
- description: 'Path to the script or scene (e.g. "res://autoload/my_manager.gd" or "autoload/my_manager.gd")',
373
- },
374
- singleton: {
375
- type: 'boolean',
376
- description: 'Register as a globally accessible singleton by name (default: true)',
377
- },
378
- },
379
- required: ['projectPath', 'autoloadName', 'autoloadPath'],
380
- },
381
- },
382
- {
383
- name: 'remove_autoload',
384
- description: 'Unregister an autoload from a project by name. Use to recover from a broken autoload that is crashing headless ops. No Godot process required. Errors if no autoload with that name exists.',
385
- annotations: { destructiveHint: true },
386
- inputSchema: {
387
- type: 'object',
388
- properties: {
389
- projectPath: { type: 'string', description: 'Path to the Godot project directory' },
390
- autoloadName: { type: 'string', description: 'Name of the autoload to remove' },
391
- },
392
- required: ['projectPath', 'autoloadName'],
393
- },
394
- },
395
- {
396
- name: 'update_autoload',
397
- description: "Modify an existing autoload's path or singleton flag. Pass either or both — omitted fields keep their current value. Use instead of remove_autoload + add_autoload (single edit, no orphan window). No Godot process required. Errors if autoloadName is not registered.",
398
- annotations: { idempotentHint: true },
399
- inputSchema: {
400
- type: 'object',
401
- properties: {
402
- projectPath: { type: 'string', description: 'Path to the Godot project directory' },
403
- autoloadName: { type: 'string', description: 'Name of the autoload to update' },
404
- autoloadPath: { type: 'string', description: 'New path to the script or scene' },
405
- singleton: { type: 'boolean', description: 'New singleton flag' },
406
- },
407
- required: ['projectPath', 'autoloadName'],
408
- },
409
- },
410
45
  {
411
46
  name: 'get_project_files',
412
- description: 'Return a recursive file tree of a Godot project as nested { name, type, path, extension?, children? } objects. Use to discover project structure when paths are unknown. Pass extensions to filter (e.g. ["gd","tscn"]); maxDepth caps recursion (-1 unlimited). Skips hidden (dot-prefixed) entries and the .mcp directory.',
47
+ description: 'Return a recursive file tree of a Godot project. Use to discover project structure when paths are unknown. Pass extensions to filter (e.g. ["gd","tscn"]); maxDepth caps recursion (-1 unlimited). Skips hidden (dot-prefixed) entries and the .mcp directory. Returns: { name, type, path, extension?, children? } (nested tree).',
413
48
  annotations: { readOnlyHint: true },
414
49
  inputSchema: {
415
50
  type: 'object',
@@ -430,7 +65,7 @@ export const projectToolDefinitions = [
430
65
  },
431
66
  {
432
67
  name: 'search_project',
433
- description: 'Plain-text (substring) search across project files. Use to find references, callers, or signatures across the codebase. Default fileTypes is ["gd","tscn","cs","gdshader"]; caseSensitive default false; maxResults default 100 (truncated:true if hit). Returns { matches: [{ file, lineNumber, line }], truncated }. Skips hidden entries and the .mcp directory.',
68
+ description: 'Plain-text (substring) search across project files. Use to find references, callers, or signatures across the codebase. Default fileTypes is ["gd","tscn","cs","gdshader"]; caseSensitive default false; maxResults default 100. Skips hidden entries and the .mcp directory. Returns: matches[] (project-relative file, 1-indexed lineNumber, line text) and truncated:true when maxResults was hit consider raising it.',
434
69
  annotations: { readOnlyHint: true },
435
70
  inputSchema: {
436
71
  type: 'object',
@@ -447,788 +82,145 @@ export const projectToolDefinitions = [
447
82
  },
448
83
  required: ['projectPath', 'pattern'],
449
84
  },
450
- },
451
- {
452
- name: 'get_scene_dependencies',
453
- description: 'Parse a .tscn file for ext_resource references (scripts, textures, subscenes). Use to inspect what a scene depends on before refactoring or moving files. Returns { scene, dependencies: [{ path, type, uid? }] }. Errors if scene file does not exist.',
454
- annotations: { readOnlyHint: true },
455
- inputSchema: {
85
+ outputSchema: {
456
86
  type: 'object',
457
87
  properties: {
458
- projectPath: { type: 'string', description: 'Path to the Godot project directory' },
459
- scenePath: {
460
- type: 'string',
461
- description: 'Path to the .tscn file relative to the project root (e.g. "scenes/main.tscn")',
88
+ matches: {
89
+ type: 'array',
90
+ items: {
91
+ type: 'object',
92
+ properties: {
93
+ file: { type: 'string' },
94
+ lineNumber: { type: 'number' },
95
+ line: { type: 'string' },
96
+ },
97
+ },
462
98
  },
99
+ truncated: { type: 'boolean' },
463
100
  },
464
- required: ['projectPath', 'scenePath'],
465
101
  },
466
102
  },
467
103
  {
468
- name: 'get_project_settings',
469
- description: 'Parse project.godot into structured JSON. Use to inspect configured display, input, rendering, etc. settings without launching Godot. Pass section to filter to one INI section (e.g. "display", "application"). Returns { settings: { [section]: { [key]: value } } } or { settings: { [key]: value } } when section is given. Complex Godot types are returned as raw strings; keys outside any section appear under __global__.',
104
+ name: 'get_scene_dependencies',
105
+ description: 'Parse a .tscn file for ext_resource references (scripts, textures, subscenes). Use to inspect what a scene depends on before refactoring or moving files. Returns: the queried scene path and dependencies[] from ext_resource refs (path, type, optional uid). Errors if scene file does not exist.',
470
106
  annotations: { readOnlyHint: true },
471
107
  inputSchema: {
472
108
  type: 'object',
473
- properties: {
474
- projectPath: { type: 'string', description: 'Path to the Godot project directory' },
475
- section: {
476
- type: 'string',
477
- description: 'Filter to a specific INI section (e.g. "display", "application"). Omit for all sections.',
478
- },
479
- },
480
- required: ['projectPath'],
481
- },
482
- },
483
- ];
484
- function ensureRuntimeSession(runner, actionDescription) {
485
- if (!runner.activeSessionMode || !runner.activeProjectPath) {
486
- return createErrorResponse(`No active runtime session. A project must be running or attached to ${actionDescription}.`, [
487
- 'Use run_project to start a Godot project first',
488
- 'Or use attach_project before launching Godot manually',
489
- ]);
490
- }
491
- if (runner.activeSessionMode === 'spawned' &&
492
- (!runner.activeProcess || runner.activeProcess.hasExited)) {
493
- return createErrorResponse(`The spawned Godot process has exited and cannot ${actionDescription}.`, [
494
- 'Use get_debug_output to inspect the last captured logs',
495
- 'Call stop_project to clean up, then run_project again',
496
- ]);
497
- }
498
- return null;
499
- }
500
- function findGodotProjects(directory, recursive) {
501
- const projects = [];
502
- try {
503
- const projectFile = join(directory, 'project.godot');
504
- if (existsSync(projectFile)) {
505
- projects.push({
506
- path: directory,
507
- name: basename(directory),
508
- });
509
- }
510
- const entries = readdirSync(directory, { withFileTypes: true });
511
- for (const entry of entries) {
512
- if (!entry.isDirectory() || entry.name.startsWith('.'))
513
- continue;
514
- const subdir = join(directory, entry.name);
515
- if (existsSync(join(subdir, 'project.godot'))) {
516
- projects.push({ path: subdir, name: entry.name });
517
- }
518
- else if (recursive) {
519
- projects.push(...findGodotProjects(subdir, true));
520
- }
521
- }
522
- }
523
- catch (error) {
524
- logDebug(`Error searching directory ${directory}: ${error}`);
525
- }
526
- return projects;
527
- }
528
- function getProjectStructure(projectPath) {
529
- const structure = {
530
- scenes: 0,
531
- scripts: 0,
532
- assets: 0,
533
- other: 0,
534
- };
535
- const scanDirectory = (currentPath) => {
536
- try {
537
- const entries = readdirSync(currentPath, { withFileTypes: true });
538
- for (const entry of entries) {
539
- const entryPath = join(currentPath, entry.name);
540
- if (entry.name.startsWith('.')) {
541
- continue;
542
- }
543
- if (entry.isDirectory()) {
544
- scanDirectory(entryPath);
545
- }
546
- else if (entry.isFile()) {
547
- const ext = entry.name.split('.').pop()?.toLowerCase();
548
- if (ext === 'tscn') {
549
- structure.scenes++;
550
- }
551
- else if (ext === 'gd' || ext === 'gdscript' || ext === 'cs') {
552
- structure.scripts++;
553
- }
554
- else if (['png', 'jpg', 'jpeg', 'webp', 'svg', 'ttf', 'wav', 'mp3', 'ogg'].includes(ext || '')) {
555
- structure.assets++;
556
- }
557
- else {
558
- structure.other++;
559
- }
560
- }
561
- }
562
- }
563
- catch (error) {
564
- logDebug(`Error scanning directory ${currentPath}: ${error}`);
565
- }
566
- };
567
- scanDirectory(projectPath);
568
- return structure;
569
- }
570
- export async function handleLaunchEditor(runner, args) {
571
- args = normalizeParameters(args);
572
- if (!args.projectPath) {
573
- return createErrorResponse('Project path is required', [
574
- 'Provide a valid path to a Godot project directory',
575
- ]);
576
- }
577
- if (!validatePath(args.projectPath)) {
578
- return createErrorResponse('Invalid project path', [
579
- 'Provide a valid path without ".." or other potentially unsafe characters',
580
- ]);
581
- }
582
- try {
583
- if (!runner.getGodotPath()) {
584
- await runner.detectGodotPath();
585
- if (!runner.getGodotPath()) {
586
- return createErrorResponse('Could not find a valid Godot executable path', [
587
- 'Ensure Godot is installed correctly',
588
- 'Set GODOT_PATH environment variable',
589
- ]);
590
- }
591
- }
592
- const projectFile = join(args.projectPath, 'project.godot');
593
- if (!existsSync(projectFile)) {
594
- return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, [
595
- 'Ensure the path points to a directory containing a project.godot file',
596
- 'Use list_projects to find valid Godot projects',
597
- ]);
598
- }
599
- logDebug(`Launching Godot editor for project: ${args.projectPath}`);
600
- const process = runner.launchEditor(args.projectPath);
601
- process.on('error', (err) => {
602
- console.error('Failed to start Godot editor:', err);
603
- });
604
- return {
605
- content: [
606
- {
607
- type: 'text',
608
- 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.`,
609
- },
610
- ],
611
- };
612
- }
613
- catch (error) {
614
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
615
- return createErrorResponse(`Failed to launch Godot editor: ${errorMessage}`, [
616
- 'Ensure Godot is installed correctly',
617
- 'Check if the GODOT_PATH environment variable is set correctly',
618
- ]);
619
- }
620
- }
621
- export async function handleRunProject(runner, args) {
622
- args = normalizeParameters(args);
623
- if (!args.projectPath) {
624
- return createErrorResponse('Project path is required', [
625
- 'Provide a valid path to a Godot project directory',
626
- ]);
627
- }
628
- if (!validatePath(args.projectPath)) {
629
- return createErrorResponse('Invalid project path', [
630
- 'Provide a valid path without ".." or other potentially unsafe characters',
631
- ]);
632
- }
633
- try {
634
- const projectFile = join(args.projectPath, 'project.godot');
635
- if (!existsSync(projectFile)) {
636
- return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, [
637
- 'Ensure the path points to a directory containing a project.godot file',
638
- 'Use list_projects to find valid Godot projects',
639
- ]);
640
- }
641
- const background = args.background === true;
642
- runner.runProject(args.projectPath, args.scene, background);
643
- const bridgeResult = await runner.waitForBridge();
644
- if (!bridgeResult.ready) {
645
- if (runner.activeProcess && runner.activeProcess.hasExited) {
646
- return createErrorResponse(`Godot process exited before the MCP bridge could initialize.\n${bridgeResult.error || ''}`, [
647
- 'Check get_debug_output for runtime errors',
648
- 'Verify a display server is available (Wayland/X11)',
649
- 'Check for broken autoloads with list_autoloads',
650
- 'Call stop_project to clean up, then try again',
651
- ]);
652
- }
653
- const lines = [
654
- 'Godot process started, but the MCP bridge did not respond within 8 seconds.',
655
- '- The process is running — bridge may still be initializing',
656
- '- Use get_debug_output to investigate',
657
- '- Runtime tools may work if you retry after a moment',
658
- '- Call stop_project when done',
659
- ];
660
- if (background) {
661
- lines.push('- Background mode: window hidden, physical input blocked');
662
- }
663
- return createErrorResponse(lines.join('\n'), [
664
- 'Use get_debug_output to inspect the last captured logs',
665
- 'Check that UDP port 9900 is not occupied by another Godot process',
666
- 'Call stop_project to clean up, then run_project again',
667
- ]);
668
- }
669
- const lines = [
670
- 'Godot project started and MCP bridge is ready.',
671
- '- Runtime tools (take_screenshot, simulate_input, get_ui_elements, run_script) are available now',
672
- '- Use get_debug_output to check runtime output and errors',
673
- '- Call stop_project when done',
674
- ];
675
- if (background) {
676
- lines.push('- Background mode: window hidden, physical input blocked');
677
- }
678
- return {
679
- content: [{ type: 'text', text: lines.join('\n') }],
680
- };
681
- }
682
- catch (error) {
683
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
684
- return createErrorResponse(`Failed to run Godot project: ${errorMessage}`, [
685
- 'Ensure Godot is installed correctly',
686
- 'Check if the GODOT_PATH environment variable is set correctly',
687
- ]);
688
- }
689
- }
690
- export async function handleAttachProject(runner, args) {
691
- args = normalizeParameters(args);
692
- if (!args.projectPath) {
693
- return createErrorResponse('Project path is required', [
694
- 'Provide a valid path to a Godot project directory',
695
- ]);
696
- }
697
- if (!validatePath(args.projectPath)) {
698
- return createErrorResponse('Invalid project path', [
699
- 'Provide a valid path without ".." or other potentially unsafe characters',
700
- ]);
701
- }
702
- try {
703
- const projectFile = join(args.projectPath, 'project.godot');
704
- if (!existsSync(projectFile)) {
705
- return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, [
706
- 'Ensure the path points to a directory containing a project.godot file',
707
- 'Use list_projects to find valid Godot projects',
708
- ]);
709
- }
710
- runner.attachProject(args.projectPath);
711
- if (args.waitForBridge === true) {
712
- const bridgeResult = await runner.waitForBridgeAttached();
713
- if (!bridgeResult.ready) {
714
- return createErrorResponse(`Project attached but the MCP bridge is not ready.\n${bridgeResult.error || ''}`, [
715
- 'Verify Godot is running with this project',
716
- 'The McpBridge autoload must be initialized and listening on UDP port 9900',
717
- 'Check that no other Godot project is occupying port 9900',
718
- 'Use detach_project or stop_project when done',
719
- ]);
720
- }
721
- return {
722
- content: [
723
- {
724
- type: 'text',
725
- text: [
726
- 'Project attached and MCP bridge is ready.',
727
- '- Runtime tools (take_screenshot, simulate_input, get_ui_elements, run_script) are available now',
728
- '- get_debug_output is unavailable in attached mode because MCP did not spawn the process',
729
- '- Use detach_project or stop_project when done to clean up the injected bridge state',
730
- ].join('\n'),
731
- },
732
- ],
733
- };
734
- }
735
- return {
736
- content: [
737
- {
738
- type: 'text',
739
- text: [
740
- 'Project attached for manual runtime use.',
741
- '- Launch Godot yourself, then call attach_project again with waitForBridge: true to confirm readiness',
742
- '- Or use runtime tools directly — they will fail with a clear error if the bridge is not yet listening',
743
- '- get_debug_output is unavailable in attached mode because MCP did not spawn the process',
744
- '- Use detach_project or stop_project when done to clean up the injected bridge state',
745
- ].join('\n'),
746
- },
747
- ],
748
- };
749
- }
750
- catch (error) {
751
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
752
- return createErrorResponse(`Failed to attach project: ${errorMessage}`, [
753
- 'Check if project.godot is accessible',
754
- 'Ensure MCP can write the bridge autoload into the project',
755
- ]);
756
- }
757
- }
758
- export function handleDetachProject(runner) {
759
- if (runner.activeSessionMode !== 'attached') {
760
- return createErrorResponse('No attached project to detach.', [
761
- 'Use attach_project first for manual-launch workflows',
762
- 'If MCP launched the game, use stop_project instead',
763
- ]);
764
- }
765
- const result = runner.stopProject();
766
- return {
767
- content: [
768
- {
769
- type: 'text',
770
- text: JSON.stringify({
771
- message: 'Detached attached project and cleaned MCP bridge state',
772
- externalProcessPreserved: result.externalProcessPreserved === true,
773
- }),
774
- },
775
- ],
776
- };
777
- }
778
- export function handleGetDebugOutput(runner, args = {}) {
779
- args = normalizeParameters(args);
780
- if (!runner.activeSessionMode) {
781
- return createErrorResponse('No active runtime session.', [
782
- 'Use run_project to start a Godot project first',
783
- 'Or use attach_project before launching Godot manually',
784
- ]);
785
- }
786
- if (runner.activeSessionMode === 'attached') {
787
- return {
788
- content: [
789
- {
790
- type: 'text',
791
- text: JSON.stringify({
792
- output: [],
793
- errors: [],
794
- running: null,
795
- attached: true,
796
- tip: 'Attached mode does not capture stdout/stderr because Godot was launched outside MCP.',
797
- }),
798
- },
799
- ],
800
- };
801
- }
802
- const proc = runner.activeProcess;
803
- if (!proc) {
804
- return createErrorResponse('No active spawned process is available for debug output.', [
805
- 'Use run_project to start a Godot project first',
806
- 'Or use attach_project only when stdout/stderr capture is not needed',
807
- ]);
808
- }
809
- const limit = typeof args.limit === 'number' ? args.limit : 200;
810
- const response = {
811
- output: proc.output.slice(-limit),
812
- errors: proc.errors.slice(-limit),
813
- running: !proc.hasExited,
814
- };
815
- if (proc.hasExited) {
816
- response.exitCode = proc.exitCode;
817
- response.tip =
818
- 'Process has exited. Call stop_project to clean up the process slot before starting a new one.';
819
- }
820
- return {
821
- content: [
822
- {
823
- type: 'text',
824
- text: JSON.stringify(response),
825
- },
826
- ],
827
- };
828
- }
829
- export function handleStopProject(runner) {
830
- const result = runner.stopProject();
831
- if (!result) {
832
- return createErrorResponse('No active Godot process to stop.', [
833
- 'Use run_project to start a Godot project first',
834
- 'The process may have already terminated',
835
- ]);
836
- }
837
- return {
838
- content: [
839
- {
840
- type: 'text',
841
- text: JSON.stringify({
842
- message: result.mode === 'attached'
843
- ? 'Attached project detached and MCP bridge state cleaned up'
844
- : 'Godot project stopped',
845
- mode: result.mode,
846
- externalProcessPreserved: result.externalProcessPreserved === true,
847
- finalOutput: result.output.slice(-200),
848
- finalErrors: result.errors.slice(-200),
849
- }),
850
- },
851
- ],
852
- };
853
- }
854
- export async function handleListProjects(args) {
855
- args = normalizeParameters(args);
856
- if (!args.directory) {
857
- return createErrorResponse('Directory is required', [
858
- 'Provide a valid directory path to search for Godot projects',
859
- ]);
860
- }
861
- if (!validatePath(args.directory)) {
862
- return createErrorResponse('Invalid directory path', [
863
- 'Provide a valid path without ".." or other potentially unsafe characters',
864
- ]);
865
- }
866
- try {
867
- if (!existsSync(args.directory)) {
868
- return createErrorResponse(`Directory does not exist: ${args.directory}`, [
869
- 'Provide a valid directory path that exists on the system',
870
- ]);
871
- }
872
- const recursive = args.recursive === true;
873
- const projects = findGodotProjects(args.directory, recursive);
874
- return {
875
- content: [{ type: 'text', text: JSON.stringify(projects) }],
876
- };
877
- }
878
- catch (error) {
879
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
880
- return createErrorResponse(`Failed to list projects: ${errorMessage}`, [
881
- 'Ensure the directory exists and is accessible',
882
- 'Check if you have permission to read the directory',
883
- ]);
884
- }
885
- }
886
- export async function handleGetProjectInfo(runner, args) {
887
- args = normalizeParameters(args);
888
- try {
889
- const version = await runner.getVersion();
890
- // If no project path, return just the Godot version
891
- if (!args.projectPath) {
892
- return {
893
- content: [{ type: 'text', text: JSON.stringify({ godotVersion: version }) }],
894
- };
895
- }
896
- if (!validatePath(args.projectPath)) {
897
- return createErrorResponse('Invalid project path', [
898
- 'Provide a valid path without ".." or other potentially unsafe characters',
899
- ]);
900
- }
901
- const projectFile = join(args.projectPath, 'project.godot');
902
- if (!existsSync(projectFile)) {
903
- return createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, [
904
- 'Ensure the path points to a directory containing a project.godot file',
905
- 'Use list_projects to find valid Godot projects',
906
- ]);
907
- }
908
- const projectStructure = getProjectStructure(args.projectPath);
909
- let projectName = basename(args.projectPath);
910
- try {
911
- const projectFileContent = readFileSync(projectFile, 'utf8');
912
- const configNameMatch = projectFileContent.match(/config\/name="([^"]+)"/);
913
- if (configNameMatch && configNameMatch[1]) {
914
- projectName = configNameMatch[1];
915
- logDebug(`Found project name in config: ${projectName}`);
916
- }
917
- }
918
- catch (error) {
919
- logDebug(`Error reading project file: ${error}`);
920
- }
921
- return {
922
- content: [
923
- {
924
- type: 'text',
925
- text: JSON.stringify({
926
- name: projectName,
927
- path: args.projectPath,
928
- godotVersion: version,
929
- structure: projectStructure,
930
- }),
931
- },
932
- ],
933
- };
934
- }
935
- catch (error) {
936
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
937
- return createErrorResponse(`Failed to get project info: ${errorMessage}`, [
938
- 'Ensure Godot is installed correctly',
939
- 'Check if the GODOT_PATH environment variable is set correctly',
940
- ]);
941
- }
942
- }
943
- export async function handleTakeScreenshot(runner, args) {
944
- args = normalizeParameters(args);
945
- const sessionError = ensureRuntimeSession(runner, 'take a screenshot');
946
- if (sessionError) {
947
- return sessionError;
948
- }
949
- const timeout = typeof args.timeout === 'number' ? args.timeout : 10000;
950
- try {
951
- const { response: responseStr, runtimeErrors } = await runner.sendCommandWithErrors('screenshot', {}, timeout);
952
- let parsed;
953
- try {
954
- parsed = JSON.parse(responseStr);
955
- }
956
- catch {
957
- return createErrorResponse(`Invalid response from screenshot server: ${responseStr}`, [
958
- 'The game may not have fully initialized yet',
959
- 'Try again after a few seconds',
960
- ]);
961
- }
962
- if (parsed.error) {
963
- return createErrorResponse(`Screenshot server error: ${parsed.error}`, [
964
- 'Ensure the game viewport is active',
965
- 'Try again after a moment',
966
- ]);
967
- }
968
- if (!parsed.path) {
969
- return createErrorResponse('Screenshot server returned no file path', [
970
- 'Try again after a few seconds',
971
- ]);
972
- }
973
- // Normalize path for the local filesystem (forward slashes from GDScript)
974
- const screenshotPath = sep === '\\' ? parsed.path.replace(/\//g, '\\') : parsed.path;
975
- if (!existsSync(screenshotPath)) {
976
- return createErrorResponse(`Screenshot file not found at: ${screenshotPath}`, [
977
- 'The screenshot may have failed to save',
978
- 'Check disk space and permissions',
979
- ]);
980
- }
981
- const imageBuffer = readFileSync(screenshotPath);
982
- const base64Data = imageBuffer.toString('base64');
983
- const content = [
984
- {
985
- type: 'image',
986
- data: base64Data,
987
- mimeType: 'image/png',
109
+ properties: {
110
+ projectPath: { type: 'string', description: 'Path to the Godot project directory' },
111
+ scenePath: {
112
+ type: 'string',
113
+ description: 'Path to the .tscn file relative to the project root (e.g. "scenes/main.tscn")',
114
+ },
988
115
  },
989
- {
990
- type: 'text',
991
- text: `Screenshot saved to: ${parsed.path}`,
116
+ required: ['projectPath', 'scenePath'],
117
+ },
118
+ outputSchema: {
119
+ type: 'object',
120
+ properties: {
121
+ scene: { type: 'string' },
122
+ dependencies: {
123
+ type: 'array',
124
+ items: {
125
+ type: 'object',
126
+ properties: {
127
+ path: { type: 'string' },
128
+ type: { type: 'string' },
129
+ uid: { type: 'string' },
130
+ },
131
+ },
132
+ },
992
133
  },
993
- ];
994
- if (runtimeErrors.length > 0) {
995
- content.push({
996
- type: 'text',
997
- text: JSON.stringify({
998
- warnings: runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES),
999
- }),
1000
- });
1001
- }
1002
- return { content };
1003
- }
1004
- catch (error) {
1005
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1006
- return createErrorResponse(`Failed to take screenshot: ${errorMessage}`, [
1007
- 'Ensure the project is running (use run_project first)',
1008
- 'The bridge may not be ready yet — use get_debug_output to investigate',
1009
- 'Check that UDP port 9900 is not blocked',
1010
- ]);
1011
- }
1012
- }
1013
- export async function handleSimulateInput(runner, args) {
1014
- args = normalizeParameters(args);
1015
- const sessionError = ensureRuntimeSession(runner, 'simulate input');
1016
- if (sessionError) {
1017
- return sessionError;
1018
- }
1019
- const actions = args.actions;
1020
- if (!Array.isArray(actions) || actions.length === 0) {
1021
- return createErrorResponse('actions must be a non-empty array of input actions', [
1022
- 'Provide at least one action object with a "type" field',
1023
- ]);
1024
- }
1025
- // Calculate timeout: sum of all wait durations + 10s buffer
1026
- let totalWaitMs = 0;
1027
- for (const action of actions) {
1028
- if (typeof action === 'object' &&
1029
- action !== null &&
1030
- action.type === 'wait' &&
1031
- typeof action.ms === 'number') {
1032
- totalWaitMs += action.ms;
1033
- }
1034
- }
1035
- const timeoutMs = totalWaitMs + 10000;
1036
- try {
1037
- const { response: responseStr, runtimeErrors } = await runner.sendCommandWithErrors('input', { actions }, timeoutMs);
1038
- let parsed;
1039
- try {
1040
- parsed = JSON.parse(responseStr);
1041
- }
1042
- catch {
1043
- return createErrorResponse(`Invalid response from bridge: ${responseStr}`, [
1044
- 'The game may not have fully initialized yet',
1045
- 'Try again after a few seconds',
1046
- ]);
1047
- }
1048
- if (parsed.error) {
1049
- return createErrorResponse(`Input simulation error: ${parsed.error}`, [
1050
- 'Check action types and parameters',
1051
- 'Ensure key names are valid Godot key names',
1052
- ]);
1053
- }
1054
- const payload = {
1055
- success: true,
1056
- actions_processed: parsed.actions_processed,
1057
- tip: 'Call take_screenshot to verify the input had the intended visual effect.',
1058
- };
1059
- if (runtimeErrors.length > 0) {
1060
- payload.warnings = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES);
1061
- }
1062
- return {
1063
- content: [
1064
- {
1065
- type: 'text',
1066
- text: JSON.stringify(payload),
134
+ },
135
+ },
136
+ {
137
+ name: 'get_project_settings',
138
+ description: 'Parse project.godot into structured JSON. Use to inspect configured display, input, rendering, etc. settings without launching Godot. Pass section to filter to one INI section (e.g. "display", "application"). Returns: { settings: { [section]: { [key]: value } } } or { settings: { [key]: value } } when section is given. Complex Godot types are returned as raw strings; keys outside any section appear under __global__.',
139
+ annotations: { readOnlyHint: true },
140
+ inputSchema: {
141
+ type: 'object',
142
+ properties: {
143
+ projectPath: { type: 'string', description: 'Path to the Godot project directory' },
144
+ section: {
145
+ type: 'string',
146
+ description: 'Filter to a specific INI section (e.g. "display", "application"). Omit for all sections.',
1067
147
  },
1068
- ],
1069
- };
1070
- }
1071
- catch (error) {
1072
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1073
- return createErrorResponse(`Failed to simulate input: ${errorMessage}`, [
1074
- 'Ensure the project is running (use run_project first)',
1075
- 'The bridge may not be ready yet — use get_debug_output to investigate',
1076
- 'Check that UDP port 9900 is not blocked',
1077
- ]);
1078
- }
1079
- }
1080
- export async function handleGetUiElements(runner, args) {
1081
- args = normalizeParameters(args);
1082
- const sessionError = ensureRuntimeSession(runner, 'query UI elements');
1083
- if (sessionError) {
1084
- return sessionError;
1085
- }
1086
- const visibleOnly = args.visibleOnly !== false;
148
+ },
149
+ required: ['projectPath'],
150
+ },
151
+ },
152
+ ];
153
+ // --- Helpers ---
154
+ const PROJECT_SCAN_BLACKLIST = new Set(['.git', '.godot', '.mcp', 'node_modules', '.svn', '.hg']);
155
+ function findGodotProjects(directory, recursive) {
156
+ const projects = [];
1087
157
  try {
1088
- const cmdParams = { visible_only: visibleOnly };
1089
- if (args.filter)
1090
- cmdParams.type_filter = args.filter;
1091
- const { response: responseStr, runtimeErrors } = await runner.sendCommandWithErrors('get_ui_elements', cmdParams);
1092
- let parsed;
1093
- try {
1094
- parsed = JSON.parse(responseStr);
1095
- }
1096
- catch {
1097
- return createErrorResponse(`Invalid response from bridge: ${responseStr}`, [
1098
- 'The game may not have fully initialized yet',
1099
- 'Try again after a few seconds',
1100
- ]);
1101
- }
1102
- if (parsed.error) {
1103
- return createErrorResponse(`UI element query error: ${parsed.error}`, [
1104
- 'Ensure the game has a UI with Control nodes',
1105
- ]);
158
+ const projectFile = projectGodotPath(directory);
159
+ if (existsSync(projectFile)) {
160
+ projects.push({
161
+ path: directory,
162
+ name: basename(directory),
163
+ });
1106
164
  }
1107
- const payload = {
1108
- ...parsed,
1109
- tip: "Use simulate_input with type 'click_element' and a node_path or text value from this list to interact with these elements.",
1110
- };
1111
- if (runtimeErrors.length > 0) {
1112
- payload.warnings = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES);
165
+ const entries = readdirSync(directory, { withFileTypes: true });
166
+ for (const entry of entries) {
167
+ if (!entry.isDirectory() || PROJECT_SCAN_BLACKLIST.has(entry.name))
168
+ continue;
169
+ const subdir = join(directory, entry.name);
170
+ if (existsSync(projectGodotPath(subdir))) {
171
+ projects.push({ path: subdir, name: entry.name });
172
+ }
173
+ else if (recursive) {
174
+ projects.push(...findGodotProjects(subdir, true));
175
+ }
1113
176
  }
1114
- return {
1115
- content: [
1116
- {
1117
- type: 'text',
1118
- text: JSON.stringify(payload),
1119
- },
1120
- ],
1121
- };
1122
177
  }
1123
178
  catch (error) {
1124
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1125
- return createErrorResponse(`Failed to get UI elements: ${errorMessage}`, [
1126
- 'Ensure the project is running (use run_project first)',
1127
- 'The bridge may not be ready yet — use get_debug_output to investigate',
1128
- 'Check that UDP port 9900 is not blocked',
1129
- ]);
179
+ logDebug(`Error searching directory ${directory}: ${error}`);
1130
180
  }
181
+ return projects;
1131
182
  }
1132
- export async function handleRunScript(runner, args) {
1133
- args = normalizeParameters(args);
1134
- const sessionError = ensureRuntimeSession(runner, 'execute scripts');
1135
- if (sessionError) {
1136
- return sessionError;
1137
- }
1138
- const script = args.script;
1139
- if (typeof script !== 'string' || script.trim() === '') {
1140
- return createErrorResponse('script is required and must be a non-empty string', [
1141
- 'Provide GDScript source code with extends RefCounted and func execute(scene_tree: SceneTree) -> Variant',
1142
- ]);
1143
- }
1144
- if (!script.includes('func execute')) {
1145
- return createErrorResponse('Script must define func execute(scene_tree: SceneTree) -> Variant', ['Add a func execute(scene_tree: SceneTree) -> Variant method to your script']);
1146
- }
1147
- // Write script to .mcp/scripts/ for audit trail
1148
- try {
1149
- const projectPath = runner.activeProjectPath;
1150
- if (projectPath) {
1151
- const scriptsDir = join(projectPath, '.mcp', 'scripts');
1152
- mkdirSync(scriptsDir, { recursive: true });
1153
- const timestamp = Date.now();
1154
- const scriptFile = join(scriptsDir, `${timestamp}.gd`);
1155
- writeFileSync(scriptFile, script, 'utf8');
1156
- logDebug(`Saved script to ${scriptFile}`);
1157
- }
1158
- }
1159
- catch (error) {
1160
- logDebug(`Failed to save script for audit: ${error}`);
1161
- }
1162
- const timeout = typeof args.timeout === 'number' ? args.timeout : 30000;
1163
- try {
1164
- const { response: responseStr, runtimeErrors } = await runner.sendCommandWithErrors('run_script', { source: script }, timeout);
1165
- let parsed;
183
+ function getProjectStructure(projectPath) {
184
+ const structure = {
185
+ scenes: 0,
186
+ scripts: 0,
187
+ assets: 0,
188
+ other: 0,
189
+ };
190
+ const scanDirectory = (currentPath) => {
1166
191
  try {
1167
- parsed = JSON.parse(responseStr);
1168
- }
1169
- catch {
1170
- return createErrorResponse(`Invalid response from bridge: ${responseStr}`, [
1171
- 'The script may have produced non-JSON output',
1172
- 'Check get_debug_output for print() statements',
1173
- ]);
1174
- }
1175
- if (parsed.error) {
1176
- return createErrorResponse(`Script execution error: ${parsed.error}`, [
1177
- 'Check your GDScript syntax',
1178
- 'Ensure the script extends RefCounted',
1179
- 'Check get_debug_output for details',
1180
- ]);
1181
- }
1182
- // Detect false-positive success: GDScript has no try-catch, so runtime errors
1183
- // return null and the real error only appears in stderr.
1184
- if (parsed.success && parsed.result === null && runner.activeSessionMode === 'spawned') {
1185
- if (runtimeErrors.length > 0) {
1186
- const errorContext = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES).join('\n');
1187
- return createErrorResponse(`Script runtime error detected:\n${errorContext}`, [
1188
- 'Fix the GDScript error in your script and retry',
1189
- 'Use get_debug_output for full process output',
1190
- ]);
192
+ const entries = readdirSync(currentPath, { withFileTypes: true });
193
+ for (const entry of entries) {
194
+ const entryPath = join(currentPath, entry.name);
195
+ if (entry.name.startsWith('.')) {
196
+ continue;
197
+ }
198
+ if (entry.isDirectory()) {
199
+ scanDirectory(entryPath);
200
+ }
201
+ else if (entry.isFile()) {
202
+ const ext = fileExtension(entry.name);
203
+ if (ext === 'tscn') {
204
+ structure.scenes++;
205
+ }
206
+ else if (ext === 'gd' || ext === 'gdscript' || ext === 'cs') {
207
+ structure.scripts++;
208
+ }
209
+ else if (['png', 'jpg', 'jpeg', 'webp', 'svg', 'ttf', 'wav', 'mp3', 'ogg'].includes(ext || '')) {
210
+ structure.assets++;
211
+ }
212
+ else {
213
+ structure.other++;
214
+ }
215
+ }
1191
216
  }
1192
- return {
1193
- content: [
1194
- {
1195
- type: 'text',
1196
- text: JSON.stringify({
1197
- success: true,
1198
- result: null,
1199
- warning: 'Script returned null. If unexpected, check get_debug_output for runtime errors — GDScript does not propagate exceptions.',
1200
- tip: 'Call take_screenshot to verify any visual changes, or get_debug_output to review print() output from your script.',
1201
- }),
1202
- },
1203
- ],
1204
- };
1205
217
  }
1206
- const payload = {
1207
- success: true,
1208
- result: parsed.result,
1209
- tip: 'Call take_screenshot to verify any visual changes, or get_debug_output to review print() output from your script.',
1210
- };
1211
- if (runtimeErrors.length > 0) {
1212
- payload.warnings = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES);
218
+ catch (error) {
219
+ logDebug(`Error scanning directory ${currentPath}: ${error}`);
1213
220
  }
1214
- return {
1215
- content: [
1216
- {
1217
- type: 'text',
1218
- text: JSON.stringify(payload),
1219
- },
1220
- ],
1221
- };
1222
- }
1223
- catch (error) {
1224
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1225
- return createErrorResponse(`Failed to execute script: ${errorMessage}`, [
1226
- 'Ensure the project is running (use run_project first)',
1227
- 'The bridge may not be ready yet — wait 2-3 seconds after starting, then check get_debug_output if the issue persists',
1228
- 'Check that UDP port 9900 is not blocked',
1229
- 'For long-running scripts, increase the timeout parameter',
1230
- ]);
1231
- }
221
+ };
222
+ scanDirectory(projectPath);
223
+ return structure;
1232
224
  }
1233
225
  function buildFilesystemTree(currentPath, relativePath, maxDepth, currentDepth, extensions) {
1234
226
  const name = basename(currentPath);
@@ -1248,7 +240,7 @@ function buildFilesystemTree(currentPath, relativePath, maxDepth, currentDepth,
1248
240
  children.push(buildFilesystemTree(join(currentPath, entry.name), childRelPath, maxDepth, currentDepth + 1, extensions));
1249
241
  }
1250
242
  else if (entry.isFile()) {
1251
- const ext = entry.name.includes('.') ? entry.name.split('.').pop().toLowerCase() : '';
243
+ const ext = fileExtension(entry.name);
1252
244
  if (extensions && !extensions.includes(ext))
1253
245
  continue;
1254
246
  children.push({ name: entry.name, type: 'file', path: childRelPath, extension: ext });
@@ -1286,7 +278,7 @@ function searchInFiles(rootPath, pattern, fileTypes, caseSensitive, maxResults)
1286
278
  searchDir(fullPath, childRelPath);
1287
279
  }
1288
280
  else if (entry.isFile()) {
1289
- const ext = entry.name.includes('.') ? entry.name.split('.').pop().toLowerCase() : '';
281
+ const ext = fileExtension(entry.name);
1290
282
  if (!fileTypes.includes(ext))
1291
283
  continue;
1292
284
  let content;
@@ -1353,122 +345,83 @@ function parseProjectSettings(projectFilePath) {
1353
345
  }
1354
346
  return result;
1355
347
  }
1356
- export async function handleListAutoloads(args) {
348
+ // --- Handlers ---
349
+ export async function handleListProjects(args) {
1357
350
  args = normalizeParameters(args);
1358
- const v = validateProjectArgs(args);
1359
- if ('isError' in v)
1360
- return v;
1361
- try {
1362
- const projectFile = join(v.projectPath, 'project.godot');
1363
- const autoloads = parseAutoloads(projectFile);
1364
- return { content: [{ type: 'text', text: JSON.stringify({ autoloads }) }] };
1365
- }
1366
- catch (error) {
1367
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1368
- return createErrorResponse(`Failed to list autoloads: ${errorMessage}`, [
1369
- 'Check if project.godot is accessible',
351
+ if (!args.directory) {
352
+ return createErrorResponse('Directory is required', [
353
+ 'Provide a valid directory path to search for Godot projects',
1370
354
  ]);
1371
355
  }
1372
- }
1373
- export async function handleAddAutoload(args) {
1374
- args = normalizeParameters(args);
1375
- const v = validateProjectArgs(args);
1376
- if ('isError' in v)
1377
- return v;
1378
- if (!args.autoloadName || !args.autoloadPath) {
1379
- return createErrorResponse('autoloadName and autoloadPath are required', [
1380
- 'Provide the autoload node name and script/scene path',
356
+ if (!validatePath(args.directory)) {
357
+ return createErrorResponse('Invalid directory path', [
358
+ 'Provide a valid path without ".." or other potentially unsafe characters',
1381
359
  ]);
1382
360
  }
1383
- if (!validatePath(args.autoloadPath)) {
1384
- return createErrorResponse('Invalid autoload path', ['Provide a valid path without ".."']);
1385
- }
1386
361
  try {
1387
- const projectFile = join(v.projectPath, 'project.godot');
1388
- const existing = parseAutoloads(projectFile);
1389
- if (existing.some((a) => a.name === args.autoloadName)) {
1390
- return createErrorResponse(`Autoload '${args.autoloadName}' already exists`, [
1391
- 'Use update_autoload to modify it',
1392
- 'Use list_autoloads to see current autoloads',
362
+ if (!existsSync(args.directory)) {
363
+ return createErrorResponse(`Directory does not exist: ${args.directory}`, [
364
+ 'Provide a valid directory path that exists on the system',
1393
365
  ]);
1394
366
  }
1395
- const isSingleton = args.singleton !== false;
1396
- addAutoloadEntry(projectFile, args.autoloadName, args.autoloadPath, isSingleton);
367
+ const recursive = args.recursive === true;
368
+ const projects = findGodotProjects(args.directory, recursive);
1397
369
  return {
1398
- content: [
1399
- {
1400
- type: 'text',
1401
- 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.`,
1402
- },
1403
- ],
370
+ content: [{ type: 'text', text: JSON.stringify(projects) }],
1404
371
  };
1405
372
  }
1406
373
  catch (error) {
1407
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1408
- return createErrorResponse(`Failed to add autoload: ${errorMessage}`, [
1409
- 'Check if project.godot is accessible',
374
+ return createErrorResponse(`Failed to list projects: ${getErrorMessage(error)}`, [
375
+ 'Ensure the directory exists and is accessible',
376
+ 'Check if you have permission to read the directory',
1410
377
  ]);
1411
378
  }
1412
379
  }
1413
- export async function handleRemoveAutoload(args) {
380
+ export async function handleGetProjectInfo(runner, args) {
1414
381
  args = normalizeParameters(args);
1415
- const v = validateProjectArgs(args);
1416
- if ('isError' in v)
1417
- return v;
1418
- if (!args.autoloadName) {
1419
- return createErrorResponse('autoloadName is required', [
1420
- 'Provide the name of the autoload to remove',
1421
- ]);
1422
- }
1423
382
  try {
1424
- const projectFile = join(v.projectPath, 'project.godot');
1425
- const removed = removeAutoloadEntry(projectFile, args.autoloadName);
1426
- if (!removed) {
1427
- return createErrorResponse(`Autoload '${args.autoloadName}' not found`, [
1428
- 'Use list_autoloads to see existing autoloads',
1429
- ]);
383
+ const version = await runner.getVersion();
384
+ // If no project path, return just the Godot version
385
+ if (!args.projectPath) {
386
+ return {
387
+ content: [{ type: 'text', text: JSON.stringify({ godotVersion: version }) }],
388
+ };
1430
389
  }
1431
- return {
1432
- content: [{ type: 'text', text: `Autoload '${args.autoloadName}' removed successfully.` }],
1433
- };
1434
- }
1435
- catch (error) {
1436
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1437
- return createErrorResponse(`Failed to remove autoload: ${errorMessage}`, [
1438
- 'Check if project.godot is accessible',
1439
- ]);
1440
- }
1441
- }
1442
- export async function handleUpdateAutoload(args) {
1443
- args = normalizeParameters(args);
1444
- const v = validateProjectArgs(args);
1445
- if ('isError' in v)
1446
- return v;
1447
- if (!args.autoloadName) {
1448
- return createErrorResponse('autoloadName is required', [
1449
- 'Provide the name of the autoload to update',
1450
- ]);
1451
- }
1452
- if (args.autoloadPath && !validatePath(args.autoloadPath)) {
1453
- return createErrorResponse('Invalid autoload path', ['Provide a valid path without ".."']);
1454
- }
1455
- try {
1456
- const projectFile = join(v.projectPath, 'project.godot');
1457
- const updated = updateAutoloadEntry(projectFile, args.autoloadName, args.autoloadPath, args.singleton);
1458
- if (!updated) {
1459
- return createErrorResponse(`Autoload '${args.autoloadName}' not found`, [
1460
- 'Use list_autoloads to see existing autoloads',
1461
- 'Use add_autoload to register a new one',
1462
- ]);
390
+ const v = validateProjectArgs(args);
391
+ if ('isError' in v)
392
+ return v;
393
+ const projectFile = projectGodotPath(v.projectPath);
394
+ const projectStructure = getProjectStructure(v.projectPath);
395
+ let projectName = basename(v.projectPath);
396
+ try {
397
+ const projectFileContent = readFileSync(projectFile, 'utf8');
398
+ const configNameMatch = projectFileContent.match(/config\/name="([^"]+)"/);
399
+ if (configNameMatch && configNameMatch[1]) {
400
+ projectName = configNameMatch[1];
401
+ logDebug(`Found project name in config: ${projectName}`);
402
+ }
403
+ }
404
+ catch (error) {
405
+ logDebug(`Error reading project file: ${error}`);
1463
406
  }
1464
407
  return {
1465
- content: [{ type: 'text', text: `Autoload '${args.autoloadName}' updated successfully.` }],
408
+ content: [
409
+ {
410
+ type: 'text',
411
+ text: JSON.stringify({
412
+ name: projectName,
413
+ path: v.projectPath,
414
+ godotVersion: version,
415
+ structure: projectStructure,
416
+ }),
417
+ },
418
+ ],
1466
419
  };
1467
420
  }
1468
421
  catch (error) {
1469
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1470
- return createErrorResponse(`Failed to update autoload: ${errorMessage}`, [
1471
- 'Check if project.godot is accessible',
422
+ return createErrorResponse(`Failed to get project info: ${getErrorMessage(error)}`, [
423
+ 'Ensure Godot is installed correctly',
424
+ 'Check if the GODOT_PATH environment variable is set correctly',
1472
425
  ]);
1473
426
  }
1474
427
  }
@@ -1486,8 +439,7 @@ export async function handleGetProjectFiles(args) {
1486
439
  return { content: [{ type: 'text', text: JSON.stringify(tree) }] };
1487
440
  }
1488
441
  catch (error) {
1489
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1490
- return createErrorResponse(`Failed to get project files: ${errorMessage}`, [
442
+ return createErrorResponse(`Failed to get project files: ${getErrorMessage(error)}`, [
1491
443
  'Check if the project directory is accessible',
1492
444
  ]);
1493
445
  }
@@ -1510,8 +462,7 @@ export async function handleSearchProject(args) {
1510
462
  return { content: [{ type: 'text', text: JSON.stringify(result) }] };
1511
463
  }
1512
464
  catch (error) {
1513
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1514
- return createErrorResponse(`Failed to search project: ${errorMessage}`, [
465
+ return createErrorResponse(`Failed to search project: ${getErrorMessage(error)}`, [
1515
466
  'Check if the project directory is accessible',
1516
467
  ]);
1517
468
  }
@@ -1526,8 +477,10 @@ export async function handleGetSceneDependencies(args) {
1526
477
  'Provide a path relative to the project root, e.g. "scenes/main.tscn"',
1527
478
  ]);
1528
479
  }
1529
- if (!validatePath(args.scenePath)) {
1530
- return createErrorResponse('Invalid scenePath', ['Provide a valid path without ".."']);
480
+ if (!validateSubPath(v.projectPath, args.scenePath)) {
481
+ return createErrorResponse('Invalid scenePath', [
482
+ 'Provide a valid relative path without ".." that stays inside the project directory',
483
+ ]);
1531
484
  }
1532
485
  try {
1533
486
  const sceneFullPath = join(v.projectPath, args.scenePath);
@@ -1562,8 +515,7 @@ export async function handleGetSceneDependencies(args) {
1562
515
  };
1563
516
  }
1564
517
  catch (error) {
1565
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1566
- return createErrorResponse(`Failed to get scene dependencies: ${errorMessage}`, [
518
+ return createErrorResponse(`Failed to get scene dependencies: ${getErrorMessage(error)}`, [
1567
519
  'Check if the scene file is accessible',
1568
520
  ]);
1569
521
  }
@@ -1574,7 +526,7 @@ export async function handleGetProjectSettings(args) {
1574
526
  if ('isError' in v)
1575
527
  return v;
1576
528
  try {
1577
- const projectFile = join(v.projectPath, 'project.godot');
529
+ const projectFile = projectGodotPath(v.projectPath);
1578
530
  const allSettings = parseProjectSettings(projectFile);
1579
531
  if (args.section && typeof args.section === 'string') {
1580
532
  const sectionData = allSettings[args.section] ?? {};
@@ -1583,8 +535,7 @@ export async function handleGetProjectSettings(args) {
1583
535
  return { content: [{ type: 'text', text: JSON.stringify({ settings: allSettings }) }] };
1584
536
  }
1585
537
  catch (error) {
1586
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1587
- return createErrorResponse(`Failed to get project settings: ${errorMessage}`, [
538
+ return createErrorResponse(`Failed to get project settings: ${getErrorMessage(error)}`, [
1588
539
  'Check if project.godot is accessible',
1589
540
  ]);
1590
541
  }