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 CHANGED
@@ -10,10 +10,14 @@
10
10
  [![](https://img.shields.io/github/forks/HaD0Yun/Gopeak-godot-mcp 'Forks')](https://github.com/HaD0Yun/Gopeak-godot-mcp/network/members)
11
11
  [![](https://img.shields.io/badge/License-MIT-red.svg 'MIT License')](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
  ![GoPeak Hero](assets/gopeak-hero-v2.png)
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 33 core tools with 22 dynamic tool groups (78 additional tools) that activate on demand — keeping token usage low while preserving full capability.
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
- - **110+ tools available** across scene/script/resource/runtime/LSP/DAP/input/assets.
81
- - **Token-efficient by default**: compact tool surface (33 tools) + dynamic tool groups. Only activate what you need no more 110-tool context bombs.
82
- - **Dynamic tool groups**: search with `tool.catalog` and matching groups auto-activate. Or manually activate with `tool.groups`.
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): 33 core tools + **22 dynamic tool groups** (78 additional tools activated on demand)
98
- - `full`: exposes full legacy tool list (110+)
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, 78 additional tools are organized into **22 groups** that activate automatically when needed:
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 | Tools | Description |
114
+ | Group | Maturity | Description / setup gate |
111
115
  |---|---|---|
112
- | `scene_advanced` | 3 | Duplicate, reparent nodes, load sprites |
113
- | `uid` | 2 | UID management for resources |
114
- | `import_export` | 5 | Import pipeline, reimport, validate project |
115
- | `autoload` | 4 | Autoload singletons, main scene |
116
- | `signal` | 2 | Disconnect signals, list connections |
117
- | `runtime` | 4 | Live scene inspection, runtime properties, metrics |
118
- | `resource` | 4 | Create/modify materials, shaders, resources |
119
- | `animation` | 5 | Animations, tracks, animation tree, state machine |
120
- | `plugin` | 3 | Enable/disable/list editor plugins |
121
- | `input` | 1 | Input action mapping |
122
- | `tilemap` | 2 | TileSet and TileMap cell painting |
123
- | `audio` | 4 | Audio buses, effects, volume |
124
- | `navigation` | 2 | Navigation regions and agents |
125
- | `theme_ui` | 3 | Theme colors, font sizes, shaders |
126
- | `asset_store` | 3 | Search/download CC0 assets |
127
- | `testing` | 6 | Screenshots, viewport capture, input injection |
128
- | `dx_tools` | 4 | Error log, project health, find usages, scaffold |
129
- | `intent_tracking` | 9 | Intent capture, decision logs, handoff briefs |
130
- | `class_advanced` | 1 | Class inheritance inspection |
131
- | `lsp` | 3 | GDScript completions, hover, symbols |
132
- | `dap` | 6 | Breakpoints, stepping, stack traces |
133
- | `version_gate` | 2 | Version validation, patch verification |
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 all 110+ definitions at once. Your AI client fetches the next page only when it needs more.
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 (Poly Haven, AmbientCG, Kenney)
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 and value.has("type"):
86
- match value["type"]:
87
- "Vector2":
88
- return Vector2(value.get("x", 0), value.get("y", 0))
89
- "Vector3":
90
- return Vector3(value.get("x", 0), value.get("y", 0), value.get("z", 0))
91
- "Color":
92
- return Color(value.get("r", 1), value.get("g", 1), value.get("b", 1), value.get("a", 1))
93
- "Vector2i":
94
- return Vector2i(value.get("x", 0), value.get("y", 0))
95
- "Vector3i":
96
- return Vector3i(value.get("x", 0), value.get("y", 0), value.get("z", 0))
97
- "Rect2":
98
- return Rect2(value.get("x", 0), value.get("y", 0), value.get("width", 0), value.get("height", 0))
99
- "Transform2D":
100
- if value.has("x") and value.has("y") and value.has("origin"):
101
- var xx: Dictionary = value["x"]
102
- var yy: Dictionary = value["y"]
103
- var oo: Dictionary = value["origin"]
104
- return Transform2D(
105
- Vector2(xx.get("x", 1), xx.get("y", 0)),
106
- Vector2(yy.get("x", 0), yy.get("y", 1)),
107
- Vector2(oo.get("x", 0), oo.get("y", 0))
108
- )
109
- "Transform3D":
110
- if value.has("basis") and value.has("origin"):
111
- var b: Dictionary = value["basis"]
112
- var o: Dictionary = value["origin"]
113
- var basis := Basis(
114
- Vector3(b.get("x", {}).get("x", 1), b.get("x", {}).get("y", 0), b.get("x", {}).get("z", 0)),
115
- Vector3(b.get("y", {}).get("x", 0), b.get("y", {}).get("y", 1), b.get("y", {}).get("z", 0)),
116
- Vector3(b.get("z", {}).get("x", 0), b.get("z", {}).get("y", 0), b.get("z", {}).get("z", 1))
117
- )
118
- return Transform3D(basis, Vector3(o.get("x", 0), o.get("y", 0), o.get("z", 0)))
119
- "NodePath":
120
- return NodePath(value.get("path", ""))
121
- "Resource":
122
- var resource_path: String = str(value.get("path", ""))
123
- if resource_path.is_empty():
124
- return null
125
- return load(resource_path)
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 val = _parse_value(properties[prop_name])
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)
@@ -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 = new Set();
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 = new Map();
754
- for (const [compactName, legacyName] of Object.entries(this.compactAliasToLegacy)) {
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
- if (query && this.toolExposureProfile === 'compact') {
790
- for (const [groupName, group] of Object.entries(TOOL_GROUPS)) {
791
- if (this.activeGroups.has(groupName))
792
- continue;
793
- // Activate if query matches a group keyword
794
- const hasMatchingKeyword = group.keywords.some((kw) => query.includes(kw) || kw.includes(query));
795
- // Or if query matches a tool NAME in the group (strict name-only match)
796
- const hasMatchingToolName = group.tools.some((t) => t.toLowerCase().includes(query));
797
- if (hasMatchingKeyword || hasMatchingToolName) {
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
- content: [{
850
- type: 'text',
851
- text: JSON.stringify({
852
- totalGroups: allGroups.length,
853
- coreGroups: coreGroups.length,
854
- dynamicGroups: dynamicGroups.length,
855
- coreTools: totalCoreTools,
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
- const available = Object.keys(TOOL_GROUPS).join(', ');
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
- return {
884
- content: [{
885
- type: 'text',
886
- text: JSON.stringify({
887
- activated: groupName,
888
- tools: TOOL_GROUPS[groupName].tools,
889
- wasAlreadyActive: !wasNew,
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
- const available = Object.keys(TOOL_GROUPS).join(', ');
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.activeGroups.has(groupName);
911
- this.activeGroups.delete(groupName);
912
- if (wasActive) {
913
- this.cachedToolDefinitions = [];
914
- this.server.sendToolListChanged().catch(() => { });
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.cachedToolDefinitions = [];
932
- this.server.sendToolListChanged().catch(() => { });
927
+ this.notifyToolListChanged();
933
928
  }
934
- return {
935
- content: [{
936
- type: 'text',
937
- text: JSON.stringify({
938
- reset: true,
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
- content: [{
964
- type: 'text',
965
- text: JSON.stringify({
966
- coreGroups: { count: coreGroupDetails.length, tools: totalCoreTools, groups: coreGroupDetails },
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
- // If the key is in snake_case, convert it to camelCase using our mapping
1003
- if (key.includes('_')) {
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 = this.reverseParameterMappings[key] || key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
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 the snake_case parameters to a valid JSON string
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
- // Escape single quotes in the JSON string to prevent command injection
1063
- const escapedParams = paramsJson.replace(/'/g, "'\\''");
1064
- // On Windows, cmd.exe does not strip single quotes, so we use
1065
- // double quotes and escape them to ensure the JSON is parsed
1066
- // correctly by Godot.
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
- ? `\"${paramsJson.replace(/\"/g, '\\"')}\"`
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
- const { stdout, stderr } = await execAsync(cmd);
1087
- return { stdout, stderr };
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.godotBridge.getStatus(), null, 2) }] };
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": {"x": 100, "y": 200}, "scale": {"x": 2, "y": 2}})',
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.5",
3
+ "version": "2.3.7",
4
4
  "mcpName": "io.github.HaD0Yun/gopeak",
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.",
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": "./build/cli.js",
10
- "godot-mcp": "./build/cli.js"
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:ci": "npm run test:smoke",
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.7"
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
+ }