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.
- 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 +2 -4
- package/.github/workflows/release-drafter.yml +3 -2
- package/.github/workflows/release.yml +3 -3
- package/CHANGELOG.md +109 -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/automation/bridge.d.ts +1 -2
- package/dist/automation/bridge.js +24 -23
- package/dist/automation/connection-manager.d.ts +1 -0
- package/dist/automation/connection-manager.js +10 -0
- package/dist/automation/message-handler.js +5 -4
- package/dist/automation/request-tracker.d.ts +4 -0
- package/dist/automation/request-tracker.js +11 -3
- 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 +58 -24
- package/dist/tools/actors.js +22 -6
- 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 +45 -61
- package/dist/tools/blueprint.js +43 -14
- package/dist/tools/consolidated-tool-definitions.js +2 -1
- 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 +11 -2
- 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.d.ts +1 -1
- package/dist/tools/handlers/actor-handlers.js +14 -13
- package/dist/tools/handlers/asset-handlers.js +454 -454
- package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
- package/dist/tools/handlers/sequence-handlers.js +24 -13
- package/dist/tools/introspection.d.ts +1 -1
- package/dist/tools/introspection.js +1 -1
- 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 +26 -18
- package/dist/tools/lighting.d.ts +54 -7
- package/dist/tools/lighting.js +9 -5
- package/dist/tools/materials.d.ts +1 -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/types/tool-types.d.ts +2 -0
- package/dist/unreal-bridge.js +4 -4
- package/dist/utils/command-validator.js +7 -5
- package/dist/utils/error-handler.d.ts +24 -2
- package/dist/utils/error-handler.js +58 -23
- package/dist/utils/normalize.d.ts +7 -4
- package/dist/utils/normalize.js +12 -10
- 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/dist/utils/response-validator.js +88 -73
- package/dist/utils/unreal-command-queue.d.ts +2 -0
- package/dist/utils/unreal-command-queue.js +8 -1
- package/docs/Migration-Guide-v0.5.0.md +1 -9
- package/docs/handler-mapping.md +4 -2
- package/docs/testing-guide.md +2 -2
- package/package.json +12 -6
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +298 -33
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +7 -8
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +229 -319
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +98 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +24 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +96 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +52 -5
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +5 -268
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +57 -2
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +0 -1
- package/scripts/run-all-tests.mjs +25 -20
- package/server.json +3 -2
- package/src/automation/bridge.ts +27 -25
- package/src/automation/connection-manager.ts +18 -0
- package/src/automation/message-handler.ts +33 -8
- package/src/automation/request-tracker.ts +39 -7
- 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/tool-registry.ts +3 -3
- package/src/server-setup.ts +3 -37
- package/src/tools/actors.ts +77 -44
- package/src/tools/animation.ts +1 -0
- package/src/tools/assets.ts +76 -65
- package/src/tools/base-tool.ts +3 -3
- package/src/tools/blueprint.ts +170 -104
- package/src/tools/consolidated-tool-definitions.ts +2 -1
- package/src/tools/consolidated-tool-handlers.ts +129 -150
- package/src/tools/dynamic-handler-registry.ts +22 -140
- package/src/tools/editor.ts +43 -29
- package/src/tools/environment.ts +21 -27
- package/src/tools/foliage.ts +28 -25
- package/src/tools/handlers/actor-handlers.ts +16 -17
- package/src/tools/handlers/asset-handlers.ts +484 -484
- package/src/tools/handlers/sequence-handlers.ts +85 -62
- package/src/tools/introspection.ts +7 -7
- package/src/tools/landscape.ts +34 -28
- package/src/tools/level.ts +100 -80
- package/src/tools/lighting.ts +25 -20
- package/src/tools/materials.ts +9 -3
- 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 +42 -26
- package/src/tools/ui.ts +1 -3
- 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/types/tool-types.ts +4 -0
- package/src/unreal-bridge.ts +71 -26
- package/src/utils/command-validator.ts +47 -5
- package/src/utils/error-handler.ts +128 -45
- package/src/utils/normalize.test.ts +162 -0
- package/src/utils/normalize.ts +38 -16
- package/src/utils/path-security.ts +43 -0
- package/src/utils/response-factory.ts +29 -24
- package/src/utils/response-validator.ts +103 -87
- package/src/utils/safe-json.test.ts +90 -0
- package/src/utils/unreal-command-queue.ts +13 -1
- 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
|
@@ -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
|
+
}
|
package/src/graphql/resolvers.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
222
|
+
log.warn('Failed to list assets - returning empty set');
|
|
194
223
|
return { assets: [], totalCount: 0 };
|
|
195
224
|
} catch (error) {
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
295
|
-
'
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
return actors.actors[0];
|
|
335
|
+
if (!context.loaders) {
|
|
336
|
+
throw new Error('Loaders not initialized');
|
|
342
337
|
}
|
|
343
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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')
|
package/src/graphql/server.ts
CHANGED
|
@@ -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),
|
package/src/graphql/types.ts
CHANGED
|
@@ -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();
|
package/src/resources/assets.ts
CHANGED
|
@@ -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:
|
|
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
|
|
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:
|
|
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;
|
package/src/server-setup.ts
CHANGED
|
@@ -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
|
}
|