@satelliteoflove/godot-mcp 3.10.0 → 3.10.1

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.
@@ -3,6 +3,11 @@ extends MCPBaseCommand
3
3
  class_name MCPInputCommands
4
4
 
5
5
  const INPUT_TIMEOUT := 30.0
6
+ # How long to wait for the game bridge to report it is ready to receive input
7
+ # before giving up. The natural workflow is run -> immediately drive the game;
8
+ # the session connects before the scene loads, so this short wait absorbs that
9
+ # gap (usually under a second) instead of dispatching input into a void (#241).
10
+ const READY_TIMEOUT := 10.0
6
11
 
7
12
  var _input_map_result: Dictionary = {}
8
13
  var _input_map_pending: bool = false
@@ -15,6 +20,9 @@ var _type_text_result: Dictionary = {}
15
20
  var _type_text_pending: bool = false
16
21
 
17
22
 
23
+ const _BRIDGE_NOT_READY_MSG := "Game is running but its MCP bridge is not ready to receive input yet (no scene up, or the game just launched). This usually clears within a second of run — retry shortly."
24
+
25
+
18
26
  func get_commands() -> Dictionary:
19
27
  return {
20
28
  "get_input_map": get_input_map,
@@ -23,6 +31,23 @@ func get_commands() -> Dictionary:
23
31
  }
24
32
 
25
33
 
34
+ # Block until the running game's bridge reports it can consume input, bounded by
35
+ # READY_TIMEOUT. Returns true once ready, false if the game stops or never comes
36
+ # up in time. In the common case (game already running) this returns immediately
37
+ # without waiting a frame. Gating input on this is the fix for #241: the debug
38
+ # session connects before the main scene loads, so input dispatched on
39
+ # has_active_session() alone lands in a game with nothing to receive it.
40
+ func _await_bridge_ready(debugger_plugin) -> bool:
41
+ var start_time := Time.get_ticks_msec()
42
+ while not debugger_plugin.is_bridge_ready():
43
+ if not EditorInterface.is_playing_scene():
44
+ return false # game stopped or crashed while we waited
45
+ await Engine.get_main_loop().process_frame
46
+ if (Time.get_ticks_msec() - start_time) / 1000.0 > READY_TIMEOUT:
47
+ return false
48
+ return true
49
+
50
+
26
51
  func get_input_map(_params: Dictionary) -> Dictionary:
27
52
  if not EditorInterface.is_playing_scene():
28
53
  return _get_editor_input_map()
@@ -113,8 +138,10 @@ func execute_input_sequence(params: Dictionary) -> Dictionary:
113
138
  return _error("NOT_RUNNING", "No game is currently running")
114
139
 
115
140
  var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
116
- if debugger_plugin == null or not debugger_plugin.has_active_session():
141
+ if debugger_plugin == null:
117
142
  return _error("NO_SESSION", "No active debug session")
143
+ if not await _await_bridge_ready(debugger_plugin):
144
+ return _error("BRIDGE_NOT_READY", _BRIDGE_NOT_READY_MSG)
118
145
 
119
146
  var max_end_time: float = 0.0
120
147
  for input in inputs:
@@ -162,8 +189,10 @@ func type_text(params: Dictionary) -> Dictionary:
162
189
  return _error("NOT_RUNNING", "No game is currently running")
163
190
 
164
191
  var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
165
- if debugger_plugin == null or not debugger_plugin.has_active_session():
192
+ if debugger_plugin == null:
166
193
  return _error("NO_SESSION", "No active debug session")
194
+ if not await _await_bridge_ready(debugger_plugin):
195
+ return _error("BRIDGE_NOT_READY", _BRIDGE_NOT_READY_MSG)
167
196
 
168
197
  var timeout := max(INPUT_TIMEOUT, (text.length() * delay_ms / 1000.0) + 5.0)
169
198
 
@@ -10,8 +10,13 @@ signal input_map_received(actions: Array, error: String)
10
10
  signal input_sequence_completed(result: Dictionary)
11
11
  signal type_text_completed(result: Dictionary)
12
12
  signal game_response(message_type: String, data: Variant)
13
+ signal bridge_ready()
13
14
 
14
15
  var _active_session_id: int = -1
16
+ # True once the running game's bridge has announced it is ready to receive input
17
+ # (its main scene is up). The debug session connects before the scene loads, so
18
+ # has_active_session() alone is not enough to know input will land (#241).
19
+ var _bridge_ready: bool = false
15
20
  var _pending_screenshot: bool = false
16
21
  var _pending_debug_output: bool = false
17
22
  var _pending_performance_metrics: bool = false
@@ -53,15 +58,30 @@ func _capture(message: String, data: Array, session_id: int) -> bool:
53
58
  "godot_mcp:game_response":
54
59
  _handle_game_response(data)
55
60
  return true
61
+ "godot_mcp:bridge_ready":
62
+ _handle_bridge_ready(session_id)
63
+ return true
56
64
  return false
57
65
 
58
66
 
67
+ func _handle_bridge_ready(session_id: int) -> void:
68
+ # Only the active session's bridge counts; ignore a late message from a prior
69
+ # run (a new _setup_session has already reset the flag for the current one).
70
+ if session_id != _active_session_id:
71
+ return
72
+ _bridge_ready = true
73
+ bridge_ready.emit()
74
+
75
+
59
76
  func _setup_session(session_id: int) -> void:
60
77
  _active_session_id = session_id
78
+ # New game session: its bridge has not announced readiness yet.
79
+ _bridge_ready = false
61
80
 
62
81
 
63
82
  func _session_stopped() -> void:
64
83
  _active_session_id = -1
84
+ _bridge_ready = false
65
85
  if _pending_screenshot:
66
86
  _pending_screenshot = false
67
87
  screenshot_received.emit(false, "", 0, 0, "Game session ended")
@@ -97,6 +117,13 @@ func has_active_session() -> bool:
97
117
  return true
98
118
 
99
119
 
120
+ # True only once the running game's bridge has reported its main scene is up and
121
+ # can consume input. Input commands gate on this (not just has_active_session) so
122
+ # a sequence injected right after run is not dispatched into a half-booted game.
123
+ func is_bridge_ready() -> bool:
124
+ return _bridge_ready and has_active_session()
125
+
126
+
100
127
  func request_screenshot(max_width: int = 1024, quality: float = 0.75) -> void:
101
128
  if _active_session_id < 0:
102
129
  screenshot_received.emit(false, "", 0, 0, "No active game session")
@@ -5,10 +5,20 @@ const DEFAULT_MAX_WIDTH := 1024
5
5
  const DEFAULT_JPEG_QUALITY := 0.75
6
6
  const Onscreen := preload("onscreen.gd")
7
7
 
8
+ # Cap on frames waited for the main scene to appear before announcing ready
9
+ # anyway. The scene is normally added within a frame or two of the bridge
10
+ # autoload's _ready; the cap only matters for a scene-less run (a SceneTree-only
11
+ # tool), so it never blocks readiness forever. ~10s at 60 fps.
12
+ const READY_SCENE_WAIT_FRAMES := 600
13
+
8
14
  var _logger: _MCPGameLogger
9
15
  var _profiler: MCPFrameProfiler
10
16
  var _sampler: MCPRuntimeStateSampler
11
17
 
18
+ # Set once the bridge has told the editor the game is ready to drive. Guards the
19
+ # announcement against firing twice and lets the headless test observe it.
20
+ var _ready_announced := false
21
+
12
22
 
13
23
  func _ready() -> void:
14
24
  # The bridge must keep processing while the scene tree is paused. Input
@@ -39,6 +49,10 @@ func _ready() -> void:
39
49
  _engage_freeze()
40
50
  MCPLog.info("Game bridge: launched frozen")
41
51
 
52
+ # Tell the editor when the game is actually drivable, so input injected right
53
+ # after `run` is not silently dropped into a half-booted game (see #241).
54
+ _announce_bridge_ready_when_drivable()
55
+
42
56
 
43
57
  func _exit_tree() -> void:
44
58
  # Guaranteed cleanup: never leave an action latched when the bridge node
@@ -50,6 +64,35 @@ func _exit_tree() -> void:
50
64
  EngineDebugger.unregister_profiler("mcp_frame_profiler")
51
65
 
52
66
 
67
+ # The bridge autoload's _ready runs BEFORE the main scene is added to the tree,
68
+ # so the debug session is live (and the editor sees has_active_session) while
69
+ # current_scene is still null. Input injected in that window is dispatched into a
70
+ # game that has nothing to consume it — reported as executed, but a silent no-op
71
+ # (#241). Wait for the scene to exist plus one frame (so its own _ready/input
72
+ # wiring has run), then announce readiness; the editor gates input on this signal.
73
+ func _announce_bridge_ready_when_drivable() -> void:
74
+ var tree := get_tree()
75
+ if tree == null:
76
+ return
77
+ var frames := 0
78
+ while tree.current_scene == null and frames < READY_SCENE_WAIT_FRAMES:
79
+ await tree.process_frame
80
+ frames += 1
81
+ # One more frame so a freshly-added scene has had its first _ready/process pass.
82
+ # process_frame fires even while paused, so launch-frozen runs still report ready.
83
+ await tree.process_frame
84
+ var scene_path := tree.current_scene.scene_file_path if tree.current_scene else ""
85
+ _emit_bridge_ready(scene_path)
86
+
87
+
88
+ func _emit_bridge_ready(scene_path: String) -> void:
89
+ if _ready_announced:
90
+ return
91
+ _ready_announced = true
92
+ EngineDebugger.send_message("godot_mcp:bridge_ready", [scene_path])
93
+ MCPLog.info("Game bridge: ready to drive (%s)" % scene_path)
94
+
95
+
53
96
  func _process(delta: float) -> void:
54
97
  _game_time_process(delta)
55
98
  _sequence_process()
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.10.0"
6
+ version="3.10.1"
7
7
  script="plugin.gd"
8
8
  godot_version_min="4.5"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@satelliteoflove/godot-mcp",
3
- "version": "3.10.0",
3
+ "version": "3.10.1",
4
4
  "description": "MCP server for Godot Engine integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",