unreal-engine-mcp-server 0.3.1 → 0.4.0
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.production +1 -1
- package/README.md +1 -2
- package/dist/index.js +16 -18
- package/dist/resources/assets.d.ts +3 -2
- package/dist/resources/assets.js +96 -72
- package/dist/resources/levels.js +2 -2
- package/dist/tools/assets.js +6 -2
- package/dist/tools/build_environment_advanced.js +46 -42
- package/dist/tools/consolidated-tool-definitions.d.ts +232 -15
- package/dist/tools/consolidated-tool-definitions.js +46 -3
- package/dist/tools/consolidated-tool-handlers.js +327 -717
- package/dist/tools/debug.js +4 -6
- package/dist/tools/rc.js +2 -2
- package/dist/tools/sequence.js +21 -2
- package/dist/utils/response-validator.d.ts +6 -1
- package/dist/utils/response-validator.js +41 -14
- package/package.json +4 -4
- package/server.json +2 -2
- package/src/index.ts +18 -19
- package/src/resources/assets.ts +97 -73
- package/src/resources/levels.ts +2 -2
- package/src/tools/assets.ts +6 -2
- package/src/tools/build_environment_advanced.ts +46 -42
- package/src/tools/consolidated-tool-definitions.ts +46 -3
- package/src/tools/consolidated-tool-handlers.ts +313 -746
- package/src/tools/debug.ts +4 -6
- package/src/tools/rc.ts +2 -2
- package/src/tools/sequence.ts +21 -2
- package/src/utils/response-validator.ts +46 -18
- package/dist/tools/tool-definitions.d.ts +0 -4919
- package/dist/tools/tool-definitions.js +0 -1065
- package/dist/tools/tool-handlers.d.ts +0 -47
- package/dist/tools/tool-handlers.js +0 -863
- package/src/tools/tool-definitions.ts +0 -1081
- package/src/tools/tool-handlers.ts +0 -973
package/src/tools/debug.ts
CHANGED
|
@@ -22,13 +22,11 @@ export class DebugVisualizationTools {
|
|
|
22
22
|
private async pyDraw(scriptBody: string) {
|
|
23
23
|
const script = `
|
|
24
24
|
import unreal
|
|
25
|
-
#
|
|
25
|
+
# Strict modern API: require UnrealEditorSubsystem (UE 5.1+)
|
|
26
26
|
ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
27
|
-
if ues:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
# Fallback to deprecated API if subsystem not available
|
|
31
|
-
world = unreal.EditorLevelLibrary.get_editor_world()
|
|
27
|
+
if not ues:
|
|
28
|
+
raise Exception('UnrealEditorSubsystem not available')
|
|
29
|
+
world = ues.get_editor_world()
|
|
32
30
|
${scriptBody}
|
|
33
31
|
`.trim()
|
|
34
32
|
.replace(/\r?\n/g, '\n');
|
package/src/tools/rc.ts
CHANGED
|
@@ -176,7 +176,7 @@ except Exception as e:
|
|
|
176
176
|
|
|
177
177
|
// Expose an actor by label/name into a preset
|
|
178
178
|
async exposeActor(params: { presetPath: string; actorName: string }) {
|
|
179
|
-
const python = `\nimport unreal, json\npreset_path = r"${params.presetPath}"\nactor_name = r"${params.actorName}"\ntry:\n preset = unreal.EditorAssetLibrary.load_asset(preset_path)\n if not preset:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Preset not found'}))\n else:\n actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)\n target = None\n for a in actor_sub.get_all_level_actors():\n if not a: continue\n try:\n if a.get_actor_label() == actor_name or a.get_name() == actor_name:\n target = a; break\n except Exception: pass\n if not target:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Actor not found'}))\n else:\n try:\n unreal.RemoteControlFunctionLibrary.expose_actor(preset, target,
|
|
179
|
+
const python = `\nimport unreal, json\npreset_path = r"${params.presetPath}"\nactor_name = r"${params.actorName}"\ntry:\n preset = unreal.EditorAssetLibrary.load_asset(preset_path)\n if not preset:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Preset not found'}))\n else:\n actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)\n target = None\n for a in actor_sub.get_all_level_actors():\n if not a: continue\n try:\n if a.get_actor_label() == actor_name or a.get_name() == actor_name:\n target = a; break\n except Exception: pass\n if not target:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Actor not found'}))\n else:\n try:\n # Expose with a default-initialized optional args struct (cannot pass None)\n args = unreal.RemoteControlOptionalExposeArgs()\n unreal.RemoteControlFunctionLibrary.expose_actor(preset, target, args)\n unreal.EditorAssetLibrary.save_asset(preset_path)\n print('RESULT:' + json.dumps({'success': True}))\n except Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\nexcept Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\n`.trim();
|
|
180
180
|
const resp = await this.executeWithRetry(
|
|
181
181
|
() => this.bridge.executePython(python),
|
|
182
182
|
'exposeActor'
|
|
@@ -194,7 +194,7 @@ except Exception as e:
|
|
|
194
194
|
|
|
195
195
|
// Expose a property on an object into a preset
|
|
196
196
|
async exposeProperty(params: { presetPath: string; objectPath: string; propertyName: string }) {
|
|
197
|
-
const python = `\nimport unreal, json\npreset_path = r"${params.presetPath}"\nobj_path = r"${params.objectPath}"\nprop_name = r"${params.propertyName}"\ntry:\n preset = unreal.EditorAssetLibrary.load_asset(preset_path)\n obj = unreal.load_object(None, obj_path)\n if not preset or not obj:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Preset or object not found'}))\n else:\n try:\n unreal.RemoteControlFunctionLibrary.expose_property(preset, obj, prop_name,
|
|
197
|
+
const python = `\nimport unreal, json\npreset_path = r"${params.presetPath}"\nobj_path = r"${params.objectPath}"\nprop_name = r"${params.propertyName}"\ntry:\n preset = unreal.EditorAssetLibrary.load_asset(preset_path)\n obj = unreal.load_object(None, obj_path)\n if not preset or not obj:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Preset or object not found'}))\n else:\n try:\n # Expose with default optional args struct (cannot pass None)\n args = unreal.RemoteControlOptionalExposeArgs()\n unreal.RemoteControlFunctionLibrary.expose_property(preset, obj, prop_name, args)\n unreal.EditorAssetLibrary.save_asset(preset_path)\n print('RESULT:' + json.dumps({'success': True}))\n except Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\nexcept Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\n`.trim();
|
|
198
198
|
const resp = await this.executeWithRetry(
|
|
199
199
|
() => this.bridge.executePython(python),
|
|
200
200
|
'exposeProperty'
|
package/src/tools/sequence.ts
CHANGED
|
@@ -334,12 +334,31 @@ except Exception as e:
|
|
|
334
334
|
* Play the current level sequence
|
|
335
335
|
*/
|
|
336
336
|
async play(params?: { startTime?: number; loopMode?: 'once' | 'loop' | 'pingpong' }) {
|
|
337
|
+
const loop = params?.loopMode || '';
|
|
337
338
|
const py = `
|
|
338
339
|
import unreal, json
|
|
340
|
+
|
|
341
|
+
# Helper to resolve SequencerLoopMode from a friendly string
|
|
342
|
+
def _resolve_loop_mode(mode_str):
|
|
343
|
+
try:
|
|
344
|
+
m = str(mode_str).lower()
|
|
345
|
+
slm = unreal.SequencerLoopMode
|
|
346
|
+
if m in ('once','noloop','no_loop'):
|
|
347
|
+
return getattr(slm, 'SLM_NoLoop', getattr(slm, 'NoLoop'))
|
|
348
|
+
if m in ('loop',):
|
|
349
|
+
return getattr(slm, 'SLM_Loop', getattr(slm, 'Loop'))
|
|
350
|
+
if m in ('pingpong','ping_pong'):
|
|
351
|
+
return getattr(slm, 'SLM_PingPong', getattr(slm, 'PingPong'))
|
|
352
|
+
except Exception:
|
|
353
|
+
pass
|
|
354
|
+
return None
|
|
355
|
+
|
|
339
356
|
try:
|
|
340
357
|
unreal.LevelSequenceEditorBlueprintLibrary.play()
|
|
341
|
-
|
|
342
|
-
|
|
358
|
+
loop_mode = _resolve_loop_mode('${loop}')
|
|
359
|
+
if loop_mode is not None:
|
|
360
|
+
unreal.LevelSequenceEditorBlueprintLibrary.set_loop_mode(loop_mode)
|
|
361
|
+
print('RESULT:' + json.dumps({'success': True, 'playing': True, 'loopMode': '${loop || 'default'}'}))
|
|
343
362
|
except Exception as e:
|
|
344
363
|
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
345
364
|
`.trim();
|
|
@@ -95,40 +95,68 @@ export class ResponseValidator {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
|
-
* Wrap a tool response with validation
|
|
98
|
+
* Wrap a tool response with validation and MCP-compliant content shape.
|
|
99
|
+
*
|
|
100
|
+
* MCP tools/call responses must contain a `content` array. Many internal
|
|
101
|
+
* handlers return structured JSON objects (e.g., { success, message, ... }).
|
|
102
|
+
* This wrapper serializes such objects into a single text block while keeping
|
|
103
|
+
* existing `content` responses intact.
|
|
99
104
|
*/
|
|
100
105
|
wrapResponse(toolName: string, response: any): any {
|
|
101
106
|
// Ensure response is safe to serialize first
|
|
102
107
|
try {
|
|
103
|
-
// The response should already be cleaned, but double-check
|
|
104
108
|
if (response && typeof response === 'object') {
|
|
105
|
-
// Make sure we can serialize it
|
|
106
109
|
JSON.stringify(response);
|
|
107
110
|
}
|
|
108
111
|
} catch (_error) {
|
|
109
112
|
log.error(`Response for ${toolName} contains circular references, cleaning...`);
|
|
110
113
|
response = cleanObject(response);
|
|
111
114
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
|
|
116
|
+
// If handler already returned MCP content, keep it as-is (still validate)
|
|
117
|
+
const alreadyMcpShaped = response && typeof response === 'object' && Array.isArray(response.content);
|
|
118
|
+
|
|
119
|
+
// Choose the payload to validate: if already MCP-shaped, validate the
|
|
120
|
+
// structured content extracted from text; otherwise validate the object directly.
|
|
121
|
+
const validationTarget = alreadyMcpShaped ? response : response;
|
|
122
|
+
const validation = this.validateResponse(toolName, validationTarget);
|
|
123
|
+
|
|
116
124
|
if (!validation.valid) {
|
|
117
125
|
log.warn(`Tool ${toolName} response validation failed:`, validation.errors);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// If it's already MCP-shaped, return as-is (optionally append validation meta)
|
|
129
|
+
if (alreadyMcpShaped) {
|
|
130
|
+
if (!validation.valid) {
|
|
131
|
+
try {
|
|
132
|
+
(response as any)._validation = { valid: false, errors: validation.errors };
|
|
133
|
+
} catch {}
|
|
125
134
|
}
|
|
135
|
+
return response;
|
|
126
136
|
}
|
|
127
137
|
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
138
|
+
// Otherwise, wrap structured result into MCP content
|
|
139
|
+
let text: string;
|
|
140
|
+
try {
|
|
141
|
+
// Pretty-print small objects for readability
|
|
142
|
+
text = typeof response === 'string'
|
|
143
|
+
? response
|
|
144
|
+
: JSON.stringify(response ?? { success: true }, null, 2);
|
|
145
|
+
} catch (_e) {
|
|
146
|
+
text = String(response);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const wrapped = {
|
|
150
|
+
content: [
|
|
151
|
+
{ type: 'text', text }
|
|
152
|
+
]
|
|
153
|
+
} as any;
|
|
154
|
+
|
|
155
|
+
if (!validation.valid) {
|
|
156
|
+
wrapped._validation = { valid: false, errors: validation.errors };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return wrapped;
|
|
132
160
|
}
|
|
133
161
|
|
|
134
162
|
/**
|