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 +15 -7
- package/build/cli/check.js +1 -1
- package/build/cli/notify.js +3 -3
- package/build/cli/setup.js +1 -1
- package/build/cli/star.js +1 -1
- package/build/cli.js +1 -1
- package/build/index.js +187 -47
- package/build/lsp_client.js +35 -4
- package/build/providers/ambientcg.js +3 -1
- package/build/providers/kenney.js +3 -0
- package/build/providers/logging.js +19 -0
- package/build/providers/manager.js +4 -3
- package/build/providers/polyhaven.js +3 -1
- package/build/scripts/godot_operations.gd +4 -0
- package/package.json +14 -7
package/README.md
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
[](https://nodejs.org/en/download/)
|
|
6
6
|
[](https://www.typescriptlang.org/)
|
|
7
7
|
[](https://www.npmjs.com/package/gopeak)
|
|
8
|
-
[](https://github.com/HaD0Yun/godot-mcp/commits/main)
|
|
9
|
-
[](https://github.com/HaD0Yun/godot-mcp/stargazers)
|
|
10
|
-
[](https://github.com/HaD0Yun/godot-mcp/network/members)
|
|
8
|
+
[](https://github.com/HaD0Yun/Gopeak-godot-mcp/commits/main)
|
|
9
|
+
[](https://github.com/HaD0Yun/Gopeak-godot-mcp/stargazers)
|
|
10
|
+
[](https://github.com/HaD0Yun/Gopeak-godot-mcp/network/members)
|
|
11
11
|
[](https://opensource.org/licenses/MIT)
|
|
12
12
|
|
|
13
13
|

|
|
@@ -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
|
|
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).
|
package/build/cli/check.js
CHANGED
|
@@ -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('');
|
package/build/cli/notify.js
CHANGED
|
@@ -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
|
}
|
package/build/cli/setup.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
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
|
-
|
|
704
|
-
|
|
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
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
6303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/build/lsp_client.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
36
|
-
"axios": "^1.
|
|
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",
|