gopeak 2.3.5 → 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 +15 -0
- 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 +176 -162
- package/build/scripts/godot_operations.gd +9 -1
- package/build/tool-definitions.js +2 -2
- package/package.json +9 -5
- package/scripts/postinstall.mjs +29 -0
package/README.md
CHANGED
|
@@ -144,6 +144,20 @@ In `compact` mode, 78 additional tools are organized into **22 groups** that act
|
|
|
144
144
|
> "Use `tool.groups` to reset all active groups."
|
|
145
145
|
|
|
146
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.
|
|
147
161
|
|
|
148
162
|
### Don't worry about tokens
|
|
149
163
|
|
|
@@ -353,6 +367,7 @@ Visualize your entire project architecture with `visualizer.map` (`map_project`
|
|
|
353
367
|
- **Project path invalid** → confirm `project.godot` exists
|
|
354
368
|
- **Runtime tools not working** → install/enable runtime addon plugin
|
|
355
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
|
|
356
371
|
|
|
357
372
|
---
|
|
358
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;
|
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
|
-
};
|
|
909
|
-
}
|
|
910
|
-
const wasActive = this.activeGroups.has(groupName);
|
|
911
|
-
this.activeGroups.delete(groupName);
|
|
912
|
-
if (wasActive) {
|
|
913
|
-
this.cachedToolDefinitions = [];
|
|
914
|
-
this.server.sendToolListChanged().catch(() => { });
|
|
914
|
+
return this.jsonTextResponse({ error: this.getUnknownDynamicGroupError(groupName) });
|
|
915
915
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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 };
|
|
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
|
|
@@ -1098,6 +1095,21 @@ class GodotServer {
|
|
|
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);
|
|
@@ -4705,11 +4717,13 @@ class GodotServer {
|
|
|
4705
4717
|
// Bridge startup issues should not take down the stdio MCP server.
|
|
4706
4718
|
try {
|
|
4707
4719
|
await this.godotBridge.start();
|
|
4720
|
+
this.bridgeStartupError = null;
|
|
4708
4721
|
const bridgeStatus = this.godotBridge.getStatus();
|
|
4709
4722
|
console.error(`[SERVER] Godot Editor Bridge started on ${bridgeStatus.host}:${bridgeStatus.port}`);
|
|
4710
4723
|
}
|
|
4711
4724
|
catch (bridgeError) {
|
|
4712
4725
|
const bridgeMessage = bridgeError instanceof Error ? bridgeError.message : String(bridgeError);
|
|
4726
|
+
this.bridgeStartupError = bridgeMessage;
|
|
4713
4727
|
console.error(`[SERVER] Warning: Godot Editor Bridge failed to start: ${bridgeMessage}`);
|
|
4714
4728
|
console.error('[SERVER] Continuing without bridge-backed editor tools.');
|
|
4715
4729
|
}
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gopeak",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.6",
|
|
4
4
|
"mcpName": "io.github.HaD0Yun/gopeak",
|
|
5
5
|
"description": "GoPeak — MCP server for Godot Engine with 110+ tools, compact/dynamic profiles, GDScript LSP, DAP debugger, screenshots, input injection, and CC0 asset search.",
|
|
6
6
|
"type": "module",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"build",
|
|
14
|
+
"scripts/postinstall.mjs",
|
|
14
15
|
"README.md",
|
|
15
16
|
"LICENSE"
|
|
16
17
|
],
|
|
@@ -22,7 +23,8 @@
|
|
|
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:regressions": "node test-regressions.mjs",
|
|
27
|
+
"test:ci": "npm run test:smoke && npm run test:regressions",
|
|
26
28
|
"ci": "npm run build && npm run typecheck && npm run test:ci",
|
|
27
29
|
"prepare": "npm run build",
|
|
28
30
|
"postinstall": "node scripts/postinstall.mjs",
|
|
@@ -30,8 +32,9 @@
|
|
|
30
32
|
"inspector": "npx @modelcontextprotocol/inspector build/index.js",
|
|
31
33
|
"pack": "npm pack --dry-run",
|
|
32
34
|
"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"
|
|
35
|
+
"test:setup": "npm run build && node test-setup-hooks.mjs && node test-metadata-consistency.mjs && node test-packaging-consistency.mjs",
|
|
36
|
+
"test:metadata": "npm run build && node test-metadata-consistency.mjs",
|
|
37
|
+
"test:packaging": "npm run build && node test-packaging-consistency.mjs"
|
|
35
38
|
},
|
|
36
39
|
"engines": {
|
|
37
40
|
"node": ">=18"
|
|
@@ -50,7 +53,8 @@
|
|
|
50
53
|
},
|
|
51
54
|
"overrides": {
|
|
52
55
|
"@hono/node-server": "^1.19.11",
|
|
53
|
-
"hono": "4.12.7"
|
|
56
|
+
"hono": "4.12.7",
|
|
57
|
+
"path-to-regexp": "8.4.1"
|
|
54
58
|
},
|
|
55
59
|
"license": "MIT",
|
|
56
60
|
"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
|
+
}
|