gopeak 2.3.2 → 2.3.4
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 +17 -7
- package/build/cli/check.js +1 -1
- package/build/cli/notify.js +3 -3
- package/build/cli/setup.js +9 -2
- package/build/cli/star.js +1 -1
- package/build/cli/utils.js +5 -0
- package/build/cli.js +1 -1
- package/build/index.js +261 -122
- 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 +16 -8
package/README.md
CHANGED
|
@@ -5,15 +5,17 @@
|
|
|
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
|

|
|
14
14
|
|
|
15
15
|
**GoPeak is an MCP server for Godot that lets AI assistants run, inspect, modify, and debug real projects end-to-end.**
|
|
16
16
|
|
|
17
|
+
> 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
|
+
|
|
17
19
|
---
|
|
18
20
|
|
|
19
21
|
## Quick Start (3 Minutes)
|
|
@@ -169,7 +171,7 @@ gopeak
|
|
|
169
171
|
### C) From source
|
|
170
172
|
|
|
171
173
|
```bash
|
|
172
|
-
git clone https://github.com/HaD0Yun/godot-mcp.git
|
|
174
|
+
git clone https://github.com/HaD0Yun/Gopeak-godot-mcp.git
|
|
173
175
|
cd godot-mcp
|
|
174
176
|
npm install
|
|
175
177
|
npm run build
|
|
@@ -183,13 +185,21 @@ GoPeak also exposes two CLI bin names:
|
|
|
183
185
|
|
|
184
186
|
---
|
|
185
187
|
|
|
188
|
+
## Documentation
|
|
189
|
+
|
|
190
|
+
- [Documentation Map](docs/README.md)
|
|
191
|
+
- [Architecture](docs/architecture.md)
|
|
192
|
+
- [Platform Roadmap](docs/platform-roadmap.md)
|
|
193
|
+
- [Unity-MCP Benchmark Plan](docs/unity-mcp-benchmark-plan.md)
|
|
194
|
+
- [Release Process](docs/release-process.md)
|
|
195
|
+
|
|
186
196
|
## CI
|
|
187
197
|
|
|
188
198
|
GitHub Actions runs on push/PR and executes:
|
|
189
199
|
|
|
190
200
|
1. `npm run build`
|
|
191
201
|
2. `npx tsc --noEmit`
|
|
192
|
-
3. `npm run
|
|
202
|
+
3. `npm run smoke`
|
|
193
203
|
|
|
194
204
|
Run the same checks locally:
|
|
195
205
|
|
|
@@ -219,13 +229,13 @@ Full release checklist: [`docs/release-process.md`](docs/release-process.md).
|
|
|
219
229
|
Install in your Godot project folder:
|
|
220
230
|
|
|
221
231
|
```bash
|
|
222
|
-
curl -sL https://raw.githubusercontent.com/HaD0Yun/godot-mcp/main/install-addon.sh | bash
|
|
232
|
+
curl -sL https://raw.githubusercontent.com/HaD0Yun/Gopeak-godot-mcp/main/install-addon.sh | bash
|
|
223
233
|
```
|
|
224
234
|
|
|
225
235
|
PowerShell:
|
|
226
236
|
|
|
227
237
|
```powershell
|
|
228
|
-
iwr https://raw.githubusercontent.com/HaD0Yun/godot-mcp/main/install-addon.ps1 -UseBasicParsing | iex
|
|
238
|
+
iwr https://raw.githubusercontent.com/HaD0Yun/Gopeak-godot-mcp/main/install-addon.ps1 -UseBasicParsing | iex
|
|
229
239
|
```
|
|
230
240
|
|
|
231
241
|
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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* with a precheck function that displays cached GoPeak update notifications.
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync, readFileSync, writeFileSync, appendFileSync } from 'fs';
|
|
8
|
-
import { getShellRcFile, getShellName, getLocalVersion, ensureGopeakDir, ONBOARDING_SHOWN_FILE, STAR_PROMPTED_FILE, } from './utils.js';
|
|
8
|
+
import { getShellRcFile, getShellName, getLocalVersion, ensureGopeakDir, ONBOARDING_SHOWN_FILE, STAR_PROMPTED_FILE, supportsShellHooks, } from './utils.js';
|
|
9
9
|
const MARKER_START = '# >>> GoPeak shell hooks >>>';
|
|
10
10
|
const MARKER_END = '# <<< GoPeak shell hooks <<<';
|
|
11
11
|
/** The shell hook block that gets appended to the RC file. */
|
|
@@ -55,6 +55,13 @@ function generateHookBlock() {
|
|
|
55
55
|
}
|
|
56
56
|
export async function setupShellHooks(args = []) {
|
|
57
57
|
const silent = args.includes('--silent');
|
|
58
|
+
if (!supportsShellHooks()) {
|
|
59
|
+
if (!silent) {
|
|
60
|
+
console.log('ℹ️ GoPeak shell hooks are only installed for bash/zsh on Unix-like systems.');
|
|
61
|
+
console.log(' Skipping shell hook setup on this platform/shell.');
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
58
65
|
const rcFile = getShellRcFile();
|
|
59
66
|
const shellName = getShellName();
|
|
60
67
|
const log = silent ? (..._args) => { } : console.log.bind(console);
|
|
@@ -106,7 +113,7 @@ function printOnboarding(log = console.log) {
|
|
|
106
113
|
log('║ ║');
|
|
107
114
|
log('║ 110+ tools for Godot Engine via MCP ║');
|
|
108
115
|
log('║ ║');
|
|
109
|
-
log('║ 📖 Docs: https://github.com/HaD0Yun/godot-mcp ║');
|
|
116
|
+
log('║ 📖 Docs: https://github.com/HaD0Yun/Gopeak-godot-mcp ║');
|
|
110
117
|
log('║ ⭐ Star: gopeak star ║');
|
|
111
118
|
log('║ 🔄 Update: npm update -g gopeak ║');
|
|
112
119
|
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/utils.js
CHANGED
|
@@ -122,6 +122,11 @@ export function getShellName() {
|
|
|
122
122
|
return 'zsh';
|
|
123
123
|
return 'bash';
|
|
124
124
|
}
|
|
125
|
+
export function supportsShellHooks(platform = process.platform, shell = process.env.SHELL ?? '') {
|
|
126
|
+
if (platform === 'win32')
|
|
127
|
+
return false;
|
|
128
|
+
return shell.includes('bash') || shell.includes('zsh');
|
|
129
|
+
}
|
|
125
130
|
/* ------------------------------------------------------------------ */
|
|
126
131
|
/* Command helpers */
|
|
127
132
|
/* ------------------------------------------------------------------ */
|
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', data: parsed.data, mimeType: 'image/png' },
|
|
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;
|
|
706
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);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
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
|
+
});
|
|
707
770
|
}
|
|
708
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.` }],
|
|
@@ -726,8 +793,41 @@ class GodotServer {
|
|
|
726
793
|
}
|
|
727
794
|
return handleDAPTool(this.dapClient, toolName, args);
|
|
728
795
|
}
|
|
796
|
+
sanitizeExportedToolName(toolName) {
|
|
797
|
+
const sanitized = toolName
|
|
798
|
+
.normalize('NFKD')
|
|
799
|
+
.replace(/[^\x00-\x7F]/g, '')
|
|
800
|
+
.replace(/[^a-zA-Z0-9-]+/g, '-')
|
|
801
|
+
.replace(/-+/g, '-')
|
|
802
|
+
.replace(/^-+|-+$/g, '')
|
|
803
|
+
.slice(0, 128);
|
|
804
|
+
return sanitized.length > 0 ? sanitized : 'tool';
|
|
805
|
+
}
|
|
806
|
+
buildToolNameResolutionMap(allTools) {
|
|
807
|
+
const resolutionMap = new Map();
|
|
808
|
+
const register = (candidateName, resolvedName) => {
|
|
809
|
+
const existing = resolutionMap.get(candidateName);
|
|
810
|
+
if (existing && existing !== resolvedName) {
|
|
811
|
+
throw new Error(`Sanitized tool name collision: "${candidateName}" maps to both "${existing}" and "${resolvedName}"`);
|
|
812
|
+
}
|
|
813
|
+
resolutionMap.set(candidateName, resolvedName);
|
|
814
|
+
};
|
|
815
|
+
for (const tool of allTools) {
|
|
816
|
+
register(tool.name, tool.name);
|
|
817
|
+
register(this.sanitizeExportedToolName(tool.name), tool.name);
|
|
818
|
+
}
|
|
819
|
+
for (const [compactName, legacyName] of Object.entries(this.compactAliasToLegacy)) {
|
|
820
|
+
register(compactName, legacyName);
|
|
821
|
+
register(this.sanitizeExportedToolName(compactName), legacyName);
|
|
822
|
+
}
|
|
823
|
+
return resolutionMap;
|
|
824
|
+
}
|
|
729
825
|
resolveToolAlias(requestedToolName) {
|
|
730
|
-
|
|
826
|
+
const allTools = this.getAllToolDefinitions();
|
|
827
|
+
const resolutionMap = this.buildToolNameResolutionMap(allTools);
|
|
828
|
+
return resolutionMap.get(requestedToolName)
|
|
829
|
+
|| resolutionMap.get(this.sanitizeExportedToolName(requestedToolName))
|
|
830
|
+
|| requestedToolName;
|
|
731
831
|
}
|
|
732
832
|
buildCompactTools(allTools) {
|
|
733
833
|
const compactTools = [];
|
|
@@ -744,6 +844,26 @@ class GodotServer {
|
|
|
744
844
|
}
|
|
745
845
|
return compactTools;
|
|
746
846
|
}
|
|
847
|
+
sanitizeToolsForList(tools) {
|
|
848
|
+
const seenNames = new Map();
|
|
849
|
+
return tools.map((tool) => {
|
|
850
|
+
const sanitizedName = this.sanitizeExportedToolName(tool.name);
|
|
851
|
+
const existing = seenNames.get(sanitizedName);
|
|
852
|
+
if (existing && existing !== tool.name) {
|
|
853
|
+
throw new Error(`Sanitized tool name collision in tools/list: "${sanitizedName}" from "${existing}" and "${tool.name}"`);
|
|
854
|
+
}
|
|
855
|
+
seenNames.set(sanitizedName, tool.name);
|
|
856
|
+
if (sanitizedName !== tool.name) {
|
|
857
|
+
this.logDebug(`Exporting tool "${tool.name}" as "${sanitizedName}" for OpenAI-compatible clients`);
|
|
858
|
+
}
|
|
859
|
+
return sanitizedName === tool.name
|
|
860
|
+
? tool
|
|
861
|
+
: {
|
|
862
|
+
...tool,
|
|
863
|
+
name: sanitizedName,
|
|
864
|
+
};
|
|
865
|
+
});
|
|
866
|
+
}
|
|
747
867
|
getExposedTools(allTools) {
|
|
748
868
|
if (this.toolExposureProfile === 'full' || this.toolExposureProfile === 'legacy') {
|
|
749
869
|
return allTools;
|
|
@@ -3559,7 +3679,7 @@ class GodotServer {
|
|
|
3559
3679
|
this.server.setRequestHandler(ListToolsRequestSchema, async (request) => {
|
|
3560
3680
|
const allTools = buildToolDefinitions();
|
|
3561
3681
|
this.cachedToolDefinitions = allTools;
|
|
3562
|
-
const exposedTools = this.getExposedTools(allTools);
|
|
3682
|
+
const exposedTools = this.sanitizeToolsForList(this.getExposedTools(allTools));
|
|
3563
3683
|
return this.paginateToolsForList(exposedTools, request.params?.cursor);
|
|
3564
3684
|
});
|
|
3565
3685
|
// Handle tool calls
|
|
@@ -4586,6 +4706,26 @@ class GodotServer {
|
|
|
4586
4706
|
})
|
|
4587
4707
|
.filter((v) => v !== null);
|
|
4588
4708
|
}
|
|
4709
|
+
extractLastJsonLine(stdout) {
|
|
4710
|
+
const lines = stdout
|
|
4711
|
+
.split(/\r?\n/)
|
|
4712
|
+
.map((line) => line.trim())
|
|
4713
|
+
.filter((line) => line.length > 0);
|
|
4714
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
4715
|
+
const line = lines[i];
|
|
4716
|
+
if (!(line.startsWith('{') || line.startsWith('['))) {
|
|
4717
|
+
continue;
|
|
4718
|
+
}
|
|
4719
|
+
try {
|
|
4720
|
+
JSON.parse(line);
|
|
4721
|
+
return line;
|
|
4722
|
+
}
|
|
4723
|
+
catch {
|
|
4724
|
+
continue;
|
|
4725
|
+
}
|
|
4726
|
+
}
|
|
4727
|
+
return null;
|
|
4728
|
+
}
|
|
4589
4729
|
/**
|
|
4590
4730
|
* Capture/update current intent snapshot
|
|
4591
4731
|
*/
|
|
@@ -5377,7 +5517,7 @@ class GodotServer {
|
|
|
5377
5517
|
return this.createErrorResponse(`Failed to list scene nodes: ${stderr}`, ['Verify the scene file is valid']);
|
|
5378
5518
|
}
|
|
5379
5519
|
return {
|
|
5380
|
-
content: [{ type: 'text', text: stdout.trim() }],
|
|
5520
|
+
content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
|
|
5381
5521
|
};
|
|
5382
5522
|
}
|
|
5383
5523
|
catch (error) {
|
|
@@ -5414,7 +5554,7 @@ class GodotServer {
|
|
|
5414
5554
|
return this.createErrorResponse(`Failed to get node properties: ${stderr}`, ['Verify the node path is correct', 'Check if the node exists in the scene']);
|
|
5415
5555
|
}
|
|
5416
5556
|
return {
|
|
5417
|
-
content: [{ type: 'text', text: stdout.trim() }],
|
|
5557
|
+
content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
|
|
5418
5558
|
};
|
|
5419
5559
|
}
|
|
5420
5560
|
catch (error) {
|
|
@@ -5603,7 +5743,7 @@ class GodotServer {
|
|
|
5603
5743
|
return this.createErrorResponse(`Failed to get import status: ${stderr}`, ['Verify the resource path if specified']);
|
|
5604
5744
|
}
|
|
5605
5745
|
return {
|
|
5606
|
-
content: [{ type: 'text', text: stdout.trim() }],
|
|
5746
|
+
content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
|
|
5607
5747
|
};
|
|
5608
5748
|
}
|
|
5609
5749
|
catch (error) {
|
|
@@ -5638,7 +5778,7 @@ class GodotServer {
|
|
|
5638
5778
|
return this.createErrorResponse(`Failed to get import options: ${stderr}`, ['Verify the resource is an importable file type']);
|
|
5639
5779
|
}
|
|
5640
5780
|
return {
|
|
5641
|
-
content: [{ type: 'text', text: stdout.trim() }],
|
|
5781
|
+
content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
|
|
5642
5782
|
};
|
|
5643
5783
|
}
|
|
5644
5784
|
catch (error) {
|
|
@@ -5744,7 +5884,7 @@ class GodotServer {
|
|
|
5744
5884
|
return this.createErrorResponse(`Failed to list export presets: ${stderr}`, ['Check if export_presets.cfg exists in the project']);
|
|
5745
5885
|
}
|
|
5746
5886
|
return {
|
|
5747
|
-
content: [{ type: 'text', text: stdout.trim() }],
|
|
5887
|
+
content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
|
|
5748
5888
|
};
|
|
5749
5889
|
}
|
|
5750
5890
|
catch (error) {
|
|
@@ -5814,7 +5954,7 @@ class GodotServer {
|
|
|
5814
5954
|
return this.createErrorResponse(`Failed to validate project: ${stderr}`, ['Verify the project structure is valid']);
|
|
5815
5955
|
}
|
|
5816
5956
|
return {
|
|
5817
|
-
content: [{ type: 'text', text: stdout.trim() }],
|
|
5957
|
+
content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
|
|
5818
5958
|
};
|
|
5819
5959
|
}
|
|
5820
5960
|
catch (error) {
|
|
@@ -5850,7 +5990,7 @@ class GodotServer {
|
|
|
5850
5990
|
return this.createErrorResponse(`Failed to get dependencies: ${stderr}`, ['Verify the resource path is correct']);
|
|
5851
5991
|
}
|
|
5852
5992
|
return {
|
|
5853
|
-
content: [{ type: 'text', text: stdout.trim() }],
|
|
5993
|
+
content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
|
|
5854
5994
|
};
|
|
5855
5995
|
}
|
|
5856
5996
|
catch (error) {
|
|
@@ -5882,7 +6022,7 @@ class GodotServer {
|
|
|
5882
6022
|
return this.createErrorResponse(`Failed to find resource usages: ${stderr}`, ['Verify the resource path is correct']);
|
|
5883
6023
|
}
|
|
5884
6024
|
return {
|
|
5885
|
-
content: [{ type: 'text', text: stdout.trim() }],
|
|
6025
|
+
content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
|
|
5886
6026
|
};
|
|
5887
6027
|
}
|
|
5888
6028
|
catch (error) {
|
|
@@ -5914,7 +6054,7 @@ class GodotServer {
|
|
|
5914
6054
|
return this.createErrorResponse(`Failed to parse error log: ${stderr}`, ['Verify the log content or ensure godot.log exists']);
|
|
5915
6055
|
}
|
|
5916
6056
|
return {
|
|
5917
|
-
content: [{ type: 'text', text: stdout.trim() }],
|
|
6057
|
+
content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
|
|
5918
6058
|
};
|
|
5919
6059
|
}
|
|
5920
6060
|
catch (error) {
|
|
@@ -5945,7 +6085,7 @@ class GodotServer {
|
|
|
5945
6085
|
return this.createErrorResponse(`Failed to get project health: ${stderr}`, ['Verify the project structure']);
|
|
5946
6086
|
}
|
|
5947
6087
|
return {
|
|
5948
|
-
content: [{ type: 'text', text: stdout.trim() }],
|
|
6088
|
+
content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
|
|
5949
6089
|
};
|
|
5950
6090
|
}
|
|
5951
6091
|
catch (error) {
|
|
@@ -5979,7 +6119,7 @@ class GodotServer {
|
|
|
5979
6119
|
return this.createErrorResponse(`Failed to get project setting: ${stderr}`, ['Verify the setting path is correct']);
|
|
5980
6120
|
}
|
|
5981
6121
|
return {
|
|
5982
|
-
content: [{ type: 'text', text: stdout.trim() }],
|
|
6122
|
+
content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
|
|
5983
6123
|
};
|
|
5984
6124
|
}
|
|
5985
6125
|
catch (error) {
|
|
@@ -6103,7 +6243,7 @@ class GodotServer {
|
|
|
6103
6243
|
return this.createErrorResponse(`Failed to list autoloads: ${stderr}`, ['Verify the project structure']);
|
|
6104
6244
|
}
|
|
6105
6245
|
return {
|
|
6106
|
-
content: [{ type: 'text', text: stdout.trim() }],
|
|
6246
|
+
content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
|
|
6107
6247
|
};
|
|
6108
6248
|
}
|
|
6109
6249
|
catch (error) {
|
|
@@ -6260,7 +6400,7 @@ class GodotServer {
|
|
|
6260
6400
|
return this.createErrorResponse(`Failed to list connections: ${stderr}`, ['Verify the scene path is correct']);
|
|
6261
6401
|
}
|
|
6262
6402
|
return {
|
|
6263
|
-
content: [{ type: 'text', text: stdout.trim() }],
|
|
6403
|
+
content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
|
|
6264
6404
|
};
|
|
6265
6405
|
}
|
|
6266
6406
|
catch (error) {
|
|
@@ -6279,32 +6419,59 @@ class GodotServer {
|
|
|
6279
6419
|
return this.createErrorResponse('Project path is required', ['Provide a valid path to a Godot project directory']);
|
|
6280
6420
|
}
|
|
6281
6421
|
try {
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6422
|
+
const runtime = await this.handleRuntimeCommand('ping', {});
|
|
6423
|
+
const runtimeText = runtime?.content?.[0]?.text || '';
|
|
6424
|
+
let runtimePayload = null;
|
|
6425
|
+
try {
|
|
6426
|
+
runtimePayload = JSON.parse(runtimeText);
|
|
6427
|
+
}
|
|
6428
|
+
catch {
|
|
6429
|
+
runtimePayload = null;
|
|
6430
|
+
}
|
|
6431
|
+
const runtimeConnected = runtimePayload?.type === 'pong';
|
|
6432
|
+
if (runtimeConnected) {
|
|
6285
6433
|
return {
|
|
6286
6434
|
content: [{
|
|
6287
6435
|
type: 'text',
|
|
6288
6436
|
text: JSON.stringify({
|
|
6289
6437
|
connected: true,
|
|
6290
6438
|
status: 'running',
|
|
6291
|
-
|
|
6439
|
+
processActive: Boolean(this.activeProcess),
|
|
6440
|
+
runtimeAddon: 'connected',
|
|
6441
|
+
note: 'Godot runtime addon responded to ping. Use inspect_runtime_tree to explore.',
|
|
6442
|
+
runtimeResponse: runtimePayload,
|
|
6292
6443
|
}, null, 2),
|
|
6293
6444
|
}],
|
|
6294
6445
|
};
|
|
6295
6446
|
}
|
|
6296
|
-
|
|
6447
|
+
if (this.activeProcess) {
|
|
6297
6448
|
return {
|
|
6298
6449
|
content: [{
|
|
6299
6450
|
type: 'text',
|
|
6300
6451
|
text: JSON.stringify({
|
|
6301
6452
|
connected: false,
|
|
6302
|
-
status: '
|
|
6303
|
-
|
|
6453
|
+
status: 'process_running_runtime_disconnected',
|
|
6454
|
+
processActive: true,
|
|
6455
|
+
runtimeAddon: 'unreachable',
|
|
6456
|
+
note: 'A Godot process is active, but the runtime addon did not respond on port 7777.',
|
|
6457
|
+
runtimeResponse: runtimeText,
|
|
6304
6458
|
}, null, 2),
|
|
6305
6459
|
}],
|
|
6306
6460
|
};
|
|
6307
6461
|
}
|
|
6462
|
+
return {
|
|
6463
|
+
content: [{
|
|
6464
|
+
type: 'text',
|
|
6465
|
+
text: JSON.stringify({
|
|
6466
|
+
connected: false,
|
|
6467
|
+
status: 'not_running',
|
|
6468
|
+
processActive: false,
|
|
6469
|
+
runtimeAddon: 'unreachable',
|
|
6470
|
+
note: 'No active Godot process or runtime addon detected. Use run_project to start one.',
|
|
6471
|
+
runtimeResponse: runtimeText,
|
|
6472
|
+
}, null, 2),
|
|
6473
|
+
}],
|
|
6474
|
+
};
|
|
6308
6475
|
}
|
|
6309
6476
|
catch (error) {
|
|
6310
6477
|
return this.createErrorResponse(`Failed to get runtime status: ${error?.message || 'Unknown error'}`, ['Ensure Godot is installed correctly']);
|
|
@@ -6319,27 +6486,14 @@ class GodotServer {
|
|
|
6319
6486
|
return this.createErrorResponse('Project path is required', ['Provide a valid path to a Godot project directory']);
|
|
6320
6487
|
}
|
|
6321
6488
|
try {
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
return {
|
|
6328
|
-
content: [{
|
|
6329
|
-
type: 'text',
|
|
6330
|
-
text: JSON.stringify({
|
|
6331
|
-
status: 'running',
|
|
6332
|
-
nodePath: args.nodePath || '/',
|
|
6333
|
-
depth: args.depth || 3,
|
|
6334
|
-
note: 'Runtime tree inspection requires the godot_mcp_runtime addon. Current output shows debug logs.',
|
|
6335
|
-
recentOutput: this.activeProcess.output.slice(-20),
|
|
6336
|
-
recentErrors: this.activeProcess.errors.slice(-10),
|
|
6337
|
-
}, null, 2),
|
|
6338
|
-
}],
|
|
6339
|
-
};
|
|
6489
|
+
return await this.handleRuntimeCommand('get_tree', {
|
|
6490
|
+
root: args.nodePath || '/root',
|
|
6491
|
+
depth: args.depth || 3,
|
|
6492
|
+
include_properties: Boolean(args.includeProperties),
|
|
6493
|
+
});
|
|
6340
6494
|
}
|
|
6341
6495
|
catch (error) {
|
|
6342
|
-
return this.createErrorResponse(`Failed to inspect runtime tree: ${error?.message || 'Unknown error'}`, ['Ensure a Godot process is running']);
|
|
6496
|
+
return this.createErrorResponse(`Failed to inspect runtime tree: ${error?.message || 'Unknown error'}`, ['Ensure a Godot process is running with the runtime addon enabled']);
|
|
6343
6497
|
}
|
|
6344
6498
|
}
|
|
6345
6499
|
/**
|
|
@@ -6351,24 +6505,11 @@ class GodotServer {
|
|
|
6351
6505
|
return this.createErrorResponse('Missing required parameters', ['Provide projectPath, nodePath, property, and value']);
|
|
6352
6506
|
}
|
|
6353
6507
|
try {
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
content: [{
|
|
6360
|
-
type: 'text',
|
|
6361
|
-
text: JSON.stringify({
|
|
6362
|
-
status: 'not_implemented',
|
|
6363
|
-
note: 'Runtime property modification requires the godot_mcp_runtime addon installed in the running project.',
|
|
6364
|
-
requested: {
|
|
6365
|
-
nodePath: args.nodePath,
|
|
6366
|
-
property: args.property,
|
|
6367
|
-
value: args.value,
|
|
6368
|
-
},
|
|
6369
|
-
}, null, 2),
|
|
6370
|
-
}],
|
|
6371
|
-
};
|
|
6508
|
+
return await this.handleRuntimeCommand('set_property', {
|
|
6509
|
+
path: args.nodePath,
|
|
6510
|
+
property: args.property,
|
|
6511
|
+
value: args.value,
|
|
6512
|
+
});
|
|
6372
6513
|
}
|
|
6373
6514
|
catch (error) {
|
|
6374
6515
|
return this.createErrorResponse(`Failed to set runtime property: ${error?.message || 'Unknown error'}`, ['Ensure a Godot process is running with the runtime addon']);
|
|
@@ -6383,24 +6524,11 @@ class GodotServer {
|
|
|
6383
6524
|
return this.createErrorResponse('Missing required parameters', ['Provide projectPath, nodePath, and method']);
|
|
6384
6525
|
}
|
|
6385
6526
|
try {
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
content: [{
|
|
6392
|
-
type: 'text',
|
|
6393
|
-
text: JSON.stringify({
|
|
6394
|
-
status: 'not_implemented',
|
|
6395
|
-
note: 'Runtime method calling requires the godot_mcp_runtime addon installed in the running project.',
|
|
6396
|
-
requested: {
|
|
6397
|
-
nodePath: args.nodePath,
|
|
6398
|
-
method: args.method,
|
|
6399
|
-
args: args.args || [],
|
|
6400
|
-
},
|
|
6401
|
-
}, null, 2),
|
|
6402
|
-
}],
|
|
6403
|
-
};
|
|
6527
|
+
return await this.handleRuntimeCommand('call_method', {
|
|
6528
|
+
path: args.nodePath,
|
|
6529
|
+
method: args.method,
|
|
6530
|
+
args: Array.isArray(args.args) ? args.args : [],
|
|
6531
|
+
});
|
|
6404
6532
|
}
|
|
6405
6533
|
catch (error) {
|
|
6406
6534
|
return this.createErrorResponse(`Failed to call runtime method: ${error?.message || 'Unknown error'}`, ['Ensure a Godot process is running with the runtime addon']);
|
|
@@ -6415,24 +6543,9 @@ class GodotServer {
|
|
|
6415
6543
|
return this.createErrorResponse('Project path is required', ['Provide a valid path to a Godot project directory']);
|
|
6416
6544
|
}
|
|
6417
6545
|
try {
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
}
|
|
6421
|
-
// Basic metrics from process output
|
|
6422
|
-
return {
|
|
6423
|
-
content: [{
|
|
6424
|
-
type: 'text',
|
|
6425
|
-
text: JSON.stringify({
|
|
6426
|
-
status: 'running',
|
|
6427
|
-
metrics: {
|
|
6428
|
-
outputLines: this.activeProcess.output.length,
|
|
6429
|
-
errorLines: this.activeProcess.errors.length,
|
|
6430
|
-
note: 'Detailed metrics require the godot_mcp_runtime addon.',
|
|
6431
|
-
},
|
|
6432
|
-
requestedMetrics: args.metrics || 'all',
|
|
6433
|
-
}, null, 2),
|
|
6434
|
-
}],
|
|
6435
|
-
};
|
|
6546
|
+
return await this.handleRuntimeCommand('get_metrics', {
|
|
6547
|
+
metrics: Array.isArray(args.metrics) ? args.metrics : [],
|
|
6548
|
+
});
|
|
6436
6549
|
}
|
|
6437
6550
|
catch (error) {
|
|
6438
6551
|
return this.createErrorResponse(`Failed to get runtime metrics: ${error?.message || 'Unknown error'}`, ['Ensure a Godot process is running']);
|
|
@@ -6861,7 +6974,7 @@ class GodotServer {
|
|
|
6861
6974
|
return this.createErrorResponse(`Failed to list plugins: ${stderr}`, ['Verify the project structure']);
|
|
6862
6975
|
}
|
|
6863
6976
|
return {
|
|
6864
|
-
content: [{ type: 'text', text: stdout.trim() }],
|
|
6977
|
+
content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
|
|
6865
6978
|
};
|
|
6866
6979
|
}
|
|
6867
6980
|
catch (error) {
|
|
@@ -7000,7 +7113,7 @@ class GodotServer {
|
|
|
7000
7113
|
return this.createErrorResponse(`Failed to search project: ${stderr}`, ['Check if the query/regex pattern is valid']);
|
|
7001
7114
|
}
|
|
7002
7115
|
return {
|
|
7003
|
-
content: [{ type: 'text', text: stdout.trim() }],
|
|
7116
|
+
content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
|
|
7004
7117
|
};
|
|
7005
7118
|
}
|
|
7006
7119
|
catch (error) {
|
|
@@ -7787,7 +7900,16 @@ uniform float dissolve_amount : hint_range(0.0, 1.0) = 0.0;
|
|
|
7787
7900
|
params.instantiable_only = args.instantiableOnly;
|
|
7788
7901
|
if (args?.instantiable_only !== undefined)
|
|
7789
7902
|
params.instantiable_only = args.instantiable_only;
|
|
7790
|
-
|
|
7903
|
+
const { stdout, stderr } = await this.executeOperation('query_classes', params, projectPath);
|
|
7904
|
+
if (stderr && stderr.trim()) {
|
|
7905
|
+
return this.createErrorResponse(`Failed to query classes: ${stderr.trim()}`, [
|
|
7906
|
+
'Check the project path and ensure project.godot exists',
|
|
7907
|
+
'Verify the category/filter arguments are valid',
|
|
7908
|
+
]);
|
|
7909
|
+
}
|
|
7910
|
+
return {
|
|
7911
|
+
content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
|
|
7912
|
+
};
|
|
7791
7913
|
}
|
|
7792
7914
|
/**
|
|
7793
7915
|
* Handle the query_class_info tool — ClassDB introspection
|
|
@@ -7808,7 +7930,16 @@ uniform float dissolve_amount : hint_range(0.0, 1.0) = 0.0;
|
|
|
7808
7930
|
params.include_inherited = args.includeInherited;
|
|
7809
7931
|
if (args?.include_inherited !== undefined)
|
|
7810
7932
|
params.include_inherited = args.include_inherited;
|
|
7811
|
-
|
|
7933
|
+
const { stdout, stderr } = await this.executeOperation('query_class_info', params, projectPath);
|
|
7934
|
+
if (stderr && stderr.trim()) {
|
|
7935
|
+
return this.createErrorResponse(`Failed to query class info: ${stderr.trim()}`, [
|
|
7936
|
+
'Check that the class name exists in the current Godot version',
|
|
7937
|
+
'Verify the project path and ClassDB availability',
|
|
7938
|
+
]);
|
|
7939
|
+
}
|
|
7940
|
+
return {
|
|
7941
|
+
content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
|
|
7942
|
+
};
|
|
7812
7943
|
}
|
|
7813
7944
|
/**
|
|
7814
7945
|
* Handle the inspect_inheritance tool — ClassDB introspection
|
|
@@ -7822,9 +7953,17 @@ uniform float dissolve_amount : hint_range(0.0, 1.0) = 0.0;
|
|
|
7822
7953
|
if (!className) {
|
|
7823
7954
|
throw new McpError(ErrorCode.InvalidParams, 'className is required');
|
|
7824
7955
|
}
|
|
7825
|
-
|
|
7956
|
+
const { stdout, stderr } = await this.executeOperation('inspect_inheritance', {
|
|
7826
7957
|
class_name: className,
|
|
7827
7958
|
}, projectPath);
|
|
7959
|
+
if (stderr && stderr.trim()) {
|
|
7960
|
+
return this.createErrorResponse(`Failed to inspect inheritance: ${stderr.trim()}`, [
|
|
7961
|
+
'Check that the class name exists in the current Godot version',
|
|
7962
|
+
]);
|
|
7963
|
+
}
|
|
7964
|
+
return {
|
|
7965
|
+
content: [{ type: 'text', text: this.extractLastJsonLine(stdout) || stdout.trim() }],
|
|
7966
|
+
};
|
|
7828
7967
|
}
|
|
7829
7968
|
/**
|
|
7830
7969
|
* 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.4",
|
|
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,23 +17,27 @@
|
|
|
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",
|
|
29
|
-
"version:bump": "node scripts/bump-version.mjs"
|
|
32
|
+
"version:bump": "node scripts/bump-version.mjs",
|
|
33
|
+
"test:setup": "npm run build && node test-setup-hooks.mjs"
|
|
30
34
|
},
|
|
31
35
|
"engines": {
|
|
32
36
|
"node": ">=18"
|
|
33
37
|
},
|
|
34
38
|
"dependencies": {
|
|
35
|
-
"@modelcontextprotocol/sdk": "^1.27.
|
|
36
|
-
"axios": "^1.
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
40
|
+
"axios": "^1.13.6",
|
|
37
41
|
"fs-extra": "^11.2.0",
|
|
38
42
|
"ws": "^8.18.0"
|
|
39
43
|
},
|
|
@@ -43,14 +47,18 @@
|
|
|
43
47
|
"esbuild": "^0.24.2",
|
|
44
48
|
"typescript": "^5.3.3"
|
|
45
49
|
},
|
|
50
|
+
"overrides": {
|
|
51
|
+
"@hono/node-server": "^1.19.11",
|
|
52
|
+
"hono": "4.12.7"
|
|
53
|
+
},
|
|
46
54
|
"license": "MIT",
|
|
47
55
|
"repository": {
|
|
48
56
|
"type": "git",
|
|
49
|
-
"url": "https://github.com/HaD0Yun/godot-mcp.git"
|
|
57
|
+
"url": "git+https://github.com/HaD0Yun/Gopeak-godot-mcp.git"
|
|
50
58
|
},
|
|
51
|
-
"homepage": "https://github.com/HaD0Yun/godot-mcp#readme",
|
|
59
|
+
"homepage": "https://github.com/HaD0Yun/Gopeak-godot-mcp#readme",
|
|
52
60
|
"bugs": {
|
|
53
|
-
"url": "https://github.com/HaD0Yun/godot-mcp/issues"
|
|
61
|
+
"url": "https://github.com/HaD0Yun/Gopeak-godot-mcp/issues"
|
|
54
62
|
},
|
|
55
63
|
"author": {
|
|
56
64
|
"name": "HaD0Yun",
|