gopeak 2.3.4 → 2.3.6
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 +32 -1
- 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 +182 -2619
- package/build/scripts/godot_operations.gd +9 -1
- package/build/server-types.js +1 -0
- package/build/server-version.js +12 -0
- package/build/tool-definitions.js +2264 -0
- package/build/tool-groups.js +174 -0
- package/package.json +11 -6
- package/scripts/postinstall.mjs +29 -0
package/README.md
CHANGED
|
@@ -39,6 +39,14 @@ npm install -g gopeak
|
|
|
39
39
|
gopeak
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
Optional shell hooks for update notifications are now **opt-in**:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
gopeak setup
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
> `gopeak setup` only modifies supported bash/zsh rc files when you run it explicitly. `npm install` no longer installs shell hooks automatically.
|
|
49
|
+
|
|
42
50
|
### 2) Add MCP client config
|
|
43
51
|
|
|
44
52
|
```json
|
|
@@ -136,6 +144,20 @@ In `compact` mode, 78 additional tools are organized into **22 groups** that act
|
|
|
136
144
|
> "Use `tool.groups` to reset all active groups."
|
|
137
145
|
|
|
138
146
|
The server sends `notifications/tools/list_changed` so MCP clients (Claude Code, Claude Desktop) automatically refresh the tool list.
|
|
147
|
+
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.
|
|
148
|
+
|
|
149
|
+
### Typed property values for scene tools
|
|
150
|
+
|
|
151
|
+
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:
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"position": { "type": "Vector2", "x": 100, "y": 200 },
|
|
156
|
+
"scale": { "type": "Vector2", "x": 2, "y": 2 }
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The internal headless serializer uses `_type`, but MCP callers should prefer `type` when they need an explicit cross-tool Godot value tag.
|
|
139
161
|
|
|
140
162
|
### Don't worry about tokens
|
|
141
163
|
|
|
@@ -168,6 +190,12 @@ npm install -g gopeak
|
|
|
168
190
|
gopeak
|
|
169
191
|
```
|
|
170
192
|
|
|
193
|
+
Optional shell hooks for update notifications remain available via:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
gopeak setup
|
|
197
|
+
```
|
|
198
|
+
|
|
171
199
|
### C) From source
|
|
172
200
|
|
|
173
201
|
```bash
|
|
@@ -201,10 +229,12 @@ GitHub Actions runs on push/PR and executes:
|
|
|
201
229
|
2. `npx tsc --noEmit`
|
|
202
230
|
3. `npm run smoke`
|
|
203
231
|
|
|
204
|
-
Run the same checks locally:
|
|
232
|
+
Run the same checks locally before opening a PR:
|
|
205
233
|
|
|
206
234
|
```bash
|
|
207
235
|
npm run ci
|
|
236
|
+
npm run test:dynamic-groups
|
|
237
|
+
npm run test:integration
|
|
208
238
|
```
|
|
209
239
|
|
|
210
240
|
---
|
|
@@ -337,6 +367,7 @@ Visualize your entire project architecture with `visualizer.map` (`map_project`
|
|
|
337
367
|
- **Project path invalid** → confirm `project.godot` exists
|
|
338
368
|
- **Runtime tools not working** → install/enable runtime addon plugin
|
|
339
369
|
- **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
|
|
370
|
+
- **`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
|
|
340
371
|
|
|
341
372
|
---
|
|
342
373
|
|
|
@@ -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;
|