godot-mcp-runtime 2.3.0 → 3.0.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 +30 -93
  2. package/dist/dispatch.d.ts +1 -11
  3. package/dist/dispatch.d.ts.map +1 -1
  4. package/dist/dispatch.js +7 -8
  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 +134 -283
  11. package/dist/scripts/mcp_bridge.gd +210 -43
  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 +187 -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 +105 -309
  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 +106 -1192
  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 +808 -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 +55 -209
  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 +33 -28
  34. package/dist/tools/validate-tools.js.map +1 -1
  35. package/dist/utils/autoload-ini.d.ts +32 -0
  36. package/dist/utils/autoload-ini.d.ts.map +1 -0
  37. package/dist/utils/autoload-ini.js +111 -0
  38. package/dist/utils/autoload-ini.js.map +1 -0
  39. package/dist/utils/bridge-manager.d.ts +29 -0
  40. package/dist/utils/bridge-manager.d.ts.map +1 -0
  41. package/dist/utils/bridge-manager.js +136 -0
  42. package/dist/utils/bridge-manager.js.map +1 -0
  43. package/dist/utils/bridge-protocol.d.ts +34 -0
  44. package/dist/utils/bridge-protocol.d.ts.map +1 -0
  45. package/dist/utils/bridge-protocol.js +65 -0
  46. package/dist/utils/bridge-protocol.js.map +1 -0
  47. package/dist/utils/godot-runner.d.ts +57 -15
  48. package/dist/utils/godot-runner.d.ts.map +1 -1
  49. package/dist/utils/godot-runner.js +309 -277
  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 +3 -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 +7 -4
@@ -1,19 +1,48 @@
1
1
  extends Node
2
2
 
3
- var udp_server: UDPServer
4
- var port: int = 9900
5
- var _is_processing_input: bool = false
3
+ # KEEP IN SYNC: src/utils/bridge-protocol.ts implements the same framing on the
4
+ # Node side. Any change here MUST be mirrored there (and vice versa).
5
+ #
6
+ # Wire format: 4-byte big-endian length prefix + UTF-8 JSON payload.
7
+ # Max frame size 16 MiB; oversize frames close the offending peer.
8
+
9
+ const DEFAULT_BRIDGE_PORT := 9900
10
+ const MAX_FRAME_BYTES := 16 * 1024 * 1024
11
+ const FRAME_HEADER_BYTES := 4
12
+
13
+ class PeerState:
14
+ extends RefCounted
15
+ var stream: StreamPeerTCP
16
+ var buffer: PackedByteArray = PackedByteArray()
17
+ var expected_len: int = -1 # -1 = waiting on header
18
+ var handling: bool = false # true while a command is awaiting a response
19
+
20
+ var tcp_server: TCPServer
21
+ var port: int = DEFAULT_BRIDGE_PORT
6
22
  var session_token: String = ""
23
+ var _peers: Array = [] # Array[PeerState]
24
+
25
+ func _resolve_port() -> int:
26
+ var raw := OS.get_environment("MCP_BRIDGE_PORT")
27
+ if raw == "":
28
+ return DEFAULT_BRIDGE_PORT
29
+ if not raw.is_valid_int():
30
+ return DEFAULT_BRIDGE_PORT
31
+ var parsed := int(raw)
32
+ if parsed <= 0 or parsed > 65535:
33
+ return DEFAULT_BRIDGE_PORT
34
+ return parsed
7
35
 
8
36
  func _ready() -> void:
9
37
  process_mode = Node.PROCESS_MODE_ALWAYS
10
38
  session_token = OS.get_environment("MCP_SESSION_TOKEN")
11
- udp_server = UDPServer.new()
12
- var err = udp_server.listen(port, "127.0.0.1")
39
+ port = _resolve_port()
40
+ tcp_server = TCPServer.new()
41
+ var err = tcp_server.listen(port, "127.0.0.1")
13
42
  if err != OK:
14
43
  push_error("McpBridge: Failed to listen on port %d (error %d)" % [port, err])
15
44
  else:
16
- print("McpBridge: Listening on UDP port %d" % port)
45
+ print("McpBridge: Listening on TCP port %d" % port)
17
46
 
18
47
  if OS.get_environment("MCP_BACKGROUND") == "1":
19
48
  DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_NO_FOCUS, true)
@@ -23,25 +52,87 @@ func _ready() -> void:
23
52
  print("McpBridge: Background mode active - window hidden, physical input blocked")
24
53
 
25
54
  func _process(_delta: float) -> void:
26
- udp_server.poll()
27
- if udp_server.is_connection_available():
28
- var peer: PacketPeerUDP = udp_server.take_connection()
29
- var packet := peer.get_packet()
30
- var data := packet.get_string_from_utf8().strip_edges()
31
-
32
- if data.begins_with("{"):
33
- _handle_json_command(peer, data)
34
- else:
35
- # Legacy plain-text commands
36
- match data:
37
- "screenshot":
38
- _handle_screenshot(peer)
39
- "ping":
40
- peer.put_packet("pong".to_utf8_buffer())
41
- _:
42
- _send_response(peer, {"error": "Unknown command: %s" % data})
43
-
44
- func _handle_json_command(peer: PacketPeerUDP, data: String) -> void:
55
+ if tcp_server == null or not tcp_server.is_listening():
56
+ return
57
+
58
+ while tcp_server.is_connection_available():
59
+ var stream := tcp_server.take_connection()
60
+ if stream == null:
61
+ break
62
+ stream.set_no_delay(true)
63
+ var peer := PeerState.new()
64
+ peer.stream = stream
65
+ _peers.append(peer)
66
+
67
+ var dead: Array = []
68
+ for peer in _peers:
69
+ _poll_peer(peer)
70
+ if peer.stream == null or peer.stream.get_status() != StreamPeerTCP.STATUS_CONNECTED:
71
+ dead.append(peer)
72
+
73
+ if not dead.is_empty():
74
+ for peer in dead:
75
+ _peers.erase(peer)
76
+
77
+ func _poll_peer(peer: PeerState) -> void:
78
+ peer.stream.poll()
79
+ var status := peer.stream.get_status()
80
+ if status != StreamPeerTCP.STATUS_CONNECTED:
81
+ return
82
+
83
+ var available := peer.stream.get_available_bytes()
84
+ if available > 0:
85
+ var chunk: Array = peer.stream.get_partial_data(available)
86
+ # get_partial_data returns [error, PackedByteArray]
87
+ if chunk[0] == OK:
88
+ peer.buffer.append_array(chunk[1])
89
+
90
+ while true:
91
+ if peer.expected_len < 0:
92
+ if peer.buffer.size() < FRAME_HEADER_BYTES:
93
+ return
94
+ # Read u32 BE header.
95
+ var header := peer.buffer.slice(0, FRAME_HEADER_BYTES)
96
+ var b0 := int(header[0])
97
+ var b1 := int(header[1])
98
+ var b2 := int(header[2])
99
+ var b3 := int(header[3])
100
+ peer.expected_len = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3
101
+ peer.buffer = peer.buffer.slice(FRAME_HEADER_BYTES)
102
+ if peer.expected_len > MAX_FRAME_BYTES:
103
+ push_error("McpBridge: Frame header exceeds limit (%d), closing peer" % peer.expected_len)
104
+ peer.stream.disconnect_from_host()
105
+ peer.stream = null
106
+ return
107
+
108
+ if peer.handling:
109
+ return
110
+ if peer.buffer.size() < peer.expected_len:
111
+ return
112
+
113
+ var frame_bytes := peer.buffer.slice(0, peer.expected_len)
114
+ peer.buffer = peer.buffer.slice(peer.expected_len)
115
+ peer.expected_len = -1
116
+
117
+ var data := frame_bytes.get_string_from_utf8().strip_edges()
118
+ peer.handling = true
119
+ _dispatch_command(peer, data)
120
+ # _dispatch_command awaits internally on async branches (input, run_script,
121
+ # screenshot, shutdown), so control returns here at the first inner await.
122
+ # `peer.handling` is the gate that blocks re-entry; it is cleared by
123
+ # `_send_response` once the handler completes.
124
+
125
+ # INVARIANT: every code path through this function and its handlers must
126
+ # eventually reach `_send_response`. `peer.handling` is set to true by the
127
+ # caller (`_poll_peer`) before dispatch and cleared inside `_send_response`.
128
+ # A handler that exits without calling `_send_response` will deadlock the
129
+ # peer — the next frame will never be polled. When adding a new branch,
130
+ # ensure the early-exit calls `_send_response` with an error payload.
131
+ func _dispatch_command(peer: PeerState, data: String) -> void:
132
+ if not data.begins_with("{"):
133
+ _send_response(peer, {"error": "Non-JSON frame (expected a JSON command object)"})
134
+ return
135
+
45
136
  var json = JSON.new()
46
137
  var err = json.parse(data)
47
138
  if err != OK:
@@ -63,16 +154,15 @@ func _handle_json_command(peer: PacketPeerUDP, data: String) -> void:
63
154
  if actions.is_empty():
64
155
  _send_response(peer, {"error": "actions array is empty"})
65
156
  return
66
- if _is_processing_input:
67
- _send_response(peer, {"error": "Input sequence already in progress"})
68
- return
69
- _handle_input(peer, actions)
157
+ await _handle_input(peer, actions)
70
158
  "get_ui_elements":
71
159
  _handle_get_ui_elements(peer, payload)
72
160
  "run_script":
73
- _handle_run_script(peer, payload)
161
+ await _handle_run_script(peer, payload)
74
162
  "screenshot":
75
- _handle_screenshot(peer)
163
+ await _handle_screenshot(peer, payload)
164
+ "shutdown":
165
+ await _handle_shutdown(peer)
76
166
  "ping":
77
167
  _send_response(peer, {"status": "pong", "session_token": session_token, "project_path": ProjectSettings.globalize_path("res://")})
78
168
  _:
@@ -80,7 +170,7 @@ func _handle_json_command(peer: PacketPeerUDP, data: String) -> void:
80
170
 
81
171
  # --- Screenshot ---
82
172
 
83
- func _handle_screenshot(peer: PacketPeerUDP) -> void:
173
+ func _handle_screenshot(peer: PeerState, payload: Dictionary = {}) -> void:
84
174
  await RenderingServer.frame_post_draw
85
175
 
86
176
  var viewport := get_viewport()
@@ -104,12 +194,40 @@ func _handle_screenshot(peer: PacketPeerUDP) -> void:
104
194
  return
105
195
 
106
196
  var safe_path := file_path.replace("\\", "/")
107
- _send_response(peer, {"path": safe_path})
197
+ var response: Dictionary = {
198
+ "path": safe_path,
199
+ "width": image.get_width(),
200
+ "height": image.get_height(),
201
+ }
202
+
203
+ var preview_max_width: int = int(payload.get("preview_max_width", 0))
204
+ var preview_max_height: int = int(payload.get("preview_max_height", 0))
205
+ if preview_max_width > 0 and preview_max_height > 0:
206
+ var scale: float = min(
207
+ 1.0,
208
+ min(
209
+ float(preview_max_width) / float(image.get_width()),
210
+ float(preview_max_height) / float(image.get_height())
211
+ )
212
+ )
213
+ var preview_width: int = max(1, int(floor(float(image.get_width()) * scale)))
214
+ var preview_height: int = max(1, int(floor(float(image.get_height()) * scale)))
215
+ # Full image already saved to disk — resize in-place to avoid a redundant copy
216
+ image.resize(preview_width, preview_height, Image.INTERPOLATE_LANCZOS)
217
+ var preview_path: String = screenshot_dir.path_join("screenshot_%s_preview.png" % timestamp)
218
+ var preview_err: Error = image.save_png(preview_path)
219
+ if preview_err != OK:
220
+ _send_response(peer, {"error": "Failed to save screenshot preview (error %d)" % preview_err})
221
+ return
222
+ response["preview_path"] = preview_path.replace("\\", "/")
223
+ response["preview_width"] = preview_width
224
+ response["preview_height"] = preview_height
225
+
226
+ _send_response(peer, response)
108
227
 
109
228
  # --- Input Simulation ---
110
229
 
111
- func _handle_input(peer: PacketPeerUDP, actions: Array) -> void:
112
- _is_processing_input = true
230
+ func _handle_input(peer: PeerState, actions: Array) -> void:
113
231
  var processed := 0
114
232
  var error_msg := ""
115
233
 
@@ -156,8 +274,6 @@ func _handle_input(peer: PacketPeerUDP, actions: Array) -> void:
156
274
 
157
275
  processed += 1
158
276
 
159
- _is_processing_input = false
160
-
161
277
  # Allow queued input events to dispatch and any signal handlers
162
278
  # (and their runtime errors) to fire before we reply, so the
163
279
  # Node-side stderr scan in sendCommandWithErrors sees them.
@@ -180,11 +296,22 @@ func _inject_key(action: Dictionary) -> String:
180
296
 
181
297
  var event = InputEventKey.new()
182
298
  event.keycode = keycode
299
+ event.physical_keycode = keycode
183
300
  event.pressed = action.get("pressed", true)
184
301
  event.echo = false
185
302
  event.shift_pressed = action.get("shift", false)
186
303
  event.ctrl_pressed = action.get("ctrl", false)
187
304
  event.alt_pressed = action.get("alt", false)
305
+ # Text-entry Controls (LineEdit, TextEdit) consume `event.unicode`, not just
306
+ # the keycode — without it, typing into a focused LineEdit produces nothing.
307
+ # Auto-derive for ASCII letters and digits; fall back to caller-supplied
308
+ # `unicode` for symbols and non-ASCII.
309
+ if action.has("unicode"):
310
+ event.unicode = int(action.unicode)
311
+ elif keycode >= KEY_A and keycode <= KEY_Z:
312
+ event.unicode = keycode if event.shift_pressed else (keycode + 32)
313
+ elif keycode >= KEY_0 and keycode <= KEY_9:
314
+ event.unicode = keycode
188
315
  Input.parse_input_event(event)
189
316
  return ""
190
317
 
@@ -296,7 +423,7 @@ func _inject_click_element(action: Dictionary) -> String:
296
423
 
297
424
  # --- UI Element Discovery ---
298
425
 
299
- func _handle_get_ui_elements(peer: PacketPeerUDP, payload: Dictionary) -> void:
426
+ func _handle_get_ui_elements(peer: PeerState, payload: Dictionary) -> void:
300
427
  var visible_only: bool = payload.get("visible_only", true)
301
428
  var type_filter: String = payload.get("type_filter", "")
302
429
  var root := get_tree().root
@@ -374,7 +501,7 @@ func _find_control_by_identifier(identifier: String) -> Control:
374
501
 
375
502
  # --- Script Execution ---
376
503
 
377
- func _handle_run_script(peer: PacketPeerUDP, payload: Dictionary) -> void:
504
+ func _handle_run_script(peer: PeerState, payload: Dictionary) -> void:
378
505
  var source: String = payload.get("source", "")
379
506
  if source.strip_edges() == "":
380
507
  _send_response(peer, {"error": "No script source provided"})
@@ -461,13 +588,53 @@ func _serialize_value(value: Variant) -> Variant:
461
588
  _:
462
589
  return str(value)
463
590
 
591
+ # --- Shutdown ---
592
+
593
+ func _handle_shutdown(peer: PeerState) -> void:
594
+ _send_response(peer, {"status": "shutting_down"})
595
+ # Let the response flush before we tear the listener down. A new command
596
+ # arriving in this 2-frame window would dispatch against a peer that's
597
+ # about to close; the response write fails gracefully and the Node side
598
+ # sees BridgeDisconnectedError. MCP serializes calls so this is theoretical.
599
+ await get_tree().process_frame
600
+ await get_tree().process_frame
601
+ _close_all_peers()
602
+ if tcp_server != null:
603
+ tcp_server.stop()
604
+ # Detach from the tree so subsequent _process ticks don't run.
605
+ queue_free()
606
+
464
607
  # --- Utility ---
465
608
 
466
- func _send_response(peer: PacketPeerUDP, data: Dictionary) -> void:
609
+ func _send_response(peer: PeerState, data: Dictionary) -> void:
467
610
  var resp := JSON.stringify(data)
468
- peer.put_packet(resp.to_utf8_buffer())
611
+ var body := resp.to_utf8_buffer()
612
+ if body.size() > MAX_FRAME_BYTES:
613
+ push_error("McpBridge: Response exceeds %d bytes; dropping" % MAX_FRAME_BYTES)
614
+ peer.handling = false
615
+ return
616
+ if peer.stream != null and peer.stream.get_status() == StreamPeerTCP.STATUS_CONNECTED:
617
+ var header := PackedByteArray()
618
+ header.resize(FRAME_HEADER_BYTES)
619
+ var size := body.size()
620
+ header[0] = (size >> 24) & 0xFF
621
+ header[1] = (size >> 16) & 0xFF
622
+ header[2] = (size >> 8) & 0xFF
623
+ header[3] = size & 0xFF
624
+ peer.stream.put_data(header)
625
+ peer.stream.put_data(body)
626
+ peer.handling = false
627
+
628
+ func _close_all_peers() -> void:
629
+ for peer in _peers:
630
+ if peer.stream != null:
631
+ peer.stream.disconnect_from_host()
632
+ peer.stream = null
633
+ _peers.clear()
469
634
 
470
635
  func _exit_tree() -> void:
471
- if udp_server != null:
472
- udp_server.stop()
636
+ _close_all_peers()
637
+ if tcp_server != null:
638
+ tcp_server.stop()
639
+ tcp_server = null
473
640
  print("McpBridge: Stopped")
@@ -0,0 +1,51 @@
1
+ import type { OperationParams, ToolDefinition } from '../utils/godot-runner.js';
2
+ export declare const autoloadToolDefinitions: ToolDefinition[];
3
+ export declare function handleListAutoloads(args: OperationParams): Promise<{
4
+ content: Array<{
5
+ type: "text";
6
+ text: string;
7
+ }>;
8
+ isError: boolean;
9
+ } | {
10
+ content: {
11
+ type: string;
12
+ text: string;
13
+ }[];
14
+ }>;
15
+ export declare function handleAddAutoload(args: OperationParams): Promise<{
16
+ content: Array<{
17
+ type: "text";
18
+ text: string;
19
+ }>;
20
+ isError: boolean;
21
+ } | {
22
+ content: {
23
+ type: string;
24
+ text: string;
25
+ }[];
26
+ }>;
27
+ export declare function handleRemoveAutoload(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 handleUpdateAutoload(args: OperationParams): Promise<{
40
+ content: Array<{
41
+ type: "text";
42
+ text: string;
43
+ }>;
44
+ isError: boolean;
45
+ } | {
46
+ content: {
47
+ type: string;
48
+ text: string;
49
+ }[];
50
+ }>;
51
+ //# sourceMappingURL=autoload-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autoload-tools.d.ts","sourceRoot":"","sources":["../../src/tools/autoload-tools.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAkBhF,eAAO,MAAM,uBAAuB,EAAE,cAAc,EAqEnD,CAAC;AAIF,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GAc9D;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GA6C5D;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GA2B/D;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;GAoC/D"}
@@ -0,0 +1,187 @@
1
+ import { readFileSync } from 'fs';
2
+ import { normalizeParameters, validatePath, validateProjectArgs, createErrorResponse, getErrorMessage, projectGodotPath, } from '../utils/godot-runner.js';
3
+ import { parseAutoloads, addAutoloadEntry, removeAutoloadEntry, updateAutoloadEntry, } from '../utils/autoload-ini.js';
4
+ // --- Tool definitions ---
5
+ export const autoloadToolDefinitions = [
6
+ {
7
+ name: 'list_autoloads',
8
+ 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 }] }.',
9
+ annotations: { readOnlyHint: true },
10
+ inputSchema: {
11
+ type: 'object',
12
+ properties: {
13
+ projectPath: { type: 'string', description: 'Path to the Godot project directory' },
14
+ },
15
+ required: ['projectPath'],
16
+ },
17
+ },
18
+ {
19
+ name: 'add_autoload',
20
+ 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. Returns plain-text confirmation with the registered name, path, and singleton flag. Errors if an autoload with the same name already exists; use update_autoload to modify.',
21
+ inputSchema: {
22
+ type: 'object',
23
+ properties: {
24
+ projectPath: { type: 'string', description: 'Path to the Godot project directory' },
25
+ autoloadName: {
26
+ type: 'string',
27
+ description: 'Name of the autoload node (e.g. "MyManager")',
28
+ },
29
+ autoloadPath: {
30
+ type: 'string',
31
+ description: 'Path to the script or scene (e.g. "res://autoload/my_manager.gd" or "autoload/my_manager.gd")',
32
+ },
33
+ singleton: {
34
+ type: 'boolean',
35
+ description: 'Register as a globally accessible singleton by name (default: true)',
36
+ },
37
+ },
38
+ required: ['projectPath', 'autoloadName', 'autoloadPath'],
39
+ },
40
+ },
41
+ {
42
+ name: 'remove_autoload',
43
+ 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. Returns plain-text confirmation on success. Errors if no autoload with that name exists.',
44
+ annotations: { destructiveHint: true },
45
+ inputSchema: {
46
+ type: 'object',
47
+ properties: {
48
+ projectPath: { type: 'string', description: 'Path to the Godot project directory' },
49
+ autoloadName: { type: 'string', description: 'Name of the autoload to remove' },
50
+ },
51
+ required: ['projectPath', 'autoloadName'],
52
+ },
53
+ },
54
+ {
55
+ name: 'update_autoload',
56
+ 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. Returns plain-text confirmation on success. Errors if autoloadName is not registered.",
57
+ annotations: { idempotentHint: true },
58
+ inputSchema: {
59
+ type: 'object',
60
+ properties: {
61
+ projectPath: { type: 'string', description: 'Path to the Godot project directory' },
62
+ autoloadName: { type: 'string', description: 'Name of the autoload to update' },
63
+ autoloadPath: { type: 'string', description: 'New path to the script or scene' },
64
+ singleton: { type: 'boolean', description: 'New singleton flag' },
65
+ },
66
+ required: ['projectPath', 'autoloadName'],
67
+ },
68
+ },
69
+ ];
70
+ // --- Handlers ---
71
+ export async function handleListAutoloads(args) {
72
+ args = normalizeParameters(args);
73
+ const v = validateProjectArgs(args);
74
+ if ('isError' in v)
75
+ return v;
76
+ try {
77
+ const projectFile = projectGodotPath(v.projectPath);
78
+ const autoloads = parseAutoloads(projectFile);
79
+ return { content: [{ type: 'text', text: JSON.stringify({ autoloads }) }] };
80
+ }
81
+ catch (error) {
82
+ return createErrorResponse(`Failed to list autoloads: ${getErrorMessage(error)}`, [
83
+ 'Check if project.godot is accessible',
84
+ ]);
85
+ }
86
+ }
87
+ export async function handleAddAutoload(args) {
88
+ args = normalizeParameters(args);
89
+ const v = validateProjectArgs(args);
90
+ if ('isError' in v)
91
+ return v;
92
+ if (!args.autoloadName || !args.autoloadPath) {
93
+ return createErrorResponse('autoloadName and autoloadPath are required', [
94
+ 'Provide the autoload node name and script/scene path',
95
+ ]);
96
+ }
97
+ if (!validatePath(args.autoloadPath)) {
98
+ return createErrorResponse('Invalid autoload path', ['Provide a valid path without ".."']);
99
+ }
100
+ try {
101
+ const projectFile = projectGodotPath(v.projectPath);
102
+ const projectFileContent = readFileSync(projectFile, 'utf8');
103
+ const existing = parseAutoloads(projectFile, projectFileContent);
104
+ if (existing.some((a) => a.name === args.autoloadName)) {
105
+ return createErrorResponse(`Autoload '${args.autoloadName}' already exists`, [
106
+ 'Use update_autoload to modify it',
107
+ 'Use list_autoloads to see current autoloads',
108
+ ]);
109
+ }
110
+ const isSingleton = args.singleton !== false;
111
+ addAutoloadEntry(projectFile, args.autoloadName, args.autoloadPath, isSingleton, projectFileContent);
112
+ return {
113
+ content: [
114
+ {
115
+ type: 'text',
116
+ 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.`,
117
+ },
118
+ ],
119
+ };
120
+ }
121
+ catch (error) {
122
+ return createErrorResponse(`Failed to add autoload: ${getErrorMessage(error)}`, [
123
+ 'Check if project.godot is accessible',
124
+ ]);
125
+ }
126
+ }
127
+ export async function handleRemoveAutoload(args) {
128
+ args = normalizeParameters(args);
129
+ const v = validateProjectArgs(args);
130
+ if ('isError' in v)
131
+ return v;
132
+ if (!args.autoloadName) {
133
+ return createErrorResponse('autoloadName is required', [
134
+ 'Provide the name of the autoload to remove',
135
+ ]);
136
+ }
137
+ try {
138
+ const projectFile = projectGodotPath(v.projectPath);
139
+ const removed = removeAutoloadEntry(projectFile, args.autoloadName);
140
+ if (!removed) {
141
+ return createErrorResponse(`Autoload '${args.autoloadName}' not found`, [
142
+ 'Use list_autoloads to see existing autoloads',
143
+ ]);
144
+ }
145
+ return {
146
+ content: [{ type: 'text', text: `Autoload '${args.autoloadName}' removed successfully.` }],
147
+ };
148
+ }
149
+ catch (error) {
150
+ return createErrorResponse(`Failed to remove autoload: ${getErrorMessage(error)}`, [
151
+ 'Check if project.godot is accessible',
152
+ ]);
153
+ }
154
+ }
155
+ export async function handleUpdateAutoload(args) {
156
+ args = normalizeParameters(args);
157
+ const v = validateProjectArgs(args);
158
+ if ('isError' in v)
159
+ return v;
160
+ if (!args.autoloadName) {
161
+ return createErrorResponse('autoloadName is required', [
162
+ 'Provide the name of the autoload to update',
163
+ ]);
164
+ }
165
+ if (args.autoloadPath && !validatePath(args.autoloadPath)) {
166
+ return createErrorResponse('Invalid autoload path', ['Provide a valid path without ".."']);
167
+ }
168
+ try {
169
+ const projectFile = projectGodotPath(v.projectPath);
170
+ const updated = updateAutoloadEntry(projectFile, args.autoloadName, args.autoloadPath, args.singleton);
171
+ if (!updated) {
172
+ return createErrorResponse(`Autoload '${args.autoloadName}' not found`, [
173
+ 'Use list_autoloads to see existing autoloads',
174
+ 'Use add_autoload to register a new one',
175
+ ]);
176
+ }
177
+ return {
178
+ content: [{ type: 'text', text: `Autoload '${args.autoloadName}' updated successfully.` }],
179
+ };
180
+ }
181
+ catch (error) {
182
+ return createErrorResponse(`Failed to update autoload: ${getErrorMessage(error)}`, [
183
+ 'Check if project.godot is accessible',
184
+ ]);
185
+ }
186
+ }
187
+ //# sourceMappingURL=autoload-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autoload-tools.js","sourceRoot":"","sources":["../../src/tools/autoload-tools.ts"],"names":[],"mappings":"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"}