gopeak 2.3.5 → 2.3.7
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 +71 -37
- package/build/addon/godot_mcp_editor/tools/scene_tools.gd +94 -43
- package/build/addon/godot_mcp_runtime/mcp_runtime_autoload.gd +3 -0
- package/build/godot-bridge.js +9 -2
- package/build/index.js +198 -163
- package/build/scripts/godot_operations.gd +9 -1
- package/build/tool-definitions.js +2 -2
- package/package.json +14 -8
- package/scripts/postinstall.mjs +29 -0
package/README.md
CHANGED
|
@@ -10,10 +10,14 @@
|
|
|
10
10
|
[](https://github.com/HaD0Yun/Gopeak-godot-mcp/network/members)
|
|
11
11
|
[](https://opensource.org/licenses/MIT)
|
|
12
12
|
|
|
13
|
+
🌐 **Languages**: **English** | [한국어](README-ko.md) | [日本語](README-ja.md) | [Deutsch](README-de.md) | [Português](README-pt_BR.md) | [简体中文](README-zh.md)
|
|
14
|
+
|
|
13
15
|

|
|
14
16
|
|
|
15
17
|
**GoPeak is an MCP server for Godot that lets AI assistants run, inspect, modify, and debug real projects end-to-end.**
|
|
16
18
|
|
|
19
|
+
> English is the canonical source of truth. Localized READMEs are concise overviews and may lag behind `README.md`.
|
|
20
|
+
|
|
17
21
|
> Discord community chat is temporarily unavailable while the invite link is refreshed. Please use GitHub Discussions in the meantime: https://github.com/HaD0Yun/Gopeak-godot-mcp/discussions
|
|
18
22
|
|
|
19
23
|
---
|
|
@@ -64,7 +68,7 @@ gopeak setup
|
|
|
64
68
|
}
|
|
65
69
|
```
|
|
66
70
|
|
|
67
|
-
> `GOPEAK_TOOL_PROFILE=compact` is the default. It exposes
|
|
71
|
+
> `GOPEAK_TOOL_PROFILE=compact` is the default. It exposes a trusted core surface with dynamic tool groups that activate on demand — keeping token usage low while making experimental or environment-dependent capabilities explicit.
|
|
68
72
|
|
|
69
73
|
### 3) First prompts to try
|
|
70
74
|
|
|
@@ -77,10 +81,10 @@ gopeak setup
|
|
|
77
81
|
## Why GoPeak
|
|
78
82
|
|
|
79
83
|
- **Real project feedback loop**: run the game, inspect logs, and fix in-context.
|
|
80
|
-
- **
|
|
81
|
-
- **Token-efficient by default**: compact
|
|
82
|
-
- **Dynamic tool groups**: search with `tool.catalog` and matching groups auto-activate
|
|
83
|
-
- **Deep Godot integration**: ClassDB queries, runtime inspection, debugger hooks, bridge-based scene/resource edits.
|
|
84
|
+
- **Trusted Godot 4 workflow tools** across project discovery, scene/script/resource editing, runtime/LSP/DAP integrations, testing, and asset workflows.
|
|
85
|
+
- **Token-efficient by default**: compact core surface + dynamic tool groups. Activate only the capability family you need instead of loading the full legacy surface.
|
|
86
|
+
- **Dynamic tool groups with explicit maturity labels**: search with `tool.catalog` and matching groups auto-activate, or manually activate with `tool.groups`.
|
|
87
|
+
- **Deep Godot integration with setup gates**: ClassDB queries, runtime inspection, debugger hooks, and bridge-based scene/resource edits clearly state addon, editor, LSP, DAP, or runtime requirements.
|
|
84
88
|
|
|
85
89
|
### Best For
|
|
86
90
|
|
|
@@ -94,9 +98,9 @@ gopeak setup
|
|
|
94
98
|
|
|
95
99
|
GoPeak supports three exposure profiles:
|
|
96
100
|
|
|
97
|
-
- `compact` (default):
|
|
98
|
-
- `full`: exposes full legacy tool list
|
|
99
|
-
- `legacy`: same exposed behavior as `full`
|
|
101
|
+
- `compact` (default): trusted core tools plus dynamic tool groups activated on demand.
|
|
102
|
+
- `full`: exposes the full legacy tool list for compatibility and audit work.
|
|
103
|
+
- `legacy`: same exposed behavior as `full` for older configurations.
|
|
100
104
|
|
|
101
105
|
Configure with either:
|
|
102
106
|
|
|
@@ -105,32 +109,32 @@ Configure with either:
|
|
|
105
109
|
|
|
106
110
|
### Dynamic Tool Groups (compact mode)
|
|
107
111
|
|
|
108
|
-
In `compact` mode,
|
|
112
|
+
In `compact` mode, additional tools are organized into dynamic groups that activate automatically when needed. Maturity labels are intentionally conservative until each group has Godot 4 fixture evidence:
|
|
109
113
|
|
|
110
|
-
| Group |
|
|
114
|
+
| Group | Maturity | Description / setup gate |
|
|
111
115
|
|---|---|---|
|
|
112
|
-
| `scene_advanced` |
|
|
113
|
-
| `uid` |
|
|
114
|
-
| `import_export` |
|
|
115
|
-
| `autoload` |
|
|
116
|
-
| `signal` |
|
|
117
|
-
| `runtime` |
|
|
118
|
-
| `resource` |
|
|
119
|
-
| `animation` |
|
|
120
|
-
| `plugin` |
|
|
121
|
-
| `input` |
|
|
122
|
-
| `tilemap` |
|
|
123
|
-
| `audio` |
|
|
124
|
-
| `navigation` |
|
|
125
|
-
| `theme_ui` |
|
|
126
|
-
| `asset_store` |
|
|
127
|
-
| `testing` |
|
|
128
|
-
| `dx_tools` |
|
|
129
|
-
| `intent_tracking` |
|
|
130
|
-
| `class_advanced` |
|
|
131
|
-
| `lsp` |
|
|
132
|
-
| `dap` |
|
|
133
|
-
| `version_gate` |
|
|
116
|
+
| `scene_advanced` | audit-required | Duplicate, reparent nodes, load sprites; verify scene ownership/persistence in a Godot 4 fixture before treating as trusted. |
|
|
117
|
+
| `uid` | audit-required | UID management for resources; gate behavior by Godot version and resource save/load evidence. |
|
|
118
|
+
| `import_export` | audit-required | Import pipeline, reimport, validate project; requires fixture checks for import settings and export command construction. |
|
|
119
|
+
| `autoload` | audit-required | Autoload singletons, main scene; requires project settings round-trip verification. |
|
|
120
|
+
| `signal` | audit-required | Disconnect signals, list connections; requires signal round-trip verification. |
|
|
121
|
+
| `runtime` | optional-runtime | Live scene inspection, runtime properties, metrics; requires the runtime addon/socket. |
|
|
122
|
+
| `resource` | audit-required | Create/modify materials, shaders, resources; requires resource save/load fixture checks. |
|
|
123
|
+
| `animation` | audit-required | Animations, tracks, animation tree, state machine; requires saved scene/resource fixture checks. |
|
|
124
|
+
| `plugin` | optional-editor | Enable/disable/list editor plugins; requires editor plugin availability. |
|
|
125
|
+
| `input` | audit-required | Input action mapping; requires project settings round-trip verification. |
|
|
126
|
+
| `tilemap` | audit-required | TileSet and TileMap cell painting; must account for Godot 4.3+ `TileMapLayer` behavior before being promoted. |
|
|
127
|
+
| `audio` | audit-required | Audio buses, effects, volume; requires bus layout fixture checks. |
|
|
128
|
+
| `navigation` | audit-required | Navigation regions and agents; requires saved scene/resource fixture checks. |
|
|
129
|
+
| `theme_ui` | audit-required | Theme colors, font sizes, shaders; requires resource save/load fixture checks. |
|
|
130
|
+
| `asset_store` | optional-network | Search/download external CC0 assets; requires network/provider availability and should remain optional. |
|
|
131
|
+
| `testing` | optional-runtime | Screenshots, viewport capture, input injection; requires runtime addon/editor availability. |
|
|
132
|
+
| `dx_tools` | audit-required | Error log, project health, find usages, scaffold; requires deterministic static fixture checks. |
|
|
133
|
+
| `intent_tracking` | workflow-layer | Intent capture, decision logs, handoff briefs; not a Godot engine primitive and should stay opt-in/workflow-scoped. |
|
|
134
|
+
| `class_advanced` | trusted-static | Class inheritance inspection; validate against ClassDB/static evidence. |
|
|
135
|
+
| `lsp` | optional-lsp | GDScript completions, hover, symbols; requires Godot LSP on port 6005. |
|
|
136
|
+
| `dap` | optional-dap | Breakpoints, stepping, stack traces; requires Godot DAP on port 6006. |
|
|
137
|
+
| `version_gate` | audit-required | Version validation and patch verification; requires explicit version fixture evidence. |
|
|
134
138
|
|
|
135
139
|
**How it works:**
|
|
136
140
|
|
|
@@ -144,10 +148,24 @@ In `compact` mode, 78 additional tools are organized into **22 groups** that act
|
|
|
144
148
|
> "Use `tool.groups` to reset all active groups."
|
|
145
149
|
|
|
146
150
|
The server sends `notifications/tools/list_changed` so MCP clients (Claude Code, Claude Desktop) automatically refresh the tool list.
|
|
151
|
+
If your MCP client caches tools aggressively and does not refresh after activation, reconnect the client or call the newly activated tool directly once to force a fresh `tools/list` round-trip.
|
|
152
|
+
|
|
153
|
+
### Typed property values for scene tools
|
|
154
|
+
|
|
155
|
+
Bridge-backed scene tools (`add_node`, `set_node_properties`) now coerce common vector payloads such as `{ "x": 100, "y": 200 }` and `[100, 200]` for typed properties like `position` and `scale`. Tagged values are still the safest cross-tool form:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"position": { "type": "Vector2", "x": 100, "y": 200 },
|
|
160
|
+
"scale": { "type": "Vector2", "x": 2, "y": 2 }
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The internal headless serializer uses `_type`, but MCP callers should prefer `type` when they need an explicit cross-tool Godot value tag.
|
|
147
165
|
|
|
148
166
|
### Don't worry about tokens
|
|
149
167
|
|
|
150
|
-
GoPeak uses **cursor-based pagination** for `tools/list` — even in `full` profile, tools are delivered in pages (default 33) instead of dumping
|
|
168
|
+
GoPeak uses **cursor-based pagination** for `tools/list` — even in `full` profile, tools are delivered in pages (default 33) instead of dumping the entire legacy definition set at once. Your AI client fetches the next page only when it needs more.
|
|
151
169
|
|
|
152
170
|
Set page size with `GOPEAK_TOOLS_PAGE_SIZE`:
|
|
153
171
|
|
|
@@ -260,15 +278,17 @@ Then enable plugins in **Project Settings → Plugins** (especially `godot_mcp_e
|
|
|
260
278
|
|
|
261
279
|
## Core Capabilities
|
|
262
280
|
|
|
281
|
+
These capabilities are grouped by workflow value. Optional-runtime/LSP/DAP/network groups require their setup gates before use; audit-required groups should be promoted only after fixture evidence.
|
|
282
|
+
|
|
263
283
|
- **Project control**: launch editor, run/stop project, capture debug output
|
|
264
284
|
- **Scene editing**: create scenes, add/delete/reparent nodes, edit properties
|
|
265
285
|
- **Script workflows**: create/modify scripts, inspect script structure
|
|
266
286
|
- **Resources**: create/modify resources, materials, shaders, tilesets
|
|
267
287
|
- **Signals/animation**: connect signals, build animations/tracks/state machines
|
|
268
|
-
- **Runtime tools**: inspect live tree, set properties, call methods, metrics
|
|
269
|
-
- **LSP + DAP**: diagnostics/completion/hover + breakpoints/step/stack trace
|
|
270
|
-
- **Input + screenshots**: keyboard/mouse/action injection and viewport capture
|
|
271
|
-
- **Asset library**: search/fetch CC0 assets (
|
|
288
|
+
- **Runtime tools**: inspect live tree, set properties, call methods, metrics (requires runtime addon/socket)
|
|
289
|
+
- **LSP + DAP**: diagnostics/completion/hover + breakpoints/step/stack trace (requires Godot LSP/DAP ports)
|
|
290
|
+
- **Input + screenshots**: keyboard/mouse/action injection and viewport capture (requires runtime/editor bridge setup)
|
|
291
|
+
- **Asset library**: search/fetch CC0 assets (optional network/provider workflow)
|
|
272
292
|
|
|
273
293
|
### Tool families (examples)
|
|
274
294
|
|
|
@@ -313,6 +333,19 @@ Visualize your entire project architecture with `visualizer.map` (`map_project`
|
|
|
313
333
|
|
|
314
334
|
---
|
|
315
335
|
|
|
336
|
+
## Migration & Deprecation Policy
|
|
337
|
+
|
|
338
|
+
GoPeak treats `compact` as the safe default and `full`/`legacy` as compatibility profiles. Any future hide, remove, rename, or API-contract change must include:
|
|
339
|
+
|
|
340
|
+
1. an old → new mapping or an explicit no-replacement note;
|
|
341
|
+
2. the profile impact (`compact`, `full`, `legacy`, or opt-in group);
|
|
342
|
+
3. whether an alias remains and the planned removal window;
|
|
343
|
+
4. README/docs and release-note updates;
|
|
344
|
+
5. a verification command proving `tools/list` exposure and alias behavior;
|
|
345
|
+
6. a migration prompt example for common Godot workflows.
|
|
346
|
+
|
|
347
|
+
Current compatibility stance: legacy tool names and compact aliases remain supported. Dynamic groups with optional external requirements (`runtime`, `testing`, `lsp`, `dap`, `asset_store`) should be documented as opt-in/setup-gated rather than marketed as always-available core behavior.
|
|
348
|
+
|
|
316
349
|
## Technical Reference
|
|
317
350
|
|
|
318
351
|
### Environment variables
|
|
@@ -353,6 +386,7 @@ Visualize your entire project architecture with `visualizer.map` (`map_project`
|
|
|
353
386
|
- **Project path invalid** → confirm `project.godot` exists
|
|
354
387
|
- **Runtime tools not working** → install/enable runtime addon plugin
|
|
355
388
|
- **Need a tool that is not visible** → run `tool.catalog` to search and auto-activate matching groups, or use `tool.groups` to activate a specific group
|
|
389
|
+
- **`get_editor_status` says disconnected while the Godot editor shows connected** → check whether another `gopeak`/MCP server instance already owns bridge port `6505`; the status payload now reports the startup error and suggests stopping duplicate servers
|
|
356
390
|
|
|
357
391
|
---
|
|
358
392
|
|
|
@@ -81,49 +81,92 @@ func _find_node(root: Node, path: String) -> Node:
|
|
|
81
81
|
return root.get_node_or_null(path)
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
func _parse_value(value):
|
|
85
|
-
if typeof(value) == TYPE_DICTIONARY
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
84
|
+
func _parse_value(value, expected_type: int = TYPE_NIL):
|
|
85
|
+
if typeof(value) == TYPE_DICTIONARY:
|
|
86
|
+
var type_tag := ""
|
|
87
|
+
if value.has("type"):
|
|
88
|
+
type_tag = str(value["type"])
|
|
89
|
+
elif value.has("_type"):
|
|
90
|
+
type_tag = str(value["_type"])
|
|
91
|
+
|
|
92
|
+
if not type_tag.is_empty():
|
|
93
|
+
match type_tag:
|
|
94
|
+
"Vector2":
|
|
95
|
+
return Vector2(value.get("x", 0), value.get("y", 0))
|
|
96
|
+
"Vector3":
|
|
97
|
+
return Vector3(value.get("x", 0), value.get("y", 0), value.get("z", 0))
|
|
98
|
+
"Color":
|
|
99
|
+
return Color(value.get("r", 1), value.get("g", 1), value.get("b", 1), value.get("a", 1))
|
|
100
|
+
"Vector2i":
|
|
101
|
+
return Vector2i(value.get("x", 0), value.get("y", 0))
|
|
102
|
+
"Vector3i":
|
|
103
|
+
return Vector3i(value.get("x", 0), value.get("y", 0), value.get("z", 0))
|
|
104
|
+
"Rect2":
|
|
105
|
+
return Rect2(value.get("x", 0), value.get("y", 0), value.get("width", 0), value.get("height", 0))
|
|
106
|
+
"Transform2D":
|
|
107
|
+
if value.has("x") and value.has("y") and value.has("origin"):
|
|
108
|
+
var xx: Dictionary = value["x"]
|
|
109
|
+
var yy: Dictionary = value["y"]
|
|
110
|
+
var oo: Dictionary = value["origin"]
|
|
111
|
+
return Transform2D(
|
|
112
|
+
Vector2(xx.get("x", 1), xx.get("y", 0)),
|
|
113
|
+
Vector2(yy.get("x", 0), yy.get("y", 1)),
|
|
114
|
+
Vector2(oo.get("x", 0), oo.get("y", 0))
|
|
115
|
+
)
|
|
116
|
+
"Transform3D":
|
|
117
|
+
if value.has("basis") and value.has("origin"):
|
|
118
|
+
var b: Dictionary = value["basis"]
|
|
119
|
+
var o: Dictionary = value["origin"]
|
|
120
|
+
var basis := Basis(
|
|
121
|
+
Vector3(b.get("x", {}).get("x", 1), b.get("x", {}).get("y", 0), b.get("x", {}).get("z", 0)),
|
|
122
|
+
Vector3(b.get("y", {}).get("x", 0), b.get("y", {}).get("y", 1), b.get("y", {}).get("z", 0)),
|
|
123
|
+
Vector3(b.get("z", {}).get("x", 0), b.get("z", {}).get("y", 0), b.get("z", {}).get("z", 1))
|
|
124
|
+
)
|
|
125
|
+
return Transform3D(basis, Vector3(o.get("x", 0), o.get("y", 0), o.get("z", 0)))
|
|
126
|
+
"NodePath":
|
|
127
|
+
return NodePath(value.get("path", ""))
|
|
128
|
+
"Resource":
|
|
129
|
+
var resource_path: String = str(value.get("path", ""))
|
|
130
|
+
if resource_path.is_empty():
|
|
131
|
+
return null
|
|
132
|
+
return load(resource_path)
|
|
133
|
+
|
|
134
|
+
match expected_type:
|
|
135
|
+
TYPE_VECTOR2:
|
|
136
|
+
if value.has("x") and value.has("y"):
|
|
137
|
+
return Vector2(value.get("x", 0), value.get("y", 0))
|
|
138
|
+
TYPE_VECTOR2I:
|
|
139
|
+
if value.has("x") and value.has("y"):
|
|
140
|
+
return Vector2i(value.get("x", 0), value.get("y", 0))
|
|
141
|
+
TYPE_VECTOR3:
|
|
142
|
+
if value.has("x") and value.has("y") and value.has("z"):
|
|
143
|
+
return Vector3(value.get("x", 0), value.get("y", 0), value.get("z", 0))
|
|
144
|
+
TYPE_VECTOR3I:
|
|
145
|
+
if value.has("x") and value.has("y") and value.has("z"):
|
|
146
|
+
return Vector3i(value.get("x", 0), value.get("y", 0), value.get("z", 0))
|
|
147
|
+
TYPE_COLOR:
|
|
148
|
+
if value.has("r") and value.has("g") and value.has("b"):
|
|
149
|
+
return Color(value.get("r", 1), value.get("g", 1), value.get("b", 1), value.get("a", 1))
|
|
150
|
+
TYPE_RECT2:
|
|
151
|
+
if value.has("x") and value.has("y") and value.has("width") and value.has("height"):
|
|
152
|
+
return Rect2(value.get("x", 0), value.get("y", 0), value.get("width", 0), value.get("height", 0))
|
|
153
|
+
TYPE_NODE_PATH:
|
|
154
|
+
if value.has("path"):
|
|
155
|
+
return NodePath(value.get("path", ""))
|
|
126
156
|
if typeof(value) == TYPE_ARRAY:
|
|
157
|
+
match expected_type:
|
|
158
|
+
TYPE_VECTOR2:
|
|
159
|
+
if value.size() >= 2:
|
|
160
|
+
return Vector2(value[0], value[1])
|
|
161
|
+
TYPE_VECTOR2I:
|
|
162
|
+
if value.size() >= 2:
|
|
163
|
+
return Vector2i(value[0], value[1])
|
|
164
|
+
TYPE_VECTOR3:
|
|
165
|
+
if value.size() >= 3:
|
|
166
|
+
return Vector3(value[0], value[1], value[2])
|
|
167
|
+
TYPE_VECTOR3I:
|
|
168
|
+
if value.size() >= 3:
|
|
169
|
+
return Vector3i(value[0], value[1], value[2])
|
|
127
170
|
var result: Array = []
|
|
128
171
|
for item in value:
|
|
129
172
|
result.append(_parse_value(item))
|
|
@@ -131,6 +174,13 @@ func _parse_value(value):
|
|
|
131
174
|
return value
|
|
132
175
|
|
|
133
176
|
|
|
177
|
+
func _get_property_type(node: Node, prop_name: String) -> int:
|
|
178
|
+
for prop in node.get_property_list():
|
|
179
|
+
if str(prop.get("name", "")) == prop_name:
|
|
180
|
+
return int(prop.get("type", TYPE_NIL))
|
|
181
|
+
return TYPE_NIL
|
|
182
|
+
|
|
183
|
+
|
|
134
184
|
func _serialize_value(value) -> Variant:
|
|
135
185
|
match typeof(value):
|
|
136
186
|
TYPE_VECTOR2:
|
|
@@ -174,7 +224,8 @@ func _serialize_value(value) -> Variant:
|
|
|
174
224
|
|
|
175
225
|
func _set_node_properties(node: Node, properties: Dictionary) -> void:
|
|
176
226
|
for prop_name in properties:
|
|
177
|
-
var
|
|
227
|
+
var expected_type := _get_property_type(node, str(prop_name))
|
|
228
|
+
var val = _parse_value(properties[prop_name], expected_type)
|
|
178
229
|
node.set(prop_name, val)
|
|
179
230
|
|
|
180
231
|
|
|
@@ -45,6 +45,9 @@ func _process(_delta: float) -> void:
|
|
|
45
45
|
continue
|
|
46
46
|
|
|
47
47
|
client.poll()
|
|
48
|
+
if client.get_status() != StreamPeerTCP.STATUS_CONNECTED:
|
|
49
|
+
clients_to_remove.append(client)
|
|
50
|
+
continue
|
|
48
51
|
var available = client.get_available_bytes()
|
|
49
52
|
if available > 0:
|
|
50
53
|
var data = client.get_utf8_string(available)
|
package/build/godot-bridge.js
CHANGED
|
@@ -335,10 +335,13 @@ export class GodotBridge extends EventEmitter {
|
|
|
335
335
|
nextSocket.on('close', (code, reasonBuffer) => {
|
|
336
336
|
const reason = reasonBuffer.toString();
|
|
337
337
|
this.log('warn', `Godot disconnected (code=${code}, reason=${reason || 'none'})`);
|
|
338
|
-
this.handleDisconnect(new Error('Godot disconnected during request'));
|
|
338
|
+
this.handleDisconnect(nextSocket, new Error('Godot disconnected during request'));
|
|
339
339
|
});
|
|
340
340
|
nextSocket.on('error', (error) => {
|
|
341
341
|
this.log('error', `WebSocket error: ${error.message}`);
|
|
342
|
+
if (nextSocket.readyState === WebSocket.CLOSED || nextSocket.readyState === WebSocket.CLOSING) {
|
|
343
|
+
this.handleDisconnect(nextSocket, error);
|
|
344
|
+
}
|
|
342
345
|
});
|
|
343
346
|
}
|
|
344
347
|
handleRawMessage(data) {
|
|
@@ -463,7 +466,11 @@ export class GodotBridge extends EventEmitter {
|
|
|
463
466
|
clearInterval(this.pingInterval);
|
|
464
467
|
this.pingInterval = null;
|
|
465
468
|
}
|
|
466
|
-
handleDisconnect(reason) {
|
|
469
|
+
handleDisconnect(disconnectedSocket, reason) {
|
|
470
|
+
if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
|
|
471
|
+
this.log('debug', 'Ignoring stale Godot socket disconnect event');
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
467
474
|
this.stopKeepalive();
|
|
468
475
|
this.socket = null;
|
|
469
476
|
this.connectionInfo = null;
|
package/build/index.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
10
|
import { join, dirname, basename, normalize } from 'path';
|
|
11
|
-
import { existsSync, readdirSync, mkdirSync, readFileSync, appendFileSync, writeFileSync } from 'fs';
|
|
11
|
+
import { existsSync, readdirSync, mkdirSync, readFileSync, appendFileSync, writeFileSync, mkdtempSync, rmSync } from 'fs';
|
|
12
|
+
import { tmpdir } from 'os';
|
|
12
13
|
import { spawn } from 'child_process';
|
|
13
14
|
import { createConnection as createTcpConnection } from 'node:net';
|
|
14
15
|
import { promisify } from 'util';
|
|
@@ -43,6 +44,7 @@ class GodotServer {
|
|
|
43
44
|
godotDebugMode = GODOT_DEBUG_MODE_DEFAULT;
|
|
44
45
|
lspClient = null;
|
|
45
46
|
dapClient = null;
|
|
47
|
+
bridgeStartupError = null;
|
|
46
48
|
lastProjectPath = null;
|
|
47
49
|
recordingMode = (process.env.LOG_MODE === 'full' ? 'full' : 'lite');
|
|
48
50
|
logQueue = [];
|
|
@@ -652,6 +654,87 @@ class GodotServer {
|
|
|
652
654
|
}
|
|
653
655
|
return compactTools;
|
|
654
656
|
}
|
|
657
|
+
jsonTextResponse(payload) {
|
|
658
|
+
return {
|
|
659
|
+
content: [{
|
|
660
|
+
type: 'text',
|
|
661
|
+
text: JSON.stringify(payload, null, 2),
|
|
662
|
+
}],
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
buildLegacyToCompactAliasMap() {
|
|
666
|
+
return new Map(Object.entries(this.compactAliasToLegacy).map(([compactName, legacyName]) => [legacyName, compactName]));
|
|
667
|
+
}
|
|
668
|
+
buildToolGroupLookup() {
|
|
669
|
+
const toolToGroup = new Map();
|
|
670
|
+
const registerGroups = (groups, type) => {
|
|
671
|
+
for (const [groupName, group] of Object.entries(groups)) {
|
|
672
|
+
for (const toolName of group.tools) {
|
|
673
|
+
toolToGroup.set(toolName, { group: groupName, type });
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
registerGroups(CORE_TOOL_GROUPS, 'core');
|
|
678
|
+
registerGroups(TOOL_GROUPS, 'dynamic');
|
|
679
|
+
return toolToGroup;
|
|
680
|
+
}
|
|
681
|
+
getActivatedToolNames() {
|
|
682
|
+
const activatedToolNames = new Set();
|
|
683
|
+
for (const groupName of this.activeGroups) {
|
|
684
|
+
const group = TOOL_GROUPS[groupName];
|
|
685
|
+
if (!group) {
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
for (const toolName of group.tools) {
|
|
689
|
+
activatedToolNames.add(toolName);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return activatedToolNames;
|
|
693
|
+
}
|
|
694
|
+
getAvailableDynamicGroups() {
|
|
695
|
+
return Object.keys(TOOL_GROUPS);
|
|
696
|
+
}
|
|
697
|
+
getUnknownDynamicGroupError(groupName) {
|
|
698
|
+
return `Unknown group '${groupName}'. Available dynamic groups: ${this.getAvailableDynamicGroups().join(', ')}`;
|
|
699
|
+
}
|
|
700
|
+
notifyToolListChanged() {
|
|
701
|
+
this.cachedToolDefinitions = [];
|
|
702
|
+
this.server.sendToolListChanged().catch(() => { });
|
|
703
|
+
}
|
|
704
|
+
autoActivateMatchingGroups(query) {
|
|
705
|
+
if (!query || this.toolExposureProfile !== 'compact') {
|
|
706
|
+
return [];
|
|
707
|
+
}
|
|
708
|
+
const newlyActivated = [];
|
|
709
|
+
for (const [groupName, group] of Object.entries(TOOL_GROUPS)) {
|
|
710
|
+
if (this.activeGroups.has(groupName)) {
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
const hasMatchingKeyword = group.keywords.some((kw) => query.includes(kw) || kw.includes(query));
|
|
714
|
+
const hasMatchingToolName = group.tools.some((toolName) => toolName.toLowerCase().includes(query));
|
|
715
|
+
if (hasMatchingKeyword || hasMatchingToolName) {
|
|
716
|
+
this.activeGroups.add(groupName);
|
|
717
|
+
newlyActivated.push(groupName);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (newlyActivated.length > 0) {
|
|
721
|
+
this.notifyToolListChanged();
|
|
722
|
+
}
|
|
723
|
+
return newlyActivated;
|
|
724
|
+
}
|
|
725
|
+
setDynamicGroupActivation(groupName, active) {
|
|
726
|
+
const wasActive = this.activeGroups.has(groupName);
|
|
727
|
+
if (active) {
|
|
728
|
+
this.activeGroups.add(groupName);
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
this.activeGroups.delete(groupName);
|
|
732
|
+
}
|
|
733
|
+
if (wasActive !== active) {
|
|
734
|
+
this.notifyToolListChanged();
|
|
735
|
+
}
|
|
736
|
+
return wasActive;
|
|
737
|
+
}
|
|
655
738
|
sanitizeToolsForList(tools) {
|
|
656
739
|
const seenNames = new Map();
|
|
657
740
|
return tools.map((tool) => {
|
|
@@ -680,15 +763,7 @@ class GodotServer {
|
|
|
680
763
|
const exposed = this.buildCompactTools(allTools);
|
|
681
764
|
// Add dynamically activated group tools (using their legacy names)
|
|
682
765
|
if (this.activeGroups.size > 0) {
|
|
683
|
-
const activatedToolNames =
|
|
684
|
-
for (const groupName of this.activeGroups) {
|
|
685
|
-
const group = TOOL_GROUPS[groupName];
|
|
686
|
-
if (!group)
|
|
687
|
-
continue;
|
|
688
|
-
for (const toolName of group.tools) {
|
|
689
|
-
activatedToolNames.add(toolName);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
766
|
+
const activatedToolNames = this.getActivatedToolNames();
|
|
692
767
|
for (const tool of allTools) {
|
|
693
768
|
if (activatedToolNames.has(tool.name)) {
|
|
694
769
|
exposed.push({
|
|
@@ -750,22 +825,8 @@ class GodotServer {
|
|
|
750
825
|
const rawLimit = typeof normalizedArgs.limit === 'number' ? normalizedArgs.limit : 30;
|
|
751
826
|
const limit = Math.max(1, Math.min(100, rawLimit));
|
|
752
827
|
const tools = this.getAllToolDefinitions();
|
|
753
|
-
const reverseAlias =
|
|
754
|
-
|
|
755
|
-
reverseAlias.set(legacyName, compactName);
|
|
756
|
-
}
|
|
757
|
-
// Build tool → group reverse map (core + dynamic)
|
|
758
|
-
const toolToGroup = new Map();
|
|
759
|
-
for (const [groupName, group] of Object.entries(CORE_TOOL_GROUPS)) {
|
|
760
|
-
for (const toolName of group.tools) {
|
|
761
|
-
toolToGroup.set(toolName, { group: groupName, type: 'core' });
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
for (const [groupName, group] of Object.entries(TOOL_GROUPS)) {
|
|
765
|
-
for (const toolName of group.tools) {
|
|
766
|
-
toolToGroup.set(toolName, { group: groupName, type: 'dynamic' });
|
|
767
|
-
}
|
|
768
|
-
}
|
|
828
|
+
const reverseAlias = this.buildLegacyToCompactAliasMap();
|
|
829
|
+
const toolToGroup = this.buildToolGroupLookup();
|
|
769
830
|
const filtered = tools.filter((tool) => {
|
|
770
831
|
if (!query)
|
|
771
832
|
return true;
|
|
@@ -785,40 +846,16 @@ class GodotServer {
|
|
|
785
846
|
// Auto-activate matching tool groups when query matches their keywords
|
|
786
847
|
// or when the query directly matches a group's tool NAME (not description).
|
|
787
848
|
// This prevents over-activation from incidental description matches.
|
|
788
|
-
const newlyActivated =
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
this.activeGroups.add(groupName);
|
|
799
|
-
newlyActivated.push(groupName);
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
// Notify clients that tool list changed so they re-fetch
|
|
803
|
-
if (newlyActivated.length > 0) {
|
|
804
|
-
this.cachedToolDefinitions = []; // Clear cache to force rebuild
|
|
805
|
-
this.server.sendToolListChanged().catch(() => { });
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
return {
|
|
809
|
-
content: [{
|
|
810
|
-
type: 'text',
|
|
811
|
-
text: JSON.stringify({
|
|
812
|
-
profile: this.toolExposureProfile,
|
|
813
|
-
totalTools: tools.length,
|
|
814
|
-
query: query || null,
|
|
815
|
-
returned: items.length,
|
|
816
|
-
activeGroups: Array.from(this.activeGroups),
|
|
817
|
-
newlyActivated: newlyActivated.length > 0 ? newlyActivated : undefined,
|
|
818
|
-
tools: items,
|
|
819
|
-
}, null, 2),
|
|
820
|
-
}],
|
|
821
|
-
};
|
|
849
|
+
const newlyActivated = this.autoActivateMatchingGroups(query);
|
|
850
|
+
return this.jsonTextResponse({
|
|
851
|
+
profile: this.toolExposureProfile,
|
|
852
|
+
totalTools: tools.length,
|
|
853
|
+
query: query || null,
|
|
854
|
+
returned: items.length,
|
|
855
|
+
activeGroups: Array.from(this.activeGroups),
|
|
856
|
+
newlyActivated: newlyActivated.length > 0 ? newlyActivated : undefined,
|
|
857
|
+
tools: items,
|
|
858
|
+
});
|
|
822
859
|
}
|
|
823
860
|
async handleManageToolGroups(args) {
|
|
824
861
|
const normalizedArgs = this.normalizeParameters(args || {});
|
|
@@ -845,102 +882,55 @@ class GodotServer {
|
|
|
845
882
|
const allGroups = [...coreGroups, ...dynamicGroups];
|
|
846
883
|
const totalCoreTools = coreGroups.reduce((sum, g) => sum + g.toolCount, 0);
|
|
847
884
|
const totalDynTools = dynamicGroups.reduce((sum, g) => sum + g.toolCount, 0);
|
|
848
|
-
return {
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
dynamicTools: totalDynTools,
|
|
857
|
-
groups: allGroups,
|
|
858
|
-
}, null, 2),
|
|
859
|
-
}],
|
|
860
|
-
};
|
|
885
|
+
return this.jsonTextResponse({
|
|
886
|
+
totalGroups: allGroups.length,
|
|
887
|
+
coreGroups: coreGroups.length,
|
|
888
|
+
dynamicGroups: dynamicGroups.length,
|
|
889
|
+
coreTools: totalCoreTools,
|
|
890
|
+
dynamicTools: totalDynTools,
|
|
891
|
+
groups: allGroups,
|
|
892
|
+
});
|
|
861
893
|
}
|
|
862
894
|
case 'activate': {
|
|
863
895
|
if (groupName && CORE_TOOL_GROUPS[groupName]) {
|
|
864
|
-
return {
|
|
865
|
-
content: [{ type: 'text', text: JSON.stringify({ error: `'${groupName}' is a core group and always visible. No activation needed.` }) }],
|
|
866
|
-
};
|
|
896
|
+
return this.jsonTextResponse({ error: `'${groupName}' is a core group and always visible. No activation needed.` });
|
|
867
897
|
}
|
|
868
898
|
if (!groupName || !TOOL_GROUPS[groupName]) {
|
|
869
|
-
|
|
870
|
-
return {
|
|
871
|
-
content: [{
|
|
872
|
-
type: 'text',
|
|
873
|
-
text: JSON.stringify({ error: `Unknown group '${groupName}'. Available dynamic groups: ${available}` }),
|
|
874
|
-
}],
|
|
875
|
-
};
|
|
876
|
-
}
|
|
877
|
-
const wasNew = !this.activeGroups.has(groupName);
|
|
878
|
-
this.activeGroups.add(groupName);
|
|
879
|
-
if (wasNew) {
|
|
880
|
-
this.cachedToolDefinitions = [];
|
|
881
|
-
this.server.sendToolListChanged().catch(() => { });
|
|
899
|
+
return this.jsonTextResponse({ error: this.getUnknownDynamicGroupError(groupName) });
|
|
882
900
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
activeGroups: Array.from(this.activeGroups),
|
|
891
|
-
}, null, 2),
|
|
892
|
-
}],
|
|
893
|
-
};
|
|
901
|
+
const wasAlreadyActive = this.setDynamicGroupActivation(groupName, true);
|
|
902
|
+
return this.jsonTextResponse({
|
|
903
|
+
activated: groupName,
|
|
904
|
+
tools: TOOL_GROUPS[groupName].tools,
|
|
905
|
+
wasAlreadyActive,
|
|
906
|
+
activeGroups: Array.from(this.activeGroups),
|
|
907
|
+
});
|
|
894
908
|
}
|
|
895
909
|
case 'deactivate': {
|
|
896
910
|
if (groupName && CORE_TOOL_GROUPS[groupName]) {
|
|
897
|
-
return {
|
|
898
|
-
content: [{ type: 'text', text: JSON.stringify({ error: `'${groupName}' is a core group and cannot be deactivated.` }) }],
|
|
899
|
-
};
|
|
911
|
+
return this.jsonTextResponse({ error: `'${groupName}' is a core group and cannot be deactivated.` });
|
|
900
912
|
}
|
|
901
913
|
if (!groupName || !TOOL_GROUPS[groupName]) {
|
|
902
|
-
|
|
903
|
-
return {
|
|
904
|
-
content: [{
|
|
905
|
-
type: 'text',
|
|
906
|
-
text: JSON.stringify({ error: `Unknown group '${groupName}'. Available dynamic groups: ${available}` }),
|
|
907
|
-
}],
|
|
908
|
-
};
|
|
914
|
+
return this.jsonTextResponse({ error: this.getUnknownDynamicGroupError(groupName) });
|
|
909
915
|
}
|
|
910
|
-
const wasActive = this.
|
|
911
|
-
this.
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
}
|
|
916
|
-
return {
|
|
917
|
-
content: [{
|
|
918
|
-
type: 'text',
|
|
919
|
-
text: JSON.stringify({
|
|
920
|
-
deactivated: groupName,
|
|
921
|
-
wasActive,
|
|
922
|
-
activeGroups: Array.from(this.activeGroups),
|
|
923
|
-
}, null, 2),
|
|
924
|
-
}],
|
|
925
|
-
};
|
|
916
|
+
const wasActive = this.setDynamicGroupActivation(groupName, false);
|
|
917
|
+
return this.jsonTextResponse({
|
|
918
|
+
deactivated: groupName,
|
|
919
|
+
wasActive,
|
|
920
|
+
activeGroups: Array.from(this.activeGroups),
|
|
921
|
+
});
|
|
926
922
|
}
|
|
927
923
|
case 'reset': {
|
|
928
924
|
const previouslyActive = Array.from(this.activeGroups);
|
|
929
925
|
this.activeGroups.clear();
|
|
930
926
|
if (previouslyActive.length > 0) {
|
|
931
|
-
this.
|
|
932
|
-
this.server.sendToolListChanged().catch(() => { });
|
|
927
|
+
this.notifyToolListChanged();
|
|
933
928
|
}
|
|
934
|
-
return {
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
deactivated: previouslyActive,
|
|
940
|
-
activeGroups: [],
|
|
941
|
-
}, null, 2),
|
|
942
|
-
}],
|
|
943
|
-
};
|
|
929
|
+
return this.jsonTextResponse({
|
|
930
|
+
reset: true,
|
|
931
|
+
deactivated: previouslyActive,
|
|
932
|
+
activeGroups: [],
|
|
933
|
+
});
|
|
944
934
|
}
|
|
945
935
|
case 'status':
|
|
946
936
|
default: {
|
|
@@ -959,16 +949,11 @@ class GodotServer {
|
|
|
959
949
|
}));
|
|
960
950
|
const totalCoreTools = coreGroupDetails.reduce((sum, g) => sum + g.tools.length, 0);
|
|
961
951
|
const totalDynamicTools = activeGroupDetails.reduce((sum, g) => sum + (g.tools?.length || 0), 0);
|
|
962
|
-
return {
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
dynamicGroups: { activeCount: this.activeGroups.size, tools: totalDynamicTools, groups: activeGroupDetails },
|
|
968
|
-
availableDynamicGroups: Object.keys(TOOL_GROUPS),
|
|
969
|
-
}, null, 2),
|
|
970
|
-
}],
|
|
971
|
-
};
|
|
952
|
+
return this.jsonTextResponse({
|
|
953
|
+
coreGroups: { count: coreGroupDetails.length, tools: totalCoreTools, groups: coreGroupDetails },
|
|
954
|
+
dynamicGroups: { activeCount: this.activeGroups.size, tools: totalDynamicTools, groups: activeGroupDetails },
|
|
955
|
+
availableDynamicGroups: this.getAvailableDynamicGroups(),
|
|
956
|
+
});
|
|
972
957
|
}
|
|
973
958
|
}
|
|
974
959
|
}
|
|
@@ -999,8 +984,11 @@ class GodotServer {
|
|
|
999
984
|
for (const key in params) {
|
|
1000
985
|
if (Object.prototype.hasOwnProperty.call(params, key)) {
|
|
1001
986
|
let normalizedKey = key;
|
|
1002
|
-
//
|
|
1003
|
-
if (key.
|
|
987
|
+
// Preserve sentinel keys like _type, but normalize regular snake_case keys.
|
|
988
|
+
if (key.startsWith('_')) {
|
|
989
|
+
normalizedKey = key;
|
|
990
|
+
}
|
|
991
|
+
else if (key.includes('_')) {
|
|
1004
992
|
normalizedKey = this.parameterMappings[key] || key.replace(/_([a-zA-Z0-9])/g, (_, letter) => letter.toUpperCase());
|
|
1005
993
|
}
|
|
1006
994
|
// Handle nested objects recursively
|
|
@@ -1023,8 +1011,10 @@ class GodotServer {
|
|
|
1023
1011
|
const result = {};
|
|
1024
1012
|
for (const key in params) {
|
|
1025
1013
|
if (Object.prototype.hasOwnProperty.call(params, key)) {
|
|
1026
|
-
// Convert camelCase to snake_case
|
|
1027
|
-
const snakeKey =
|
|
1014
|
+
// Convert camelCase to snake_case while preserving sentinel keys like _type.
|
|
1015
|
+
const snakeKey = key.startsWith('_')
|
|
1016
|
+
? key
|
|
1017
|
+
: (this.reverseParameterMappings[key] || key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`));
|
|
1028
1018
|
// Handle nested objects recursively
|
|
1029
1019
|
if (typeof params[key] === 'object' && params[key] !== null && !Array.isArray(params[key])) {
|
|
1030
1020
|
result[snakeKey] = this.convertCamelToSnakeCase(params[key]);
|
|
@@ -1057,16 +1047,18 @@ class GodotServer {
|
|
|
1057
1047
|
}
|
|
1058
1048
|
}
|
|
1059
1049
|
try {
|
|
1060
|
-
// Serialize
|
|
1050
|
+
// Serialize parameters into a temp file to avoid shell/cmd JSON escaping issues
|
|
1051
|
+
// (notably Windows command-line parsing of sequences such as \t, \r, and \").
|
|
1061
1052
|
const paramsJson = JSON.stringify(snakeCaseParams);
|
|
1062
|
-
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
//
|
|
1066
|
-
|
|
1053
|
+
const paramsDir = mkdtempSync(join(tmpdir(), 'gopeak-params-'));
|
|
1054
|
+
const paramsFilePath = join(paramsDir, `${operation}.json`);
|
|
1055
|
+
writeFileSync(paramsFilePath, paramsJson, 'utf8');
|
|
1056
|
+
// Escape the params file reference for the current shell.
|
|
1057
|
+
const paramsFileArg = `@file:${paramsFilePath}`;
|
|
1058
|
+
const escapedParams = paramsFileArg.replace(/'/g, "'\\''");
|
|
1067
1059
|
const isWindows = process.platform === 'win32';
|
|
1068
1060
|
const quotedParams = isWindows
|
|
1069
|
-
? `\"${
|
|
1061
|
+
? `\"${paramsFileArg.replace(/\"/g, '\\"')}\"`
|
|
1070
1062
|
: `'${escapedParams}'`;
|
|
1071
1063
|
// Add debug arguments if debug mode is enabled
|
|
1072
1064
|
const debugArgs = this.godotDebugMode ? ['--debug-godot'] : [];
|
|
@@ -1083,8 +1075,13 @@ class GodotServer {
|
|
|
1083
1075
|
...debugArgs,
|
|
1084
1076
|
].join(' ');
|
|
1085
1077
|
this.logDebug(`Command: ${cmd}`);
|
|
1086
|
-
|
|
1087
|
-
|
|
1078
|
+
try {
|
|
1079
|
+
const { stdout, stderr } = await execAsync(cmd);
|
|
1080
|
+
return { stdout, stderr: this.sanitizeGodotStderr(stderr) };
|
|
1081
|
+
}
|
|
1082
|
+
finally {
|
|
1083
|
+
rmSync(paramsDir, { recursive: true, force: true });
|
|
1084
|
+
}
|
|
1088
1085
|
}
|
|
1089
1086
|
catch (error) {
|
|
1090
1087
|
// If execAsync throws, it still contains stdout/stderr
|
|
@@ -1092,12 +1089,27 @@ class GodotServer {
|
|
|
1092
1089
|
const execError = error;
|
|
1093
1090
|
return {
|
|
1094
1091
|
stdout: execError.stdout,
|
|
1095
|
-
stderr: execError.stderr,
|
|
1092
|
+
stderr: this.sanitizeGodotStderr(execError.stderr),
|
|
1096
1093
|
};
|
|
1097
1094
|
}
|
|
1098
1095
|
throw error;
|
|
1099
1096
|
}
|
|
1100
1097
|
}
|
|
1098
|
+
getEditorStatusPayload() {
|
|
1099
|
+
const status = this.godotBridge.getStatus();
|
|
1100
|
+
const isPortConflict = this.bridgeStartupError?.includes('EADDRINUSE') ?? false;
|
|
1101
|
+
return {
|
|
1102
|
+
...status,
|
|
1103
|
+
bridgeAvailable: this.bridgeStartupError === null,
|
|
1104
|
+
startupError: this.bridgeStartupError,
|
|
1105
|
+
note: isPortConflict
|
|
1106
|
+
? 'Bridge port is already in use. Another gopeak instance may own the editor bridge, so this server cannot report that editor connection.'
|
|
1107
|
+
: undefined,
|
|
1108
|
+
suggestion: isPortConflict
|
|
1109
|
+
? 'Stop duplicate gopeak/MCP server instances or re-run the command from the same server process that owns the bridge port.'
|
|
1110
|
+
: undefined,
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1101
1113
|
/**
|
|
1102
1114
|
* Get the structure of a Godot project
|
|
1103
1115
|
* @param projectPath Path to the Godot project
|
|
@@ -1449,7 +1461,7 @@ class GodotServer {
|
|
|
1449
1461
|
return await this.handleViaBridge('modify_resource', normalizedArgs);
|
|
1450
1462
|
// Editor Plugin Bridge Status
|
|
1451
1463
|
case 'get_editor_status':
|
|
1452
|
-
return { content: [{ type: 'text', text: JSON.stringify(this.
|
|
1464
|
+
return { content: [{ type: 'text', text: JSON.stringify(this.getEditorStatusPayload(), null, 2) }] };
|
|
1453
1465
|
// Project Visualizer Tool
|
|
1454
1466
|
case 'map_project':
|
|
1455
1467
|
return await this.handleMapProject(request.params.arguments);
|
|
@@ -2275,6 +2287,27 @@ class GodotServer {
|
|
|
2275
2287
|
}
|
|
2276
2288
|
return null;
|
|
2277
2289
|
}
|
|
2290
|
+
sanitizeGodotStderr(stderr) {
|
|
2291
|
+
if (!stderr) {
|
|
2292
|
+
return stderr;
|
|
2293
|
+
}
|
|
2294
|
+
const ignoredPatterns = [
|
|
2295
|
+
/WARNING: ObjectDB instances leaked at exit/i,
|
|
2296
|
+
/at:\s+cleanup\s+\(core\/object\/object\.cpp:/i,
|
|
2297
|
+
/ERROR:\s+\d+\s+resources still in use at exit/i,
|
|
2298
|
+
/at:\s+clear\s+\(core\/io\/resource\.cpp:/i,
|
|
2299
|
+
];
|
|
2300
|
+
const filteredLines = stderr
|
|
2301
|
+
.split(/\r?\n/)
|
|
2302
|
+
.filter((line) => {
|
|
2303
|
+
const trimmed = line.trim();
|
|
2304
|
+
if (!trimmed) {
|
|
2305
|
+
return false;
|
|
2306
|
+
}
|
|
2307
|
+
return !ignoredPatterns.some((pattern) => pattern.test(trimmed));
|
|
2308
|
+
});
|
|
2309
|
+
return filteredLines.join('\n').trim();
|
|
2310
|
+
}
|
|
2278
2311
|
/**
|
|
2279
2312
|
* Capture/update current intent snapshot
|
|
2280
2313
|
*/
|
|
@@ -4705,11 +4738,13 @@ class GodotServer {
|
|
|
4705
4738
|
// Bridge startup issues should not take down the stdio MCP server.
|
|
4706
4739
|
try {
|
|
4707
4740
|
await this.godotBridge.start();
|
|
4741
|
+
this.bridgeStartupError = null;
|
|
4708
4742
|
const bridgeStatus = this.godotBridge.getStatus();
|
|
4709
4743
|
console.error(`[SERVER] Godot Editor Bridge started on ${bridgeStatus.host}:${bridgeStatus.port}`);
|
|
4710
4744
|
}
|
|
4711
4745
|
catch (bridgeError) {
|
|
4712
4746
|
const bridgeMessage = bridgeError instanceof Error ? bridgeError.message : String(bridgeError);
|
|
4747
|
+
this.bridgeStartupError = bridgeMessage;
|
|
4713
4748
|
console.error(`[SERVER] Warning: Godot Editor Bridge failed to start: ${bridgeMessage}`);
|
|
4714
4749
|
console.error('[SERVER] Continuing without bridge-backed editor tools.');
|
|
4715
4750
|
}
|
|
@@ -34,7 +34,15 @@ func _init():
|
|
|
34
34
|
|
|
35
35
|
var operation = args[operation_index]
|
|
36
36
|
var params_json = args[params_index]
|
|
37
|
-
|
|
37
|
+
if params_json.begins_with("@file:"):
|
|
38
|
+
var params_file_path = params_json.substr(6)
|
|
39
|
+
var params_file = FileAccess.open(params_file_path, FileAccess.READ)
|
|
40
|
+
if params_file == null:
|
|
41
|
+
log_error("Failed to open params file: " + params_file_path)
|
|
42
|
+
quit(1)
|
|
43
|
+
params_json = params_file.get_as_text()
|
|
44
|
+
params_file.close()
|
|
45
|
+
|
|
38
46
|
log_info("Operation: " + operation)
|
|
39
47
|
log_debug("Params JSON: " + params_json)
|
|
40
48
|
|
|
@@ -365,7 +365,7 @@ export function buildToolDefinitions(godotBridgePort) {
|
|
|
365
365
|
},
|
|
366
366
|
properties: {
|
|
367
367
|
type: 'string',
|
|
368
|
-
description: 'Optional properties to set on the node (as JSON string)',
|
|
368
|
+
description: 'Optional properties to set on the node (as JSON string). Tagged Godot values such as {"position":{"type":"Vector2","x":100,"y":200}} are the most explicit form; common typed properties like Vector2 also accept inferred shapes such as {"position":{"x":100,"y":200}} or {"position":[100,200]}.',
|
|
369
369
|
},
|
|
370
370
|
},
|
|
371
371
|
required: ['projectPath', 'scenePath', 'nodeType', 'nodeName'],
|
|
@@ -526,7 +526,7 @@ export function buildToolDefinitions(godotBridgePort) {
|
|
|
526
526
|
},
|
|
527
527
|
properties: {
|
|
528
528
|
type: 'string',
|
|
529
|
-
description: 'JSON object of properties to set (e.g., {"position":
|
|
529
|
+
description: 'JSON object of properties to set. Tagged Godot values are the most explicit form (e.g., {"position":{"type":"Vector2","x":100,"y":200},"scale":{"type":"Vector2","x":2,"y":2}}), but typed properties like Vector2 also accept inferred {"x","y"} objects and numeric arrays.',
|
|
530
530
|
},
|
|
531
531
|
saveScene: {
|
|
532
532
|
type: 'boolean',
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gopeak",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.7",
|
|
4
4
|
"mcpName": "io.github.HaD0Yun/gopeak",
|
|
5
|
-
"description": "GoPeak — MCP server for Godot
|
|
5
|
+
"description": "GoPeak — MCP server for trusted Godot 4 workflows with compact/dynamic tool profiles, setup-gated LSP/DAP/runtime integrations, and migration-safe legacy compatibility.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./build/index.js",
|
|
8
8
|
"bin": {
|
|
9
|
-
"gopeak": "
|
|
10
|
-
"godot-mcp": "
|
|
9
|
+
"gopeak": "build/cli.js",
|
|
10
|
+
"godot-mcp": "build/cli.js"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"build",
|
|
14
|
+
"scripts/postinstall.mjs",
|
|
14
15
|
"README.md",
|
|
15
16
|
"LICENSE"
|
|
16
17
|
],
|
|
@@ -22,7 +23,9 @@
|
|
|
22
23
|
"test:smoke": "node scripts/smoke-test.mjs",
|
|
23
24
|
"test:integration": "node test-bridge.mjs",
|
|
24
25
|
"test:dynamic-groups": "node test-dynamic-groups.mjs",
|
|
25
|
-
"test:
|
|
26
|
+
"test:docs": "node test-readme-localization-consistency.mjs && node test-docs-package-policy.mjs",
|
|
27
|
+
"test:regressions": "node test-regressions.mjs",
|
|
28
|
+
"test:ci": "npm run test:smoke && npm run test:regressions",
|
|
26
29
|
"ci": "npm run build && npm run typecheck && npm run test:ci",
|
|
27
30
|
"prepare": "npm run build",
|
|
28
31
|
"postinstall": "node scripts/postinstall.mjs",
|
|
@@ -30,8 +33,10 @@
|
|
|
30
33
|
"inspector": "npx @modelcontextprotocol/inspector build/index.js",
|
|
31
34
|
"pack": "npm pack --dry-run",
|
|
32
35
|
"version:bump": "node scripts/bump-version.mjs",
|
|
33
|
-
"test:setup": "npm run build && node test-setup-hooks.mjs && node test-metadata-consistency.mjs",
|
|
34
|
-
"test:metadata": "npm run build && node test-metadata-consistency.mjs"
|
|
36
|
+
"test:setup": "npm run build && node test-setup-hooks.mjs && node test-metadata-consistency.mjs && node test-packaging-consistency.mjs",
|
|
37
|
+
"test:metadata": "npm run build && node test-metadata-consistency.mjs",
|
|
38
|
+
"test:packaging": "npm run build && node test-packaging-consistency.mjs",
|
|
39
|
+
"test:docs-policy": "node test-docs-package-policy.mjs"
|
|
35
40
|
},
|
|
36
41
|
"engines": {
|
|
37
42
|
"node": ">=18"
|
|
@@ -50,7 +55,8 @@
|
|
|
50
55
|
},
|
|
51
56
|
"overrides": {
|
|
52
57
|
"@hono/node-server": "^1.19.11",
|
|
53
|
-
"hono": "4.12.
|
|
58
|
+
"hono": "^4.12.18",
|
|
59
|
+
"path-to-regexp": "8.4.1"
|
|
54
60
|
},
|
|
55
61
|
"license": "MIT",
|
|
56
62
|
"repository": {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
const OPT_IN_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
|
8
|
+
const shouldInstallHooks = OPT_IN_VALUES.has(String(process.env.GOPEAK_SETUP_HOOKS || '').trim().toLowerCase());
|
|
9
|
+
|
|
10
|
+
if (!shouldInstallHooks) {
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
const cliPath = join(__dirname, '..', 'build', 'cli.js');
|
|
17
|
+
|
|
18
|
+
if (!existsSync(cliPath)) {
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
execFileSync(process.execPath, [cliPath, 'setup', '--silent'], {
|
|
24
|
+
stdio: 'ignore',
|
|
25
|
+
env: process.env,
|
|
26
|
+
});
|
|
27
|
+
} catch {
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|