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.
- package/README.md +30 -93
- package/dist/dispatch.d.ts +1 -11
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +7 -8
- package/dist/dispatch.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -10
- package/dist/index.js.map +1 -1
- package/dist/scripts/godot_operations.gd +134 -283
- package/dist/scripts/mcp_bridge.gd +210 -43
- package/dist/tools/autoload-tools.d.ts +51 -0
- package/dist/tools/autoload-tools.d.ts.map +1 -0
- package/dist/tools/autoload-tools.js +187 -0
- package/dist/tools/autoload-tools.js.map +1 -0
- package/dist/tools/node-tools.d.ts +9 -78
- package/dist/tools/node-tools.d.ts.map +1 -1
- package/dist/tools/node-tools.js +105 -309
- package/dist/tools/node-tools.js.map +1 -1
- package/dist/tools/project-tools.d.ts +0 -168
- package/dist/tools/project-tools.d.ts.map +1 -1
- package/dist/tools/project-tools.js +106 -1192
- package/dist/tools/project-tools.js.map +1 -1
- package/dist/tools/runtime-tools.d.ts +108 -0
- package/dist/tools/runtime-tools.d.ts.map +1 -0
- package/dist/tools/runtime-tools.js +808 -0
- package/dist/tools/runtime-tools.js.map +1 -0
- package/dist/tools/scene-tools.d.ts +6 -48
- package/dist/tools/scene-tools.d.ts.map +1 -1
- package/dist/tools/scene-tools.js +55 -209
- package/dist/tools/scene-tools.js.map +1 -1
- package/dist/tools/validate-tools.d.ts.map +1 -1
- package/dist/tools/validate-tools.js +33 -28
- package/dist/tools/validate-tools.js.map +1 -1
- package/dist/utils/autoload-ini.d.ts +32 -0
- package/dist/utils/autoload-ini.d.ts.map +1 -0
- package/dist/utils/autoload-ini.js +111 -0
- package/dist/utils/autoload-ini.js.map +1 -0
- package/dist/utils/bridge-manager.d.ts +29 -0
- package/dist/utils/bridge-manager.d.ts.map +1 -0
- package/dist/utils/bridge-manager.js +136 -0
- package/dist/utils/bridge-manager.js.map +1 -0
- package/dist/utils/bridge-protocol.d.ts +34 -0
- package/dist/utils/bridge-protocol.d.ts.map +1 -0
- package/dist/utils/bridge-protocol.js +65 -0
- package/dist/utils/bridge-protocol.js.map +1 -0
- package/dist/utils/godot-runner.d.ts +57 -15
- package/dist/utils/godot-runner.d.ts.map +1 -1
- package/dist/utils/godot-runner.js +309 -277
- package/dist/utils/godot-runner.js.map +1 -1
- package/dist/utils/handler-helpers.d.ts +34 -0
- package/dist/utils/handler-helpers.d.ts.map +1 -0
- package/dist/utils/handler-helpers.js +55 -0
- package/dist/utils/handler-helpers.js.map +1 -0
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +11 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +7 -4
|
@@ -1,19 +1,48 @@
|
|
|
1
1
|
extends Node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
var
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
609
|
+
func _send_response(peer: PeerState, data: Dictionary) -> void:
|
|
467
610
|
var resp := JSON.stringify(data)
|
|
468
|
-
|
|
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
|
-
|
|
472
|
-
|
|
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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"}
|