unreal-engine-mcp-server 0.5.0 → 0.5.2

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 (188) 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 +2 -4
  6. package/.github/workflows/release-drafter.yml +3 -2
  7. package/.github/workflows/release.yml +3 -3
  8. package/CHANGELOG.md +109 -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/automation/bridge.d.ts +1 -2
  14. package/dist/automation/bridge.js +24 -23
  15. package/dist/automation/connection-manager.d.ts +1 -0
  16. package/dist/automation/connection-manager.js +10 -0
  17. package/dist/automation/message-handler.js +5 -4
  18. package/dist/automation/request-tracker.d.ts +4 -0
  19. package/dist/automation/request-tracker.js +11 -3
  20. package/dist/config.d.ts +0 -1
  21. package/dist/config.js +0 -1
  22. package/dist/constants.d.ts +4 -0
  23. package/dist/constants.js +4 -0
  24. package/dist/graphql/loaders.d.ts +64 -0
  25. package/dist/graphql/loaders.js +117 -0
  26. package/dist/graphql/resolvers.d.ts +3 -3
  27. package/dist/graphql/resolvers.js +33 -30
  28. package/dist/graphql/server.js +3 -1
  29. package/dist/graphql/types.d.ts +2 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +13 -2
  32. package/dist/server-setup.d.ts +0 -1
  33. package/dist/server-setup.js +0 -40
  34. package/dist/tools/actors.d.ts +58 -24
  35. package/dist/tools/actors.js +22 -6
  36. package/dist/tools/assets.d.ts +19 -71
  37. package/dist/tools/assets.js +28 -22
  38. package/dist/tools/base-tool.d.ts +4 -4
  39. package/dist/tools/base-tool.js +1 -1
  40. package/dist/tools/blueprint.d.ts +45 -61
  41. package/dist/tools/blueprint.js +43 -14
  42. package/dist/tools/consolidated-tool-definitions.js +2 -1
  43. package/dist/tools/consolidated-tool-handlers.js +96 -110
  44. package/dist/tools/dynamic-handler-registry.d.ts +11 -9
  45. package/dist/tools/dynamic-handler-registry.js +17 -95
  46. package/dist/tools/editor.d.ts +19 -193
  47. package/dist/tools/editor.js +11 -2
  48. package/dist/tools/environment.d.ts +8 -14
  49. package/dist/tools/foliage.d.ts +18 -143
  50. package/dist/tools/foliage.js +4 -2
  51. package/dist/tools/handlers/actor-handlers.d.ts +1 -1
  52. package/dist/tools/handlers/actor-handlers.js +14 -13
  53. package/dist/tools/handlers/asset-handlers.js +454 -454
  54. package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
  55. package/dist/tools/handlers/sequence-handlers.js +24 -13
  56. package/dist/tools/introspection.d.ts +1 -1
  57. package/dist/tools/introspection.js +1 -1
  58. package/dist/tools/landscape.d.ts +16 -116
  59. package/dist/tools/landscape.js +7 -3
  60. package/dist/tools/level.d.ts +22 -103
  61. package/dist/tools/level.js +26 -18
  62. package/dist/tools/lighting.d.ts +54 -7
  63. package/dist/tools/lighting.js +9 -5
  64. package/dist/tools/materials.d.ts +1 -1
  65. package/dist/tools/materials.js +5 -1
  66. package/dist/tools/niagara.js +37 -2
  67. package/dist/tools/performance.d.ts +0 -1
  68. package/dist/tools/performance.js +0 -1
  69. package/dist/tools/physics.js +5 -1
  70. package/dist/tools/sequence.d.ts +24 -24
  71. package/dist/tools/sequence.js +13 -0
  72. package/dist/tools/ui.d.ts +0 -2
  73. package/dist/types/automation-responses.d.ts +115 -0
  74. package/dist/types/automation-responses.js +2 -0
  75. package/dist/types/responses.d.ts +249 -0
  76. package/dist/types/responses.js +2 -0
  77. package/dist/types/tool-interfaces.d.ts +135 -135
  78. package/dist/types/tool-types.d.ts +2 -0
  79. package/dist/unreal-bridge.js +4 -4
  80. package/dist/utils/command-validator.js +7 -5
  81. package/dist/utils/error-handler.d.ts +24 -2
  82. package/dist/utils/error-handler.js +58 -23
  83. package/dist/utils/normalize.d.ts +7 -4
  84. package/dist/utils/normalize.js +12 -10
  85. package/dist/utils/path-security.d.ts +2 -0
  86. package/dist/utils/path-security.js +24 -0
  87. package/dist/utils/response-factory.d.ts +4 -4
  88. package/dist/utils/response-factory.js +15 -21
  89. package/dist/utils/response-validator.js +88 -73
  90. package/dist/utils/unreal-command-queue.d.ts +2 -0
  91. package/dist/utils/unreal-command-queue.js +8 -1
  92. package/docs/Migration-Guide-v0.5.0.md +1 -9
  93. package/docs/handler-mapping.md +4 -2
  94. package/docs/testing-guide.md +2 -2
  95. package/package.json +12 -6
  96. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +298 -33
  97. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +7 -8
  98. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +229 -319
  99. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +98 -0
  100. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +24 -0
  101. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +96 -0
  102. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +52 -5
  103. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +5 -268
  104. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +57 -2
  105. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +0 -1
  106. package/scripts/run-all-tests.mjs +25 -20
  107. package/server.json +3 -2
  108. package/src/automation/bridge.ts +27 -25
  109. package/src/automation/connection-manager.ts +18 -0
  110. package/src/automation/message-handler.ts +33 -8
  111. package/src/automation/request-tracker.ts +39 -7
  112. package/src/config.ts +1 -1
  113. package/src/constants.ts +7 -0
  114. package/src/graphql/loaders.ts +244 -0
  115. package/src/graphql/resolvers.ts +47 -49
  116. package/src/graphql/server.ts +3 -1
  117. package/src/graphql/types.ts +3 -0
  118. package/src/index.ts +15 -2
  119. package/src/resources/assets.ts +5 -4
  120. package/src/server/tool-registry.ts +3 -3
  121. package/src/server-setup.ts +3 -37
  122. package/src/tools/actors.ts +77 -44
  123. package/src/tools/animation.ts +1 -0
  124. package/src/tools/assets.ts +76 -65
  125. package/src/tools/base-tool.ts +3 -3
  126. package/src/tools/blueprint.ts +170 -104
  127. package/src/tools/consolidated-tool-definitions.ts +2 -1
  128. package/src/tools/consolidated-tool-handlers.ts +129 -150
  129. package/src/tools/dynamic-handler-registry.ts +22 -140
  130. package/src/tools/editor.ts +43 -29
  131. package/src/tools/environment.ts +21 -27
  132. package/src/tools/foliage.ts +28 -25
  133. package/src/tools/handlers/actor-handlers.ts +16 -17
  134. package/src/tools/handlers/asset-handlers.ts +484 -484
  135. package/src/tools/handlers/sequence-handlers.ts +85 -62
  136. package/src/tools/introspection.ts +7 -7
  137. package/src/tools/landscape.ts +34 -28
  138. package/src/tools/level.ts +100 -80
  139. package/src/tools/lighting.ts +25 -20
  140. package/src/tools/materials.ts +9 -3
  141. package/src/tools/niagara.ts +44 -2
  142. package/src/tools/performance.ts +1 -2
  143. package/src/tools/physics.ts +7 -1
  144. package/src/tools/sequence.ts +42 -26
  145. package/src/tools/ui.ts +1 -3
  146. package/src/types/automation-responses.ts +119 -0
  147. package/src/types/responses.ts +355 -0
  148. package/src/types/tool-interfaces.ts +135 -135
  149. package/src/types/tool-types.ts +4 -0
  150. package/src/unreal-bridge.ts +71 -26
  151. package/src/utils/command-validator.ts +47 -5
  152. package/src/utils/error-handler.ts +128 -45
  153. package/src/utils/normalize.test.ts +162 -0
  154. package/src/utils/normalize.ts +38 -16
  155. package/src/utils/path-security.ts +43 -0
  156. package/src/utils/response-factory.ts +29 -24
  157. package/src/utils/response-validator.ts +103 -87
  158. package/src/utils/safe-json.test.ts +90 -0
  159. package/src/utils/unreal-command-queue.ts +13 -1
  160. package/src/utils/validation.test.ts +184 -0
  161. package/tests/test-animation.mjs +358 -33
  162. package/tests/test-asset-graph.mjs +311 -0
  163. package/tests/test-audio.mjs +314 -116
  164. package/tests/test-behavior-tree.mjs +327 -144
  165. package/tests/test-blueprint-graph.mjs +343 -12
  166. package/tests/test-control-editor.mjs +85 -53
  167. package/tests/test-graphql.mjs +58 -8
  168. package/tests/test-input.mjs +349 -0
  169. package/tests/test-inspect.mjs +291 -61
  170. package/tests/test-landscape.mjs +304 -48
  171. package/tests/test-lighting.mjs +428 -0
  172. package/tests/test-manage-level.mjs +70 -51
  173. package/tests/test-performance.mjs +539 -0
  174. package/tests/test-sequence.mjs +82 -46
  175. package/tests/test-system.mjs +72 -33
  176. package/tests/test-wasm.mjs +98 -8
  177. package/vitest.config.ts +35 -0
  178. package/.github/release-drafter.yml +0 -148
  179. package/dist/prompts/index.d.ts +0 -21
  180. package/dist/prompts/index.js +0 -217
  181. package/dist/tools/blueprint/helpers.d.ts +0 -29
  182. package/dist/tools/blueprint/helpers.js +0 -182
  183. package/src/prompts/index.ts +0 -249
  184. package/src/tools/blueprint/helpers.ts +0 -189
  185. package/tests/test-blueprint-events.mjs +0 -35
  186. package/tests/test-extra-tools.mjs +0 -38
  187. package/tests/test-render.mjs +0 -33
  188. package/tests/test-search-assets.mjs +0 -66
@@ -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
+ }
@@ -129,6 +129,35 @@ interface Blueprint {
129
129
  scsHierarchy?: Record<string, any>;
130
130
  }
131
131
 
132
+ import { Logger } from '../utils/logger.js';
133
+
134
+ const log = new Logger('GraphQL:Resolvers');
135
+
136
+ /**
137
+ * Creates a GraphQL-friendly error with proper extensions
138
+ */
139
+ class GraphQLResolverError extends Error {
140
+ extensions: { code: string; originalError?: string };
141
+
142
+ constructor(message: string, code: string = 'UNREAL_ENGINE_ERROR', originalError?: Error) {
143
+ super(message);
144
+ this.name = 'GraphQLResolverError';
145
+ this.extensions = {
146
+ code,
147
+ originalError: originalError?.message
148
+ };
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Helper to create resolver errors with proper logging
154
+ */
155
+ function createResolverError(operation: string, error: unknown): GraphQLResolverError {
156
+ const message = error instanceof Error ? error.message : String(error);
157
+ log.error(`${operation} failed:`, message);
158
+ return new GraphQLResolverError(`${operation} failed: ${message}`, 'UNREAL_ENGINE_ERROR', error instanceof Error ? error : undefined);
159
+ }
160
+
132
161
  function logAutomationFailure(source: string, response: any) {
133
162
  try {
134
163
  if (!response || response.success !== false) {
@@ -138,7 +167,7 @@ function logAutomationFailure(source: string, response: any) {
138
167
  if (errorText.length === 0) {
139
168
  return;
140
169
  }
141
- console.error(`[GraphQL] ${source} automation failure:`, errorText);
170
+ log.error(`${source} automation failure:`, errorText);
142
171
  } catch {
143
172
  }
144
173
  }
@@ -158,7 +187,7 @@ async function getActorProperties(
158
187
  });
159
188
  return result.success ? result.value || {} : {};
160
189
  } catch (error) {
161
- console.error('Failed to get actor properties:', error);
190
+ log.error('Failed to get actor properties:', error);
162
191
  return {};
163
192
  }
164
193
  }
@@ -190,11 +219,11 @@ async function listAssets(
190
219
  }
191
220
 
192
221
  logAutomationFailure('list_assets', response);
193
- console.error('Failed to list assets:', response);
222
+ log.warn('Failed to list assets - returning empty set');
194
223
  return { assets: [], totalCount: 0 };
195
224
  } catch (error) {
196
- console.error('Failed to list assets:', error);
197
- return { assets: [], totalCount: 0 };
225
+ log.error('Failed to list assets:', error);
226
+ throw createResolverError('listAssets', error);
198
227
  }
199
228
  }
200
229
 
@@ -229,33 +258,7 @@ async function listActors(
229
258
  }
230
259
  }
231
260
 
232
- /**
233
- * Helper to get blueprint details
234
- */
235
- async function getBlueprint(
236
- automationBridge: AutomationBridge,
237
- blueprintPath: string
238
- ): Promise<Blueprint | null> {
239
- try {
240
- const response = await automationBridge.sendAutomationRequest(
241
- 'get_blueprint',
242
- {
243
- blueprintPath
244
- },
245
- { timeoutMs: 30000 }
246
- );
247
261
 
248
- if (response.success && response.result) {
249
- return response.result as Blueprint;
250
- }
251
-
252
- logAutomationFailure('get_blueprint', response);
253
- return null;
254
- } catch (error) {
255
- console.error('Failed to get blueprint:', error);
256
- return null;
257
- }
258
- }
259
262
 
260
263
  /**
261
264
  * GraphQL Resolvers Implementation
@@ -291,17 +294,10 @@ export const resolvers = {
291
294
 
292
295
  asset: async (_: any, { path }: { path: string }, context: GraphQLContext) => {
293
296
  try {
294
- const response = await context.automationBridge.sendAutomationRequest(
295
- 'get_asset',
296
- { assetPath: path },
297
- { timeoutMs: 10000 }
298
- );
299
-
300
- if (response.success && response.result) {
301
- return response.result;
297
+ if (!context.loaders) {
298
+ throw new Error('Loaders not initialized');
302
299
  }
303
-
304
- return null;
300
+ return await context.loaders.assetLoader.load(path);
305
301
  } catch (error) {
306
302
  console.error('Failed to get asset:', error);
307
303
  return null;
@@ -336,11 +332,10 @@ export const resolvers = {
336
332
 
337
333
  actor: async (_: any, { name }: { name: string }, context: GraphQLContext) => {
338
334
  try {
339
- const actors = await listActors(context.automationBridge, { tag: name });
340
- if (actors.actors.length > 0) {
341
- return actors.actors[0];
335
+ if (!context.loaders) {
336
+ throw new Error('Loaders not initialized');
342
337
  }
343
- return null;
338
+ return await context.loaders.actorLoader.load(name);
344
339
  } catch (error) {
345
340
  console.error('Failed to get actor:', error);
346
341
  return null;
@@ -394,7 +389,10 @@ export const resolvers = {
394
389
  },
395
390
 
396
391
  blueprint: async (_: any, { path }: { path: string }, context: GraphQLContext) => {
397
- return await getBlueprint(context.automationBridge, path);
392
+ if (!context.loaders) {
393
+ throw new Error('Loaders not initialized');
394
+ }
395
+ return await context.loaders.blueprintLoader.load(path);
398
396
  },
399
397
 
400
398
  levels: async (_: any, __: any, context: GraphQLContext) => {
@@ -524,9 +522,9 @@ export const resolvers = {
524
522
  { subAction: 'get_cells' },
525
523
  { timeoutMs: 10000 }
526
524
  );
527
-
525
+
528
526
  if (response.success && response.result) {
529
- return (response.result as any).cells || [];
527
+ return (response.result as any).cells || [];
530
528
  }
531
529
  return [];
532
530
  } catch (error) {
@@ -550,7 +548,7 @@ export const resolvers = {
550
548
  const edges = assets.map((asset, index) => ({
551
549
  node: {
552
550
  ...asset,
553
- emitters: [],
551
+ emitters: [],
554
552
  parameters: []
555
553
  },
556
554
  cursor: Buffer.from(`${asset.path}:${offset + index}`).toString('base64')
@@ -5,6 +5,7 @@ import { createGraphQLSchema } from './schema.js';
5
5
  import type { GraphQLContext } from './types.js';
6
6
  import type { UnrealBridge } from '../unreal-bridge.js';
7
7
  import { AutomationBridge } from '../automation/index.js';
8
+ import { createLoaders } from './loaders.js';
8
9
 
9
10
  export interface GraphQLServerConfig {
10
11
  enabled?: boolean;
@@ -65,7 +66,8 @@ export class GraphQLServer {
65
66
  },
66
67
  context: (): GraphQLContext => ({
67
68
  bridge: this.bridge,
68
- automationBridge: this.automationBridge
69
+ automationBridge: this.automationBridge,
70
+ loaders: createLoaders(this.automationBridge)
69
71
  }),
70
72
  logging: {
71
73
  debug: (...args) => this.log.debug('[GraphQL]', ...args),
@@ -1,7 +1,10 @@
1
1
  import type { UnrealBridge } from '../unreal-bridge.js';
2
2
  import { AutomationBridge } from '../automation/index.js';
3
+ import type { GraphQLLoaders } from './loaders.js';
3
4
 
4
5
  export interface GraphQLContext {
5
6
  bridge: UnrealBridge;
6
7
  automationBridge: AutomationBridge;
8
+ /** DataLoaders for batching and caching - solves N+1 query problem */
9
+ loaders?: GraphQLLoaders;
7
10
  }
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ import { HealthMonitor } from './services/health-monitor.js';
13
13
  import { ServerSetup } from './server-setup.js';
14
14
  import { startMetricsServer } from './services/metrics-server.js';
15
15
  import { config } from './config.js';
16
+ import { GraphQLServer } from './graphql/server.js';
16
17
 
17
18
  const require = createRequire(import.meta.url);
18
19
  const packageInfo: { name?: string; version?: string } = (() => {
@@ -106,6 +107,12 @@ export function createServer() {
106
107
  // Optionally expose Prometheus-style metrics via /metrics
107
108
  startMetricsServer({ healthMonitor, automationBridge, logger: log });
108
109
 
110
+ // Initialize GraphQL server (controlled by GRAPHQL_ENABLED env var)
111
+ const graphqlServer = new GraphQLServer(bridge, automationBridge);
112
+ graphqlServer.start().catch((error) => {
113
+ log.warn('GraphQL server failed to start:', error);
114
+ });
115
+
109
116
  // Initialize WebAssembly module for high-performance operations (5-8x faster)
110
117
  log.debug('Initializing WebAssembly integration...');
111
118
  initializeWASM().then(() => {
@@ -145,7 +152,7 @@ export function createServer() {
145
152
  const serverSetup = new ServerSetup(server, bridge, automationBridge, log, healthMonitor);
146
153
  serverSetup.setup(); // Register tools, resources, and prompts
147
154
 
148
- return { server, bridge, automationBridge };
155
+ return { server, bridge, automationBridge, graphqlServer };
149
156
  }
150
157
 
151
158
  // Export configuration schema for session UI and runtime validation
@@ -170,7 +177,7 @@ export default function createServerDefault({ config }: { config?: any } = {}) {
170
177
  }
171
178
 
172
179
  export async function startStdioServer() {
173
- const { server, automationBridge } = createServer();
180
+ const { server, automationBridge, graphqlServer } = createServer();
174
181
  const transport = new StdioServerTransport();
175
182
  let shuttingDown = false;
176
183
 
@@ -187,6 +194,12 @@ export async function startStdioServer() {
187
194
  log.warn('Failed to stop automation bridge cleanly', error);
188
195
  }
189
196
 
197
+ try {
198
+ await graphqlServer.stop();
199
+ } catch (error) {
200
+ log.warn('Failed to stop GraphQL server cleanly', error);
201
+ }
202
+
190
203
  try {
191
204
  if (typeof (server as any).close === 'function') {
192
205
  await (server as any).close();
@@ -1,6 +1,7 @@
1
1
  import { BaseTool } from '../tools/base-tool.js';
2
2
  import { IAssetResources } from '../types/tool-interfaces.js';
3
3
  import { coerceString } from '../utils/result-helpers.js';
4
+ import { AutomationResponse } from '../types/automation-responses.js';
4
5
 
5
6
  export class AssetResources extends BaseTool implements IAssetResources {
6
7
  // Simple in-memory cache for asset listing
@@ -175,14 +176,14 @@ export class AssetResources extends BaseTool implements IAssetResources {
175
176
  // Use the native C++ plugin's list action instead of Python
176
177
  try {
177
178
  const normalizedDir = this.normalizeDir(dir);
178
- const response = await this.sendAutomationRequest(
179
+ const response = await this.sendAutomationRequest<AutomationResponse>(
179
180
  'list',
180
181
  { directory: normalizedDir, limit, recursive: false },
181
182
  { timeoutMs: 30000 }
182
183
  );
183
184
 
184
185
  if (response.success !== false && response.result) {
185
- const payload = response.result;
186
+ const payload = response.result as any;
186
187
 
187
188
  const foldersArr = Array.isArray(payload.folders_list)
188
189
  ? payload.folders_list.map((f: any) => ({
@@ -257,12 +258,12 @@ export class AssetResources extends BaseTool implements IAssetResources {
257
258
 
258
259
  try {
259
260
  const normalizedPath = this.normalizeDir(assetPath);
260
- const response = await this.sendAutomationRequest(
261
+ const response = await this.sendAutomationRequest<AutomationResponse>(
261
262
  'asset_exists',
262
263
  { asset_path: normalizedPath }
263
264
  );
264
265
 
265
- return response?.success !== false && response?.result?.exists === true;
266
+ return response?.success !== false && (response?.result as any)?.exists === true;
266
267
  } catch {
267
268
  return false;
268
269
  }
@@ -210,7 +210,7 @@ export class ToolRegistry {
210
210
 
211
211
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
212
212
  const { name } = request.params;
213
- let args: any = request.params.arguments || {};
213
+ let args: Record<string, unknown> = request.params.arguments || {};
214
214
  const startTime = Date.now();
215
215
 
216
216
  const connected = await this.ensureConnected();
@@ -258,13 +258,13 @@ export class ToolRegistry {
258
258
  const props = inputSchema.properties || {};
259
259
  const required: string[] = Array.isArray(inputSchema.required) ? inputSchema.required : [];
260
260
  const missing = required.filter((k: string) => {
261
- const v = (args as any)[k];
261
+ const v = (args as Record<string, unknown>)[k];
262
262
  if (v === undefined || v === null) return true;
263
263
  if (typeof v === 'string' && v.trim() === '') return true;
264
264
  return false;
265
265
  });
266
266
 
267
- const primitiveProps: any = {};
267
+ const primitiveProps: Record<string, unknown> = {};
268
268
  for (const k of missing) {
269
269
  const p = props[k];
270
270
  if (!p || typeof p !== 'object') continue;
@@ -1,10 +1,10 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
- import { ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';
2
+ // import { ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';
3
3
  import { UnrealBridge } from './unreal-bridge.js';
4
4
  import { AutomationBridge } from './automation/index.js';
5
5
  import { Logger } from './utils/logger.js';
6
6
  import { HealthMonitor } from './services/health-monitor.js';
7
- import { prompts } from './prompts/index.js';
7
+ // import { prompts } from './prompts/index.js';
8
8
  import { AssetResources } from './resources/assets.js';
9
9
  import { ActorResources } from './resources/actors.js';
10
10
  import { LevelResources } from './resources/levels.js';
@@ -73,7 +73,7 @@ export class ServerSetup {
73
73
  );
74
74
  toolRegistry.register();
75
75
 
76
- this.registerPrompts();
76
+ // this.registerPrompts();
77
77
  }
78
78
 
79
79
  private validateEnvironment() {
@@ -110,39 +110,5 @@ export class ServerSetup {
110
110
  return ok;
111
111
  }
112
112
 
113
- private registerPrompts() {
114
- this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
115
- return {
116
- prompts: prompts.map(p => ({
117
- name: p.name,
118
- description: p.description,
119
- arguments: Object.entries(p.arguments || {}).map(([name, schema]) => {
120
- const meta: Record<string, unknown> = {};
121
- if (schema.type) meta.type = schema.type;
122
- if (schema.enum) meta.enum = schema.enum;
123
- if (schema.default !== undefined) meta.default = schema.default;
124
- return {
125
- name,
126
- description: schema.description,
127
- required: schema.required ?? false,
128
- ...(Object.keys(meta).length ? { _meta: meta } : {})
129
- };
130
- })
131
- }))
132
- };
133
- });
134
113
 
135
- this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
136
- const prompt = prompts.find(p => p.name === request.params.name);
137
- if (!prompt) {
138
- throw new Error(`Unknown prompt: ${request.params.name}`);
139
- }
140
- const args = (request.params.arguments || {}) as Record<string, unknown>;
141
- const messages = prompt.build(args);
142
- return {
143
- description: prompt.description,
144
- messages
145
- };
146
- });
147
- }
148
114
  }