@satelliteoflove/godot-mcp 3.14.0 → 3.15.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.
@@ -23,6 +23,7 @@ func setup(plugin: EditorPlugin) -> void:
23
23
  _register_handler(MCPProfilerCommands.new(), plugin)
24
24
  _register_handler(MCPRuntimeStateCommands.new(), plugin)
25
25
  _register_handler(MCPGameTimeCommands.new(), plugin)
26
+ _register_handler(MCPExecCommands.new(), plugin)
26
27
 
27
28
 
28
29
  func _register_handler(handler: MCPBaseCommand, plugin: EditorPlugin) -> void:
@@ -0,0 +1,117 @@
1
+ @tool
2
+ extends MCPBaseCommand
3
+ class_name MCPExecCommands
4
+
5
+ # godot_exec relay (#243): run / list / remove / clear execute in the game
6
+ # bridge (mcp_game_bridge.gd); this side only forwards over the debugger
7
+ # channel and waits, exactly like game_time_commands.gd. The server derives the
8
+ # timeout cascade from the call's declared budget and pushes relay_timeout_ms
9
+ # in params; the constants are fallbacks for an older server that pushes none.
10
+ # RUN_TIMEOUT is generous because a synchronous user script cannot be aborted
11
+ # mid-flight — the relay waiting is what turns a hung script into a typed
12
+ # TIMEOUT instead of a socket kill.
13
+ const BASE_TIMEOUT := 10.0
14
+ const RUN_TIMEOUT := 28.0
15
+
16
+ var _last_error: Dictionary = {}
17
+ var _call_seq := 0
18
+
19
+
20
+ func get_commands() -> Dictionary:
21
+ return {
22
+ "exec_run": exec_run,
23
+ "exec_list": exec_list,
24
+ "exec_remove": exec_remove,
25
+ "exec_clear": exec_clear,
26
+ }
27
+
28
+
29
+ func exec_run(params: Dictionary) -> Dictionary:
30
+ return await _relay("exec_run", params, _relay_timeout(params, RUN_TIMEOUT))
31
+
32
+
33
+ func exec_list(params: Dictionary) -> Dictionary:
34
+ return await _relay("exec_list", params, BASE_TIMEOUT)
35
+
36
+
37
+ func exec_remove(params: Dictionary) -> Dictionary:
38
+ return await _relay("exec_remove", params, BASE_TIMEOUT)
39
+
40
+
41
+ func exec_clear(params: Dictionary) -> Dictionary:
42
+ return await _relay("exec_clear", params, BASE_TIMEOUT)
43
+
44
+
45
+ func _relay_timeout(params: Dictionary, fallback: float) -> float:
46
+ # Use the server-pushed relay budget when present (#276); the local constant
47
+ # is only a fallback for an older server that does not derive the cascade.
48
+ var ms: float = float(params.get("relay_timeout_ms", fallback * 1000.0))
49
+ return ms / 1000.0
50
+
51
+
52
+ func _relay(msg_type: String, params: Dictionary, timeout: float) -> Dictionary:
53
+ # Explicit request/response correlation: the debugger plugin keys responses
54
+ # by msg_type alone, so a timed-out call's LATE response (a slow script that
55
+ # finished after we gave up) could otherwise be consumed as the answer to
56
+ # the next call of the same type — wrong result, silently, and every result
57
+ # after it shifted by one. The bridge echoes call_id; the wait loop discards
58
+ # mismatches. A response with no call_id (older addon) is accepted as-is.
59
+ _call_seq += 1
60
+ var call_id := _call_seq
61
+ params = params.duplicate()
62
+ params["call_id"] = call_id
63
+ var response = await _send_and_wait(msg_type, [params], timeout, call_id)
64
+ if response == null:
65
+ return _last_error
66
+ if response is Dictionary:
67
+ response.erase("call_id") # transport detail, not part of the result
68
+ if response.has("error"):
69
+ return _error("EXEC_ERROR", str(response["error"]))
70
+ return _success(response)
71
+ return _success({"data": response})
72
+
73
+
74
+ func _send_and_wait(msg_type: String, args: Array, timeout: float, call_id: int):
75
+ if not EditorInterface.is_playing_scene():
76
+ _last_error = _error("NOT_RUNNING", "No game is currently running")
77
+ return null
78
+
79
+ var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
80
+ if debugger_plugin == null or not debugger_plugin.has_active_session():
81
+ _last_error = _error("NO_SESSION", "No active debug session")
82
+ return null
83
+
84
+ var sent: bool = debugger_plugin.send_game_message(msg_type, args)
85
+ if not sent:
86
+ _last_error = _error("SEND_FAILED", "Failed to send message to game")
87
+ return null
88
+
89
+ var start_time := Time.get_ticks_msec()
90
+ while true:
91
+ await Engine.get_main_loop().process_frame
92
+ # A hard runtime error (or failed assert) in exec'd code breaks the game
93
+ # into the editor debugger, suspending the bridge handler mid-call —
94
+ # left alone, the game sits paused and this relay times out.
95
+ # Auto-continue: the handler resumes, the error lands in its logger
96
+ # window, and the response arrives with runtime_errors as designed.
97
+ # Honest limit: the break REASON is not queryable, so this resumes ANY
98
+ # break in the window — including a developer's own breakpoint hit by
99
+ # unrelated game code while an exec call is in flight. Accepted for the
100
+ # dev-tooling threat model and surfaced in the tool description.
101
+ if debugger_plugin.is_session_breaked():
102
+ debugger_plugin.continue_session()
103
+ if debugger_plugin.has_response(msg_type):
104
+ var response = debugger_plugin.get_response(msg_type)
105
+ debugger_plugin.clear_response(msg_type)
106
+ if response is Dictionary and response.has("call_id") \
107
+ and int(response["call_id"]) != call_id:
108
+ continue # a previous call's late response — discard, keep waiting for ours
109
+ return response
110
+ if (Time.get_ticks_msec() - start_time) / 1000.0 > timeout:
111
+ debugger_plugin.clear_response(msg_type)
112
+ var hint := ""
113
+ if debugger_plugin.is_session_breaked():
114
+ hint = " (the game is paused in the editor debugger and did not resume; press Continue in the editor or run godot_editor stop)"
115
+ _last_error = _error("TIMEOUT", "Timed out waiting for %s response%s" % [msg_type, hint])
116
+ return null
117
+ return null # unreachable; satisfies the parser
@@ -0,0 +1 @@
1
+ uid://clbw4kvqpf2py
@@ -326,3 +326,27 @@ func toggle_frame_profiler(enable: bool) -> void:
326
326
  var session := get_session(_active_session_id)
327
327
  if session:
328
328
  session.toggle_profiler("mcp_frame_profiler", enable)
329
+
330
+
331
+ # True when the debugger has paused the running game (script error, failed
332
+ # assert, or breakpoint). A break suspends whatever bridge handler was running
333
+ # mid-call, so relays that would otherwise time out can detect and recover.
334
+ func is_session_breaked() -> bool:
335
+ if _active_session_id < 0:
336
+ return false
337
+ var session := get_session(_active_session_id)
338
+ return session != null and session.is_breaked()
339
+
340
+
341
+ # Resume a debugger-paused game. EditorDebuggerSession exposes no continue API,
342
+ # but the raw "continue" command is the same wire message the editor's Continue
343
+ # button sends (ScriptEditorDebugger::_put_msg), and the game's RemoteDebugger
344
+ # handles it in its break loop. Returns false when there is nothing to resume.
345
+ func continue_session() -> bool:
346
+ if _active_session_id < 0:
347
+ return false
348
+ var session := get_session(_active_session_id)
349
+ if session == null or not session.is_breaked():
350
+ return false
351
+ session.send_message("continue", [])
352
+ return true
@@ -0,0 +1,225 @@
1
+ extends RefCounted
2
+ class_name MCPExecGuard
3
+
4
+ ## Pure helpers behind godot_exec (#243): the static denylist scan and the
5
+ ## function-body wrapper that turn agent-provided GDScript into something the
6
+ ## game bridge can compile and run. No engine-state access — everything here is
7
+ ## headless-testable (test/exec_guard_headless_test.gd).
8
+ ##
9
+ ## The scan is an ACCIDENT GUARD, not a security boundary: GDScript cannot be
10
+ ## sandboxed, and a determined author can trivially evade a token scan (string-
11
+ ## built calls, indirection). Its job is to stop a well-meaning agent from
12
+ ## casually reaching outside the game process (spawning processes, writing
13
+ ## files, persisting project damage) — the threat model of local dev tooling
14
+ ## where the agent already has shell access anyway.
15
+
16
+ # Tokens that reach outside the game process or persist damage to disk.
17
+ # Granularity rationale:
18
+ # - OS.* spawn/kill/shell methods individually (the rest of OS is harmless and
19
+ # frequently legitimate: get_ticks, environment reads, ...).
20
+ # - DirAccess whole-class: every directory mutation lives there; losing
21
+ # read-only listing is acceptable collateral.
22
+ # - FileAccess at the WRITE-constant level: any direct write-mode open must
23
+ # name one of these constants, while read access (loading a save file to
24
+ # inspect it) stays available — which is why FileAccess is not class-blocked.
25
+ # - ResourceSaver whole-class: writes res:// and user:// resources.
26
+ # - EditorInterface: belt-and-suspenders; not normally reachable from a game.
27
+ const DENYLIST: Array[String] = [
28
+ "OS.execute",
29
+ "OS.execute_with_pipe",
30
+ "OS.create_process",
31
+ "OS.create_instance",
32
+ "OS.kill",
33
+ "OS.shell_open",
34
+ "OS.shell_show_in_file_manager",
35
+ "OS.move_to_trash",
36
+ "DirAccess",
37
+ "FileAccess.WRITE",
38
+ "FileAccess.READ_WRITE",
39
+ "FileAccess.WRITE_READ",
40
+ "ResourceSaver",
41
+ "ProjectSettings.save",
42
+ "ProjectSettings.save_custom",
43
+ "EditorInterface",
44
+ ]
45
+
46
+ # Wrapped user code must compile under projects that escalate warnings to
47
+ # errors, and snippets legitimately ignore parameters, shadow autoload names,
48
+ # discard return values, etc. An invalid warning name here is itself a parse
49
+ # error — pinned by the headless compile test on the Godot version in use.
50
+ const WRAPPER_WARNING_IGNORES := "@warning_ignore(\"unused_parameter\", \"shadowed_global_identifier\", \"shadowed_variable\", \"shadowed_variable_base_class\", \"standalone_expression\", \"return_value_discarded\", \"unused_variable\", \"unreachable_code\", \"integer_division\")"
51
+
52
+
53
+ ## Scan agent-provided source BEFORE compiling. Comments and string contents
54
+ ## are stripped first, so `print("OS.execute")` passes while `OS.execute(...)`
55
+ ## is caught. Returns {ok: true} or {ok: false, token, message}.
56
+ static func scan_source(source: String) -> Dictionary:
57
+ var lex := _lex(_normalize(source))
58
+ var stripped: String = lex["stripped"]
59
+ if stripped.strip_edges().is_empty():
60
+ return {
61
+ "ok": false,
62
+ "token": "",
63
+ "message": "NO_CODE: exec source contains no executable code (comments only or empty)",
64
+ }
65
+ # Normalize the SCAN copy (never the wrapper input) so the two formatter-
66
+ # plausible token splits — `OS . execute` and a line continuation after the
67
+ # dot — still match. Over-matching here only over-blocks, which is the safe
68
+ # direction for an accident guard.
69
+ stripped = stripped.replace("\\\n", "")
70
+ var dot_ws := RegEx.new()
71
+ dot_ws.compile("\\s*\\.\\s*")
72
+ stripped = dot_ws.sub(stripped, ".", true)
73
+ if _token_re("await").search(stripped) != null:
74
+ return {
75
+ "ok": false,
76
+ "token": "await",
77
+ "message": "SYNC_ONLY: exec source is synchronous-only; 'await' is not allowed. " +
78
+ "For waiting on game state, compose with godot_game_time step/step_until; " +
79
+ "for sustained behavior, attach a node under `holder`.",
80
+ }
81
+ for token in DENYLIST:
82
+ if _token_re(token).search(stripped) != null:
83
+ return {
84
+ "ok": false,
85
+ "token": token,
86
+ "message": "DENIED_TOKEN: source contains '%s' — blocked by the exec denylist " % token +
87
+ "(an accident guard against process/file-write escape, not a security boundary). " +
88
+ "Exec is for mutating the running game's state, not the system around it.",
89
+ }
90
+ return {"ok": true}
91
+
92
+
93
+ ## Wrap user source as the body of `_mcp_run(<binding_names>)` on a RefCounted,
94
+ ## so bindings are bare names (`G.wave = 5` just works) and multi-statement
95
+ ## code with control flow compiles. Lines inside multiline strings are left
96
+ ## untouched (prefixing them would corrupt string content); everything else is
97
+ ## prefixed with one indent unit matched to the user's own indent character, so
98
+ ## the wrapper can never introduce a mixed-tabs-and-spaces parse error.
99
+ static func build_wrapper(source: String, binding_names: PackedStringArray) -> String:
100
+ var normalized := _normalize(source)
101
+ var lex := _lex(normalized)
102
+ var in_string_lines: Array = lex["in_string_lines"]
103
+ var lines := normalized.split("\n")
104
+ var indent := _detect_indent_unit(lines, in_string_lines, lex["in_bracket_lines"])
105
+ var body: Array = []
106
+ for i in lines.size():
107
+ var line: String = lines[i]
108
+ var starts_in_string: bool = in_string_lines[i] if i < in_string_lines.size() else false
109
+ if starts_in_string or line.strip_edges().is_empty():
110
+ body.append(line)
111
+ else:
112
+ body.append(indent + line)
113
+ return "extends RefCounted\n\n%s\nfunc _mcp_run(%s):\n%s\n" % [
114
+ WRAPPER_WARNING_IGNORES,
115
+ ", ".join(binding_names),
116
+ "\n".join(body),
117
+ ]
118
+
119
+
120
+ static func _normalize(source: String) -> String:
121
+ return source.replace("\r\n", "\n").replace("\r", "\n")
122
+
123
+
124
+ static func _token_re(token: String) -> RegEx:
125
+ # Word-boundary match so `OS.execute` does not hit `MyOS.executed` — and,
126
+ # because `_` is a word character, `OS.execute` does NOT match
127
+ # `OS.execute_with_pipe` (which has its own entry).
128
+ var re := RegEx.new()
129
+ re.compile("\\b%s\\b" % token.replace(".", "\\."))
130
+ return re
131
+
132
+
133
+ ## One lexical pass tracking string/comment/bracket state. Returns:
134
+ ## stripped: source with comments removed and string CONTENTS removed
135
+ ## (quotes kept), line structure preserved — the scan target
136
+ ## in_string_lines: per line, whether it STARTS inside a (triple-quoted)
137
+ ## string — those lines must not be indent-prefixed
138
+ ## in_bracket_lines: per line, whether it STARTS inside an open ()/[]/{} —
139
+ ## bracket interiors have free-form indentation, so these
140
+ ## lines must not drive indent-unit detection
141
+ ## Known accepted miss: raw strings (r"...") treat backslash as an escape here
142
+ ## though GDScript does not — the failure mode is over-blocking or an honest
143
+ ## compile error, never silent corruption.
144
+ static func _lex(source: String) -> Dictionary:
145
+ var stripped := ""
146
+ var in_string_lines: Array = [false]
147
+ var in_bracket_lines: Array = [false]
148
+ var quote := "" # active quote: '"', "'", '"""', or "'''"
149
+ var depth := 0 # open-bracket depth, tracked outside strings/comments
150
+ var i := 0
151
+ var n := source.length()
152
+ while i < n:
153
+ var c := source[i]
154
+ if quote != "":
155
+ if c == "\n":
156
+ stripped += "\n"
157
+ in_string_lines.append(true)
158
+ in_bracket_lines.append(depth > 0)
159
+ i += 1
160
+ elif c == "\\":
161
+ # An escaped char never ends the string — but an escaped
162
+ # NEWLINE (a line continuation inside the string) still starts
163
+ # a new physical line. Record it, or every following line would
164
+ # be mis-indexed and build_wrapper would prefix indent into the
165
+ # string's runtime value: silent corruption.
166
+ if i + 1 < n and source[i + 1] == "\n":
167
+ stripped += "\n"
168
+ in_string_lines.append(true)
169
+ in_bracket_lines.append(depth > 0)
170
+ i += 2
171
+ elif source.substr(i, quote.length()) == quote:
172
+ stripped += quote
173
+ i += quote.length()
174
+ quote = ""
175
+ else:
176
+ i += 1
177
+ continue
178
+ if c == "#":
179
+ while i < n and source[i] != "\n":
180
+ i += 1
181
+ continue
182
+ if c == "\"" or c == "'":
183
+ quote = c.repeat(3) if source.substr(i, 3) == c.repeat(3) else c
184
+ stripped += quote
185
+ i += quote.length()
186
+ continue
187
+ if c == "(" or c == "[" or c == "{":
188
+ depth += 1
189
+ elif c == ")" or c == "]" or c == "}":
190
+ depth = maxi(0, depth - 1)
191
+ if c == "\n":
192
+ stripped += "\n"
193
+ in_string_lines.append(false)
194
+ in_bracket_lines.append(depth > 0)
195
+ i += 1
196
+ continue
197
+ stripped += c
198
+ i += 1
199
+ return {
200
+ "stripped": stripped,
201
+ "in_string_lines": in_string_lines,
202
+ "in_bracket_lines": in_bracket_lines,
203
+ }
204
+
205
+
206
+ static func _detect_indent_unit(lines: PackedStringArray, in_string_lines: Array, in_bracket_lines: Array) -> String:
207
+ # Match the user's indent character: GDScript rejects mixed tabs/spaces in
208
+ # one indentation run, so the wrapper prefix must agree with the body. Four
209
+ # spaces composes with any space width (nesting only has to increase).
210
+ # Bracket-continuation lines are skipped: their indentation is free-form
211
+ # (a space-aligned array literal in otherwise tab-indented code is legal),
212
+ # so they must not decide the prefix for real block lines.
213
+ for i in lines.size():
214
+ if i < in_string_lines.size() and in_string_lines[i]:
215
+ continue
216
+ if i < in_bracket_lines.size() and in_bracket_lines[i]:
217
+ continue
218
+ var line := lines[i]
219
+ if line.strip_edges().is_empty():
220
+ continue
221
+ if line.begins_with("\t"):
222
+ return "\t"
223
+ if line.begins_with(" "):
224
+ return " "
225
+ return "\t"
@@ -0,0 +1 @@
1
+ uid://cdscqjpsiv412
@@ -374,6 +374,18 @@ func _on_debugger_message(message: String, data: Array) -> bool:
374
374
  "game_time_status":
375
375
  _handle_game_time_status(data)
376
376
  return true
377
+ "exec_run":
378
+ _handle_exec_run(data)
379
+ return true
380
+ "exec_list":
381
+ _handle_exec_list(data)
382
+ return true
383
+ "exec_remove":
384
+ _handle_exec_remove(data)
385
+ return true
386
+ "exec_clear":
387
+ _handle_exec_clear(data)
388
+ return true
377
389
  return false
378
390
 
379
391
 
@@ -1031,7 +1043,7 @@ func _list_autoload_paths(scene_root: Node) -> Array:
1031
1043
  if tree == null or tree.root == null:
1032
1044
  return out
1033
1045
  for child in tree.root.get_children():
1034
- if child == scene_root or child == self:
1046
+ if child == scene_root or child == self or child == _exec_holder:
1035
1047
  continue
1036
1048
  out.append("/root/" + str(child.name))
1037
1049
  return out
@@ -1075,6 +1087,10 @@ func _handle_watch_stop() -> void:
1075
1087
  class _MCPGameLogger extends Logger:
1076
1088
  var _output: PackedStringArray = []
1077
1089
  var _max_lines := 1000
1090
+ # Lines trimmed off the front of the ring buffer, ever. Lets a caller hold a
1091
+ # STABLE mark (dropped + size) across trims instead of a raw index that
1092
+ # silently drifts when the buffer overflows (exec's runtime-error window).
1093
+ var _dropped := 0
1078
1094
  var _mutex := Mutex.new()
1079
1095
 
1080
1096
  func _log_message(message: String, error: bool) -> void:
@@ -1083,6 +1099,7 @@ class _MCPGameLogger extends Logger:
1083
1099
  _output.append(prefix + message)
1084
1100
  if _output.size() > _max_lines:
1085
1101
  _output.remove_at(0)
1102
+ _dropped += 1
1086
1103
  _mutex.unlock()
1087
1104
 
1088
1105
  func _log_error(function: String, file: String, line: int, code: String,
@@ -1093,13 +1110,18 @@ class _MCPGameLogger extends Logger:
1093
1110
  _output.append("[ERROR] " + msg)
1094
1111
  if _output.size() > _max_lines:
1095
1112
  _output.remove_at(0)
1113
+ _dropped += 1
1096
1114
  _mutex.unlock()
1097
1115
 
1098
1116
  func get_output() -> PackedStringArray:
1099
1117
  return _output
1100
1118
 
1119
+ func get_dropped() -> int:
1120
+ return _dropped
1121
+
1101
1122
  func clear() -> void:
1102
1123
  _mutex.lock()
1124
+ _dropped += _output.size() # cleared lines are gone the same as trimmed ones
1103
1125
  _output.clear()
1104
1126
  _mutex.unlock()
1105
1127
 
@@ -1844,3 +1866,213 @@ func _finish_step() -> void:
1844
1866
  _step_predicate_inputs = []
1845
1867
  _step_report = []
1846
1868
  _send_game_time_response(response_type, result)
1869
+
1870
+
1871
+ # ── godot_exec: run agent-provided GDScript in this game process (#243) ───────
1872
+ #
1873
+ # One-shot scripts compile as the body of `_mcp_run(<bindings>)` (see
1874
+ # MCPExecGuard.build_wrapper) and run synchronously right here. Persistent
1875
+ # behaviors are nodes the script attaches under the `holder` binding — a child
1876
+ # of /root, NOT of this bridge: bridge children inherit PROCESS_MODE_ALWAYS and
1877
+ # would keep acting under a freeze, while a root child pauses with the tree
1878
+ # (bots respect freeze/step) yet survives scene reloads. The game process dying
1879
+ # on stop is the cleanup guarantee.
1880
+
1881
+ # Cap on runtime-error lines echoed back per exec — the full text stays in the
1882
+ # game console either way.
1883
+ const EXEC_MAX_ERROR_LINES := 20
1884
+
1885
+ var _exec_holder: Node = null
1886
+
1887
+
1888
+ func _exec_params(data: Array) -> Dictionary:
1889
+ return data[0] if data.size() > 0 and data[0] is Dictionary else {}
1890
+
1891
+
1892
+ # Responses correlate by message type alone in the editor plugin, so a late
1893
+ # response from a timed-out call could be consumed as the answer to the NEXT
1894
+ # call of the same type — wrong result, silently. Echoing the relay's call_id
1895
+ # lets the relay discard mismatches. Absent when the relay pushed none (older
1896
+ # server): the relay accepts unmatched responses then, so skew is safe both ways.
1897
+ func _send_exec_response(msg_type: String, result: Dictionary, params: Dictionary) -> void:
1898
+ if params.has("call_id"):
1899
+ result["call_id"] = params["call_id"]
1900
+ EngineDebugger.send_message("godot_mcp:game_response", [msg_type, result])
1901
+
1902
+
1903
+ func _ensure_exec_holder() -> Node:
1904
+ if _exec_holder != null and is_instance_valid(_exec_holder):
1905
+ return _exec_holder
1906
+ _exec_holder = Node.new()
1907
+ _exec_holder.name = "MCPExecHolder"
1908
+ # Stamp attach time on whatever user scripts add, so exec_list can report an
1909
+ # age without trusting the script to record one.
1910
+ _exec_holder.child_entered_tree.connect(func(child: Node) -> void:
1911
+ child.set_meta("mcp_exec_attached_ms", Time.get_ticks_msec()))
1912
+ get_tree().root.add_child(_exec_holder)
1913
+ return _exec_holder
1914
+
1915
+
1916
+ # A stable position in the logger stream (survives ring-buffer trims): lines
1917
+ # appended ever = dropped + currently held.
1918
+ func _exec_logger_mark() -> int:
1919
+ return (_logger.get_dropped() + _logger.get_output().size()) if _logger else 0
1920
+
1921
+
1922
+ # The window of logger lines produced since `mark` (an _exec_logger_mark
1923
+ # value), error lines only. Process-wide, not per-script: a concurrent game
1924
+ # error inside the window rides along — acceptable for an honest "what went
1925
+ # wrong" echo. If the ring buffer trimmed past the mark (a >1000-line script),
1926
+ # the lost stretch is reported instead of silently misattributed.
1927
+ func _exec_logger_delta(mark: int) -> Array:
1928
+ var out: Array = []
1929
+ if _logger == null:
1930
+ return out
1931
+ var lines := _logger.get_output()
1932
+ var start := mark - _logger.get_dropped()
1933
+ if start < 0:
1934
+ out.append("... (log buffer overflowed; %d earlier lines lost — see the game console)" % -start)
1935
+ start = 0
1936
+ for i in range(start, lines.size()):
1937
+ if not lines[i].begins_with("[ERROR] "):
1938
+ continue
1939
+ if out.size() >= EXEC_MAX_ERROR_LINES:
1940
+ out.append("... (more errors truncated; see the game console)")
1941
+ break
1942
+ out.append(lines[i].substr(8))
1943
+ return out
1944
+
1945
+
1946
+ func _build_exec_context() -> Dictionary:
1947
+ # The step_until predicate context (autoloads by name + tree/root), plus
1948
+ # `holder`. If a project defines an autoload literally named "holder", the
1949
+ # autoload keeps the name (a duplicate parameter would be a parse error);
1950
+ # the exec holder is still reachable as root.get_node("MCPExecHolder").
1951
+ var ctx := _build_predicate_context()
1952
+ var names: PackedStringArray = ctx["names"]
1953
+ var inputs: Array = ctx["inputs"]
1954
+ if not ("holder" in names):
1955
+ names.append("holder")
1956
+ inputs.append(_ensure_exec_holder())
1957
+ return {"names": names, "inputs": inputs}
1958
+
1959
+
1960
+ func _handle_exec_run(data: Array) -> void:
1961
+ var params := _exec_params(data)
1962
+ var source: String = str(params.get("source", ""))
1963
+
1964
+ var scan := MCPExecGuard.scan_source(source)
1965
+ if not scan.get("ok", false):
1966
+ _send_exec_response("exec_run", {"error": str(scan.get("message", "exec source rejected"))}, params)
1967
+ return
1968
+
1969
+ var ctx := _build_exec_context()
1970
+ var script := GDScript.new()
1971
+ script.source_code = MCPExecGuard.build_wrapper(source, ctx["names"])
1972
+
1973
+ var mark := _exec_logger_mark()
1974
+ if script.reload() != OK or not script.can_instantiate():
1975
+ var detail := "; ".join(PackedStringArray(_exec_logger_delta(mark)))
1976
+ var msg := "exec compile error"
1977
+ if detail.is_empty():
1978
+ msg += " (parser text not captured; check the game console, e.g. minimal-godot-mcp get_console_output)"
1979
+ else:
1980
+ msg += ": " + detail
1981
+ _send_exec_response("exec_run", {"error": msg}, params)
1982
+ return
1983
+
1984
+ var inst: Object = script.new()
1985
+ mark = _exec_logger_mark()
1986
+ var t0 := Time.get_ticks_msec()
1987
+ # Synchronous and non-preemptible: an infinite loop here hangs the game (the
1988
+ # relay/server time out; godot_editor stop kills the process). That is the
1989
+ # documented contract — no wall budget is pretended.
1990
+ var result: Variant = inst.callv("_mcp_run", ctx["inputs"])
1991
+ var duration := Time.get_ticks_msec() - t0
1992
+
1993
+ # Runtime backstop for the scanner's SYNC_ONLY rule (a string-built await
1994
+ # can slip past a token scan): a suspended call returns a function state.
1995
+ if typeof(result) == TYPE_OBJECT and result != null \
1996
+ and result.get_class() == "GDScriptFunctionState":
1997
+ _send_exec_response("exec_run", {"error":
1998
+ "SCRIPT_SUSPENDED: the script hit an await and suspended (exec is synchronous-only; " +
1999
+ "side effects before the await have already run). Use godot_game_time step/step_until to wait."}, params)
2000
+ return
2001
+
2002
+ var out: Dictionary = {
2003
+ "completed": true,
2004
+ "result": _sanitize_value(result),
2005
+ "duration_ms": duration,
2006
+ "holder_children": _exec_holder.get_child_count() \
2007
+ if _exec_holder != null and is_instance_valid(_exec_holder) else 0,
2008
+ }
2009
+ var errs := _exec_logger_delta(mark)
2010
+ if not errs.is_empty():
2011
+ out["runtime_errors"] = errs
2012
+ _send_exec_response("exec_run", out, params)
2013
+
2014
+
2015
+ func _handle_exec_list(data: Array) -> void:
2016
+ var params := _exec_params(data)
2017
+ var nodes: Array = []
2018
+ if _exec_holder != null and is_instance_valid(_exec_holder):
2019
+ var now := Time.get_ticks_msec()
2020
+ for child in _exec_holder.get_children():
2021
+ var script_chars := 0
2022
+ var s: Variant = child.get_script()
2023
+ if s is GDScript:
2024
+ script_chars = (s as GDScript).source_code.length()
2025
+ nodes.append({
2026
+ "name": str(child.name),
2027
+ "class": child.get_class(),
2028
+ "script_chars": script_chars,
2029
+ "age_ms": now - int(child.get_meta("mcp_exec_attached_ms", now)),
2030
+ # Internal processing too: Timers and tweened nodes drive
2031
+ # themselves internally and would otherwise read as idle.
2032
+ "processing": child.is_processing() or child.is_physics_processing() \
2033
+ or child.is_processing_internal() or child.is_physics_processing_internal(),
2034
+ })
2035
+ _send_exec_response("exec_list", {"nodes": nodes, "count": nodes.size()}, params)
2036
+
2037
+
2038
+ func _handle_exec_remove(data: Array) -> void:
2039
+ var params := _exec_params(data)
2040
+ var node_name := str(params.get("name", ""))
2041
+ var child: Node = null
2042
+ if _exec_holder != null and is_instance_valid(_exec_holder) and not node_name.is_empty():
2043
+ # Name EQUALITY against direct children — never a NodePath lookup: a
2044
+ # path-like name would traverse out of the holder (".." resolves to
2045
+ # /root, "a/b" to a grandchild) and queue_free whatever it lands on.
2046
+ for c in _exec_holder.get_children():
2047
+ if str(c.name) == node_name:
2048
+ child = c
2049
+ break
2050
+ if child == null:
2051
+ var have: Array = []
2052
+ if _exec_holder != null and is_instance_valid(_exec_holder):
2053
+ for c in _exec_holder.get_children():
2054
+ have.append(str(c.name))
2055
+ _send_exec_response("exec_remove", {"error":
2056
+ "NOT_FOUND: no exec node named '%s' (have: %s)" % [
2057
+ node_name, ", ".join(PackedStringArray(have)) if not have.is_empty() else "none"]}, params)
2058
+ return
2059
+ # Detach immediately so a list right after this call already shows it gone;
2060
+ # queue_free still frees the detached node at the end of the frame.
2061
+ _exec_holder.remove_child(child)
2062
+ child.queue_free()
2063
+ _send_exec_response("exec_remove", {
2064
+ "removed": true,
2065
+ "name": node_name,
2066
+ "remaining": _exec_holder.get_child_count(),
2067
+ }, params)
2068
+
2069
+
2070
+ func _handle_exec_clear(data: Array) -> void:
2071
+ var params := _exec_params(data)
2072
+ var removed := 0
2073
+ if _exec_holder != null and is_instance_valid(_exec_holder):
2074
+ for child in _exec_holder.get_children():
2075
+ _exec_holder.remove_child(child)
2076
+ child.queue_free()
2077
+ removed += 1
2078
+ _send_exec_response("exec_clear", {"removed_count": removed}, params)
package/addon/plugin.cfg CHANGED
@@ -3,6 +3,6 @@
3
3
  name="Godot MCP"
4
4
  description="Model Context Protocol server for AI assistant integration"
5
5
  author="godot-mcp"
6
- version="3.14.0"
6
+ version="3.15.0"
7
7
  script="plugin.gd"
8
8
  godot_version_min="4.5"
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { deriveTimeouts, maxInGameBudgetMs, QUICK_TIMEOUT_MS, ABSOLUTE_CEILING_MS, READY_WAIT_MS, BRIDGE_WALL_SLOP_MS, STEP_BUDGET_CAP_MS, INPUT_BUDGET_CAP_MS, } from '../../connection/timeouts.js';
2
+ import { deriveTimeouts, maxInGameBudgetMs, QUICK_TIMEOUT_MS, ABSOLUTE_CEILING_MS, READY_WAIT_MS, BRIDGE_WALL_SLOP_MS, STEP_BUDGET_CAP_MS, INPUT_BUDGET_CAP_MS, EXEC_BUDGET_CAP_MS, } from '../../connection/timeouts.js';
3
3
  describe('timeout cascade (#276)', () => {
4
4
  describe('deriveTimeouts stagger invariant', () => {
5
5
  // The whole point: bridge answers before relay answers before the server
@@ -50,6 +50,9 @@ describe('timeout cascade (#276)', () => {
50
50
  it('input cap <= max in-game budget (with ready-wait)', () => {
51
51
  expect(INPUT_BUDGET_CAP_MS).toBeLessThanOrEqual(maxInGameBudgetMs({ readyWait: true }));
52
52
  });
53
+ it('exec cap <= max in-game budget (no ready-wait)', () => {
54
+ expect(EXEC_BUDGET_CAP_MS).toBeLessThanOrEqual(maxInGameBudgetMs({ readyWait: false }));
55
+ });
53
56
  });
54
57
  it('keeps the quick default unchanged at 30s', () => {
55
58
  expect(QUICK_TIMEOUT_MS).toBe(30_000);
@@ -1 +1 @@
1
- {"version":3,"file":"timeouts.test.js","sourceRoot":"","sources":["../../../src/__tests__/connection/timeouts.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"timeouts.test.js","sourceRoot":"","sources":["../../../src/__tests__/connection/timeouts.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=exec.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exec.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/tools/exec.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,124 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { createMockGodot, createToolContext, structuredOf } from '../helpers/mock-godot.js';
3
+ import { exec } from '../../tools/exec.js';
4
+ import { deriveTimeouts, EXEC_BUDGET_CAP_MS } from '../../connection/timeouts.js';
5
+ describe('godot_exec tool', () => {
6
+ let mock;
7
+ beforeEach(() => {
8
+ mock = createMockGodot();
9
+ });
10
+ describe('schema validation', () => {
11
+ it('run requires a non-empty source within the size cap', () => {
12
+ expect(exec.schema.safeParse({ action: 'run' }).success).toBe(false);
13
+ expect(exec.schema.safeParse({ action: 'run', source: '' }).success).toBe(false);
14
+ expect(exec.schema.safeParse({ action: 'run', source: 'return 1' }).success).toBe(true);
15
+ expect(exec.schema.safeParse({ action: 'run', source: 'x'.repeat(16_384) }).success).toBe(true);
16
+ expect(exec.schema.safeParse({ action: 'run', source: 'x'.repeat(16_385) }).success).toBe(false);
17
+ });
18
+ it('run enforces the published budget cap', () => {
19
+ expect(exec.schema.safeParse({ action: 'run', source: 'pass', budget_ms: EXEC_BUDGET_CAP_MS }).success).toBe(true);
20
+ expect(exec.schema.safeParse({ action: 'run', source: 'pass', budget_ms: EXEC_BUDGET_CAP_MS + 1 }).success).toBe(false);
21
+ expect(exec.schema.safeParse({ action: 'run', source: 'pass', budget_ms: 0 }).success).toBe(false);
22
+ });
23
+ it('remove requires a name; list and clear take no extra arguments', () => {
24
+ expect(exec.schema.safeParse({ action: 'remove' }).success).toBe(false);
25
+ expect(exec.schema.safeParse({ action: 'remove', name: '' }).success).toBe(false);
26
+ expect(exec.schema.safeParse({ action: 'remove', name: 'GodGuard' }).success).toBe(true);
27
+ expect(exec.schema.safeParse({ action: 'list' }).success).toBe(true);
28
+ expect(exec.schema.safeParse({ action: 'clear' }).success).toBe(true);
29
+ });
30
+ });
31
+ describe('run', () => {
32
+ it('forwards the source and returns the structured result', async () => {
33
+ mock.mockResponse({ completed: true, result: 5, duration_ms: 2, holder_children: 0 });
34
+ const ctx = createToolContext(mock);
35
+ const result = await exec.execute({ action: 'run', source: 'G.wave = 5\nreturn G.wave' }, ctx);
36
+ expect(mock.calls[0].command).toBe('exec_run');
37
+ expect(mock.calls[0].params.source).toBe('G.wave = 5\nreturn G.wave');
38
+ const data = structuredOf(result);
39
+ expect(data.completed).toBe(true);
40
+ expect(data.result).toBe(5);
41
+ });
42
+ it('passes runtime_errors through (the call still completes)', async () => {
43
+ mock.mockResponse({
44
+ completed: true,
45
+ result: null,
46
+ duration_ms: 1,
47
+ holder_children: 0,
48
+ runtime_errors: ["Invalid access to property or key 'foo' on a base object of type 'Nil'."],
49
+ });
50
+ const ctx = createToolContext(mock);
51
+ const result = await exec.execute({ action: 'run', source: 'var x = null\nreturn x.foo' }, ctx);
52
+ const data = structuredOf(result);
53
+ expect(data.completed).toBe(true);
54
+ expect(data.runtime_errors).toHaveLength(1);
55
+ });
56
+ it('derives the timeout cascade from budget_ms and pushes the relay budget', async () => {
57
+ mock.mockResponse({ completed: true, result: null, duration_ms: 0, holder_children: 0 });
58
+ const ctx = createToolContext(mock);
59
+ await exec.execute({ action: 'run', source: 'pass', budget_ms: 5000 }, ctx);
60
+ const call = mock.calls[0];
61
+ const t = deriveTimeouts(5000); // no ready-wait, same class as game_time
62
+ expect(call.params.relay_timeout_ms).toBe(t.relayMs);
63
+ expect(call.opts?.timeoutMs).toBe(t.serverMs);
64
+ // No wall budget: a synchronous script cannot be aborted, so none is pretended.
65
+ expect(call.params.wall_budget_ms).toBeUndefined();
66
+ });
67
+ it('defaults the budget to 10s when omitted', async () => {
68
+ mock.mockResponse({ completed: true, result: null, duration_ms: 0, holder_children: 0 });
69
+ const ctx = createToolContext(mock);
70
+ await exec.execute({ action: 'run', source: 'pass' }, ctx);
71
+ expect(mock.calls[0].opts?.timeoutMs).toBe(deriveTimeouts(10_000).serverMs);
72
+ });
73
+ it('propagates a bridge-side rejection (denylist, compile error)', async () => {
74
+ mock.mockError(new Error("DENIED_TOKEN: source contains 'OS.execute'"));
75
+ const ctx = createToolContext(mock);
76
+ await expect(exec.execute({ action: 'run', source: 'OS.execute("cmd", [])' }, ctx)).rejects.toThrow('DENIED_TOKEN');
77
+ });
78
+ });
79
+ describe('lifecycle', () => {
80
+ it('list returns the structured holder inventory', async () => {
81
+ mock.mockResponse({
82
+ nodes: [{ name: 'GodGuard', class: 'Timer', script_chars: 0, age_ms: 4200, processing: true }],
83
+ count: 1,
84
+ });
85
+ const ctx = createToolContext(mock);
86
+ const result = await exec.execute({ action: 'list' }, ctx);
87
+ expect(mock.calls[0].command).toBe('exec_list');
88
+ const data = structuredOf(result);
89
+ expect(data.count).toBe(1);
90
+ expect(data.nodes[0].name).toBe('GodGuard');
91
+ });
92
+ it('list keeps the structured shape when the holder is empty', async () => {
93
+ mock.mockResponse({ nodes: [], count: 0 });
94
+ const ctx = createToolContext(mock);
95
+ const result = await exec.execute({ action: 'list' }, ctx);
96
+ const data = structuredOf(result);
97
+ expect(data.count).toBe(0);
98
+ expect(data.nodes).toEqual([]);
99
+ });
100
+ it('remove forwards the name and returns the structured result', async () => {
101
+ mock.mockResponse({ removed: true, name: 'GodGuard', remaining: 0 });
102
+ const ctx = createToolContext(mock);
103
+ const result = await exec.execute({ action: 'remove', name: 'GodGuard' }, ctx);
104
+ expect(mock.calls[0].command).toBe('exec_remove');
105
+ expect(mock.calls[0].params.name).toBe('GodGuard');
106
+ const data = structuredOf(result);
107
+ expect(data.removed).toBe(true);
108
+ expect(data.remaining).toBe(0);
109
+ });
110
+ it('remove propagates NOT_FOUND from the bridge', async () => {
111
+ mock.mockError(new Error("NOT_FOUND: no exec node named 'Bogus' (have: GodGuard)"));
112
+ const ctx = createToolContext(mock);
113
+ await expect(exec.execute({ action: 'remove', name: 'Bogus' }, ctx)).rejects.toThrow('NOT_FOUND');
114
+ });
115
+ it('clear reports the removed count', async () => {
116
+ mock.mockResponse({ removed_count: 2 });
117
+ const ctx = createToolContext(mock);
118
+ const result = await exec.execute({ action: 'clear' }, ctx);
119
+ expect(mock.calls[0].command).toBe('exec_clear');
120
+ expect(result).toContain('Removed 2');
121
+ });
122
+ });
123
+ });
124
+ //# sourceMappingURL=exec.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exec.test.js","sourceRoot":"","sources":["../../../src/__tests__/tools/exec.test.ts"],"names":[],"mappings":"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"}
@@ -44,4 +44,5 @@ export declare function maxInGameBudgetMs(opts?: DeriveOptions): number;
44
44
  export declare function deriveTimeouts(inGameBudgetMs: number, opts?: DeriveOptions): DerivedTimeouts;
45
45
  export declare const STEP_BUDGET_CAP_MS = 50000;
46
46
  export declare const INPUT_BUDGET_CAP_MS = 40000;
47
+ export declare const EXEC_BUDGET_CAP_MS = 30000;
47
48
  //# sourceMappingURL=timeouts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"timeouts.d.ts","sourceRoot":"","sources":["../../src/connection/timeouts.ts"],"names":[],"mappings":"AAuBA,kGAAkG;AAClG,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAEvC;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAE1C,6GAA6G;AAC7G,eAAO,MAAM,aAAa,QAAS,CAAC;AAEpC,sGAAsG;AACtG,eAAO,MAAM,mBAAmB,OAAQ,CAAC;AAEzC,6EAA6E;AAC7E,eAAO,MAAM,eAAe,OAAQ,CAAC;AAErC,sEAAsE;AACtE,eAAO,MAAM,gBAAgB,OAAQ,CAAC;AAItC,MAAM,WAAW,aAAa;IAC5B,0FAA0F;IAC1F,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,oFAAoF;IACpF,YAAY,EAAE,MAAM,CAAC;IACrB,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAC;IAChB,iGAAiG;IACjG,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,GAAE,aAAkB,GAAG,MAAM,CAGlE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,eAAe,CAQhG;AAOD,eAAO,MAAM,kBAAkB,QAAS,CAAC;AACzC,eAAO,MAAM,mBAAmB,QAAS,CAAC"}
1
+ {"version":3,"file":"timeouts.d.ts","sourceRoot":"","sources":["../../src/connection/timeouts.ts"],"names":[],"mappings":"AAuBA,kGAAkG;AAClG,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAEvC;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAE1C,6GAA6G;AAC7G,eAAO,MAAM,aAAa,QAAS,CAAC;AAEpC,sGAAsG;AACtG,eAAO,MAAM,mBAAmB,OAAQ,CAAC;AAEzC,6EAA6E;AAC7E,eAAO,MAAM,eAAe,OAAQ,CAAC;AAErC,sEAAsE;AACtE,eAAO,MAAM,gBAAgB,OAAQ,CAAC;AAItC,MAAM,WAAW,aAAa;IAC5B,0FAA0F;IAC1F,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,oFAAoF;IACpF,YAAY,EAAE,MAAM,CAAC;IACrB,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAC;IAChB,iGAAiG;IACjG,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,GAAE,aAAkB,GAAG,MAAM,CAGlE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,eAAe,CAQhG;AAOD,eAAO,MAAM,kBAAkB,QAAS,CAAC;AACzC,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAC1C,eAAO,MAAM,kBAAkB,QAAS,CAAC"}
@@ -69,4 +69,5 @@ export function deriveTimeouts(inGameBudgetMs, opts = {}) {
69
69
  // the published cap can never time out server-side.
70
70
  export const STEP_BUDGET_CAP_MS = 50_000; // game_time step / step_until (no ready-wait)
71
71
  export const INPUT_BUDGET_CAP_MS = 40_000; // godot_input sequence span / capture offsets / type_text duration (ready-wait)
72
+ export const EXEC_BUDGET_CAP_MS = 30_000; // godot_exec run (no ready-wait; scripts should be fast, the cap is patience not enforcement)
72
73
  //# sourceMappingURL=timeouts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"timeouts.js","sourceRoot":"","sources":["../../src/connection/timeouts.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,EAAE;AACF,0EAA0E;AAC1E,qEAAqE;AACrE,6EAA6E;AAC7E,2EAA2E;AAC3E,2EAA2E;AAC3E,4EAA4E;AAC5E,uEAAuE;AACvE,EAAE;AACF,+EAA+E;AAC/E,+EAA+E;AAC/E,sDAAsD;AACtD,EAAE;AACF,oFAAoF;AACpF,wFAAwF;AACxF,yFAAyF;AACzF,wFAAwF;AACxF,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,4CAA4C;AAE5C,kGAAkG;AAClG,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEvC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAE1C,6GAA6G;AAC7G,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC;AAEpC,sGAAsG;AACtG,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,CAAC;AAEzC,6EAA6E;AAC7E,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,CAAC;AAErC,sEAAsE;AACtE,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,CAAC;AAEtC,MAAM,iBAAiB,GAAG,mBAAmB,GAAG,eAAe,GAAG,gBAAgB,CAAC;AAkBnF;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAsB,EAAE;IACxD,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,OAAO,mBAAmB,GAAG,iBAAiB,GAAG,KAAK,CAAC;AACzD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,cAAsB,EAAE,OAAsB,EAAE;IAC7E,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/E,MAAM,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/E,MAAM,YAAY,GAAG,eAAe,GAAG,mBAAmB,CAAC;IAC3D,MAAM,OAAO,GAAG,KAAK,GAAG,YAAY,GAAG,eAAe,CAAC;IACvD,MAAM,QAAQ,GAAG,OAAO,GAAG,gBAAgB,CAAC;IAC5C,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAC;AAC9D,CAAC;AAED,8EAA8E;AAC9E,gFAAgF;AAChF,8EAA8E;AAC9E,gFAAgF;AAChF,oDAAoD;AACpD,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC,CAAI,8CAA8C;AAC3F,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,CAAG,gFAAgF"}
1
+ {"version":3,"file":"timeouts.js","sourceRoot":"","sources":["../../src/connection/timeouts.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,EAAE;AACF,0EAA0E;AAC1E,qEAAqE;AACrE,6EAA6E;AAC7E,2EAA2E;AAC3E,2EAA2E;AAC3E,4EAA4E;AAC5E,uEAAuE;AACvE,EAAE;AACF,+EAA+E;AAC/E,+EAA+E;AAC/E,sDAAsD;AACtD,EAAE;AACF,oFAAoF;AACpF,wFAAwF;AACxF,yFAAyF;AACzF,wFAAwF;AACxF,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,4CAA4C;AAE5C,kGAAkG;AAClG,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEvC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAE1C,6GAA6G;AAC7G,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC;AAEpC,sGAAsG;AACtG,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,CAAC;AAEzC,6EAA6E;AAC7E,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,CAAC;AAErC,sEAAsE;AACtE,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,CAAC;AAEtC,MAAM,iBAAiB,GAAG,mBAAmB,GAAG,eAAe,GAAG,gBAAgB,CAAC;AAkBnF;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAsB,EAAE;IACxD,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,OAAO,mBAAmB,GAAG,iBAAiB,GAAG,KAAK,CAAC;AACzD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,cAAsB,EAAE,OAAsB,EAAE;IAC7E,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/E,MAAM,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/E,MAAM,YAAY,GAAG,eAAe,GAAG,mBAAmB,CAAC;IAC3D,MAAM,OAAO,GAAG,KAAK,GAAG,YAAY,GAAG,eAAe,CAAC;IACvD,MAAM,QAAQ,GAAG,OAAO,GAAG,gBAAgB,CAAC;IAC5C,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAC;AAC9D,CAAC;AAED,8EAA8E;AAC9E,gFAAgF;AAChF,8EAA8E;AAC9E,gFAAgF;AAChF,oDAAoD;AACpD,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC,CAAI,8CAA8C;AAC3F,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,CAAG,gFAAgF;AAC7H,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC,CAAI,8FAA8F"}
@@ -0,0 +1,16 @@
1
+ import { z } from 'zod';
2
+ import type { AnyToolDefinition } from '../core/types.js';
3
+ export declare const exec: import("../core/types.js").ToolDefinition<z.ZodDiscriminatedUnion<[z.ZodObject<{
4
+ action: z.ZodLiteral<"run">;
5
+ source: z.ZodString;
6
+ budget_ms: z.ZodOptional<z.ZodNumber>;
7
+ }, z.core.$strip>, z.ZodObject<{
8
+ action: z.ZodLiteral<"list">;
9
+ }, z.core.$strip>, z.ZodObject<{
10
+ action: z.ZodLiteral<"remove">;
11
+ name: z.ZodString;
12
+ }, z.core.$strip>, z.ZodObject<{
13
+ action: z.ZodLiteral<"clear">;
14
+ }, z.core.$strip>], "action">>;
15
+ export declare const execTools: AnyToolDefinition[];
16
+ //# sourceMappingURL=exec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../src/tools/exec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AA6E1D,eAAO,MAAM,IAAI;;;;;;;;;;;8BAuDf,CAAC;AAEH,eAAO,MAAM,SAAS,EAAa,iBAAiB,EAAE,CAAC"}
@@ -0,0 +1,102 @@
1
+ import { z } from 'zod';
2
+ import { defineTool } from '../core/define-tool.js';
3
+ import { structured } from '../core/structured.js';
4
+ import { deriveTimeouts, EXEC_BUDGET_CAP_MS } from '../connection/timeouts.js';
5
+ // Generous for a script, tiny for the transport: the editor websocket moves
6
+ // multi-megabyte screenshots, so this cap is about keeping exec calls
7
+ // reviewable, not about what the wire can carry.
8
+ const EXEC_SOURCE_MAX_CHARS = 16_384;
9
+ const EXEC_DEFAULT_BUDGET_MS = 10_000;
10
+ const ExecSchema = z.discriminatedUnion('action', [
11
+ z.object({
12
+ action: z
13
+ .literal('run')
14
+ .describe('Run one-shot GDScript inside the running game and return its value'),
15
+ source: z
16
+ .string()
17
+ .min(1)
18
+ .max(EXEC_SOURCE_MAX_CHARS)
19
+ .describe('GDScript statements, compiled as a function body. In scope: every autoload by its own ' +
20
+ 'name (e.g. `G.wave = 5`), `tree` (SceneTree), `root` (root Window) — the same context as ' +
21
+ 'step_until predicates — plus `holder`, a Node that survives scene reloads: attach nodes ' +
22
+ 'under it for behavior that persists between tool calls (a Timer-driven guard, an autofire ' +
23
+ 'bot), then manage them with list/remove/clear. Holder children pause with the tree, so a ' +
24
+ 'bot armed under a freeze acts only after thaw/step. Use an explicit `return` to get a ' +
25
+ 'value back (there is no implicit return): primitives (bool/int/float/String) come back ' +
26
+ 'intact; any other type (Array/Dictionary/Object/Vector2) comes back as a str() preview ' +
27
+ 'TRUNCATED to 200 chars — return JSON.stringify(...) yourself when you need structure. ' +
28
+ 'print() output is not returned — use return values, or minimal-godot-mcp ' +
29
+ 'get_console_output. Function bodies cannot declare top-level func/class — use lambdas ' +
30
+ 'for callbacks, or build a sub-script with GDScript.new() and set_script() it onto a ' +
31
+ 'holder child for _process-driven behavior. No `await` (synchronous-only; compose with ' +
32
+ 'godot_game_time to wait). A runtime error or failed assert() breaks the game into the ' +
33
+ 'editor debugger mid-call; the relay auto-resumes it and the error comes back in ' +
34
+ 'runtime_errors (any debugger break in the call window is resumed, including a breakpoint ' +
35
+ 'hit by unrelated game code).'),
36
+ budget_ms: z
37
+ .number()
38
+ .int()
39
+ .min(1)
40
+ .max(EXEC_BUDGET_CAP_MS)
41
+ .optional()
42
+ .describe(`Wall-clock patience for the call, used to size the timeout cascade (default ${EXEC_DEFAULT_BUDGET_MS}, ` +
43
+ `max ${EXEC_BUDGET_CAP_MS}). NOT enforcement: a synchronous script cannot be preempted, so an ` +
44
+ 'infinite loop hangs the game past any budget — recover with godot_editor stop.'),
45
+ }),
46
+ z.object({
47
+ action: z
48
+ .literal('list')
49
+ .describe('List the nodes currently attached under the exec holder (name, class, age, processing state)'),
50
+ }),
51
+ z.object({
52
+ action: z.literal('remove').describe('Remove one exec holder child by name (queue_free)'),
53
+ name: z.string().min(1).describe('Node name as reported by list'),
54
+ }),
55
+ z.object({
56
+ action: z.literal('clear').describe('Remove every exec holder child (queue_free all)'),
57
+ }),
58
+ ]);
59
+ export const exec = defineTool({
60
+ name: 'godot_exec',
61
+ annotations: { title: 'Game Script Execution', readOnlyHint: false, destructiveHint: true, openWorldHint: false },
62
+ description: 'Execute GDScript inside the RUNNING game process — the scenario-setup primitive: grant ' +
63
+ 'weapons, skip waves, spawn entities, arm persistent test bots, without baking debug hooks ' +
64
+ 'into game code. Errors when no game is running. For launch-time setup, compose: godot_editor ' +
65
+ 'run frozen=true -> godot_exec run (mutate state, attach bots under `holder`) -> ' +
66
+ 'godot_game_time thaw. A static denylist rejects accidental process/file-write escape ' +
67
+ '(OS.execute, DirAccess, write-mode FileAccess, ResourceSaver, ProjectSettings.save, ...) and ' +
68
+ 'names the offending token — an accident guard, NOT a security boundary. Compile errors ' +
69
+ 'reject the call with the parser message; runtime errors come back in runtime_errors with the ' +
70
+ 'call still completing.',
71
+ schema: ExecSchema,
72
+ async execute(args, { godot }) {
73
+ switch (args.action) {
74
+ case 'run': {
75
+ // Same cascade class as game_time (no bridge-ready wait), but no
76
+ // wall_budget_ms is pushed: the bridge cannot abort a synchronous
77
+ // script mid-flight, and pretending it enforces a wall would be
78
+ // dishonest. The stagger still holds — the relay and this socket
79
+ // simply wait budget-long before declaring the script hung.
80
+ const t = deriveTimeouts(args.budget_ms ?? EXEC_DEFAULT_BUDGET_MS);
81
+ const result = await godot.sendCommand('exec_run', { source: args.source, relay_timeout_ms: t.relayMs }, { timeoutMs: t.serverMs });
82
+ return structured(result);
83
+ }
84
+ case 'list': {
85
+ const result = await godot.sendCommand('exec_list');
86
+ // Structured in both branches: a result whose SHAPE depends on runtime
87
+ // data would make the empty case unparseable for structured readers.
88
+ return structured(result);
89
+ }
90
+ case 'remove': {
91
+ const result = await godot.sendCommand('exec_remove', { name: args.name });
92
+ return structured(result);
93
+ }
94
+ case 'clear': {
95
+ const result = await godot.sendCommand('exec_clear');
96
+ return `Removed ${result.removed_count} exec node(s).`;
97
+ }
98
+ }
99
+ },
100
+ });
101
+ export const execTools = [exec];
102
+ //# sourceMappingURL=exec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/tools/exec.ts"],"names":[],"mappings":"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"}
@@ -12,4 +12,5 @@ export { inputTools } from './input.js';
12
12
  export { profilerTools } from './profiler.js';
13
13
  export { runtimeStateTools } from './runtime-state.js';
14
14
  export { gameTimeTools } from './game-time.js';
15
+ export { execTools } from './exec.js';
15
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAeA,wBAAgB,gBAAgB,IAAI,IAAI,CAcvC;AAED,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAgBA,wBAAgB,gBAAgB,IAAI,IAAI,CAevC;AAED,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC"}
@@ -12,6 +12,7 @@ import { inputTools } from './input.js';
12
12
  import { profilerTools } from './profiler.js';
13
13
  import { runtimeStateTools } from './runtime-state.js';
14
14
  import { gameTimeTools } from './game-time.js';
15
+ import { execTools } from './exec.js';
15
16
  export function registerAllTools() {
16
17
  registry.registerTools(sceneTools);
17
18
  registry.registerTools(nodeTools);
@@ -26,6 +27,7 @@ export function registerAllTools() {
26
27
  registry.registerTools(profilerTools);
27
28
  registry.registerTools(runtimeStateTools);
28
29
  registry.registerTools(gameTimeTools);
30
+ registry.registerTools(execTools);
29
31
  }
30
32
  export { sceneTools } from './scene.js';
31
33
  export { nodeTools } from './node.js';
@@ -40,4 +42,5 @@ export { inputTools } from './input.js';
40
42
  export { profilerTools } from './profiler.js';
41
43
  export { runtimeStateTools } from './runtime-state.js';
42
44
  export { gameTimeTools } from './game-time.js';
45
+ export { execTools } from './exec.js';
43
46
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE/C,MAAM,UAAU,gBAAgB;IAC9B,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;IACnC,QAAQ,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;IAClC,QAAQ,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;IACpC,QAAQ,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC;IACrC,QAAQ,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC;IACvC,QAAQ,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC;IACrC,QAAQ,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;IACtC,QAAQ,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC;IACrC,QAAQ,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;IAClC,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;IACnC,QAAQ,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;IACtC,QAAQ,CAAC,aAAa,CAAC,iBAAiB,CAAC,CAAC;IAC1C,QAAQ,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;AACxC,CAAC;AAED,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,UAAU,gBAAgB;IAC9B,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;IACnC,QAAQ,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;IAClC,QAAQ,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;IACpC,QAAQ,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC;IACrC,QAAQ,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC;IACvC,QAAQ,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC;IACrC,QAAQ,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;IACtC,QAAQ,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC;IACrC,QAAQ,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;IAClC,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;IACnC,QAAQ,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;IACtC,QAAQ,CAAC,aAAa,CAAC,iBAAiB,CAAC,CAAC;IAC1C,QAAQ,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;IACtC,QAAQ,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;AACpC,CAAC;AAED,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@satelliteoflove/godot-mcp",
3
- "version": "3.14.0",
3
+ "version": "3.15.0",
4
4
  "description": "MCP server for Godot Engine integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",