@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.
- package/README.md +75 -43
- package/addon/core/mcp_constants.gd +5 -0
- package/addon/core/mcp_enums.gd +10 -0
- package/addon/plugin.cfg +1 -1
- package/addon/plugin.gd +163 -5
- package/addon/ui/status_panel.gd +171 -9
- package/addon/ui/status_panel.tscn +80 -4
- package/addon/websocket_server.gd +22 -5
- package/dist/__tests__/utils/connection-strategy.test.d.ts +2 -0
- package/dist/__tests__/utils/connection-strategy.test.d.ts.map +1 -0
- package/dist/__tests__/utils/connection-strategy.test.js +106 -0
- package/dist/__tests__/utils/connection-strategy.test.js.map +1 -0
- package/dist/__tests__/utils/host-ip-resolver.test.d.ts +2 -0
- package/dist/__tests__/utils/host-ip-resolver.test.d.ts.map +1 -0
- package/dist/__tests__/utils/host-ip-resolver.test.js +116 -0
- package/dist/__tests__/utils/host-ip-resolver.test.js.map +1 -0
- package/dist/__tests__/utils/wsl-detection.test.d.ts +2 -0
- package/dist/__tests__/utils/wsl-detection.test.d.ts.map +1 -0
- package/dist/__tests__/utils/wsl-detection.test.js +116 -0
- package/dist/__tests__/utils/wsl-detection.test.js.map +1 -0
- package/dist/connection/websocket.d.ts +3 -1
- package/dist/connection/websocket.d.ts.map +1 -1
- package/dist/connection/websocket.js +21 -11
- package/dist/connection/websocket.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/resources/script.d.ts.map +1 -1
- package/dist/resources/script.js +4 -5
- package/dist/resources/script.js.map +1 -1
- package/dist/tools/editor.js +2 -2
- package/dist/tools/editor.js.map +1 -1
- package/dist/utils/connection-strategy.d.ts +23 -0
- package/dist/utils/connection-strategy.d.ts.map +1 -0
- package/dist/utils/connection-strategy.js +44 -0
- package/dist/utils/connection-strategy.js.map +1 -0
- package/dist/utils/gateway-resolver.d.ts +18 -0
- package/dist/utils/gateway-resolver.d.ts.map +1 -0
- package/dist/utils/gateway-resolver.js +173 -0
- package/dist/utils/gateway-resolver.js.map +1 -0
- package/dist/utils/host-ip-resolver.d.ts +19 -0
- package/dist/utils/host-ip-resolver.d.ts.map +1 -0
- package/dist/utils/host-ip-resolver.js +60 -0
- package/dist/utils/host-ip-resolver.js.map +1 -0
- package/dist/utils/wsl-detection.d.ts +11 -0
- package/dist/utils/wsl-detection.d.ts.map +1 -0
- package/dist/utils/wsl-detection.js +43 -0
- package/dist/utils/wsl-detection.js.map +1 -0
- package/package.json +5 -2
- package/addon/command_router.gd.uid +0 -1
- package/addon/commands/animation_commands.gd.uid +0 -1
- package/addon/commands/debug_commands.gd.uid +0 -1
- package/addon/commands/node_commands.gd.uid +0 -1
- package/addon/commands/project_commands.gd.uid +0 -1
- package/addon/commands/resource_commands.gd.uid +0 -1
- package/addon/commands/scene3d_commands.gd.uid +0 -1
- package/addon/commands/scene_commands.gd.uid +0 -1
- package/addon/commands/screenshot_commands.gd.uid +0 -1
- package/addon/commands/script_commands.gd.uid +0 -1
- package/addon/commands/selection_commands.gd.uid +0 -1
- package/addon/commands/tilemap_commands.gd.uid +0 -1
- package/addon/core/base_command.gd.uid +0 -1
- package/addon/core/mcp_debugger_plugin.gd.uid +0 -1
- package/addon/core/mcp_logger.gd.uid +0 -1
- package/addon/core/mcp_utils.gd.uid +0 -1
- package/addon/game_bridge/mcp_game_bridge.gd.uid +0 -1
- package/addon/plugin.gd.uid +0 -1
- package/addon/ui/status_panel.gd.uid +0 -1
- package/addon/websocket_server.gd.uid +0 -1
package/README.md
CHANGED
|
@@ -1,33 +1,18 @@
|
|
|
1
1
|
# godot-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
MCP server that connects Claude to your Godot editor. Less copy-paste, more creating.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Why This Exists
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
|
13
|
+
### 1. Configure your AI assistant
|
|
29
14
|
|
|
30
|
-
**Claude Desktop**
|
|
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**
|
|
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
|
|
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
|
-
|
|
47
|
+
Enable in Godot: **Project Settings > Plugins > Godot MCP**
|
|
63
48
|
|
|
64
|
-
### 3.
|
|
49
|
+
### 3. Go
|
|
65
50
|
|
|
66
|
-
Open your Godot project
|
|
51
|
+
Open your Godot project, restart your AI assistant, and start building.
|
|
67
52
|
|
|
68
|
-
|
|
53
|
+
## What Claude Can Do
|
|
69
54
|
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,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
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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:
|
package/addon/ui/status_panel.gd
CHANGED
|
@@ -1,11 +1,60 @@
|
|
|
1
1
|
@tool
|
|
2
2
|
extends Control
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|