@uemcp/server 0.3.0 → 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/README.md +30 -0
- package/dist/index.mjs +488 -142
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -137,6 +137,36 @@ This is intended to let an MCP client resolve the canonical Unreal project ident
|
|
|
137
137
|
- Spawns `clang-tidy` (server-configured via `CLANG_TIDY_PATH`, else `PATH`) over the translation units and returns structured findings (file, line, column, check, severity, message)
|
|
138
138
|
- Report only — does not apply fixes
|
|
139
139
|
|
|
140
|
+
## MCP Resources
|
|
141
|
+
|
|
142
|
+
Resources carry context a client can attach without spending a tool call and a model turn. Every one of them is served from in-memory state or the local filesystem: **a resource read never opens a bridge socket and never waits on the editor**, so an unreachable or absent editor produces a structured payload rather than a timeout.
|
|
143
|
+
|
|
144
|
+
All resources use the `uemcp://` URI scheme, are `application/json`, and are capped at 64 KB (`MAX_RESOURCE_PAYLOAD_BYTES`). A read that hits the cap drops entries from the end of the list and sets `truncated: true` while `totalCount` stays authoritative — the same convention the paged tools use. The authority segment (`registry`, `engines`, `workspace`) is the type discriminator.
|
|
145
|
+
|
|
146
|
+
| URI | Contents |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| `uemcp://registry` | Every registered Unreal Editor: `projectId`, `projectName`, `projectPath`, `port`, `pid`, `registeredAt`, `lastSeenAt`, plus `totalCount` and `truncated` |
|
|
149
|
+
| `uemcp://registry/{projectId}` | One registration, addressed by canonical ProjectID. Enumerable — the template's `list` callback returns the current registry, and `projectId` offers completions from it |
|
|
150
|
+
| `uemcp://engines` | Unreal installs found under `UE_INSTALLS_DIR`, ascending by version, each with `engineVersion`, `engineRoot`, and `unrealBuildToolResolved`. Source builds and GUID engine associations are excluded, matching `resolveEngine` |
|
|
151
|
+
| `uemcp://workspace/project` | The Unreal project identity of the server's working directory, resolved from `Config/DefaultGame.ini` |
|
|
152
|
+
|
|
153
|
+
`uemcp://registry/{projectId}` reports `found: false` with a `reason` for an unknown or malformed ProjectID, and `uemcp://engines` / `uemcp://workspace/project` report a `reason` when `UE_INSTALLS_DIR` is unset or the directory is not an Unreal project — none of them fail the read.
|
|
154
|
+
|
|
155
|
+
Registering or deregistering an editor over the HTTP API emits `notifications/resources/list_changed`. There is no `resources/subscribe`: the MCP SDK's `McpServer` does not implement it, so the list-changed notification is the only change signal.
|
|
156
|
+
|
|
157
|
+
## MCP Prompts
|
|
158
|
+
|
|
159
|
+
Prompts ship the workflows this server knows about as discoverable entry points for any MCP client, not just ones that can read the repo's `.agents/skills/` runbooks. They are compiled into the bundle, reference only tools this server registers, and assume no shell, no checkout, and no particular client.
|
|
160
|
+
|
|
161
|
+
| Prompt | Arguments | What it drives |
|
|
162
|
+
| --- | --- | --- |
|
|
163
|
+
| `diagnose_crash` | `projectId` | Registration check, `get_crash_callstack`, `get_editor_state`, then the output log around the crash |
|
|
164
|
+
| `diagnose_bridge_disconnect` | `projectPath` (optional) | Which leg of the bridge is broken: registry state, `get_project_id` cross-check, `ping_project`, plugin-side log evidence |
|
|
165
|
+
| `smoke_test_bridge` | `projectId` | Green-path proof: registration, `ping_project`, `get_editor_state`, and a full log subscribe-emit-drain cycle |
|
|
166
|
+
| `add_gameplay_feature` | `projectId`, `className`, `parentClass`, `moduleName` (optional) | `create_cpp_class` → `wait_for_job` → implement → `trigger_live_compile` → `run_automation_tests` → `save_dirty_packages` |
|
|
167
|
+
|
|
168
|
+
`diagnose_bridge_disconnect` and `smoke_test_bridge` are distilled from the same-named skills in `.agents/skills/`. Those files stay separate on purpose: they are maintainer runbooks that reference `just` recipes, `registry.json` paths, and pino module loggers, and the published package ships `dist` only, so they cannot be read at runtime. Each skill carries a pointer to its prompt counterpart so the two do not silently diverge.
|
|
169
|
+
|
|
140
170
|
## Configuration
|
|
141
171
|
|
|
142
172
|
- Engine toolchain (`build_target`, `generate_project_files`, `clean_build`):
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import os, { tmpdir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { z } from "zod";
|
|
@@ -2429,6 +2429,13 @@ var RegistryWriteQueue = class {
|
|
|
2429
2429
|
function createHttpApi(options) {
|
|
2430
2430
|
const app = Fastify({ logger: false });
|
|
2431
2431
|
const registryWriteQueue = options.registryWriteQueue ?? new RegistryWriteQueue();
|
|
2432
|
+
const notifyRegistryChanged = () => {
|
|
2433
|
+
try {
|
|
2434
|
+
options.onRegistryChanged?.();
|
|
2435
|
+
} catch (error) {
|
|
2436
|
+
options.logger.warn({ error }, "Failed to notify listeners that the registry changed");
|
|
2437
|
+
}
|
|
2438
|
+
};
|
|
2432
2439
|
app.setErrorHandler((error, _request, reply) => {
|
|
2433
2440
|
if (error instanceof HttpError) {
|
|
2434
2441
|
reply.code(error.statusCode).send({
|
|
@@ -2513,6 +2520,7 @@ function createHttpApi(options) {
|
|
|
2513
2520
|
projectName: entry.projectName,
|
|
2514
2521
|
projectPath: entry.projectPath
|
|
2515
2522
|
}, "Upserted Unreal Editor project registration");
|
|
2523
|
+
notifyRegistryChanged();
|
|
2516
2524
|
return {
|
|
2517
2525
|
projectId: entry.projectId,
|
|
2518
2526
|
stored: true
|
|
@@ -2543,6 +2551,7 @@ function createHttpApi(options) {
|
|
|
2543
2551
|
existed,
|
|
2544
2552
|
projectId
|
|
2545
2553
|
}, "Removed Unreal Editor project registration");
|
|
2554
|
+
if (existed) notifyRegistryChanged();
|
|
2546
2555
|
return {
|
|
2547
2556
|
projectId,
|
|
2548
2557
|
removed: true
|
|
@@ -2972,144 +2981,6 @@ function isLogLevel(value) {
|
|
|
2972
2981
|
return value === "fatal" || value === "error" || value === "warn" || value === "info" || value === "debug" || value === "trace" || value === "silent";
|
|
2973
2982
|
}
|
|
2974
2983
|
//#endregion
|
|
2975
|
-
//#region src/buildDiagnostics.ts
|
|
2976
|
-
const CLANG_DIAGNOSTIC_PATTERN = /^(?<file>.+?):(?<line>\d+):(?:(?<column>\d+):)?\s+(?<severity>fatal error|error|warning):\s+(?<message>.*)$/u;
|
|
2977
|
-
const MSVC_DIAGNOSTIC_PATTERN = /^(?<file>.+?)\((?<line>\d+)(?:,\d+)?\):\s+(?<severity>fatal error|error|warning)\s+(?<code>[A-Z]+\d+):\s+(?<message>.*)$/u;
|
|
2978
|
-
const CLANG_TIDY_PATTERN = /^(?<file>.+?):(?<line>\d+):(?<column>\d+):\s+(?<severity>error|warning):\s+(?<message>.*?)(?:\s+\[(?<check>[^\]]+)\])?$/u;
|
|
2979
|
-
const COOK_LINE_PREFIX_PATTERN = /^(?:\[[^\]]*\]\s*)*(?:[A-Za-z]+Helper:\s*[^:]*:\s*)?/u;
|
|
2980
|
-
const COOK_DIAGNOSTIC_PATTERN = /^(?<category>[A-Za-z_][A-Za-z0-9_]*):\s+(?<severity>Error|Warning)(?::)?\s+(?<message>.+)$/u;
|
|
2981
|
-
const UAT_DIAGNOSTIC_PATTERN = /^(?<severity>ERROR|WARNING):\s+(?<message>.+)$/u;
|
|
2982
|
-
function diagnosticKey(diagnostic) {
|
|
2983
|
-
return `${diagnostic.file ?? ""}|${diagnostic.line ?? ""}|${diagnostic.message}`;
|
|
2984
|
-
}
|
|
2985
|
-
function matchCookDiagnostic(rawLine) {
|
|
2986
|
-
const line = rawLine.trim().replace(COOK_LINE_PREFIX_PATTERN, "").trim();
|
|
2987
|
-
if (line.length === 0) return null;
|
|
2988
|
-
const categoryMatch = COOK_DIAGNOSTIC_PATTERN.exec(line);
|
|
2989
|
-
if (categoryMatch?.groups) {
|
|
2990
|
-
const { category, message, severity } = categoryMatch.groups;
|
|
2991
|
-
if (category === void 0 || message === void 0 || severity === void 0) return null;
|
|
2992
|
-
return {
|
|
2993
|
-
isError: severity === "Error",
|
|
2994
|
-
message: `${category}: ${message.trim()}`
|
|
2995
|
-
};
|
|
2996
|
-
}
|
|
2997
|
-
const uatMatch = UAT_DIAGNOSTIC_PATTERN.exec(line);
|
|
2998
|
-
if (!uatMatch?.groups) return null;
|
|
2999
|
-
const { message, severity } = uatMatch.groups;
|
|
3000
|
-
if (message === void 0 || severity === void 0) return null;
|
|
3001
|
-
return {
|
|
3002
|
-
isError: severity === "ERROR",
|
|
3003
|
-
message: message.trim()
|
|
3004
|
-
};
|
|
3005
|
-
}
|
|
3006
|
-
var CookDiagnosticCollector = class {
|
|
3007
|
-
maxEntriesPerSeverity;
|
|
3008
|
-
errors = [];
|
|
3009
|
-
warnings = [];
|
|
3010
|
-
seenErrors = /* @__PURE__ */ new Set();
|
|
3011
|
-
seenWarnings = /* @__PURE__ */ new Set();
|
|
3012
|
-
pending = "";
|
|
3013
|
-
truncatedFlag = false;
|
|
3014
|
-
constructor(maxEntriesPerSeverity) {
|
|
3015
|
-
this.maxEntriesPerSeverity = maxEntriesPerSeverity;
|
|
3016
|
-
}
|
|
3017
|
-
push(chunk) {
|
|
3018
|
-
const lines = (this.pending + chunk).split(/\r?\n/u);
|
|
3019
|
-
this.pending = lines.pop() ?? "";
|
|
3020
|
-
for (const line of lines) this.pushLine(line);
|
|
3021
|
-
}
|
|
3022
|
-
flush() {
|
|
3023
|
-
if (this.pending.length === 0) return;
|
|
3024
|
-
const remaining = this.pending;
|
|
3025
|
-
this.pending = "";
|
|
3026
|
-
this.pushLine(remaining);
|
|
3027
|
-
}
|
|
3028
|
-
snapshot() {
|
|
3029
|
-
return {
|
|
3030
|
-
errorCount: this.errors.length,
|
|
3031
|
-
errors: [...this.errors],
|
|
3032
|
-
truncated: this.truncatedFlag,
|
|
3033
|
-
warningCount: this.warnings.length,
|
|
3034
|
-
warnings: [...this.warnings]
|
|
3035
|
-
};
|
|
3036
|
-
}
|
|
3037
|
-
pushLine(rawLine) {
|
|
3038
|
-
const matched = matchCookDiagnostic(rawLine);
|
|
3039
|
-
if (!matched) return;
|
|
3040
|
-
const diagnostic = { message: matched.message };
|
|
3041
|
-
const target = matched.isError ? this.errors : this.warnings;
|
|
3042
|
-
const seen = matched.isError ? this.seenErrors : this.seenWarnings;
|
|
3043
|
-
const key = diagnosticKey(diagnostic);
|
|
3044
|
-
if (seen.has(key)) return;
|
|
3045
|
-
seen.add(key);
|
|
3046
|
-
if (target.length >= this.maxEntriesPerSeverity) {
|
|
3047
|
-
this.truncatedFlag = true;
|
|
3048
|
-
return;
|
|
3049
|
-
}
|
|
3050
|
-
target.push(diagnostic);
|
|
3051
|
-
}
|
|
3052
|
-
};
|
|
3053
|
-
function parseBuildDiagnostics(output) {
|
|
3054
|
-
const errors = [];
|
|
3055
|
-
const warnings = [];
|
|
3056
|
-
const seenErrors = /* @__PURE__ */ new Set();
|
|
3057
|
-
const seenWarnings = /* @__PURE__ */ new Set();
|
|
3058
|
-
for (const rawLine of output.split(/\r?\n/u)) {
|
|
3059
|
-
const line = rawLine.trim();
|
|
3060
|
-
if (line.length === 0) continue;
|
|
3061
|
-
const match = CLANG_DIAGNOSTIC_PATTERN.exec(line) ?? MSVC_DIAGNOSTIC_PATTERN.exec(line);
|
|
3062
|
-
if (!match?.groups) continue;
|
|
3063
|
-
const groups = match.groups;
|
|
3064
|
-
const { file, line: lineText, message, severity } = groups;
|
|
3065
|
-
if (file === void 0 || lineText === void 0 || message === void 0 || severity === void 0) continue;
|
|
3066
|
-
const code = typeof groups.code === "string" ? `${groups.code}: ` : "";
|
|
3067
|
-
const diagnostic = {
|
|
3068
|
-
file,
|
|
3069
|
-
line: Number.parseInt(lineText, 10),
|
|
3070
|
-
message: `${code}${message}`.trim()
|
|
3071
|
-
};
|
|
3072
|
-
const isError = severity === "error" || severity === "fatal error";
|
|
3073
|
-
const target = isError ? errors : warnings;
|
|
3074
|
-
const seen = isError ? seenErrors : seenWarnings;
|
|
3075
|
-
const key = diagnosticKey(diagnostic);
|
|
3076
|
-
if (seen.has(key)) continue;
|
|
3077
|
-
seen.add(key);
|
|
3078
|
-
target.push(diagnostic);
|
|
3079
|
-
}
|
|
3080
|
-
return {
|
|
3081
|
-
errorCount: errors.length,
|
|
3082
|
-
errors,
|
|
3083
|
-
warningCount: warnings.length,
|
|
3084
|
-
warnings
|
|
3085
|
-
};
|
|
3086
|
-
}
|
|
3087
|
-
function parseClangTidyFindings(output) {
|
|
3088
|
-
const findings = [];
|
|
3089
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3090
|
-
for (const rawLine of output.split(/\r?\n/u)) {
|
|
3091
|
-
const match = CLANG_TIDY_PATTERN.exec(rawLine.trim());
|
|
3092
|
-
if (!match?.groups) continue;
|
|
3093
|
-
const groups = match.groups;
|
|
3094
|
-
const { file, line, message } = groups;
|
|
3095
|
-
if (file === void 0 || line === void 0 || message === void 0) continue;
|
|
3096
|
-
const severity = groups.severity === "error" ? "error" : "warning";
|
|
3097
|
-
const finding = {
|
|
3098
|
-
check: groups.check ?? "unknown",
|
|
3099
|
-
file,
|
|
3100
|
-
line: Number.parseInt(line, 10),
|
|
3101
|
-
message: message.trim(),
|
|
3102
|
-
severity
|
|
3103
|
-
};
|
|
3104
|
-
if (typeof groups.column === "string") finding.column = Number.parseInt(groups.column, 10);
|
|
3105
|
-
const key = `${finding.file}|${finding.line}|${finding.column ?? ""}|${finding.check}|${finding.message}`;
|
|
3106
|
-
if (seen.has(key)) continue;
|
|
3107
|
-
seen.add(key);
|
|
3108
|
-
findings.push(finding);
|
|
3109
|
-
}
|
|
3110
|
-
return findings;
|
|
3111
|
-
}
|
|
3112
|
-
//#endregion
|
|
3113
2984
|
//#region src/engineToolchain.ts
|
|
3114
2985
|
const UNREAL_BUILD_CONFIGURATIONS = [
|
|
3115
2986
|
"Debug",
|
|
@@ -3140,6 +3011,7 @@ function cookCommandletTargetPlatform(platform) {
|
|
|
3140
3011
|
const DEFAULT_PROCESS_TIMEOUT_MS = 18e5;
|
|
3141
3012
|
const MAX_PROCESS_TIMEOUT_MS = 72e5;
|
|
3142
3013
|
const DEFAULT_PACKAGE_TIMEOUT_MS = 54e5;
|
|
3014
|
+
const ENGINE_INSTALL_DIRECTORY_PATTERN = /^UE_(\d+(?:\.\d+)*)$/u;
|
|
3143
3015
|
async function defaultFileExists(filePath) {
|
|
3144
3016
|
try {
|
|
3145
3017
|
await access(filePath);
|
|
@@ -3357,6 +3229,47 @@ async function resolveEngine(options) {
|
|
|
3357
3229
|
unrealBuildTool
|
|
3358
3230
|
};
|
|
3359
3231
|
}
|
|
3232
|
+
async function listEngineInstalls(context) {
|
|
3233
|
+
const ueInstallsDir = context.env.UE_INSTALLS_DIR?.trim();
|
|
3234
|
+
if (!ueInstallsDir) throw new EngineToolchainError("UE_INSTALLS_DIR is not set; define it in .env (see .env.example).");
|
|
3235
|
+
let entries;
|
|
3236
|
+
try {
|
|
3237
|
+
entries = await context.listDirectory(ueInstallsDir);
|
|
3238
|
+
} catch (error) {
|
|
3239
|
+
throw new EngineToolchainError(`Failed to read UE_INSTALLS_DIR ${ueInstallsDir}: ${error instanceof Error ? error.message : String(error)}`, { cause: error instanceof Error ? error : void 0 });
|
|
3240
|
+
}
|
|
3241
|
+
const installs = [];
|
|
3242
|
+
for (const entry of entries) {
|
|
3243
|
+
const engineVersion = ENGINE_INSTALL_DIRECTORY_PATTERN.exec(entry)?.[1];
|
|
3244
|
+
if (engineVersion === void 0) continue;
|
|
3245
|
+
const engineRoot = path.join(ueInstallsDir, entry);
|
|
3246
|
+
installs.push({
|
|
3247
|
+
engineRoot,
|
|
3248
|
+
engineVersion,
|
|
3249
|
+
unrealBuildToolResolved: await isUnrealBuildToolResolvable(engineRoot, context)
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
return installs.sort((left, right) => compareEngineVersions(left.engineVersion, right.engineVersion));
|
|
3253
|
+
}
|
|
3254
|
+
async function isUnrealBuildToolResolvable(engineRoot, context) {
|
|
3255
|
+
try {
|
|
3256
|
+
await resolveUnrealBuildTool(engineRoot, context);
|
|
3257
|
+
return true;
|
|
3258
|
+
} catch (error) {
|
|
3259
|
+
if (error instanceof EngineToolchainError) return false;
|
|
3260
|
+
throw error;
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
function compareEngineVersions(left, right) {
|
|
3264
|
+
const leftSegments = left.split(".");
|
|
3265
|
+
const rightSegments = right.split(".");
|
|
3266
|
+
const segmentCount = Math.max(leftSegments.length, rightSegments.length);
|
|
3267
|
+
for (let index = 0; index < segmentCount; index += 1) {
|
|
3268
|
+
const difference = Number(leftSegments[index] ?? 0) - Number(rightSegments[index] ?? 0);
|
|
3269
|
+
if (difference !== 0) return difference;
|
|
3270
|
+
}
|
|
3271
|
+
return 0;
|
|
3272
|
+
}
|
|
3360
3273
|
async function resolveUnrealBuildTool(engineRoot, context) {
|
|
3361
3274
|
if (context.platform === "win32") {
|
|
3362
3275
|
const ubtExe = path.join(engineRoot, "Engine", "Binaries", "DotNET", "UnrealBuildTool", "UnrealBuildTool.exe");
|
|
@@ -3467,6 +3380,425 @@ function buildUnrealBuildToolInvocation(engine, extraArgs) {
|
|
|
3467
3380
|
command: engine.unrealBuildTool.command
|
|
3468
3381
|
};
|
|
3469
3382
|
}
|
|
3383
|
+
const RESOURCE_MIME_TYPE = "application/json";
|
|
3384
|
+
const REGISTRY_RESOURCE_URI = "uemcp://registry";
|
|
3385
|
+
const REGISTRY_PROJECT_RESOURCE_URI_TEMPLATE = "uemcp://registry/{projectId}";
|
|
3386
|
+
const ENGINES_RESOURCE_URI = "uemcp://engines";
|
|
3387
|
+
const WORKSPACE_PROJECT_RESOURCE_URI = "uemcp://workspace/project";
|
|
3388
|
+
function readRegistryResource(dependencies) {
|
|
3389
|
+
const entries = dependencies.registry.snapshot();
|
|
3390
|
+
return clampResourceList(entries, (visibleEntries, truncated) => ({
|
|
3391
|
+
entries: visibleEntries,
|
|
3392
|
+
totalCount: entries.length,
|
|
3393
|
+
truncated
|
|
3394
|
+
}));
|
|
3395
|
+
}
|
|
3396
|
+
function readRegistryProjectResource(projectId, dependencies) {
|
|
3397
|
+
const parsedProjectId = projectIdSchema.safeParse(projectId);
|
|
3398
|
+
if (!parsedProjectId.success) return {
|
|
3399
|
+
entry: null,
|
|
3400
|
+
found: false,
|
|
3401
|
+
projectId,
|
|
3402
|
+
reason: "The requested projectId is not a canonical 32-character Unreal ProjectID.",
|
|
3403
|
+
truncated: false
|
|
3404
|
+
};
|
|
3405
|
+
const entry = dependencies.registry.getProject(parsedProjectId.data);
|
|
3406
|
+
if (!entry) return {
|
|
3407
|
+
entry: null,
|
|
3408
|
+
found: false,
|
|
3409
|
+
projectId: parsedProjectId.data,
|
|
3410
|
+
reason: "No Unreal Editor is registered with this server for that projectId.",
|
|
3411
|
+
truncated: false
|
|
3412
|
+
};
|
|
3413
|
+
return clampResourcePayload({
|
|
3414
|
+
entry,
|
|
3415
|
+
found: true,
|
|
3416
|
+
projectId: entry.projectId,
|
|
3417
|
+
truncated: false
|
|
3418
|
+
}, () => ({
|
|
3419
|
+
entry: null,
|
|
3420
|
+
found: true,
|
|
3421
|
+
projectId: parsedProjectId.data,
|
|
3422
|
+
reason: "The registry entry exceeded the resource payload budget.",
|
|
3423
|
+
truncated: true
|
|
3424
|
+
}));
|
|
3425
|
+
}
|
|
3426
|
+
async function readEnginesResource(dependencies) {
|
|
3427
|
+
const context = dependencies.engineToolchainContext ?? createEngineToolchainContext();
|
|
3428
|
+
const installsDirectory = context.env.UE_INSTALLS_DIR?.trim() ?? null;
|
|
3429
|
+
let engineInstalls;
|
|
3430
|
+
try {
|
|
3431
|
+
engineInstalls = await listEngineInstalls(context);
|
|
3432
|
+
} catch (error) {
|
|
3433
|
+
if (!(error instanceof EngineToolchainError)) throw error;
|
|
3434
|
+
dependencies.logger.warn({ error }, "Failed to enumerate installed Unreal engines for the engines resource");
|
|
3435
|
+
return {
|
|
3436
|
+
engineInstalls: [],
|
|
3437
|
+
installsDirectory: installsDirectory && installsDirectory.length > 0 ? installsDirectory : null,
|
|
3438
|
+
reason: error.message,
|
|
3439
|
+
totalCount: 0,
|
|
3440
|
+
truncated: false
|
|
3441
|
+
};
|
|
3442
|
+
}
|
|
3443
|
+
return clampResourceList(engineInstalls, (visibleInstalls, truncated) => ({
|
|
3444
|
+
engineInstalls: visibleInstalls,
|
|
3445
|
+
installsDirectory: installsDirectory && installsDirectory.length > 0 ? installsDirectory : null,
|
|
3446
|
+
totalCount: engineInstalls.length,
|
|
3447
|
+
truncated
|
|
3448
|
+
}));
|
|
3449
|
+
}
|
|
3450
|
+
async function readWorkspaceProjectResource(dependencies) {
|
|
3451
|
+
const workingDirectory = dependencies.workingDirectory ?? process.cwd();
|
|
3452
|
+
try {
|
|
3453
|
+
const resolved = await dependencies.projectIdResolver.resolveProjectIdFromProjectPath(workingDirectory);
|
|
3454
|
+
return clampResourcePayload({
|
|
3455
|
+
configPath: resolved.configPath,
|
|
3456
|
+
projectId: resolved.projectId,
|
|
3457
|
+
projectPath: resolved.projectPath,
|
|
3458
|
+
resolved: true,
|
|
3459
|
+
truncated: false,
|
|
3460
|
+
workingDirectory
|
|
3461
|
+
}, () => ({
|
|
3462
|
+
reason: "The resolved workspace project exceeded the resource payload budget.",
|
|
3463
|
+
resolved: false,
|
|
3464
|
+
truncated: true,
|
|
3465
|
+
workingDirectory
|
|
3466
|
+
}));
|
|
3467
|
+
} catch (error) {
|
|
3468
|
+
if (!(error instanceof ProjectIdResolutionError)) throw error;
|
|
3469
|
+
return {
|
|
3470
|
+
configPath: error.filePath,
|
|
3471
|
+
reason: error.reason,
|
|
3472
|
+
resolved: false,
|
|
3473
|
+
truncated: false,
|
|
3474
|
+
workingDirectory
|
|
3475
|
+
};
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
function registerMcpResources(server, dependencies) {
|
|
3479
|
+
server.registerResource("registry", REGISTRY_RESOURCE_URI, {
|
|
3480
|
+
description: "Every Unreal Editor currently registered with this server, straight from the in-memory registry. Attach this instead of calling list_registered_projects when you only need to know which editors are live and which projectId to pass to the tools. Reads no files and never contacts an editor.",
|
|
3481
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
3482
|
+
title: "Registered Unreal Editors"
|
|
3483
|
+
}, (uri) => buildResourceContents(uri, readRegistryResource(dependencies)));
|
|
3484
|
+
server.registerResource("registry-project", new ResourceTemplate(REGISTRY_PROJECT_RESOURCE_URI_TEMPLATE, {
|
|
3485
|
+
complete: { projectId: (value) => completeRegisteredProjectId(value, dependencies) },
|
|
3486
|
+
list: () => listRegistryProjectResources(dependencies)
|
|
3487
|
+
}), {
|
|
3488
|
+
description: "The registration for a single Unreal Editor, addressed by canonical 32-character ProjectID. Reports found: false with a reason rather than failing when the projectId is unknown or malformed.",
|
|
3489
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
3490
|
+
title: "Registered Unreal Editor"
|
|
3491
|
+
}, (uri, variables) => buildResourceContents(uri, readRegistryProjectResource(readTemplateVariable(variables.projectId), dependencies)));
|
|
3492
|
+
server.registerResource("engines", ENGINES_RESOURCE_URI, {
|
|
3493
|
+
description: "Unreal Engine installs discovered under UE_INSTALLS_DIR, each with its version, engine root, and whether UnrealBuildTool resolved inside it. Source builds and GUID engine associations are excluded because the build tools cannot resolve them either.",
|
|
3494
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
3495
|
+
title: "Installed Unreal Engines"
|
|
3496
|
+
}, async (uri) => buildResourceContents(uri, await readEnginesResource(dependencies)));
|
|
3497
|
+
server.registerResource("workspace-project", WORKSPACE_PROJECT_RESOURCE_URI, {
|
|
3498
|
+
description: "The Unreal project identity of the server's working directory, resolved from Config/DefaultGame.ini. Reports resolved: false with a reason when the working directory is not an Unreal project.",
|
|
3499
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
3500
|
+
title: "Workspace Unreal Project"
|
|
3501
|
+
}, async (uri) => buildResourceContents(uri, await readWorkspaceProjectResource(dependencies)));
|
|
3502
|
+
}
|
|
3503
|
+
function listRegistryProjectResources(dependencies) {
|
|
3504
|
+
return { resources: dependencies.registry.snapshot().map((entry) => ({
|
|
3505
|
+
description: `Registration for ${entry.projectName} at ${entry.projectPath}`,
|
|
3506
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
3507
|
+
name: entry.projectId,
|
|
3508
|
+
title: entry.projectName,
|
|
3509
|
+
uri: buildRegistryProjectUri(entry.projectId)
|
|
3510
|
+
})) };
|
|
3511
|
+
}
|
|
3512
|
+
function completeRegisteredProjectId(value, dependencies) {
|
|
3513
|
+
const prefix = value.trim().toLowerCase();
|
|
3514
|
+
return dependencies.registry.snapshot().map((entry) => entry.projectId).filter((projectId) => projectId.startsWith(prefix));
|
|
3515
|
+
}
|
|
3516
|
+
function buildRegistryProjectUri(projectId) {
|
|
3517
|
+
return `${REGISTRY_RESOURCE_URI}/${projectId}`;
|
|
3518
|
+
}
|
|
3519
|
+
function buildResourceContents(uri, payload) {
|
|
3520
|
+
return { contents: [{
|
|
3521
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
3522
|
+
text: serializeResourcePayload(payload),
|
|
3523
|
+
uri: uri.href
|
|
3524
|
+
}] };
|
|
3525
|
+
}
|
|
3526
|
+
function readTemplateVariable(value) {
|
|
3527
|
+
if (Array.isArray(value)) return value[0] ?? "";
|
|
3528
|
+
return value ?? "";
|
|
3529
|
+
}
|
|
3530
|
+
function clampResourceList(items, build) {
|
|
3531
|
+
const fullPayload = build(items, false);
|
|
3532
|
+
if (measureResourcePayloadBytes(fullPayload) <= 65536) return fullPayload;
|
|
3533
|
+
let smallestRejected = items.length;
|
|
3534
|
+
let largestAccepted = 0;
|
|
3535
|
+
while (largestAccepted + 1 < smallestRejected) {
|
|
3536
|
+
const candidateCount = Math.floor((largestAccepted + smallestRejected) / 2);
|
|
3537
|
+
if (measureResourcePayloadBytes(build(items.slice(0, candidateCount), true)) <= 65536) {
|
|
3538
|
+
largestAccepted = candidateCount;
|
|
3539
|
+
continue;
|
|
3540
|
+
}
|
|
3541
|
+
smallestRejected = candidateCount;
|
|
3542
|
+
}
|
|
3543
|
+
return build(items.slice(0, largestAccepted), true);
|
|
3544
|
+
}
|
|
3545
|
+
function clampResourcePayload(payload, buildMinimal) {
|
|
3546
|
+
if (measureResourcePayloadBytes(payload) <= 65536) return payload;
|
|
3547
|
+
return buildMinimal();
|
|
3548
|
+
}
|
|
3549
|
+
function measureResourcePayloadBytes(payload) {
|
|
3550
|
+
return Buffer$1.byteLength(serializeResourcePayload(payload), "utf8");
|
|
3551
|
+
}
|
|
3552
|
+
function serializeResourcePayload(payload) {
|
|
3553
|
+
return JSON.stringify(payload, null, 2);
|
|
3554
|
+
}
|
|
3555
|
+
//#endregion
|
|
3556
|
+
//#region src/mcpPrompts.ts
|
|
3557
|
+
const projectIdPromptArgument = projectIdSchema.describe("Canonical 32-character Unreal ProjectID of the target editor. Read uemcp://registry to find one.");
|
|
3558
|
+
const diagnoseCrashPromptArgs = { projectId: projectIdPromptArgument };
|
|
3559
|
+
const diagnoseBridgeDisconnectPromptArgs = { projectPath: z.string().trim().min(1).optional().describe("Filesystem path of the Unreal project directory or .uproject, when it is known.") };
|
|
3560
|
+
const smokeTestBridgePromptArgs = { projectId: projectIdPromptArgument };
|
|
3561
|
+
const addGameplayFeaturePromptArgs = {
|
|
3562
|
+
className: bridgeCreateCppClassRequestSchema.shape.className.describe("Name of the C++ class to create, without the type prefix (for example PatrolComponent)."),
|
|
3563
|
+
moduleName: bridgeCreateCppClassRequestSchema.shape.moduleName.describe("Target module for the new class. Omitted means the project’s primary game module."),
|
|
3564
|
+
parentClass: bridgeCreateCppClassRequestSchema.shape.parentClass.describe("Parent class to derive from (for example Actor, ActorComponent, or a project class)."),
|
|
3565
|
+
projectId: projectIdPromptArgument
|
|
3566
|
+
};
|
|
3567
|
+
function buildDiagnoseCrashPrompt(args) {
|
|
3568
|
+
return buildPromptResult("Investigate the most recent Unreal Editor crash for a registered project.", [
|
|
3569
|
+
`Investigate the most recent crash for Unreal project ${args.projectId}.`,
|
|
3570
|
+
"",
|
|
3571
|
+
"Work in this order and stop as soon as the cause is established:",
|
|
3572
|
+
`1. Read ${buildRegistryProjectUri(args.projectId)}, then call ping_project. A registration that is present but unreachable is the signature of a crash: deregistration only runs on a clean shutdown, so a killed process leaves its entry behind. An absent entry proves nothing on its own — the editor may never have registered, or a prior health check may already have evicted it.`,
|
|
3573
|
+
"2. Call get_crash_callstack for the project. Read the newest record first. Note the crash type, the assert or exception text, and the topmost project frames — engine frames below them are usually the victim, not the cause.",
|
|
3574
|
+
"3. Call get_editor_state to see what the editor was doing (PIE state, loaded level, current selection). A crash during PIE and a crash while idle have different suspects.",
|
|
3575
|
+
"4. Call stream_output_log with maxEvents around 50 and minVerbosity of warning. The first call establishes the subscription and returns no events; call it again with the returned cursor to drain. Look for the warnings immediately preceding the crash.",
|
|
3576
|
+
"5. If the callstack names a Blueprint, call get_blueprint_compile_errors for it; if it names a C++ symbol, call get_compile_status to check whether the running binary matches the current source.",
|
|
3577
|
+
"",
|
|
3578
|
+
"Report the crash type, the failing frame, the evidence that supports it, and the smallest change that would address it. Say so plainly if the callstack is inconclusive rather than guessing."
|
|
3579
|
+
].join("\n"));
|
|
3580
|
+
}
|
|
3581
|
+
function buildDiagnoseBridgeDisconnectPrompt(args) {
|
|
3582
|
+
const projectPathStep = args.projectPath ? `2. Call get_project_id with projectPath ${args.projectPath} to get the canonical ProjectID, then compare it to the registry entries from step 1. A mismatch means the editor and this server are reading different Config/DefaultGame.ini files.` : "2. Ask for the project path if you need one, then call get_project_id with it and compare the result to the registry entries from step 1. A mismatch means the editor and this server are reading different Config/DefaultGame.ini files.";
|
|
3583
|
+
return buildPromptResult("Diagnose why this server cannot reach a running Unreal Editor over the MCPBridge.", [
|
|
3584
|
+
"The MCP server cannot reach a running Unreal Editor. Find which leg of the connection is broken before changing anything.",
|
|
3585
|
+
"",
|
|
3586
|
+
"The two processes meet in two places: the editor registers itself over this server’s HTTP API, and this server opens a TCP bridge socket to the port the editor recorded in that registration.",
|
|
3587
|
+
"",
|
|
3588
|
+
`1. Read ${REGISTRY_RESOURCE_URI}. No entry at all means registration never landed or was evicted. An entry with a stale lastSeenAt means registration landed but health checks are failing.`,
|
|
3589
|
+
projectPathStep,
|
|
3590
|
+
"3. Call ping_project with the registered projectId. reachable: false with registrationRemoved: true means the editor’s TCP listener is gone — the editor died, or the plugin lost its port. Note that this call evicts a stale registration, so read the registry resource before calling it, not after.",
|
|
3591
|
+
"4. If the registration looks healthy but one specific tool fails, call stream_output_log and drain with the returned cursor. Plugin-side faults are logged under the LogMCPBridgeRuntime category: failures resolving the ProjectID, failures binding the bridge socket, and rejected registrations all name themselves there.",
|
|
3592
|
+
"",
|
|
3593
|
+
"Common causes, in rough order of likelihood: the editor was closed or crashed and the registration is stale; the editor is open on a different project than the one being asked about; the project directory was copied, so two directories share one ProjectID and fight over one registration; the plugin is pointed at a different server address.",
|
|
3594
|
+
"",
|
|
3595
|
+
"Report which leg is broken and the single smallest action that fixes it. Do not propose code changes until the diagnosis is complete — this failure is usually configuration, not a bug."
|
|
3596
|
+
].join("\n"));
|
|
3597
|
+
}
|
|
3598
|
+
function buildSmokeTestBridgePrompt(args) {
|
|
3599
|
+
return buildPromptResult("Prove the MCPBridge works end to end for a registered project.", [
|
|
3600
|
+
`Verify the MCPBridge is healthy for Unreal project ${args.projectId}. This is green-path verification, not fault diagnosis.`,
|
|
3601
|
+
"",
|
|
3602
|
+
`1. Read ${buildRegistryProjectUri(args.projectId)}. Expect a registration whose lastSeenAt is recent and whose port is in the 7000-7100 range.`,
|
|
3603
|
+
"2. Call ping_project. Expect reachable: true, registrationRemoved: false, and attemptsUsed: 1.",
|
|
3604
|
+
"3. Call get_editor_state. Expect the loaded level and PIE state of the editor actually open in front of the user.",
|
|
3605
|
+
"4. Exercise log streaming, which is the one path that covers subscribe, emit, drain, and unsubscribe together:",
|
|
3606
|
+
" a. Call stream_output_log with categoryAllow of [\"LogTemp\"], textContains of \"smoke-bridge-output\", and maxEvents of 10. The first call returns an empty events array and a cursor. That is correct — the subscription has nothing buffered yet.",
|
|
3607
|
+
" b. Call exec_console_command with the command: log LogTemp Warning smoke-bridge-output ping",
|
|
3608
|
+
" c. Call stream_output_log again with the same arguments plus the cursor from step 4a. Expect at least one event whose message contains smoke-bridge-output, and a nextCursor that has advanced.",
|
|
3609
|
+
"",
|
|
3610
|
+
"Report each step as pass or fail with the value that proves it. If any step fails, stop and say which one — the diagnose_bridge_disconnect prompt takes it from there. Treat an empty first drain as expected, never as a failure."
|
|
3611
|
+
].join("\n"));
|
|
3612
|
+
}
|
|
3613
|
+
function buildAddGameplayFeaturePrompt(args) {
|
|
3614
|
+
const moduleClause = args.moduleName ? ` in module ${args.moduleName}` : "";
|
|
3615
|
+
return buildPromptResult("Add a C++ gameplay class to a live Unreal project and take it through compile and test.", [
|
|
3616
|
+
`Add a C++ class ${args.className} deriving from ${args.parentClass}${moduleClause} to Unreal project ${args.projectId}, then get it compiling and tested.`,
|
|
3617
|
+
"",
|
|
3618
|
+
"Every step here is asynchronous. Each start returns a job id; do not assume success from the start call alone.",
|
|
3619
|
+
"",
|
|
3620
|
+
`1. Call create_cpp_class with className ${args.className}, parentClass ${args.parentClass}${args.moduleName ? `, moduleName ${args.moduleName}` : ""}. It returns a createClassJobId immediately.`,
|
|
3621
|
+
"2. Wait for it with wait_for_job using kind create_class, or poll get_create_class_status. Do not edit the generated files until it reports completion — the editor is still writing them.",
|
|
3622
|
+
"3. Read the generated header and source, then write the actual implementation. A generated class compiles but does nothing.",
|
|
3623
|
+
"4. Call trigger_live_compile, then wait_for_job with kind compile. On failure, read the diagnostics from get_compile_status, fix the source, and compile again. Do not proceed past a failing compile.",
|
|
3624
|
+
"5. Call list_automation_tests to find the tests covering this area, then run_automation_tests, then wait_for_job with kind test_run. Report which tests ran and which failed.",
|
|
3625
|
+
"6. Call save_dirty_packages once the work is done so nothing is left unsaved in the editor.",
|
|
3626
|
+
"",
|
|
3627
|
+
"If the project has no automation tests covering the new class, say so rather than reporting the step as passed."
|
|
3628
|
+
].join("\n"));
|
|
3629
|
+
}
|
|
3630
|
+
function registerMcpPrompts(server) {
|
|
3631
|
+
server.registerPrompt("diagnose_crash", {
|
|
3632
|
+
argsSchema: diagnoseCrashPromptArgs,
|
|
3633
|
+
description: "Walk the most recent Unreal Editor crash for a project back to its cause using the crash callstack, editor state, and output log.",
|
|
3634
|
+
title: "Diagnose Unreal Crash"
|
|
3635
|
+
}, (args) => buildDiagnoseCrashPrompt(args));
|
|
3636
|
+
server.registerPrompt("diagnose_bridge_disconnect", {
|
|
3637
|
+
argsSchema: diagnoseBridgeDisconnectPromptArgs,
|
|
3638
|
+
description: "Find which leg of the MCPBridge is broken when this server cannot reach a running Unreal Editor — missing registration, ProjectID mismatch, dead listener, or a plugin-side failure.",
|
|
3639
|
+
title: "Diagnose Bridge Disconnect"
|
|
3640
|
+
}, (args) => buildDiagnoseBridgeDisconnectPrompt(args));
|
|
3641
|
+
server.registerPrompt("smoke_test_bridge", {
|
|
3642
|
+
argsSchema: smokeTestBridgePromptArgs,
|
|
3643
|
+
description: "Prove the MCPBridge round-trips end to end for a project: registration, ping, editor state, and a full log subscribe-emit-drain cycle.",
|
|
3644
|
+
title: "Smoke-Test the Bridge"
|
|
3645
|
+
}, (args) => buildSmokeTestBridgePrompt(args));
|
|
3646
|
+
server.registerPrompt("add_gameplay_feature", {
|
|
3647
|
+
argsSchema: addGameplayFeaturePromptArgs,
|
|
3648
|
+
description: "Add a C++ gameplay class to a live Unreal project and drive it through class creation, implementation, live compile, and automation tests.",
|
|
3649
|
+
title: "Add Gameplay Feature"
|
|
3650
|
+
}, (args) => buildAddGameplayFeaturePrompt(args));
|
|
3651
|
+
}
|
|
3652
|
+
function buildPromptResult(description, text) {
|
|
3653
|
+
return {
|
|
3654
|
+
description,
|
|
3655
|
+
messages: [{
|
|
3656
|
+
content: {
|
|
3657
|
+
text,
|
|
3658
|
+
type: "text"
|
|
3659
|
+
},
|
|
3660
|
+
role: "user"
|
|
3661
|
+
}]
|
|
3662
|
+
};
|
|
3663
|
+
}
|
|
3664
|
+
//#endregion
|
|
3665
|
+
//#region src/buildDiagnostics.ts
|
|
3666
|
+
const CLANG_DIAGNOSTIC_PATTERN = /^(?<file>.+?):(?<line>\d+):(?:(?<column>\d+):)?\s+(?<severity>fatal error|error|warning):\s+(?<message>.*)$/u;
|
|
3667
|
+
const MSVC_DIAGNOSTIC_PATTERN = /^(?<file>.+?)\((?<line>\d+)(?:,\d+)?\):\s+(?<severity>fatal error|error|warning)\s+(?<code>[A-Z]+\d+):\s+(?<message>.*)$/u;
|
|
3668
|
+
const CLANG_TIDY_PATTERN = /^(?<file>.+?):(?<line>\d+):(?<column>\d+):\s+(?<severity>error|warning):\s+(?<message>.*?)(?:\s+\[(?<check>[^\]]+)\])?$/u;
|
|
3669
|
+
const COOK_LINE_PREFIX_PATTERN = /^(?:\[[^\]]*\]\s*)*(?:[A-Za-z]+Helper:\s*[^:]*:\s*)?/u;
|
|
3670
|
+
const COOK_DIAGNOSTIC_PATTERN = /^(?<category>[A-Za-z_][A-Za-z0-9_]*):\s+(?<severity>Error|Warning)(?::)?\s+(?<message>.+)$/u;
|
|
3671
|
+
const UAT_DIAGNOSTIC_PATTERN = /^(?<severity>ERROR|WARNING):\s+(?<message>.+)$/u;
|
|
3672
|
+
function diagnosticKey(diagnostic) {
|
|
3673
|
+
return `${diagnostic.file ?? ""}|${diagnostic.line ?? ""}|${diagnostic.message}`;
|
|
3674
|
+
}
|
|
3675
|
+
function matchCookDiagnostic(rawLine) {
|
|
3676
|
+
const line = rawLine.trim().replace(COOK_LINE_PREFIX_PATTERN, "").trim();
|
|
3677
|
+
if (line.length === 0) return null;
|
|
3678
|
+
const categoryMatch = COOK_DIAGNOSTIC_PATTERN.exec(line);
|
|
3679
|
+
if (categoryMatch?.groups) {
|
|
3680
|
+
const { category, message, severity } = categoryMatch.groups;
|
|
3681
|
+
if (category === void 0 || message === void 0 || severity === void 0) return null;
|
|
3682
|
+
return {
|
|
3683
|
+
isError: severity === "Error",
|
|
3684
|
+
message: `${category}: ${message.trim()}`
|
|
3685
|
+
};
|
|
3686
|
+
}
|
|
3687
|
+
const uatMatch = UAT_DIAGNOSTIC_PATTERN.exec(line);
|
|
3688
|
+
if (!uatMatch?.groups) return null;
|
|
3689
|
+
const { message, severity } = uatMatch.groups;
|
|
3690
|
+
if (message === void 0 || severity === void 0) return null;
|
|
3691
|
+
return {
|
|
3692
|
+
isError: severity === "ERROR",
|
|
3693
|
+
message: message.trim()
|
|
3694
|
+
};
|
|
3695
|
+
}
|
|
3696
|
+
var CookDiagnosticCollector = class {
|
|
3697
|
+
maxEntriesPerSeverity;
|
|
3698
|
+
errors = [];
|
|
3699
|
+
warnings = [];
|
|
3700
|
+
seenErrors = /* @__PURE__ */ new Set();
|
|
3701
|
+
seenWarnings = /* @__PURE__ */ new Set();
|
|
3702
|
+
pending = "";
|
|
3703
|
+
truncatedFlag = false;
|
|
3704
|
+
constructor(maxEntriesPerSeverity) {
|
|
3705
|
+
this.maxEntriesPerSeverity = maxEntriesPerSeverity;
|
|
3706
|
+
}
|
|
3707
|
+
push(chunk) {
|
|
3708
|
+
const lines = (this.pending + chunk).split(/\r?\n/u);
|
|
3709
|
+
this.pending = lines.pop() ?? "";
|
|
3710
|
+
for (const line of lines) this.pushLine(line);
|
|
3711
|
+
}
|
|
3712
|
+
flush() {
|
|
3713
|
+
if (this.pending.length === 0) return;
|
|
3714
|
+
const remaining = this.pending;
|
|
3715
|
+
this.pending = "";
|
|
3716
|
+
this.pushLine(remaining);
|
|
3717
|
+
}
|
|
3718
|
+
snapshot() {
|
|
3719
|
+
return {
|
|
3720
|
+
errorCount: this.errors.length,
|
|
3721
|
+
errors: [...this.errors],
|
|
3722
|
+
truncated: this.truncatedFlag,
|
|
3723
|
+
warningCount: this.warnings.length,
|
|
3724
|
+
warnings: [...this.warnings]
|
|
3725
|
+
};
|
|
3726
|
+
}
|
|
3727
|
+
pushLine(rawLine) {
|
|
3728
|
+
const matched = matchCookDiagnostic(rawLine);
|
|
3729
|
+
if (!matched) return;
|
|
3730
|
+
const diagnostic = { message: matched.message };
|
|
3731
|
+
const target = matched.isError ? this.errors : this.warnings;
|
|
3732
|
+
const seen = matched.isError ? this.seenErrors : this.seenWarnings;
|
|
3733
|
+
const key = diagnosticKey(diagnostic);
|
|
3734
|
+
if (seen.has(key)) return;
|
|
3735
|
+
seen.add(key);
|
|
3736
|
+
if (target.length >= this.maxEntriesPerSeverity) {
|
|
3737
|
+
this.truncatedFlag = true;
|
|
3738
|
+
return;
|
|
3739
|
+
}
|
|
3740
|
+
target.push(diagnostic);
|
|
3741
|
+
}
|
|
3742
|
+
};
|
|
3743
|
+
function parseBuildDiagnostics(output) {
|
|
3744
|
+
const errors = [];
|
|
3745
|
+
const warnings = [];
|
|
3746
|
+
const seenErrors = /* @__PURE__ */ new Set();
|
|
3747
|
+
const seenWarnings = /* @__PURE__ */ new Set();
|
|
3748
|
+
for (const rawLine of output.split(/\r?\n/u)) {
|
|
3749
|
+
const line = rawLine.trim();
|
|
3750
|
+
if (line.length === 0) continue;
|
|
3751
|
+
const match = CLANG_DIAGNOSTIC_PATTERN.exec(line) ?? MSVC_DIAGNOSTIC_PATTERN.exec(line);
|
|
3752
|
+
if (!match?.groups) continue;
|
|
3753
|
+
const groups = match.groups;
|
|
3754
|
+
const { file, line: lineText, message, severity } = groups;
|
|
3755
|
+
if (file === void 0 || lineText === void 0 || message === void 0 || severity === void 0) continue;
|
|
3756
|
+
const code = typeof groups.code === "string" ? `${groups.code}: ` : "";
|
|
3757
|
+
const diagnostic = {
|
|
3758
|
+
file,
|
|
3759
|
+
line: Number.parseInt(lineText, 10),
|
|
3760
|
+
message: `${code}${message}`.trim()
|
|
3761
|
+
};
|
|
3762
|
+
const isError = severity === "error" || severity === "fatal error";
|
|
3763
|
+
const target = isError ? errors : warnings;
|
|
3764
|
+
const seen = isError ? seenErrors : seenWarnings;
|
|
3765
|
+
const key = diagnosticKey(diagnostic);
|
|
3766
|
+
if (seen.has(key)) continue;
|
|
3767
|
+
seen.add(key);
|
|
3768
|
+
target.push(diagnostic);
|
|
3769
|
+
}
|
|
3770
|
+
return {
|
|
3771
|
+
errorCount: errors.length,
|
|
3772
|
+
errors,
|
|
3773
|
+
warningCount: warnings.length,
|
|
3774
|
+
warnings
|
|
3775
|
+
};
|
|
3776
|
+
}
|
|
3777
|
+
function parseClangTidyFindings(output) {
|
|
3778
|
+
const findings = [];
|
|
3779
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3780
|
+
for (const rawLine of output.split(/\r?\n/u)) {
|
|
3781
|
+
const match = CLANG_TIDY_PATTERN.exec(rawLine.trim());
|
|
3782
|
+
if (!match?.groups) continue;
|
|
3783
|
+
const groups = match.groups;
|
|
3784
|
+
const { file, line, message } = groups;
|
|
3785
|
+
if (file === void 0 || line === void 0 || message === void 0) continue;
|
|
3786
|
+
const severity = groups.severity === "error" ? "error" : "warning";
|
|
3787
|
+
const finding = {
|
|
3788
|
+
check: groups.check ?? "unknown",
|
|
3789
|
+
file,
|
|
3790
|
+
line: Number.parseInt(line, 10),
|
|
3791
|
+
message: message.trim(),
|
|
3792
|
+
severity
|
|
3793
|
+
};
|
|
3794
|
+
if (typeof groups.column === "string") finding.column = Number.parseInt(groups.column, 10);
|
|
3795
|
+
const key = `${finding.file}|${finding.line}|${finding.column ?? ""}|${finding.check}|${finding.message}`;
|
|
3796
|
+
if (seen.has(key)) continue;
|
|
3797
|
+
seen.add(key);
|
|
3798
|
+
findings.push(finding);
|
|
3799
|
+
}
|
|
3800
|
+
return findings;
|
|
3801
|
+
}
|
|
3470
3802
|
//#endregion
|
|
3471
3803
|
//#region src/moduleScaffold.ts
|
|
3472
3804
|
const MODULE_DESCRIPTOR_TYPES = [
|
|
@@ -6882,7 +7214,7 @@ function isMissingFileError(error) {
|
|
|
6882
7214
|
//#endregion
|
|
6883
7215
|
//#region src/server.ts
|
|
6884
7216
|
const SERVER_NAME = "ue-mcp-server";
|
|
6885
|
-
const SERVER_VERSION = "0.
|
|
7217
|
+
const SERVER_VERSION = "0.4.0";
|
|
6886
7218
|
const logger = createModuleLogger("bootstrap");
|
|
6887
7219
|
const enginePingLogger = createModuleLogger("engine-ping");
|
|
6888
7220
|
const httpApiLogger = createModuleLogger("http-api");
|
|
@@ -6901,8 +7233,12 @@ async function startServer(options) {
|
|
|
6901
7233
|
readTimeoutMs: options.config.startupPingTimeoutMs
|
|
6902
7234
|
});
|
|
6903
7235
|
await pruneRegistryEntries(registry, store, options.config);
|
|
7236
|
+
let notifyRegistryChanged = () => {};
|
|
6904
7237
|
const httpApi = createHttpApi({
|
|
6905
7238
|
logger: httpApiLogger,
|
|
7239
|
+
onRegistryChanged: () => {
|
|
7240
|
+
notifyRegistryChanged();
|
|
7241
|
+
},
|
|
6906
7242
|
projectIdResolver,
|
|
6907
7243
|
registry,
|
|
6908
7244
|
registryWriteQueue,
|
|
@@ -6915,7 +7251,7 @@ async function startServer(options) {
|
|
|
6915
7251
|
const listeningHttpPort = getListeningHttpPort(httpApi, options.config.httpPort);
|
|
6916
7252
|
let mcpServer;
|
|
6917
7253
|
try {
|
|
6918
|
-
|
|
7254
|
+
const createdMcpServer = createMcpServer({
|
|
6919
7255
|
logger: mcpToolLogger,
|
|
6920
7256
|
logSessionManager,
|
|
6921
7257
|
pingTimeoutMs: options.config.startupPingTimeoutMs,
|
|
@@ -6924,6 +7260,10 @@ async function startServer(options) {
|
|
|
6924
7260
|
registryWriteQueue,
|
|
6925
7261
|
store
|
|
6926
7262
|
});
|
|
7263
|
+
mcpServer = createdMcpServer;
|
|
7264
|
+
notifyRegistryChanged = () => {
|
|
7265
|
+
createdMcpServer.sendResourceListChanged();
|
|
7266
|
+
};
|
|
6927
7267
|
if (options.connectMcpTransport !== false) await mcpServer.connect(new StdioServerTransport());
|
|
6928
7268
|
} catch (error) {
|
|
6929
7269
|
try {
|
|
@@ -6977,8 +7317,14 @@ function createMcpServer(dependencies) {
|
|
|
6977
7317
|
const server = new McpServer({
|
|
6978
7318
|
name: SERVER_NAME,
|
|
6979
7319
|
version: SERVER_VERSION
|
|
6980
|
-
}, { capabilities: {
|
|
7320
|
+
}, { capabilities: {
|
|
7321
|
+
prompts: {},
|
|
7322
|
+
resources: {},
|
|
7323
|
+
tools: {}
|
|
7324
|
+
} });
|
|
6981
7325
|
registerMcpTools(server, dependencies);
|
|
7326
|
+
registerMcpResources(server, dependencies);
|
|
7327
|
+
registerMcpPrompts(server);
|
|
6982
7328
|
return server;
|
|
6983
7329
|
}
|
|
6984
7330
|
async function pruneRegistryEntries(registry, store, config) {
|