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,19 +1,40 @@
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
+ # Port is baked into this script at inject time by BridgeManager.inject — the
10
+ # integer literal below is rewritten in the project copy. The 9900 here is the
11
+ # source-of-truth default that ships with the script so it remains runnable
12
+ # standalone (e.g. validate, manual debugging).
13
+ const PORT := 9900 # MCP_BRIDGE_PORT_BAKED
14
+ const MAX_FRAME_BYTES := 16 * 1024 * 1024
15
+ const FRAME_HEADER_BYTES := 4
16
+
17
+ class PeerState:
18
+ extends RefCounted
19
+ var stream: StreamPeerTCP
20
+ var buffer: PackedByteArray = PackedByteArray()
21
+ var expected_len: int = -1 # -1 = waiting on header
22
+ var handling: bool = false # true while a command is awaiting a response
23
+
24
+ var tcp_server: TCPServer
6
25
  var session_token: String = ""
26
+ var _peers: Array = [] # Array[PeerState]
27
+ var _shutting_down: bool = false # One-shot: set true in shutdown(); never reset (autoload is recreated on next session)
7
28
 
8
29
  func _ready() -> void:
9
30
  process_mode = Node.PROCESS_MODE_ALWAYS
10
31
  session_token = OS.get_environment("MCP_SESSION_TOKEN")
11
- udp_server = UDPServer.new()
12
- var err = udp_server.listen(port, "127.0.0.1")
32
+ tcp_server = TCPServer.new()
33
+ var err = tcp_server.listen(PORT, "127.0.0.1")
13
34
  if err != OK:
14
- push_error("McpBridge: Failed to listen on port %d (error %d)" % [port, err])
35
+ push_error("McpBridge: Failed to listen on port %d (error %d)" % [PORT, err])
15
36
  else:
16
- print("McpBridge: Listening on UDP port %d" % port)
37
+ print("McpBridge: Listening on TCP port %d" % PORT)
17
38
 
18
39
  if OS.get_environment("MCP_BACKGROUND") == "1":
19
40
  DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_NO_FOCUS, true)
@@ -23,25 +44,87 @@ func _ready() -> void:
23
44
  print("McpBridge: Background mode active - window hidden, physical input blocked")
24
45
 
25
46
  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:
47
+ if tcp_server == null or not tcp_server.is_listening():
48
+ return
49
+
50
+ while tcp_server.is_connection_available():
51
+ var stream := tcp_server.take_connection()
52
+ if stream == null:
53
+ break
54
+ stream.set_no_delay(true)
55
+ var peer := PeerState.new()
56
+ peer.stream = stream
57
+ _peers.append(peer)
58
+
59
+ # Backwards iteration so remove_at() doesn't shift entries we haven't seen
60
+ # yet, and avoids the O(n) cost of Array.erase() per removal.
61
+ var i := _peers.size()
62
+ while i > 0:
63
+ i -= 1
64
+ var peer = _peers[i]
65
+ _poll_peer(peer)
66
+ if peer.stream == null or peer.stream.get_status() != StreamPeerTCP.STATUS_CONNECTED:
67
+ _peers.remove_at(i)
68
+
69
+ func _poll_peer(peer: PeerState) -> void:
70
+ peer.stream.poll()
71
+ var status := peer.stream.get_status()
72
+ if status != StreamPeerTCP.STATUS_CONNECTED:
73
+ return
74
+
75
+ var available := peer.stream.get_available_bytes()
76
+ if available > 0:
77
+ var chunk: Array = peer.stream.get_partial_data(available)
78
+ # get_partial_data returns [error, PackedByteArray]
79
+ if chunk[0] == OK:
80
+ peer.buffer.append_array(chunk[1])
81
+
82
+ while true:
83
+ if peer.expected_len < 0:
84
+ if peer.buffer.size() < FRAME_HEADER_BYTES:
85
+ return
86
+ # Read u32 BE header.
87
+ var header := peer.buffer.slice(0, FRAME_HEADER_BYTES)
88
+ var b0 := int(header[0])
89
+ var b1 := int(header[1])
90
+ var b2 := int(header[2])
91
+ var b3 := int(header[3])
92
+ peer.expected_len = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3
93
+ peer.buffer = peer.buffer.slice(FRAME_HEADER_BYTES)
94
+ if peer.expected_len > MAX_FRAME_BYTES:
95
+ push_error("McpBridge: Frame header exceeds limit (%d), closing peer" % peer.expected_len)
96
+ peer.stream.disconnect_from_host()
97
+ peer.stream = null
98
+ return
99
+
100
+ if peer.handling:
101
+ return
102
+ if peer.buffer.size() < peer.expected_len:
103
+ return
104
+
105
+ var frame_bytes := peer.buffer.slice(0, peer.expected_len)
106
+ peer.buffer = peer.buffer.slice(peer.expected_len)
107
+ peer.expected_len = -1
108
+
109
+ var data := frame_bytes.get_string_from_utf8().strip_edges()
110
+ peer.handling = true
111
+ _dispatch_command(peer, data)
112
+ # _dispatch_command awaits internally on async branches (input, run_script,
113
+ # screenshot, shutdown), so control returns here at the first inner await.
114
+ # `peer.handling` is the gate that blocks re-entry; it is cleared by
115
+ # `_send_response` once the handler completes.
116
+
117
+ # INVARIANT: every code path through this function and its handlers must
118
+ # eventually reach `_send_response`. `peer.handling` is set to true by the
119
+ # caller (`_poll_peer`) before dispatch and cleared inside `_send_response`.
120
+ # A handler that exits without calling `_send_response` will deadlock the
121
+ # peer — the next frame will never be polled. When adding a new branch,
122
+ # ensure the early-exit calls `_send_response` with an error payload.
123
+ func _dispatch_command(peer: PeerState, data: String) -> void:
124
+ if not data.begins_with("{"):
125
+ _send_response(peer, {"error": "Non-JSON frame (expected a JSON command object)"})
126
+ return
127
+
45
128
  var json = JSON.new()
46
129
  var err = json.parse(data)
47
130
  if err != OK:
@@ -63,16 +146,15 @@ func _handle_json_command(peer: PacketPeerUDP, data: String) -> void:
63
146
  if actions.is_empty():
64
147
  _send_response(peer, {"error": "actions array is empty"})
65
148
  return
66
- if _is_processing_input:
67
- _send_response(peer, {"error": "Input sequence already in progress"})
68
- return
69
- _handle_input(peer, actions)
149
+ await _handle_input(peer, actions)
70
150
  "get_ui_elements":
71
151
  _handle_get_ui_elements(peer, payload)
72
152
  "run_script":
73
- _handle_run_script(peer, payload)
153
+ await _handle_run_script(peer, payload)
74
154
  "screenshot":
75
- _handle_screenshot(peer)
155
+ await _handle_screenshot(peer, payload)
156
+ "shutdown":
157
+ await _handle_shutdown(peer)
76
158
  "ping":
77
159
  _send_response(peer, {"status": "pong", "session_token": session_token, "project_path": ProjectSettings.globalize_path("res://")})
78
160
  _:
@@ -80,7 +162,7 @@ func _handle_json_command(peer: PacketPeerUDP, data: String) -> void:
80
162
 
81
163
  # --- Screenshot ---
82
164
 
83
- func _handle_screenshot(peer: PacketPeerUDP) -> void:
165
+ func _handle_screenshot(peer: PeerState, payload: Dictionary = {}) -> void:
84
166
  await RenderingServer.frame_post_draw
85
167
 
86
168
  var viewport := get_viewport()
@@ -104,12 +186,40 @@ func _handle_screenshot(peer: PacketPeerUDP) -> void:
104
186
  return
105
187
 
106
188
  var safe_path := file_path.replace("\\", "/")
107
- _send_response(peer, {"path": safe_path})
189
+ var response: Dictionary = {
190
+ "path": safe_path,
191
+ "width": image.get_width(),
192
+ "height": image.get_height(),
193
+ }
194
+
195
+ var preview_max_width: int = int(payload.get("preview_max_width", 0))
196
+ var preview_max_height: int = int(payload.get("preview_max_height", 0))
197
+ if preview_max_width > 0 and preview_max_height > 0:
198
+ var scale: float = min(
199
+ 1.0,
200
+ min(
201
+ float(preview_max_width) / float(image.get_width()),
202
+ float(preview_max_height) / float(image.get_height())
203
+ )
204
+ )
205
+ var preview_width: int = max(1, int(floor(float(image.get_width()) * scale)))
206
+ var preview_height: int = max(1, int(floor(float(image.get_height()) * scale)))
207
+ # Full image already saved to disk — resize in-place to avoid a redundant copy
208
+ image.resize(preview_width, preview_height, Image.INTERPOLATE_LANCZOS)
209
+ var preview_path: String = screenshot_dir.path_join("screenshot_%s_preview.png" % timestamp)
210
+ var preview_err: Error = image.save_png(preview_path)
211
+ if preview_err != OK:
212
+ _send_response(peer, {"error": "Failed to save screenshot preview (error %d)" % preview_err})
213
+ return
214
+ response["preview_path"] = preview_path.replace("\\", "/")
215
+ response["preview_width"] = preview_width
216
+ response["preview_height"] = preview_height
217
+
218
+ _send_response(peer, response)
108
219
 
109
220
  # --- Input Simulation ---
110
221
 
111
- func _handle_input(peer: PacketPeerUDP, actions: Array) -> void:
112
- _is_processing_input = true
222
+ func _handle_input(peer: PeerState, actions: Array) -> void:
113
223
  var processed := 0
114
224
  var error_msg := ""
115
225
 
@@ -156,8 +266,6 @@ func _handle_input(peer: PacketPeerUDP, actions: Array) -> void:
156
266
 
157
267
  processed += 1
158
268
 
159
- _is_processing_input = false
160
-
161
269
  # Allow queued input events to dispatch and any signal handlers
162
270
  # (and their runtime errors) to fire before we reply, so the
163
271
  # Node-side stderr scan in sendCommandWithErrors sees them.
@@ -180,11 +288,22 @@ func _inject_key(action: Dictionary) -> String:
180
288
 
181
289
  var event = InputEventKey.new()
182
290
  event.keycode = keycode
291
+ event.physical_keycode = keycode
183
292
  event.pressed = action.get("pressed", true)
184
293
  event.echo = false
185
294
  event.shift_pressed = action.get("shift", false)
186
295
  event.ctrl_pressed = action.get("ctrl", false)
187
296
  event.alt_pressed = action.get("alt", false)
297
+ # Text-entry Controls (LineEdit, TextEdit) consume `event.unicode`, not just
298
+ # the keycode — without it, typing into a focused LineEdit produces nothing.
299
+ # Auto-derive for ASCII letters and digits; fall back to caller-supplied
300
+ # `unicode` for symbols and non-ASCII.
301
+ if action.has("unicode"):
302
+ event.unicode = int(action.unicode)
303
+ elif keycode >= KEY_A and keycode <= KEY_Z:
304
+ event.unicode = keycode if event.shift_pressed else (keycode + 32)
305
+ elif keycode >= KEY_0 and keycode <= KEY_9:
306
+ event.unicode = keycode
188
307
  Input.parse_input_event(event)
189
308
  return ""
190
309
 
@@ -296,7 +415,7 @@ func _inject_click_element(action: Dictionary) -> String:
296
415
 
297
416
  # --- UI Element Discovery ---
298
417
 
299
- func _handle_get_ui_elements(peer: PacketPeerUDP, payload: Dictionary) -> void:
418
+ func _handle_get_ui_elements(peer: PeerState, payload: Dictionary) -> void:
300
419
  var visible_only: bool = payload.get("visible_only", true)
301
420
  var type_filter: String = payload.get("type_filter", "")
302
421
  var root := get_tree().root
@@ -374,7 +493,7 @@ func _find_control_by_identifier(identifier: String) -> Control:
374
493
 
375
494
  # --- Script Execution ---
376
495
 
377
- func _handle_run_script(peer: PacketPeerUDP, payload: Dictionary) -> void:
496
+ func _handle_run_script(peer: PeerState, payload: Dictionary) -> void:
378
497
  var source: String = payload.get("source", "")
379
498
  if source.strip_edges() == "":
380
499
  _send_response(peer, {"error": "No script source provided"})
@@ -461,13 +580,56 @@ func _serialize_value(value: Variant) -> Variant:
461
580
  _:
462
581
  return str(value)
463
582
 
583
+ # --- Shutdown ---
584
+
585
+ func _handle_shutdown(peer: PeerState) -> void:
586
+ _shutting_down = true
587
+ _send_response(peer, {"status": "shutting_down"})
588
+ # Let the response flush before we tear the listener down. A new command
589
+ # arriving in this 2-frame window would dispatch against a peer that's
590
+ # about to close; the response write fails gracefully and the Node side
591
+ # sees BridgeDisconnectedError. MCP serializes calls so this is theoretical.
592
+ await get_tree().process_frame
593
+ await get_tree().process_frame
594
+ _close_all_peers()
595
+ if tcp_server != null:
596
+ tcp_server.stop()
597
+ # Detach from the tree so subsequent _process ticks don't run.
598
+ queue_free()
599
+
464
600
  # --- Utility ---
465
601
 
466
- func _send_response(peer: PacketPeerUDP, data: Dictionary) -> void:
602
+ func _send_response(peer: PeerState, data: Dictionary) -> void:
467
603
  var resp := JSON.stringify(data)
468
- peer.put_packet(resp.to_utf8_buffer())
604
+ var body := resp.to_utf8_buffer()
605
+ if body.size() > MAX_FRAME_BYTES:
606
+ push_error("McpBridge: Response exceeds %d bytes; dropping" % MAX_FRAME_BYTES)
607
+ peer.handling = false
608
+ return
609
+ if peer.stream != null and peer.stream.get_status() == StreamPeerTCP.STATUS_CONNECTED:
610
+ var header := PackedByteArray()
611
+ header.resize(FRAME_HEADER_BYTES)
612
+ var size := body.size()
613
+ header[0] = (size >> 24) & 0xFF
614
+ header[1] = (size >> 16) & 0xFF
615
+ header[2] = (size >> 8) & 0xFF
616
+ header[3] = size & 0xFF
617
+ peer.stream.put_data(header)
618
+ peer.stream.put_data(body)
619
+ peer.handling = false
620
+
621
+ func _close_all_peers() -> void:
622
+ for peer in _peers:
623
+ if peer.stream != null:
624
+ peer.stream.disconnect_from_host()
625
+ peer.stream = null
626
+ _peers.clear()
469
627
 
470
628
  func _exit_tree() -> void:
471
- if udp_server != null:
472
- udp_server.stop()
629
+ if not _shutting_down:
630
+ push_warning("McpBridge: removed from tree without shutdown — bridge connection will be lost")
631
+ _close_all_peers()
632
+ if tcp_server != null:
633
+ tcp_server.stop()
634
+ tcp_server = null
473
635
  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): {
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): {
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): {
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): {
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,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;EAcxD;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;EA+CtD;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;EA2BzD;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;EAsCzD"}
@@ -0,0 +1,191 @@
1
+ import { readFileSync } from 'fs';
2
+ import { normalizeParameters, validateSubPath, 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: [{ 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 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 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 (!validateSubPath(v.projectPath, args.autoloadPath)) {
98
+ return createErrorResponse('Invalid autoload path', [
99
+ 'Provide a valid relative path or res:// URI that stays inside the project directory',
100
+ ]);
101
+ }
102
+ try {
103
+ const projectFile = projectGodotPath(v.projectPath);
104
+ const projectFileContent = readFileSync(projectFile, 'utf8');
105
+ const existing = parseAutoloads(projectFile, projectFileContent);
106
+ if (existing.some((a) => a.name === args.autoloadName)) {
107
+ return createErrorResponse(`Autoload '${args.autoloadName}' already exists`, [
108
+ 'Use update_autoload to modify it',
109
+ 'Use list_autoloads to see current autoloads',
110
+ ]);
111
+ }
112
+ const isSingleton = args.singleton !== false;
113
+ addAutoloadEntry(projectFile, args.autoloadName, args.autoloadPath, isSingleton, projectFileContent);
114
+ return {
115
+ content: [
116
+ {
117
+ type: 'text',
118
+ 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.`,
119
+ },
120
+ ],
121
+ };
122
+ }
123
+ catch (error) {
124
+ return createErrorResponse(`Failed to add autoload: ${getErrorMessage(error)}`, [
125
+ 'Check if project.godot is accessible',
126
+ ]);
127
+ }
128
+ }
129
+ export function handleRemoveAutoload(args) {
130
+ args = normalizeParameters(args);
131
+ const v = validateProjectArgs(args);
132
+ if ('isError' in v)
133
+ return v;
134
+ if (!args.autoloadName) {
135
+ return createErrorResponse('autoloadName is required', [
136
+ 'Provide the name of the autoload to remove',
137
+ ]);
138
+ }
139
+ try {
140
+ const projectFile = projectGodotPath(v.projectPath);
141
+ const removed = removeAutoloadEntry(projectFile, args.autoloadName);
142
+ if (!removed) {
143
+ return createErrorResponse(`Autoload '${args.autoloadName}' not found`, [
144
+ 'Use list_autoloads to see existing autoloads',
145
+ ]);
146
+ }
147
+ return {
148
+ content: [{ type: 'text', text: `Autoload '${args.autoloadName}' removed successfully.` }],
149
+ };
150
+ }
151
+ catch (error) {
152
+ return createErrorResponse(`Failed to remove autoload: ${getErrorMessage(error)}`, [
153
+ 'Check if project.godot is accessible',
154
+ ]);
155
+ }
156
+ }
157
+ export function handleUpdateAutoload(args) {
158
+ args = normalizeParameters(args);
159
+ const v = validateProjectArgs(args);
160
+ if ('isError' in v)
161
+ return v;
162
+ if (!args.autoloadName) {
163
+ return createErrorResponse('autoloadName is required', [
164
+ 'Provide the name of the autoload to update',
165
+ ]);
166
+ }
167
+ if (args.autoloadPath && !validateSubPath(v.projectPath, args.autoloadPath)) {
168
+ return createErrorResponse('Invalid autoload path', [
169
+ 'Provide a valid relative path or res:// URI that stays inside the project directory',
170
+ ]);
171
+ }
172
+ try {
173
+ const projectFile = projectGodotPath(v.projectPath);
174
+ const updated = updateAutoloadEntry(projectFile, args.autoloadName, args.autoloadPath, args.singleton);
175
+ if (!updated) {
176
+ return createErrorResponse(`Autoload '${args.autoloadName}' not found`, [
177
+ 'Use list_autoloads to see existing autoloads',
178
+ 'Use add_autoload to register a new one',
179
+ ]);
180
+ }
181
+ return {
182
+ content: [{ type: 'text', text: `Autoload '${args.autoloadName}' updated successfully.` }],
183
+ };
184
+ }
185
+ catch (error) {
186
+ return createErrorResponse(`Failed to update autoload: ${getErrorMessage(error)}`, [
187
+ 'Check if project.godot is accessible',
188
+ ]);
189
+ }
190
+ }
191
+ //# 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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"}