@uemcp/server 1.1.0 → 1.2.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 +19 -3
- package/dist/index.mjs +338 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,6 +44,10 @@ The MCP tool surface currently exposes:
|
|
|
44
44
|
- `redo`
|
|
45
45
|
- `open_level`
|
|
46
46
|
- `get_open_level_status`
|
|
47
|
+
- `get_source_control_status`
|
|
48
|
+
- `refresh_source_control_status`
|
|
49
|
+
- `checkout_assets`
|
|
50
|
+
- `get_source_control_job_status`
|
|
47
51
|
|
|
48
52
|
## Project Identity
|
|
49
53
|
|
|
@@ -101,9 +105,9 @@ This is intended to let an MCP client resolve the canonical Unreal project ident
|
|
|
101
105
|
### `wait_for_job`
|
|
102
106
|
|
|
103
107
|
- Blocks server-side until an async job reaches a terminal state, then returns the payload the matching `get_*_status` tool would have returned under `status`
|
|
104
|
-
- Replaces a client-side poll loop for `kind` `compile` (`trigger_live_compile` / `trigger_hot_reload`), `create_class`, `test_run`, and `
|
|
108
|
+
- Replaces a client-side poll loop for `kind` `compile` (`trigger_live_compile` / `trigger_hot_reload`), `create_class`, `test_run`, `pie`, and `source_control` (`checkout_assets` / `refresh_source_control_status`); `kind` is required because job ids are bare GUIDs
|
|
105
109
|
- `outcome` is `completed`, `timed_out` (last observed status still returned), or `unknown_job` (unknown/expired/cancelled id, returned on the first poll)
|
|
106
|
-
- Per-kind default budgets (compile 120s, create_class 60s, pie 30s, test_run 300s), clamped to 600s; polls back off from 250ms (1s for test runs) and a transient bridge read timeout or dropped socket is retried rather than failing the wait
|
|
110
|
+
- Per-kind default budgets (compile 120s, create_class 60s, pie 30s, source_control 60s, test_run 300s), clamped to 600s; polls back off from 250ms (1s for test runs) and a transient bridge read timeout or dropped socket is retried rather than failing the wait
|
|
107
111
|
- For `test_run`, `status.results` is the first page — page the rest with `get_test_run_status` and `nextCursor`
|
|
108
112
|
|
|
109
113
|
### `build_target` / `generate_project_files` / `clean_build`
|
|
@@ -182,6 +186,18 @@ This is intended to let an MCP client resolve the canonical Unreal project ident
|
|
|
182
186
|
- Actor paths do not survive a load: re-run `list_actors` against the new world rather than reusing paths from the old one
|
|
183
187
|
- One load at a time per editor; a second request while one is running is refused with `open_level_in_progress` rather than queued
|
|
184
188
|
|
|
189
|
+
### `get_source_control_status` / `refresh_source_control_status` / `checkout_assets` / `get_source_control_job_status`
|
|
190
|
+
|
|
191
|
+
- The prerequisite for every asset write on a team project. Under Perforce or Git-LFS file locking a `.uasset` stays read-only until it is checked out, so a write that skips this fails later at save time — several tool calls away from its cause
|
|
192
|
+
- `get_source_control_status` answers **inline**: it reads the provider's own state cache, which is a memory lookup rather than a server round trip. It reports the `provider` name, `enabled` (is source control configured at all), `available` (is it currently connected), and one entry per requested path with a `state` of `checked_out_by_me`, `checked_out_by_other` (plus `checkedOutBy` naming the holder), `marked_for_add`, `not_checked_out`, `not_in_depot`, or `unknown`
|
|
193
|
+
- `enabled` is what separates "no source control configured" from "configured, and this file is simply not checked out". Those are never collapsed into one answer
|
|
194
|
+
- A file the editor has never asked the server about reports `unknown`, **not** a guess. `refresh_source_control_status` is how it becomes a real answer. A path that resolves to no package on disk also reports `unknown`, but carries an `error` saying so — an unresolvable path never passes as a depot answer
|
|
195
|
+
- `refresh_source_control_status` and `checkout_assets` talk to the source control server over the network, which runs far past the plugin's 5 s game-thread budget, so neither runs inline: each returns `state: 'accepted'` with a `sourceControlJobId`. Poll it with `get_source_control_job_status`, or wait with `wait_for_job` using `kind: 'source_control'`
|
|
196
|
+
- The poll reports `successCount` / `failureCount` and one result per file with its post-operation `state`, so a checkout is confirmed without a second query. One asset the provider refuses does not abort the rest — it reports `ok: false` with its own `error`
|
|
197
|
+
- Checkout only. Submitting is never done for you, and a checked-out asset stays checked out until a human submits or reverts it
|
|
198
|
+
- A request against an editor with no provider configured is refused with `source_control_disabled`, and one against a configured-but-offline provider with `source_control_unavailable`. Neither degrades into a filesystem check
|
|
199
|
+
- In a non-editor plugin build all four report `state: 'unsupported'` and schedule nothing
|
|
200
|
+
|
|
185
201
|
### `read_data_table` / `write_data_table_row`
|
|
186
202
|
|
|
187
203
|
- A DataTable is where a project keeps its tuning data, and it is a binary `.uasset` — `find_assets` can locate one but nothing can read it off disk, so these are the only way in. `read_data_table` loads the asset and reports the row struct it is typed on, an ordered column list (`{ name, type }`, using the same type rendering `get_class_reflection` emits), and each row as a column-name-to-value map
|
|
@@ -192,7 +208,7 @@ This is intended to let an MCP client resolve the canonical Unreal project ident
|
|
|
192
208
|
- Numeric and boolean values are range-checked **before** `ImportText` rather than after: the engine's importer takes `not-a-number` as `0` and reports success, and silent coercion is exactly the failure a tuning-data write cannot afford
|
|
193
209
|
- The write runs inside a single `FScopedTransaction` (Ctrl+Z reverses it) and marks the package dirty **without saving** — compose with `save_dirty_packages`. `dryRun: true` validates everything and reports `state: 'validated'`, opening no transaction
|
|
194
210
|
- Deleting a row that does not exist is **not** a failure: it reports `rowExisted: false` with an explanatory message, because the table is already in the requested state
|
|
195
|
-
- A `.uasset` sits read-only on disk until it is checked out under Perforce or Git-LFS file locking. That case is detected before anything is mutated
|
|
211
|
+
- A `.uasset` sits read-only on disk until it is checked out under Perforce or Git-LFS file locking. That case is detected before anything is mutated rather than surfacing later as an unexplained save error. Where a provider is configured the answer comes from the provider itself — `source_control_checkout_required` for a file that is simply not checked out, `source_control_checked_out_by_other` naming the holder when someone else has it — and only where none is does it fall back to the on-disk read-only check. Fix either with `checkout_assets`
|
|
196
212
|
- In a non-editor plugin build `read_data_table` fails with `unsupported` and `write_data_table_row` returns `state: 'unsupported'`
|
|
197
213
|
|
|
198
214
|
### `export_data_table` / `import_data_table`
|
package/dist/index.mjs
CHANGED
|
@@ -477,6 +477,27 @@ const BRIDGE_DATA_TABLE_WRITE_STATES = [
|
|
|
477
477
|
const BRIDGE_DATA_TABLE_FORMATS = ["json", "csv"];
|
|
478
478
|
const BRIDGE_DATA_TABLE_MAX_EXPORT_CHARS = 24576;
|
|
479
479
|
const BRIDGE_DATA_TABLE_MAX_IMPORT_CHARS = 16384;
|
|
480
|
+
const BRIDGE_GET_SOURCE_CONTROL_STATUS_REQUEST_TYPE = "get_source_control_status";
|
|
481
|
+
const BRIDGE_REFRESH_SOURCE_CONTROL_STATUS_REQUEST_TYPE = "refresh_source_control_status";
|
|
482
|
+
const BRIDGE_CHECKOUT_ASSETS_REQUEST_TYPE = "checkout_assets";
|
|
483
|
+
const BRIDGE_GET_SOURCE_CONTROL_JOB_STATUS_REQUEST_TYPE = "get_source_control_job_status";
|
|
484
|
+
const BRIDGE_SOURCE_CONTROL_FILE_STATES = [
|
|
485
|
+
"checked_out_by_me",
|
|
486
|
+
"checked_out_by_other",
|
|
487
|
+
"marked_for_add",
|
|
488
|
+
"not_checked_out",
|
|
489
|
+
"not_in_depot",
|
|
490
|
+
"unknown"
|
|
491
|
+
];
|
|
492
|
+
const BRIDGE_SOURCE_CONTROL_STATUS_STATES = ["reported", "unsupported"];
|
|
493
|
+
const BRIDGE_SOURCE_CONTROL_JOB_ACCEPT_STATES = ["accepted", "unsupported"];
|
|
494
|
+
const BRIDGE_SOURCE_CONTROL_JOB_STATES = [
|
|
495
|
+
"running",
|
|
496
|
+
"succeeded",
|
|
497
|
+
"failed",
|
|
498
|
+
"unknown_job"
|
|
499
|
+
];
|
|
500
|
+
const BRIDGE_SOURCE_CONTROL_OPERATIONS = ["checkout", "refresh"];
|
|
480
501
|
const requestIdSchema = z.string().trim().min(1);
|
|
481
502
|
const nonEmptyStringSchema = z.string().trim().min(1);
|
|
482
503
|
const boundedStringSchema = z.string().trim().max(512);
|
|
@@ -595,6 +616,13 @@ const dataTableFormatSchema = z.enum(BRIDGE_DATA_TABLE_FORMATS);
|
|
|
595
616
|
const dataTableValueMapSchema = z.record(z.string().trim().min(1).max(256), z.string().max(BRIDGE_DATA_TABLE_VALUE_MAX_CHARS)).refine((value) => Object.keys(value).length <= 200, { message: `A write carries at most ${String(200)} columns.` });
|
|
596
617
|
const dataTableImportContentSchema = z.string().min(1).max(BRIDGE_DATA_TABLE_MAX_IMPORT_CHARS);
|
|
597
618
|
const dataTableAssetNameSchema = z.string().trim().min(1).max(256).refine((value) => !value.includes("/") && !value.includes("."), { message: "An asset name must not contain a path separator; use destinationPath to move the asset." });
|
|
619
|
+
const sourceControlAssetPathBatchSchema = z.array(assetPathSchema).min(1).max(64);
|
|
620
|
+
const sourceControlFileStateSchema = z.enum(BRIDGE_SOURCE_CONTROL_FILE_STATES);
|
|
621
|
+
const sourceControlStatusStateSchema = z.enum(BRIDGE_SOURCE_CONTROL_STATUS_STATES);
|
|
622
|
+
const sourceControlJobAcceptStateSchema = z.enum(BRIDGE_SOURCE_CONTROL_JOB_ACCEPT_STATES);
|
|
623
|
+
const sourceControlJobStateSchema = z.enum(BRIDGE_SOURCE_CONTROL_JOB_STATES);
|
|
624
|
+
const sourceControlOperationSchema = z.enum(BRIDGE_SOURCE_CONTROL_OPERATIONS);
|
|
625
|
+
const sourceControlJobIdSchema = z.string().trim().min(1);
|
|
598
626
|
const vectorInputSchema = z.object({
|
|
599
627
|
x: z.number().default(0),
|
|
600
628
|
y: z.number().default(0),
|
|
@@ -1080,6 +1108,30 @@ const bridgeRenameDataTableRowRequestSchema = z.object({
|
|
|
1080
1108
|
type: z.literal(BRIDGE_RENAME_DATA_TABLE_ROW_REQUEST_TYPE),
|
|
1081
1109
|
version: z.literal(2)
|
|
1082
1110
|
});
|
|
1111
|
+
const bridgeGetSourceControlStatusRequestSchema = z.object({
|
|
1112
|
+
assetPaths: sourceControlAssetPathBatchSchema,
|
|
1113
|
+
requestId: requestIdSchema,
|
|
1114
|
+
type: z.literal(BRIDGE_GET_SOURCE_CONTROL_STATUS_REQUEST_TYPE),
|
|
1115
|
+
version: z.literal(2)
|
|
1116
|
+
});
|
|
1117
|
+
const bridgeRefreshSourceControlStatusRequestSchema = z.object({
|
|
1118
|
+
assetPaths: sourceControlAssetPathBatchSchema,
|
|
1119
|
+
requestId: requestIdSchema,
|
|
1120
|
+
type: z.literal(BRIDGE_REFRESH_SOURCE_CONTROL_STATUS_REQUEST_TYPE),
|
|
1121
|
+
version: z.literal(2)
|
|
1122
|
+
});
|
|
1123
|
+
const bridgeCheckoutAssetsRequestSchema = z.object({
|
|
1124
|
+
assetPaths: sourceControlAssetPathBatchSchema,
|
|
1125
|
+
requestId: requestIdSchema,
|
|
1126
|
+
type: z.literal(BRIDGE_CHECKOUT_ASSETS_REQUEST_TYPE),
|
|
1127
|
+
version: z.literal(2)
|
|
1128
|
+
});
|
|
1129
|
+
const bridgeGetSourceControlJobStatusRequestSchema = z.object({
|
|
1130
|
+
requestId: requestIdSchema,
|
|
1131
|
+
sourceControlJobId: sourceControlJobIdSchema,
|
|
1132
|
+
type: z.literal(BRIDGE_GET_SOURCE_CONTROL_JOB_STATUS_REQUEST_TYPE),
|
|
1133
|
+
version: z.literal(2)
|
|
1134
|
+
});
|
|
1083
1135
|
const bridgeGetAssetDependenciesRequestSchema = z.object({
|
|
1084
1136
|
assetPath: assetPathSchema,
|
|
1085
1137
|
categories: z.array(assetDependencyCategorySchema).max(BRIDGE_ASSET_DEPENDENCY_CATEGORIES.length).default(["package"]),
|
|
@@ -1943,6 +1995,90 @@ const bridgeRenameDataTableRowSuccessResponseSchema = z.object({
|
|
|
1943
1995
|
type: z.literal(BRIDGE_RENAME_DATA_TABLE_ROW_REQUEST_TYPE),
|
|
1944
1996
|
version: z.literal(2)
|
|
1945
1997
|
}).refine(isLevelEditStateConsistent, levelEditStateRefinement);
|
|
1998
|
+
const bridgeSourceControlFileStateSchema = z.object({
|
|
1999
|
+
assetPath: z.string(),
|
|
2000
|
+
checkedOutBy: z.string().trim().min(1).optional(),
|
|
2001
|
+
error: z.string().optional(),
|
|
2002
|
+
index: z.number().int().min(0),
|
|
2003
|
+
state: sourceControlFileStateSchema
|
|
2004
|
+
});
|
|
2005
|
+
const bridgeGetSourceControlStatusSuccessResponseSchema = z.object({
|
|
2006
|
+
available: z.boolean(),
|
|
2007
|
+
enabled: z.boolean(),
|
|
2008
|
+
files: z.array(bridgeSourceControlFileStateSchema).max(64),
|
|
2009
|
+
message: nonEmptyStringSchema.optional(),
|
|
2010
|
+
ok: z.literal(true),
|
|
2011
|
+
provider: z.string().trim().min(1),
|
|
2012
|
+
requestId: requestIdSchema,
|
|
2013
|
+
state: sourceControlStatusStateSchema,
|
|
2014
|
+
type: z.literal(BRIDGE_GET_SOURCE_CONTROL_STATUS_REQUEST_TYPE),
|
|
2015
|
+
version: z.literal(2)
|
|
2016
|
+
}).refine((value) => value.state === "reported" || value.files.length === 0, {
|
|
2017
|
+
message: "An unsupported get_source_control_status response must carry no file entries.",
|
|
2018
|
+
path: ["files"]
|
|
2019
|
+
});
|
|
2020
|
+
const sourceControlJobAcceptFields = {
|
|
2021
|
+
message: nonEmptyStringSchema.optional(),
|
|
2022
|
+
ok: z.literal(true),
|
|
2023
|
+
requestId: requestIdSchema,
|
|
2024
|
+
sourceControlJobId: sourceControlJobIdSchema.optional(),
|
|
2025
|
+
state: sourceControlJobAcceptStateSchema,
|
|
2026
|
+
version: z.literal(2)
|
|
2027
|
+
};
|
|
2028
|
+
const sourceControlJobAcceptRefinement = {
|
|
2029
|
+
message: "A source control response must carry sourceControlJobId exactly when state is 'accepted'.",
|
|
2030
|
+
path: ["sourceControlJobId"]
|
|
2031
|
+
};
|
|
2032
|
+
function isSourceControlJobAcceptConsistent(value) {
|
|
2033
|
+
return value.state === "accepted" === (value.sourceControlJobId !== void 0);
|
|
2034
|
+
}
|
|
2035
|
+
const bridgeRefreshSourceControlStatusSuccessResponseSchema = z.object({
|
|
2036
|
+
...sourceControlJobAcceptFields,
|
|
2037
|
+
type: z.literal(BRIDGE_REFRESH_SOURCE_CONTROL_STATUS_REQUEST_TYPE)
|
|
2038
|
+
}).refine(isSourceControlJobAcceptConsistent, sourceControlJobAcceptRefinement);
|
|
2039
|
+
const bridgeCheckoutAssetsSuccessResponseSchema = z.object({
|
|
2040
|
+
...sourceControlJobAcceptFields,
|
|
2041
|
+
type: z.literal(BRIDGE_CHECKOUT_ASSETS_REQUEST_TYPE)
|
|
2042
|
+
}).refine(isSourceControlJobAcceptConsistent, sourceControlJobAcceptRefinement);
|
|
2043
|
+
const bridgeSourceControlJobItemResultSchema = z.object({
|
|
2044
|
+
assetPath: z.string(),
|
|
2045
|
+
error: z.string().optional(),
|
|
2046
|
+
index: z.number().int().min(0),
|
|
2047
|
+
ok: z.boolean(),
|
|
2048
|
+
state: sourceControlFileStateSchema
|
|
2049
|
+
});
|
|
2050
|
+
const bridgeGetSourceControlJobStatusSuccessResponseSchema = z.object({
|
|
2051
|
+
durationMs: z.number().int().min(0).optional(),
|
|
2052
|
+
failureCount: z.number().int().min(0),
|
|
2053
|
+
finishedAt: nonEmptyStringSchema.optional(),
|
|
2054
|
+
message: nonEmptyStringSchema.optional(),
|
|
2055
|
+
ok: z.literal(true),
|
|
2056
|
+
operation: sourceControlOperationSchema.optional(),
|
|
2057
|
+
requestId: requestIdSchema,
|
|
2058
|
+
results: z.array(bridgeSourceControlJobItemResultSchema).max(64),
|
|
2059
|
+
sourceControlJobId: sourceControlJobIdSchema,
|
|
2060
|
+
state: sourceControlJobStateSchema,
|
|
2061
|
+
successCount: z.number().int().min(0),
|
|
2062
|
+
type: z.literal(BRIDGE_GET_SOURCE_CONTROL_JOB_STATUS_REQUEST_TYPE),
|
|
2063
|
+
version: z.literal(2)
|
|
2064
|
+
}).superRefine((value, ctx) => {
|
|
2065
|
+
if (value.state === "succeeded" || value.state === "failed") return;
|
|
2066
|
+
if (value.durationMs !== void 0) ctx.addIssue({
|
|
2067
|
+
code: z.ZodIssueCode.custom,
|
|
2068
|
+
message: "durationMs must be absent until the job finishes",
|
|
2069
|
+
path: ["durationMs"]
|
|
2070
|
+
});
|
|
2071
|
+
if (value.finishedAt !== void 0) ctx.addIssue({
|
|
2072
|
+
code: z.ZodIssueCode.custom,
|
|
2073
|
+
message: "finishedAt must be absent until the job finishes",
|
|
2074
|
+
path: ["finishedAt"]
|
|
2075
|
+
});
|
|
2076
|
+
if (value.results.length > 0) ctx.addIssue({
|
|
2077
|
+
code: z.ZodIssueCode.custom,
|
|
2078
|
+
message: "results must be empty until the job finishes",
|
|
2079
|
+
path: ["results"]
|
|
2080
|
+
});
|
|
2081
|
+
});
|
|
1946
2082
|
const bridgeAssetDependencySchema = z.object({
|
|
1947
2083
|
category: assetDependencyCategorySchema,
|
|
1948
2084
|
isHard: z.boolean(),
|
|
@@ -2228,6 +2364,10 @@ const bridgeCreateDataTableResponseSchema = z.union([bridgeCreateDataTableSucces
|
|
|
2228
2364
|
const bridgeRenameDataTableResponseSchema = z.union([bridgeRenameDataTableSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
2229
2365
|
const bridgeDeleteDataTableResponseSchema = z.union([bridgeDeleteDataTableSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
2230
2366
|
const bridgeRenameDataTableRowResponseSchema = z.union([bridgeRenameDataTableRowSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
2367
|
+
const bridgeGetSourceControlStatusResponseSchema = z.union([bridgeGetSourceControlStatusSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
2368
|
+
const bridgeRefreshSourceControlStatusResponseSchema = z.union([bridgeRefreshSourceControlStatusSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
2369
|
+
const bridgeCheckoutAssetsResponseSchema = z.union([bridgeCheckoutAssetsSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
2370
|
+
const bridgeGetSourceControlJobStatusResponseSchema = z.union([bridgeGetSourceControlJobStatusSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
2231
2371
|
const bridgeFindAssetsResponseSchema = z.union([bridgeFindAssetsSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
2232
2372
|
const bridgeGetAssetDependenciesResponseSchema = z.union([bridgeGetAssetDependenciesSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
2233
2373
|
const bridgeFindBlueprintReferencesResponseSchema = z.union([bridgeFindBlueprintReferencesSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
@@ -2327,6 +2467,38 @@ function createBridgeRenameDataTableRowRequest(assetPath, rowName, newRowName, o
|
|
|
2327
2467
|
version: 2
|
|
2328
2468
|
});
|
|
2329
2469
|
}
|
|
2470
|
+
function createBridgeGetSourceControlStatusRequest(assetPaths, requestId = randomUUID()) {
|
|
2471
|
+
return bridgeGetSourceControlStatusRequestSchema.parse({
|
|
2472
|
+
assetPaths,
|
|
2473
|
+
requestId,
|
|
2474
|
+
type: BRIDGE_GET_SOURCE_CONTROL_STATUS_REQUEST_TYPE,
|
|
2475
|
+
version: 2
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
function createBridgeRefreshSourceControlStatusRequest(assetPaths, requestId = randomUUID()) {
|
|
2479
|
+
return bridgeRefreshSourceControlStatusRequestSchema.parse({
|
|
2480
|
+
assetPaths,
|
|
2481
|
+
requestId,
|
|
2482
|
+
type: BRIDGE_REFRESH_SOURCE_CONTROL_STATUS_REQUEST_TYPE,
|
|
2483
|
+
version: 2
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
function createBridgeCheckoutAssetsRequest(assetPaths, requestId = randomUUID()) {
|
|
2487
|
+
return bridgeCheckoutAssetsRequestSchema.parse({
|
|
2488
|
+
assetPaths,
|
|
2489
|
+
requestId,
|
|
2490
|
+
type: BRIDGE_CHECKOUT_ASSETS_REQUEST_TYPE,
|
|
2491
|
+
version: 2
|
|
2492
|
+
});
|
|
2493
|
+
}
|
|
2494
|
+
function createBridgeGetSourceControlJobStatusRequest(sourceControlJobId, requestId = randomUUID()) {
|
|
2495
|
+
return bridgeGetSourceControlJobStatusRequestSchema.parse({
|
|
2496
|
+
requestId,
|
|
2497
|
+
sourceControlJobId,
|
|
2498
|
+
type: BRIDGE_GET_SOURCE_CONTROL_JOB_STATUS_REQUEST_TYPE,
|
|
2499
|
+
version: 2
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2330
2502
|
function createBridgeFindAssetsRequest(options = {}, requestId = randomUUID()) {
|
|
2331
2503
|
return bridgeFindAssetsRequestSchema.parse({
|
|
2332
2504
|
classNames: options.classNames,
|
|
@@ -2957,6 +3129,18 @@ async function sendBridgeOpenLevelRequest(request, options) {
|
|
|
2957
3129
|
async function sendBridgeGetOpenLevelStatusRequest(request, options) {
|
|
2958
3130
|
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeGetOpenLevelStatusResponseSchema, request.requestId);
|
|
2959
3131
|
}
|
|
3132
|
+
async function sendBridgeGetSourceControlStatusRequest(request, options) {
|
|
3133
|
+
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeGetSourceControlStatusResponseSchema, request.requestId);
|
|
3134
|
+
}
|
|
3135
|
+
async function sendBridgeRefreshSourceControlStatusRequest(request, options) {
|
|
3136
|
+
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeRefreshSourceControlStatusResponseSchema, request.requestId);
|
|
3137
|
+
}
|
|
3138
|
+
async function sendBridgeCheckoutAssetsRequest(request, options) {
|
|
3139
|
+
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeCheckoutAssetsResponseSchema, request.requestId);
|
|
3140
|
+
}
|
|
3141
|
+
async function sendBridgeGetSourceControlJobStatusRequest(request, options) {
|
|
3142
|
+
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeGetSourceControlJobStatusResponseSchema, request.requestId);
|
|
3143
|
+
}
|
|
2960
3144
|
async function sendBridgeGetAssetDependenciesRequest(request, options) {
|
|
2961
3145
|
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeGetAssetDependenciesResponseSchema, request.requestId);
|
|
2962
3146
|
}
|
|
@@ -5264,6 +5448,7 @@ const WAIT_FOR_JOB_KINDS = [
|
|
|
5264
5448
|
"compile",
|
|
5265
5449
|
"create_class",
|
|
5266
5450
|
"pie",
|
|
5451
|
+
"source_control",
|
|
5267
5452
|
"test_run"
|
|
5268
5453
|
];
|
|
5269
5454
|
const WAIT_FOR_JOB_MAX_TIMEOUT_MS = 6e5;
|
|
@@ -5271,6 +5456,7 @@ const WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS = {
|
|
|
5271
5456
|
compile: 12e4,
|
|
5272
5457
|
create_class: 6e4,
|
|
5273
5458
|
pie: 3e4,
|
|
5459
|
+
source_control: 6e4,
|
|
5274
5460
|
test_run: 3e5
|
|
5275
5461
|
};
|
|
5276
5462
|
const WAIT_FOR_JOB_POLL_CADENCE_MS = {
|
|
@@ -5286,6 +5472,10 @@ const WAIT_FOR_JOB_POLL_CADENCE_MS = {
|
|
|
5286
5472
|
capMs: 2e3,
|
|
5287
5473
|
initialMs: 250
|
|
5288
5474
|
},
|
|
5475
|
+
source_control: {
|
|
5476
|
+
capMs: 2e3,
|
|
5477
|
+
initialMs: 250
|
|
5478
|
+
},
|
|
5289
5479
|
test_run: {
|
|
5290
5480
|
capMs: 5e3,
|
|
5291
5481
|
initialMs: 1e3
|
|
@@ -5393,7 +5583,7 @@ const waitForJobInputSchema = {
|
|
|
5393
5583
|
jobId: z.string().trim().min(1).max(512).describe("Job identifier to wait on: compileJobId (compile), createClassJobId (create_class), testRunId (test_run), or pieSessionId (pie)."),
|
|
5394
5584
|
kind: z.enum(WAIT_FOR_JOB_KINDS).describe("Which async job the id belongs to. Required — job ids are bare GUIDs and carry no type tag, so the kind cannot be inferred."),
|
|
5395
5585
|
projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor owns the job."),
|
|
5396
|
-
timeoutMs: z.number().int().min(1).optional().describe(`Wall-clock budget for the wait, clamped to ${String(WAIT_FOR_JOB_MAX_TIMEOUT_MS)}ms. Defaults per kind: compile ${String(WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS.compile)}ms, create_class ${String(WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS.create_class)}ms, pie ${String(WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS.pie)}ms, test_run ${String(WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS.test_run)}ms.`)
|
|
5586
|
+
timeoutMs: z.number().int().min(1).optional().describe(`Wall-clock budget for the wait, clamped to ${String(WAIT_FOR_JOB_MAX_TIMEOUT_MS)}ms. Defaults per kind: compile ${String(WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS.compile)}ms, create_class ${String(WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS.create_class)}ms, pie ${String(WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS.pie)}ms, source_control ${String(WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS.source_control)}ms, test_run ${String(WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS.test_run)}ms.`)
|
|
5397
5587
|
};
|
|
5398
5588
|
const getEditorStateInputSchema = { projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should report its state.") };
|
|
5399
5589
|
const saveDirtyPackagesInputSchema = {
|
|
@@ -6208,6 +6398,22 @@ const getOpenLevelStatusInputSchema = {
|
|
|
6208
6398
|
openLevelJobId: bridgeGetOpenLevelStatusRequestSchema.shape.openLevelJobId.describe("openLevelJobId returned by open_level identifying the load job to poll."),
|
|
6209
6399
|
projectId: projectIdSchema
|
|
6210
6400
|
};
|
|
6401
|
+
const getSourceControlStatusInputSchema = {
|
|
6402
|
+
assetPaths: bridgeGetSourceControlStatusRequestSchema.shape.assetPaths.describe("Package names or object paths of the assets to report on (1-64), e.g. /Game/Data/DT_Weapons."),
|
|
6403
|
+
projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should query its source control provider.")
|
|
6404
|
+
};
|
|
6405
|
+
const refreshSourceControlStatusInputSchema = {
|
|
6406
|
+
assetPaths: bridgeRefreshSourceControlStatusRequestSchema.shape.assetPaths.describe("Package names or object paths of the assets whose depot state should be re-read (1-64)."),
|
|
6407
|
+
projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should refresh its source control state.")
|
|
6408
|
+
};
|
|
6409
|
+
const checkoutAssetsInputSchema = {
|
|
6410
|
+
assetPaths: bridgeCheckoutAssetsRequestSchema.shape.assetPaths.describe("Package names or object paths of the assets to check out (1-64), e.g. /Game/Data/DT_Weapons."),
|
|
6411
|
+
projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should check the assets out.")
|
|
6412
|
+
};
|
|
6413
|
+
const getSourceControlJobStatusInputSchema = {
|
|
6414
|
+
projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor owns the job."),
|
|
6415
|
+
sourceControlJobId: bridgeGetSourceControlJobStatusRequestSchema.shape.sourceControlJobId.describe("sourceControlJobId returned by checkout_assets or refresh_source_control_status.")
|
|
6416
|
+
};
|
|
6211
6417
|
const getAssetDependenciesInputSchema = {
|
|
6212
6418
|
assetPath: bridgeGetAssetDependenciesRequestSchema.shape.assetPath.describe("Package name or object path of the asset to inspect (e.g. /Game/BP/BP_Player or /Game/BP/BP_Player.BP_Player)."),
|
|
6213
6419
|
categories: bridgeGetAssetDependenciesRequestSchema.shape.categories.describe("Dependency categories to include: 'package' (default), 'searchable_name', 'manage'."),
|
|
@@ -6853,6 +7059,46 @@ function registerMcpTools(server, dependencies) {
|
|
|
6853
7059
|
projectId
|
|
6854
7060
|
}, dependencies));
|
|
6855
7061
|
});
|
|
7062
|
+
server.registerTool("get_source_control_status", {
|
|
7063
|
+
description: "Report the editor's source control provider and the per-file state of up to 64 assets. Answered from the provider's state cache, so it costs no network round trip and returns inline. enabled distinguishes 'no source control configured' from 'configured, and this file is simply not checked out'; per-file state is one of checked_out_by_me, checked_out_by_other (with checkedOutBy naming the holder), marked_for_add, not_checked_out, not_in_depot, unknown. A file the editor has never queried reports 'unknown' — call refresh_source_control_status to fetch its real state from the server. Call this before any asset write on a team project: under Perforce or Git-LFS file locking a .uasset stays read-only until checked out, and the write otherwise fails later at save time.",
|
|
7064
|
+
inputSchema: getSourceControlStatusInputSchema,
|
|
7065
|
+
title: "Get Source Control Status"
|
|
7066
|
+
}, async ({ assetPaths, projectId }) => {
|
|
7067
|
+
return toToolSuccess(await getSourceControlStatus({
|
|
7068
|
+
assetPaths,
|
|
7069
|
+
projectId
|
|
7070
|
+
}, dependencies));
|
|
7071
|
+
});
|
|
7072
|
+
server.registerTool("refresh_source_control_status", {
|
|
7073
|
+
description: "Re-read the depot state of up to 64 assets from the source control server, so a later get_source_control_status reports something better than 'unknown'. This is a network round trip to the provider and runs far past what a bridge request can wait for, so it only accepts the job: it returns state 'accepted' with a sourceControlJobId to poll with get_source_control_job_status (or wait_for_job with kind source_control).",
|
|
7074
|
+
inputSchema: refreshSourceControlStatusInputSchema,
|
|
7075
|
+
title: "Refresh Source Control Status"
|
|
7076
|
+
}, async ({ assetPaths, projectId }) => {
|
|
7077
|
+
return toToolSuccess(await refreshSourceControlStatus({
|
|
7078
|
+
assetPaths,
|
|
7079
|
+
projectId
|
|
7080
|
+
}, dependencies));
|
|
7081
|
+
});
|
|
7082
|
+
server.registerTool("checkout_assets", {
|
|
7083
|
+
description: "Check up to 64 assets out of source control so they become writable. This is a network round trip to the provider and runs far past what a bridge request can wait for, so it only accepts the job: it returns state 'accepted' with a sourceControlJobId to poll with get_source_control_job_status (or wait_for_job with kind source_control). The poll reports per-item results with successCount / failureCount — one asset the provider refuses does not abort the rest. This checks out only; it never submits, and a checked-out asset stays checked out until a human submits or reverts it.",
|
|
7084
|
+
inputSchema: checkoutAssetsInputSchema,
|
|
7085
|
+
title: "Checkout Assets"
|
|
7086
|
+
}, async ({ assetPaths, projectId }) => {
|
|
7087
|
+
return toToolSuccess(await checkoutAssets({
|
|
7088
|
+
assetPaths,
|
|
7089
|
+
projectId
|
|
7090
|
+
}, dependencies));
|
|
7091
|
+
});
|
|
7092
|
+
server.registerTool("get_source_control_job_status", {
|
|
7093
|
+
description: "Poll a source control job started by checkout_assets or refresh_source_control_status for a projectId; returns state (running, succeeded, failed, unknown_job), which operation it was, and once finished the per-file results with successCount / failureCount and timing. Poll until the state leaves 'running' before writing to any asset in the batch.",
|
|
7094
|
+
inputSchema: getSourceControlJobStatusInputSchema,
|
|
7095
|
+
title: "Get Source Control Job Status"
|
|
7096
|
+
}, async ({ projectId, sourceControlJobId }) => {
|
|
7097
|
+
return toToolSuccess(await getSourceControlJobStatus({
|
|
7098
|
+
projectId,
|
|
7099
|
+
sourceControlJobId
|
|
7100
|
+
}, dependencies));
|
|
7101
|
+
});
|
|
6856
7102
|
server.registerTool("find_assets", {
|
|
6857
7103
|
description: "Query a project's Unreal Asset Registry for assets by class and/or content path, without loading them; returns each match's name, class, package name, package path, and object path. Requires at least one of classNames or packagePaths — an unfiltered query would stall the editor enumerating every asset. Results are paged: pass the previous response nextCursor to fetch the next page (totalCount reports the full match count). scanning=true means the registry is still indexing, so an empty result does not prove the asset is absent.",
|
|
6858
7104
|
inputSchema: findAssetsInputSchema,
|
|
@@ -7345,6 +7591,91 @@ async function getOpenLevelStatus(input, dependencies) {
|
|
|
7345
7591
|
if (response.durationMs !== void 0) result.durationMs = response.durationMs;
|
|
7346
7592
|
return result;
|
|
7347
7593
|
}
|
|
7594
|
+
async function getSourceControlStatus(input, dependencies) {
|
|
7595
|
+
const normalizedProjectId = projectIdSchema.parse(input.projectId);
|
|
7596
|
+
const entry = dependencies.registry.getProject(normalizedProjectId);
|
|
7597
|
+
if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
|
|
7598
|
+
const response = await sendBridgeGetSourceControlStatusRequest(createBridgeGetSourceControlStatusRequest(input.assetPaths), {
|
|
7599
|
+
connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
|
|
7600
|
+
host: BRIDGE_LOOPBACK_HOST,
|
|
7601
|
+
port: entry.port,
|
|
7602
|
+
readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
|
|
7603
|
+
});
|
|
7604
|
+
if (!response.ok) throw new Error(`Bridge get_source_control_status failed (${response.error.code}): ${response.error.message}`);
|
|
7605
|
+
const result = {
|
|
7606
|
+
available: response.available,
|
|
7607
|
+
enabled: response.enabled,
|
|
7608
|
+
files: response.files,
|
|
7609
|
+
projectId: entry.projectId,
|
|
7610
|
+
provider: response.provider,
|
|
7611
|
+
state: response.state
|
|
7612
|
+
};
|
|
7613
|
+
if (response.message !== void 0) result.message = response.message;
|
|
7614
|
+
return result;
|
|
7615
|
+
}
|
|
7616
|
+
async function refreshSourceControlStatus(input, dependencies) {
|
|
7617
|
+
const normalizedProjectId = projectIdSchema.parse(input.projectId);
|
|
7618
|
+
const entry = dependencies.registry.getProject(normalizedProjectId);
|
|
7619
|
+
if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
|
|
7620
|
+
const request = createBridgeRefreshSourceControlStatusRequest(input.assetPaths);
|
|
7621
|
+
const response = await sendBridgeRefreshSourceControlStatusRequest(request, {
|
|
7622
|
+
connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
|
|
7623
|
+
host: BRIDGE_LOOPBACK_HOST,
|
|
7624
|
+
port: entry.port,
|
|
7625
|
+
readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
|
|
7626
|
+
});
|
|
7627
|
+
if (!response.ok) throw new Error(`Bridge refresh_source_control_status failed (${response.error.code}): ${response.error.message}`);
|
|
7628
|
+
return buildSourceControlJobAcceptResult(response, request.assetPaths, entry.projectId);
|
|
7629
|
+
}
|
|
7630
|
+
async function checkoutAssets(input, dependencies) {
|
|
7631
|
+
const normalizedProjectId = projectIdSchema.parse(input.projectId);
|
|
7632
|
+
const entry = dependencies.registry.getProject(normalizedProjectId);
|
|
7633
|
+
if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
|
|
7634
|
+
const request = createBridgeCheckoutAssetsRequest(input.assetPaths);
|
|
7635
|
+
const response = await sendBridgeCheckoutAssetsRequest(request, {
|
|
7636
|
+
connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
|
|
7637
|
+
host: BRIDGE_LOOPBACK_HOST,
|
|
7638
|
+
port: entry.port,
|
|
7639
|
+
readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
|
|
7640
|
+
});
|
|
7641
|
+
if (!response.ok) throw new Error(`Bridge checkout_assets failed (${response.error.code}): ${response.error.message}`);
|
|
7642
|
+
return buildSourceControlJobAcceptResult(response, request.assetPaths, entry.projectId);
|
|
7643
|
+
}
|
|
7644
|
+
function buildSourceControlJobAcceptResult(response, assetPaths, projectId) {
|
|
7645
|
+
const result = {
|
|
7646
|
+
assetPaths,
|
|
7647
|
+
projectId,
|
|
7648
|
+
state: response.state
|
|
7649
|
+
};
|
|
7650
|
+
if (response.sourceControlJobId !== void 0) result.sourceControlJobId = response.sourceControlJobId;
|
|
7651
|
+
if (response.message !== void 0) result.message = response.message;
|
|
7652
|
+
return result;
|
|
7653
|
+
}
|
|
7654
|
+
async function getSourceControlJobStatus(input, dependencies) {
|
|
7655
|
+
const normalizedProjectId = projectIdSchema.parse(input.projectId);
|
|
7656
|
+
const entry = dependencies.registry.getProject(normalizedProjectId);
|
|
7657
|
+
if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
|
|
7658
|
+
const response = await sendBridgeGetSourceControlJobStatusRequest(createBridgeGetSourceControlJobStatusRequest(input.sourceControlJobId), {
|
|
7659
|
+
connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
|
|
7660
|
+
host: BRIDGE_LOOPBACK_HOST,
|
|
7661
|
+
port: entry.port,
|
|
7662
|
+
readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
|
|
7663
|
+
});
|
|
7664
|
+
if (!response.ok) throw new Error(`Bridge get_source_control_job_status failed (${response.error.code}): ${response.error.message}`);
|
|
7665
|
+
const result = {
|
|
7666
|
+
failureCount: response.failureCount,
|
|
7667
|
+
projectId: entry.projectId,
|
|
7668
|
+
results: response.results,
|
|
7669
|
+
sourceControlJobId: response.sourceControlJobId,
|
|
7670
|
+
state: response.state,
|
|
7671
|
+
successCount: response.successCount
|
|
7672
|
+
};
|
|
7673
|
+
if (response.operation !== void 0) result.operation = response.operation;
|
|
7674
|
+
if (response.message !== void 0) result.message = response.message;
|
|
7675
|
+
if (response.finishedAt !== void 0) result.finishedAt = response.finishedAt;
|
|
7676
|
+
if (response.durationMs !== void 0) result.durationMs = response.durationMs;
|
|
7677
|
+
return result;
|
|
7678
|
+
}
|
|
7348
7679
|
async function findAssets(input, dependencies) {
|
|
7349
7680
|
const normalizedProjectId = projectIdSchema.parse(input.projectId);
|
|
7350
7681
|
const entry = dependencies.registry.getProject(normalizedProjectId);
|
|
@@ -8678,6 +9009,10 @@ async function pollJobStatus(kind, jobId, projectId, dependencies) {
|
|
|
8678
9009
|
pieSessionId: jobId,
|
|
8679
9010
|
projectId
|
|
8680
9011
|
}, dependencies);
|
|
9012
|
+
case "source_control": return await getSourceControlJobStatus({
|
|
9013
|
+
projectId,
|
|
9014
|
+
sourceControlJobId: jobId
|
|
9015
|
+
}, dependencies);
|
|
8681
9016
|
case "test_run": return await getTestRunStatus({
|
|
8682
9017
|
projectId,
|
|
8683
9018
|
testRunId: jobId
|
|
@@ -8689,6 +9024,7 @@ function classifyJobStatus(kind, status) {
|
|
|
8689
9024
|
case "compile": return status.state === "compiling" ? "pending" : toTerminalOutcome(status.state === "unknown_job");
|
|
8690
9025
|
case "create_class": return status.state === "creating" ? "pending" : toTerminalOutcome(status.state === "unknown_job");
|
|
8691
9026
|
case "pie": return status.state === "starting" ? "pending" : "completed";
|
|
9027
|
+
case "source_control": return status.state === "running" ? "pending" : toTerminalOutcome(status.state === "unknown_job");
|
|
8692
9028
|
case "test_run": return status.state === "running" ? "pending" : toTerminalOutcome(status.state === "unknown_run");
|
|
8693
9029
|
}
|
|
8694
9030
|
}
|
|
@@ -8945,7 +9281,7 @@ function isMissingFileError(error) {
|
|
|
8945
9281
|
//#endregion
|
|
8946
9282
|
//#region src/server.ts
|
|
8947
9283
|
const SERVER_NAME = "ue-mcp-server";
|
|
8948
|
-
const SERVER_VERSION = "1.
|
|
9284
|
+
const SERVER_VERSION = "1.2.0";
|
|
8949
9285
|
const logger = createModuleLogger("bootstrap");
|
|
8950
9286
|
const enginePingLogger = createModuleLogger("engine-ping");
|
|
8951
9287
|
const httpApiLogger = createModuleLogger("http-api");
|