unity-open-mcp 1.2.3 → 1.3.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 +33 -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 +71 -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 +88 -0
- package/dist/cli/setup-command.js +444 -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 +55 -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 +86 -0
- package/dist/project-path.js +159 -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 +28 -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/portable-config.d.ts +60 -0
- package/dist/setup/portable-config.js +249 -0
- package/dist/setup/portable-config.js.map +1 -0
- package/dist/skill/SKILL.md +68 -0
- 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/tool-router.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
import { testRunOperation, projectCommandOperation } from "./jobs/adapters.js";
|
|
2
|
+
import { JobManager, JobManagerError } from "./jobs/job-manager.js";
|
|
3
|
+
import { handleJobs } from "./jobs/handler.js";
|
|
4
|
+
import { PROCESS_AGENT_ID, PROCESS_IDENTITY } from "./agent-identity.js";
|
|
5
|
+
import { buildEditorSearch } from "./editor-search.js";
|
|
6
|
+
import { withSchemaDefaults } from "./schema-defaults.js";
|
|
7
|
+
import { validateSchema, wireArguments } from "./tool-contract.js";
|
|
8
|
+
import { discoverTools, compactDiscoverySchema } from "./capabilities/discovery.js";
|
|
9
|
+
import { PingCache } from "./ping-cache.js";
|
|
10
|
+
import { resolveProjectPath, validateUnityProjectRoot } from "./project-path.js";
|
|
11
|
+
import { resolvePort, resolveAuthToken } from "./instance-discovery.js";
|
|
12
|
+
import { skipsReloadProbe } from "./local-tools.js";
|
|
13
|
+
import { confirmBridgeReleaseTag, resolveLatestVersion } from "./release-lookup.js";
|
|
14
|
+
import { LiveClient } from "./live-client.js";
|
|
1
15
|
import { AssetModelCache, isCompressible, routeCompressible } from "./compressible-router.js";
|
|
2
16
|
import { listAssetsOffline, findReferencesOffline, dependenciesOffline } from "./offline.js";
|
|
3
17
|
import { resolveEditorLogPath, readLogTail, DEFAULT_LOG_TAIL_BYTES, detectStaleLog, detectStaleAssembly, detectErrorsPredatingEdits, parseLogAuthorship, compareLogAuthorship } from "./unity-log.js";
|
|
@@ -53,7 +67,8 @@ const defaultHubBackend = {
|
|
|
53
67
|
getInstallPath: () => getInstallPath(),
|
|
54
68
|
setInstallPath: (path) => setInstallPath(path),
|
|
55
69
|
};
|
|
56
|
-
import { BATCH_TOOL_NAMES, ALWAYS_BATCH_TOOLS } from "./batch-spawn.js";
|
|
70
|
+
import { BatchSpawn, BATCH_TOOL_NAMES, ALWAYS_BATCH_TOOLS, activeHeadlessRun } from "./batch-spawn.js";
|
|
71
|
+
import { filterVisibleTools } from "./tool-session-state.js";
|
|
57
72
|
import { readProfileAndDetail, applyPaging, attachPagination, foldVerifyResult, parseResultBody, withResultBody, isOutputProfile, } from "./output-profile.js";
|
|
58
73
|
/**
|
|
59
74
|
* Single source of truth for the `_source` tag. Every route-tagging site goes
|
|
@@ -186,6 +201,17 @@ function transformLiveResult(result, source, meta, transform) {
|
|
|
186
201
|
function localError(code, message) {
|
|
187
202
|
return sourceResult({ error: { code, message } }, "local", true);
|
|
188
203
|
}
|
|
204
|
+
const defaultReleaseLookup = {
|
|
205
|
+
latest: () => resolveLatestVersion(globalThis.fetch.bind(globalThis)),
|
|
206
|
+
confirmBridgeTag: (version) => confirmBridgeReleaseTag(version, globalThis.fetch.bind(globalThis)),
|
|
207
|
+
};
|
|
208
|
+
let releaseLookup = defaultReleaseLookup;
|
|
209
|
+
/** Test seam for the upgrade route's network lookups. Returns a restore function. */
|
|
210
|
+
export function setReleaseLookupForTest(lookup) {
|
|
211
|
+
const previous = releaseLookup;
|
|
212
|
+
releaseLookup = lookup ?? defaultReleaseLookup;
|
|
213
|
+
return () => { releaseLookup = previous; };
|
|
214
|
+
}
|
|
189
215
|
// M26 Plan 2 — gate-consistent envelope shape for the mutating Hub control
|
|
190
216
|
// tools (install_editor / install_modules / set_install_path). These are
|
|
191
217
|
// system-level ops, not project-asset mutations, so paths_hint is N/A and the
|
|
@@ -341,7 +367,7 @@ function groupLiveRefsByKind(paths) {
|
|
|
341
367
|
return counts;
|
|
342
368
|
}
|
|
343
369
|
// Operator-facing "what to do next" hint for each coarse status. Kept short
|
|
344
|
-
// and action-oriented so
|
|
370
|
+
// and action-oriented so an operator UI can render it inline.
|
|
345
371
|
function bridgeStatusNextStep(status, wedge) {
|
|
346
372
|
if (status === "wedged") {
|
|
347
373
|
return wedge?.reason === "main_thread_wedged"
|
|
@@ -392,6 +418,57 @@ function bridgeStatusNextStep(status, wedge) {
|
|
|
392
418
|
"Bridge) and confirm it is started.";
|
|
393
419
|
}
|
|
394
420
|
}
|
|
421
|
+
// This server's own headless batch run holds the project. The Unity process
|
|
422
|
+
// scan deliberately skips that child (it is not an Editor the user owns), so
|
|
423
|
+
// Editor-ownership surfaces would otherwise report "no Unity running" and send
|
|
424
|
+
// the agent to relaunch via the Hub, which would collide with the run.
|
|
425
|
+
function describeHeadlessRun(run, now = Date.now()) {
|
|
426
|
+
return {
|
|
427
|
+
tool: run.tool,
|
|
428
|
+
operation: run.operation,
|
|
429
|
+
pid: run.pid,
|
|
430
|
+
startedAt: new Date(run.startedAt).toISOString(),
|
|
431
|
+
elapsedMs: Math.max(0, now - run.startedAt),
|
|
432
|
+
timeoutMs: run.timeoutMs,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function headlessRunSentence(run, now = Date.now()) {
|
|
436
|
+
const elapsedS = Math.round(Math.max(0, now - run.startedAt) / 1000);
|
|
437
|
+
const timeoutS = Math.round(run.timeoutMs / 1000);
|
|
438
|
+
return (`The only Unity process for this project is this MCP server's own headless ` +
|
|
439
|
+
`run for ${run.tool}` +
|
|
440
|
+
(run.pid !== null ? ` (pid ${run.pid})` : "") +
|
|
441
|
+
`, started ${elapsedS}s ago with a ${timeoutS}s timeout. It is not an ` +
|
|
442
|
+
`interactive Editor, and it exits on its own when the operation finishes ` +
|
|
443
|
+
`or the timeout elapses.`);
|
|
444
|
+
}
|
|
445
|
+
const HEADLESS_RUN_NEXT_STEPS = [
|
|
446
|
+
"Wait for the pending headless call to return; it releases the project " +
|
|
447
|
+
"when it finishes or when its timeout elapses.",
|
|
448
|
+
"Do not relaunch Unity for this project via the Hub meanwhile: Unity " +
|
|
449
|
+
"allows one Editor per project, so a second instance collides with the " +
|
|
450
|
+
"headless run's project lock.",
|
|
451
|
+
];
|
|
452
|
+
function headlessRunErrorResult(action, run, projectPath) {
|
|
453
|
+
const now = Date.now();
|
|
454
|
+
const tail = action === "restart_editor"
|
|
455
|
+
? " restart_editor never targets the server's own headless run, so there is no Editor to restart."
|
|
456
|
+
: " resource_pressure watches the interactive Editor's descriptor leak, which a short-lived headless run does not accumulate.";
|
|
457
|
+
return sourceResult({
|
|
458
|
+
error: {
|
|
459
|
+
code: "headless_run_in_progress",
|
|
460
|
+
message: headlessRunSentence(run, now) + tail,
|
|
461
|
+
projectPath,
|
|
462
|
+
...describeHeadlessRun(run, now),
|
|
463
|
+
},
|
|
464
|
+
agentNextSteps: [
|
|
465
|
+
...HEADLESS_RUN_NEXT_STEPS,
|
|
466
|
+
action === "restart_editor"
|
|
467
|
+
? "Call restart_editor again only if an interactive Editor for this project hangs after the run completes."
|
|
468
|
+
: "Once the run completes and an interactive Editor is open, call resource_pressure again; pass an explicit pid to sample the headless process anyway.",
|
|
469
|
+
],
|
|
470
|
+
}, "local", true);
|
|
471
|
+
}
|
|
395
472
|
/**
|
|
396
473
|
* specs/feedback.md 2026-08-24 — wire-contract staleness block.
|
|
397
474
|
*
|
|
@@ -471,6 +548,60 @@ export class ToolRouter {
|
|
|
471
548
|
onToolListChanged;
|
|
472
549
|
hubBackend;
|
|
473
550
|
globalLogPathOverride;
|
|
551
|
+
jobManager;
|
|
552
|
+
get jobs() { return this.jobManager ??= new JobManager(); }
|
|
553
|
+
closeJobs() { this.jobManager?.close(); }
|
|
554
|
+
async routeJobs(args, identity = PROCESS_IDENTITY) {
|
|
555
|
+
const definition = ALL_TOOLS.find(t => t.name === "unity_open_mcp_jobs");
|
|
556
|
+
const errors = validateSchema(args, definition.inputSchema);
|
|
557
|
+
if (errors.length)
|
|
558
|
+
return localError("invalid_arguments", errors.join("; "));
|
|
559
|
+
const owner = { project: this.projectPath || "default", agent: identity.agent,
|
|
560
|
+
...(identity.port === undefined ? {} : { port: identity.port }) };
|
|
561
|
+
try {
|
|
562
|
+
// A port override names a bridge, not a project: bind the job client to
|
|
563
|
+
// the Editor whose lock claims that port (its bearer token and its lock
|
|
564
|
+
// diagnostics), exactly like a direct override call. resolveAuthToken
|
|
565
|
+
// yields no token for an explicit port, and the default project's lock
|
|
566
|
+
// would describe the wrong Editor.
|
|
567
|
+
const clientFor = (jobOwner) => jobOwner.port === undefined
|
|
568
|
+
? jobOwner.agent === PROCESS_AGENT_ID ? this.live : this.live.forAgent(jobOwner.agent)
|
|
569
|
+
: LiveClient.forPortOverride(jobOwner.port, this.projectPath, jobOwner.agent);
|
|
570
|
+
if (args.action === "start" && typeof args.tool_or_command === "string") {
|
|
571
|
+
const name = args.tool_or_command, key = args.idempotency_key;
|
|
572
|
+
if (name === "unity_senses_run_tests") {
|
|
573
|
+
if (!this.jobs.hasOperation(name))
|
|
574
|
+
this.jobs.register(name, testRunOperation(clientFor));
|
|
575
|
+
}
|
|
576
|
+
else if (name.startsWith("project.") && !(typeof key === "string" && this.jobs.hasKey(owner, key))) {
|
|
577
|
+
// A retry whose key already names a job gets that job (or
|
|
578
|
+
// idempotency_conflict) from start() without the catalog, which may
|
|
579
|
+
// be unreachable mid-reload or declare a contract changed since.
|
|
580
|
+
// Re-read the catalog on every new start: a domain reload can change the
|
|
581
|
+
// command's async/cancellable declaration, and an adapter frozen at
|
|
582
|
+
// first registration would refuse the job for the rest of the session.
|
|
583
|
+
const catalog = await clientFor(owner).projectCommands({ action: "describe", id: name });
|
|
584
|
+
if (catalog.error) {
|
|
585
|
+
// A transport/catalog failure (bridge down, reloading, too old) is
|
|
586
|
+
// not "this command is unsupported": surface the catalog's own
|
|
587
|
+
// code so the agent retries instead of abandoning the plan.
|
|
588
|
+
const failure = catalog.error;
|
|
589
|
+
throw new JobManagerError(failure.code ?? "catalog_unavailable", failure.message ?? "Project command catalog is unavailable.");
|
|
590
|
+
}
|
|
591
|
+
const command = catalog.command;
|
|
592
|
+
if (!command?.available || !command.async)
|
|
593
|
+
throw new JobManagerError("job_operation_unsupported", "Target must be an available async project command.");
|
|
594
|
+
const cancellable = command.cancellable === true;
|
|
595
|
+
if (this.jobs.operation(name)?.cancellable !== cancellable)
|
|
596
|
+
this.jobs.register(name, projectCommandOperation(name, cancellable, clientFor), true);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return sourceResult(await handleJobs(this.jobs, owner, args), "local");
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
return localError(error instanceof JobManagerError ? error.code : "invalid_arguments", error instanceof Error ? error.message : String(error));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
474
605
|
modelCache = new AssetModelCache();
|
|
475
606
|
/**
|
|
476
607
|
* M28-refactoring Plan 3 (T3.2) — named-tool dispatch map. Replaces the
|
|
@@ -487,15 +618,24 @@ export class ToolRouter {
|
|
|
487
618
|
if (this.routeHandlers)
|
|
488
619
|
return this.routeHandlers;
|
|
489
620
|
const handlers = new Map([
|
|
490
|
-
//
|
|
621
|
+
// Disk reads and live compiler evidence with an offline log fallback.
|
|
491
622
|
["unity_open_mcp_list_assets", (_l, a) => this.routeListAssets(a)],
|
|
492
|
-
["unity_open_mcp_read_compile_errors", (
|
|
623
|
+
["unity_open_mcp_read_compile_errors", (l, a) => this.routeReadCompileErrors(a, l)],
|
|
493
624
|
// Local-routed tools (server-only; no live/batch hop).
|
|
494
625
|
["unity_senses_pull_events", (l, a) => this.routePullEvents(a, l)],
|
|
495
626
|
["unity_open_mcp_capabilities", (l, a) => this.routeCapabilities(a, l)],
|
|
496
627
|
["unity_open_mcp_list_rules", (_l, a) => this.routeListRules(a)],
|
|
497
628
|
["unity_open_mcp_generate_skill", (_l, a) => this.routeGenerateSkill(a)],
|
|
498
|
-
["unity_open_mcp_manage_tools", (l, a) => this.routeManageTools(a, l)],
|
|
629
|
+
["unity_open_mcp_manage_tools", (l, a, id) => this.routeManageTools(a, l, id)],
|
|
630
|
+
["unity_open_mcp_project_commands", (l, a) => this.routeProjectCommands(a, l)],
|
|
631
|
+
// Live tool whose release lookups (npm latest, GitHub tag) run HERE so
|
|
632
|
+
// the Editor main thread never blocks on the network.
|
|
633
|
+
["unity_open_mcp_upgrade", (l, a) => this.routeUpgrade(a, l)],
|
|
634
|
+
// The owner is the request identity (agent id + per-request port
|
|
635
|
+
// override), never the live client's: the default client always sends
|
|
636
|
+
// the process agent id, whatever _meta.agentId the call carried. Direct,
|
|
637
|
+
// manage_tools(invoke) and port-override calls therefore share one owner.
|
|
638
|
+
["unity_open_mcp_jobs", (_l, a, id) => this.routeJobs(a, id)],
|
|
499
639
|
["unity_open_mcp_bridge_status", (l, a) => this.routeBridgeStatus(a, l)],
|
|
500
640
|
// M31 Plan 3 — Editor fd-exhaustion operator surfaces. restart_editor
|
|
501
641
|
// is the reactive kill half (acts AFTER the Editor is hung);
|
|
@@ -545,28 +685,132 @@ export class ToolRouter {
|
|
|
545
685
|
this.hubBackend = hubBackend;
|
|
546
686
|
this.globalLogPathOverride = globalLogPathOverride;
|
|
547
687
|
}
|
|
548
|
-
async route(toolName, args) {
|
|
549
|
-
return this.routeCore(this.live, toolName, args);
|
|
688
|
+
async route(toolName, args, identity = PROCESS_IDENTITY) {
|
|
689
|
+
return this.routeCore(this.live, toolName, args, identity);
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* `capabilities` / `bridge_status` with an explicit project_path, from any
|
|
693
|
+
* entry point (direct, manage_tools invoke, port override). The probe router
|
|
694
|
+
* gets the arguments without project_path, so it dispatches normally. A
|
|
695
|
+
* per-request port override is refused rather than dropped or mixed in:
|
|
696
|
+
* project_path brings its own port/auth discovery, and pinging the override
|
|
697
|
+
* port would present another bridge's answer as the probed project's.
|
|
698
|
+
*/
|
|
699
|
+
async routeProjectProbe(toolName, args, identity) {
|
|
700
|
+
if (typeof args.project_path !== "string" || !args.project_path.trim())
|
|
701
|
+
return localError("invalid_project_path", "project_path must name a Unity project root.");
|
|
702
|
+
if (identity.port !== undefined)
|
|
703
|
+
return localError("invalid_arguments", "project_path discovers that project's own bridge port; drop the _meta.port (or port) override for this call.");
|
|
704
|
+
const selected = resolveProjectPath({ flagPath: args.project_path }).absolute;
|
|
705
|
+
if (!validateUnityProjectRoot(selected).valid)
|
|
706
|
+
return localError("invalid_project_path", "project_path must contain Assets, Packages, and ProjectSettings.");
|
|
707
|
+
const { project_path: _selected, ...rest } = args;
|
|
708
|
+
return this.projectProbeRouter(selected).route(toolName, rest, identity);
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Router for a read-only probe of another project: its own port/auth
|
|
712
|
+
* discovery and a throwaway copy of this session. Discovery still reconciles
|
|
713
|
+
* auto-activation against the probed project's inventory, so the probe
|
|
714
|
+
* reports that project's view, but only the copy changes — this session's
|
|
715
|
+
* active groups and tools/list stay put, and no list_changed is wired. The
|
|
716
|
+
* probe tools are local-only and never spawn Unity; the probe's own batch
|
|
717
|
+
* router only answers headless availability for that project's editor.
|
|
718
|
+
*/
|
|
719
|
+
projectProbeRouter(projectPath) {
|
|
720
|
+
const live = new LiveClient(resolvePort(projectPath), new PingCache(), resolveAuthToken(projectPath), projectPath);
|
|
721
|
+
return new ToolRouter(live, this.projectProbeBatch(projectPath), projectPath, this.eventStream, this.sessionState.clone());
|
|
722
|
+
}
|
|
723
|
+
// Batch routers of probed projects, keyed by project path and bounded to
|
|
724
|
+
// the most recently probed few. Constructing a BatchSpawn walks every Hub
|
|
725
|
+
// install root synchronously; a polled probe must not pay that on every
|
|
726
|
+
// call, and the router keeps its own editor-resolution cache (revalidated
|
|
727
|
+
// when the project version or the env inputs change).
|
|
728
|
+
static PROBE_BATCH_CACHE_SIZE = 8;
|
|
729
|
+
probeBatchByProject = new Map();
|
|
730
|
+
projectProbeBatch(projectPath) {
|
|
731
|
+
const cached = this.probeBatchByProject.get(projectPath);
|
|
732
|
+
if (cached) {
|
|
733
|
+
// Re-insert so the map keeps insertion order as recency order.
|
|
734
|
+
this.probeBatchByProject.delete(projectPath);
|
|
735
|
+
this.probeBatchByProject.set(projectPath, cached);
|
|
736
|
+
return cached;
|
|
737
|
+
}
|
|
738
|
+
const batch = new BatchSpawn({ projectPath });
|
|
739
|
+
this.probeBatchByProject.set(projectPath, batch);
|
|
740
|
+
while (this.probeBatchByProject.size > ToolRouter.PROBE_BATCH_CACHE_SIZE) {
|
|
741
|
+
const oldest = this.probeBatchByProject.keys().next().value;
|
|
742
|
+
if (oldest === undefined)
|
|
743
|
+
break;
|
|
744
|
+
this.probeBatchByProject.delete(oldest);
|
|
745
|
+
}
|
|
746
|
+
return batch;
|
|
550
747
|
}
|
|
551
748
|
/**
|
|
552
749
|
* M23 Plan 3 — route with a per-call live-client override. Used when a
|
|
553
750
|
* per-request `_meta.port` override targets a different bridge instance than
|
|
554
751
|
* the default. The override client bypasses shared session state (it is a
|
|
555
752
|
* fresh LiveClient aimed at the override port); all other routing logic is
|
|
556
|
-
* identical to {@link route}.
|
|
557
|
-
*
|
|
753
|
+
* identical to {@link route}. `identity` carries the override port and the
|
|
754
|
+
* request's agent id.
|
|
558
755
|
*/
|
|
559
|
-
async routeOverride(toolName, args, overrideLive) {
|
|
560
|
-
return this.routeCore(overrideLive, toolName, args);
|
|
756
|
+
async routeOverride(toolName, args, overrideLive, identity) {
|
|
757
|
+
return this.routeCore(overrideLive, toolName, args, identity);
|
|
758
|
+
}
|
|
759
|
+
async routeCore(live, toolName, args, identity) {
|
|
760
|
+
// Every entry point funnels through here, so an explicit project_path is
|
|
761
|
+
// retargeted before validation and wiring wherever the call came from.
|
|
762
|
+
if ((toolName === "unity_open_mcp_capabilities" || toolName === "unity_open_mcp_bridge_status") && args.project_path !== undefined)
|
|
763
|
+
return this.routeProjectProbe(toolName, args, identity);
|
|
764
|
+
const definition = ALL_TOOLS.find(t => t.name === toolName);
|
|
765
|
+
if (definition) {
|
|
766
|
+
const errors = validateSchema(args, definition.inputSchema);
|
|
767
|
+
if (errors.length)
|
|
768
|
+
return sourceResult({ error: { code: "invalid_arguments", message: errors.join("; ") }, errors, _route: { route: "local" } }, "local", true);
|
|
769
|
+
const notes = [];
|
|
770
|
+
const wire = wireArguments(args, definition.inputSchema, notes);
|
|
771
|
+
if (toolName === "unity_open_mcp_batch_execute" && Array.isArray(wire.commands)) {
|
|
772
|
+
wire.commands = wire.commands.map(command => {
|
|
773
|
+
const nested = ALL_TOOLS.find(t => t.name === command.tool);
|
|
774
|
+
return nested
|
|
775
|
+
? { ...command, params: wireArguments(command.params ?? {}, nested.inputSchema, notes) }
|
|
776
|
+
: command;
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
// Recursive dispatch only after validation; alias notes are attached to the normal result.
|
|
780
|
+
return this.routeValidated(live, toolName, wire, notes, identity);
|
|
781
|
+
}
|
|
782
|
+
return this.routeValidated(live, toolName, args, [], identity);
|
|
783
|
+
}
|
|
784
|
+
async routeValidated(live, toolName, args, notes, identity) {
|
|
785
|
+
const result = await this.routeUnchecked(live, toolName, args, identity);
|
|
786
|
+
if (notes.length)
|
|
787
|
+
result.content.push({ type: "text", text: JSON.stringify({ deprecations: [...new Set(notes)] }) });
|
|
788
|
+
return result;
|
|
561
789
|
}
|
|
562
|
-
async
|
|
790
|
+
async routeUnchecked(live, toolName, args, identity) {
|
|
791
|
+
const localProbe = skipsReloadProbe(toolName);
|
|
792
|
+
const lock = !localProbe && this.projectPath ? readInstanceLock(this.projectPath) : null;
|
|
793
|
+
// The lock describes the default project's Editor. A per-request port
|
|
794
|
+
// override aimed elsewhere has no lock of its own (override clients never
|
|
795
|
+
// read locks) and its LiveClient handles that bridge's readiness, so the
|
|
796
|
+
// default Editor's reload must not refuse it. An override naming the
|
|
797
|
+
// default Editor's own port still targets the reloading Editor.
|
|
798
|
+
const targetsLockedEditor = identity.port === undefined || identity.port === lock?.port;
|
|
799
|
+
if (targetsLockedEditor && classifyInstance(lock) === "reloading") {
|
|
800
|
+
// One bounded re-probe. No headless fallback can acquire this Editor's project.
|
|
801
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
802
|
+
const available = await live.isLiveAvailable(750);
|
|
803
|
+
if (!available || classifyInstance(readInstanceLock(this.projectPath)) === "reloading") {
|
|
804
|
+
return sourceResult({ error: { code: "editor_reloading", message: "The live Editor is compiling or reloading. Retry after it settles." }, retryAfterMs: 1000 }, "local", true);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
563
807
|
// Named-tool dispatch (T3.2): the handler map covers every exact-name
|
|
564
808
|
// route. The per-tool policy rationale (offline / local / hub / offline-
|
|
565
809
|
// first / verify-fold / scene-paging) lives in each routeXxx method's
|
|
566
810
|
// header comment.
|
|
567
811
|
const handler = this.getRouteHandlers().get(toolName);
|
|
568
812
|
if (handler) {
|
|
569
|
-
return handler(live, args);
|
|
813
|
+
return handler(live, args, identity);
|
|
570
814
|
}
|
|
571
815
|
// Compact drill-down reads: offline-first for text-serialized assets, fall
|
|
572
816
|
// back to live bridge for binary formats. The compressible-router handles
|
|
@@ -713,7 +957,7 @@ export class ToolRouter {
|
|
|
713
957
|
return meta;
|
|
714
958
|
}
|
|
715
959
|
}
|
|
716
|
-
async routeReadCompileErrors(args) {
|
|
960
|
+
async routeReadCompileErrors(args, live) {
|
|
717
961
|
const tailBytes = typeof args.tail_bytes === "number" && args.tail_bytes >= 4096
|
|
718
962
|
? Math.min(Math.floor(args.tail_bytes), 1048576)
|
|
719
963
|
: DEFAULT_LOG_TAIL_BYTES;
|
|
@@ -731,6 +975,40 @@ export class ToolRouter {
|
|
|
731
975
|
const resolvedLog = resolveEditorLogPath(this.projectPath, undefined, livePid, this.globalLogPathOverride);
|
|
732
976
|
const logPath = resolvedLog.path;
|
|
733
977
|
const tail = readLogTail(logPath, tailBytes);
|
|
978
|
+
const compile = await live.readCompileState?.();
|
|
979
|
+
if (compile && compile.status !== "indeterminate") {
|
|
980
|
+
const historical = summarizeProjectHealth(tail.content);
|
|
981
|
+
const failed = compile.status === "compile_failed";
|
|
982
|
+
const pipelineErrors = failed && Array.isArray(compile.errors) ? compile.errors : [];
|
|
983
|
+
// Unity can report a failed compile the bridge never observed (it ran
|
|
984
|
+
// before the bridge domain loaded, e.g. the Editor-startup compile), so
|
|
985
|
+
// the pipeline holds no diagnostics for it. Editor.log does: promote its
|
|
986
|
+
// errors instead of answering "failed" with nothing to fix.
|
|
987
|
+
const fromLog = failed && pipelineErrors.length === 0;
|
|
988
|
+
const currentErrors = fromLog ? historical.compilerErrors : pipelineErrors;
|
|
989
|
+
return sourceResult({
|
|
990
|
+
...compile, errorCount: currentErrors.length, errors: currentErrors,
|
|
991
|
+
errorSource: fromLog ? "Editor.log" : "CompilationPipeline",
|
|
992
|
+
unhealthy: failed,
|
|
993
|
+
staleAssembly: compile.status === "assembly_stale",
|
|
994
|
+
headline: compile.status === "no_errors_found" ? "Current compile is clean; source content matches the completed generation."
|
|
995
|
+
: compile.status === "currently_compiling" ? "Compilation is active; wait for completion."
|
|
996
|
+
: compile.status === "assembly_stale" ? "Source content changed after the last compile; recompile before trusting assemblies."
|
|
997
|
+
: !fromLog ? "Current compilation failed. Fix the compiler diagnostics and recompile."
|
|
998
|
+
: currentErrors.length > 0
|
|
999
|
+
? "Current compilation failed in a compile the bridge did not observe (e.g. at Editor " +
|
|
1000
|
+
`startup); errors are the ${currentErrors.length} compiler diagnostic(s) from Editor.log. ` +
|
|
1001
|
+
"Fix them and recompile."
|
|
1002
|
+
: "Current compilation failed in a compile the bridge did not observe (e.g. at Editor " +
|
|
1003
|
+
"startup), and the Editor.log tail holds no compiler diagnostics. Raise tail_bytes or " +
|
|
1004
|
+
`recompile (${toolHintReference("unity_open_mcp_recompile_scripts")}) to capture them.`,
|
|
1005
|
+
compileSource: "CompilationPipeline", logPath, logSource: resolvedLog.reason,
|
|
1006
|
+
logAuthorship: parseLogAuthorship(tail.content),
|
|
1007
|
+
historicalLogErrors: fromLog ? [] : historical.compilerErrors,
|
|
1008
|
+
historicalLogIssues: historical.issues,
|
|
1009
|
+
logReadError: tail.error ?? null,
|
|
1010
|
+
}, "live");
|
|
1011
|
+
}
|
|
734
1012
|
if (tail.error) {
|
|
735
1013
|
return {
|
|
736
1014
|
content: [
|
|
@@ -1122,6 +1400,13 @@ export class ToolRouter {
|
|
|
1122
1400
|
};
|
|
1123
1401
|
}
|
|
1124
1402
|
async routeCapabilities(args, live) {
|
|
1403
|
+
if (["tool_name", "query", "group", "tag", "available", "active", "route", "mutating"].some(k => args[k] !== undefined)) {
|
|
1404
|
+
if (args.kind !== undefined && args.kind !== "tools")
|
|
1405
|
+
return localError("invalid_arguments", "Tool discovery filters require kind: tools (or omit kind).");
|
|
1406
|
+
const inventory = await this.fetchInventoryAndReconcile(live);
|
|
1407
|
+
const found = discoverTools(ALL_TOOLS, BATCH_TOOL_NAMES, this.sessionState, inventory?.tools, args, this.batch.headlessAvailable());
|
|
1408
|
+
return sourceResult(found, "local", !!found.error);
|
|
1409
|
+
}
|
|
1125
1410
|
const kind = args.kind === "tools" || args.kind === "rules" || args.kind === "fixes"
|
|
1126
1411
|
? args.kind
|
|
1127
1412
|
: undefined;
|
|
@@ -1229,9 +1514,49 @@ export class ToolRouter {
|
|
|
1229
1514
|
// when reachable; when the bridge is offline it reports `available: null`
|
|
1230
1515
|
// (unknown) and the agent should fall back to `unity_open_mcp_capabilities`
|
|
1231
1516
|
// for the authoritative compiled-state report.
|
|
1232
|
-
async routeManageTools(args, live) {
|
|
1517
|
+
async routeManageTools(args, live, identity) {
|
|
1233
1518
|
const action = typeof args.action === "string" ? args.action : "";
|
|
1234
1519
|
const group = typeof args.group === "string" ? args.group.trim() : "";
|
|
1520
|
+
const actionKeys = {
|
|
1521
|
+
list_groups: [], reset: [], activate: ["group"], deactivate: ["group"],
|
|
1522
|
+
suggest: ["intent", "tags"], activate_for: ["intent", "tags"],
|
|
1523
|
+
invoke: ["tool_name", "arguments"], editor_search: ["search_text", "asset_type", "open_ui"],
|
|
1524
|
+
};
|
|
1525
|
+
const ignored = Object.keys(args).filter(k => k !== "action" && actionKeys[action] && !actionKeys[action].includes(k));
|
|
1526
|
+
if (ignored.length)
|
|
1527
|
+
return this.manageToolsError("invalid_arguments", `Keys not used by ${action}: ${ignored.join(", ")}`);
|
|
1528
|
+
if (action === "editor_search") {
|
|
1529
|
+
const query = buildEditorSearch(String(args.search_text ?? ""), args.asset_type);
|
|
1530
|
+
if (args.open_ui !== true)
|
|
1531
|
+
return sourceResult({ query, opened: false, purpose: "Human collaboration; use search_assets/find_references for structured results." }, "local");
|
|
1532
|
+
const result = await this.routeCore(live, "unity_open_mcp_execute_csharp", {
|
|
1533
|
+
code: `UnityEditor.Search.SearchService.ShowWindow(UnityEditor.Search.SearchService.CreateContext(${JSON.stringify(query)})); return ${JSON.stringify(query)};`,
|
|
1534
|
+
read_only: true,
|
|
1535
|
+
}, identity);
|
|
1536
|
+
// The Search window opens through a read_only snippet, which a running
|
|
1537
|
+
// project job refuses (the bridge cannot verify a snippet writes
|
|
1538
|
+
// nothing). The handoff is optional UI, so report the query instead of
|
|
1539
|
+
// surfacing the refusal as a failed search.
|
|
1540
|
+
const body = result.isError ? parseResultBody(result) : null;
|
|
1541
|
+
const mutation = body?.mutation;
|
|
1542
|
+
const refusal = mutation?.error ?? body?.error;
|
|
1543
|
+
if (refusal?.code === "job_busy") {
|
|
1544
|
+
return sourceResult({
|
|
1545
|
+
query, opened: false, reason: "job_busy",
|
|
1546
|
+
...(refusal.job ? { job: refusal.job } : {}),
|
|
1547
|
+
message: "Unity Search cannot be opened while an asynchronous project command job runs. " +
|
|
1548
|
+
"Paste the query into Unity Search manually, or retry open_ui once the job reaches a terminal state.",
|
|
1549
|
+
}, "local");
|
|
1550
|
+
}
|
|
1551
|
+
result.content.push({ type: "text", text: JSON.stringify({ query, opened: !result.isError }) });
|
|
1552
|
+
return result;
|
|
1553
|
+
}
|
|
1554
|
+
if (action === "invoke") {
|
|
1555
|
+
const name = typeof args.tool_name === "string" ? args.tool_name : "";
|
|
1556
|
+
if (name === "unity_open_mcp_manage_tools" || !filterVisibleTools(ALL_TOOLS, this.sessionState).some(t => t.name === name))
|
|
1557
|
+
return this.manageToolsError("tool_not_active", "Activate the target group first; recursive manage_tools invocation is forbidden.");
|
|
1558
|
+
return this.routeCore(live, name, withSchemaDefaults(ALL_TOOLS.find(t => t.name === name), (args.arguments ?? {})), identity);
|
|
1559
|
+
}
|
|
1235
1560
|
if (action === "list_groups") {
|
|
1236
1561
|
return this.manageToolsListGroups(live);
|
|
1237
1562
|
}
|
|
@@ -1299,7 +1624,94 @@ export class ToolRouter {
|
|
|
1299
1624
|
});
|
|
1300
1625
|
}
|
|
1301
1626
|
return this.manageToolsError("unknown_action", `Unknown action '${action}'. Valid actions: list_groups, activate, ` +
|
|
1302
|
-
`deactivate, reset, suggest, activate_for.`);
|
|
1627
|
+
`deactivate, reset, suggest, activate_for, invoke, editor_search.`);
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* `unity_open_mcp_upgrade` is a live tool, but its network lookups run HERE
|
|
1631
|
+
* rather than inside the bridge: the bridge tool executes on the Editor main
|
|
1632
|
+
* thread, where a 10 s registry timeout would freeze the Editor and every
|
|
1633
|
+
* queued bridge request. The bridge only ever receives a resolved
|
|
1634
|
+
* `target_version`, and an apply that will re-pin through UPM is forwarded
|
|
1635
|
+
* only after the `bridge-v<version>` release tag was confirmed to exist —
|
|
1636
|
+
* the same preview → confirm → apply order the bridge window follows.
|
|
1637
|
+
*/
|
|
1638
|
+
async routeUpgrade(args, live) {
|
|
1639
|
+
const forwarded = { ...args };
|
|
1640
|
+
if (forwarded.target_version === undefined) {
|
|
1641
|
+
try {
|
|
1642
|
+
forwarded.target_version = (await releaseLookup.latest()).version;
|
|
1643
|
+
}
|
|
1644
|
+
catch (error) {
|
|
1645
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1646
|
+
return injectRouteMeta(localError("latest_version_failed", `Could not resolve the latest published version: ${detail}. Pass target_version explicitly.`), { route: "local" });
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
if (forwarded.dry_run === false && forwarded.update_upm !== false) {
|
|
1650
|
+
// Preview first: only a plan that really re-pins through UPM needs the
|
|
1651
|
+
// GitHub tag check (an embedded or file: bridge install skips UPM, so it
|
|
1652
|
+
// must also work offline).
|
|
1653
|
+
const preview = await live.route("unity_open_mcp_upgrade", { ...forwarded, dry_run: true });
|
|
1654
|
+
const previewBody = parseResultBody(preview);
|
|
1655
|
+
const mutation = previewBody?.mutation;
|
|
1656
|
+
const output = (mutation && typeof mutation === "object" ? mutation.output : previewBody);
|
|
1657
|
+
if (preview.isError || (output && typeof output === "object" && output.error !== undefined)) {
|
|
1658
|
+
return injectRouteMeta(preview, { route: "live" });
|
|
1659
|
+
}
|
|
1660
|
+
if (output && typeof output === "object" && output.upmEnabled === true) {
|
|
1661
|
+
const version = String(forwarded.target_version);
|
|
1662
|
+
const tag = await releaseLookup.confirmBridgeTag(version);
|
|
1663
|
+
if (!tag.confirmed) {
|
|
1664
|
+
return injectRouteMeta(localError("release_tag_unavailable", tag.error ?? `Release tag bridge-v${version} was not found.`), { route: "local" });
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
return injectRouteMeta(await live.route("unity_open_mcp_upgrade", forwarded), { route: "live" });
|
|
1669
|
+
}
|
|
1670
|
+
async routeProjectCommands(args, live) {
|
|
1671
|
+
if (args.action === "invoke") {
|
|
1672
|
+
const allowed = ["action", "command_id", "args", "schema_version", "paths_hint", "gate", "ignore_scene_dirty", "confirm_bypass", "timeout_ms"];
|
|
1673
|
+
let identity = { id: args.command_id };
|
|
1674
|
+
const fail = (code, message) => injectRouteMeta(sourceResult({ projectCommand: identity, error: { code, message } }, "local", true), { route: "local" });
|
|
1675
|
+
if (!args.command_id || Object.keys(args).some(k => !allowed.includes(k)))
|
|
1676
|
+
return fail("invalid_arguments", "invoke requires command_id and accepts command args plus transport safety options only.");
|
|
1677
|
+
const catalog = await live.projectCommands({ action: "describe", id: args.command_id });
|
|
1678
|
+
if (catalog.error)
|
|
1679
|
+
return injectRouteMeta(sourceResult(catalog, "live", true), { route: "live" });
|
|
1680
|
+
const command = catalog.command;
|
|
1681
|
+
if (!command?.available || !command.invocationSupported || !command.inputSchema || !command.schemaVersion)
|
|
1682
|
+
return fail("command_unavailable", "Command is unavailable or this bridge does not support invocation.");
|
|
1683
|
+
identity = { id: command.id, schemaVersion: command.schemaVersion, declaringAssembly: command.declaringAssembly,
|
|
1684
|
+
declaringType: command.declaringType, lifecycle: command.lifecycle };
|
|
1685
|
+
if (args.schema_version !== undefined && args.schema_version !== command.schemaVersion)
|
|
1686
|
+
return fail("command_schema_changed", "Describe the command again before invoking its changed contract.");
|
|
1687
|
+
const errors = validateSchema(args.args ?? {}, command.inputSchema);
|
|
1688
|
+
if (errors.length)
|
|
1689
|
+
return fail("invalid_arguments", errors.join("; "));
|
|
1690
|
+
// Deprecated parameter aliases are published like built-in aliases.
|
|
1691
|
+
const notes = [];
|
|
1692
|
+
const wire = wireArguments(args.args ?? {}, command.inputSchema, notes);
|
|
1693
|
+
const result = await live.route("unity_open_mcp_project_commands", { ...args, args: wire, schema_version: command.schemaVersion }, command.lifecycle);
|
|
1694
|
+
// Reload/transport failures have no bridge envelope; retain the exact catalog identity.
|
|
1695
|
+
result.content = result.content.map(item => {
|
|
1696
|
+
if (item.type !== "text")
|
|
1697
|
+
return item;
|
|
1698
|
+
try {
|
|
1699
|
+
const body = JSON.parse(item.text);
|
|
1700
|
+
if (body && typeof body === "object" && !Array.isArray(body) && !body.projectCommand)
|
|
1701
|
+
return { ...item, text: JSON.stringify({ ...body, projectCommand: identity }) };
|
|
1702
|
+
}
|
|
1703
|
+
catch { /* Preserve non-JSON transport diagnostics. */ }
|
|
1704
|
+
return item;
|
|
1705
|
+
});
|
|
1706
|
+
if (notes.length)
|
|
1707
|
+
result.content.push({ type: "text", text: JSON.stringify({ deprecations: [...new Set(notes)] }) });
|
|
1708
|
+
return injectRouteMeta(result, { route: "live" });
|
|
1709
|
+
}
|
|
1710
|
+
const allowed = args.action === "describe" ? ["action", "id"] : ["action", "query", "tags", "group", "package", "offset", "limit"];
|
|
1711
|
+
if (Object.keys(args).some(k => !allowed.includes(k)) || (args.action === "describe" && !args.id))
|
|
1712
|
+
return sourceResult({ error: { code: "invalid_arguments", message: "describe requires an exact id only; list accepts filters and paging only." } }, "local", true);
|
|
1713
|
+
const body = await live.projectCommands(args);
|
|
1714
|
+
return injectRouteMeta(sourceResult(body, "live", !!body.error), { route: "live" });
|
|
1303
1715
|
}
|
|
1304
1716
|
async manageToolsListGroups(live) {
|
|
1305
1717
|
// M20 Plan 7 / T20.7.0 — fetch the compiled-state inventory once and
|
|
@@ -1388,6 +1800,7 @@ export class ToolRouter {
|
|
|
1388
1800
|
// `availableReason`. Emits `notifications/tools/list_changed` exactly once
|
|
1389
1801
|
// when the visible set changed (idempotent when already active).
|
|
1390
1802
|
async manageToolsActivateFor(rec, live) {
|
|
1803
|
+
const groupsBeforeInventory = new Set(this.sessionState.activeGroups());
|
|
1391
1804
|
const inventory = await this.fetchInventoryAndReconcile(live);
|
|
1392
1805
|
const compiledAvailability = this.computeCompiledAvailability(inventory, groupToTools());
|
|
1393
1806
|
const before = this.sessionState.activeGroups();
|
|
@@ -1422,6 +1835,7 @@ export class ToolRouter {
|
|
|
1422
1835
|
intent: rec,
|
|
1423
1836
|
groups,
|
|
1424
1837
|
activated,
|
|
1838
|
+
tools: compactDiscoverySchema(discoverTools(ALL_TOOLS, BATCH_TOOL_NAMES, this.sessionState, inventory?.tools, { groups: this.sessionState.activeGroups().filter(g => !groupsBeforeInventory.has(g)), profile: "full", page_size: 1000 }, this.batch.headlessAvailable()).tools),
|
|
1425
1839
|
skipped,
|
|
1426
1840
|
unavailable,
|
|
1427
1841
|
activeGroups: this.sessionState.activeGroups(),
|
|
@@ -1735,6 +2149,11 @@ export class ToolRouter {
|
|
|
1735
2149
|
else {
|
|
1736
2150
|
status = "stopped";
|
|
1737
2151
|
}
|
|
2152
|
+
// A "stopped" bridge while this server's own headless run holds the
|
|
2153
|
+
// project: the process scan skips that child, so without this hint the
|
|
2154
|
+
// generic nextStep would send the agent to launch Unity into the run's
|
|
2155
|
+
// project lock.
|
|
2156
|
+
const headlessRun = status === "stopped" ? activeHeadlessRun(this.projectPath) : null;
|
|
1738
2157
|
const body = {
|
|
1739
2158
|
status,
|
|
1740
2159
|
// Coarse ready flag for clients that want a single boolean: true only
|
|
@@ -1784,7 +2203,12 @@ export class ToolRouter {
|
|
|
1784
2203
|
// `stale: true` block is the signal to reinstall the Unity package
|
|
1785
2204
|
// rather than file a regression.
|
|
1786
2205
|
wireContract: wireContractBlock(pingReachable, pingBody?.wireContract),
|
|
1787
|
-
|
|
2206
|
+
...(headlessRun !== null ? { headlessRun: describeHeadlessRun(headlessRun) } : {}),
|
|
2207
|
+
nextStep: headlessRun !== null
|
|
2208
|
+
? headlessRunSentence(headlessRun) +
|
|
2209
|
+
" The bridge stays offline until then. Wait for the pending headless call to return; " +
|
|
2210
|
+
"do not launch Unity for this project meanwhile."
|
|
2211
|
+
: bridgeStatusNextStep(status, wedge),
|
|
1788
2212
|
};
|
|
1789
2213
|
// bridge_status never reports an error — even a stopped bridge is a
|
|
1790
2214
|
// successful status read. _source=local because the synthesis happens in
|
|
@@ -1916,6 +2340,16 @@ export class ToolRouter {
|
|
|
1916
2340
|
pid = null;
|
|
1917
2341
|
}
|
|
1918
2342
|
}
|
|
2343
|
+
// No interactive Editor, but this server's own headless run holds the
|
|
2344
|
+
// project: say so (both dry-run and confirmed) instead of the misleading
|
|
2345
|
+
// "relaunch via the Hub" of unity_process_not_found. The scan never
|
|
2346
|
+
// returns that child, so it is never a kill target.
|
|
2347
|
+
if (pid === null) {
|
|
2348
|
+
const headlessRun = activeHeadlessRun(this.projectPath);
|
|
2349
|
+
if (headlessRun !== null) {
|
|
2350
|
+
return headlessRunErrorResult("restart_editor", headlessRun, this.projectPath);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
1919
2353
|
// 3. Dry-run branch: confirm was false/absent. Return the diagnosis +
|
|
1920
2354
|
// the PID the tool WOULD kill, with no side effect. Lets an agent
|
|
1921
2355
|
// preview before committing. feedback #2: include the PID source and
|
|
@@ -2117,6 +2551,10 @@ export class ToolRouter {
|
|
|
2117
2551
|
}
|
|
2118
2552
|
}
|
|
2119
2553
|
if (pid === null) {
|
|
2554
|
+
const headlessRun = activeHeadlessRun(this.projectPath);
|
|
2555
|
+
if (headlessRun !== null) {
|
|
2556
|
+
return headlessRunErrorResult("resource_pressure", headlessRun, this.projectPath);
|
|
2557
|
+
}
|
|
2120
2558
|
return sourceResult({
|
|
2121
2559
|
error: {
|
|
2122
2560
|
code: "unity_process_not_found",
|