unity-open-mcp 1.2.3 → 1.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 +34 -2
- package/dist/agent-identity.d.ts +12 -0
- package/dist/agent-identity.js +2 -0
- package/dist/agent-identity.js.map +1 -1
- package/dist/batch-spawn.d.ts +39 -0
- package/dist/batch-spawn.js +287 -69
- package/dist/batch-spawn.js.map +1 -1
- package/dist/capabilities/build-capabilities.js +2 -0
- package/dist/capabilities/build-capabilities.js.map +1 -1
- package/dist/capabilities/discovery.d.ts +51 -0
- package/dist/capabilities/discovery.js +87 -0
- package/dist/capabilities/discovery.js.map +1 -0
- package/dist/capabilities/lifecycle.d.ts +6 -0
- package/dist/capabilities/lifecycle.js +17 -1
- package/dist/capabilities/lifecycle.js.map +1 -1
- package/dist/capabilities/tool-groups.js +1 -0
- package/dist/capabilities/tool-groups.js.map +1 -1
- package/dist/child-supervision.d.ts +6 -0
- package/dist/child-supervision.js +12 -0
- package/dist/child-supervision.js.map +1 -1
- package/dist/cli/args.d.ts +24 -2
- package/dist/cli/args.js +97 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/cli.js +42 -3
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/help-text.d.ts +1 -0
- package/dist/cli/help-text.js +83 -3
- package/dist/cli/help-text.js.map +1 -1
- package/dist/cli/json-guards.d.ts +2 -0
- package/dist/cli/json-guards.js +5 -0
- package/dist/cli/json-guards.js.map +1 -0
- package/dist/cli/setup-command.d.ts +116 -0
- package/dist/cli/setup-command.js +696 -0
- package/dist/cli/setup-command.js.map +1 -0
- package/dist/cli/update-command.d.ts +30 -0
- package/dist/cli/update-command.js +271 -0
- package/dist/cli/update-command.js.map +1 -0
- package/dist/compile-verify.d.ts +3 -0
- package/dist/compile-verify.js +4 -0
- package/dist/compile-verify.js.map +1 -1
- package/dist/constants.d.ts +10 -2
- package/dist/constants.js +10 -2
- package/dist/constants.js.map +1 -1
- package/dist/dialog-dismiss.d.ts +108 -12
- package/dist/dialog-dismiss.js +294 -109
- package/dist/dialog-dismiss.js.map +1 -1
- package/dist/dialog-policy.d.ts +55 -9
- package/dist/dialog-policy.js +85 -15
- package/dist/dialog-policy.js.map +1 -1
- package/dist/editor-search.d.ts +2 -0
- package/dist/editor-search.js +8 -0
- package/dist/editor-search.js.map +1 -0
- package/dist/gate-error.d.ts +3 -0
- package/dist/gate-error.js.map +1 -1
- package/dist/index.js +56 -1
- package/dist/index.js.map +1 -1
- package/dist/instance-discovery.d.ts +16 -0
- package/dist/instance-discovery.js +45 -0
- package/dist/instance-discovery.js.map +1 -1
- package/dist/jobs/adapters.d.ts +15 -0
- package/dist/jobs/adapters.js +147 -0
- package/dist/jobs/adapters.js.map +1 -0
- package/dist/jobs/handler.d.ts +3 -0
- package/dist/jobs/handler.js +29 -0
- package/dist/jobs/handler.js.map +1 -0
- package/dist/jobs/job-manager.d.ts +123 -0
- package/dist/jobs/job-manager.js +290 -0
- package/dist/jobs/job-manager.js.map +1 -0
- package/dist/live-client.d.ts +50 -2
- package/dist/live-client.js +221 -26
- package/dist/live-client.js.map +1 -1
- package/dist/local-tools.d.ts +17 -0
- package/dist/local-tools.js +47 -0
- package/dist/local-tools.js.map +1 -0
- package/dist/project-path.d.ts +99 -0
- package/dist/project-path.js +171 -0
- package/dist/project-path.js.map +1 -0
- package/dist/release-lookup.d.ts +32 -0
- package/dist/release-lookup.js +147 -0
- package/dist/release-lookup.js.map +1 -0
- package/dist/routers.d.ts +17 -3
- package/dist/routers.js +29 -8
- package/dist/routers.js.map +1 -1
- package/dist/running-unity.d.ts +21 -0
- package/dist/running-unity.js +50 -0
- package/dist/running-unity.js.map +1 -1
- package/dist/schema-defaults.d.ts +4 -3
- package/dist/schema-defaults.js +20 -6
- package/dist/schema-defaults.js.map +1 -1
- package/dist/server-instructions.js +7 -1
- package/dist/server-instructions.js.map +1 -1
- package/dist/server.d.ts +10 -3
- package/dist/server.js +24 -25
- package/dist/server.js.map +1 -1
- package/dist/setup/codex-toml.d.ts +23 -0
- package/dist/setup/codex-toml.js +514 -0
- package/dist/setup/codex-toml.js.map +1 -0
- package/dist/setup/portable-config.d.ts +59 -0
- package/dist/setup/portable-config.js +241 -0
- package/dist/setup/portable-config.js.map +1 -0
- package/dist/skill/SKILL.md +68 -0
- package/dist/skill/client-paths.js +2 -4
- package/dist/skill/client-paths.js.map +1 -1
- package/dist/skill/copy-references.d.ts +2 -0
- package/dist/skill/copy-references.js +16 -0
- package/dist/skill/copy-references.js.map +1 -0
- package/dist/skill/generate-skill.js +5 -3
- package/dist/skill/generate-skill.js.map +1 -1
- package/dist/skill/references/batch-and-gates.md +142 -0
- package/dist/skill/references/compile-and-safe-mode.md +163 -0
- package/dist/skill/references/discovery-and-groups.md +30 -0
- package/dist/skill/references/routing-and-lifecycle.md +62 -0
- package/dist/skill/references/senses-and-tests.md +41 -0
- package/dist/skill/references/yaml-and-offline-work.md +33 -0
- package/dist/templates/mcp-wrapper.sh +35 -0
- package/dist/tool-contract.d.ts +8 -0
- package/dist/tool-contract.js +253 -0
- package/dist/tool-contract.js.map +1 -0
- package/dist/tool-router.d.ts +62 -7
- package/dist/tool-router.js +455 -17
- package/dist/tool-router.js.map +1 -1
- package/dist/tool-session-state.d.ts +6 -0
- package/dist/tool-session-state.js +14 -0
- package/dist/tool-session-state.js.map +1 -1
- package/dist/tools/agent-capabilities.js +9 -0
- package/dist/tools/agent-capabilities.js.map +1 -1
- package/dist/tools/batch-execute.js +17 -14
- package/dist/tools/batch-execute.js.map +1 -1
- package/dist/tools/bridge-status.js +4 -5
- package/dist/tools/bridge-status.js.map +1 -1
- package/dist/tools/compile-check.js +6 -4
- package/dist/tools/compile-check.js.map +1 -1
- package/dist/tools/component-get.js +2 -2
- package/dist/tools/component-get.js.map +1 -1
- package/dist/tools/component-modify.js +4 -4
- package/dist/tools/component-modify.js.map +1 -1
- package/dist/tools/execute-csharp.js +8 -4
- package/dist/tools/execute-csharp.js.map +1 -1
- package/dist/tools/gameobject-modify.js +2 -1
- package/dist/tools/gameobject-modify.js.map +1 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +10 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inputsim-key.js +12 -14
- package/dist/tools/inputsim-key.js.map +1 -1
- package/dist/tools/inputsim-pointer.js +3 -3
- package/dist/tools/inputsim-pointer.js.map +1 -1
- package/dist/tools/inputsim-step.js +7 -8
- package/dist/tools/inputsim-step.js.map +1 -1
- package/dist/tools/inputsim-touch.js +9 -10
- package/dist/tools/inputsim-touch.js.map +1 -1
- package/dist/tools/jobs.d.ts +40 -0
- package/dist/tools/jobs.js +18 -0
- package/dist/tools/jobs.js.map +1 -0
- package/dist/tools/manage-tools.js +10 -5
- package/dist/tools/manage-tools.js.map +1 -1
- package/dist/tools/project-commands.d.ts +40 -0
- package/dist/tools/project-commands.js +23 -0
- package/dist/tools/project-commands.js.map +1 -0
- package/dist/tools/read-compile-errors.js +14 -93
- package/dist/tools/read-compile-errors.js.map +1 -1
- package/dist/tools/scan-paths.js +4 -4
- package/dist/tools/scan-paths.js.map +1 -1
- package/dist/tools/schema-fragments.d.ts +15 -3
- package/dist/tools/schema-fragments.js +23 -13
- package/dist/tools/schema-fragments.js.map +1 -1
- package/dist/tools/upgrade.d.ts +40 -0
- package/dist/tools/upgrade.js +55 -0
- package/dist/tools/upgrade.js.map +1 -0
- package/dist/unity-install-discovery.d.ts +24 -0
- package/dist/unity-install-discovery.js +113 -4
- package/dist/unity-install-discovery.js.map +1 -1
- package/package.json +4 -3
- package/templates/mcp-wrapper.sh +35 -0
package/dist/live-client.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { PingCache } from "./ping-cache.js";
|
|
1
3
|
import { BridgeToolsCache } from "./bridge-tools-cache.js";
|
|
2
4
|
import { StaleAssemblyCache } from "./stale-assembly-cache.js";
|
|
3
5
|
import { EditorWedgeCache, scanForFdExhaustion } from "./editor-wedge.js";
|
|
4
6
|
import { detectStaleAssembly } from "./unity-log.js";
|
|
5
7
|
import { deriveIsError } from "./gate-error.js";
|
|
6
|
-
import { readFileSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
8
|
+
import { readFileSync, readdirSync, realpathSync, statSync, unlinkSync } from "node:fs";
|
|
7
9
|
import { join } from "node:path";
|
|
8
10
|
import { pollAndDismissDialogs, readDismissConfig, } from "./dialog-dismiss.js";
|
|
9
|
-
import { readInstanceLock, classifyInstance, lockPath, isPidAlive, statusDir, hasRecentPendingTestRun, resolveRefreshedEndpoint } from "./instance-discovery.js";
|
|
10
|
-
import { bridgeBaseUrl, BRIDGE_DEFAULT_TIMEOUT_MS, BRIDGE_HOST_SAFE_TIMEOUT_CAP_MS, BRIDGE_MIN_TIMEOUT_MS, PORT_ENV_VAR, PROJECT_PATH_ENV_VAR, } from "./constants.js";
|
|
11
|
+
import { readInstanceLock, classifyInstance, lockPath, isPidAlive, statusDir, hasRecentPendingTestRun, resolveRefreshedEndpoint, normalizePath, findInstanceLockByPort, computePort } from "./instance-discovery.js";
|
|
12
|
+
import { bridgeBaseUrl, BRIDGE_DEFAULT_TIMEOUT_MS, BRIDGE_HOST_SAFE_TIMEOUT_CAP_MS, BRIDGE_MIN_TIMEOUT_MS, COMPILE_STATE_HEADER, PORT_ENV_VAR, PROJECT_PATH_ENV_VAR, } from "./constants.js";
|
|
11
13
|
import { findUnityForProject } from "./running-unity.js";
|
|
12
14
|
import { makeErrorResult } from "./results.js";
|
|
13
15
|
import { checkBridgeCompat, isVersionCheckSuppressed, } from "./compat.js";
|
|
@@ -21,10 +23,19 @@ import { readRetryTunables, } from "./retry-policy.js";
|
|
|
21
23
|
// an agent can branch when a recompile reported success but the compiled state
|
|
22
24
|
// did not advance.
|
|
23
25
|
import { detectCompileVerify, buildCompileVerifyAnnotation, } from "./compile-verify.js";
|
|
24
|
-
import {
|
|
26
|
+
import { lifecycleForCall } from "./capabilities/lifecycle.js";
|
|
25
27
|
// M23 Plan 3 — per-process agent identity (sent as X-Agent-Id so the bridge's
|
|
26
28
|
// fair round-robin queue can schedule across agents).
|
|
27
29
|
import { PROCESS_AGENT_ID } from "./agent-identity.js";
|
|
30
|
+
function canonicalProjectPath(p) {
|
|
31
|
+
let real = p;
|
|
32
|
+
try {
|
|
33
|
+
real = realpathSync.native(p);
|
|
34
|
+
}
|
|
35
|
+
catch { /* unresolvable here: compare as spelled */ }
|
|
36
|
+
const norm = normalizePath(real);
|
|
37
|
+
return process.platform === "win32" ? norm.toLowerCase() : norm;
|
|
38
|
+
}
|
|
28
39
|
const PING_TIMEOUT_MS = 5_000;
|
|
29
40
|
// BRIDGE_DEFAULT_TIMEOUT_MS now lives in constants.ts (single source shared
|
|
30
41
|
// with the bridge C# parity test). The client fetch timeout must never
|
|
@@ -85,6 +96,7 @@ const COMPILE_RELOAD_ANNOTATION_TOOLS = new Set([
|
|
|
85
96
|
"unity_open_mcp_package_remove",
|
|
86
97
|
"unity_open_mcp_reimport_package",
|
|
87
98
|
"unity_open_mcp_compile_check",
|
|
99
|
+
"unity_open_mcp_recompile_scripts",
|
|
88
100
|
]);
|
|
89
101
|
/**
|
|
90
102
|
* M4 — should the compile-verify annotation run for this tool? Returns true
|
|
@@ -182,6 +194,11 @@ function buildOfflineHint(projectPath) {
|
|
|
182
194
|
}
|
|
183
195
|
return `${base} (lock not readable — Unity may not be running).`;
|
|
184
196
|
}
|
|
197
|
+
/** `/project-command-jobs` answered without a job snapshot; `status` is its non-OK HTTP status. */
|
|
198
|
+
export class ProjectJobHttpError extends Error {
|
|
199
|
+
status;
|
|
200
|
+
constructor(status) { super(`Project job transport HTTP ${status}`); this.status = status; }
|
|
201
|
+
}
|
|
185
202
|
export class LiveClient {
|
|
186
203
|
baseUrl;
|
|
187
204
|
pingCache;
|
|
@@ -216,6 +233,8 @@ export class LiveClient {
|
|
|
216
233
|
/** specs/feedback.md 2026-07-03 — opt-in for the "Unsaved changes to scene"
|
|
217
234
|
* modal (destructive under every policy, off by default). */
|
|
218
235
|
allowUnsavedSceneDismiss;
|
|
236
|
+
/** Explicit opt-in for continuing through a non-matching Editor warning. */
|
|
237
|
+
allowVersionMismatch;
|
|
219
238
|
/** M14 — per-session bearer token auto-discovered from the instance lock.
|
|
220
239
|
* Undefined when no live lock was found (older bridge / env port override);
|
|
221
240
|
* in that case no Authorization header is sent and the bridge must be in
|
|
@@ -246,6 +265,8 @@ export class LiveClient {
|
|
|
246
265
|
* the constructor used; without it a refresh would silently switch from an
|
|
247
266
|
* env-pinned port to the lock port. */
|
|
248
267
|
envPort;
|
|
268
|
+
/** Memoized outcome of the bridge-vs-server project-path comparison. */
|
|
269
|
+
projectMatch = null;
|
|
249
270
|
constructor(port, pingCache, authToken, projectPath, agentId = PROCESS_AGENT_ID, envPort) {
|
|
250
271
|
this.baseUrl = bridgeBaseUrl(port);
|
|
251
272
|
this.pingCache = pingCache;
|
|
@@ -281,6 +302,7 @@ export class LiveClient {
|
|
|
281
302
|
this.dialogPolicy = dismissCfg.policy;
|
|
282
303
|
this.allowProjectUpgrade = dismissCfg.allowProjectUpgrade;
|
|
283
304
|
this.allowUnsavedSceneDismiss = dismissCfg.allowUnsavedSceneDismiss;
|
|
305
|
+
this.allowVersionMismatch = dismissCfg.allowVersionMismatch;
|
|
284
306
|
this.compatWarned = false;
|
|
285
307
|
}
|
|
286
308
|
/**
|
|
@@ -303,6 +325,57 @@ export class LiveClient {
|
|
|
303
325
|
console.warn(result.message);
|
|
304
326
|
}
|
|
305
327
|
}
|
|
328
|
+
/** Bounded read-only main-thread snapshot; unavailable bridges fail open to logs. */
|
|
329
|
+
async readCompileState() {
|
|
330
|
+
try {
|
|
331
|
+
this.refreshEndpointFromLock();
|
|
332
|
+
const res = await this.fetchWithTimeout("/compile-state", { method: "GET" }, 2000);
|
|
333
|
+
const body = await res.json();
|
|
334
|
+
return res.ok ? this.acceptCompileState(body) : null;
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/** The compile state that qualifies a tool result: the snapshot the bridge
|
|
341
|
+
* took inside the call's own dispatch when the call asked for it
|
|
342
|
+
* (COMPILE_STATE_HEADER), else a fresh read. An older bridge never attaches
|
|
343
|
+
* one, and the bridge leaves it off once a compile ran during the call's
|
|
344
|
+
* settle wait, so neither path can qualify a result with a state older
|
|
345
|
+
* than the call. */
|
|
346
|
+
async compileStateFor(res) {
|
|
347
|
+
const header = res.headers.get(COMPILE_STATE_HEADER);
|
|
348
|
+
if (header !== null) {
|
|
349
|
+
try {
|
|
350
|
+
return this.acceptCompileState(JSON.parse(Buffer.from(header, "base64").toString("utf8")));
|
|
351
|
+
}
|
|
352
|
+
catch { /* malformed: read it instead */ }
|
|
353
|
+
}
|
|
354
|
+
return this.readCompileState();
|
|
355
|
+
}
|
|
356
|
+
/** A well-formed compile state reported for this client's project, else null. */
|
|
357
|
+
acceptCompileState(body) {
|
|
358
|
+
if (body === null || typeof body !== "object")
|
|
359
|
+
return null;
|
|
360
|
+
if (typeof body.generation !== "number" || typeof body.status !== "string")
|
|
361
|
+
return null;
|
|
362
|
+
if (this.projectPath && !this.sameProject(body.projectPath))
|
|
363
|
+
return null;
|
|
364
|
+
return body;
|
|
365
|
+
}
|
|
366
|
+
/** The bridge reports DirectoryInfo.FullName while the server holds a
|
|
367
|
+
* path.resolve'd string; separators, drive-letter case and /private-style
|
|
368
|
+
* symlinks can differ for the same project, and a strict compare would
|
|
369
|
+
* silently disable live compile state. Compare canonical forms instead. */
|
|
370
|
+
sameProject(reported) {
|
|
371
|
+
if (typeof reported !== "string" || !this.projectPath)
|
|
372
|
+
return false;
|
|
373
|
+
if (this.projectMatch?.reported === reported)
|
|
374
|
+
return this.projectMatch.same;
|
|
375
|
+
const same = canonicalProjectPath(reported) === canonicalProjectPath(this.projectPath);
|
|
376
|
+
this.projectMatch = { reported, same };
|
|
377
|
+
return same;
|
|
378
|
+
}
|
|
306
379
|
async isLiveAvailable(fetchTimeoutMs) {
|
|
307
380
|
try {
|
|
308
381
|
const res = await this.fetchWithTimeout("/ping", { method: "GET" }, fetchTimeoutMs);
|
|
@@ -328,14 +401,14 @@ export class LiveClient {
|
|
|
328
401
|
getCachedPing() {
|
|
329
402
|
return this.pingCache.get();
|
|
330
403
|
}
|
|
331
|
-
async route(toolName, args) {
|
|
404
|
+
async route(toolName, args, commandLifecycle) {
|
|
332
405
|
if (toolName === "unity_open_mcp_ping") {
|
|
333
406
|
return this.handlePing();
|
|
334
407
|
}
|
|
335
408
|
if (toolName === "unity_senses_run_tests") {
|
|
336
409
|
return this.handleRunTests(args);
|
|
337
410
|
}
|
|
338
|
-
return this.handleToolCall(toolName, args);
|
|
411
|
+
return this.handleToolCall(toolName, args, commandLifecycle);
|
|
339
412
|
}
|
|
340
413
|
async handlePing() {
|
|
341
414
|
try {
|
|
@@ -441,7 +514,7 @@ export class LiveClient {
|
|
|
441
514
|
});
|
|
442
515
|
}
|
|
443
516
|
}
|
|
444
|
-
async handleToolCall(toolName, args) {
|
|
517
|
+
async handleToolCall(toolName, args, commandLifecycle) {
|
|
445
518
|
const readyError = await this.ensureReady();
|
|
446
519
|
if (readyError)
|
|
447
520
|
return readyError;
|
|
@@ -460,15 +533,64 @@ export class LiveClient {
|
|
|
460
533
|
// warning that told the agent to "force a rebuild" after `return 42;`.
|
|
461
534
|
// The set below is the subset of compile-reload tools whose PURPOSE is to
|
|
462
535
|
// trigger or observe a recompile; see `shouldAnnotateCompileVerify`.
|
|
463
|
-
const isCompileReload =
|
|
536
|
+
const isCompileReload = lifecycleForCall(toolName, args).class === "compile-reload";
|
|
464
537
|
const annotate = isCompileReload && shouldAnnotateCompileVerify(toolName);
|
|
465
538
|
const before = annotate ? await this.captureCompileSnapshot() : null;
|
|
466
|
-
const result = await this.postTool(toolName, args, true);
|
|
539
|
+
const result = await this.postTool(toolName, args, true, 1, commandLifecycle);
|
|
467
540
|
if (annotate && !result.isError && before !== null) {
|
|
468
541
|
return this.annotateCompileVerify(toolName, result, before, args);
|
|
469
542
|
}
|
|
470
543
|
return result;
|
|
471
544
|
}
|
|
545
|
+
/**
|
|
546
|
+
* Transient client for a per-request `_meta.port` override. The override
|
|
547
|
+
* names a bridge, not a project, so the client binds to the project whose
|
|
548
|
+
* instance lock claims that port: its lock-derived diagnostics
|
|
549
|
+
* (bridge_compile_failed, cold Safe Mode, the ping lockCheck, the offline
|
|
550
|
+
* hint) and its bearer token then describe the Editor actually called, not
|
|
551
|
+
* the configured project. With no claiming lock it binds to the configured
|
|
552
|
+
* project only when that project has no lock and the port is its
|
|
553
|
+
* deterministic one; otherwise it runs without a project, so a failure
|
|
554
|
+
* reports plain bridge_offline instead of another Editor's state. The port
|
|
555
|
+
* is passed as envPort: an explicit override never re-resolves from a lock.
|
|
556
|
+
* A private PingCache keeps the default bridge's readiness snapshot from
|
|
557
|
+
* short-circuiting this bridge's own /ping.
|
|
558
|
+
*/
|
|
559
|
+
static forPortOverride(port, defaultProjectPath, agentId) {
|
|
560
|
+
const lock = findInstanceLockByPort(port);
|
|
561
|
+
const projectPath = lock
|
|
562
|
+
? lock.projectPath
|
|
563
|
+
: defaultProjectPath && !readInstanceLock(defaultProjectPath) && computePort(defaultProjectPath) === port
|
|
564
|
+
? defaultProjectPath
|
|
565
|
+
: undefined;
|
|
566
|
+
const token = lock && isPidAlive(lock.pid) ? lock.authToken : undefined;
|
|
567
|
+
const authToken = typeof token === "string" && token.length > 0 ? token : undefined;
|
|
568
|
+
return new LiveClient(port, new PingCache(), authToken, projectPath, agentId, port);
|
|
569
|
+
}
|
|
570
|
+
/** Preserve configured endpoint/auth when attaching an independent job owner. */
|
|
571
|
+
forAgent(agent) {
|
|
572
|
+
return new LiveClient(Number(new URL(this.baseUrl).port), new PingCache(), this.authToken, this.projectPath, agent, this.envPort);
|
|
573
|
+
}
|
|
574
|
+
/** Job-owned test start has no transport replay; poll the existing file handoff. */
|
|
575
|
+
async runTestsJob(args, onStarted) {
|
|
576
|
+
const readyError = await this.ensureReady();
|
|
577
|
+
if (readyError)
|
|
578
|
+
return readyError;
|
|
579
|
+
const { timeout_ms: _budget, ...startArgs } = args;
|
|
580
|
+
const response = await this.fetchWithTimeout("/tools/unity_senses_run_tests", {
|
|
581
|
+
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(startArgs),
|
|
582
|
+
}, 10_000);
|
|
583
|
+
if (!response.ok)
|
|
584
|
+
throw new Error(`Test start HTTP ${response.status}; execution outcome unknown.`);
|
|
585
|
+
const body = await response.json();
|
|
586
|
+
if (body.status !== "started" || body.runId !== args.run_id) {
|
|
587
|
+
if (body.error || body.mutation)
|
|
588
|
+
return { content: [{ type: "text", text: JSON.stringify(body) }], isError: true };
|
|
589
|
+
throw new Error("Test start did not confirm the owned run id.");
|
|
590
|
+
}
|
|
591
|
+
onStarted();
|
|
592
|
+
return this.pollTestResults(String(args.run_id), args);
|
|
593
|
+
}
|
|
472
594
|
async handleRunTests(args) {
|
|
473
595
|
const readyError = await this.ensureReady();
|
|
474
596
|
if (readyError)
|
|
@@ -669,20 +791,22 @@ export class LiveClient {
|
|
|
669
791
|
* (1 initial attempt + N retries), matching the documented RETRY_CONFIG
|
|
670
792
|
* semantics; once exhausted the offline error is surfaced instead of recursing.
|
|
671
793
|
*/
|
|
672
|
-
async postTool(toolName, args, retryOn503, attempt = 1) {
|
|
794
|
+
async postTool(toolName, args, retryOn503, attempt = 1, commandLifecycle) {
|
|
673
795
|
try {
|
|
674
|
-
const res = await this.postToolFetch(toolName, args, retryOn503);
|
|
796
|
+
const res = await this.postToolFetch(toolName, args, retryOn503, commandLifecycle);
|
|
675
797
|
// 503 + compile settled cleanly → re-POST once without 503 retry. This is
|
|
676
798
|
// a deliberate single re-dispatch (retryOn503 flips to false, so a second
|
|
677
799
|
// 503 surfaces the error), but it still counts as a POST attempt for the
|
|
678
800
|
// recursion bound.
|
|
679
801
|
if (isCompileRepostSentinel(res)) {
|
|
680
|
-
return this.postTool(toolName, args, false, attempt + 1);
|
|
802
|
+
return this.postTool(toolName, args, false, attempt + 1, commandLifecycle);
|
|
681
803
|
}
|
|
682
804
|
return res;
|
|
683
805
|
}
|
|
684
806
|
catch (err) {
|
|
685
|
-
|
|
807
|
+
if (err instanceof Error && err.message.startsWith("bridge_response_request_mismatch"))
|
|
808
|
+
return makeErrorResult({ code: "bridge_response_request_mismatch", message: err.message });
|
|
809
|
+
return this.handlePostFailure(toolName, args, err, retryOn503, attempt, commandLifecycle);
|
|
686
810
|
}
|
|
687
811
|
}
|
|
688
812
|
/**
|
|
@@ -693,7 +817,7 @@ export class LiveClient {
|
|
|
693
817
|
* should re-POST without 503 retry. Throws on transient fetch failure so the
|
|
694
818
|
* caller's catch runs {@link handlePostFailure}.
|
|
695
819
|
*/
|
|
696
|
-
async postToolFetch(toolName, args, retryOn503) {
|
|
820
|
+
async postToolFetch(toolName, args, retryOn503, commandLifecycle) {
|
|
697
821
|
const rawTimeout = args.timeout_ms;
|
|
698
822
|
const explicitTimeout = typeof rawTimeout === "number";
|
|
699
823
|
const requestedTimeoutMs = explicitTimeout ? rawTimeout : 60_000;
|
|
@@ -719,7 +843,12 @@ export class LiveClient {
|
|
|
719
843
|
const fetchTimeout = Math.max(timeoutMs + 10_000, BRIDGE_DEFAULT_TIMEOUT_MS + 10_000);
|
|
720
844
|
const res = await this.fetchWithTimeout(`/tools/${toolName}`, {
|
|
721
845
|
method: "POST",
|
|
722
|
-
headers: {
|
|
846
|
+
headers: {
|
|
847
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
848
|
+
// shapeToolResult qualifies these results with the compile state;
|
|
849
|
+
// the bridge snapshots it inside this call's dispatch on request.
|
|
850
|
+
...(STALE_DOMAIN_ANNOTATION_TOOLS.has(toolName) ? { [COMPILE_STATE_HEADER]: "1" } : {}),
|
|
851
|
+
},
|
|
723
852
|
body: JSON.stringify(forwardedArgs),
|
|
724
853
|
}, fetchTimeout);
|
|
725
854
|
if (res.status === 503 && retryOn503) {
|
|
@@ -743,7 +872,7 @@ export class LiveClient {
|
|
|
743
872
|
},
|
|
744
873
|
});
|
|
745
874
|
}
|
|
746
|
-
const result = await this.shapeToolResult(toolName, res);
|
|
875
|
+
const result = await this.shapeToolResult(toolName, res, commandLifecycle, args);
|
|
747
876
|
// Surface the clamp ONLY when the caller explicitly requested a timeout
|
|
748
877
|
// above the cap (feedback N1). When timeout_ms is absent the server default
|
|
749
878
|
// (60s) is silently clamped to the cap — there is no caller expectation to
|
|
@@ -776,7 +905,7 @@ export class LiveClient {
|
|
|
776
905
|
* - direct body (verbatim, isError when a top-level error field is present),
|
|
777
906
|
* - unparsable body (bridge_response_unparsable corruption error).
|
|
778
907
|
*/
|
|
779
|
-
async shapeToolResult(toolName, res) {
|
|
908
|
+
async shapeToolResult(toolName, res, commandLifecycle, args = {}) {
|
|
780
909
|
// The bridge returns TWO response shapes from /tools/*:
|
|
781
910
|
// 1. A MUTATION envelope: { mutation: { success, ... }, gate: {...} }
|
|
782
911
|
// for gate-tracked mutating tools (gameobject_*, assets_*, etc.).
|
|
@@ -830,7 +959,27 @@ export class LiveClient {
|
|
|
830
959
|
STALE_DOMAIN_ANNOTATION_TOOLS.has(toolName) &&
|
|
831
960
|
parsed.error == null &&
|
|
832
961
|
parsed._staleDomain === undefined) {
|
|
833
|
-
const
|
|
962
|
+
const liveCompile = await this.compileStateFor(res);
|
|
963
|
+
const staleAsm = liveCompile?.status === "no_errors_found" && liveCompile.sourceMatches === true
|
|
964
|
+
? null : liveCompile?.status === "assembly_stale"
|
|
965
|
+
? { staleAssembly: true, newerSources: [], hint: "Source content changed after the completed compile generation. Recompile before trusting this result." }
|
|
966
|
+
: this.resolveStaleAssembly();
|
|
967
|
+
if (liveCompile) {
|
|
968
|
+
// Compact projection only. The full body carries `errors[]` — every
|
|
969
|
+
// CS diagnostic of a failed compile — and would repeat it on every
|
|
970
|
+
// read result for the rest of the session; read_compile_errors owns
|
|
971
|
+
// the list. Keep just the fields that qualify THIS result.
|
|
972
|
+
parsed._compileState = {
|
|
973
|
+
status: liveCompile.status,
|
|
974
|
+
generation: liveCompile.generation,
|
|
975
|
+
sourceMatches: liveCompile.sourceMatches,
|
|
976
|
+
beforeAssemblyMtimeMs: liveCompile.beforeAssemblyMtimeMs,
|
|
977
|
+
afterAssemblyMtimeMs: liveCompile.afterAssemblyMtimeMs,
|
|
978
|
+
// The header snapshot carries the count; a /compile-state read, the list.
|
|
979
|
+
errorCount: Array.isArray(liveCompile.errors) ? liveCompile.errors.length
|
|
980
|
+
: typeof liveCompile.errorCount === "number" ? liveCompile.errorCount : 0,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
834
983
|
if (staleAsm != null && staleAsm.staleAssembly) {
|
|
835
984
|
// specs/feedback.md 2026-08-14 — a stale assembly plus a wedged build
|
|
836
985
|
// driver is not a footnote, it is an execution-integrity failure. In
|
|
@@ -950,7 +1099,7 @@ export class LiveClient {
|
|
|
950
1099
|
// 2026-07-03-c contract) — that path keeps bridge_response_unparsable.
|
|
951
1100
|
if (parsed == null) {
|
|
952
1101
|
const isEmptyLikeReload = rawText.trim().length === 0;
|
|
953
|
-
if (isEmptyLikeReload &&
|
|
1102
|
+
if (isEmptyLikeReload && (commandLifecycle === "restart_then_settle" || lifecycleForCall(toolName, args).class === "compile-reload")) {
|
|
954
1103
|
return {
|
|
955
1104
|
content: [
|
|
956
1105
|
{
|
|
@@ -1036,7 +1185,7 @@ export class LiveClient {
|
|
|
1036
1185
|
* itself bounded (`transientRetryAttempts` iterations of `/ping`); this outer
|
|
1037
1186
|
* bound caps how many times we re-enter that recovery + re-POST cycle.
|
|
1038
1187
|
*/
|
|
1039
|
-
async handlePostFailure(toolName, args, err, retryOn503, attempt) {
|
|
1188
|
+
async handlePostFailure(toolName, args, err, retryOn503, attempt, commandLifecycle) {
|
|
1040
1189
|
const endpointBefore = this.baseUrl;
|
|
1041
1190
|
const recovered = await this.handleTransientOffline("post");
|
|
1042
1191
|
if (recovered !== null)
|
|
@@ -1046,9 +1195,10 @@ export class LiveClient {
|
|
|
1046
1195
|
// budget is the initial attempt plus the configured transient retry count.
|
|
1047
1196
|
const maxAttempts = 1 + this.retry.transientRetryAttempts;
|
|
1048
1197
|
const attemptsExhausted = attempt >= maxAttempts;
|
|
1049
|
-
if (
|
|
1198
|
+
if (toolName !== "unity_open_mcp_project_commands" &&
|
|
1199
|
+
!attemptsExhausted &&
|
|
1050
1200
|
shouldRetryPostAfterFailure(err, endpointChanged)) {
|
|
1051
|
-
return this.postTool(toolName, args, retryOn503, attempt + 1);
|
|
1201
|
+
return this.postTool(toolName, args, retryOn503, attempt + 1, commandLifecycle);
|
|
1052
1202
|
}
|
|
1053
1203
|
const exhaustedNote = attemptsExhausted
|
|
1054
1204
|
? ` The POST retry budget (${maxAttempts} attempt${maxAttempts === 1 ? "" : "s"}) was exhausted after ${attempt} attempt${attempt === 1 ? "" : "s"}; the bridge is reachable by /ping but POSTs keep failing.`
|
|
@@ -1461,6 +1611,7 @@ export class LiveClient {
|
|
|
1461
1611
|
policy: this.dialogPolicy,
|
|
1462
1612
|
allowProjectUpgrade: this.allowProjectUpgrade,
|
|
1463
1613
|
allowUnsavedSceneDismiss: this.allowUnsavedSceneDismiss,
|
|
1614
|
+
allowVersionMismatch: this.allowVersionMismatch,
|
|
1464
1615
|
abortSignal: dismissAbort.signal,
|
|
1465
1616
|
};
|
|
1466
1617
|
dismissDone = this.runDismissLoop(dismissOpts);
|
|
@@ -1558,6 +1709,36 @@ export class LiveClient {
|
|
|
1558
1709
|
};
|
|
1559
1710
|
}
|
|
1560
1711
|
}
|
|
1712
|
+
/** Private job transport: one bounded request, never an automatic mutation retry. */
|
|
1713
|
+
async projectCommandJob(args) {
|
|
1714
|
+
const response = await this.fetchWithTimeout("/project-command-jobs", {
|
|
1715
|
+
method: "POST", body: JSON.stringify(args), headers: { "Content-Type": "application/json" },
|
|
1716
|
+
}, 10_000);
|
|
1717
|
+
if (!response.ok)
|
|
1718
|
+
throw new ProjectJobHttpError(response.status);
|
|
1719
|
+
return await response.json();
|
|
1720
|
+
}
|
|
1721
|
+
/** Fresh authenticated bounded catalog read; no disk/session cache can hide a reload. */
|
|
1722
|
+
async projectCommands(args) {
|
|
1723
|
+
const query = new URLSearchParams({ catalog: "project_commands" });
|
|
1724
|
+
for (const key of ["action", "id", "query", "group", "package", "offset", "limit"])
|
|
1725
|
+
if (args[key] !== undefined)
|
|
1726
|
+
query.set(key, String(args[key]));
|
|
1727
|
+
for (const tag of args.tags ?? [])
|
|
1728
|
+
query.append("tag", tag);
|
|
1729
|
+
try {
|
|
1730
|
+
const response = await this.fetchWithTimeout(`/tools?${query}`, { method: "GET" }, 5_000);
|
|
1731
|
+
if (!response.ok)
|
|
1732
|
+
return { error: { code: "catalog_unavailable", message: `Bridge catalog HTTP ${response.status}.` } };
|
|
1733
|
+
const body = await response.json();
|
|
1734
|
+
if (body.catalogVersion !== 1 || (!body.error && (args.action === "describe" ? !body.command : !Array.isArray(body.commands))))
|
|
1735
|
+
return { error: { code: "catalog_unsupported", message: "Bridge does not support project command catalog version 1. Update the bridge." } };
|
|
1736
|
+
return body;
|
|
1737
|
+
}
|
|
1738
|
+
catch {
|
|
1739
|
+
return { error: { code: "catalog_unavailable", message: "Project command catalog requires a reachable live Editor. Retry after compilation/reload settles." } };
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1561
1742
|
/**
|
|
1562
1743
|
* M18 Plan 2 / T18.2.3 — fetch the compiled-state tool inventory from the
|
|
1563
1744
|
* bridge (`GET /tools`). Returns the set of tool names the bridge compiled
|
|
@@ -1637,6 +1818,12 @@ export class LiveClient {
|
|
|
1637
1818
|
const dllMtime = this.newestScriptAssembliesMtime();
|
|
1638
1819
|
if (dllMtime !== undefined)
|
|
1639
1820
|
snap.dllMtimeMs = dllMtime;
|
|
1821
|
+
const compile = await this.readCompileState();
|
|
1822
|
+
if (compile) {
|
|
1823
|
+
snap.compileGeneration = compile.generation;
|
|
1824
|
+
snap.compileStatus = compile.status;
|
|
1825
|
+
snap.sourceMatches = compile.sourceMatches === true && compile.status === "no_errors_found";
|
|
1826
|
+
}
|
|
1640
1827
|
return snap;
|
|
1641
1828
|
}
|
|
1642
1829
|
/**
|
|
@@ -1739,8 +1926,6 @@ export class LiveClient {
|
|
|
1739
1926
|
: undefined;
|
|
1740
1927
|
const detection = detectCompileVerify({ before, after, sourceMtimeMs });
|
|
1741
1928
|
const annotation = buildCompileVerifyAnnotation(detection);
|
|
1742
|
-
if (annotation === null)
|
|
1743
|
-
return result;
|
|
1744
1929
|
// Inject into the first text content block's JSON body. Mirrors the
|
|
1745
1930
|
// injectRouteMeta shape in tool-router.ts.
|
|
1746
1931
|
const textIndex = result.content.findIndex((c) => c.type === "text");
|
|
@@ -1751,7 +1936,7 @@ export class LiveClient {
|
|
|
1751
1936
|
return result;
|
|
1752
1937
|
try {
|
|
1753
1938
|
const body = JSON.parse(block.text);
|
|
1754
|
-
body._compileVerify = annotation;
|
|
1939
|
+
body._compileVerify = { ...annotation, before, after };
|
|
1755
1940
|
const newContent = result.content.slice();
|
|
1756
1941
|
newContent[textIndex] = { type: "text", text: JSON.stringify(body) };
|
|
1757
1942
|
return { ...result, content: newContent };
|
|
@@ -1819,6 +2004,8 @@ export class LiveClient {
|
|
|
1819
2004
|
// from the instance lock. Merge with any caller-supplied headers so we
|
|
1820
2005
|
// never clobber a per-request value.
|
|
1821
2006
|
const headers = new Headers(init.headers);
|
|
2007
|
+
const requestId = randomUUID();
|
|
2008
|
+
headers.set("X-Request-Id", requestId);
|
|
1822
2009
|
if (this.authToken && !headers.has("Authorization")) {
|
|
1823
2010
|
headers.set("Authorization", `Bearer ${this.authToken}`);
|
|
1824
2011
|
}
|
|
@@ -1833,6 +2020,12 @@ export class LiveClient {
|
|
|
1833
2020
|
headers,
|
|
1834
2021
|
signal: controller.signal,
|
|
1835
2022
|
}).then((res) => {
|
|
2023
|
+
const echoedId = res.headers.get("X-Request-Id");
|
|
2024
|
+
if (echoedId !== null && echoedId !== requestId) {
|
|
2025
|
+
controller.abort();
|
|
2026
|
+
dispose();
|
|
2027
|
+
throw new Error("bridge_response_request_mismatch: response belongs to another request");
|
|
2028
|
+
}
|
|
1836
2029
|
// Wrap the body-reading accessors so the abort timer is cleared when
|
|
1837
2030
|
// the body is consumed (the common path) or errors. The wrappers
|
|
1838
2031
|
// delegate to the original methods; they only add the dispose() side
|
|
@@ -1855,8 +2048,10 @@ export class LiveClient {
|
|
|
1855
2048
|
// body WAS claimed, the fallback is a no-op — the abort timer
|
|
1856
2049
|
// remains armed to protect the in-progress read.
|
|
1857
2050
|
claimTimer = setTimeout(() => {
|
|
1858
|
-
if (!bodyClaimed)
|
|
2051
|
+
if (!bodyClaimed) {
|
|
2052
|
+
controller.abort();
|
|
1859
2053
|
dispose();
|
|
2054
|
+
}
|
|
1860
2055
|
}, 100);
|
|
1861
2056
|
claimTimer.unref();
|
|
1862
2057
|
}
|