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.
Files changed (139) hide show
  1. package/.env.example +1 -1
  2. package/.github/release-drafter-config.yml +51 -0
  3. package/.github/workflows/greetings.yml +5 -1
  4. package/.github/workflows/labeler.yml +2 -1
  5. package/.github/workflows/publish-mcp.yml +1 -0
  6. package/.github/workflows/release-drafter.yml +1 -1
  7. package/.github/workflows/release.yml +3 -3
  8. package/CHANGELOG.md +71 -0
  9. package/CONTRIBUTING.md +1 -1
  10. package/GEMINI.md +115 -0
  11. package/Public/Plugin_setup_guide.mp4 +0 -0
  12. package/README.md +166 -200
  13. package/dist/config.d.ts +0 -1
  14. package/dist/config.js +0 -1
  15. package/dist/constants.d.ts +4 -0
  16. package/dist/constants.js +4 -0
  17. package/dist/graphql/loaders.d.ts +64 -0
  18. package/dist/graphql/loaders.js +117 -0
  19. package/dist/graphql/resolvers.d.ts +3 -3
  20. package/dist/graphql/resolvers.js +33 -30
  21. package/dist/graphql/server.js +3 -1
  22. package/dist/graphql/types.d.ts +2 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +13 -2
  25. package/dist/server-setup.d.ts +0 -1
  26. package/dist/server-setup.js +0 -40
  27. package/dist/tools/actors.d.ts +40 -24
  28. package/dist/tools/actors.js +8 -2
  29. package/dist/tools/assets.d.ts +19 -71
  30. package/dist/tools/assets.js +28 -22
  31. package/dist/tools/base-tool.d.ts +4 -4
  32. package/dist/tools/base-tool.js +1 -1
  33. package/dist/tools/blueprint.d.ts +33 -61
  34. package/dist/tools/consolidated-tool-handlers.js +96 -110
  35. package/dist/tools/dynamic-handler-registry.d.ts +11 -9
  36. package/dist/tools/dynamic-handler-registry.js +17 -95
  37. package/dist/tools/editor.d.ts +19 -193
  38. package/dist/tools/editor.js +8 -0
  39. package/dist/tools/environment.d.ts +8 -14
  40. package/dist/tools/foliage.d.ts +18 -143
  41. package/dist/tools/foliage.js +4 -2
  42. package/dist/tools/handlers/actor-handlers.js +0 -5
  43. package/dist/tools/handlers/asset-handlers.js +454 -454
  44. package/dist/tools/landscape.d.ts +16 -116
  45. package/dist/tools/landscape.js +7 -3
  46. package/dist/tools/level.d.ts +22 -103
  47. package/dist/tools/level.js +24 -16
  48. package/dist/tools/lighting.js +5 -1
  49. package/dist/tools/materials.js +5 -1
  50. package/dist/tools/niagara.js +37 -2
  51. package/dist/tools/performance.d.ts +0 -1
  52. package/dist/tools/performance.js +0 -1
  53. package/dist/tools/physics.js +5 -1
  54. package/dist/tools/sequence.d.ts +24 -24
  55. package/dist/tools/sequence.js +13 -0
  56. package/dist/tools/ui.d.ts +0 -2
  57. package/dist/types/automation-responses.d.ts +115 -0
  58. package/dist/types/automation-responses.js +2 -0
  59. package/dist/types/responses.d.ts +249 -0
  60. package/dist/types/responses.js +2 -0
  61. package/dist/types/tool-interfaces.d.ts +135 -135
  62. package/dist/utils/command-validator.js +3 -2
  63. package/dist/utils/path-security.d.ts +2 -0
  64. package/dist/utils/path-security.js +24 -0
  65. package/dist/utils/response-factory.d.ts +4 -4
  66. package/dist/utils/response-factory.js +15 -21
  67. package/docs/Migration-Guide-v0.5.0.md +1 -9
  68. package/docs/testing-guide.md +2 -2
  69. package/package.json +12 -6
  70. package/scripts/run-all-tests.mjs +25 -20
  71. package/server.json +3 -2
  72. package/src/config.ts +1 -1
  73. package/src/constants.ts +7 -0
  74. package/src/graphql/loaders.ts +244 -0
  75. package/src/graphql/resolvers.ts +47 -49
  76. package/src/graphql/server.ts +3 -1
  77. package/src/graphql/types.ts +3 -0
  78. package/src/index.ts +15 -2
  79. package/src/resources/assets.ts +5 -4
  80. package/src/server-setup.ts +3 -37
  81. package/src/tools/actors.ts +36 -28
  82. package/src/tools/animation.ts +1 -0
  83. package/src/tools/assets.ts +74 -63
  84. package/src/tools/base-tool.ts +3 -3
  85. package/src/tools/blueprint.ts +59 -59
  86. package/src/tools/consolidated-tool-handlers.ts +129 -150
  87. package/src/tools/dynamic-handler-registry.ts +22 -140
  88. package/src/tools/editor.ts +39 -26
  89. package/src/tools/environment.ts +21 -27
  90. package/src/tools/foliage.ts +28 -25
  91. package/src/tools/handlers/actor-handlers.ts +2 -8
  92. package/src/tools/handlers/asset-handlers.ts +484 -484
  93. package/src/tools/handlers/sequence-handlers.ts +1 -1
  94. package/src/tools/landscape.ts +34 -28
  95. package/src/tools/level.ts +96 -76
  96. package/src/tools/lighting.ts +6 -1
  97. package/src/tools/materials.ts +8 -2
  98. package/src/tools/niagara.ts +44 -2
  99. package/src/tools/performance.ts +1 -2
  100. package/src/tools/physics.ts +7 -1
  101. package/src/tools/sequence.ts +41 -25
  102. package/src/tools/ui.ts +0 -2
  103. package/src/types/automation-responses.ts +119 -0
  104. package/src/types/responses.ts +355 -0
  105. package/src/types/tool-interfaces.ts +135 -135
  106. package/src/utils/command-validator.ts +3 -2
  107. package/src/utils/normalize.test.ts +162 -0
  108. package/src/utils/path-security.ts +43 -0
  109. package/src/utils/response-factory.ts +29 -24
  110. package/src/utils/safe-json.test.ts +90 -0
  111. package/src/utils/validation.test.ts +184 -0
  112. package/tests/test-animation.mjs +358 -33
  113. package/tests/test-asset-graph.mjs +311 -0
  114. package/tests/test-audio.mjs +314 -116
  115. package/tests/test-behavior-tree.mjs +327 -144
  116. package/tests/test-blueprint-graph.mjs +343 -12
  117. package/tests/test-control-editor.mjs +85 -53
  118. package/tests/test-graphql.mjs +58 -8
  119. package/tests/test-input.mjs +349 -0
  120. package/tests/test-inspect.mjs +291 -61
  121. package/tests/test-landscape.mjs +304 -48
  122. package/tests/test-lighting.mjs +428 -0
  123. package/tests/test-manage-level.mjs +70 -51
  124. package/tests/test-performance.mjs +539 -0
  125. package/tests/test-sequence.mjs +82 -46
  126. package/tests/test-system.mjs +72 -33
  127. package/tests/test-wasm.mjs +98 -8
  128. package/vitest.config.ts +35 -0
  129. package/.github/release-drafter.yml +0 -148
  130. package/dist/prompts/index.d.ts +0 -21
  131. package/dist/prompts/index.js +0 -217
  132. package/dist/tools/blueprint/helpers.d.ts +0 -29
  133. package/dist/tools/blueprint/helpers.js +0 -182
  134. package/src/prompts/index.ts +0 -249
  135. package/src/tools/blueprint/helpers.ts +0 -189
  136. package/tests/test-blueprint-events.mjs +0 -35
  137. package/tests/test-extra-tools.mjs +0 -38
  138. package/tests/test-render.mjs +0 -33
  139. 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,2 @@
1
+ export declare function sanitizePath(path: string, allowedRoots?: string[]): string;
2
+ //# sourceMappingURL=path-security.d.ts.map
@@ -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 { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
1
+ import { StandardActionResponse } from '../types/tool-interfaces.js';
2
2
  export declare class ResponseFactory {
3
- static success(message: string, data?: unknown): CallToolResult;
4
- static error(message: string, error?: unknown): CallToolResult;
5
- static json(data: unknown): CallToolResult;
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(message, data) {
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
- content,
12
- isError: false
5
+ success: true,
6
+ message,
7
+ data: cleanObject(data)
13
8
  };
14
9
  }
15
- static error(message, error) {
16
- const errorText = error instanceof Error ? error.message : String(error);
17
- const fullMessage = error ? `${message}: ${errorText}` : message;
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
- content: [{ type: 'text', text: fullMessage }],
20
- isError: true
14
+ success: false,
15
+ message: errorMessage,
16
+ data: null
21
17
  };
22
18
  }
23
- static json(data) {
19
+ static validationError(message) {
24
20
  return {
25
- content: [{
26
- type: 'text',
27
- text: JSON.stringify(data, null, 2)
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
@@ -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.6
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.6**
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.0",
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
- "axios": "^1.13.2",
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', // Pass
6
- 'test:control_editor', // Pass
7
- 'test:manage_level', // Pass
8
- 'test:animation', // Pass
9
- 'test:materials', // Pass
10
- 'test:niagara', // Pass
11
- 'test:landscape', // Pass
12
- 'test:sequence', // Pass
13
- 'test:system', // Pass
14
- 'test:console_command', // Pass
15
- 'test:inspect', // Pass
16
- 'test:manage_asset', // Pass
17
- 'test:blueprint', // Pass
18
- 'test:blueprint_graph', // Pass
19
- // 'test:graphql', // Pass
20
- 'test:wasm:all', // Pass
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', // Pass
23
- 'test:asset_advanced', // Pass
24
- 'test:render', // Pass
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.0",
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.0",
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
- UE_SCREENSHOT_DIR: z.string().optional(),
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
+ }