@satelliteoflove/godot-mcp 2.11.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +75 -43
  2. package/addon/core/mcp_constants.gd +5 -0
  3. package/addon/core/mcp_enums.gd +10 -0
  4. package/addon/plugin.cfg +1 -1
  5. package/addon/plugin.gd +163 -5
  6. package/addon/ui/status_panel.gd +171 -9
  7. package/addon/ui/status_panel.tscn +80 -4
  8. package/addon/websocket_server.gd +22 -5
  9. package/dist/__tests__/utils/connection-strategy.test.d.ts +2 -0
  10. package/dist/__tests__/utils/connection-strategy.test.d.ts.map +1 -0
  11. package/dist/__tests__/utils/connection-strategy.test.js +106 -0
  12. package/dist/__tests__/utils/connection-strategy.test.js.map +1 -0
  13. package/dist/__tests__/utils/host-ip-resolver.test.d.ts +2 -0
  14. package/dist/__tests__/utils/host-ip-resolver.test.d.ts.map +1 -0
  15. package/dist/__tests__/utils/host-ip-resolver.test.js +116 -0
  16. package/dist/__tests__/utils/host-ip-resolver.test.js.map +1 -0
  17. package/dist/__tests__/utils/wsl-detection.test.d.ts +2 -0
  18. package/dist/__tests__/utils/wsl-detection.test.d.ts.map +1 -0
  19. package/dist/__tests__/utils/wsl-detection.test.js +116 -0
  20. package/dist/__tests__/utils/wsl-detection.test.js.map +1 -0
  21. package/dist/connection/websocket.d.ts +3 -1
  22. package/dist/connection/websocket.d.ts.map +1 -1
  23. package/dist/connection/websocket.js +21 -11
  24. package/dist/connection/websocket.js.map +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +2 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/resources/script.d.ts.map +1 -1
  29. package/dist/resources/script.js +4 -5
  30. package/dist/resources/script.js.map +1 -1
  31. package/dist/tools/editor.js +2 -2
  32. package/dist/tools/editor.js.map +1 -1
  33. package/dist/utils/connection-strategy.d.ts +23 -0
  34. package/dist/utils/connection-strategy.d.ts.map +1 -0
  35. package/dist/utils/connection-strategy.js +44 -0
  36. package/dist/utils/connection-strategy.js.map +1 -0
  37. package/dist/utils/gateway-resolver.d.ts +18 -0
  38. package/dist/utils/gateway-resolver.d.ts.map +1 -0
  39. package/dist/utils/gateway-resolver.js +173 -0
  40. package/dist/utils/gateway-resolver.js.map +1 -0
  41. package/dist/utils/host-ip-resolver.d.ts +19 -0
  42. package/dist/utils/host-ip-resolver.d.ts.map +1 -0
  43. package/dist/utils/host-ip-resolver.js +60 -0
  44. package/dist/utils/host-ip-resolver.js.map +1 -0
  45. package/dist/utils/wsl-detection.d.ts +11 -0
  46. package/dist/utils/wsl-detection.d.ts.map +1 -0
  47. package/dist/utils/wsl-detection.js +43 -0
  48. package/dist/utils/wsl-detection.js.map +1 -0
  49. package/package.json +5 -2
  50. package/addon/command_router.gd.uid +0 -1
  51. package/addon/commands/animation_commands.gd.uid +0 -1
  52. package/addon/commands/debug_commands.gd.uid +0 -1
  53. package/addon/commands/node_commands.gd.uid +0 -1
  54. package/addon/commands/project_commands.gd.uid +0 -1
  55. package/addon/commands/resource_commands.gd.uid +0 -1
  56. package/addon/commands/scene3d_commands.gd.uid +0 -1
  57. package/addon/commands/scene_commands.gd.uid +0 -1
  58. package/addon/commands/screenshot_commands.gd.uid +0 -1
  59. package/addon/commands/script_commands.gd.uid +0 -1
  60. package/addon/commands/selection_commands.gd.uid +0 -1
  61. package/addon/commands/tilemap_commands.gd.uid +0 -1
  62. package/addon/core/base_command.gd.uid +0 -1
  63. package/addon/core/mcp_debugger_plugin.gd.uid +0 -1
  64. package/addon/core/mcp_logger.gd.uid +0 -1
  65. package/addon/core/mcp_utils.gd.uid +0 -1
  66. package/addon/game_bridge/mcp_game_bridge.gd.uid +0 -1
  67. package/addon/plugin.gd.uid +0 -1
  68. package/addon/ui/status_panel.gd.uid +0 -1
  69. package/addon/websocket_server.gd.uid +0 -1
package/README.md CHANGED
@@ -1,33 +1,18 @@
1
1
  # godot-mcp
2
2
 
3
- An MCP server that gives Claude direct visibility into your Godot editor and running game. Instead of copy-pasting debug output or describing what you're seeing, Claude can observe it directly.
3
+ MCP server that connects Claude to your Godot editor. Less copy-paste, more creating.
4
4
 
5
- ## Core Capabilities
5
+ ## Why This Exists
6
6
 
7
- **Observe** - Claude sees what you see
8
- - Live editor state, selection, and open scenes
9
- - Screenshots from editor viewports or running game
10
- - Debug output and performance metrics from runtime
11
- - Camera position and viewport in 2D and 3D
7
+ Using AI assistants for game dev means a lot of back-and-forth: copying error messages, describing what's on screen, pasting debug output, manually applying suggested changes. It works, but it's tedious.
12
8
 
13
- **Inspect** - Deep access to your project
14
- - Scene tree traversal with node properties
15
- - 3D spatial data: transforms, bounding boxes, visibility
16
- - Resource introspection: SpriteFrames, TileSets, Materials
17
- - Project settings and input mappings
18
-
19
- **Modify** - Direct manipulation when needed
20
- - Create, update, delete, and reparent nodes
21
- - Attach and detach scripts
22
- - Edit TileMapLayers and GridMaps cell-by-cell
23
- - Control animation playback and edit tracks/keyframes
24
- - Run and stop the game from the editor
9
+ This MCP gives Claude direct access to your Godot editor. It can see your scene tree, capture screenshots, read errors, and make changes directly. You stay focused on the creative work while the mechanical relay disappears. Faster iterations, less busywork, more time building the game you actually want to make.
25
10
 
26
11
  ## Quick Start
27
12
 
28
- ### 1. Configure Your AI Assistant
13
+ ### 1. Configure your AI assistant
29
14
 
30
- **Claude Desktop** - Add to config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
15
+ **Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
31
16
 
32
17
  ```json
33
18
  {
@@ -40,7 +25,7 @@ An MCP server that gives Claude direct visibility into your Godot editor and run
40
25
  }
41
26
  ```
42
27
 
43
- **Claude Code** - Add to `.mcp.json`:
28
+ **Claude Code** (`.mcp.json` in your project):
44
29
 
45
30
  ```json
46
31
  {
@@ -53,44 +38,91 @@ An MCP server that gives Claude direct visibility into your Godot editor and run
53
38
  }
54
39
  ```
55
40
 
56
- ### 2. Install the Godot Addon
41
+ ### 2. Install the Godot addon
57
42
 
58
43
  ```bash
59
44
  npx @satelliteoflove/godot-mcp --install-addon /path/to/your/godot/project
60
45
  ```
61
46
 
62
- Then enable the addon in Godot: Project Settings > Plugins > Godot MCP.
47
+ Enable in Godot: **Project Settings > Plugins > Godot MCP**
63
48
 
64
- ### 3. Start Using
49
+ ### 3. Go
65
50
 
66
- Open your Godot project (with addon enabled), restart your AI assistant, and start building.
51
+ Open your Godot project, restart your AI assistant, and start building.
67
52
 
68
- **Version Sync:** The MCP server auto-updates via npx. Version mismatches are detected automatically on connection. If prompted, re-run the install command to update the addon.
53
+ ## What Claude Can Do
69
54
 
70
- ## Tools
55
+ - **See** your editor, scenes, running game, errors, and performance
56
+ - **Inspect** nodes, resources, animations, tilemaps, 3D spatial data
57
+ - **Modify** scenes, nodes, scripts, animations, tilemaps directly
58
+ - **Test** by running the game and injecting input
59
+ - **Learn** by fetching Godot docs on demand
71
60
 
72
- | Tool | Description |
73
- |------|-------------|
74
- | `scene` | Manage scenes: open, save, or create scenes |
75
- | `node` | Manage scene nodes: get properties, find, create, update, delete, reparent, attach/detach scripts, connect signals |
76
- | `editor` | Control the Godot editor: get state (includes viewport/camera info), manage selection, run/stop project, get debug output, get_log_messages (errors/warnings with filter/limit), get_stack_trace (backtrace from last error), get performance metrics, capture screenshots, set 2D viewport position/zoom |
77
- | `project` | Get project information and settings |
78
- | `animation` | Query, control, and edit animations |
79
- | `tilemap` | Query and edit TileMapLayer data: list layers, get info, get/set cells, convert coordinates |
80
- | `gridmap` | Query and edit GridMap data: list gridmaps, get info, get/set cells |
81
- | `resource` | Manage Godot resources: inspect Resource files by path |
82
- | `scene3d` | Get spatial information for 3D nodes: global transforms, bounding boxes, visibility |
83
- | `godot_docs` | Fetch Godot Engine documentation |
84
- | `input` | Inject input into a running Godot game for testing |
61
+ ## Documentation
85
62
 
86
- See [docs/](docs/) for detailed API reference, including the [Claude Code Setup Guide](docs/claude-code-setup.md).
63
+ - [Claude Code Setup Guide](../docs/claude-code-setup.md) - CLAUDE.md templates and workflows
64
+ - [Tools Reference](../docs/tools/README.md) - All 11 tools with full API docs
65
+ - [Resources Reference](../docs/resources.md) - MCP resources for reading project data
66
+ - [Contributing](../CONTRIBUTING.md) - Dev setup, adding tools, release process
67
+ - [Changelog](CHANGELOG.md) - Release history
87
68
 
88
69
  ## Architecture
89
70
 
90
71
  ```
91
- [AI Assistant] <--stdio--> [MCP Server] <--WebSocket--> [Godot Addon]
72
+ [Claude/AI Assistant/MCP Client] <--stdio--> [MCP Server] <--WebSocket:6550--> [Godot MCP Bridge Addon]
73
+ ```
74
+
75
+ ### WSL Support
76
+
77
+ The MCP server has built-in support for Windows Subsystem for Linux (WSL2):
78
+
79
+ - **Auto-detection**: MCP server automatically detects WSL environment via environment variables and `/proc/version`
80
+ - **Host IP discovery**: Auto-discovers Windows host IP from WSL to connect to Godot running on Windows
81
+ - **Configuration**:
82
+ - `GODOT_HOST` overrides the Godot addon host (auto-detected in WSL)
83
+ - `GODOT_PORT` overrides the Godot addon port (default `6550`)
84
+
85
+ **Security note:** the Godot addon binds to `127.0.0.1` by default.
86
+
87
+ In the Godot Editor bottom panel (**MCP**), you can configure what the addon listens on:
88
+ - **Bind mode: Localhost** (default) → `127.0.0.1`
89
+ - **Bind mode: WSL** → Windows `vEthernet (WSL)` IPv4 (required for Windows Godot + WSL2 server)
90
+ - **Bind mode: Custom** → bind to a specific IP
91
+ - **Port override** → change the listen port from `6550`
92
+
93
+ If you enable **Port override**, set `GODOT_PORT` on the server to match.
94
+
95
+ ## CLI smoke test (paste-ready JSON-RPC)
96
+
97
+ If you run the server manually via CLI (for example: `npx -y @satelliteoflove/godot-mcp`), you can paste these **stdio JSON-RPC frames** to verify it responds and can reach Godot:
98
+
99
+ 1) Write in CLI
100
+
101
+ ```json
102
+ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"cli-test","version":"0"}}}
103
+ ```
104
+
105
+ 2) Response
106
+
107
+ ```json
108
+ {"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{},"resources":{},"logging":{}},"serverInfo":{"name":"godot-mcp","version":"2.11.0"}},"jsonrpc":"2.0","id":1}
92
109
  ```
93
110
 
111
+ 3) Call a tool
112
+
113
+ ```json
114
+ {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"editor","arguments":{"action":"get_state"}}}
115
+ ```
116
+
117
+ 4) Response
118
+
119
+ ```json
120
+ {"result":{"content":[{"type":"text","text":"{\n \"current_scene\": null,\n \"godot_version\": \"4.5.1-stable (official)\",\n \"is_playing\": false,\n \"main_screen\": \"unknown\",\n \"open_scenes\": []\n}"}]},"jsonrpc":"2.0","id":2}
121
+ ```
122
+
123
+ Tip: If you enabled **Port override** in the Godot MCP panel, start the server with matching env vars (or export as environment variable):
124
+ `GODOT_HOST=... GODOT_PORT=... npm run start` or `GODOT_HOST=... GODOT_PORT=... npx -y @satelliteoflove/godot-mcp`
125
+
94
126
  ## Development
95
127
 
96
128
  ```bash
@@ -0,0 +1,5 @@
1
+ class_name MCPConstants
2
+
3
+ const LOCALHOST_BIND_ADDRESS = "127.0.0.1"
4
+ const PORT_MIN = 1
5
+ const PORT_MAX = 65535
@@ -0,0 +1,10 @@
1
+ class_name MCPEnums
2
+
3
+ enum BindMode { LOCALHOST, WSL, CUSTOM }
4
+
5
+ static func get_mode_name(mode: BindMode) -> String:
6
+ match mode:
7
+ BindMode.LOCALHOST: return "Localhost"
8
+ BindMode.WSL: return "WSL"
9
+ BindMode.CUSTOM: return "Custom"
10
+ _: return "Unknown"
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="2.11.0"
6
+ version="2.12.0"
7
7
  script="plugin.gd"
8
8
  godot_version_min="4.5"
package/addon/plugin.gd CHANGED
@@ -9,11 +9,19 @@ const MCPDebuggerPlugin := preload("res://addons/godot_mcp/core/mcp_debugger_plu
9
9
  const GAME_BRIDGE_AUTOLOAD := "MCPGameBridge"
10
10
  const GAME_BRIDGE_PATH := "res://addons/godot_mcp/game_bridge/mcp_game_bridge.gd"
11
11
 
12
+ const SETTING_BIND_MODE := "godot_mcp/bind_mode"
13
+ const SETTING_CUSTOM_BIND_IP := "godot_mcp/custom_bind_ip"
14
+ const SETTING_PORT_OVERRIDE_ENABLED := "godot_mcp/port_override_enabled"
15
+ const SETTING_PORT_OVERRIDE := "godot_mcp/port_override"
16
+
12
17
  var _websocket_server: WebSocketServer
13
18
  var _command_router: CommandRouter
14
19
  var _status_panel: Control
15
20
  var _debugger_plugin: MCPDebuggerPlugin
16
21
 
22
+ var _current_bind_address := MCPConstants.LOCALHOST_BIND_ADDRESS
23
+ var _current_bind_mode: MCPEnums.BindMode = MCPEnums.BindMode.LOCALHOST
24
+
17
25
 
18
26
  func _enter_tree() -> void:
19
27
  _command_router = CommandRouter.new()
@@ -27,14 +35,14 @@ func _enter_tree() -> void:
27
35
 
28
36
  _status_panel = StatusPanel.instantiate()
29
37
  add_control_to_bottom_panel(_status_panel, "MCP")
30
- _update_status("Waiting for connection...")
31
38
 
32
39
  _debugger_plugin = MCPDebuggerPlugin.new()
33
40
  add_debugger_plugin(_debugger_plugin)
34
41
 
35
42
  _ensure_game_bridge_autoload()
36
-
37
- _websocket_server.start_server()
43
+ _ensure_bind_settings()
44
+ _setup_bind_ui()
45
+ _apply_bind_settings(true)
38
46
  MCPLog.info("Plugin initialized")
39
47
 
40
48
 
@@ -57,6 +65,152 @@ func _exit_tree() -> void:
57
65
  MCPLog.info("Plugin disabled")
58
66
 
59
67
 
68
+ func _ensure_bind_settings() -> void:
69
+ if not ProjectSettings.has_setting(SETTING_BIND_MODE):
70
+ ProjectSettings.set_setting(SETTING_BIND_MODE, MCPEnums.BindMode.LOCALHOST)
71
+ if not ProjectSettings.has_setting(SETTING_CUSTOM_BIND_IP):
72
+ ProjectSettings.set_setting(SETTING_CUSTOM_BIND_IP, "")
73
+ if not ProjectSettings.has_setting(SETTING_PORT_OVERRIDE_ENABLED):
74
+ ProjectSettings.set_setting(SETTING_PORT_OVERRIDE_ENABLED, false)
75
+ if not ProjectSettings.has_setting(SETTING_PORT_OVERRIDE):
76
+ ProjectSettings.set_setting(SETTING_PORT_OVERRIDE, WebSocketServer.DEFAULT_PORT)
77
+ ProjectSettings.save()
78
+
79
+
80
+ func _setup_bind_ui() -> void:
81
+ if not _status_panel:
82
+ return
83
+ if _status_panel.has_method("set_config"):
84
+ _status_panel.set_config(_get_bind_mode(), _get_custom_bind_ip(), _get_port_override_enabled(), _get_port_override())
85
+ if _status_panel.has_signal("config_applied") and not _status_panel.config_applied.is_connected(_on_config_applied):
86
+ _status_panel.config_applied.connect(_on_config_applied)
87
+
88
+
89
+ func _get_bind_mode() -> MCPEnums.BindMode:
90
+ return ProjectSettings.get_setting(SETTING_BIND_MODE, MCPEnums.BindMode.LOCALHOST) as MCPEnums.BindMode
91
+
92
+
93
+ func _get_custom_bind_ip() -> String:
94
+ return str(ProjectSettings.get_setting(SETTING_CUSTOM_BIND_IP, ""))
95
+
96
+
97
+ func _get_port_override_enabled() -> bool:
98
+ return bool(ProjectSettings.get_setting(SETTING_PORT_OVERRIDE_ENABLED, false))
99
+
100
+
101
+ func _get_port_override() -> int:
102
+ var raw_value := ProjectSettings.get_setting(SETTING_PORT_OVERRIDE, WebSocketServer.DEFAULT_PORT)
103
+ var port := int(raw_value)
104
+ if port < MCPConstants.PORT_MIN or port > MCPConstants.PORT_MAX:
105
+ MCPLog.warn("Invalid port override '%s'; falling back to default port %d" % [str(raw_value), WebSocketServer.DEFAULT_PORT])
106
+ return WebSocketServer.DEFAULT_PORT
107
+ return port
108
+
109
+
110
+ func _get_listen_port() -> int:
111
+ return _get_port_override() if _get_port_override_enabled() else WebSocketServer.DEFAULT_PORT
112
+
113
+
114
+ func _resolve_bind_address() -> String:
115
+ match _get_bind_mode():
116
+ MCPEnums.BindMode.WSL:
117
+ var ip := _get_wsl_vethernet_ipv4()
118
+ if ip.is_empty():
119
+ MCPLog.warn("WSL bind mode selected but vEthernet (WSL) IPv4 was not found; falling back to %s" % MCPConstants.LOCALHOST_BIND_ADDRESS)
120
+ return MCPConstants.LOCALHOST_BIND_ADDRESS
121
+ return ip
122
+ MCPEnums.BindMode.CUSTOM:
123
+ var ip := _get_custom_bind_ip().strip_edges()
124
+ if ip.is_empty():
125
+ MCPLog.warn("Custom bind mode selected but no IP was configured; falling back to %s" % MCPConstants.LOCALHOST_BIND_ADDRESS)
126
+ return MCPConstants.LOCALHOST_BIND_ADDRESS
127
+ if not _is_valid_ipv4(ip):
128
+ MCPLog.warn("Custom bind mode selected but IP '%s' is not a valid IPv4 address; falling back to %s" % [ip, MCPConstants.LOCALHOST_BIND_ADDRESS])
129
+ return MCPConstants.LOCALHOST_BIND_ADDRESS
130
+ return ip
131
+ _:
132
+ return MCPConstants.LOCALHOST_BIND_ADDRESS
133
+
134
+
135
+ func _is_valid_ipv4(ip: String) -> bool:
136
+ var s := ip.strip_edges()
137
+ if s.is_empty():
138
+ return false
139
+ var parts := s.split(".")
140
+ if parts.size() != 4:
141
+ return false
142
+ for p in parts:
143
+ if p.is_empty() or not p.is_valid_int():
144
+ return false
145
+ var n := int(p)
146
+ if n < 0 or n > 255:
147
+ return false
148
+ return true
149
+
150
+
151
+ func _get_wsl_vethernet_ipv4() -> String:
152
+ # Autodetect "vEthernet (WSL)" IPv4 via PowerShell (Windows only).
153
+ if OS.get_name() != "Windows":
154
+ return ""
155
+
156
+ var output := []
157
+ # Use ErrorAction Stop + catch so failures return an empty string and don't emit noisy errors.
158
+ # Match any adapter alias that contains "WSL" to be resilient to name variations.
159
+ # Note: The wildcard pattern 'vEthernet*WSL*' provides flexibility but may match
160
+ # unexpected adapters in custom network configurations. Document expected adapter names.
161
+ # SECURITY NOTE: Keep this PowerShell command as a fixed string literal. Do NOT concatenate
162
+ # user input, project settings, environment variables, or any other external data into it,
163
+ # as that could introduce command injection vulnerabilities. If dynamic behavior is needed,
164
+ # implement strict validation and avoid direct string interpolation into PowerShell.
165
+ var cmd := "try { $ip = (Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop | Where-Object { $_.InterfaceAlias -like 'vEthernet*WSL*' } | Select-Object -First 1 -ExpandProperty IPAddress); if ($ip) { $ip } else { '' } } catch { '' }"
166
+ var args := ["-NoProfile", "-Command", cmd]
167
+ var code := OS.execute("powershell", args, output, false)
168
+ if code != 0 or output.is_empty():
169
+ return ""
170
+
171
+ var text := String(output[0]).replace("\r", "")
172
+ for line in text.split("\n"):
173
+ var candidate := String(line).strip_edges()
174
+ if _is_valid_ipv4(candidate):
175
+ return candidate
176
+ return ""
177
+
178
+
179
+ func _restart_server() -> void:
180
+ if _websocket_server:
181
+ _websocket_server.stop_server()
182
+ var bind := _resolve_bind_address()
183
+ var port := _get_listen_port()
184
+ _current_bind_address = bind
185
+ _current_bind_mode = _get_bind_mode()
186
+ var mode_name := MCPEnums.get_mode_name(_current_bind_mode)
187
+ if _websocket_server:
188
+ var err := _websocket_server.start_server(port, bind)
189
+ if err != OK:
190
+ _update_status("Failed to bind %s:%d [%s]" % [bind, port, mode_name])
191
+ return
192
+ _update_status("Waiting for connection... (bind %s:%d [%s])" % [bind, port, mode_name])
193
+ MCPLog.info("Server listening on %s:%d [%s]" % [bind, port, mode_name])
194
+
195
+
196
+ func _apply_bind_settings(restart: bool) -> void:
197
+ _current_bind_address = _resolve_bind_address()
198
+ _current_bind_mode = _get_bind_mode()
199
+ if restart:
200
+ _restart_server()
201
+ else:
202
+ _update_status("Waiting for connection... (bind %s:%d [%s])" % [_current_bind_address, _get_listen_port(), MCPEnums.get_mode_name(_current_bind_mode)])
203
+
204
+
205
+ func _on_config_applied(config: Dictionary) -> void:
206
+ ProjectSettings.set_setting(SETTING_BIND_MODE, config.get("bind_mode", MCPEnums.BindMode.LOCALHOST))
207
+ ProjectSettings.set_setting(SETTING_CUSTOM_BIND_IP, str(config.get("custom_ip", "")))
208
+ ProjectSettings.set_setting(SETTING_PORT_OVERRIDE_ENABLED, bool(config.get("port_override_enabled", false)))
209
+ ProjectSettings.set_setting(SETTING_PORT_OVERRIDE, int(config.get("port_override", WebSocketServer.DEFAULT_PORT)))
210
+ ProjectSettings.save()
211
+ _apply_bind_settings(true)
212
+
213
+
60
214
  func _ensure_game_bridge_autoload() -> void:
61
215
  if not ProjectSettings.has_setting("autoload/" + GAME_BRIDGE_AUTOLOAD):
62
216
  ProjectSettings.set_setting("autoload/" + GAME_BRIDGE_AUTOLOAD, GAME_BRIDGE_PATH)
@@ -75,8 +229,12 @@ func _on_command_received(id: String, command: String, params: Dictionary) -> vo
75
229
 
76
230
 
77
231
  func _on_client_connected() -> void:
78
- _update_status("Connected")
79
- MCPLog.info("Client connected")
232
+ var host_info := ""
233
+ if _websocket_server.get_connected_host():
234
+ host_info = " from %s:%d" % [_websocket_server.get_connected_host(), _websocket_server.get_connected_port()]
235
+ var bind_info := "(%s: %s:%d)" % [MCPEnums.get_mode_name(_current_bind_mode), _current_bind_address, _get_listen_port()]
236
+ _update_status("Connected%s %s" % [host_info, bind_info])
237
+ MCPLog.info("Client connected%s %s" % [host_info, bind_info])
80
238
 
81
239
 
82
240
  func _on_client_disconnected() -> void:
@@ -1,11 +1,60 @@
1
1
  @tool
2
2
  extends Control
3
3
 
4
- @onready var status_label: Label = $MarginContainer/HBoxContainer/StatusLabel
5
- @onready var status_icon: ColorRect = $MarginContainer/HBoxContainer/StatusIcon
4
+ signal config_applied(config: Dictionary)
5
+
6
+ @onready var status_label: Label = $MarginContainer/VBoxContainer/StatusRow/StatusLabel
7
+ @onready var status_icon: ColorRect = $MarginContainer/VBoxContainer/StatusRow/StatusIcon
8
+ @onready var bind_mode_option: OptionButton = $MarginContainer/VBoxContainer/SettingsGrid/BindModeOption
9
+ @onready var custom_ip_label: Label = $MarginContainer/VBoxContainer/SettingsGrid/CustomIpLabel
10
+ @onready var custom_ip_edit: LineEdit = $MarginContainer/VBoxContainer/SettingsGrid/CustomIpControls/CustomIpEdit
11
+ @onready var port_override_label: Label = $MarginContainer/VBoxContainer/SettingsGrid/PortOverrideLabel
12
+ @onready var port_override_enabled: CheckBox = $MarginContainer/VBoxContainer/SettingsGrid/PortOverrideControls/PortOverrideEnabled
13
+ @onready var port_override_spin: SpinBox = $MarginContainer/VBoxContainer/SettingsGrid/PortOverrideControls/PortOverrideSpin
14
+ @onready var apply_button: Button = $MarginContainer/VBoxContainer/ApplyRow/ApplyButton
15
+
16
+ var _updating_ui := false
6
17
 
7
18
 
8
19
  func _ready() -> void:
20
+ if bind_mode_option:
21
+ _updating_ui = true
22
+ bind_mode_option.clear()
23
+ bind_mode_option.add_item("Localhost", 0)
24
+ bind_mode_option.add_item("WSL", 1)
25
+ bind_mode_option.add_item("Custom", 2)
26
+ bind_mode_option.selected = 0
27
+ _updating_ui = false
28
+ bind_mode_option.item_selected.connect(_on_bind_mode_selected)
29
+
30
+ if apply_button:
31
+ apply_button.pressed.connect(_on_apply_pressed)
32
+
33
+ if port_override_enabled:
34
+ port_override_enabled.toggled.connect(_on_port_override_toggled)
35
+ if port_override_spin:
36
+ port_override_spin.value = 6550
37
+
38
+ # Keyboard navigation / focus
39
+ _for_control_focus(bind_mode_option)
40
+ _for_control_focus(custom_ip_edit)
41
+ _for_control_focus(port_override_enabled)
42
+ _for_control_focus(port_override_spin)
43
+ _for_control_focus(apply_button)
44
+
45
+ # Set up focus chain: each control points to the next in sequence
46
+ if bind_mode_option and (custom_ip_edit or apply_button):
47
+ bind_mode_option.focus_next = custom_ip_edit.get_path() if custom_ip_edit else apply_button.get_path()
48
+ if custom_ip_edit and (port_override_enabled or apply_button):
49
+ custom_ip_edit.focus_next = port_override_enabled.get_path() if port_override_enabled else apply_button.get_path()
50
+ if port_override_enabled and (port_override_spin or apply_button):
51
+ port_override_enabled.focus_next = port_override_spin.get_path() if port_override_spin else apply_button.get_path()
52
+ if port_override_spin and (apply_button or bind_mode_option):
53
+ port_override_spin.focus_next = apply_button.get_path() if apply_button else bind_mode_option.get_path()
54
+ if apply_button and (bind_mode_option or apply_button):
55
+ apply_button.focus_next = bind_mode_option.get_path() if bind_mode_option else apply_button.get_path()
56
+
57
+ _update_controls_enabled()
9
58
  set_status("Initializing...")
10
59
 
11
60
 
@@ -14,10 +63,123 @@ func set_status(status: String) -> void:
14
63
  status_label.text = status
15
64
 
16
65
  if status_icon:
17
- match status:
18
- "Connected":
19
- status_icon.color = Color.GREEN
20
- "Disconnected", "Waiting for connection...":
21
- status_icon.color = Color.ORANGE
22
- _:
23
- status_icon.color = Color.GRAY
66
+ if status.begins_with("Connected"):
67
+ status_icon.color = Color.GREEN
68
+ elif status.begins_with("Disconnected") or status.begins_with("Waiting"):
69
+ status_icon.color = Color.ORANGE
70
+ else:
71
+ status_icon.color = Color.GRAY
72
+
73
+
74
+ func set_bind_mode(mode: MCPEnums.BindMode) -> void:
75
+ if not bind_mode_option:
76
+ return
77
+ _updating_ui = true
78
+ match mode:
79
+ MCPEnums.BindMode.WSL:
80
+ bind_mode_option.select(1)
81
+ MCPEnums.BindMode.CUSTOM:
82
+ bind_mode_option.select(2)
83
+ _:
84
+ bind_mode_option.select(0)
85
+ _updating_ui = false
86
+ _update_controls_enabled()
87
+
88
+
89
+ func get_bind_mode() -> MCPEnums.BindMode:
90
+ if not bind_mode_option:
91
+ return MCPEnums.BindMode.LOCALHOST
92
+ match bind_mode_option.selected:
93
+ 1:
94
+ return MCPEnums.BindMode.WSL
95
+ 2:
96
+ return MCPEnums.BindMode.CUSTOM
97
+ _:
98
+ return MCPEnums.BindMode.LOCALHOST
99
+
100
+
101
+ func set_custom_ip(ip: String) -> void:
102
+ if not custom_ip_edit:
103
+ return
104
+ _updating_ui = true
105
+ custom_ip_edit.text = ip
106
+ _updating_ui = false
107
+
108
+
109
+ func get_custom_ip() -> String:
110
+ return custom_ip_edit.text if custom_ip_edit else ""
111
+
112
+
113
+ func _on_bind_mode_selected(_idx: int) -> void:
114
+ if _updating_ui:
115
+ return
116
+ _update_controls_enabled()
117
+
118
+
119
+ func _on_port_override_toggled(_enabled: bool) -> void:
120
+ if _updating_ui:
121
+ return
122
+ _update_controls_enabled()
123
+
124
+
125
+ func _on_apply_pressed() -> void:
126
+ config_applied.emit(get_config())
127
+
128
+
129
+ func get_config() -> Dictionary:
130
+ return {
131
+ "bind_mode": get_bind_mode(),
132
+ "custom_ip": get_custom_ip(),
133
+ "port_override_enabled": port_override_enabled.button_pressed if port_override_enabled else false,
134
+ "port_override": int(port_override_spin.value) if port_override_spin else 6550,
135
+ }
136
+
137
+
138
+ func set_config(bind_mode: MCPEnums.BindMode, custom_ip: String, port_enabled: bool, port_value: int) -> void:
139
+ set_bind_mode(bind_mode)
140
+ set_custom_ip(custom_ip)
141
+ if port_override_enabled:
142
+ _updating_ui = true
143
+ port_override_enabled.button_pressed = port_enabled
144
+ _updating_ui = false
145
+ if port_override_spin:
146
+ _updating_ui = true
147
+ port_override_spin.value = clamp(port_value, 1, 65535)
148
+ _updating_ui = false
149
+ _update_controls_enabled()
150
+
151
+
152
+ func _for_control_focus(c: Control) -> void:
153
+ if not c:
154
+ return
155
+ c.focus_mode = Control.FOCUS_ALL
156
+
157
+
158
+ func _unhandled_key_input(event: InputEvent) -> void:
159
+ if not (event is InputEventKey):
160
+ return
161
+ var e := event as InputEventKey
162
+ if not e.pressed or e.echo:
163
+ return
164
+ if e.keycode == KEY_ENTER or e.keycode == KEY_KP_ENTER:
165
+ if apply_button:
166
+ _on_apply_pressed()
167
+ get_viewport().set_input_as_handled()
168
+
169
+
170
+ func _update_controls_enabled() -> void:
171
+ # Custom IP only editable in Custom mode
172
+ var custom_ip_enabled := get_bind_mode() == MCPEnums.BindMode.CUSTOM
173
+ if custom_ip_edit:
174
+ custom_ip_edit.editable = custom_ip_enabled
175
+ custom_ip_edit.modulate.a = 1.0 if custom_ip_enabled else 0.5
176
+ if custom_ip_label:
177
+ custom_ip_label.modulate.a = 1.0 if custom_ip_enabled else 0.5
178
+
179
+ # Port override controls
180
+ var port_enabled := port_override_enabled and port_override_enabled.button_pressed
181
+ if port_override_spin:
182
+ port_override_spin.editable = port_enabled
183
+ port_override_spin.modulate.a = 1.0 if port_enabled else 0.5
184
+ if port_override_label:
185
+ port_override_label.modulate.a = 1.0 if port_enabled else 0.5