blueprint-extractor-mcp 2.1.0 → 2.3.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/README.md +4 -3
- package/dist/automation-controller.d.ts +94 -0
- package/dist/automation-controller.js +348 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1165 -79
- package/dist/project-controller.d.ts +38 -0
- package/dist/project-controller.js +127 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ MCP server for the Unreal Engine `BlueprintExtractor` plugin.
|
|
|
4
4
|
|
|
5
5
|
This package exposes the `blueprint-extractor` server over stdio and talks to a running Unreal Editor through the Remote Control HTTP API.
|
|
6
6
|
|
|
7
|
-
The current v2 MCP contract exposes
|
|
7
|
+
The current v2 MCP contract exposes 87 tools, 12 resources, 4 resource templates, and 4 prompts.
|
|
8
8
|
Public tools use canonical `snake_case` inputs and return structured JSON success or error envelopes.
|
|
9
9
|
|
|
10
10
|
Current surface area includes:
|
|
@@ -13,7 +13,8 @@ Current surface area includes:
|
|
|
13
13
|
- explicit-save authoring tools for the supported editor-side asset families, including compact widget extraction, incremental widget-structure ops, widget class-default routing, composable material authoring (`set_material_settings`, `add_material_expression`, `connect_material_expressions`, `bind_material_property`), and the advanced `modify_material` escape hatch
|
|
14
14
|
- dedicated Enhanced Input authoring tools for `InputAction` and `InputMappingContext` assets (`create_input_action`, `modify_input_action`, `create_input_mapping_context`, `modify_input_mapping_context`)
|
|
15
15
|
- async import and reimport tools with polling for generic assets plus typed texture and mesh helpers
|
|
16
|
-
- host-side project automation tools for external builds, Live Coding requests, restart/reconnect orchestration,
|
|
16
|
+
- host-side project automation tools for external builds, Live Coding requests, restart/reconnect orchestration, a thin window-polish helper, and runtime automation artifacts that surface verification screenshots back to the caller
|
|
17
|
+
- a shared visual-verification artifact contract across widget captures, capture diffs, and automation-run screenshots so the caller can inspect rendered results instead of relying on semantic success alone
|
|
17
18
|
- static guidance resources, resource templates, and prompts for authoring conventions, selector rules, font roles, project automation, example payloads, widget patterns, unsupported surfaces, safe UI redesign, and classic material graph guidance
|
|
18
19
|
|
|
19
20
|
## Requirements
|
|
@@ -63,7 +64,7 @@ npm install
|
|
|
63
64
|
npm run build
|
|
64
65
|
npm test
|
|
65
66
|
npm run test:pack-smoke
|
|
66
|
-
npm publish
|
|
67
|
+
npm run test:publish-gate
|
|
67
68
|
```
|
|
68
69
|
|
|
69
70
|
For the gated live smoke test:
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
export type AutomationRunStatus = 'queued' | 'running' | 'succeeded' | 'failed' | 'timed_out' | 'cancelled';
|
|
3
|
+
export interface AutomationArtifact {
|
|
4
|
+
name: string;
|
|
5
|
+
path: string;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
resourceUri: string;
|
|
8
|
+
relativePath?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface AutomationRunSummary {
|
|
11
|
+
successful: boolean;
|
|
12
|
+
totalTests?: number;
|
|
13
|
+
passedTests?: number;
|
|
14
|
+
failedTests?: number;
|
|
15
|
+
skippedTests?: number;
|
|
16
|
+
warningCount?: number;
|
|
17
|
+
reportAvailable: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface AutomationRunResult {
|
|
20
|
+
success: boolean;
|
|
21
|
+
operation: 'run_automation_tests' | 'get_automation_test_run';
|
|
22
|
+
runId: string;
|
|
23
|
+
automationFilter: string;
|
|
24
|
+
status: AutomationRunStatus;
|
|
25
|
+
terminal: boolean;
|
|
26
|
+
engineRoot: string;
|
|
27
|
+
projectPath: string;
|
|
28
|
+
projectDir: string;
|
|
29
|
+
target?: string;
|
|
30
|
+
reportOutputDir: string;
|
|
31
|
+
command: {
|
|
32
|
+
executable: string;
|
|
33
|
+
args: string[];
|
|
34
|
+
};
|
|
35
|
+
diagnostics: string[];
|
|
36
|
+
startedAt?: string;
|
|
37
|
+
completedAt?: string;
|
|
38
|
+
durationMs?: number;
|
|
39
|
+
exitCode?: number;
|
|
40
|
+
timeoutMs: number;
|
|
41
|
+
nullRhi: boolean;
|
|
42
|
+
artifacts: AutomationArtifact[];
|
|
43
|
+
summary?: AutomationRunSummary;
|
|
44
|
+
}
|
|
45
|
+
export interface AutomationRunListResult {
|
|
46
|
+
success: boolean;
|
|
47
|
+
operation: 'list_automation_test_runs';
|
|
48
|
+
includeCompleted: boolean;
|
|
49
|
+
runCount: number;
|
|
50
|
+
runs: AutomationRunResult[];
|
|
51
|
+
}
|
|
52
|
+
export interface RunAutomationTestsRequest {
|
|
53
|
+
engineRoot: string;
|
|
54
|
+
projectPath: string;
|
|
55
|
+
target?: string;
|
|
56
|
+
automationFilter: string;
|
|
57
|
+
reportOutputDir?: string;
|
|
58
|
+
timeoutMs?: number;
|
|
59
|
+
nullRhi?: boolean;
|
|
60
|
+
}
|
|
61
|
+
export interface AutomationControllerLike {
|
|
62
|
+
runAutomationTests(request: RunAutomationTestsRequest): Promise<AutomationRunResult>;
|
|
63
|
+
getAutomationTestRun(runId: string): Promise<AutomationRunResult | null>;
|
|
64
|
+
listAutomationTestRuns(includeCompleted?: boolean): Promise<AutomationRunListResult>;
|
|
65
|
+
readAutomationArtifact(runId: string, artifactName: string): Promise<AutomationArtifactReadResult | null>;
|
|
66
|
+
}
|
|
67
|
+
export interface AutomationArtifactReadResult {
|
|
68
|
+
artifact: AutomationArtifact;
|
|
69
|
+
data: Buffer;
|
|
70
|
+
}
|
|
71
|
+
export interface AutomationControllerOptions {
|
|
72
|
+
env?: NodeJS.ProcessEnv;
|
|
73
|
+
platform?: NodeJS.Platform;
|
|
74
|
+
now?: () => Date;
|
|
75
|
+
spawnProcess?: typeof spawn;
|
|
76
|
+
resolveEditorCommand?: (engineRoot: string, platform: NodeJS.Platform) => Promise<string>;
|
|
77
|
+
}
|
|
78
|
+
export declare class AutomationController implements AutomationControllerLike {
|
|
79
|
+
private readonly env;
|
|
80
|
+
private readonly platform;
|
|
81
|
+
private readonly now;
|
|
82
|
+
private readonly spawnProcess;
|
|
83
|
+
private readonly resolveEditorCommandFn;
|
|
84
|
+
private readonly runs;
|
|
85
|
+
constructor(options?: AutomationControllerOptions);
|
|
86
|
+
runAutomationTests(request: RunAutomationTestsRequest): Promise<AutomationRunResult>;
|
|
87
|
+
getAutomationTestRun(runId: string): Promise<AutomationRunResult | null>;
|
|
88
|
+
listAutomationTestRuns(includeCompleted?: boolean): Promise<AutomationRunListResult>;
|
|
89
|
+
readAutomationArtifact(runId: string, artifactName: string): Promise<AutomationArtifactReadResult | null>;
|
|
90
|
+
private cloneRun;
|
|
91
|
+
private registerArtifact;
|
|
92
|
+
private collectReportArtifacts;
|
|
93
|
+
private buildRunSummary;
|
|
94
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { createWriteStream } from 'node:fs';
|
|
5
|
+
import { dirname, extname, join, relative, resolve } from 'node:path';
|
|
6
|
+
import { resolveCommandInvocation, resolveEditorExecutable } from './project-controller.js';
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
|
|
8
|
+
function defaultNow() {
|
|
9
|
+
return new Date();
|
|
10
|
+
}
|
|
11
|
+
function sanitizeSegment(value) {
|
|
12
|
+
const sanitized = value
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/[^a-z0-9]+/gu, '_')
|
|
15
|
+
.replace(/^_+|_+$/gu, '');
|
|
16
|
+
return sanitized.length > 0 ? sanitized : 'automation';
|
|
17
|
+
}
|
|
18
|
+
function inferMimeType(filePath) {
|
|
19
|
+
switch (extname(filePath).toLowerCase()) {
|
|
20
|
+
case '.json':
|
|
21
|
+
return 'application/json';
|
|
22
|
+
case '.html':
|
|
23
|
+
case '.htm':
|
|
24
|
+
return 'text/html';
|
|
25
|
+
case '.png':
|
|
26
|
+
return 'image/png';
|
|
27
|
+
case '.jpg':
|
|
28
|
+
case '.jpeg':
|
|
29
|
+
return 'image/jpeg';
|
|
30
|
+
case '.txt':
|
|
31
|
+
case '.log':
|
|
32
|
+
return 'text/plain';
|
|
33
|
+
default:
|
|
34
|
+
return 'application/octet-stream';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function collectFilesRecursive(root) {
|
|
38
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
39
|
+
const files = await Promise.all(entries.map(async (entry) => {
|
|
40
|
+
const fullPath = join(root, entry.name);
|
|
41
|
+
if (entry.isDirectory()) {
|
|
42
|
+
return await collectFilesRecursive(fullPath);
|
|
43
|
+
}
|
|
44
|
+
return [fullPath];
|
|
45
|
+
}));
|
|
46
|
+
return files.flat();
|
|
47
|
+
}
|
|
48
|
+
function firstNumericRecord(value, keys) {
|
|
49
|
+
if (typeof value !== 'object' || value === null) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const record = value;
|
|
53
|
+
for (const key of keys) {
|
|
54
|
+
const candidate = record[key];
|
|
55
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate)) {
|
|
56
|
+
return candidate;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const nested of Object.values(record)) {
|
|
60
|
+
const found = firstNumericRecord(nested, keys);
|
|
61
|
+
if (typeof found === 'number') {
|
|
62
|
+
return found;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
function buildSummaryFromReport(report, successful) {
|
|
68
|
+
return {
|
|
69
|
+
successful,
|
|
70
|
+
totalTests: firstNumericRecord(report, ['totalTests', 'total', 'numTotal']),
|
|
71
|
+
passedTests: firstNumericRecord(report, ['passedTests', 'succeeded', 'numSucceeded', 'passCount']),
|
|
72
|
+
failedTests: firstNumericRecord(report, ['failedTests', 'failed', 'numFailed', 'failCount']),
|
|
73
|
+
skippedTests: firstNumericRecord(report, ['skippedTests', 'skipped', 'numSkipped']),
|
|
74
|
+
warningCount: firstNumericRecord(report, ['warningCount', 'warnings', 'numWarnings']),
|
|
75
|
+
reportAvailable: true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async function tryReadJson(filePath) {
|
|
79
|
+
try {
|
|
80
|
+
const raw = await readFile(filePath, 'utf8');
|
|
81
|
+
return JSON.parse(raw);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function buildResourceUri(runId, artifactName) {
|
|
88
|
+
return `blueprint://test-runs/${encodeURIComponent(runId)}/${encodeURIComponent(artifactName)}`;
|
|
89
|
+
}
|
|
90
|
+
async function resolveEditorCommand(engineRoot, platform) {
|
|
91
|
+
return await resolveEditorExecutable(engineRoot, platform, 'commandlet');
|
|
92
|
+
}
|
|
93
|
+
export class AutomationController {
|
|
94
|
+
env;
|
|
95
|
+
platform;
|
|
96
|
+
now;
|
|
97
|
+
spawnProcess;
|
|
98
|
+
resolveEditorCommandFn;
|
|
99
|
+
runs = new Map();
|
|
100
|
+
constructor(options = {}) {
|
|
101
|
+
this.env = options.env ?? process.env;
|
|
102
|
+
this.platform = options.platform ?? process.platform;
|
|
103
|
+
this.now = options.now ?? defaultNow;
|
|
104
|
+
this.spawnProcess = options.spawnProcess ?? spawn;
|
|
105
|
+
this.resolveEditorCommandFn = options.resolveEditorCommand ?? resolveEditorCommand;
|
|
106
|
+
}
|
|
107
|
+
async runAutomationTests(request) {
|
|
108
|
+
if (!request.engineRoot) {
|
|
109
|
+
throw new Error('run_automation_tests requires engine_root');
|
|
110
|
+
}
|
|
111
|
+
if (!request.projectPath) {
|
|
112
|
+
throw new Error('run_automation_tests requires project_path');
|
|
113
|
+
}
|
|
114
|
+
if (!request.automationFilter) {
|
|
115
|
+
throw new Error('run_automation_tests requires automation_filter');
|
|
116
|
+
}
|
|
117
|
+
const started = this.now();
|
|
118
|
+
const filterSlug = sanitizeSegment(request.automationFilter);
|
|
119
|
+
const runId = `${filterSlug}_${started.getTime()}_${randomUUID().slice(0, 8)}`;
|
|
120
|
+
const projectDir = dirname(request.projectPath);
|
|
121
|
+
const runRoot = request.reportOutputDir
|
|
122
|
+
? resolve(request.reportOutputDir)
|
|
123
|
+
: resolve(projectDir, 'Saved', 'BlueprintExtractor', 'AutomationRuns', runId);
|
|
124
|
+
const reportOutputDir = resolve(runRoot, 'reports');
|
|
125
|
+
const stdoutPath = resolve(runRoot, 'stdout.log');
|
|
126
|
+
const stderrPath = resolve(runRoot, 'stderr.log');
|
|
127
|
+
const summaryPath = resolve(runRoot, 'summary.json');
|
|
128
|
+
const timeoutMs = request.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
129
|
+
const nullRhi = request.nullRhi ?? true;
|
|
130
|
+
await mkdir(reportOutputDir, { recursive: true });
|
|
131
|
+
const editorCmd = await this.resolveEditorCommandFn(request.engineRoot, this.platform);
|
|
132
|
+
const args = [
|
|
133
|
+
request.projectPath,
|
|
134
|
+
'-unattended',
|
|
135
|
+
'-nop4',
|
|
136
|
+
'-nosplash',
|
|
137
|
+
...(nullRhi ? ['-NullRHI'] : []),
|
|
138
|
+
'-RCWebControlEnable',
|
|
139
|
+
'-RCWebInterfaceEnable',
|
|
140
|
+
`-ReportExportPath=${reportOutputDir}`,
|
|
141
|
+
`-ExecCmds=Automation RunTests ${request.automationFilter};Quit`,
|
|
142
|
+
];
|
|
143
|
+
const invocation = resolveCommandInvocation(editorCmd, args, this.platform, this.env);
|
|
144
|
+
const run = {
|
|
145
|
+
success: true,
|
|
146
|
+
operation: 'run_automation_tests',
|
|
147
|
+
runId,
|
|
148
|
+
automationFilter: request.automationFilter,
|
|
149
|
+
status: 'queued',
|
|
150
|
+
terminal: false,
|
|
151
|
+
engineRoot: request.engineRoot,
|
|
152
|
+
projectPath: request.projectPath,
|
|
153
|
+
projectDir,
|
|
154
|
+
target: request.target,
|
|
155
|
+
reportOutputDir,
|
|
156
|
+
command: {
|
|
157
|
+
executable: invocation.executable,
|
|
158
|
+
args: invocation.args,
|
|
159
|
+
},
|
|
160
|
+
diagnostics: [],
|
|
161
|
+
timeoutMs,
|
|
162
|
+
nullRhi,
|
|
163
|
+
artifacts: [],
|
|
164
|
+
artifactMap: new Map(),
|
|
165
|
+
};
|
|
166
|
+
this.runs.set(runId, run);
|
|
167
|
+
const stdoutStream = createWriteStream(stdoutPath, { encoding: 'utf8' });
|
|
168
|
+
const stderrStream = createWriteStream(stderrPath, { encoding: 'utf8' });
|
|
169
|
+
const child = this.spawnProcess(invocation.executable, invocation.args, {
|
|
170
|
+
cwd: projectDir,
|
|
171
|
+
env: this.env,
|
|
172
|
+
shell: false,
|
|
173
|
+
windowsVerbatimArguments: false,
|
|
174
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
175
|
+
});
|
|
176
|
+
run.status = 'running';
|
|
177
|
+
run.startedAt = started.toISOString();
|
|
178
|
+
child.stdout?.on('data', (chunk) => {
|
|
179
|
+
stdoutStream.write(chunk);
|
|
180
|
+
});
|
|
181
|
+
child.stderr?.on('data', (chunk) => {
|
|
182
|
+
stderrStream.write(chunk);
|
|
183
|
+
});
|
|
184
|
+
const finalize = async (status, exitCode, diagnostic) => {
|
|
185
|
+
if (run.terminal) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
run.status = status;
|
|
189
|
+
run.exitCode = exitCode;
|
|
190
|
+
run.completedAt = this.now().toISOString();
|
|
191
|
+
run.durationMs = new Date(run.completedAt).getTime() - started.getTime();
|
|
192
|
+
run.success = status !== 'failed' && status !== 'timed_out' && status !== 'cancelled';
|
|
193
|
+
if (diagnostic) {
|
|
194
|
+
run.diagnostics.push(diagnostic);
|
|
195
|
+
}
|
|
196
|
+
await Promise.all([
|
|
197
|
+
new Promise((resolveClose) => stdoutStream.end(resolveClose)),
|
|
198
|
+
new Promise((resolveClose) => stderrStream.end(resolveClose)),
|
|
199
|
+
]);
|
|
200
|
+
this.registerArtifact(run, 'stdout', stdoutPath, 'text/plain');
|
|
201
|
+
this.registerArtifact(run, 'stderr', stderrPath, 'text/plain');
|
|
202
|
+
const reportFiles = await this.collectReportArtifacts(runId, reportOutputDir);
|
|
203
|
+
for (const artifact of reportFiles) {
|
|
204
|
+
this.registerArtifact(run, artifact.name, artifact.path, artifact.mimeType, artifact.relativePath);
|
|
205
|
+
}
|
|
206
|
+
const summary = await this.buildRunSummary(run, reportOutputDir);
|
|
207
|
+
run.summary = summary;
|
|
208
|
+
await writeFile(summaryPath, JSON.stringify({
|
|
209
|
+
runId,
|
|
210
|
+
automationFilter: run.automationFilter,
|
|
211
|
+
status: run.status,
|
|
212
|
+
exitCode: run.exitCode,
|
|
213
|
+
durationMs: run.durationMs,
|
|
214
|
+
diagnostics: run.diagnostics,
|
|
215
|
+
reportOutputDir: run.reportOutputDir,
|
|
216
|
+
summary,
|
|
217
|
+
}, null, 2), 'utf8');
|
|
218
|
+
this.registerArtifact(run, 'summary', summaryPath, 'application/json');
|
|
219
|
+
run.terminal = true;
|
|
220
|
+
};
|
|
221
|
+
const timeoutHandle = setTimeout(() => {
|
|
222
|
+
if (!run.terminal) {
|
|
223
|
+
child.kill('SIGTERM');
|
|
224
|
+
}
|
|
225
|
+
}, timeoutMs);
|
|
226
|
+
child.on('error', async (error) => {
|
|
227
|
+
clearTimeout(timeoutHandle);
|
|
228
|
+
await finalize('failed', 1, error.message);
|
|
229
|
+
});
|
|
230
|
+
child.on('close', async (code, signal) => {
|
|
231
|
+
clearTimeout(timeoutHandle);
|
|
232
|
+
const status = signal === 'SIGTERM' && !run.terminal
|
|
233
|
+
? 'timed_out'
|
|
234
|
+
: (code ?? 1) === 0
|
|
235
|
+
? 'succeeded'
|
|
236
|
+
: 'failed';
|
|
237
|
+
const diagnostic = signal === 'SIGTERM' && status === 'timed_out'
|
|
238
|
+
? `Automation run exceeded timeout of ${timeoutMs}ms`
|
|
239
|
+
: undefined;
|
|
240
|
+
await finalize(status, code ?? 1, diagnostic);
|
|
241
|
+
});
|
|
242
|
+
return this.cloneRun(run, 'run_automation_tests');
|
|
243
|
+
}
|
|
244
|
+
async getAutomationTestRun(runId) {
|
|
245
|
+
const run = this.runs.get(runId);
|
|
246
|
+
return run ? this.cloneRun(run, 'get_automation_test_run') : null;
|
|
247
|
+
}
|
|
248
|
+
async listAutomationTestRuns(includeCompleted = true) {
|
|
249
|
+
const runs = Array.from(this.runs.values())
|
|
250
|
+
.filter((run) => includeCompleted || !run.terminal)
|
|
251
|
+
.sort((left, right) => {
|
|
252
|
+
const leftTime = left.startedAt ? Date.parse(left.startedAt) : 0;
|
|
253
|
+
const rightTime = right.startedAt ? Date.parse(right.startedAt) : 0;
|
|
254
|
+
return rightTime - leftTime;
|
|
255
|
+
})
|
|
256
|
+
.map((run) => this.cloneRun(run, 'get_automation_test_run'));
|
|
257
|
+
return {
|
|
258
|
+
success: true,
|
|
259
|
+
operation: 'list_automation_test_runs',
|
|
260
|
+
includeCompleted,
|
|
261
|
+
runCount: runs.length,
|
|
262
|
+
runs,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
async readAutomationArtifact(runId, artifactName) {
|
|
266
|
+
const run = this.runs.get(runId);
|
|
267
|
+
if (!run) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
const artifact = run.artifactMap.get(artifactName);
|
|
271
|
+
if (!artifact) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
const data = await readFile(artifact.path);
|
|
275
|
+
return {
|
|
276
|
+
artifact,
|
|
277
|
+
data,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
cloneRun(run, operation) {
|
|
281
|
+
return {
|
|
282
|
+
...run,
|
|
283
|
+
operation,
|
|
284
|
+
artifacts: [...run.artifacts],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
registerArtifact(run, name, filePath, mimeType, relativePath) {
|
|
288
|
+
const existing = run.artifactMap.get(name);
|
|
289
|
+
const artifact = {
|
|
290
|
+
name,
|
|
291
|
+
path: filePath,
|
|
292
|
+
mimeType,
|
|
293
|
+
relativePath,
|
|
294
|
+
resourceUri: buildResourceUri(run.runId, name),
|
|
295
|
+
};
|
|
296
|
+
if (existing) {
|
|
297
|
+
const index = run.artifacts.findIndex((candidate) => candidate.name === name);
|
|
298
|
+
if (index >= 0) {
|
|
299
|
+
run.artifacts[index] = artifact;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
run.artifacts.push(artifact);
|
|
304
|
+
}
|
|
305
|
+
run.artifactMap.set(name, artifact);
|
|
306
|
+
}
|
|
307
|
+
async collectReportArtifacts(runId, reportOutputDir) {
|
|
308
|
+
try {
|
|
309
|
+
const files = await collectFilesRecursive(reportOutputDir);
|
|
310
|
+
return files.map((filePath) => {
|
|
311
|
+
const rel = relative(reportOutputDir, filePath).replaceAll('\\', '/');
|
|
312
|
+
const lowerRel = rel.toLowerCase();
|
|
313
|
+
const name = lowerRel === 'index.json'
|
|
314
|
+
? 'report'
|
|
315
|
+
: lowerRel === 'index.html'
|
|
316
|
+
? 'report_html'
|
|
317
|
+
: `report__${sanitizeSegment(rel.replaceAll('/', '__'))}`;
|
|
318
|
+
return {
|
|
319
|
+
name,
|
|
320
|
+
path: filePath,
|
|
321
|
+
mimeType: inferMimeType(filePath),
|
|
322
|
+
relativePath: rel,
|
|
323
|
+
resourceUri: buildResourceUri(runId, name),
|
|
324
|
+
};
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async buildRunSummary(run, reportOutputDir) {
|
|
332
|
+
const reportArtifact = run.artifactMap.get('report');
|
|
333
|
+
if (!reportArtifact) {
|
|
334
|
+
return {
|
|
335
|
+
successful: run.status === 'succeeded',
|
|
336
|
+
reportAvailable: false,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const parsed = await tryReadJson(reportArtifact.path);
|
|
340
|
+
if (!parsed) {
|
|
341
|
+
return {
|
|
342
|
+
successful: run.status === 'succeeded',
|
|
343
|
+
reportAvailable: true,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return buildSummaryFromReport(parsed, run.status === 'succeeded');
|
|
347
|
+
}
|
|
348
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { UEClient } from './ue-client.js';
|
|
5
5
|
import { type ProjectControllerLike } from './project-controller.js';
|
|
6
|
+
import { type AutomationControllerLike } from './automation-controller.js';
|
|
6
7
|
export type UEClientLike = Pick<UEClient, 'callSubsystem'> & Partial<Pick<UEClient, 'checkConnection'>>;
|
|
7
8
|
type ToolExample = {
|
|
8
9
|
title: string;
|
|
@@ -22,5 +23,5 @@ type PromptCatalogEntry = {
|
|
|
22
23
|
};
|
|
23
24
|
export declare const exampleCatalog: Record<string, ExampleFamily>;
|
|
24
25
|
export declare const promptCatalog: Record<string, PromptCatalogEntry>;
|
|
25
|
-
export declare function createBlueprintExtractorServer(client?: UEClientLike, projectController?: ProjectControllerLike): McpServer;
|
|
26
|
+
export declare function createBlueprintExtractorServer(client?: UEClientLike, projectController?: ProjectControllerLike, automationController?: AutomationControllerLike): McpServer;
|
|
26
27
|
export {};
|