unreal-engine-mcp-server 0.2.1 → 0.3.1
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 +6 -1
- package/Dockerfile +11 -28
- package/README.md +0 -17
- package/dist/index.js +108 -40
- package/dist/resources/actors.js +71 -13
- package/dist/resources/assets.d.ts +1 -1
- package/dist/resources/assets.js +3 -3
- package/dist/tools/consolidated-tool-definitions.js +278 -14
- package/dist/tools/consolidated-tool-handlers.js +5 -2
- package/dist/tools/tool-definitions.js +211 -37
- package/dist/tools/tool-handlers.js +42 -9
- package/dist/unreal-bridge.d.ts +4 -1
- package/dist/unreal-bridge.js +211 -53
- package/dist/utils/http.js +4 -2
- package/dist/utils/response-validator.js +5 -4
- package/dist/utils/safe-json.js +1 -1
- package/package.json +4 -11
- package/server.json +2 -2
- package/src/index.ts +107 -42
- package/src/resources/actors.ts +51 -13
- package/src/resources/assets.ts +3 -3
- package/src/tools/consolidated-tool-definitions.ts +278 -14
- package/src/tools/consolidated-tool-handlers.ts +6 -2
- package/src/tools/tool-definitions.ts +211 -37
- package/src/tools/tool-handlers.ts +41 -9
- package/src/unreal-bridge.ts +163 -60
- package/src/utils/http.ts +7 -4
- package/src/utils/response-validator.ts +5 -4
- package/src/utils/safe-json.ts +1 -1
package/.env.production
CHANGED
|
@@ -10,11 +10,16 @@ UE_RC_WS_PORT=30020
|
|
|
10
10
|
USE_CONSOLIDATED_TOOLS=true
|
|
11
11
|
|
|
12
12
|
# Logging Level
|
|
13
|
+
# Available levels (from quietest to most verbose):
|
|
14
|
+
# - error : only errors
|
|
15
|
+
# - warn : warnings and errors
|
|
16
|
+
# - info : info, warnings, errors (default)
|
|
17
|
+
# - debug : full debug, very verbose
|
|
13
18
|
LOG_LEVEL=info
|
|
14
19
|
|
|
15
20
|
# Server Settings
|
|
16
21
|
SERVER_NAME=unreal-engine-mcp
|
|
17
|
-
SERVER_VERSION=
|
|
22
|
+
SERVER_VERSION=0.3.1
|
|
18
23
|
|
|
19
24
|
# Connection Settings
|
|
20
25
|
MAX_RETRY_ATTEMPTS=3
|
package/Dockerfile
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Multi-stage build for efficient image size
|
|
2
|
-
FROM node:
|
|
2
|
+
FROM node:22-alpine AS builder
|
|
3
3
|
|
|
4
4
|
# Set working directory
|
|
5
5
|
WORKDIR /app
|
|
@@ -18,37 +18,20 @@ COPY src ./src
|
|
|
18
18
|
RUN npm run build
|
|
19
19
|
|
|
20
20
|
# Production stage
|
|
21
|
-
FROM node:
|
|
21
|
+
FROM cgr.dev/chainguard/node:latest
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
RUN apk add --no-cache dumb-init
|
|
23
|
+
ENV NODE_ENV=production
|
|
25
24
|
|
|
26
|
-
#
|
|
27
|
-
RUN addgroup -g 1001 -S nodejs && \
|
|
28
|
-
adduser -S nodejs -u 1001
|
|
29
|
-
|
|
30
|
-
# Set working directory
|
|
25
|
+
# Chainguard node runs as nonroot by default (user: node)
|
|
31
26
|
WORKDIR /app
|
|
32
27
|
|
|
33
|
-
# Copy package
|
|
34
|
-
COPY package*.json ./
|
|
35
|
-
|
|
36
|
-
# Install production dependencies only (skip prepare script)
|
|
37
|
-
RUN npm ci --only=production --ignore-scripts && \
|
|
38
|
-
npm cache clean --force
|
|
28
|
+
# Copy only package manifests and install production deps in a clean layer
|
|
29
|
+
COPY --chown=node:node package*.json ./
|
|
30
|
+
RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force
|
|
39
31
|
|
|
40
32
|
# Copy built application from builder stage
|
|
41
|
-
COPY --from=builder /app/dist ./dist
|
|
42
|
-
|
|
43
|
-
# Change ownership to nodejs user
|
|
44
|
-
RUN chown -R nodejs:nodejs /app
|
|
45
|
-
|
|
46
|
-
# Switch to non-root user
|
|
47
|
-
USER nodejs
|
|
48
|
-
|
|
49
|
-
# MCP servers use stdio, no port exposure needed
|
|
50
|
-
# Use dumb-init to handle signals properly
|
|
51
|
-
ENTRYPOINT ["dumb-init", "--"]
|
|
33
|
+
COPY --chown=node:node --from=builder /app/dist ./dist
|
|
52
34
|
|
|
53
|
-
#
|
|
54
|
-
|
|
35
|
+
# No shell, no init needed; run as provided nonroot user
|
|
36
|
+
ENTRYPOINT ["/usr/bin/node"]
|
|
37
|
+
CMD ["dist/cli.js"]
|
package/README.md
CHANGED
|
@@ -78,23 +78,6 @@ Then enable Python execution in: Edit > Project Settings > Plugins > Remote Cont
|
|
|
78
78
|
|
|
79
79
|
### Claude Desktop / Cursor
|
|
80
80
|
|
|
81
|
-
#### For NPM Installation (Global)
|
|
82
|
-
|
|
83
|
-
```json
|
|
84
|
-
{
|
|
85
|
-
"mcpServers": {
|
|
86
|
-
"unreal-engine": {
|
|
87
|
-
"command": "unreal-engine-mcp-server",
|
|
88
|
-
"env": {
|
|
89
|
-
"UE_HOST": "127.0.0.1",
|
|
90
|
-
"UE_RC_HTTP_PORT": "30010",
|
|
91
|
-
"UE_RC_WS_PORT": "30020"
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
81
|
#### For NPM Installation (Local)
|
|
99
82
|
|
|
100
83
|
```json
|
package/dist/index.js
CHANGED
|
@@ -51,16 +51,19 @@ const metrics = {
|
|
|
51
51
|
uptime: Date.now(),
|
|
52
52
|
recentErrors: []
|
|
53
53
|
};
|
|
54
|
+
// Health check timer and last success tracking (stop pings after inactivity)
|
|
55
|
+
let healthCheckTimer;
|
|
56
|
+
let lastHealthSuccessAt = 0;
|
|
54
57
|
// Configuration
|
|
55
58
|
const CONFIG = {
|
|
56
|
-
// Tool mode: true = consolidated (
|
|
59
|
+
// Tool mode: true = consolidated (13 tools), false = individual (36+ tools)
|
|
57
60
|
USE_CONSOLIDATED_TOOLS: process.env.USE_CONSOLIDATED_TOOLS !== 'false',
|
|
58
61
|
// Connection retry settings
|
|
59
62
|
MAX_RETRY_ATTEMPTS: 3,
|
|
60
63
|
RETRY_DELAY_MS: 2000,
|
|
61
64
|
// Server info
|
|
62
65
|
SERVER_NAME: 'unreal-engine-mcp',
|
|
63
|
-
SERVER_VERSION: '0.
|
|
66
|
+
SERVER_VERSION: '0.3.1',
|
|
64
67
|
// Monitoring
|
|
65
68
|
HEALTH_CHECK_INTERVAL_MS: 30000 // 30 seconds
|
|
66
69
|
};
|
|
@@ -84,11 +87,16 @@ function trackPerformance(startTime, success) {
|
|
|
84
87
|
}
|
|
85
88
|
// Health check function
|
|
86
89
|
async function performHealthCheck(bridge) {
|
|
90
|
+
// If not connected, do not attempt any ping (stay quiet)
|
|
91
|
+
if (!bridge.isConnected) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
87
94
|
try {
|
|
88
95
|
// Use a safe echo command that doesn't affect any settings
|
|
89
96
|
await bridge.executeConsoleCommand('echo MCP Server Health Check');
|
|
90
97
|
metrics.connectionStatus = 'connected';
|
|
91
98
|
metrics.lastHealthCheck = new Date();
|
|
99
|
+
lastHealthSuccessAt = Date.now();
|
|
92
100
|
return true;
|
|
93
101
|
}
|
|
94
102
|
catch (err1) {
|
|
@@ -97,55 +105,80 @@ async function performHealthCheck(bridge) {
|
|
|
97
105
|
await bridge.executePython("import sys; sys.stdout.write('OK')");
|
|
98
106
|
metrics.connectionStatus = 'connected';
|
|
99
107
|
metrics.lastHealthCheck = new Date();
|
|
108
|
+
lastHealthSuccessAt = Date.now();
|
|
100
109
|
return true;
|
|
101
110
|
}
|
|
102
111
|
catch (err2) {
|
|
103
112
|
metrics.connectionStatus = 'error';
|
|
104
113
|
metrics.lastHealthCheck = new Date();
|
|
105
|
-
|
|
114
|
+
// Avoid noisy warnings when engine may be shutting down; log at debug
|
|
115
|
+
log.debug('Health check failed (console and python):', err1, err2);
|
|
106
116
|
return false;
|
|
107
117
|
}
|
|
108
118
|
}
|
|
109
119
|
}
|
|
110
120
|
export async function createServer() {
|
|
111
121
|
const bridge = new UnrealBridge();
|
|
122
|
+
// Disable auto-reconnect loops; connect only on-demand
|
|
123
|
+
bridge.setAutoReconnectEnabled(false);
|
|
112
124
|
// Initialize response validation with schemas
|
|
113
|
-
log.
|
|
125
|
+
log.debug('Initializing response validation...');
|
|
114
126
|
const toolDefs = CONFIG.USE_CONSOLIDATED_TOOLS ? consolidatedToolDefinitions : toolDefinitions;
|
|
115
127
|
toolDefs.forEach((tool) => {
|
|
116
128
|
if (tool.outputSchema) {
|
|
117
129
|
responseValidator.registerSchema(tool.name, tool.outputSchema);
|
|
118
130
|
}
|
|
119
131
|
});
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
metrics.connectionStatus = 'disconnected';
|
|
133
|
-
// Schedule automatic reconnection attempts
|
|
134
|
-
setInterval(async () => {
|
|
132
|
+
// Summary at debug level to avoid repeated noisy blocks in some shells
|
|
133
|
+
log.debug(`Registered ${responseValidator.getStats().totalSchemas} output schemas for validation`);
|
|
134
|
+
// Do NOT connect to Unreal at startup; connect on demand
|
|
135
|
+
log.debug('Server starting without connecting to Unreal Engine');
|
|
136
|
+
metrics.connectionStatus = 'disconnected';
|
|
137
|
+
// Health checks manager (only active when connected)
|
|
138
|
+
const startHealthChecks = () => {
|
|
139
|
+
if (healthCheckTimer)
|
|
140
|
+
return;
|
|
141
|
+
lastHealthSuccessAt = Date.now();
|
|
142
|
+
healthCheckTimer = setInterval(async () => {
|
|
143
|
+
// Only attempt health pings while connected; stay silent otherwise
|
|
135
144
|
if (!bridge.isConnected) {
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
// Optionally pause fully after 5 minutes of no success
|
|
146
|
+
const FIVE_MIN_MS = 5 * 60 * 1000;
|
|
147
|
+
if (!lastHealthSuccessAt || Date.now() - lastHealthSuccessAt > FIVE_MIN_MS) {
|
|
148
|
+
if (healthCheckTimer) {
|
|
149
|
+
clearInterval(healthCheckTimer);
|
|
150
|
+
healthCheckTimer = undefined;
|
|
151
|
+
}
|
|
152
|
+
log.info('Health checks paused after 5 minutes without a successful response');
|
|
141
153
|
}
|
|
154
|
+
return;
|
|
142
155
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
156
|
+
await performHealthCheck(bridge);
|
|
157
|
+
// Stop sending echoes if we haven't had a successful response in > 5 minutes
|
|
158
|
+
const FIVE_MIN_MS = 5 * 60 * 1000;
|
|
159
|
+
if (!lastHealthSuccessAt || Date.now() - lastHealthSuccessAt > FIVE_MIN_MS) {
|
|
160
|
+
if (healthCheckTimer) {
|
|
161
|
+
clearInterval(healthCheckTimer);
|
|
162
|
+
healthCheckTimer = undefined;
|
|
163
|
+
log.info('Health checks paused after 5 minutes without a successful response');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}, CONFIG.HEALTH_CHECK_INTERVAL_MS);
|
|
167
|
+
};
|
|
168
|
+
// On-demand connection helper
|
|
169
|
+
const ensureConnectedOnDemand = async () => {
|
|
170
|
+
if (bridge.isConnected)
|
|
171
|
+
return true;
|
|
172
|
+
const ok = await bridge.tryConnect(3, 5000, 1000);
|
|
173
|
+
if (ok) {
|
|
174
|
+
metrics.connectionStatus = 'connected';
|
|
175
|
+
startHealthChecks();
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
metrics.connectionStatus = 'disconnected';
|
|
179
|
+
}
|
|
180
|
+
return ok;
|
|
181
|
+
};
|
|
149
182
|
// Resources
|
|
150
183
|
const assetResources = new AssetResources(bridge);
|
|
151
184
|
const actorResources = new ActorResources(bridge);
|
|
@@ -230,6 +263,10 @@ export async function createServer() {
|
|
|
230
263
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
231
264
|
const uri = request.params.uri;
|
|
232
265
|
if (uri === 'ue://assets') {
|
|
266
|
+
const ok = await ensureConnectedOnDemand();
|
|
267
|
+
if (!ok) {
|
|
268
|
+
return { contents: [{ uri, mimeType: 'text/plain', text: 'Unreal Engine not connected (after 3 attempts).' }] };
|
|
269
|
+
}
|
|
233
270
|
const list = await assetResources.list('/Game', true);
|
|
234
271
|
return {
|
|
235
272
|
contents: [{
|
|
@@ -240,6 +277,10 @@ export async function createServer() {
|
|
|
240
277
|
};
|
|
241
278
|
}
|
|
242
279
|
if (uri === 'ue://actors') {
|
|
280
|
+
const ok = await ensureConnectedOnDemand();
|
|
281
|
+
if (!ok) {
|
|
282
|
+
return { contents: [{ uri, mimeType: 'text/plain', text: 'Unreal Engine not connected (after 3 attempts).' }] };
|
|
283
|
+
}
|
|
243
284
|
const list = await actorResources.listActors();
|
|
244
285
|
return {
|
|
245
286
|
contents: [{
|
|
@@ -250,6 +291,10 @@ export async function createServer() {
|
|
|
250
291
|
};
|
|
251
292
|
}
|
|
252
293
|
if (uri === 'ue://level') {
|
|
294
|
+
const ok = await ensureConnectedOnDemand();
|
|
295
|
+
if (!ok) {
|
|
296
|
+
return { contents: [{ uri, mimeType: 'text/plain', text: 'Unreal Engine not connected (after 3 attempts).' }] };
|
|
297
|
+
}
|
|
253
298
|
const level = await levelResources.getCurrentLevel();
|
|
254
299
|
return {
|
|
255
300
|
contents: [{
|
|
@@ -260,6 +305,10 @@ export async function createServer() {
|
|
|
260
305
|
};
|
|
261
306
|
}
|
|
262
307
|
if (uri === 'ue://exposed') {
|
|
308
|
+
const ok = await ensureConnectedOnDemand();
|
|
309
|
+
if (!ok) {
|
|
310
|
+
return { contents: [{ uri, mimeType: 'text/plain', text: 'Unreal Engine not connected (after 3 attempts).' }] };
|
|
311
|
+
}
|
|
263
312
|
try {
|
|
264
313
|
const exposed = await bridge.getExposed();
|
|
265
314
|
return {
|
|
@@ -282,17 +331,19 @@ export async function createServer() {
|
|
|
282
331
|
}
|
|
283
332
|
if (uri === 'ue://health') {
|
|
284
333
|
const uptimeMs = Date.now() - metrics.uptime;
|
|
285
|
-
// Query engine version and feature flags
|
|
334
|
+
// Query engine version and feature flags only when connected
|
|
286
335
|
let versionInfo = {};
|
|
287
336
|
let featureFlags = {};
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
337
|
+
if (bridge.isConnected) {
|
|
338
|
+
try {
|
|
339
|
+
versionInfo = await bridge.getEngineVersion();
|
|
340
|
+
}
|
|
341
|
+
catch { }
|
|
342
|
+
try {
|
|
343
|
+
featureFlags = await bridge.getFeatureFlags();
|
|
344
|
+
}
|
|
345
|
+
catch { }
|
|
294
346
|
}
|
|
295
|
-
catch { }
|
|
296
347
|
const health = {
|
|
297
348
|
status: metrics.connectionStatus,
|
|
298
349
|
uptime: Math.floor(uptimeMs / 1000),
|
|
@@ -328,6 +379,10 @@ export async function createServer() {
|
|
|
328
379
|
};
|
|
329
380
|
}
|
|
330
381
|
if (uri === 'ue://version') {
|
|
382
|
+
const ok = await ensureConnectedOnDemand();
|
|
383
|
+
if (!ok) {
|
|
384
|
+
return { contents: [{ uri, mimeType: 'text/plain', text: 'Unreal Engine not connected (after 3 attempts).' }] };
|
|
385
|
+
}
|
|
331
386
|
const info = await bridge.getEngineVersion();
|
|
332
387
|
return {
|
|
333
388
|
contents: [{
|
|
@@ -339,17 +394,30 @@ export async function createServer() {
|
|
|
339
394
|
}
|
|
340
395
|
throw new Error(`Unknown resource: ${uri}`);
|
|
341
396
|
});
|
|
342
|
-
// Handle tool listing - switch between consolidated (
|
|
397
|
+
// Handle tool listing - switch between consolidated (13) or individual (36) tools
|
|
343
398
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
344
399
|
log.info(`Serving ${CONFIG.USE_CONSOLIDATED_TOOLS ? 'consolidated' : 'individual'} tools`);
|
|
345
400
|
return {
|
|
346
401
|
tools: CONFIG.USE_CONSOLIDATED_TOOLS ? consolidatedToolDefinitions : toolDefinitions
|
|
347
402
|
};
|
|
348
403
|
});
|
|
349
|
-
// Handle tool calls - switch between consolidated (
|
|
404
|
+
// Handle tool calls - switch between consolidated (13) or individual (36) tools
|
|
350
405
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
351
406
|
const { name, arguments: args } = request.params;
|
|
352
407
|
const startTime = Date.now();
|
|
408
|
+
// Ensure connection only when needed, with 3 attempts
|
|
409
|
+
const connected = await ensureConnectedOnDemand();
|
|
410
|
+
if (!connected) {
|
|
411
|
+
const notConnected = {
|
|
412
|
+
content: [{ type: 'text', text: 'Unreal Engine is not connected (after 3 attempts). Please open UE and try again.' }],
|
|
413
|
+
success: false,
|
|
414
|
+
error: 'UE_NOT_CONNECTED',
|
|
415
|
+
retriable: false,
|
|
416
|
+
scope: `tool-call/${name}`
|
|
417
|
+
};
|
|
418
|
+
trackPerformance(startTime, false);
|
|
419
|
+
return notConnected;
|
|
420
|
+
}
|
|
353
421
|
// Create tools object for handler
|
|
354
422
|
const tools = {
|
|
355
423
|
actorTools,
|
package/dist/resources/actors.js
CHANGED
|
@@ -25,25 +25,83 @@ export class ActorResources {
|
|
|
25
25
|
// Use Python to get actors via EditorActorSubsystem
|
|
26
26
|
try {
|
|
27
27
|
const pythonCode = `
|
|
28
|
-
import unreal
|
|
28
|
+
import unreal, json
|
|
29
29
|
actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
30
|
-
actors = actor_subsystem.get_all_level_actors()
|
|
30
|
+
actors = actor_subsystem.get_all_level_actors() if actor_subsystem else []
|
|
31
31
|
actor_list = []
|
|
32
32
|
for actor in actors:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
try:
|
|
34
|
+
if actor:
|
|
35
|
+
actor_list.append({
|
|
36
|
+
'name': actor.get_name(),
|
|
37
|
+
'class': actor.get_class().get_name(),
|
|
38
|
+
'path': actor.get_path_name()
|
|
39
|
+
})
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
print('RESULT:' + json.dumps({'success': True, 'count': len(actor_list), 'actors': actor_list}))
|
|
40
43
|
`.trim();
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
const resp = await this.bridge.executePythonWithResult(pythonCode);
|
|
45
|
+
if (resp && typeof resp === 'object' && resp.success === true && Array.isArray(resp.actors)) {
|
|
46
|
+
this.setCache('listActors', resp);
|
|
47
|
+
return resp;
|
|
48
|
+
}
|
|
49
|
+
// Fallback manual extraction with bracket matching
|
|
50
|
+
const raw = await this.bridge.executePython(pythonCode);
|
|
51
|
+
let output = '';
|
|
52
|
+
if (raw?.LogOutput && Array.isArray(raw.LogOutput))
|
|
53
|
+
output = raw.LogOutput.map((l) => l.Output || '').join('');
|
|
54
|
+
else if (typeof raw === 'string')
|
|
55
|
+
output = raw;
|
|
56
|
+
else
|
|
57
|
+
output = JSON.stringify(raw);
|
|
58
|
+
const marker = 'RESULT:';
|
|
59
|
+
const idx = output.lastIndexOf(marker);
|
|
60
|
+
if (idx !== -1) {
|
|
61
|
+
let i = idx + marker.length;
|
|
62
|
+
while (i < output.length && output[i] !== '{')
|
|
63
|
+
i++;
|
|
64
|
+
if (i < output.length) {
|
|
65
|
+
let depth = 0, inStr = false, esc = false, j = i;
|
|
66
|
+
for (; j < output.length; j++) {
|
|
67
|
+
const ch = output[j];
|
|
68
|
+
if (esc) {
|
|
69
|
+
esc = false;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (ch === '\\') {
|
|
73
|
+
esc = true;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (ch === '"') {
|
|
77
|
+
inStr = !inStr;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (!inStr) {
|
|
81
|
+
if (ch === '{')
|
|
82
|
+
depth++;
|
|
83
|
+
else if (ch === '}') {
|
|
84
|
+
depth--;
|
|
85
|
+
if (depth === 0) {
|
|
86
|
+
j++;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const jsonStr = output.slice(i, j);
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(jsonStr);
|
|
95
|
+
this.setCache('listActors', parsed);
|
|
96
|
+
return parsed;
|
|
97
|
+
}
|
|
98
|
+
catch { }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { success: false, error: 'Failed to parse actors list' };
|
|
44
102
|
}
|
|
45
103
|
catch (err) {
|
|
46
|
-
return { error: `Failed to list actors: ${err}` };
|
|
104
|
+
return { success: false, error: `Failed to list actors: ${err}` };
|
|
47
105
|
}
|
|
48
106
|
}
|
|
49
107
|
async getActorByName(actorName) {
|
|
@@ -5,7 +5,7 @@ export declare class AssetResources {
|
|
|
5
5
|
private cache;
|
|
6
6
|
private get ttlMs();
|
|
7
7
|
private makeKey;
|
|
8
|
-
list(dir?: string,
|
|
8
|
+
list(dir?: string, _recursive?: boolean, limit?: number): Promise<any>;
|
|
9
9
|
/**
|
|
10
10
|
* List assets with pagination support
|
|
11
11
|
* @param dir Directory to list assets from
|
package/dist/resources/assets.js
CHANGED
|
@@ -9,10 +9,10 @@ export class AssetResources {
|
|
|
9
9
|
makeKey(dir, recursive, page) {
|
|
10
10
|
return page !== undefined ? `${dir}::${recursive ? 1 : 0}::${page}` : `${dir}::${recursive ? 1 : 0}`;
|
|
11
11
|
}
|
|
12
|
-
async list(dir = '/Game',
|
|
12
|
+
async list(dir = '/Game', _recursive = false, limit = 50) {
|
|
13
13
|
// ALWAYS use non-recursive listing to show only immediate children
|
|
14
14
|
// This prevents timeouts and makes navigation clearer
|
|
15
|
-
|
|
15
|
+
_recursive = false; // Force non-recursive
|
|
16
16
|
// Cache fast-path
|
|
17
17
|
try {
|
|
18
18
|
const key = this.makeKey(dir, false);
|
|
@@ -94,7 +94,7 @@ export class AssetResources {
|
|
|
94
94
|
* Directory-based listing for paths with too many assets
|
|
95
95
|
* Shows only immediate children (folders and files) to avoid timeouts
|
|
96
96
|
*/
|
|
97
|
-
async listDirectoryOnly(dir,
|
|
97
|
+
async listDirectoryOnly(dir, _recursive, limit) {
|
|
98
98
|
// Always return only immediate children to avoid timeout and improve navigation
|
|
99
99
|
try {
|
|
100
100
|
const py = `
|