@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.
Files changed (3) hide show
  1. package/README.md +19 -3
  2. package/dist/index.mjs +338 -2
  3. 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 `pie`; `kind` is required because job ids are bare GUIDs
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 and reported as `source_control_checkout_required` rather than surfacing later as an unexplained save error
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.1.0";
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uemcp/server",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "MCP server for making Unreal Engine and C++ development easier",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",