unreal-engine-mcp-server 0.5.1 → 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 (75) hide show
  1. package/.github/workflows/publish-mcp.yml +1 -4
  2. package/.github/workflows/release-drafter.yml +2 -1
  3. package/CHANGELOG.md +38 -0
  4. package/dist/automation/bridge.d.ts +1 -2
  5. package/dist/automation/bridge.js +24 -23
  6. package/dist/automation/connection-manager.d.ts +1 -0
  7. package/dist/automation/connection-manager.js +10 -0
  8. package/dist/automation/message-handler.js +5 -4
  9. package/dist/automation/request-tracker.d.ts +4 -0
  10. package/dist/automation/request-tracker.js +11 -3
  11. package/dist/tools/actors.d.ts +19 -1
  12. package/dist/tools/actors.js +15 -5
  13. package/dist/tools/assets.js +1 -1
  14. package/dist/tools/blueprint.d.ts +12 -0
  15. package/dist/tools/blueprint.js +43 -14
  16. package/dist/tools/consolidated-tool-definitions.js +2 -1
  17. package/dist/tools/editor.js +3 -2
  18. package/dist/tools/handlers/actor-handlers.d.ts +1 -1
  19. package/dist/tools/handlers/actor-handlers.js +14 -8
  20. package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
  21. package/dist/tools/handlers/sequence-handlers.js +24 -13
  22. package/dist/tools/introspection.d.ts +1 -1
  23. package/dist/tools/introspection.js +1 -1
  24. package/dist/tools/level.js +3 -3
  25. package/dist/tools/lighting.d.ts +54 -7
  26. package/dist/tools/lighting.js +4 -4
  27. package/dist/tools/materials.d.ts +1 -1
  28. package/dist/types/tool-types.d.ts +2 -0
  29. package/dist/unreal-bridge.js +4 -4
  30. package/dist/utils/command-validator.js +6 -5
  31. package/dist/utils/error-handler.d.ts +24 -2
  32. package/dist/utils/error-handler.js +58 -23
  33. package/dist/utils/normalize.d.ts +7 -4
  34. package/dist/utils/normalize.js +12 -10
  35. package/dist/utils/response-validator.js +88 -73
  36. package/dist/utils/unreal-command-queue.d.ts +2 -0
  37. package/dist/utils/unreal-command-queue.js +8 -1
  38. package/docs/handler-mapping.md +4 -2
  39. package/package.json +1 -1
  40. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +298 -33
  41. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +7 -8
  42. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +229 -319
  43. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +98 -0
  44. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +24 -0
  45. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +96 -0
  46. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +52 -5
  47. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +5 -268
  48. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +57 -2
  49. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +0 -1
  50. package/server.json +3 -3
  51. package/src/automation/bridge.ts +27 -25
  52. package/src/automation/connection-manager.ts +18 -0
  53. package/src/automation/message-handler.ts +33 -8
  54. package/src/automation/request-tracker.ts +39 -7
  55. package/src/server/tool-registry.ts +3 -3
  56. package/src/tools/actors.ts +44 -19
  57. package/src/tools/assets.ts +3 -3
  58. package/src/tools/blueprint.ts +115 -49
  59. package/src/tools/consolidated-tool-definitions.ts +2 -1
  60. package/src/tools/editor.ts +4 -3
  61. package/src/tools/handlers/actor-handlers.ts +14 -9
  62. package/src/tools/handlers/sequence-handlers.ts +86 -63
  63. package/src/tools/introspection.ts +7 -7
  64. package/src/tools/level.ts +6 -6
  65. package/src/tools/lighting.ts +19 -19
  66. package/src/tools/materials.ts +1 -1
  67. package/src/tools/sequence.ts +1 -1
  68. package/src/tools/ui.ts +1 -1
  69. package/src/types/tool-types.ts +4 -0
  70. package/src/unreal-bridge.ts +71 -26
  71. package/src/utils/command-validator.ts +46 -5
  72. package/src/utils/error-handler.ts +128 -45
  73. package/src/utils/normalize.ts +38 -16
  74. package/src/utils/response-validator.ts +103 -87
  75. package/src/utils/unreal-command-queue.ts +13 -1
@@ -27,114 +27,130 @@ function buildSummaryText(toolName: string, payload: unknown): string {
27
27
  return `${toolName} responded`;
28
28
  }
29
29
 
30
- // 1. Check for specific "data" or "result" wrapper
30
+ // Recursively flatten data/result wrappers into effective payload
31
31
  const effectivePayload: Record<string, any> = { ...(payload as object) };
32
- if (isRecord(effectivePayload.data)) {
33
- Object.assign(effectivePayload, effectivePayload.data);
34
- }
35
- if (isRecord(effectivePayload.result)) {
36
- Object.assign(effectivePayload, effectivePayload.result);
37
- }
38
32
 
39
- const parts: string[] = [];
33
+ const flattenWrappers = (obj: Record<string, any>, depth = 0): void => {
34
+ if (depth > 5) return; // Prevent infinite loops
35
+ if (isRecord(obj.data)) {
36
+ Object.assign(obj, obj.data);
37
+ delete obj.data;
38
+ flattenWrappers(obj, depth + 1);
39
+ }
40
+ if (isRecord(obj.result)) {
41
+ Object.assign(obj, obj.result);
42
+ delete obj.result;
43
+ flattenWrappers(obj, depth + 1);
44
+ }
45
+ };
46
+ flattenWrappers(effectivePayload);
40
47
 
41
- // 2. Identify "List" responses (Arrays) - Prioritize showing content
42
- const listKeys = ['actors', 'levels', 'assets', 'folders', 'blueprints', 'components', 'pawnClasses', 'foliageTypes', 'nodes', 'tracks', 'bindings', 'keys'];
43
- for (const key of listKeys) {
44
- if (Array.isArray(effectivePayload[key])) {
45
- const arr = effectivePayload[key] as any[];
46
- const names = arr.map(i => isRecord(i) ? (i.name || i.path || i.id || i.assetName || i.objectPath || i.packageName || i.nodeName || '<?>') : String(i));
47
- const count = arr.length;
48
- const preview = names.slice(0, 100).join(', '); // Show up to 100
49
- const suffix = count > 100 ? `, ... (+${count - 100} more)` : '';
50
- parts.push(`${key}: ${preview}${suffix} (Total: ${count})`);
48
+ const parts: string[] = [];
49
+ const addedKeys = new Set<string>();
50
+
51
+ // Keys to skip (internal/redundant)
52
+ const skipKeys = new Set(['requestId', 'type', 'data', 'result', 'warnings']);
53
+
54
+ // Helper to format a value for display
55
+ const formatValue = (val: unknown): string => {
56
+ if (val === null || val === undefined) return '';
57
+ if (typeof val === 'string') return val.length > 150 ? val.slice(0, 150) + '...' : val;
58
+ if (typeof val === 'number' || typeof val === 'boolean') return String(val);
59
+
60
+ // Handle arrays - show items with names/paths
61
+ if (Array.isArray(val)) {
62
+ if (val.length === 0) return '[] (0)';
63
+ const items = val.slice(0, 30).map(v => {
64
+ if (isRecord(v)) {
65
+ // Try common identifier fields
66
+ return v.name || v.path || v.id || v.nodeId || v.nodeName || v.className ||
67
+ v.displayName || v.type || v.assetPath || v.objectPath ||
68
+ JSON.stringify(v).slice(0, 50);
69
+ }
70
+ return String(v);
71
+ });
72
+ const suffix = val.length > 30 ? `, ... (+${val.length - 30} more)` : '';
73
+ return `[${items.join(', ')}${suffix}] (${val.length})`;
51
74
  }
52
- }
53
75
 
54
- // 3. Identify "Entity" operations (Single significant object)
55
- if (typeof effectivePayload.actor === 'string' || isRecord(effectivePayload.actor)) {
56
- const a = effectivePayload.actor;
57
- const name = isRecord(a) ? (a.name || a.path) : a;
58
- const loc = isRecord(effectivePayload.location) ? ` at [${effectivePayload.location.x},${effectivePayload.location.y},${effectivePayload.location.z}]` : '';
59
- parts.push(`Actor: ${name}${loc}`);
60
- }
76
+ // Handle transform-like objects (location/rotation/scale)
77
+ if (isRecord(val)) {
78
+ const keys = Object.keys(val);
79
+ // Check if it looks like a 3D vector/transform
80
+ if (keys.some(k => ['x', 'y', 'z', 'pitch', 'yaw', 'roll'].includes(k))) {
81
+ const x = val.x ?? val.pitch ?? 0;
82
+ const y = val.y ?? val.yaw ?? 0;
83
+ const z = val.z ?? val.roll ?? 0;
84
+ return `[${x}, ${y}, ${z}]`;
85
+ }
86
+ // Generic object - show key=value pairs
87
+ const entries = Object.entries(val).slice(0, 8);
88
+ const formatted = entries.map(([k, v]) => {
89
+ const vStr = typeof v === 'object' ? JSON.stringify(v).slice(0, 40) : String(v);
90
+ return `${k}=${vStr}`;
91
+ });
92
+ return `{ ${formatted.join(', ')}${keys.length > 8 ? ' ...' : ''} }`;
93
+ }
61
94
 
62
- if (typeof effectivePayload.asset === 'string' || isRecord(effectivePayload.asset)) {
63
- const a = effectivePayload.asset;
64
- const path = isRecord(a) ? (a.path || a.name) : a;
65
- parts.push(`Asset: ${path}`);
95
+ return String(val);
96
+ };
97
+
98
+ // Process all keys in priority order
99
+ // 1. First add 'success' and 'error' at the start
100
+ for (const key of ['success', 'error']) {
101
+ if (effectivePayload[key] !== undefined && !addedKeys.has(key)) {
102
+ const formatted = formatValue(effectivePayload[key]);
103
+ if (formatted) {
104
+ parts.push(`${key}: ${formatted}`);
105
+ addedKeys.add(key);
106
+ }
107
+ }
66
108
  }
67
109
 
68
- if (typeof effectivePayload.blueprint === 'string' || isRecord(effectivePayload.blueprint)) {
69
- const bp = effectivePayload.blueprint;
70
- const name = isRecord(bp) ? (bp.name || bp.path || effectivePayload.blueprintPath) : bp;
71
- parts.push(`Blueprint: ${name}`);
72
- }
110
+ // 2. Then add ALL other keys dynamically
111
+ let hasArrays = false;
112
+ for (const [key, val] of Object.entries(effectivePayload)) {
113
+ if (addedKeys.has(key)) continue;
114
+ if (skipKeys.has(key)) continue;
115
+ if (val === undefined || val === null) continue;
116
+ if (typeof val === 'string' && val.trim() === '') continue;
73
117
 
74
- if (typeof effectivePayload.sequence === 'string' || isRecord(effectivePayload.sequence)) {
75
- const seq = effectivePayload.sequence;
76
- const name = isRecord(seq) ? (seq.name || seq.path) : seq;
77
- parts.push(`Sequence: ${name}`);
78
- }
118
+ // Skip 'message' for now - handle later to avoid duplication
119
+ if (key === 'message') continue;
79
120
 
80
- // 4. Generic Key-Value Summary (Contextual)
81
- // Added: sequencePath, graphName, nodeName, variableName, memberName, scriptName, etc.
82
- const usefulKeys = [
83
- 'success', 'error', 'message', 'assets', 'folders', 'count', 'totalCount',
84
- 'saved', 'valid', 'issues', 'class', 'skeleton', 'parent',
85
- 'package', 'dependencies', 'graph', 'tags', 'metadata', 'properties'
86
- ];
87
-
88
- for (const key of usefulKeys) {
89
- if (effectivePayload[key] !== undefined && effectivePayload[key] !== null) {
90
- const val = effectivePayload[key];
91
- // Special handling for objects like metadata
92
- if (typeof val === 'object') {
93
- if (key === 'metadata' || key === 'properties' || key === 'tags') {
94
- const entries = Object.entries(val as object);
95
- // Format as "Key=Value", skip generic types if possible, or just show raw
96
- const formatted = entries.map(([k, v]) => `${k}=${v}`);
97
- const limit = 50; // Show more items as requested
98
- parts.push(`${key}: { ${formatted.slice(0, limit).join(', ')}${formatted.length > limit ? '...' : ''} }`);
99
- continue;
100
- }
101
- // Try to find a name if it's an object
102
- // Skip complex objects unless handled above
103
- continue;
104
- }
121
+ // Track if we have arrays (to skip duplicate count/totalCount later)
122
+ if (Array.isArray(val) && val.length > 0) hasArrays = true;
105
123
 
106
- const strVal = String(val);
107
- // Avoid traversing huge strings
108
- if (strVal.length > 100) continue;
124
+ // Skip count/totalCount if we already have arrays showing counts
125
+ if ((key === 'count' || key === 'totalCount') && hasArrays) continue;
109
126
 
110
- if (!parts.some(p => p.includes(strVal))) {
111
- parts.push(`${key}: ${strVal}`);
112
- }
127
+ const formatted = formatValue(val);
128
+ if (formatted) {
129
+ parts.push(`${key}: ${formatted}`);
130
+ addedKeys.add(key);
113
131
  }
114
132
  }
115
133
 
116
- // 5. Add standard status messages LAST
117
- const success = typeof payload.success === 'boolean' ? payload.success : undefined;
118
- const message = typeof payload.message === 'string' ? normalizeText(payload.message) : '';
119
- const error = typeof payload.error === 'string' ? normalizeText(payload.error) : '';
134
+ // 3. Handle message last - but skip if it duplicates existing info
135
+ const message = typeof effectivePayload.message === 'string' ? normalizeText(effectivePayload.message) : '';
136
+ if (message && message.toLowerCase() !== 'success') {
137
+ // Skip if message duplicates count info
138
+ const isDuplicateInfo = /^(found|listed|retrieved|got|loaded|created|deleted|saved|spawned)\s+\d+/i.test(message) ||
139
+ /Folders:\s*\[/.test(message) ||
140
+ /\d+\s+(assets?|folders?|items?|actors?|components?)\s+(and|in|at)/i.test(message);
120
141
 
121
- if (parts.length > 0) {
122
- if (message && message.toLowerCase() !== 'success') {
142
+ // Also skip if message content is already represented in parts
143
+ const messageInParts = parts.some(p => p.toLowerCase().includes(message.toLowerCase().slice(0, 30)));
144
+
145
+ if (!isDuplicateInfo && !messageInParts) {
123
146
  parts.push(message);
124
147
  }
125
- } else {
126
- // No data parts, rely on message/error
127
- if (message) parts.push(message);
128
- if (error) parts.push(`Error: ${error}`);
129
- if (parts.length === 0 && success !== undefined) {
130
- parts.push(success ? 'Success' : 'Failed');
131
- }
132
148
  }
133
149
 
134
- // 6. Warnings
135
- const warnings = Array.isArray(payload.warnings) ? payload.warnings : [];
150
+ // 4. Warnings at end
151
+ const warnings = Array.isArray(effectivePayload.warnings) ? effectivePayload.warnings : [];
136
152
  if (warnings.length > 0) {
137
- parts.push(`Warnings: ${warnings.length}`);
153
+ parts.push(`Warnings: ${warnings.map((w: any) => typeof w === 'string' ? w : JSON.stringify(w)).join('; ')}`);
138
154
  }
139
155
 
140
156
  return parts.length > 0 ? parts.join(' | ') : `${toolName} responded`;
@@ -14,6 +14,7 @@ export class UnrealCommandQueue {
14
14
  private isProcessing = false;
15
15
  private lastCommandTime = 0;
16
16
  private lastStatCommandTime = 0;
17
+ private processorInterval?: ReturnType<typeof setInterval>;
17
18
 
18
19
  // Config
19
20
  private readonly MIN_COMMAND_DELAY = 100;
@@ -139,13 +140,24 @@ export class UnrealCommandQueue {
139
140
  }
140
141
 
141
142
  private startProcessor(): void {
142
- setInterval(() => {
143
+ this.processorInterval = setInterval(() => {
143
144
  if (!this.isProcessing && this.queue.length > 0) {
144
145
  this.processQueue();
145
146
  }
146
147
  }, 1000);
147
148
  }
148
149
 
150
+ /**
151
+ * Stop the command queue processor and clean up the interval.
152
+ * Should be called during shutdown to allow clean process exit.
153
+ */
154
+ stopProcessor(): void {
155
+ if (this.processorInterval) {
156
+ clearInterval(this.processorInterval);
157
+ this.processorInterval = undefined;
158
+ }
159
+ }
160
+
149
161
  private delay(ms: number): Promise<void> {
150
162
  return new Promise(resolve => setTimeout(resolve, ms));
151
163
  }