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.
- package/.github/workflows/publish-mcp.yml +1 -4
- package/.github/workflows/release-drafter.yml +2 -1
- package/CHANGELOG.md +38 -0
- 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/tools/actors.d.ts +19 -1
- package/dist/tools/actors.js +15 -5
- package/dist/tools/assets.js +1 -1
- package/dist/tools/blueprint.d.ts +12 -0
- package/dist/tools/blueprint.js +43 -14
- package/dist/tools/consolidated-tool-definitions.js +2 -1
- package/dist/tools/editor.js +3 -2
- package/dist/tools/handlers/actor-handlers.d.ts +1 -1
- package/dist/tools/handlers/actor-handlers.js +14 -8
- 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/level.js +3 -3
- package/dist/tools/lighting.d.ts +54 -7
- package/dist/tools/lighting.js +4 -4
- package/dist/tools/materials.d.ts +1 -1
- package/dist/types/tool-types.d.ts +2 -0
- package/dist/unreal-bridge.js +4 -4
- package/dist/utils/command-validator.js +6 -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/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/handler-mapping.md +4 -2
- package/package.json +1 -1
- 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/server.json +3 -3
- 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/server/tool-registry.ts +3 -3
- package/src/tools/actors.ts +44 -19
- package/src/tools/assets.ts +3 -3
- package/src/tools/blueprint.ts +115 -49
- package/src/tools/consolidated-tool-definitions.ts +2 -1
- package/src/tools/editor.ts +4 -3
- package/src/tools/handlers/actor-handlers.ts +14 -9
- package/src/tools/handlers/sequence-handlers.ts +86 -63
- package/src/tools/introspection.ts +7 -7
- package/src/tools/level.ts +6 -6
- package/src/tools/lighting.ts +19 -19
- package/src/tools/materials.ts +1 -1
- package/src/tools/sequence.ts +1 -1
- package/src/tools/ui.ts +1 -1
- package/src/types/tool-types.ts +4 -0
- package/src/unreal-bridge.ts +71 -26
- package/src/utils/command-validator.ts +46 -5
- package/src/utils/error-handler.ts +128 -45
- package/src/utils/normalize.ts +38 -16
- package/src/utils/response-validator.ts +103 -87
- 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
|
-
//
|
|
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
|
|
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
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
//
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
//
|
|
135
|
-
const warnings = Array.isArray(
|
|
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.
|
|
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
|
}
|