unity-open-mcp 1.4.0 → 1.6.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/dist/capabilities/cost-hints.js +73 -0
- package/dist/capabilities/cost-hints.js.map +1 -1
- package/dist/capabilities/intent-groups.js +76 -4
- package/dist/capabilities/intent-groups.js.map +1 -1
- package/dist/capabilities/lifecycle.js +9 -0
- package/dist/capabilities/lifecycle.js.map +1 -1
- package/dist/capabilities/tool-groups.js +75 -17
- package/dist/capabilities/tool-groups.js.map +1 -1
- package/dist/compressible-router.d.ts +4 -1
- package/dist/compressible-router.js +27 -11
- package/dist/compressible-router.js.map +1 -1
- package/dist/constants.d.ts +2 -2
- package/dist/constants.js +2 -2
- package/dist/event-stream.d.ts +10 -0
- package/dist/event-stream.js +29 -0
- package/dist/event-stream.js.map +1 -1
- package/dist/game-view-settle.d.ts +18 -0
- package/dist/game-view-settle.js +81 -0
- package/dist/game-view-settle.js.map +1 -0
- package/dist/jobs/adapters.d.ts +59 -0
- package/dist/jobs/adapters.js +226 -0
- package/dist/jobs/adapters.js.map +1 -1
- package/dist/live-client.d.ts +17 -60
- package/dist/live-client.js +52 -87
- package/dist/live-client.js.map +1 -1
- package/dist/local-tools.js +3 -0
- package/dist/local-tools.js.map +1 -1
- package/dist/offline/api.d.ts +2 -0
- package/dist/offline/api.js +4 -2
- package/dist/offline/api.js.map +1 -1
- package/dist/offline/model.js +1 -1
- package/dist/offline/model.js.map +1 -1
- package/dist/offline/parse.d.ts +1 -1
- package/dist/offline/parse.js +20 -8
- package/dist/offline/parse.js.map +1 -1
- package/dist/playmode-routes.d.ts +17 -0
- package/dist/playmode-routes.js +50 -0
- package/dist/playmode-routes.js.map +1 -0
- package/dist/playtest/png.d.ts +27 -0
- package/dist/playtest/png.js +248 -0
- package/dist/playtest/png.js.map +1 -0
- package/dist/playtest/recorder.d.ts +203 -0
- package/dist/playtest/recorder.js +945 -0
- package/dist/playtest/recorder.js.map +1 -0
- package/dist/playtest/redact.d.ts +16 -0
- package/dist/playtest/redact.js +124 -0
- package/dist/playtest/redact.js.map +1 -0
- package/dist/playtest/report.d.ts +73 -0
- package/dist/playtest/report.js +477 -0
- package/dist/playtest/report.js.map +1 -0
- package/dist/playtest/session-store.d.ts +78 -0
- package/dist/playtest/session-store.js +224 -0
- package/dist/playtest/session-store.js.map +1 -0
- package/dist/skill/SKILL.md +22 -3
- package/dist/skill/references/senses-and-tests.md +3 -0
- package/dist/skill/references/yaml-and-offline-work.md +1 -1
- package/dist/tool-router.d.ts +44 -0
- package/dist/tool-router.js +249 -7
- package/dist/tool-router.js.map +1 -1
- package/dist/tools/batch-execute.js +5 -1
- package/dist/tools/batch-execute.js.map +1 -1
- package/dist/tools/capture-inline.js +30 -12
- package/dist/tools/capture-inline.js.map +1 -1
- package/dist/tools/capture-series.d.ts +46 -0
- package/dist/tools/capture-series.js +116 -0
- package/dist/tools/capture-series.js.map +1 -0
- package/dist/tools/game-view.d.ts +44 -0
- package/dist/tools/game-view.js +103 -0
- package/dist/tools/game-view.js.map +1 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +34 -7
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inputsim-gesture.d.ts +46 -0
- package/dist/tools/inputsim-gesture.js +123 -0
- package/dist/tools/inputsim-gesture.js.map +1 -0
- package/dist/tools/inputsim-pointer.js +35 -2
- package/dist/tools/inputsim-pointer.js.map +1 -1
- package/dist/tools/inputsim-touch.js +42 -22
- package/dist/tools/inputsim-touch.js.map +1 -1
- package/dist/tools/jobs.js +1 -1
- package/dist/tools/jobs.js.map +1 -1
- package/dist/tools/player-state.d.ts +42 -0
- package/dist/tools/player-state.js +142 -0
- package/dist/tools/player-state.js.map +1 -0
- package/dist/tools/playtest-conditions.d.ts +334 -0
- package/dist/tools/playtest-conditions.js +99 -0
- package/dist/tools/playtest-conditions.js.map +1 -0
- package/dist/tools/playtest-session.d.ts +45 -0
- package/dist/tools/playtest-session.js +105 -0
- package/dist/tools/playtest-session.js.map +1 -0
- package/dist/tools/playtest-wait.d.ts +46 -0
- package/dist/tools/playtest-wait.js +86 -0
- package/dist/tools/playtest-wait.js.map +1 -0
- package/dist/tools/screenshot.js +31 -8
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/text-target.d.ts +22 -0
- package/dist/tools/text-target.js +39 -0
- package/dist/tools/text-target.js.map +1 -0
- package/dist/tools/ui-snapshot.d.ts +41 -0
- package/dist/tools/ui-snapshot.js +87 -0
- package/dist/tools/ui-snapshot.js.map +1 -0
- package/package.json +1 -1
package/dist/tool-router.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { testRunOperation, projectCommandOperation, playmodeOperation, pollPlaymodeOperation, playmodeResult, toolOutput, toolResultBody, PLAYMODE_OPERATION_TOOLS, playmodeEstimateMs, playmodeFailureCode } from "./jobs/adapters.js";
|
|
3
|
+
import { PLAYMODE_ROUTES } from "./playmode-routes.js";
|
|
4
|
+
import { routeGameView as settleGameView } from "./game-view-settle.js";
|
|
2
5
|
import { JobManager, JobManagerError } from "./jobs/job-manager.js";
|
|
3
6
|
import { handleJobs } from "./jobs/handler.js";
|
|
4
7
|
import { PROCESS_AGENT_ID, PROCESS_IDENTITY } from "./agent-identity.js";
|
|
@@ -29,8 +32,12 @@ import { generateSkill } from "./skill/generate-skill.js";
|
|
|
29
32
|
import { getKnownClientKeys } from "./skill/client-paths.js";
|
|
30
33
|
import { ALL_TOOLS } from "./tools/index.js";
|
|
31
34
|
import { lockPath, readInstanceLock, classifyInstance, isPidAlive } from "./instance-discovery.js";
|
|
32
|
-
import { PORT_ENV_VAR, EXPECTED_BRIDGE_WIRE_CONTRACT } from "./constants.js";
|
|
35
|
+
import { PORT_ENV_VAR, EXPECTED_BRIDGE_WIRE_CONTRACT, BRIDGE_HOST_SAFE_TIMEOUT_CAP_MS } from "./constants.js";
|
|
33
36
|
import { findUnityForProject, readProcessCommandLine } from "./running-unity.js";
|
|
37
|
+
import { PlaytestRecorder, PlaytestError, PLAYTEST_SESSION_TOOL } from "./playtest/recorder.js";
|
|
38
|
+
import { secretEnvValues } from "./playtest/redact.js";
|
|
39
|
+
import { sessionsRoot } from "./playtest/session-store.js";
|
|
40
|
+
import { PLAYTEST_SESSION_ACTION_KEYS } from "./tools/playtest-session.js";
|
|
34
41
|
// M31-optimizations Plan 1 / L14 — opt-in route-logging gate. Resolved once at
|
|
35
42
|
// module load into a boolean; the per-dispatch log sites read the boolean
|
|
36
43
|
// (not process.env on every call) so the hot path stays a single branch.
|
|
@@ -573,6 +580,11 @@ export class ToolRouter {
|
|
|
573
580
|
if (!this.jobs.hasOperation(name))
|
|
574
581
|
this.jobs.register(name, testRunOperation(clientFor));
|
|
575
582
|
}
|
|
583
|
+
else if (PLAYMODE_OPERATION_TOOLS.has(name)) {
|
|
584
|
+
// One generic adapter for every tool on the bridge's shared play-mode scheduler.
|
|
585
|
+
if (!this.jobs.hasOperation(name))
|
|
586
|
+
this.jobs.register(name, playmodeOperation(name, PLAYMODE_OPERATION_TOOLS.get(name), clientFor));
|
|
587
|
+
}
|
|
576
588
|
else if (name.startsWith("project.") && !(typeof key === "string" && this.jobs.hasKey(owner, key))) {
|
|
577
589
|
// A retry whose key already names a job gets that job (or
|
|
578
590
|
// idempotency_conflict) from start() without the catalog, which may
|
|
@@ -602,6 +614,80 @@ export class ToolRouter {
|
|
|
602
614
|
return localError(error instanceof JobManagerError ? error.code : "invalid_arguments", error instanceof Error ? error.message : String(error));
|
|
603
615
|
}
|
|
604
616
|
}
|
|
617
|
+
/**
|
|
618
|
+
* Play-mode operations (`inputsim_gesture`, `playtest_wait`,
|
|
619
|
+
* `capture_series`): the bridge starts the operation on its shared play-mode
|
|
620
|
+
* scheduler and answers at once; this route polls POST /playmode-operations
|
|
621
|
+
* until the terminal result, inside the host-safe window. An operation whose
|
|
622
|
+
* worst case (`playmodeEstimateMs`: last keyframe / capture, the wait's
|
|
623
|
+
* timeout, a series' start_on wait + schedule + encoding) exceeds the inline
|
|
624
|
+
* budget is started as a `jobs` play-mode operation instead and the call
|
|
625
|
+
* returns that job id. One that outlives the inline poll anyway (gameplay
|
|
626
|
+
* time, a paused Editor) is cancelled — which releases held input and
|
|
627
|
+
* restores a wait's time scale — and the error points at the jobs route.
|
|
628
|
+
*/
|
|
629
|
+
async routePlaymodeOperation(name, args, live, identity) {
|
|
630
|
+
const spec = PLAYMODE_ROUTES[name];
|
|
631
|
+
const estimateMs = playmodeEstimateMs(name, args);
|
|
632
|
+
if (estimateMs > spec.inlineLimitMs) {
|
|
633
|
+
// The job owns the bridge operation id; a caller's operation_id keeps
|
|
634
|
+
// its retry meaning as the job's idempotency key.
|
|
635
|
+
const { operation_id: retryKey, ...jobArgs } = args;
|
|
636
|
+
const started = await this.routeJobs({ action: "start", tool_or_command: name, args: jobArgs,
|
|
637
|
+
...(typeof retryKey === "string" ? { idempotency_key: retryKey } : {}) }, identity);
|
|
638
|
+
const body = toolResultBody(started);
|
|
639
|
+
if (started.isError || typeof body.job_id !== "string")
|
|
640
|
+
return started;
|
|
641
|
+
return sourceResult({ ...body, note: `${spec.label} can take ${Math.round(estimateMs)} ms (> ${spec.inlineLimitMs} ms inline budget); it runs as a job.`,
|
|
642
|
+
agentNextSteps: [`Call unity_open_mcp_jobs {"action":"wait","job_id":"${body.job_id}","timeout_ms":30000} until the state is terminal; the result holds ${spec.resultHint}.`] }, "local");
|
|
643
|
+
}
|
|
644
|
+
const operationId = typeof args.operation_id === "string" ? args.operation_id : randomUUID();
|
|
645
|
+
const start = await live.route(name, { ...args, operation_id: operationId });
|
|
646
|
+
const started = toolOutput(start);
|
|
647
|
+
if (start.isError || started.error || !started.operation)
|
|
648
|
+
return injectRouteMeta(start, { route: "live" });
|
|
649
|
+
const outcome = await pollPlaymodeOperation(live, operationId, {
|
|
650
|
+
deadlineMs: Math.min(estimateMs + 15_000, BRIDGE_HOST_SAFE_TIMEOUT_CAP_MS - 5_000),
|
|
651
|
+
pollIntervalMs: estimateMs < 2_000 ? 50 : 100,
|
|
652
|
+
unreachableMs: 10_000,
|
|
653
|
+
});
|
|
654
|
+
const meta = spec.startMeta(started);
|
|
655
|
+
// The poll's own deadline cancel, acknowledged: the operation stalled (a
|
|
656
|
+
// paused Editor never ticks it), it did not end by itself.
|
|
657
|
+
const stalled = outcome.kind === "deadline" || (outcome.kind === "terminal" && outcome.cancelSent && outcome.snapshot.state === "cancelled");
|
|
658
|
+
if (stalled) {
|
|
659
|
+
return sourceResult({ error: { code: spec.stalledCode, message: `The ${spec.noun} did not finish inside the inline window (a paused Editor, gameplay time, or a slow frame rate); it was cancelled and its cleanup ${spec.cleanup}.` },
|
|
660
|
+
operationId, last: outcome.snapshot, agentNextSteps: [`Check that play mode is running (not paused) and the Game view renders, then retry — or run it as a job: unity_open_mcp_jobs {"action":"start","tool_or_command":"${name}","args":{…}}, then wait on the job id.`] }, "live", true);
|
|
661
|
+
}
|
|
662
|
+
switch (outcome.kind) {
|
|
663
|
+
case "lost":
|
|
664
|
+
return sourceResult({ error: { code: "operation_lost", message: `The bridge lost the ${spec.noun} (domain reload or Editor restart); its cleanup ${spec.cleanup}.` }, operationId }, "live", true);
|
|
665
|
+
case "unreachable":
|
|
666
|
+
return sourceResult({ error: { code: "job_status_unavailable", message: outcome.error }, operationId }, "live", true);
|
|
667
|
+
}
|
|
668
|
+
if (outcome.kind !== "terminal")
|
|
669
|
+
throw new Error("unreachable play-mode poll outcome");
|
|
670
|
+
const snapshot = outcome.snapshot;
|
|
671
|
+
const result = { ...meta, ...playmodeResult(snapshot) };
|
|
672
|
+
if (snapshot.state === "succeeded")
|
|
673
|
+
return sourceResult({ status: "ok", ...result }, "live");
|
|
674
|
+
const code = playmodeFailureCode(snapshot, spec.failedCode);
|
|
675
|
+
// The operation's own `error` (a message string) becomes error.message.
|
|
676
|
+
return sourceResult({ ...result, error: { code, message: String(snapshot.result?.error ?? `${spec.label} ended ${snapshot.reason ?? snapshot.state}; its cleanup ${spec.cleanup}.`) },
|
|
677
|
+
...(spec.failureNextSteps(code) ? { agentNextSteps: spec.failureNextSteps(code) } : {}) }, "live", true);
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* `game_view`: set_resolution / set_device in play mode return once the game
|
|
681
|
+
* reports the new size — the bridge starts a settle watch on its play-mode
|
|
682
|
+
* scheduler and this route polls it (then re-reads `get`), or answers
|
|
683
|
+
* `not_settled`. get / list_devices and edit-mode changes pass through.
|
|
684
|
+
*/
|
|
685
|
+
async routeGameView(args, live) {
|
|
686
|
+
const outcome = await settleGameView(live, args);
|
|
687
|
+
if (outcome.kind === "passthrough")
|
|
688
|
+
return injectRouteMeta(outcome.result, { route: "live" });
|
|
689
|
+
return sourceResult(outcome.body, "live", outcome.isError);
|
|
690
|
+
}
|
|
605
691
|
modelCache = new AssetModelCache();
|
|
606
692
|
/**
|
|
607
693
|
* M28-refactoring Plan 3 (T3.2) — named-tool dispatch map. Replaces the
|
|
@@ -628,6 +714,9 @@ export class ToolRouter {
|
|
|
628
714
|
["unity_open_mcp_generate_skill", (_l, a) => this.routeGenerateSkill(a)],
|
|
629
715
|
["unity_open_mcp_manage_tools", (l, a, id) => this.routeManageTools(a, l, id)],
|
|
630
716
|
["unity_open_mcp_project_commands", (l, a) => this.routeProjectCommands(a, l)],
|
|
717
|
+
// Playtest session recorder: the log lives here (the server sees every
|
|
718
|
+
// call); bridge reads go through the normal route as the same owner.
|
|
719
|
+
[PLAYTEST_SESSION_TOOL, (l, a, id) => this.routePlaytestSession(a, l, id)],
|
|
631
720
|
// Live tool whose release lookups (npm latest, GitHub tag) run HERE so
|
|
632
721
|
// the Editor main thread never blocks on the network.
|
|
633
722
|
["unity_open_mcp_upgrade", (l, a) => this.routeUpgrade(a, l)],
|
|
@@ -636,6 +725,15 @@ export class ToolRouter {
|
|
|
636
725
|
// the process agent id, whatever _meta.agentId the call carried. Direct,
|
|
637
726
|
// manage_tools(invoke) and port-override calls therefore share one owner.
|
|
638
727
|
["unity_open_mcp_jobs", (_l, a, id) => this.routeJobs(a, id)],
|
|
728
|
+
// Gestures, waits and capture series run on the bridge's play-mode
|
|
729
|
+
// scheduler: start, then poll the operation to its terminal result here
|
|
730
|
+
// (long ones become a job).
|
|
731
|
+
["unity_open_mcp_inputsim_gesture", (l, a, id) => this.routePlaymodeOperation("unity_open_mcp_inputsim_gesture", a, l, id)],
|
|
732
|
+
["unity_open_mcp_playtest_wait", (l, a, id) => this.routePlaymodeOperation("unity_open_mcp_playtest_wait", a, l, id)],
|
|
733
|
+
["unity_senses_capture_series", (l, a, id) => this.routePlaymodeOperation("unity_senses_capture_series", a, l, id)],
|
|
734
|
+
// Game view / Simulator changes settle over play-mode frames: poll the
|
|
735
|
+
// bridge's settle watch here so set_* returns once the game sees the size.
|
|
736
|
+
["unity_open_mcp_game_view", (l, a) => this.routeGameView(a, l)],
|
|
639
737
|
["unity_open_mcp_bridge_status", (l, a) => this.routeBridgeStatus(a, l)],
|
|
640
738
|
// M31 Plan 3 — Editor fd-exhaustion operator surfaces. restart_editor
|
|
641
739
|
// is the reactive kill half (acts AFTER the Editor is hung);
|
|
@@ -756,7 +854,142 @@ export class ToolRouter {
|
|
|
756
854
|
async routeOverride(toolName, args, overrideLive, identity) {
|
|
757
855
|
return this.routeCore(overrideLive, toolName, args, identity);
|
|
758
856
|
}
|
|
857
|
+
/**
|
|
858
|
+
* Every entry point funnels through here (direct, manage_tools invoke, port
|
|
859
|
+
* override). While the caller's agent owner has an open playtest session, a
|
|
860
|
+
* recorded call (input / wait / capture / project command / play-mode and
|
|
861
|
+
* Game view changes) is logged around its dispatch; everything else — and
|
|
862
|
+
* every call when no session is open — goes straight through. The recorder
|
|
863
|
+
* never changes a result and never fails a call.
|
|
864
|
+
*/
|
|
759
865
|
async routeCore(live, toolName, args, identity) {
|
|
866
|
+
const recorder = this.playtestRecorder;
|
|
867
|
+
if (!recorder || recorder.openCount === 0 || toolName === PLAYTEST_SESSION_TOOL)
|
|
868
|
+
return this.routeDispatch(live, toolName, args, identity);
|
|
869
|
+
const owner = this.ownerOf(identity);
|
|
870
|
+
if (!recorder.isRecording(owner))
|
|
871
|
+
return this.routeDispatch(live, toolName, args, identity);
|
|
872
|
+
if (toolName === "unity_open_mcp_jobs" && (args.action === "wait" || args.action === "status")) {
|
|
873
|
+
const observed = await this.routeDispatch(live, toolName, args, identity);
|
|
874
|
+
try {
|
|
875
|
+
recorder.observeJob(owner, args, observed);
|
|
876
|
+
}
|
|
877
|
+
catch (error) {
|
|
878
|
+
console.error("[unity-open-mcp] playtest recorder:", error);
|
|
879
|
+
}
|
|
880
|
+
return observed;
|
|
881
|
+
}
|
|
882
|
+
let handle = null;
|
|
883
|
+
try {
|
|
884
|
+
handle = recorder.begin(owner, toolName, args);
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
console.error("[unity-open-mcp] playtest recorder:", error);
|
|
888
|
+
}
|
|
889
|
+
if (!handle)
|
|
890
|
+
return this.routeDispatch(live, toolName, args, identity);
|
|
891
|
+
this.eventStream.reconnectNow?.call(this.eventStream);
|
|
892
|
+
let result;
|
|
893
|
+
try {
|
|
894
|
+
result = await this.routeDispatch(live, toolName, args, identity);
|
|
895
|
+
}
|
|
896
|
+
catch (error) {
|
|
897
|
+
try {
|
|
898
|
+
recorder.finish(handle, args, null, error);
|
|
899
|
+
}
|
|
900
|
+
catch (inner) {
|
|
901
|
+
console.error("[unity-open-mcp] playtest recorder:", inner);
|
|
902
|
+
}
|
|
903
|
+
throw error;
|
|
904
|
+
}
|
|
905
|
+
try {
|
|
906
|
+
recorder.finish(handle, args, result);
|
|
907
|
+
}
|
|
908
|
+
catch (error) {
|
|
909
|
+
console.error("[unity-open-mcp] playtest recorder:", error);
|
|
910
|
+
}
|
|
911
|
+
return result;
|
|
912
|
+
}
|
|
913
|
+
/** The owner a request files sessions (and jobs) under. */
|
|
914
|
+
ownerOf(identity) {
|
|
915
|
+
return { project: this.projectPath || "default", agent: identity.agent, ...(identity.port === undefined ? {} : { port: identity.port }) };
|
|
916
|
+
}
|
|
917
|
+
playtestRecorder;
|
|
918
|
+
/** The session recorder, created on first use and fed by the bridge event stream. */
|
|
919
|
+
get playtest() {
|
|
920
|
+
if (!this.playtestRecorder) {
|
|
921
|
+
this.usePlaytestRecorder(new PlaytestRecorder({
|
|
922
|
+
root: sessionsRoot(),
|
|
923
|
+
projectPath: this.projectPath || "default",
|
|
924
|
+
knownSecrets: [this.projectPath ? resolveAuthToken(this.projectPath) : undefined, ...secretEnvValues()],
|
|
925
|
+
streamConnected: () => this.eventStream.isConnected === true,
|
|
926
|
+
}));
|
|
927
|
+
}
|
|
928
|
+
return this.playtestRecorder;
|
|
929
|
+
}
|
|
930
|
+
/** Install a recorder (tests use a temporary root). */
|
|
931
|
+
usePlaytestRecorder(recorder) {
|
|
932
|
+
this.playtestRecorder = recorder;
|
|
933
|
+
this.eventStream.addListener?.call(this.eventStream, event => recorder.onEvent(event));
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* `unity_open_mcp_playtest_session` — local route. Bridge reads the session
|
|
937
|
+
* needs (company / product, the Game view state, a player-state snapshot or
|
|
938
|
+
* diff) go through the normal route as the same owner, and only when the
|
|
939
|
+
* bridge answers /ping; otherwise the recorder falls back to disk.
|
|
940
|
+
*/
|
|
941
|
+
async routePlaytestSession(args, live, identity) {
|
|
942
|
+
const action = String(args.action ?? "");
|
|
943
|
+
const allowed = PLAYTEST_SESSION_ACTION_KEYS[action];
|
|
944
|
+
if (!allowed)
|
|
945
|
+
return localError("invalid_arguments", `Unknown action '${action}'.`);
|
|
946
|
+
const extra = Object.keys(args).filter(key => key !== "action" && !allowed.includes(key));
|
|
947
|
+
if (extra.length)
|
|
948
|
+
return localError("invalid_arguments", `Keys not used by ${action}: ${extra.join(", ")}.`);
|
|
949
|
+
const recorder = this.playtest;
|
|
950
|
+
const owner = this.ownerOf(identity);
|
|
951
|
+
let available;
|
|
952
|
+
const call = async (tool, toolArgs) => {
|
|
953
|
+
available ??= await live.isLiveAvailable(1_500);
|
|
954
|
+
if (!available)
|
|
955
|
+
return localError("bridge_unavailable", "The Unity bridge is not reachable.");
|
|
956
|
+
const definition = ALL_TOOLS.find(t => t.name === tool);
|
|
957
|
+
return this.routeDispatch(live, tool, definition ? withSchemaDefaults(definition, toolArgs) : toolArgs, identity);
|
|
958
|
+
};
|
|
959
|
+
const sessionArg = typeof args.session === "string" ? args.session : undefined;
|
|
960
|
+
try {
|
|
961
|
+
if (action !== "status" || recorder.isRecording(owner) === false)
|
|
962
|
+
await recorder.prepare(call);
|
|
963
|
+
if (action === "start" || action === "resume" || action === "end")
|
|
964
|
+
this.eventStream.ensureSubscription?.call(this.eventStream);
|
|
965
|
+
let body;
|
|
966
|
+
switch (action) {
|
|
967
|
+
case "start":
|
|
968
|
+
body = await recorder.start(owner, args, call);
|
|
969
|
+
break;
|
|
970
|
+
case "mark":
|
|
971
|
+
body = recorder.mark(owner, args, sessionArg);
|
|
972
|
+
break;
|
|
973
|
+
case "status":
|
|
974
|
+
body = recorder.status(owner);
|
|
975
|
+
break;
|
|
976
|
+
case "resume":
|
|
977
|
+
body = recorder.resume(owner, sessionArg);
|
|
978
|
+
break;
|
|
979
|
+
case "end":
|
|
980
|
+
body = await recorder.end(owner, args, call);
|
|
981
|
+
break;
|
|
982
|
+
default: body = recorder.list(owner, typeof args.limit === "number" ? args.limit : 10);
|
|
983
|
+
}
|
|
984
|
+
return sourceResult(body, "local");
|
|
985
|
+
}
|
|
986
|
+
catch (error) {
|
|
987
|
+
if (error instanceof PlaytestError)
|
|
988
|
+
return sourceResult({ error: { code: error.code, message: error.message }, ...error.detail }, "local", true);
|
|
989
|
+
return localError("internal_error", error instanceof Error ? error.message : String(error));
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
async routeDispatch(live, toolName, args, identity) {
|
|
760
993
|
// Every entry point funnels through here, so an explicit project_path is
|
|
761
994
|
// retargeted before validation and wiring wherever the call came from.
|
|
762
995
|
if ((toolName === "unity_open_mcp_capabilities" || toolName === "unity_open_mcp_bridge_status") && args.project_path !== undefined)
|
|
@@ -3166,15 +3399,24 @@ function pageSceneNodes(body, pageSize, cursor) {
|
|
|
3166
3399
|
*/
|
|
3167
3400
|
function flattenSceneBody(body) {
|
|
3168
3401
|
const out = [];
|
|
3169
|
-
|
|
3402
|
+
// Rows are flat (no nested `children`), so each carries its hierarchy
|
|
3403
|
+
// `path` — otherwise same-named nodes on a page cannot be told apart or
|
|
3404
|
+
// addressed once a depth limit or truncation desyncs `childCount`.
|
|
3405
|
+
const walk = (node, parentPath) => {
|
|
3170
3406
|
if (!node || typeof node !== "object")
|
|
3171
3407
|
return;
|
|
3172
3408
|
const obj = node;
|
|
3173
|
-
|
|
3174
|
-
const
|
|
3409
|
+
const { children, ...flatNode } = obj;
|
|
3410
|
+
const name = typeof obj.name === "string" ? obj.name : undefined;
|
|
3411
|
+
const path = typeof obj.path === "string"
|
|
3412
|
+
? obj.path
|
|
3413
|
+
: name === undefined ? undefined : parentPath === undefined ? name : `${parentPath}/${name}`;
|
|
3414
|
+
if (path !== undefined && flatNode.path === undefined)
|
|
3415
|
+
flatNode.path = path;
|
|
3416
|
+
out.push(flatNode);
|
|
3175
3417
|
if (Array.isArray(children)) {
|
|
3176
3418
|
for (const child of children)
|
|
3177
|
-
walk(child);
|
|
3419
|
+
walk(child, path);
|
|
3178
3420
|
}
|
|
3179
3421
|
};
|
|
3180
3422
|
// M3 — the bridge nests everything one level down under `body.scene`
|
|
@@ -3191,7 +3433,7 @@ function flattenSceneBody(body) {
|
|
|
3191
3433
|
const rootField = Array.isArray(scene.roots) ? scene.roots : scene.rootGameObjects;
|
|
3192
3434
|
if (Array.isArray(rootField)) {
|
|
3193
3435
|
for (const root of rootField)
|
|
3194
|
-
walk(root);
|
|
3436
|
+
walk(root, undefined);
|
|
3195
3437
|
}
|
|
3196
3438
|
return out;
|
|
3197
3439
|
}
|