unreal-engine-mcp-server 0.5.0 → 0.5.1
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/.env.example +1 -1
- package/.github/release-drafter-config.yml +51 -0
- package/.github/workflows/greetings.yml +5 -1
- package/.github/workflows/labeler.yml +2 -1
- package/.github/workflows/publish-mcp.yml +1 -0
- package/.github/workflows/release-drafter.yml +1 -1
- package/.github/workflows/release.yml +3 -3
- package/CHANGELOG.md +71 -0
- package/CONTRIBUTING.md +1 -1
- package/GEMINI.md +115 -0
- package/Public/Plugin_setup_guide.mp4 +0 -0
- package/README.md +166 -200
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +4 -0
- package/dist/graphql/loaders.d.ts +64 -0
- package/dist/graphql/loaders.js +117 -0
- package/dist/graphql/resolvers.d.ts +3 -3
- package/dist/graphql/resolvers.js +33 -30
- package/dist/graphql/server.js +3 -1
- package/dist/graphql/types.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -2
- package/dist/server-setup.d.ts +0 -1
- package/dist/server-setup.js +0 -40
- package/dist/tools/actors.d.ts +40 -24
- package/dist/tools/actors.js +8 -2
- package/dist/tools/assets.d.ts +19 -71
- package/dist/tools/assets.js +28 -22
- package/dist/tools/base-tool.d.ts +4 -4
- package/dist/tools/base-tool.js +1 -1
- package/dist/tools/blueprint.d.ts +33 -61
- package/dist/tools/consolidated-tool-handlers.js +96 -110
- package/dist/tools/dynamic-handler-registry.d.ts +11 -9
- package/dist/tools/dynamic-handler-registry.js +17 -95
- package/dist/tools/editor.d.ts +19 -193
- package/dist/tools/editor.js +8 -0
- package/dist/tools/environment.d.ts +8 -14
- package/dist/tools/foliage.d.ts +18 -143
- package/dist/tools/foliage.js +4 -2
- package/dist/tools/handlers/actor-handlers.js +0 -5
- package/dist/tools/handlers/asset-handlers.js +454 -454
- package/dist/tools/landscape.d.ts +16 -116
- package/dist/tools/landscape.js +7 -3
- package/dist/tools/level.d.ts +22 -103
- package/dist/tools/level.js +24 -16
- package/dist/tools/lighting.js +5 -1
- package/dist/tools/materials.js +5 -1
- package/dist/tools/niagara.js +37 -2
- package/dist/tools/performance.d.ts +0 -1
- package/dist/tools/performance.js +0 -1
- package/dist/tools/physics.js +5 -1
- package/dist/tools/sequence.d.ts +24 -24
- package/dist/tools/sequence.js +13 -0
- package/dist/tools/ui.d.ts +0 -2
- package/dist/types/automation-responses.d.ts +115 -0
- package/dist/types/automation-responses.js +2 -0
- package/dist/types/responses.d.ts +249 -0
- package/dist/types/responses.js +2 -0
- package/dist/types/tool-interfaces.d.ts +135 -135
- package/dist/utils/command-validator.js +3 -2
- package/dist/utils/path-security.d.ts +2 -0
- package/dist/utils/path-security.js +24 -0
- package/dist/utils/response-factory.d.ts +4 -4
- package/dist/utils/response-factory.js +15 -21
- package/docs/Migration-Guide-v0.5.0.md +1 -9
- package/docs/testing-guide.md +2 -2
- package/package.json +12 -6
- package/scripts/run-all-tests.mjs +25 -20
- package/server.json +3 -2
- package/src/config.ts +1 -1
- package/src/constants.ts +7 -0
- package/src/graphql/loaders.ts +244 -0
- package/src/graphql/resolvers.ts +47 -49
- package/src/graphql/server.ts +3 -1
- package/src/graphql/types.ts +3 -0
- package/src/index.ts +15 -2
- package/src/resources/assets.ts +5 -4
- package/src/server-setup.ts +3 -37
- package/src/tools/actors.ts +36 -28
- package/src/tools/animation.ts +1 -0
- package/src/tools/assets.ts +74 -63
- package/src/tools/base-tool.ts +3 -3
- package/src/tools/blueprint.ts +59 -59
- package/src/tools/consolidated-tool-handlers.ts +129 -150
- package/src/tools/dynamic-handler-registry.ts +22 -140
- package/src/tools/editor.ts +39 -26
- package/src/tools/environment.ts +21 -27
- package/src/tools/foliage.ts +28 -25
- package/src/tools/handlers/actor-handlers.ts +2 -8
- package/src/tools/handlers/asset-handlers.ts +484 -484
- package/src/tools/handlers/sequence-handlers.ts +1 -1
- package/src/tools/landscape.ts +34 -28
- package/src/tools/level.ts +96 -76
- package/src/tools/lighting.ts +6 -1
- package/src/tools/materials.ts +8 -2
- package/src/tools/niagara.ts +44 -2
- package/src/tools/performance.ts +1 -2
- package/src/tools/physics.ts +7 -1
- package/src/tools/sequence.ts +41 -25
- package/src/tools/ui.ts +0 -2
- package/src/types/automation-responses.ts +119 -0
- package/src/types/responses.ts +355 -0
- package/src/types/tool-interfaces.ts +135 -135
- package/src/utils/command-validator.ts +3 -2
- package/src/utils/normalize.test.ts +162 -0
- package/src/utils/path-security.ts +43 -0
- package/src/utils/response-factory.ts +29 -24
- package/src/utils/safe-json.test.ts +90 -0
- package/src/utils/validation.test.ts +184 -0
- package/tests/test-animation.mjs +358 -33
- package/tests/test-asset-graph.mjs +311 -0
- package/tests/test-audio.mjs +314 -116
- package/tests/test-behavior-tree.mjs +327 -144
- package/tests/test-blueprint-graph.mjs +343 -12
- package/tests/test-control-editor.mjs +85 -53
- package/tests/test-graphql.mjs +58 -8
- package/tests/test-input.mjs +349 -0
- package/tests/test-inspect.mjs +291 -61
- package/tests/test-landscape.mjs +304 -48
- package/tests/test-lighting.mjs +428 -0
- package/tests/test-manage-level.mjs +70 -51
- package/tests/test-performance.mjs +539 -0
- package/tests/test-sequence.mjs +82 -46
- package/tests/test-system.mjs +72 -33
- package/tests/test-wasm.mjs +98 -8
- package/vitest.config.ts +35 -0
- package/.github/release-drafter.yml +0 -148
- package/dist/prompts/index.d.ts +0 -21
- package/dist/prompts/index.js +0 -217
- package/dist/tools/blueprint/helpers.d.ts +0 -29
- package/dist/tools/blueprint/helpers.js +0 -182
- package/src/prompts/index.ts +0 -249
- package/src/tools/blueprint/helpers.ts +0 -189
- package/tests/test-blueprint-events.mjs +0 -35
- package/tests/test-extra-tools.mjs +0 -38
- package/tests/test-render.mjs +0 -33
- package/tests/test-search-assets.mjs +0 -66
|
@@ -5,14 +5,15 @@ export class CommandValidator {
|
|
|
5
5
|
'viewmode visualizebuffer worldnormal',
|
|
6
6
|
'r.gpucrash',
|
|
7
7
|
'buildpaths',
|
|
8
|
-
'rebuildnavigation'
|
|
8
|
+
'rebuildnavigation',
|
|
9
|
+
'obj garbage', 'obj list', 'memreport'
|
|
9
10
|
];
|
|
10
11
|
static FORBIDDEN_TOKENS = [
|
|
11
12
|
'rm ', 'rm-', 'del ', 'format ', 'shutdown', 'reboot',
|
|
12
13
|
'rmdir', 'mklink', 'copy ', 'move ', 'start "', 'system(',
|
|
13
14
|
'import os', 'import subprocess', 'subprocess.', 'os.system',
|
|
14
15
|
'exec(', 'eval(', '__import__', 'import sys', 'import importlib',
|
|
15
|
-
'with open', 'open('
|
|
16
|
+
'with open', 'open(', 'write(', 'read('
|
|
16
17
|
];
|
|
17
18
|
static INVALID_PATTERNS = [
|
|
18
19
|
/^\d+$/,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function sanitizePath(path, allowedRoots = ['/Game', '/Engine']) {
|
|
2
|
+
if (!path || typeof path !== 'string') {
|
|
3
|
+
throw new Error('Invalid path: must be a non-empty string');
|
|
4
|
+
}
|
|
5
|
+
const trimmed = path.trim();
|
|
6
|
+
if (trimmed.length === 0) {
|
|
7
|
+
throw new Error('Invalid path: cannot be empty');
|
|
8
|
+
}
|
|
9
|
+
const normalized = trimmed.replace(/\\/g, '/');
|
|
10
|
+
if (normalized.includes('..')) {
|
|
11
|
+
throw new Error('Invalid path: directory traversal (..) is not allowed');
|
|
12
|
+
}
|
|
13
|
+
const isAllowed = allowedRoots.some(root => normalized.toLowerCase() === root.toLowerCase() ||
|
|
14
|
+
normalized.toLowerCase().startsWith(`${root.toLowerCase()}/`));
|
|
15
|
+
if (!isAllowed) {
|
|
16
|
+
throw new Error(`Invalid path: must start with one of [${allowedRoots.join(', ')}]`);
|
|
17
|
+
}
|
|
18
|
+
const invalidChars = /[<>:"|?*\x00-\x1f]/;
|
|
19
|
+
if (invalidChars.test(normalized)) {
|
|
20
|
+
throw new Error('Invalid path: contains illegal characters');
|
|
21
|
+
}
|
|
22
|
+
return normalized;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=path-security.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { StandardActionResponse } from '../types/tool-interfaces.js';
|
|
2
2
|
export declare class ResponseFactory {
|
|
3
|
-
static success(
|
|
4
|
-
static error(
|
|
5
|
-
static
|
|
3
|
+
static success(data: any, message?: string): StandardActionResponse;
|
|
4
|
+
static error(error: any, defaultMessage?: string): StandardActionResponse;
|
|
5
|
+
static validationError(message: string): StandardActionResponse;
|
|
6
6
|
}
|
|
7
7
|
//# sourceMappingURL=response-factory.d.ts.map
|
|
@@ -1,32 +1,26 @@
|
|
|
1
|
+
import { cleanObject } from './safe-json.js';
|
|
1
2
|
export class ResponseFactory {
|
|
2
|
-
static success(
|
|
3
|
-
const content = [{ type: 'text', text: message }];
|
|
4
|
-
if (data) {
|
|
5
|
-
content.push({
|
|
6
|
-
type: 'text',
|
|
7
|
-
text: JSON.stringify(data, null, 2)
|
|
8
|
-
});
|
|
9
|
-
}
|
|
3
|
+
static success(data, message = 'Operation successful') {
|
|
10
4
|
return {
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
success: true,
|
|
6
|
+
message,
|
|
7
|
+
data: cleanObject(data)
|
|
13
8
|
};
|
|
14
9
|
}
|
|
15
|
-
static error(
|
|
16
|
-
const
|
|
17
|
-
|
|
10
|
+
static error(error, defaultMessage = 'Operation failed') {
|
|
11
|
+
const errorMessage = error instanceof Error ? error.message : String(error || defaultMessage);
|
|
12
|
+
console.error('[ResponseFactory] Error:', error);
|
|
18
13
|
return {
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
success: false,
|
|
15
|
+
message: errorMessage,
|
|
16
|
+
data: null
|
|
21
17
|
};
|
|
22
18
|
}
|
|
23
|
-
static
|
|
19
|
+
static validationError(message) {
|
|
24
20
|
return {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}],
|
|
29
|
-
isError: false
|
|
21
|
+
success: false,
|
|
22
|
+
message: `Validation Error: ${message}`,
|
|
23
|
+
data: null
|
|
30
24
|
};
|
|
31
25
|
}
|
|
32
26
|
}
|
|
@@ -652,18 +652,10 @@ echo $WASM_ENABLED # Should be "true"
|
|
|
652
652
|
### Documentation
|
|
653
653
|
- [GraphQL API Documentation](GraphQL-API.md)
|
|
654
654
|
- [WebAssembly Integration Guide](WebAssembly-Integration.md)
|
|
655
|
-
- Added **GraphQL API** (optional) for complex queries.
|
|
656
|
-
- Disabled by default (`GRAPHQL_ENABLED=false`).
|
|
657
|
-
- Uses Apollo Server.
|
|
658
|
-
- Integrated with **Apollo Sandbox** (local IDE) or [Apollo Studio](https://studio.apollographql.com). - GraphQL platform
|
|
659
|
-
- [Postman](https://www.postman.com/) - API testing
|
|
660
|
-
|
|
661
|
-
### Learning
|
|
662
|
-
- [GraphQL.org](https://graphql.org/) - Official GraphQL documentation
|
|
663
|
-
- [WebAssembly.org](https://webassembly.org/) - WebAssembly specification
|
|
664
655
|
|
|
665
656
|
### Tools
|
|
666
657
|
- [GraphiQL](https://github.com/graphql/graphiql) - In-browser GraphQL IDE
|
|
658
|
+
- [Apollo Studio](https://studio.apollographql.com) - GraphQL platform
|
|
667
659
|
- [Postman](https://www.postman.com/) - API testing
|
|
668
660
|
|
|
669
661
|
### Learning
|
package/docs/testing-guide.md
CHANGED
|
@@ -230,7 +230,7 @@ This prevents false positives from connection errors.
|
|
|
230
230
|
**Problem**: Unreal Engine is not running or not accessible
|
|
231
231
|
|
|
232
232
|
**Solutions**:
|
|
233
|
-
1. Launch Unreal Engine 5.
|
|
233
|
+
1. Launch Unreal Engine 5.0-5.7
|
|
234
234
|
2. Open your project
|
|
235
235
|
3. Verify Unreal Engine is running with the MCP plugin enabled
|
|
236
236
|
4. Check `DefaultEngine.ini` configuration
|
|
@@ -417,7 +417,7 @@ For issues with:
|
|
|
417
417
|
## Next Steps
|
|
418
418
|
|
|
419
419
|
1. **Build the server**: `npm run build`
|
|
420
|
-
2. **Start Unreal Engine 5.
|
|
420
|
+
2. **Start Unreal Engine 5.0-5.7**
|
|
421
421
|
3. **Run your first test**: `npm run test:system`
|
|
422
422
|
4. **Review the report**: Check `tests/reports/` folder
|
|
423
423
|
5. **Test other tools**: Run `npm run test:<toolname>` for each tool you use
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unreal-engine-mcp-server",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"mcpName": "io.github.ChiR24/unreal-engine-mcp",
|
|
5
5
|
"description": "A comprehensive Model Context Protocol (MCP) server that enables AI assistants to control Unreal Engine via native automation bridge. Built with TypeScript and designed for game development automation.",
|
|
6
6
|
"type": "module",
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
"automation:sync": "node scripts/sync-mcp-plugin.js",
|
|
27
27
|
"clean:tmp": "node scripts/clean-tmp.js",
|
|
28
28
|
"test": "node scripts/run-all-tests.mjs",
|
|
29
|
+
"test:unit": "vitest run",
|
|
30
|
+
"test:unit:watch": "vitest",
|
|
31
|
+
"test:unit:coverage": "vitest run --coverage",
|
|
29
32
|
"test:all": "node scripts/run-all-tests.mjs",
|
|
30
33
|
"test:manage_asset": "node tests/test-manage-asset.mjs",
|
|
31
34
|
"test:plugin-handshake": "node tests/test-plugin-handshake.mjs",
|
|
@@ -43,12 +46,15 @@
|
|
|
43
46
|
"test:console_command": "node tests/test-console-command.mjs",
|
|
44
47
|
"test:inspect": "node tests/test-inspect.mjs",
|
|
45
48
|
"test:asset_advanced": "node tests/test-asset-advanced.mjs",
|
|
46
|
-
"test:render": "node tests/test-render.mjs",
|
|
47
49
|
"test:world_partition": "node tests/test-world-partition.mjs",
|
|
48
50
|
"test:no-inline-python": "node tests/test-no-inline-python.mjs",
|
|
49
51
|
"test:graphql": "node tests/test-graphql.mjs",
|
|
50
52
|
"test:audio": "node tests/test-audio.mjs",
|
|
51
53
|
"test:behavior_tree": "node tests/test-behavior-tree.mjs",
|
|
54
|
+
"test:lighting": "node tests/test-lighting.mjs",
|
|
55
|
+
"test:performance": "node tests/test-performance.mjs",
|
|
56
|
+
"test:input": "node tests/test-input.mjs",
|
|
57
|
+
"test:asset_graph": "node tests/test-asset-graph.mjs",
|
|
52
58
|
"test:wasm": "node tests/test-wasm.mjs",
|
|
53
59
|
"build:wasm": "echo 'Building WebAssembly module...' && cd wasm && wasm-pack build --target web --out-dir ../src/wasm/pkg && cd .. && node scripts/patch-wasm.js",
|
|
54
60
|
"test:wasm:all": "npm run build:core && npm run build:wasm && node tests/test-wasm.mjs",
|
|
@@ -76,13 +82,11 @@
|
|
|
76
82
|
"@graphql-tools/utils": "^10.11.0",
|
|
77
83
|
"@modelcontextprotocol/sdk": "^1.25.0",
|
|
78
84
|
"ajv": "^8.17.1",
|
|
79
|
-
"
|
|
85
|
+
"dataloader": "^2.2.3",
|
|
80
86
|
"dotenv": "^17.2.3",
|
|
81
87
|
"graphql": "^16.12.0",
|
|
82
88
|
"graphql-yoga": "^5.17.1",
|
|
83
|
-
"json5": "^2.2.3",
|
|
84
89
|
"ws": "^8.18.3",
|
|
85
|
-
"yargs": "^18.0.0",
|
|
86
90
|
"zod": "^4.2.1"
|
|
87
91
|
},
|
|
88
92
|
"devDependencies": {
|
|
@@ -91,9 +95,11 @@
|
|
|
91
95
|
"@types/ws": "^8.18.1",
|
|
92
96
|
"@typescript-eslint/eslint-plugin": "^8.50.0",
|
|
93
97
|
"@typescript-eslint/parser": "^8.49.0",
|
|
98
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
94
99
|
"eslint": "^9.39.2",
|
|
95
100
|
"rimraf": "^6.1.2",
|
|
96
101
|
"ts-node": "^10.9.2",
|
|
97
|
-
"typescript": "^5.9.3"
|
|
102
|
+
"typescript": "^5.9.3",
|
|
103
|
+
"vitest": "^4.0.16"
|
|
98
104
|
}
|
|
99
105
|
}
|
|
@@ -2,27 +2,32 @@
|
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
|
|
4
4
|
const tests = [
|
|
5
|
-
'test:control_actor', //
|
|
6
|
-
'test:control_editor', //
|
|
7
|
-
'test:manage_level', //
|
|
8
|
-
'test:animation',
|
|
9
|
-
'test:materials',
|
|
10
|
-
'test:niagara',
|
|
11
|
-
'test:landscape',
|
|
12
|
-
'test:sequence', //
|
|
13
|
-
'test:system',
|
|
14
|
-
'test:console_command',
|
|
15
|
-
'test:inspect',
|
|
16
|
-
'test:manage_asset',
|
|
17
|
-
'test:blueprint',
|
|
18
|
-
'test:blueprint_graph',
|
|
19
|
-
|
|
20
|
-
'test:
|
|
5
|
+
'test:control_actor', // 1
|
|
6
|
+
'test:control_editor', // 1
|
|
7
|
+
'test:manage_level', // crashed 1
|
|
8
|
+
'test:animation',
|
|
9
|
+
'test:materials',
|
|
10
|
+
'test:niagara',
|
|
11
|
+
'test:landscape',
|
|
12
|
+
'test:sequence', // crashed 1
|
|
13
|
+
'test:system',
|
|
14
|
+
'test:console_command',
|
|
15
|
+
'test:inspect',
|
|
16
|
+
'test:manage_asset',
|
|
17
|
+
'test:blueprint',
|
|
18
|
+
'test:blueprint_graph',
|
|
19
|
+
'test:audio',
|
|
20
|
+
'test:behavior_tree',
|
|
21
|
+
'test:lighting',
|
|
22
|
+
'test:performance',
|
|
23
|
+
'test:input',
|
|
24
|
+
'test:asset_graph',
|
|
25
|
+
'test:graphql',
|
|
26
|
+
'test:wasm:all',
|
|
21
27
|
'test:no-inline-python',
|
|
22
|
-
'test:plugin-handshake',
|
|
23
|
-
'test:asset_advanced',
|
|
24
|
-
'test:
|
|
25
|
-
'test:world_partition' // Pass
|
|
28
|
+
'test:plugin-handshake',
|
|
29
|
+
'test:asset_advanced',
|
|
30
|
+
'test:world_partition'
|
|
26
31
|
];
|
|
27
32
|
|
|
28
33
|
const isWindows = process.platform === 'win32';
|
package/server.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/modelcontextprotocol/registry/main/schema.json",
|
|
2
3
|
"name": "io.github.ChiR24/unreal-engine-mcp",
|
|
3
4
|
"description": "MCP server for Unreal Engine 5 with 17 tools for game development automation.",
|
|
4
|
-
"version": "0.5.
|
|
5
|
+
"version": "0.5.1",
|
|
5
6
|
"packages": [
|
|
6
7
|
{
|
|
7
8
|
"registryType": "npm",
|
|
8
9
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
9
10
|
"identifier": "unreal-engine-mcp-server",
|
|
10
|
-
"version": "0.5.
|
|
11
|
+
"version": "0.5.1",
|
|
11
12
|
"transport": {
|
|
12
13
|
"type": "stdio"
|
|
13
14
|
},
|
package/src/config.ts
CHANGED
|
@@ -40,7 +40,7 @@ export const EnvSchema = z.object({
|
|
|
40
40
|
// Unreal Settings
|
|
41
41
|
UE_PROJECT_PATH: z.string().optional(),
|
|
42
42
|
UE_EDITOR_EXE: z.string().optional(),
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
|
|
45
45
|
// Connection Settings
|
|
46
46
|
MCP_AUTOMATION_PORT: z.preprocess((v) => stringToNumber(v, 8091), z.number().default(8091)),
|
package/src/constants.ts
CHANGED
|
@@ -9,4 +9,11 @@ export const DEFAULT_MAX_PENDING_REQUESTS = 25;
|
|
|
9
9
|
export const DEFAULT_TIME_OF_DAY = 9;
|
|
10
10
|
export const DEFAULT_SUN_INTENSITY = 10000;
|
|
11
11
|
export const DEFAULT_SKYLIGHT_INTENSITY = 1;
|
|
12
|
+
|
|
12
13
|
export const DEFAULT_SCREENSHOT_RESOLUTION = '1920x1080';
|
|
14
|
+
|
|
15
|
+
// Operation Timeouts
|
|
16
|
+
export const DEFAULT_OPERATION_TIMEOUT_MS = 30000;
|
|
17
|
+
export const DEFAULT_ASSET_OP_TIMEOUT_MS = 60000;
|
|
18
|
+
export const EXTENDED_ASSET_OP_TIMEOUT_MS = 120000;
|
|
19
|
+
export const LONG_RUNNING_OP_TIMEOUT_MS = 300000;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataLoader instances for GraphQL N+1 query optimization
|
|
3
|
+
*
|
|
4
|
+
* DataLoaders batch and cache requests within a single GraphQL request,
|
|
5
|
+
* preventing the N+1 query problem when resolving nested fields.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import DataLoader from 'dataloader';
|
|
9
|
+
import type { AutomationBridge } from '../automation/index.js';
|
|
10
|
+
import { Logger } from '../utils/logger.js';
|
|
11
|
+
|
|
12
|
+
const log = new Logger('GraphQL:Loaders');
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Types
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
export interface Actor {
|
|
19
|
+
name: string;
|
|
20
|
+
label?: string;
|
|
21
|
+
class?: string;
|
|
22
|
+
path?: string;
|
|
23
|
+
location?: { x: number; y: number; z: number };
|
|
24
|
+
rotation?: { pitch: number; yaw: number; roll: number };
|
|
25
|
+
scale?: { x: number; y: number; z: number };
|
|
26
|
+
tags?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Asset {
|
|
30
|
+
name: string;
|
|
31
|
+
path: string;
|
|
32
|
+
class?: string;
|
|
33
|
+
packagePath?: string;
|
|
34
|
+
size?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface Blueprint {
|
|
38
|
+
name: string;
|
|
39
|
+
path: string;
|
|
40
|
+
parentClass?: string;
|
|
41
|
+
variables?: Array<{ name: string; type: string; defaultValue?: unknown }>;
|
|
42
|
+
functions?: Array<{ name: string; parameters?: Array<{ name: string; type: string }> }>;
|
|
43
|
+
components?: Array<{ name: string; type: string }>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Loader Factory
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
export interface GraphQLLoaders {
|
|
51
|
+
actorLoader: DataLoader<string, Actor | null>;
|
|
52
|
+
assetLoader: DataLoader<string, Asset | null>;
|
|
53
|
+
blueprintLoader: DataLoader<string, Blueprint | null>;
|
|
54
|
+
actorComponentsLoader: DataLoader<string, Array<{ name: string; type: string }> | null>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Creates DataLoader instances for a GraphQL request context.
|
|
59
|
+
*
|
|
60
|
+
* Each GraphQL request should have its own set of loaders to ensure
|
|
61
|
+
* proper request-scoped caching and batching.
|
|
62
|
+
*/
|
|
63
|
+
export function createLoaders(automationBridge: AutomationBridge): GraphQLLoaders {
|
|
64
|
+
return {
|
|
65
|
+
/**
|
|
66
|
+
* Batches actor fetches by name
|
|
67
|
+
*/
|
|
68
|
+
actorLoader: new DataLoader<string, Actor | null>(
|
|
69
|
+
async (names: readonly string[]) => {
|
|
70
|
+
log.debug(`Batching actor fetch for ${names.length} actors`);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Use batch fetch if available, otherwise fall back to individual fetches
|
|
74
|
+
const result = await automationBridge.sendAutomationRequest<{
|
|
75
|
+
success: boolean;
|
|
76
|
+
actors?: Actor[];
|
|
77
|
+
}>('control_actor', {
|
|
78
|
+
action: 'batch_get',
|
|
79
|
+
actorNames: [...names]
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (result.success && result.actors) {
|
|
83
|
+
// Map results back to input order
|
|
84
|
+
return names.map(name =>
|
|
85
|
+
result.actors?.find(a => a.name === name || a.label === name) ?? null
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
log.debug('Batch fetch not supported, falling back to individual fetches');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Fallback: fetch individually
|
|
93
|
+
const results = await Promise.all(
|
|
94
|
+
names.map(async (name) => {
|
|
95
|
+
try {
|
|
96
|
+
const result = await automationBridge.sendAutomationRequest<{
|
|
97
|
+
success: boolean;
|
|
98
|
+
actor?: Actor;
|
|
99
|
+
}>('control_actor', {
|
|
100
|
+
action: 'find_by_name',
|
|
101
|
+
actorName: name
|
|
102
|
+
});
|
|
103
|
+
return result.success ? (result.actor ?? null) : null;
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
);
|
|
109
|
+
return results;
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
cache: true,
|
|
113
|
+
maxBatchSize: 50
|
|
114
|
+
}
|
|
115
|
+
),
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Batches asset fetches by path
|
|
119
|
+
*/
|
|
120
|
+
assetLoader: new DataLoader<string, Asset | null>(
|
|
121
|
+
async (paths: readonly string[]) => {
|
|
122
|
+
log.debug(`Batching asset fetch for ${paths.length} assets`);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const result = await automationBridge.sendAutomationRequest<{
|
|
126
|
+
success: boolean;
|
|
127
|
+
assets?: Asset[];
|
|
128
|
+
}>('manage_asset', {
|
|
129
|
+
action: 'batch_get',
|
|
130
|
+
assetPaths: [...paths]
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (result.success && result.assets) {
|
|
134
|
+
return paths.map(path =>
|
|
135
|
+
result.assets?.find(a => a.path === path) ?? null
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
log.debug('Batch asset fetch not supported');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Fallback: check existence individually
|
|
143
|
+
const results = await Promise.all(
|
|
144
|
+
paths.map(async (path) => {
|
|
145
|
+
try {
|
|
146
|
+
const result = await automationBridge.sendAutomationRequest<{
|
|
147
|
+
success: boolean;
|
|
148
|
+
exists?: boolean;
|
|
149
|
+
asset?: Asset;
|
|
150
|
+
}>('manage_asset', {
|
|
151
|
+
action: 'exists',
|
|
152
|
+
assetPath: path
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (result.success && result.exists) {
|
|
156
|
+
return result.asset ?? { name: path.split('/').pop() || '', path };
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
);
|
|
164
|
+
return results;
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
cache: true,
|
|
168
|
+
maxBatchSize: 100
|
|
169
|
+
}
|
|
170
|
+
),
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Batches blueprint fetches by path
|
|
174
|
+
*/
|
|
175
|
+
blueprintLoader: new DataLoader<string, Blueprint | null>(
|
|
176
|
+
async (paths: readonly string[]) => {
|
|
177
|
+
log.debug(`Batching blueprint fetch for ${paths.length} blueprints`);
|
|
178
|
+
|
|
179
|
+
const results = await Promise.all(
|
|
180
|
+
paths.map(async (path) => {
|
|
181
|
+
try {
|
|
182
|
+
const result = await automationBridge.sendAutomationRequest<{
|
|
183
|
+
success: boolean;
|
|
184
|
+
blueprint?: Blueprint;
|
|
185
|
+
}>('manage_blueprint', {
|
|
186
|
+
action: 'get_blueprint',
|
|
187
|
+
blueprintPath: path
|
|
188
|
+
});
|
|
189
|
+
return result.success ? (result.blueprint ?? null) : null;
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
return results;
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
cache: true,
|
|
199
|
+
maxBatchSize: 20
|
|
200
|
+
}
|
|
201
|
+
),
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Batches actor component fetches
|
|
205
|
+
*/
|
|
206
|
+
actorComponentsLoader: new DataLoader<string, Array<{ name: string; type: string }> | null>(
|
|
207
|
+
async (actorNames: readonly string[]) => {
|
|
208
|
+
log.debug(`Batching component fetch for ${actorNames.length} actors`);
|
|
209
|
+
|
|
210
|
+
const results = await Promise.all(
|
|
211
|
+
actorNames.map(async (actorName) => {
|
|
212
|
+
try {
|
|
213
|
+
const result = await automationBridge.sendAutomationRequest<{
|
|
214
|
+
success: boolean;
|
|
215
|
+
components?: Array<{ name: string; type: string }>;
|
|
216
|
+
}>('control_actor', {
|
|
217
|
+
action: 'get_components',
|
|
218
|
+
actorName
|
|
219
|
+
});
|
|
220
|
+
return result.success ? (result.components ?? null) : null;
|
|
221
|
+
} catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
return results;
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
cache: true,
|
|
230
|
+
maxBatchSize: 30
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Clears all loader caches (useful between mutations)
|
|
238
|
+
*/
|
|
239
|
+
export function clearLoaders(loaders: GraphQLLoaders): void {
|
|
240
|
+
loaders.actorLoader.clearAll();
|
|
241
|
+
loaders.assetLoader.clearAll();
|
|
242
|
+
loaders.blueprintLoader.clearAll();
|
|
243
|
+
loaders.actorComponentsLoader.clearAll();
|
|
244
|
+
}
|