gopeak 2.3.2 → 2.3.3

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
@@ -5,9 +5,9 @@
5
5
  [![](https://img.shields.io/badge/Node.js-339933?style=flat&logo=nodedotjs&logoColor=white 'Node.js')](https://nodejs.org/en/download/)
6
6
  [![](https://img.shields.io/badge/TypeScript-3178C6?style=flat&logo=typescript&logoColor=white 'TypeScript')](https://www.typescriptlang.org/)
7
7
  [![npm](https://img.shields.io/npm/v/gopeak?style=flat&logo=npm&logoColor=white 'npm')](https://www.npmjs.com/package/gopeak)
8
- [![](https://img.shields.io/github/last-commit/HaD0Yun/godot-mcp 'Last Commit')](https://github.com/HaD0Yun/godot-mcp/commits/main)
9
- [![](https://img.shields.io/github/stars/HaD0Yun/godot-mcp 'Stars')](https://github.com/HaD0Yun/godot-mcp/stargazers)
10
- [![](https://img.shields.io/github/forks/HaD0Yun/godot-mcp 'Forks')](https://github.com/HaD0Yun/godot-mcp/network/members)
8
+ [![](https://img.shields.io/github/last-commit/HaD0Yun/Gopeak-godot-mcp 'Last Commit')](https://github.com/HaD0Yun/Gopeak-godot-mcp/commits/main)
9
+ [![](https://img.shields.io/github/stars/HaD0Yun/Gopeak-godot-mcp 'Stars')](https://github.com/HaD0Yun/Gopeak-godot-mcp/stargazers)
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
13
  ![GoPeak Hero](assets/gopeak-hero-v2.png)
@@ -169,7 +169,7 @@ gopeak
169
169
  ### C) From source
170
170
 
171
171
  ```bash
172
- git clone https://github.com/HaD0Yun/godot-mcp.git
172
+ git clone https://github.com/HaD0Yun/Gopeak-godot-mcp.git
173
173
  cd godot-mcp
174
174
  npm install
175
175
  npm run build
@@ -183,13 +183,21 @@ GoPeak also exposes two CLI bin names:
183
183
 
184
184
  ---
185
185
 
186
+ ## Documentation
187
+
188
+ - [Documentation Map](docs/README.md)
189
+ - [Architecture](docs/architecture.md)
190
+ - [Platform Roadmap](docs/platform-roadmap.md)
191
+ - [Unity-MCP Benchmark Plan](docs/unity-mcp-benchmark-plan.md)
192
+ - [Release Process](docs/release-process.md)
193
+
186
194
  ## CI
187
195
 
188
196
  GitHub Actions runs on push/PR and executes:
189
197
 
190
198
  1. `npm run build`
191
199
  2. `npx tsc --noEmit`
192
- 3. `npm run test:ci`
200
+ 3. `npm run smoke`
193
201
 
194
202
  Run the same checks locally:
195
203
 
@@ -219,13 +227,13 @@ Full release checklist: [`docs/release-process.md`](docs/release-process.md).
219
227
  Install in your Godot project folder:
220
228
 
221
229
  ```bash
222
- curl -sL https://raw.githubusercontent.com/HaD0Yun/godot-mcp/main/install-addon.sh | bash
230
+ curl -sL https://raw.githubusercontent.com/HaD0Yun/Gopeak-godot-mcp/main/install-addon.sh | bash
223
231
  ```
224
232
 
225
233
  PowerShell:
226
234
 
227
235
  ```powershell
228
- iwr https://raw.githubusercontent.com/HaD0Yun/godot-mcp/main/install-addon.ps1 -UseBasicParsing | iex
236
+ iwr https://raw.githubusercontent.com/HaD0Yun/Gopeak-godot-mcp/main/install-addon.ps1 -UseBasicParsing | iex
229
237
  ```
230
238
 
231
239
  Then enable plugins in **Project Settings → Plugins** (especially `godot_mcp_editor` for bridge-backed scene/resource tools).
@@ -63,7 +63,7 @@ async function backgroundCheck() {
63
63
  function printUpdateBox(current, latest) {
64
64
  const line1 = ` 🚀 GoPeak v${latest} available! (current: v${current})`;
65
65
  const line2 = ` npm update -g gopeak`;
66
- const line3 = ` https://github.com/HaD0Yun/godot-mcp/releases`;
66
+ const line3 = ` https://github.com/HaD0Yun/Gopeak-godot-mcp/releases`;
67
67
  const maxLen = Math.max(line1.length, line2.length, line3.length) + 2;
68
68
  const pad = (s) => s + ' '.repeat(Math.max(0, maxLen - s.length));
69
69
  console.log('');
@@ -7,7 +7,7 @@
7
7
  import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
8
8
  import { createInterface } from 'readline';
9
9
  import { NOTIFY_FILE, STAR_PROMPTED_FILE, ensureGopeakDir, commandExists, runCommand, } from './utils.js';
10
- const REPO_URL = 'https://github.com/HaD0Yun/godot-mcp';
10
+ const REPO_URL = 'https://github.com/HaD0Yun/Gopeak-godot-mcp';
11
11
  export async function showNotification() {
12
12
  ensureGopeakDir();
13
13
  const hasUpdate = existsSync(NOTIFY_FILE);
@@ -61,12 +61,12 @@ async function handleStar() {
61
61
  return;
62
62
  }
63
63
  // Check if already starred
64
- const checkResult = await runCommand('gh api user/starred/HaD0Yun/godot-mcp');
64
+ const checkResult = await runCommand('gh api user/starred/HaD0Yun/Gopeak-godot-mcp');
65
65
  if (checkResult.code === 0) {
66
66
  console.log(' ⭐ Already starred! Thank you!');
67
67
  return;
68
68
  }
69
- const starResult = await runCommand('gh api -X PUT user/starred/HaD0Yun/godot-mcp');
69
+ const starResult = await runCommand('gh api -X PUT user/starred/HaD0Yun/Gopeak-godot-mcp');
70
70
  if (starResult.code === 0) {
71
71
  console.log(' ⭐ Starred! Thank you for your support!');
72
72
  }
@@ -106,7 +106,7 @@ function printOnboarding(log = console.log) {
106
106
  log('║ ║');
107
107
  log('║ 110+ tools for Godot Engine via MCP ║');
108
108
  log('║ ║');
109
- log('║ 📖 Docs: https://github.com/HaD0Yun/godot-mcp ║');
109
+ log('║ 📖 Docs: https://github.com/HaD0Yun/Gopeak-godot-mcp ║');
110
110
  log('║ ⭐ Star: gopeak star ║');
111
111
  log('║ 🔄 Update: npm update -g gopeak ║');
112
112
  log('╚══════════════════════════════════════════════════════╝');
package/build/cli/star.js CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { commandExists, runCommand, STAR_PROMPTED_FILE, ensureGopeakDir } from './utils.js';
9
9
  import { existsSync, writeFileSync } from 'fs';
10
- const REPO = 'HaD0Yun/godot-mcp';
10
+ const REPO = 'HaD0Yun/Gopeak-godot-mcp';
11
11
  const REPO_URL = `https://github.com/${REPO}`;
12
12
  export async function starGoPeak() {
13
13
  // 1. Check if gh CLI exists
package/build/cli.js CHANGED
@@ -82,7 +82,7 @@ Usage:
82
82
  Shell hooks wrap these commands with update notifications:
83
83
  claude, codex, gemini, opencode, omc, omx
84
84
 
85
- More info: https://github.com/HaD0Yun/godot-mcp
85
+ More info: https://github.com/HaD0Yun/Gopeak-godot-mcp
86
86
  `.trim());
87
87
  }
88
88
  main().catch((err) => {
package/build/index.js CHANGED
@@ -671,42 +671,109 @@ class GodotServer {
671
671
  const payload = JSON.stringify({ command, params, id: Date.now() });
672
672
  socket.write(payload + '\n');
673
673
  });
674
- let responseData = '';
674
+ let responseBuffer = Buffer.alloc(0);
675
+ let resolved = false;
675
676
  const timer = setTimeout(() => {
677
+ if (resolved) {
678
+ return;
679
+ }
680
+ resolved = true;
676
681
  socket.destroy();
677
682
  resolve({
678
683
  content: [{ type: 'text', text: `Runtime command '${command}' timed out after ${TIMEOUT_MS}ms. Ensure the Godot game is running with the MCP runtime addon enabled.` }],
679
684
  });
680
685
  }, TIMEOUT_MS);
681
- socket.setEncoding('utf8');
686
+ const resolveRuntimePayload = (parsed) => {
687
+ if (resolved) {
688
+ return;
689
+ }
690
+ resolved = true;
691
+ clearTimeout(timer);
692
+ socket.destroy();
693
+ if (parsed.type === 'screenshot' && parsed.data) {
694
+ resolve({
695
+ content: [
696
+ { type: 'text', text: `Screenshot captured: ${parsed.width}x${parsed.height} ${parsed.format}` },
697
+ { type: 'image', text: parsed.data },
698
+ ],
699
+ });
700
+ return;
701
+ }
702
+ resolve({
703
+ content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }],
704
+ });
705
+ };
682
706
  socket.on('data', (chunk) => {
683
- responseData += chunk;
684
- if (responseData.includes('\n') || responseData.includes('}')) {
685
- clearTimeout(timer);
686
- socket.destroy();
707
+ responseBuffer = Buffer.concat([responseBuffer, Buffer.from(chunk)]);
708
+ const parsedMessages = [];
709
+ const parseCandidate = (candidate) => {
710
+ const trimmed = candidate.trim();
711
+ if (!trimmed) {
712
+ return;
713
+ }
687
714
  try {
688
- const parsed = JSON.parse(responseData.trim());
689
- if (parsed.type === 'screenshot' && parsed.data) {
690
- resolve({
691
- content: [
692
- { type: 'text', text: `Screenshot captured: ${parsed.width}x${parsed.height} ${parsed.format}` },
693
- { type: 'image', text: parsed.data },
694
- ],
695
- });
696
- return;
697
- }
698
- resolve({
699
- content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }],
700
- });
715
+ parsedMessages.push(JSON.parse(trimmed));
701
716
  }
702
717
  catch {
703
- resolve({
704
- content: [{ type: 'text', text: responseData.trim() || 'Command sent successfully (no structured response).' }],
705
- });
718
+ // Ignore malformed frame/line and keep scanning.
719
+ }
720
+ };
721
+ // First, parse the framed payload format emitted by Godot's StreamPeerTCP.put_utf8_string().
722
+ let offset = 0;
723
+ while (offset + 4 <= responseBuffer.length) {
724
+ const frameLength = responseBuffer.readUInt32LE(offset);
725
+ if (frameLength <= 0 || offset + 4 + frameLength > responseBuffer.length) {
726
+ break;
727
+ }
728
+ const frame = responseBuffer.subarray(offset + 4, offset + 4 + frameLength).toString('utf8');
729
+ parseCandidate(frame);
730
+ offset += 4 + frameLength;
731
+ }
732
+ if (offset > 0) {
733
+ responseBuffer = responseBuffer.subarray(offset);
734
+ }
735
+ // Fallback for plain newline-delimited JSON payloads.
736
+ let newlineIndex = responseBuffer.indexOf(0x0a);
737
+ while (newlineIndex !== -1) {
738
+ const line = responseBuffer.subarray(0, newlineIndex).toString('utf8');
739
+ responseBuffer = responseBuffer.subarray(newlineIndex + 1);
740
+ parseCandidate(line);
741
+ newlineIndex = responseBuffer.indexOf(0x0a);
742
+ }
743
+ if (parsedMessages.length > 0) {
744
+ const candidate = parsedMessages.find((message) => message?.type === 'screenshot' && message?.data)
745
+ ?? parsedMessages.find((message) => message?.type === 'pong')
746
+ ?? parsedMessages.find((message) => message?.type && message.type !== 'welcome')
747
+ ?? null;
748
+ if (candidate) {
749
+ resolveRuntimePayload(candidate);
706
750
  }
707
751
  }
708
752
  });
753
+ socket.on('end', () => {
754
+ if (resolved) {
755
+ return;
756
+ }
757
+ clearTimeout(timer);
758
+ const responseData = responseBuffer.toString('utf8').trim();
759
+ resolved = true;
760
+ try {
761
+ const parsed = JSON.parse(responseData);
762
+ resolve({
763
+ content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }],
764
+ });
765
+ }
766
+ catch {
767
+ resolve({
768
+ content: [{ type: 'text', text: responseData || 'Command sent successfully (no structured response).' }],
769
+ });
770
+ }
771
+ });
709
772
  socket.on('error', (error) => {
773
+ if (resolved) {
774
+ return;
775
+ }
776
+ resolved = true;
710
777
  clearTimeout(timer);
711
778
  resolve({
712
779
  content: [{ type: 'text', text: `Failed to connect to Godot runtime addon at ${RUNTIME_HOST}:${RUNTIME_PORT}: ${error.message}. Ensure the game is running with the MCP runtime autoload enabled.` }],
@@ -4586,6 +4653,26 @@ class GodotServer {
4586
4653
  })
4587
4654
  .filter((v) => v !== null);
4588
4655
  }
4656
+ extractLastJsonLine(stdout) {
4657
+ const lines = stdout
4658
+ .split(/\r?\n/)
4659
+ .map((line) => line.trim())
4660
+ .filter((line) => line.length > 0);
4661
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
4662
+ const line = lines[i];
4663
+ if (!(line.startsWith('{') || line.startsWith('['))) {
4664
+ continue;
4665
+ }
4666
+ try {
4667
+ JSON.parse(line);
4668
+ return line;
4669
+ }
4670
+ catch {
4671
+ continue;
4672
+ }
4673
+ }
4674
+ return null;
4675
+ }
4589
4676
  /**
4590
4677
  * Capture/update current intent snapshot
4591
4678
  */
@@ -5377,7 +5464,7 @@ class GodotServer {
5377
5464
  return this.createErrorResponse(`Failed to list scene nodes: ${stderr}`, ['Verify the scene file is valid']);
5378
5465
  }
5379
5466
  return {
5380
- content: [{ type: 'text', text: stdout.trim() }],
5467
+ content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
5381
5468
  };
5382
5469
  }
5383
5470
  catch (error) {
@@ -5414,7 +5501,7 @@ class GodotServer {
5414
5501
  return this.createErrorResponse(`Failed to get node properties: ${stderr}`, ['Verify the node path is correct', 'Check if the node exists in the scene']);
5415
5502
  }
5416
5503
  return {
5417
- content: [{ type: 'text', text: stdout.trim() }],
5504
+ content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
5418
5505
  };
5419
5506
  }
5420
5507
  catch (error) {
@@ -5603,7 +5690,7 @@ class GodotServer {
5603
5690
  return this.createErrorResponse(`Failed to get import status: ${stderr}`, ['Verify the resource path if specified']);
5604
5691
  }
5605
5692
  return {
5606
- content: [{ type: 'text', text: stdout.trim() }],
5693
+ content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
5607
5694
  };
5608
5695
  }
5609
5696
  catch (error) {
@@ -5638,7 +5725,7 @@ class GodotServer {
5638
5725
  return this.createErrorResponse(`Failed to get import options: ${stderr}`, ['Verify the resource is an importable file type']);
5639
5726
  }
5640
5727
  return {
5641
- content: [{ type: 'text', text: stdout.trim() }],
5728
+ content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
5642
5729
  };
5643
5730
  }
5644
5731
  catch (error) {
@@ -5744,7 +5831,7 @@ class GodotServer {
5744
5831
  return this.createErrorResponse(`Failed to list export presets: ${stderr}`, ['Check if export_presets.cfg exists in the project']);
5745
5832
  }
5746
5833
  return {
5747
- content: [{ type: 'text', text: stdout.trim() }],
5834
+ content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
5748
5835
  };
5749
5836
  }
5750
5837
  catch (error) {
@@ -5814,7 +5901,7 @@ class GodotServer {
5814
5901
  return this.createErrorResponse(`Failed to validate project: ${stderr}`, ['Verify the project structure is valid']);
5815
5902
  }
5816
5903
  return {
5817
- content: [{ type: 'text', text: stdout.trim() }],
5904
+ content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
5818
5905
  };
5819
5906
  }
5820
5907
  catch (error) {
@@ -5850,7 +5937,7 @@ class GodotServer {
5850
5937
  return this.createErrorResponse(`Failed to get dependencies: ${stderr}`, ['Verify the resource path is correct']);
5851
5938
  }
5852
5939
  return {
5853
- content: [{ type: 'text', text: stdout.trim() }],
5940
+ content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
5854
5941
  };
5855
5942
  }
5856
5943
  catch (error) {
@@ -5882,7 +5969,7 @@ class GodotServer {
5882
5969
  return this.createErrorResponse(`Failed to find resource usages: ${stderr}`, ['Verify the resource path is correct']);
5883
5970
  }
5884
5971
  return {
5885
- content: [{ type: 'text', text: stdout.trim() }],
5972
+ content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
5886
5973
  };
5887
5974
  }
5888
5975
  catch (error) {
@@ -5914,7 +6001,7 @@ class GodotServer {
5914
6001
  return this.createErrorResponse(`Failed to parse error log: ${stderr}`, ['Verify the log content or ensure godot.log exists']);
5915
6002
  }
5916
6003
  return {
5917
- content: [{ type: 'text', text: stdout.trim() }],
6004
+ content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
5918
6005
  };
5919
6006
  }
5920
6007
  catch (error) {
@@ -5945,7 +6032,7 @@ class GodotServer {
5945
6032
  return this.createErrorResponse(`Failed to get project health: ${stderr}`, ['Verify the project structure']);
5946
6033
  }
5947
6034
  return {
5948
- content: [{ type: 'text', text: stdout.trim() }],
6035
+ content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
5949
6036
  };
5950
6037
  }
5951
6038
  catch (error) {
@@ -5979,7 +6066,7 @@ class GodotServer {
5979
6066
  return this.createErrorResponse(`Failed to get project setting: ${stderr}`, ['Verify the setting path is correct']);
5980
6067
  }
5981
6068
  return {
5982
- content: [{ type: 'text', text: stdout.trim() }],
6069
+ content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
5983
6070
  };
5984
6071
  }
5985
6072
  catch (error) {
@@ -6103,7 +6190,7 @@ class GodotServer {
6103
6190
  return this.createErrorResponse(`Failed to list autoloads: ${stderr}`, ['Verify the project structure']);
6104
6191
  }
6105
6192
  return {
6106
- content: [{ type: 'text', text: stdout.trim() }],
6193
+ content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
6107
6194
  };
6108
6195
  }
6109
6196
  catch (error) {
@@ -6260,7 +6347,7 @@ class GodotServer {
6260
6347
  return this.createErrorResponse(`Failed to list connections: ${stderr}`, ['Verify the scene path is correct']);
6261
6348
  }
6262
6349
  return {
6263
- content: [{ type: 'text', text: stdout.trim() }],
6350
+ content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
6264
6351
  };
6265
6352
  }
6266
6353
  catch (error) {
@@ -6279,32 +6366,59 @@ class GodotServer {
6279
6366
  return this.createErrorResponse('Project path is required', ['Provide a valid path to a Godot project directory']);
6280
6367
  }
6281
6368
  try {
6282
- // Runtime connection requires a running Godot instance with the addon
6283
- // For now, return status based on active process
6284
- if (this.activeProcess) {
6369
+ const runtime = await this.handleRuntimeCommand('ping', {});
6370
+ const runtimeText = runtime?.content?.[0]?.text || '';
6371
+ let runtimePayload = null;
6372
+ try {
6373
+ runtimePayload = JSON.parse(runtimeText);
6374
+ }
6375
+ catch {
6376
+ runtimePayload = null;
6377
+ }
6378
+ const runtimeConnected = runtimePayload?.type === 'pong';
6379
+ if (runtimeConnected) {
6285
6380
  return {
6286
6381
  content: [{
6287
6382
  type: 'text',
6288
6383
  text: JSON.stringify({
6289
6384
  connected: true,
6290
6385
  status: 'running',
6291
- note: 'A Godot process is active. Use inspect_runtime_tree to explore.',
6386
+ processActive: Boolean(this.activeProcess),
6387
+ runtimeAddon: 'connected',
6388
+ note: 'Godot runtime addon responded to ping. Use inspect_runtime_tree to explore.',
6389
+ runtimeResponse: runtimePayload,
6292
6390
  }, null, 2),
6293
6391
  }],
6294
6392
  };
6295
6393
  }
6296
- else {
6394
+ if (this.activeProcess) {
6297
6395
  return {
6298
6396
  content: [{
6299
6397
  type: 'text',
6300
6398
  text: JSON.stringify({
6301
6399
  connected: false,
6302
- status: 'not_running',
6303
- note: 'No active Godot process. Use run_project to start one.',
6400
+ status: 'process_running_runtime_disconnected',
6401
+ processActive: true,
6402
+ runtimeAddon: 'unreachable',
6403
+ note: 'A Godot process is active, but the runtime addon did not respond on port 7777.',
6404
+ runtimeResponse: runtimeText,
6304
6405
  }, null, 2),
6305
6406
  }],
6306
6407
  };
6307
6408
  }
6409
+ return {
6410
+ content: [{
6411
+ type: 'text',
6412
+ text: JSON.stringify({
6413
+ connected: false,
6414
+ status: 'not_running',
6415
+ processActive: false,
6416
+ runtimeAddon: 'unreachable',
6417
+ note: 'No active Godot process or runtime addon detected. Use run_project to start one.',
6418
+ runtimeResponse: runtimeText,
6419
+ }, null, 2),
6420
+ }],
6421
+ };
6308
6422
  }
6309
6423
  catch (error) {
6310
6424
  return this.createErrorResponse(`Failed to get runtime status: ${error?.message || 'Unknown error'}`, ['Ensure Godot is installed correctly']);
@@ -6861,7 +6975,7 @@ class GodotServer {
6861
6975
  return this.createErrorResponse(`Failed to list plugins: ${stderr}`, ['Verify the project structure']);
6862
6976
  }
6863
6977
  return {
6864
- content: [{ type: 'text', text: stdout.trim() }],
6978
+ content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
6865
6979
  };
6866
6980
  }
6867
6981
  catch (error) {
@@ -7000,7 +7114,7 @@ class GodotServer {
7000
7114
  return this.createErrorResponse(`Failed to search project: ${stderr}`, ['Check if the query/regex pattern is valid']);
7001
7115
  }
7002
7116
  return {
7003
- content: [{ type: 'text', text: stdout.trim() }],
7117
+ content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
7004
7118
  };
7005
7119
  }
7006
7120
  catch (error) {
@@ -7787,7 +7901,16 @@ uniform float dissolve_amount : hint_range(0.0, 1.0) = 0.0;
7787
7901
  params.instantiable_only = args.instantiableOnly;
7788
7902
  if (args?.instantiable_only !== undefined)
7789
7903
  params.instantiable_only = args.instantiable_only;
7790
- return await this.executeOperation('query_classes', params, projectPath);
7904
+ const { stdout, stderr } = await this.executeOperation('query_classes', params, projectPath);
7905
+ if (stderr && stderr.trim()) {
7906
+ return this.createErrorResponse(`Failed to query classes: ${stderr.trim()}`, [
7907
+ 'Check the project path and ensure project.godot exists',
7908
+ 'Verify the category/filter arguments are valid',
7909
+ ]);
7910
+ }
7911
+ return {
7912
+ content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
7913
+ };
7791
7914
  }
7792
7915
  /**
7793
7916
  * Handle the query_class_info tool — ClassDB introspection
@@ -7808,7 +7931,16 @@ uniform float dissolve_amount : hint_range(0.0, 1.0) = 0.0;
7808
7931
  params.include_inherited = args.includeInherited;
7809
7932
  if (args?.include_inherited !== undefined)
7810
7933
  params.include_inherited = args.include_inherited;
7811
- return await this.executeOperation('query_class_info', params, projectPath);
7934
+ const { stdout, stderr } = await this.executeOperation('query_class_info', params, projectPath);
7935
+ if (stderr && stderr.trim()) {
7936
+ return this.createErrorResponse(`Failed to query class info: ${stderr.trim()}`, [
7937
+ 'Check that the class name exists in the current Godot version',
7938
+ 'Verify the project path and ClassDB availability',
7939
+ ]);
7940
+ }
7941
+ return {
7942
+ content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
7943
+ };
7812
7944
  }
7813
7945
  /**
7814
7946
  * Handle the inspect_inheritance tool — ClassDB introspection
@@ -7822,9 +7954,17 @@ uniform float dissolve_amount : hint_range(0.0, 1.0) = 0.0;
7822
7954
  if (!className) {
7823
7955
  throw new McpError(ErrorCode.InvalidParams, 'className is required');
7824
7956
  }
7825
- return await this.executeOperation('inspect_inheritance', {
7957
+ const { stdout, stderr } = await this.executeOperation('inspect_inheritance', {
7826
7958
  class_name: className,
7827
7959
  }, projectPath);
7960
+ if (stderr && stderr.trim()) {
7961
+ return this.createErrorResponse(`Failed to inspect inheritance: ${stderr.trim()}`, [
7962
+ 'Check that the class name exists in the current Godot version',
7963
+ ]);
7964
+ }
7965
+ return {
7966
+ content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
7967
+ };
7828
7968
  }
7829
7969
  /**
7830
7970
  * Handle the modify_resource tool
@@ -1,6 +1,6 @@
1
- import { readFile } from 'node:fs/promises';
1
+ import { readFile, realpath } from 'node:fs/promises';
2
2
  import { createConnection } from 'node:net';
3
- import { dirname, resolve } from 'node:path';
3
+ import { dirname, resolve, sep } from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
5
  export class GodotLSPClient {
6
6
  socket = null;
@@ -464,6 +464,38 @@ function normalizeLSPError(error) {
464
464
  }
465
465
  return String(error);
466
466
  }
467
+ function normalizePathForComparison(pathValue) {
468
+ const resolved = resolve(pathValue);
469
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
470
+ }
471
+ function isPathWithinRoot(rootPath, candidatePath) {
472
+ const normalizedRoot = normalizePathForComparison(rootPath);
473
+ const normalizedCandidate = normalizePathForComparison(candidatePath);
474
+ const rootPrefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`;
475
+ return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(rootPrefix);
476
+ }
477
+ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
478
+ const requestedProjectPath = resolve(projectPathValue);
479
+ let projectPath;
480
+ try {
481
+ projectPath = await realpath(requestedProjectPath);
482
+ }
483
+ catch {
484
+ throw new Error(`Project path does not exist: ${requestedProjectPath}`);
485
+ }
486
+ const requestedScriptPath = resolve(projectPath, scriptPathValue);
487
+ let scriptPath;
488
+ try {
489
+ scriptPath = await realpath(requestedScriptPath);
490
+ }
491
+ catch {
492
+ throw new Error(`Script file does not exist: ${requestedScriptPath}`);
493
+ }
494
+ if (!isPathWithinRoot(projectPath, scriptPath)) {
495
+ throw new Error('scriptPath resolves outside the project root boundary.');
496
+ }
497
+ return { projectPath, scriptPath };
498
+ }
467
499
  export async function handleLSPTool(client, toolName, args) {
468
500
  try {
469
501
  if (!args || typeof args !== 'object') {
@@ -478,8 +510,7 @@ export async function handleLSPTool(client, toolName, args) {
478
510
  if (typeof scriptPathValue !== 'string' || scriptPathValue.length === 0) {
479
511
  throw new Error('Missing required argument: scriptPath');
480
512
  }
481
- const projectPath = resolve(projectPathValue);
482
- const scriptPath = resolve(projectPath, scriptPathValue);
513
+ const { projectPath, scriptPath } = await resolveLSPPaths(projectPathValue, scriptPathValue);
483
514
  const content = await readFile(scriptPath, 'utf8');
484
515
  await client.initialize(projectPath);
485
516
  switch (toolName) {
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, createWriteStream, appendFileSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import * as https from 'https';
4
+ import { providerLog } from './logging.js';
4
5
  const AMBIENTCG_CONFIG = {
5
6
  name: 'ambientcg',
6
7
  displayName: 'AmbientCG',
@@ -111,7 +112,7 @@ export class AmbientCGProvider {
111
112
  });
112
113
  }
113
114
  catch (error) {
114
- console.error(`[AmbientCG] Search failed: ${error}`);
115
+ providerLog('error', this.config.name, 'Search failed', error);
115
116
  return [];
116
117
  }
117
118
  }
@@ -159,6 +160,7 @@ export class AmbientCGProvider {
159
160
  };
160
161
  }
161
162
  catch (error) {
163
+ providerLog('error', this.config.name, 'Download failed', error);
162
164
  return {
163
165
  success: false,
164
166
  assetId,
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, createWriteStream, appendFileSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import * as https from 'https';
4
+ import { providerLog } from './logging.js';
4
5
  const KENNEY_CONFIG = {
5
6
  name: 'kenney',
6
7
  displayName: 'Kenney',
@@ -105,6 +106,7 @@ export class KenneyProvider {
105
106
  const { assetId, projectPath, targetFolder = 'downloaded_assets/kenney' } = options;
106
107
  const pack = KENNEY_ASSET_PACKS.find(p => p.id === assetId);
107
108
  if (!pack) {
109
+ providerLog('warn', this.config.name, `Asset pack not found: ${assetId}`);
108
110
  return {
109
111
  success: false,
110
112
  assetId,
@@ -144,6 +146,7 @@ export class KenneyProvider {
144
146
  };
145
147
  }
146
148
  catch (error) {
149
+ providerLog('error', this.config.name, 'Download failed', error);
147
150
  return {
148
151
  success: false,
149
152
  assetId,
@@ -0,0 +1,19 @@
1
+ export function formatProviderError(error) {
2
+ if (error instanceof Error) {
3
+ return error.stack ?? error.message;
4
+ }
5
+ if (typeof error === 'string') {
6
+ return error;
7
+ }
8
+ try {
9
+ return JSON.stringify(error);
10
+ }
11
+ catch {
12
+ return String(error);
13
+ }
14
+ }
15
+ export function providerLog(level, provider, message, error) {
16
+ const errorSuffix = error === undefined ? '' : ` | error=${formatProviderError(error)}`;
17
+ const timestamp = new Date().toISOString();
18
+ console.error(`[${timestamp}] [providers:${provider}] [${level.toUpperCase()}] ${message}${errorSuffix}`);
19
+ }
@@ -2,6 +2,7 @@ import { PROVIDER_PRIORITY, } from './types.js';
2
2
  import { PolyHavenProvider } from './polyhaven.js';
3
3
  import { AmbientCGProvider } from './ambientcg.js';
4
4
  import { KenneyProvider } from './kenney.js';
5
+ import { providerLog } from './logging.js';
5
6
  export class AssetManager {
6
7
  providers = new Map();
7
8
  sortedProviders = [];
@@ -40,7 +41,7 @@ export class AssetManager {
40
41
  allResults.push(...results);
41
42
  }
42
43
  catch (error) {
43
- console.error(`[AssetManager] Search failed for ${provider.config.name}: ${error}`);
44
+ providerLog('error', 'asset-manager', `Search failed for ${provider.config.name}`, error);
44
45
  }
45
46
  }
46
47
  return allResults
@@ -66,7 +67,7 @@ export class AssetManager {
66
67
  }
67
68
  }
68
69
  catch (error) {
69
- console.error(`[AssetManager] Search failed for ${provider.config.name}: ${error}`);
70
+ providerLog('error', 'asset-manager', `Search failed for ${provider.config.name}`, error);
70
71
  }
71
72
  }
72
73
  return [];
@@ -104,7 +105,7 @@ export class AssetManager {
104
105
  }
105
106
  }
106
107
  catch (error) {
107
- console.error(`[AssetManager] Download failed for ${provider.config.name}: ${error}`);
108
+ providerLog('error', 'asset-manager', `Download failed for ${provider.config.name}`, error);
108
109
  }
109
110
  }
110
111
  return {
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, createWriteStream, appendFileSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import * as https from 'https';
4
+ import { providerLog } from './logging.js';
4
5
  const POLYHAVEN_CONFIG = {
5
6
  name: 'polyhaven',
6
7
  displayName: 'Poly Haven',
@@ -115,7 +116,7 @@ export class PolyHavenProvider {
115
116
  .slice(0, maxResults);
116
117
  }
117
118
  catch (error) {
118
- console.error(`[PolyHaven] Search failed: ${error}`);
119
+ providerLog('error', this.config.name, 'Search failed', error);
119
120
  return [];
120
121
  }
121
122
  }
@@ -176,6 +177,7 @@ export class PolyHavenProvider {
176
177
  };
177
178
  }
178
179
  catch (error) {
180
+ providerLog('error', this.config.name, 'Download failed', error);
179
181
  return {
180
182
  success: false,
181
183
  assetId,
@@ -2835,6 +2835,10 @@ func get_node_by_path_v2(scene_root: Node, node_path: String) -> Node:
2835
2835
 
2836
2836
  return scene_root.get_node_or_null(clean_path)
2837
2837
 
2838
+ # Backward-compatible alias for newer helpers that still call the old name
2839
+ func get_node_from_path(scene_root: Node, node_path: String) -> Node:
2840
+ return get_node_by_path_v2(scene_root, node_path)
2841
+
2838
2842
  # Helper function to build node tree structure recursively
2839
2843
  func build_node_tree(node: Node, current_depth: int, max_depth: int, include_properties: bool) -> Dictionary:
2840
2844
  var result = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gopeak",
3
- "version": "2.3.2",
3
+ "version": "2.3.3",
4
4
  "mcpName": "io.github.HaD0Yun/gopeak",
5
5
  "description": "GoPeak — The most comprehensive MCP server for Godot Engine. 95+ tools: scene management, GDScript LSP diagnostics, DAP debugger, screenshot capture, input injection, ClassDB introspection, CC0 asset library. AI-assisted game development with Claude, Cursor, Cline, OpenCode.",
6
6
  "type": "module",
@@ -17,12 +17,15 @@
17
17
  "scripts": {
18
18
  "build": "tsc && node scripts/build-visualizer.js && node scripts/build.js",
19
19
  "typecheck": "tsc --noEmit",
20
+ "test": "npm run test:ci",
21
+ "smoke": "npm run test:ci",
20
22
  "test:smoke": "node scripts/smoke-test.mjs",
21
23
  "test:integration": "node test-bridge.mjs",
24
+ "test:dynamic-groups": "node test-dynamic-groups.mjs",
22
25
  "test:ci": "npm run test:smoke",
23
26
  "ci": "npm run build && npm run typecheck && npm run test:ci",
24
27
  "prepare": "npm run build",
25
- "postinstall": "node build/cli.js setup --silent 2>/dev/null || true",
28
+ "postinstall": "node -e \"try{require('child_process').execFileSync(process.execPath,['build/cli.js','setup','--silent'],{stdio:'ignore'})}catch{}\"",
26
29
  "watch": "tsc --watch",
27
30
  "inspector": "npx @modelcontextprotocol/inspector build/index.js",
28
31
  "pack": "npm pack --dry-run",
@@ -32,8 +35,8 @@
32
35
  "node": ">=18"
33
36
  },
34
37
  "dependencies": {
35
- "@modelcontextprotocol/sdk": "^1.27.0",
36
- "axios": "^1.7.9",
38
+ "@modelcontextprotocol/sdk": "^1.27.1",
39
+ "axios": "^1.13.6",
37
40
  "fs-extra": "^11.2.0",
38
41
  "ws": "^8.18.0"
39
42
  },
@@ -43,14 +46,18 @@
43
46
  "esbuild": "^0.24.2",
44
47
  "typescript": "^5.3.3"
45
48
  },
49
+ "overrides": {
50
+ "@hono/node-server": "^1.19.11",
51
+ "hono": "^4.12.5"
52
+ },
46
53
  "license": "MIT",
47
54
  "repository": {
48
55
  "type": "git",
49
- "url": "https://github.com/HaD0Yun/godot-mcp.git"
56
+ "url": "git+https://github.com/HaD0Yun/Gopeak-godot-mcp.git"
50
57
  },
51
- "homepage": "https://github.com/HaD0Yun/godot-mcp#readme",
58
+ "homepage": "https://github.com/HaD0Yun/Gopeak-godot-mcp#readme",
52
59
  "bugs": {
53
- "url": "https://github.com/HaD0Yun/godot-mcp/issues"
60
+ "url": "https://github.com/HaD0Yun/Gopeak-godot-mcp/issues"
54
61
  },
55
62
  "author": {
56
63
  "name": "HaD0Yun",