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.
Files changed (174) hide show
  1. package/README.md +34 -2
  2. package/dist/agent-identity.d.ts +12 -0
  3. package/dist/agent-identity.js +2 -0
  4. package/dist/agent-identity.js.map +1 -1
  5. package/dist/batch-spawn.d.ts +39 -0
  6. package/dist/batch-spawn.js +287 -69
  7. package/dist/batch-spawn.js.map +1 -1
  8. package/dist/capabilities/build-capabilities.js +2 -0
  9. package/dist/capabilities/build-capabilities.js.map +1 -1
  10. package/dist/capabilities/discovery.d.ts +51 -0
  11. package/dist/capabilities/discovery.js +87 -0
  12. package/dist/capabilities/discovery.js.map +1 -0
  13. package/dist/capabilities/lifecycle.d.ts +6 -0
  14. package/dist/capabilities/lifecycle.js +17 -1
  15. package/dist/capabilities/lifecycle.js.map +1 -1
  16. package/dist/capabilities/tool-groups.js +1 -0
  17. package/dist/capabilities/tool-groups.js.map +1 -1
  18. package/dist/child-supervision.d.ts +6 -0
  19. package/dist/child-supervision.js +12 -0
  20. package/dist/child-supervision.js.map +1 -1
  21. package/dist/cli/args.d.ts +24 -2
  22. package/dist/cli/args.js +97 -2
  23. package/dist/cli/args.js.map +1 -1
  24. package/dist/cli/cli.js +42 -3
  25. package/dist/cli/cli.js.map +1 -1
  26. package/dist/cli/help-text.d.ts +1 -0
  27. package/dist/cli/help-text.js +83 -3
  28. package/dist/cli/help-text.js.map +1 -1
  29. package/dist/cli/json-guards.d.ts +2 -0
  30. package/dist/cli/json-guards.js +5 -0
  31. package/dist/cli/json-guards.js.map +1 -0
  32. package/dist/cli/setup-command.d.ts +116 -0
  33. package/dist/cli/setup-command.js +696 -0
  34. package/dist/cli/setup-command.js.map +1 -0
  35. package/dist/cli/update-command.d.ts +30 -0
  36. package/dist/cli/update-command.js +271 -0
  37. package/dist/cli/update-command.js.map +1 -0
  38. package/dist/compile-verify.d.ts +3 -0
  39. package/dist/compile-verify.js +4 -0
  40. package/dist/compile-verify.js.map +1 -1
  41. package/dist/constants.d.ts +10 -2
  42. package/dist/constants.js +10 -2
  43. package/dist/constants.js.map +1 -1
  44. package/dist/dialog-dismiss.d.ts +108 -12
  45. package/dist/dialog-dismiss.js +294 -109
  46. package/dist/dialog-dismiss.js.map +1 -1
  47. package/dist/dialog-policy.d.ts +55 -9
  48. package/dist/dialog-policy.js +85 -15
  49. package/dist/dialog-policy.js.map +1 -1
  50. package/dist/editor-search.d.ts +2 -0
  51. package/dist/editor-search.js +8 -0
  52. package/dist/editor-search.js.map +1 -0
  53. package/dist/gate-error.d.ts +3 -0
  54. package/dist/gate-error.js.map +1 -1
  55. package/dist/index.js +56 -1
  56. package/dist/index.js.map +1 -1
  57. package/dist/instance-discovery.d.ts +16 -0
  58. package/dist/instance-discovery.js +45 -0
  59. package/dist/instance-discovery.js.map +1 -1
  60. package/dist/jobs/adapters.d.ts +15 -0
  61. package/dist/jobs/adapters.js +147 -0
  62. package/dist/jobs/adapters.js.map +1 -0
  63. package/dist/jobs/handler.d.ts +3 -0
  64. package/dist/jobs/handler.js +29 -0
  65. package/dist/jobs/handler.js.map +1 -0
  66. package/dist/jobs/job-manager.d.ts +123 -0
  67. package/dist/jobs/job-manager.js +290 -0
  68. package/dist/jobs/job-manager.js.map +1 -0
  69. package/dist/live-client.d.ts +50 -2
  70. package/dist/live-client.js +221 -26
  71. package/dist/live-client.js.map +1 -1
  72. package/dist/local-tools.d.ts +17 -0
  73. package/dist/local-tools.js +47 -0
  74. package/dist/local-tools.js.map +1 -0
  75. package/dist/project-path.d.ts +99 -0
  76. package/dist/project-path.js +171 -0
  77. package/dist/project-path.js.map +1 -0
  78. package/dist/release-lookup.d.ts +32 -0
  79. package/dist/release-lookup.js +147 -0
  80. package/dist/release-lookup.js.map +1 -0
  81. package/dist/routers.d.ts +17 -3
  82. package/dist/routers.js +29 -8
  83. package/dist/routers.js.map +1 -1
  84. package/dist/running-unity.d.ts +21 -0
  85. package/dist/running-unity.js +50 -0
  86. package/dist/running-unity.js.map +1 -1
  87. package/dist/schema-defaults.d.ts +4 -3
  88. package/dist/schema-defaults.js +20 -6
  89. package/dist/schema-defaults.js.map +1 -1
  90. package/dist/server-instructions.js +7 -1
  91. package/dist/server-instructions.js.map +1 -1
  92. package/dist/server.d.ts +10 -3
  93. package/dist/server.js +24 -25
  94. package/dist/server.js.map +1 -1
  95. package/dist/setup/codex-toml.d.ts +23 -0
  96. package/dist/setup/codex-toml.js +514 -0
  97. package/dist/setup/codex-toml.js.map +1 -0
  98. package/dist/setup/portable-config.d.ts +59 -0
  99. package/dist/setup/portable-config.js +241 -0
  100. package/dist/setup/portable-config.js.map +1 -0
  101. package/dist/skill/SKILL.md +68 -0
  102. package/dist/skill/client-paths.js +2 -4
  103. package/dist/skill/client-paths.js.map +1 -1
  104. package/dist/skill/copy-references.d.ts +2 -0
  105. package/dist/skill/copy-references.js +16 -0
  106. package/dist/skill/copy-references.js.map +1 -0
  107. package/dist/skill/generate-skill.js +5 -3
  108. package/dist/skill/generate-skill.js.map +1 -1
  109. package/dist/skill/references/batch-and-gates.md +142 -0
  110. package/dist/skill/references/compile-and-safe-mode.md +163 -0
  111. package/dist/skill/references/discovery-and-groups.md +30 -0
  112. package/dist/skill/references/routing-and-lifecycle.md +62 -0
  113. package/dist/skill/references/senses-and-tests.md +41 -0
  114. package/dist/skill/references/yaml-and-offline-work.md +33 -0
  115. package/dist/templates/mcp-wrapper.sh +35 -0
  116. package/dist/tool-contract.d.ts +8 -0
  117. package/dist/tool-contract.js +253 -0
  118. package/dist/tool-contract.js.map +1 -0
  119. package/dist/tool-router.d.ts +62 -7
  120. package/dist/tool-router.js +455 -17
  121. package/dist/tool-router.js.map +1 -1
  122. package/dist/tool-session-state.d.ts +6 -0
  123. package/dist/tool-session-state.js +14 -0
  124. package/dist/tool-session-state.js.map +1 -1
  125. package/dist/tools/agent-capabilities.js +9 -0
  126. package/dist/tools/agent-capabilities.js.map +1 -1
  127. package/dist/tools/batch-execute.js +17 -14
  128. package/dist/tools/batch-execute.js.map +1 -1
  129. package/dist/tools/bridge-status.js +4 -5
  130. package/dist/tools/bridge-status.js.map +1 -1
  131. package/dist/tools/compile-check.js +6 -4
  132. package/dist/tools/compile-check.js.map +1 -1
  133. package/dist/tools/component-get.js +2 -2
  134. package/dist/tools/component-get.js.map +1 -1
  135. package/dist/tools/component-modify.js +4 -4
  136. package/dist/tools/component-modify.js.map +1 -1
  137. package/dist/tools/execute-csharp.js +8 -4
  138. package/dist/tools/execute-csharp.js.map +1 -1
  139. package/dist/tools/gameobject-modify.js +2 -1
  140. package/dist/tools/gameobject-modify.js.map +1 -1
  141. package/dist/tools/index.d.ts +2 -0
  142. package/dist/tools/index.js +10 -2
  143. package/dist/tools/index.js.map +1 -1
  144. package/dist/tools/inputsim-key.js +12 -14
  145. package/dist/tools/inputsim-key.js.map +1 -1
  146. package/dist/tools/inputsim-pointer.js +3 -3
  147. package/dist/tools/inputsim-pointer.js.map +1 -1
  148. package/dist/tools/inputsim-step.js +7 -8
  149. package/dist/tools/inputsim-step.js.map +1 -1
  150. package/dist/tools/inputsim-touch.js +9 -10
  151. package/dist/tools/inputsim-touch.js.map +1 -1
  152. package/dist/tools/jobs.d.ts +40 -0
  153. package/dist/tools/jobs.js +18 -0
  154. package/dist/tools/jobs.js.map +1 -0
  155. package/dist/tools/manage-tools.js +10 -5
  156. package/dist/tools/manage-tools.js.map +1 -1
  157. package/dist/tools/project-commands.d.ts +40 -0
  158. package/dist/tools/project-commands.js +23 -0
  159. package/dist/tools/project-commands.js.map +1 -0
  160. package/dist/tools/read-compile-errors.js +14 -93
  161. package/dist/tools/read-compile-errors.js.map +1 -1
  162. package/dist/tools/scan-paths.js +4 -4
  163. package/dist/tools/scan-paths.js.map +1 -1
  164. package/dist/tools/schema-fragments.d.ts +15 -3
  165. package/dist/tools/schema-fragments.js +23 -13
  166. package/dist/tools/schema-fragments.js.map +1 -1
  167. package/dist/tools/upgrade.d.ts +40 -0
  168. package/dist/tools/upgrade.js +55 -0
  169. package/dist/tools/upgrade.js.map +1 -0
  170. package/dist/unity-install-discovery.d.ts +24 -0
  171. package/dist/unity-install-discovery.js +113 -4
  172. package/dist/unity-install-discovery.js.map +1 -1
  173. package/package.json +4 -3
  174. package/templates/mcp-wrapper.sh +35 -0
@@ -0,0 +1,142 @@
1
+ # Batch and gates
2
+
3
+ ## Core loop: mutate → gate → fix
4
+
5
+ 1. **Discover** — `capabilities`, then `find_members` for reflection targets.
6
+ 2. **Declare scope** — `paths_hint` for every asset path you intend to touch.
7
+ 3. **Mutate** — typed tools preferred over `execute_csharp` / `invoke_method` / `execute_menu`; default `gate: "enforce"`.
8
+ 4. **Read the gate** — on `isError: true`, inspect `gate.delta.newIssues` + `agentNextSteps`.
9
+ 5. **Fix** — address the top error; `apply_fix` with `dry_run: true` first when a `fixId` is present. Each issue carries `rootCause` (machine-readable, branch on it) + `evidence` (the specific broken ref/value) + `fixCandidates` (every fix option with its `safe` flag) + `remediation` (the human-readable next step) — read these before choosing a fix.
10
+ 6. **Retry** — confirm `gate.delta.resolvedErrors > 0` or `newErrors == 0`.
11
+
12
+ ### Gate modes
13
+
14
+
15
+ | Mode | When |
16
+ | ------------------- | ------------------------------------------------------- |
17
+ | `enforce` (default) | Normal edits — fail fast on new errors |
18
+ | `warn` | Exploratory — read `gate.delta` but call does not error |
19
+ | `off` | Trusted admin scripts only — no checkpoint/validate |
20
+
21
+
22
+ ### Gate failure (canonical shape)
23
+
24
+ ```json
25
+ {
26
+ "mutation": { "success": true, "output": "Player(Clone)", "error": null },
27
+ "gate": {
28
+ "mode": "enforce",
29
+ "validation": {
30
+ "passed": false,
31
+ "issues": [{
32
+ "severity": "Error", "code": "MISSING_SCRIPT",
33
+ "assetPath": "Assets/Prefabs/Player.prefab",
34
+ "fixId": "remove_missing_script", "fixSafe": true
35
+ }]
36
+ },
37
+ "delta": {
38
+ "newErrors": 1, "resolvedErrors": 0,
39
+ "newIssues": ["missing_references|Error|Assets/Prefabs/Player.prefab|MISSING_SCRIPT"]
40
+ }
41
+ },
42
+ "logs": [
43
+ { "severity": "warning", "message": "...", "source": "unity" }
44
+ ],
45
+ "agentNextSteps": [
46
+ "New error: missing_references MISSING_SCRIPT on Assets/Prefabs/Player.prefab",
47
+ "Fix available: use unity_open_mcp_apply_fix with fix_id=\"remove_missing_script\""
48
+ ]
49
+ }
50
+ ```
51
+
52
+ Issue keys in `gate.delta.newIssues` are `ruleId|severity|assetPath|issueCode` (severity is `ERROR` / `WARN` / `INFO`). The `newIssues` / `resolvedIssues` arrays are **bounded** (first 25 keys) with a `…Truncated` count and a `newIssuesByRule` / `resolvedIssuesByRule` histogram — branch on `gate.outcome` + the counts (`newErrors`, `newWarnings`); fetch the full list via `validate_edit` / `scan_paths` when you need it. On success, `validation.passed: true`, empty `issues`, `agentNextSteps: []`. The `logs` array carries Unity console entries emitted *during this call* (always present, `[]` when none) — read it inline instead of polling `read_console` after a mutation. Stacks are omitted here; `read_console` stays the verbose path.
53
+
54
+ **`gate.delta` can be `null`** — never a zeroed object — when no delta was computed. A checkpoint failure reports `gate.outcome: "failed"` with `mutation.success: false`; a mutation failure (the tool returned `success: false` or threw before validation) reports `gate.outcome: "skipped"` with `skippedReason: "mutation_failed"` and `mutation.success: false` — the gate never ran, so detect it from `mutation.success`, not from `gate.outcome`; a validate-scan failure (the scan threw, or a verify rule threw on either side of the delta) reports `gate.outcome: "validate_scan_failed"` with the mutation committed. All three carry `isError: true`: the mutation's health is **unknown** (or it failed outright). When a verify rule threw, `gate.rulesFailed` lists the rule ids whose findings are missing. Do NOT retry the mutation (it already applied) and do NOT treat the missing delta as a clean pass — run `validate_edit` / `scan_paths` on the touched paths to confirm health. `scan_paths` / `validate_edit` responses carry the sibling signal as `rulesFailed` + `scanIncomplete: true` when any selected rule threw.
55
+
56
+ ### Verify rules and issue codes
57
+
58
+ Authoritative via `capabilities` (call for the live list). Implemented:
59
+
60
+ - `**missing_references**` — per-PPtr-field view. Codes: `missing_guid` (Error, fix `relink_broken_guid` — unsafe, needs `target_guid`), `missing_fileid` (Error), `missing_script` (Error, fix `remove_missing_script`), `missing_local_fileid` (Warning), `empty_local_ref` (Warning on **user-script** fields — the real bugs; **Info** on built-in empty-by-default fields like `m_SelectOn*`, sprite swaps, TMP material/style — the ~98% noise floor), `missing_method` / `type_mismatch` / `duplicate_component` / `invalid_layer` (Warning, full-scan only). `empty_local_ref` keys are `<transformPath>:<property>` (content-addressed, survives prefab rebuilds); use `fail_on_severity` / `profile` to control what surfaces.
61
+ - `**scene_prefab_health**` — structural health. Codes: `broken_reference` (Error), `high_risk_bootstrap` / `scene_object_count` / `component_hotspot` / `inactive_expensive` / `inactive_heavy` / `deep_nesting` / `override_explosion` (Warning).
62
+ - `**dependencies**` — forward dependency graph. Codes: `broken_dependency` (Error — asset-graph edge to a missing asset; fix `relink_broken_guid` — unsafe), `dependency_cycle` (Warning).
63
+ - `**asmdef_audit**` — assembly definition health. Codes: `broken_asmdef_reference` (Error — reference that does not resolve to a compiled assembly or known asmdef), `asmdef_missing_name` (Error — no `name` field), `malformed_asmdef` (Error — JSON failed to parse), `asmdef_duplicate_name` (Error, full-scan — name shared by 2+ asmdefs), `asmdef_circular_reference` (Error, full-scan — DFS cycle over the name-based reference graph), `asmdef_editor_in_runtime` (Warning — runtime assembly referencing an editor assembly), `asmdef_auto_referenced_orphan` (Warning, full-scan — `autoReferenced=false` and unreferenced), `asmdef_platform_filter_broad` (Warning — no platform filters), `asmdef_platform_filter_contradict` (Warning — simultaneous include + exclude platforms), `asmdef_version_define_invalid` (Warning — version define references a `com.*` package).
64
+ - `**project_health**` — whole-project integrity, full-scan only (does not fire on a scoped validate_edit). Codes: `orphan_meta` (Warning — `.meta` with no companion asset, fix `remove_orphan_meta` safe), `duplicate_guid` (Error — GUID shared by 2+ assets, fix `fix_duplicate_guid` unsafe), `missing_project_setting` (Error — required ProjectSettings file missing), `project_empty_folder` / `project_meta_only_folder` (Warning), `project_deep_nesting` (Warning — depth > 8), `project_large_folder` (Warning — > 200 files), `project_broken_asset` (Error — asset failed to load), `project_empty_scene` (Warning — zero root objects).
65
+ - `**materials**` — material reference + performance health. Per-asset codes: `missing_shader` (Error — null shader or InternalErrorShader, i.e. the original shader failed to compile/is missing), `builtin_shader` (Warning — Standard/Legacy/Mobile builtin), `builtin_texture` (Warning — unity_builtin texture), `render_queue_override` (Warning), `unable_to_load` (Error). Note: there is no `missing_texture` code — a null texture slot is usually legitimate (most optional slots like `_BumpMap` are intentionally empty); a genuinely-broken texture PPtr surfaces via the `missing_references` rule's `missing_guid` instead. Full-scan-only codes: `duplicate_material` (Warning — SHA-256 fingerprint match), `unused_material` (Warning — unreferenced + not in Resources), `variant_parent_invalid` (Error), `variant_deep_chain` / `variant_heavy_overrides` (Warning), `gpu_instancing_off` / `srp_batcher_incompatible` (Warning), `null_material` / `null_material_slot` / `builtin_material` (Warning, renderer-side).
66
+ - `**animation_analysis**` — animator controller + clip health. Per-asset codes: `missing_clip` (Error — state with no motion assigned), `empty_clip` (Warning — `.anim` declaring no curves), `unreachable_state` (Warning — not reachable from entry/default/any-state via BFS), `complexity_over_threshold` (Warning — > 50 states), `anystate_overuse` (Warning — > 5 any-state transitions), `parameter_mismatch` (Warning — script references a param not in the controller), `expensive_curves_density` / `expensive_curves_count` (Warning). Full-scan-only: `duplicate_clip` (Warning — byte-size match).
67
+ - `**shader_analysis**` — shader compile + variant health. Per-asset codes: `shader_compile_error` (Error — InternalErrorShader), `missing_shader_asset` (Error — `.shader` failed to load), `variant_explosion` (Warning — `2^keywords × passes` over threshold), `pass_count_exceeded` (Warning), `fallback_shader` (Warning — has a `Fallback` directive), `expensive_feature_platform` (Warning — mobile-expensive keywords; fires only when the scan runs with the mobile profile — `scan_all`/`baseline_create`/`regression_check` with `platform_profile: "mobile"`; live `validate_edit` / `scan_paths` always use desktop), `platform_keyword_mismatch` (Warning — HDRP shader on mobile profile, same mobile-profile gating). Full-scan-only: `duplicate_keyword_profiles` (Warning — materials sharing a keyword set).
68
+
69
+ ### Issue explainability
70
+
71
+ Every issue in a `scan_paths` / `validate_edit` response carries optional explainability fields (beyond `ruleId` / `severity` / `code` / `assetPath` / `description`). Two code-shaped fields carry different things — match the right one:
72
+
73
+ - **`code`** — the BARE catalog code (`missing_script`, `invalid_layer`, …). This is what `capabilities` / `list_rules` / the table above advertise. Branch on `code === "<bare>"`.
74
+ - **`issueCode`** — the full key-discriminator form (`missing_script:<guid>`, `invalid_layer:7`, `duplicate_component:<type>:<gameObject>`). Used internally for gate-delta tracking (so two broken GUIDs on one asset do not collapse to one key) and by `apply_fix` to target the exact instance. Pass `issueCode` (or the whole `issue_id`) back to `apply_fix`, but branch recovery logic on the bare `code` / `rootCause`.
75
+
76
+ - **`rootCause`** — a stable machine-readable code identifying *why* the issue class happens. Branch recovery on this, not on free-text. Values: `missing_guid_reference`, `missing_fileid_reference`, `missing_script_class`, `missing_dependency`, `orphaned_meta`, `duplicate_guid`, `structural_complexity`, `configuration_mismatch`, `resource_missing`, `build_blocker`. The same code is declared on the issue descriptor in `capabilities` / `list_rules` (`issues[].rootCause`).
77
+ - **`evidence`** — the per-instance payload that fired *this* issue (the broken reference's GUID / fileID / line, the duplicate group's paths, the count vs threshold, etc.). Keys are issue-class-specific, always string-valued. Absent when the rule has no per-instance detail.
78
+ - **`fixCandidates`** — every fix that can resolve the issue, each `{fixId, safe}`. Use this over the legacy single `fixId` / `fixSafe` pair (kept for backwards compat): it lists safe **and** unsafe options so you pick deliberately. Absent when no fix exists.
79
+ - **`remediation`** — a short, clean, user-visible playbook for the issue class (the human-readable next step). Pair with `rootCause`: branch on the code, surface the remediation text.
80
+
81
+ ### Fixes
82
+
83
+ `apply_fix` defaults to `dry_run: true` (the dry-run short-circuits the gate entirely — returns description/candidates without checkpoint+validate):
84
+
85
+ - `**remove_missing_script**` (safe) — strips `MonoBehaviour` whose script GUID no longer resolves. Works on `.prefab` / `.unity`.
86
+ - `**remove_orphan_meta**` (safe) — deletes a `.meta` whose companion asset was deleted. Producer: `project_health` (live) / `offline_integrity` (offline). No asset data lost.
87
+ - `**relink_broken_guid**` (unsafe) — rewrites a broken external GUID reference. Dry-run advertises candidate targets; apply requires `target_guid`. Never auto-applied.
88
+ - `**fix_duplicate_guid**` (unsafe) — regenerates the GUID of one colliding asset. Re-GUIDing silently rewires the asset graph, so pick the less-referenced asset deliberately; apply on that asset's issue id. Producer: `project_health` / `offline_integrity`. Never auto-applied.
89
+ - `**reassign_missing_shader**` (unsafe) — assigns a shader to a material whose shader is null / the error shader. Dry-run advertises candidate shaders; apply requires `target_shader` (shader name e.g. `Standard`, or asset path). Producer: `materials`. Never auto-applied.
90
+
91
+ If `fix_id` omitted, the response lists every fix that can resolve the given `issue_id`.
92
+
93
+ **Safe auto-fix rollback.** A non-dry-run `apply_fix` runs checkpoint → apply → validate → delta, and if the fix fails to apply **or** introduces new errors under `enforce`, the touched files are restored to their pre-fix state and the response carries a top-level `rollback` block: `{rolledBack: true, reason, restoredPaths[]}`. Read `gate.delta.newErrors` + `rollback` together — a rolled-back fix left no project change, so inspect the issue manually before retrying. Rollback is **not** triggered by new warnings (informational) or under `warn`/`off` gate modes (report-only). Applying with `gate: "off"` commits without rollback protection; the response carries `rollbackDisabled: true` so the unguarded mutation is visible — verify asset health with `validate_edit` afterward.
94
+
95
+ ## Gate intelligence: plan before, explain after
96
+
97
+ Three read-only, gate-free tools compose gate foundations into agent-actionable shapes. They do **not** run a rule scan or mutate — treat outputs as guidance (every response carries a `heuristicNote`).
98
+
99
+ - **Before mutating** — `unity_open_mcp_impact_preview` (`paths_hint`): resolves the auto-selected rule set, classifies each path, reports coarse `risk.band` (`low`/`moderate`/`high`) with `confidence`. Size risk before paying for a checkpoint.
100
+ - **Before mutating** — `unity_open_mcp_gate_budget_estimate` (`paths_hint`, `mode: "cache"` | `"sample"`): forecasts `estimatedDurationMs` (lower bound) + `estimatedIssueBudget` (upper bound) with `basis` + `confidence`. `sample` runs a cheap checkpoint scan (grounded); `cache` inspects the latest VerifyCacheService snapshot (cheap, coarse).
101
+ - **After mutating** — `unity_open_mcp_mutation_explain` (`checkpoint_id?`, `tool_name?`): projects the most recent gate run into a `narrative` + structured `summary` (outcome, new/resolved counts, durations, `agentNextSteps`).
102
+
103
+ Typical sequence: `impact_preview` (size) → `gate_budget_estimate` `mode: "sample"` (cost) → mutate → `mutation_explain` (narrate).
104
+
105
+ ### Checkpoint → mutate → delta (large refactors)
106
+
107
+ `checkpoint_create` with scoped paths → run mutations (`gate: off` for bulk, or `enforce` per call) → `delta` against the checkpoint for a single verification pass.
108
+
109
+ - **Session-scoped.** Checkpoints live in an in-memory ring buffer (capacity 20, LRU-evicted on access recency) and are cleared on script recompile, domain reload, or editor restart — they are never persisted to disk. Capture immediately before mutating and delta right after; do not hold a checkpoint id across a recompile.
110
+ - **Missing checkpoint is non-blocking.** If `delta` (or `mutation_explain` with a `checkpoint_id`) references a checkpoint that is gone (evicted, or lost to a reload), the call returns success with `"unavailable": true` + `agentNextSteps` rather than an error. Treat it as "no baseline to delta against" and fall back to `validate_edit` / `scan_paths` on the relevant paths. It does not block the workflow.
111
+ - **Clear from the editor.** The Bridge window → Gate tab → Checkpoint history has a "Clear history" button (two-click confirm) that empties the session ring buffer. It touches nothing on disk and leaves gate-run history intact.
112
+
113
+ ### Batch multiple mutations into one call (`batch_execute`)
114
+
115
+ When you need to set up many things at once (spawn N objects + materials + assignments), one `**unity_open_mcp_batch_execute`** call runs the whole sequence in a single HTTP round trip, cutting latency and token cost. The batch is **strictly safer than sequential calls**: the WHOLE sequence shares ONE gate cycle (one checkpoint → all steps → one validate/delta) and ONE undo group.
116
+
117
+ - **When to use.** Multi-step setup where each step is independent and typed (create 3 cubes + 3 materials + assign). Prefer the per-tool array surfaces (`component_modify.fields[]`, `gameobject_modify` multi-surface) when a *single* tool already batches — `batch_execute` is for bundling *different* tools.
118
+ - **Always pass `paths_hint`.** The batch is mutating; scope `paths_hint` to the union of every path the nested steps touch (scene paths, asset paths). Required.
119
+ - **Prefer typed tools inside the batch.** Each `commands[i]` carries a full tool id + its `params` (omit `paths_hint`/`gate` per step — the batch owns those for the sequence). Nested meta-tools (`execute_csharp`, `invoke_method`, `execute_menu`), `batch_execute` itself, `compile_check`, and any `restart_then_settle` tool (`scene_open`, `scene_create` in Single mode, `package_add`/`remove`, `asmdef_create`/`modify`, `build_set_*`, `settings_set_player`, `reimport_package`) are **blocked** in v1 — a domain reload or scene switch mid-batch would silently abort every later step. The refusal is `batch_nested_reload_unsafe`. Use these as single top-level calls.
120
+ - **`run_tests` is blocked too, for a different reason** (`batch_step_requires_server_poll`): its terminal result comes from the server polling a results file, which only the top-level route does. Nested, the step would report `success` with a non-terminal `{status:"started"}` body and you would never see the outcome.
121
+ - **Nestability is NOT the `batchCapable` flag.** That flag answers "is there a headless batch-spawn fallback when the bridge is down?" — a different axis. Read the refusals above.
122
+ - **Every refusal is pre-flight and names a reachable alternative.** Nothing ran, `batch.results[]` is empty, nothing to undo. Where a meta-tool equivalent exists the message gives it (`recompile_scripts` → `CompilationPipeline.RequestScriptCompilation()` via `execute_csharp` + poll `editor_status`; `run_tests` → `invoke_method` on the test method) — useful when your client did not refresh its tool list after `manage_tools` activated a group, so the "single top-level call" it points at is not visible to you.
123
+ - **`fail_fast: true` (default) stops on the first failure**; later entries are `skipped` (not executed). Set `fail_fast: false` to run every step and collect per-step errors.
124
+ - **Read `gate.delta` ONCE at the end** — the batch-level gate reports new issues introduced by the whole sequence. Partial-failure note: v1 does NOT roll back successful steps when a later step fails (same as the competitor); undo the whole batch with a single `editor_undo` if needed.
125
+ - **Limits.** 25 commands default, 100 hard max (`batchExecuteMaxCommands` in `.unity-open-mcp/settings.json`). `parallel: true` is accepted but ignored (Unity API is main-thread; sequential only).
126
+ - **Response shape:** `batch.results[]` carries `{ index, tool, status }` per step (`success`/`failed`/`skipped`) with `output` on success and `error` on failure.
127
+
128
+ ### Read-only requests and batch preflight
129
+
130
+ Use `execute_csharp(read_only: true)` only for inspection without writes,
131
+ compile/refresh, scene switching, or play transitions. Pure reads run against
132
+ dirty scenes without saving them, skip gates even with `paths_hint`, and return
133
+ lifecycle `none`. This assertion does not sandbox indirect C# calls. Known
134
+ explicit disruptive calls retain scope and dirty-scene protection.
135
+
136
+ All-read `batch_execute` calls, including screenshots, need no `paths_hint` and
137
+ create no checkpoint or undo group. Mixed/mutating batches require the explicit
138
+ union scope. Every step is schema/lifecycle checked before dispatch; fix all
139
+ reported errors before retrying. A failed mutation before validation reports
140
+ `gate.outcome: "skipped"` and `skippedReason: "mutation_failed"`. Read
141
+ `mutation.success` separately: committed work in a partial batch still receives
142
+ validation, so a passed gate does not mean every step succeeded.
@@ -0,0 +1,163 @@
1
+ # Compile and Safe Mode
2
+
3
+ ## Compile and reload evidence
4
+
5
+ `read_compile_errors` prefers a bounded live CompilationPipeline snapshot, falling
6
+ back to logs if unavailable. `currently_compiling` means wait; `assembly_stale`
7
+ means source changed after the last completed compile; `compile_failed` reports
8
+ current diagnostics (`errorSource: "Editor.log"` when the failing compile ran
9
+ before the bridge loaded, e.g. at Editor startup). A confirmed `no_errors_found`
10
+ with `sourceMatches:true` outranks `historicalLogErrors`. Inspect generation and
11
+ before/after assembly mtimes.
12
+ For stale sources, activate `typed-editor`, call `recompile_scripts`, then re-read.
13
+ `capabilities` and `bridge_status` accept `project_path` for a different project
14
+ (also via `manage_tools` invoke); omit `_meta.port` on that call.
15
+
16
+ On `editor_reloading`, retry after `retryAfterMs`; do not start another Unity.
17
+ Disk-backed reads (`list_assets`, `read_asset`, `search_assets`,
18
+ `find_references`, `dependencies`, `read_compile_errors`) keep answering during
19
+ a compile or reload.
20
+ `compile_check` is headless-only. It captures its own log and classifies compiler,
21
+ project-load, and spawn failures. `compile_indeterminate` means exit 0 lacked
22
+ sufficient completion evidence; never infer success from exit 0 alone. A
23
+ `batch_in_progress` response means wait for the current child. On response
24
+ correlation/JSON errors, inspect post-state before repeating a mutation.
25
+
26
+ ## Unity state triage (before edits/tests, and on `bridge_offline`)
27
+
28
+ The single most common agent mistake is misclassifying Unity's state. Follow this **in order** before running tests, before launching Unity, and whenever a tool returns `bridge_offline` or `bridge_compile_failed`.
29
+
30
+ > **Editing C#?** An offline bridge after a C# edit is frequently a *symptom* of a failed compile, not "Unity isn't running." Skip to [Compile failure recovery](#compile-failure-recovery) and call `read_compile_errors` — it reads `Editor.log` offline and survives a dead bridge.
31
+
32
+ ### Step 1 — Is Unity running at all?
33
+
34
+ **Preferred — let the server tell you.** Call `unity_open_mcp_bridge_status` (or any live tool / `ping`). It scans for a live Unity process matching this project and reports `dead_bridge` with a `recoveryHint` → `read_compile_errors` when Unity is in **cold Safe Mode** (launched into Safe Mode before the bridge ever wrote an instance lock). Likewise, live tools and `ping` return `bridge_compile_failed` with the same recovery guidance in that case. If you see either, **go directly to Step 4** — no manual `ps` needed.
35
+
36
+ **Manual fallback** (Linux, or Unity opened without `-projectPath` — both are known blind spots of the auto-scan):
37
+
38
+ ```bash
39
+ cat ~/.unity-open-mcp/instances/*.json 2>/dev/null
40
+ ps aux | grep -i "Unity.app/Contents/MacOS/Unity" | grep -v grep # macOS
41
+ # Windows (PowerShell): Get-Process Unity -ErrorAction SilentlyContinue
42
+ ```
43
+
44
+ - **No instance files AND no Unity process** → Unity isn't running. Open the project.
45
+ - **No instance files BUT a Unity process exists** → booting or in **Safe Mode** (bridge never started). Go to Step 4.
46
+ - **Instance files exist** → go to Step 2.
47
+
48
+ ### Step 2 — Read the instance lock, classify state
49
+
50
+ The lock at `~/.unity-open-mcp/instances/<sha256(projectPath)>.json` (lowercase hex sha256, forward slashes, no trailing slash) carries `pid`, `port`, `state`, `heartbeatAt` (refreshed every 0.5s), `isCompiling`, `unityVersion`. Check `pid` liveness with `kill -0 <pid>` (exit 0 = alive) and heartbeat freshness (<10s = fresh):
51
+
52
+
53
+ | State | Meaning | Action |
54
+ | --------------------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------- |
55
+ | Lock missing / `pid` dead | Unity not running | Open Unity or use batch fallback |
56
+ | `pid` alive, heartbeat fresh, `state: idle` | Healthy | **Proceed** |
57
+ | `pid` alive, `state: compiling` / `isCompiling: true` | Unity compiling | **Wait** — poll `curl http://127.0.0.1:<port>/ping`, re-read `isCompiling`. Don't edit/test |
58
+ | `pid` alive, `state: reloading`, heartbeat **stale** (>10s) | **Safe Mode** — bridge assembly failed to recompile | Go to Step 4 |
59
+ | `pid` alive, heartbeat fresh, but every tool returns `bridge_offline` | **Port mismatch** | Go to Step 3 |
60
+
61
+
62
+ ### Step 3 — Port mismatch (`UNITY_OPEN_MCP_BRIDGE_PORT` trap)
63
+
64
+ The lock's `port` is authoritative. An MCP-client config pinned `UNITY_OPEN_MCP_BRIDGE_PORT` to a stale value (the env var always wins on both sides). The agent cannot edit the MCP client config at runtime, so **tell the user** the bridge's actual port and that the `UNITY_OPEN_MCP_BRIDGE_PORT` value in their MCP client config (e.g. `.zcode/cli/config.json`, `.cursor/mcp.json`, `.claude.json`) is wrong — removing it lets both sides derive the per-project hash and agree.
65
+
66
+ ### Step 4 — Safe Mode / `bridge_compile_failed` recovery
67
+
68
+ A C# edit broke the bridge assembly (or a dependency); Unity is stuck mid-reload showing the "Enter Safe Mode?" dialog.
69
+
70
+ 1. Call `**unity_open_mcp_read_compile_errors`** — reads `Editor.log` tail offline, returns structured CSxxxx errors (`file`/`line`/`code`). The **only** diagnostic that survives a dead bridge. (`compile_check` does **not** work here — its batch entry point lives in the same broken assembly, and the per-project lock blocks a second instance.)
71
+ 2. Read `errors[].file` / `line` / `code` and **fix the CS error in source first.** Do not retry tests, relaunch Unity, or call `compile_check`.
72
+ 3. Trigger a recompile (see [Local package source recompile caveat](#local-package-source-recompile-caveat) if you develop against `packages/` source; otherwise a normal recompile from Unity is enough); the bridge reloads itself once the assembly compiles. The MCP server auto-dismisses the Safe Mode dialog under the default `UNITY_OPEN_MCP_DIALOG_POLICY=ignore`; set `manual` (or `UNITY_OPEN_MCP_NO_AUTO_DISMISS_LAUNCH_ERRORS=1`) to opt out entirely. Full policy matrix: [Dialog policy](https://github.com/AlexeyPerov/Unity-Open-MCP/blob/main/docs/dialog-policy.md).
73
+ 4. Only when `read_compile_errors` reports no errors AND the lock shows fresh heartbeat + `state: idle` is the bridge back.
74
+
75
+ ### Step 5 — Never conclude "Unity not running" while a process is alive
76
+
77
+ Safe Mode still owns the per-project lock and may show a window, but the bridge is dead — looks identical to "not running" from the MCP server. Always confirm with `kill -0 <pid>`; if alive, you are in Step 4.
78
+
79
+ ## Compile failure recovery
80
+
81
+ Compile failures surface as **machine-readable error codes** — branch on the code, not free-text. The four you will hit:
82
+
83
+ - `**bridge_compile_failed`** — bridge assembly itself failed; live Editor stuck (Safe Mode), every live tool refuses. **Recovery:** `read_compile_errors` (reads `Editor.log` offline; survives the broken bridge) → fix the CS error → trigger a recompile.
84
+ - **`bridge_offline`** — Unity may not be running, or not yet recompiled. If Unity **is** open it has already written the latest CSxxxx diagnostics to `Editor.log`; `read_compile_errors` retrieves them.
85
+ - **`main_thread_blocked`** — the Unity main thread did not pick up the dispatch within the timeout. A Unity modal dialog (unsaved changes, scene modified externally, safe mode) or a long editor operation is almost certainly blocking it. The dispatch **never started**, so do NOT raise `timeout_ms`. **Recovery:** if a scene is dirty call `scene_save` then retry; check `editor_status` / `bridge_status`; the dismiss loop (`UNITY_OPEN_MCP_DIALOG_POLICY`) handles safe-mode / scene-modified-externally modals automatically, and `UNITY_OPEN_MCP_ALLOW_UNSAVED_SCENE_DISMISS=1` opts in to the destructive unsaved-changes modal; if unreachable, the editor must be restarted. Distinct from a plain timeout (the work started but ran long — that one CAN warrant a higher `timeout_ms`).
86
+ - **`timeout` on `execute_menu`** — the menu may still be **running**; a timeout here is a wait that elapsed, not a failure (`Assets/Refresh` after an importer version bump reimports the whole content tree and routinely outlasts the default 30s). `execute_menu` accepts `timeout_ms` (same semantics + ~55s host-safe cap as `execute_csharp`) — raise it rather than retrying. Confirm the outcome with `editor_status` / an asset probe: a blind retry of an authoring menu can **double-write** assets.
87
+ - **A tool failing in a way this file says is fixed** — check `bridge_status` → `wireContract` before concluding it regressed. `stale: true` means the installed Unity bridge package predates this MCP server's contract (reinstall/update it and retry); `stale: false` means it really is a regression worth reporting. `bridgeVersion` alone cannot tell you: the package semver does not move for a wire-contract fix, so a stale install and a current one report the same string.
88
+ - **`status: "wedged"`** (`bridge_status`) — the Editor process, heartbeat and `/ping` all look fine but the Editor cannot do the work. `wedged.reason = "editor_fd_exhaustion"`: the build driver is dead, `Library/ScriptAssemblies` has stopped updating, C# edits never take effect — only a restart recovers (`execute_csharp` fails with `editor_build_wedged` in this state rather than returning stale output). `wedged.reason = "main_thread_wedged"`: a modal dialog is blocking Unity's message pump — the heartbeat is stale but `/ping` still answers, which a failed bridge assembly could never do, so this is **not** Safe Mode and there is no compile error to chase. No tool can dismiss a modal; an operator must close it in the Unity UI. Prefer non-interactive APIs over `execute_menu`/`ExecuteMenuItem` for converter-style menus.
89
+ - **`editor_instance_locked`** — a batch/`compile_check` spawn cannot run while an Editor holds the project lock. The error names the tool you actually called and its message + `agentNextSteps[]` split the diagnosis three ways: a **fresh instance lock** (the live bridge should be reachable — retry the live route / check `bridge_status`), a **live process with the listener not up yet** (a booting Editor or Safe Mode — wait and retry; the call takes the live route once the listener binds), or **no matching process** (a stale `Temp/UnityLockfile` from a crashed session). Do not read this code as "close the Editor" until you have read which variant it reports.
90
+ - **`unity_spawn_refused`** — the Unity binary could not be executed (spawn `ENOENT`/`EACCES`, or exit code `127`). Do **not** retry `compile_check` blindly. Verify `UNITY_PATH`, then fall back to `read_compile_errors` / `bridge_status` to check compile state without headless spawn.
91
+ - **`_compileVerify: { code: "compile_noop" | "dll_stale" }`** — a recompile *reported success* but the compiled state did not advance. Surfaced as an additive annotation on a **successful** result (not an error), so check for it after any `compile-reload` tool. `compile_noop` = tool registry count + DLL mtime unchanged (incremental no-op); `dll_stale` = `Library/ScriptAssemblies/*.dll` older than your source edit. Both mean: do **not** trust the success — force a rebuild (no-op `package_add`/`package_remove`, or operator refocus of the Editor), then verify DLL mtime > edit mtime before tests.
92
+ - **`compilePending: true`** (additive, on the gate envelope next to `settleMs`) — the editor was **still compiling** when the gate validated after the post-mutation settle wait. The gate's `passed` / `delta.newErrors:0` reflects the **PRE-compile** state, not the new code, so a clean-looking gate does **not** mean the new code is verified healthy. `agentNextSteps[]` carries the same advisory. Poll `editor_status.isCompiling` until `false`, then `read_compile_errors` to confirm the new code. (Root cause: `RequestScriptCompilation` can no-op silently — the compile may be queued but not yet started, or the settle cap elapsed mid-compile.)
93
+ - **`_route.fallbackReason: "editor_busy"`** (with `_route.editorPid`) — a tool fell back to the batch route because the live bridge was unreachable, and the batch reported `editor_instance_locked` with a **live** editor PID. The actionable truth is "the editor is busy (a TestRunner run or long import is holding the main thread); retry shortly", **not** a lock conflict. Retry the call after a short wait; if it persists, check `editor_status` / `bridge_status`. Contrast `fallbackReason: "live_unavailable"` (no live PID, or a genuine lock conflict / offline bridge), which is not transient.
94
+ - **`status: "triggered_reload"`** (not an error) — a compile-reload tool (`reimport_package`, `asmdef_create`/`asmdef_modify`, `package_add`/`package_remove`, `script_write`, `execute_csharp`, …) returned HTTP 200 with an EMPTY body. This is the signature of a domain reload the mutation itself triggered — the bridge tore down its HTTP socket mid-response and the in-flight response was lost. The mutation almost certainly committed; do **not** retry blindly (that would re-run the mutation). Verify post-state instead: poll `editor_status` / `bridge_status` until running + not compiling, then `read_compile_errors` to confirm no CSxxxx errors. The response carries `agentNextSteps[]` spelling this out.
95
+
96
+ For `bridge_compile_failed` / `bridge_offline`: call `**unity_open_mcp_read_compile_errors`** → fix `errors[].file`/`line`/`code` → trigger recompile. (If no Editor is open, `Editor.log` is stale from the previous session and won't reflect your latest edits — recompile verification needs Unity running.)
97
+
98
+ **Stale-log guard.** When the response carries `staleLogSuspected: true`, the cited source files were edited more recently than `Editor.log` — the error block may reference on-disk code you have **already fixed** (Unity's incremental compiler no-op'd the recompile of the broken assembly, so the log's most-recent error block never got rewritten). Before chasing the errors, call `**unity_open_mcp_recompile_scripts`** (in the `typed-editor` group — activate it first) and re-read; with the Editor closed or unreachable, `compile_check` spawns a fresh headless recompile instead. The response also carries `staleLogHint` + `staleLogNewerFiles[]` (the offending files).
99
+
100
+ **Errors-predate-edits guard.** `staleLogSuspected` compares against `Editor.log`'s **file** mtime, which unrelated asset imports keep refreshing — so the file can look fresh while the error BLOCK inside it is old. The response therefore also carries `errorsMayPredateEdits: true` + `errorsMayPredateEditsFiles[]` + a headline caveat when a file the errors **cite** is newer than the newest `Library/ScriptAssemblies/*.dll`: no compile has completed since you edited it, so the block is necessarily from an earlier compile. Do not re-read or re-fix that code — force a recompile and read again.
101
+
102
+ **`status: "stale_log"` downgrade.** When the log is stale (`staleLogSuspected`) **or** authored by a different Unity (`logAuthorshipMismatch`), the top-level `status` is downgraded from `compile_failed` to `"stale_log"` and `unhealthy` is suppressed — the `errors[]` are still attached as evidence, but the headline makes clear they **may not apply** to the running editor and must be confirmed by a genuine recompile before acting on them. The raw log verdict is preserved in `logVerdict`. Branch on `unhealthy`/`status === "stale_log"` rather than `errorCount > 0` so you do not "fix" a burst of phantom errors that were already resolved on disk.
103
+
104
+ **Stale-assembly guard.** The response carries `staleAssembly: true` when at least one `Assets/**/*.cs` source is newer than the newest `Library/ScriptAssemblies/*.dll` — the running assembly predates the latest source (Unity's incremental compiler no-op'd a recompile), so a `no_errors_found` signal **cannot** be trusted until the assembly is rebuilt. This runs even on a clean log, and when it coincides with errors the **headline itself** carries the may-predate caveat (the error block can come from an earlier compile — do not act on it as fact). Recovery: call `**unity_open_mcp_recompile_scripts`** (deterministic `CompilationPipeline.RequestScriptCompilation`, blocks until settled, reports `dllMtimeBefore`/`After` + `recompiled`), then re-read compile errors. Prefer it over `assets_refresh` / `execute_csharp` when you need a guaranteed recompile of edited C#.
105
+
106
+ **`status: "indeterminate"` (rotated-log read).** When `logSource` is a `prev_log_*` value (the resolver read the ROTATED `Editor-prev.log`) and the read finds no errors, `status` is `indeterminate`, **not** `no_errors_found` — a clean read from the rotated log is not a verified clean bill of health while a live Editor may be writing a different log. Confirm via the live bridge (`editor_status` / a live probe) or re-read after the Editor settles; the raw verdict is preserved in `logVerdict`.
107
+
108
+ **Stale-domain guard (`_staleDomain`).** `execute_csharp` / `invoke_method` responses carry `_staleDomain: { hint, newerSources, dllMtimeMs }` (additive, on a successful non-error body) when an `Assets/**/*.cs` source is newer than the newest built DLL — the snippet ran against the **pre-edit** assembly, so its results reflect code that has not been loaded. This is the runtime-facing twin of the `staleAssembly` read guard: same mtime check, attached to the two tools where a stale domain silently produces wrong answers. The signal is promoted to a top-level `staleAssembly: true` + `warning` on the same body so it cannot be skimmed past next to a `mutation.success: true`. Treat any such result as suspect: call `**unity_open_mcp_recompile_scripts`**, then re-invoke. The signal is cached short-TTL and refreshed on dead-bridge/compile-settle, so it tracks a genuine reload without re-scanning on every call.
109
+
110
+ **`editor_build_wedged`.** When the assembly is stale **and** the freshest `Editor.log` carries the `editor_fd_exhaustion` signature, `execute_csharp` / `invoke_method` **fail** with `error.code = "editor_build_wedged"` instead of returning a successful-looking result. The build driver is dead: your edits are on disk but were never compiled, and the snippet ran against the previous assembly. A recompile will **not** clear this — save scene work and restart the Editor, then discard any conclusion drawn from results returned since it wedged.
111
+
112
+ **Log-authorship guard.** The response carries `logUnityVersion`, `logBatchMode`, and `liveUnityVersion` (from the instance lock) when the log header was parseable. On `logAuthorshipMismatch: true`, a **different Unity** (often a `-batchmode` run of a newer version) wrote the log — its errors (e.g. an API deprecation that is only an error on that version) **cannot all occur** in the live editor. Do **not** "fix" those files or refuse to continue on a `compile_failed` from a mismatched log; the inverse of the stale-log trap is just as dangerous. Confirm live health via `bridge_status` (`unityVersion`) instead.
113
+
114
+ ## Editor fd-exhaustion recovery (Bee build-driver hang)
115
+
116
+ Long Editor sessions with many domain reloads leak file-descriptor numbers until one crosses Mono's internal ~1024 ceiling, and Mono's IOSelector refuses to register it — `System.NotSupportedException: Could not register to wait for file descriptor N` thrown from the Bee build driver. The Editor **hangs mid-build and never recovers on its own**; there is no C# error to fix. The bridge dies with it. Three always-visible local tools cover this failure mode:
117
+
118
+ - **Diagnose (reactive):** `unity_open_mcp_read_compile_errors` surfaces an `editor_fd_exhaustion` issue kind (with restart hint) from the same `Editor.log` tail that carries CSxxxx errors.
119
+ - **Health (reactive):** `unity_open_mcp_bridge_status` folds the same log scan in and reports `status: "wedged"` with `wedged.reason = "editor_fd_exhaustion"` + a non-null `recoveryHint`. A `running` / `healthy` result means *the listener answers*, **not** *the Editor can compile* — process, heartbeat and `/ping` all survive a dead build driver, which is why the scan exists.
120
+ - **Kill (reactive):** `unity_open_mcp_restart_editor` with `confirm: true` terminates the hung Unity (SIGTERM → SIGKILL on macOS/Linux; `taskkill /T /F` on Windows). It refuses when the `editor_fd_exhaustion` signature is absent — never restart on a fixable compile failure. When the resolved `Editor.log` does not carry the signature **but the bridge is still reachable**, it falls back to scanning the live Unity console (`unity_senses_read_console`) — the Bee driver exception is raised into the console independently of whichever log file the resolver picked; the response reports `signatureSource: "editor_log"` or `"live_console"` so you can tell which confirmed it. **Relaunch is NOT automatic** (the Hub owns the interactive-Editor launch recipe); the response tells the operator to relaunch via the Hub, then poll `bridge_status` until `running`.
121
+ - **Predict (proactive):** `unity_open_mcp_resource_pressure` samples the live Unity process's fd usage and reports headroom against the Mono fd ceiling (default ~1024, NOT the OS soft limit, which is misleading for a GUI-launched Unity on macOS). Returns `state` (`ok` / `warn` at ≥80% / `critical` at ≥90% / `over_ceiling` / `unknown`), a `trend` (`stable`/`rising`/`leaking` — a monotonic climb across successive samples is the leak signature), `ceiling` + `ceilingSource` (`"default"` vs `"config"`), and the session-scoped sample ring. Sample after heavy automation to catch fd growth before the Editor hangs. **`over_ceiling`:** with a real fd count (`fdMethod` lsof/proc — only numbered descriptors are counted, never mmap/txt rows) this IS an alarm: at/past the ceiling every NEW descriptor number lands above it and the next Mono IOSelector registration can hang the Editor, so the response carries a critical-level `warning` (a fresh healthy Unity 6 editor sits at ~160 fds). The one soft case is a Windows `HandleCount` probe (handles are far broader than fds — an over-ceiling handle count carries an informational `pressureNote`, and only a `leaking` trend alarms there); a timed-out `lsof` (`partial: true`) is *not* soft — its count is a lower bound on real fds, so the normal thresholds apply. `execute_csharp` responses also carry an in-band fd-pressure advisory in `agentNextSteps` once the Editor passes 80% of the ceiling (same configured ceiling, so the two surfaces never contradict). Override the ceiling for a runtime whose internal limit differs from Mono's 1024 (e.g. Unity 6 / CoreCLR) via `.unity-open-mcp/settings.json` (`resourcePressure.fdCeiling`).
122
+
123
+ **Workflow.** After heavy automation, call `resource_pressure`. When the response carries a `warning` (`state` warn/critical, a real-fd `over_ceiling`, or a `leaking` trend), surface the risk to the operator (recommend saving scene work + planning a restart) — do NOT call `restart_editor` yet (the Editor is still healthy). A domain reload (e.g. `recompile_scripts`) releases leaked descriptors, so a recompile is a valid pressure-relief step while work continues. Only a Windows HandleCount over the proxy ceiling is soft (a `pressureNote`) — treat it as a concern when the trend becomes `leaking`. Once `read_compile_errors` reports an actual `editor_fd_exhaustion` issue (the Editor is hung), call `restart_editor` with `confirm: true`, then tell the operator to relaunch via the Hub. Both tools act on the OS process — neither depends on the bridge being reachable. `headless_run_in_progress` from either tool (or a `headlessRun` block on a `stopped` `bridge_status`) means the only Unity for the project is this server's own headless run: wait for that pending call to return (it exits by itself when done or at `timeoutMs`), and do not relaunch Unity via the Hub meanwhile.
124
+
125
+ Use `**unity_open_mcp_compile_check**` only for a deliberate "does this build clean from scratch?" check — it spawns a fresh headless Unity and **always** routes to batch. It is not first-line recovery for a broken bridge assembly. When a live Editor already has the project open, the headless spawn cannot acquire Unity's one-Editor-per-project lock and returns `editor_instance_locked` — either close the live Editor and retry, or verify compile state via the live bridge instead (`execute_csharp` + `Library/ScriptAssemblies/*.dll` mtime check, or `read_compile_errors`).
126
+
127
+ ## Batch fallback / Unity discovery
128
+
129
+ `compile_check` is always batch. Other batch-capable tools fall back to batch only when the live bridge is down. The MCP server auto-discovers Unity from OS-default Hub install paths (macOS `/Applications/Unity/Hub/Editor`, Windows `C:\Program Files\Unity\Hub\Editor`, Linux `~/Unity/Hub/Editor`) plus the `UNITY_HUB` override; picks the newest unless the lock records a `unityVersion`, then matches that minor line. Explicit env vars:
130
+
131
+ - `UNITY_PATH` — **optional**, override auto-discovered Unity executable (highest priority).
132
+ - `UNITY_PROJECT_PATH` — absolute project root (optional when a lock exists).
133
+ - `UNITY_HUB` — **optional**, override Hub install root.
134
+
135
+ If Unity can't be found, batch tools return `unity_not_discovered`; only offline reads + `read_compile_errors` still work.
136
+
137
+ A headless failure before its report (`compile_failed`, `project_load_failed`, `compile_indeterminate`, `markers_missing`, `batch_aborted`) ends its message with `Last output: …` — the run's own Unity log. Read the diagnosis there (a package-resolution failure prints the Package Manager notice as its last lines); `read_compile_errors` cannot see a batch run.
138
+
139
+ > **In-Editor progress.** The Unity Open MCP bridge window's **Activity** tab has a **Batch runs** section — a read-only view of in-Editor batch runs (live progress: pending / running / done / failed; per-entry tool name, args summary, pass/fail, error text). It observes batch state; it does not start batches. Useful when an operator wants to watch a batch run from inside Unity.
140
+
141
+ ---
142
+
143
+ ### Local package source recompile caveat
144
+
145
+ Edits under `packages/` live **outside** Unity's `Assets/` watch root — Unity does not auto-detect them, and neither `assets_refresh` nor `RequestScriptCompilation()` reliably picks them up. If you skip this, you'll run tests against the stale DLL and conclude your fix failed when it was never compiled in.
146
+
147
+ After editing `packages/` source, before tests:
148
+
149
+ 1. Confirm the new source has no CS errors (only meaningful once Unity has seen it once — see [Safe Mode recovery](#step-4--safe-mode--bridge_compile_failed-recovery) if the bridge is already dead).
150
+ 2. Trigger the recompile via one of:
151
+ - **(a, recommended)** `reimport_package` with `package_id` — force-reimports the local package's source and reports `dllMtimeBefore`/`After` + a `recompiled` boolean, so a no-op recompile is detectable (on a no-op the response's `agentNextSteps` points at a standalone Roslyn-compile fallback). The package id is the scope; `paths_hint` is optional.
152
+ - **(b)** `package_add` / `package_remove` a no-op entry to force UPM resolution + domain reload, then revert it.
153
+ - **(c)** Ask the user to focus the Unity window after you touch a tracked `Assets/` file.
154
+ - **(d, if bridge up)** `execute_csharp` calling `AssetDatabase.ImportAsset("Packages/<pkg>/...", ImportAssetOptions.ForceUpdate)` then `CompilationPipeline.RequestScriptCompilation()`.
155
+ 3. **Verify the DLL actually rebuilt before tests:**
156
+ ```bash
157
+ stat -f "%Sm %N" Library/ScriptAssemblies/<assembly>.dll # macOS
158
+ ```
159
+ Compare mtime to your last edit; do not run tests until DLL mtime > edit mtime.
160
+
161
+ > **Stale-DLL trap:** if tests fail identically to before your fix, suspect the DLL never recompiled before concluding the fix is wrong. Two further pitfalls when the DLL mtime refuses to advance:
162
+ > - **`read_compile_errors` never shows a `compile_check` / batch run.** The headless run logs only to its captured output (returned as the error's `Last output` tail), not to `Editor.log` — so a subsequent `read_compile_errors` reads the live editor's or a previous session's log, and a "no errors" result there proves nothing about the batch run. Diagnose a batch failure from that tail.
163
+ > - **Stale DLL + `isCompiling:false` can be a crash, not a no-op.** If `RequestScriptCompilation` returns success but the `Library/ScriptAssemblies/*.dll` mtime never advances *and* `read_compile_errors` is clean, do **not** conclude "incremental no-op" — grep the live editor log for `Unhandled exception during build` / `Bee.BeeDriver` / `NotSupportedException` / `file descriptor`. A Bee build-backend crash (fd/socket exhaustion) aborts before emitting the DLL and leaves the editor idle with a stale DLL; it needs an editor restart to clear the broken state. `editor_status` does not distinguish "completed-ok" from "crashed."
@@ -0,0 +1,30 @@
1
+ # Discovery and groups
2
+
3
+ Use `capabilities(tool_name: "unity_open_mcp_component_modify")` for one full schema, example and lifecycle contract. Browse with `query`, `group`, `tag`, `available`, `active`, `route`, or `mutating`, plus `page_size` and `cursor`. Availability is null when the live inventory is unknown; batch-route tools instead report whether an editor for the project's Unity version resolves for a headless spawn.
4
+
5
+ Use `manage_tools(action: "activate_for", intent: "…")`; its `tools` list contains schemas for newly activated tools. If your client ignores list changes, use `manage_tools(action: "invoke", tool_name: "…", arguments: {...})`. This uses the target tool’s normal route and protections; recovery calls do not belong in a live batch.
6
+
7
+ Canonical locators are `asset_path`, `game_object_path`, `component_type`, and patch `property_path`. Deprecated aliases produce notes; do not mix an alias and its canonical key. Inspect exact schemas instead of guessing.
8
+
9
+ ## Version drift and updates
10
+
11
+ Use `unity-open-mcp status` to compare the running server and bridge versions.
12
+ `unity-open-mcp update --check` checks the MCP npm release only: exit `0` means
13
+ current, `10` means an update is available, and `11` means npm and the GitHub
14
+ fallback were unreachable or invalid. `unity-open-mcp update` can update a
15
+ global or project-local npm install; under `npx` it prints the pin to change.
16
+ It intentionally does **not** edit Unity package pins, client configuration, or
17
+ project prose. Move the Bridge and Verify packages together through the bridge
18
+ Updates UI when available, or use the setup/manual package path, then restart
19
+ the MCP client and confirm with `ping` / `status`.
20
+
21
+ For an already-installed open project, activate `typed-editor` and call
22
+ `unity_open_mcp_upgrade` with its default `dry_run: true`. Review every file and
23
+ skip reason, then repeat with `dry_run: false` only when requested. Home configs
24
+ are rewritten only when their ownership markers belong exclusively to this
25
+ project; embedded/`file:` bridge installs preserve their package pins. The tool
26
+ returns before a possible UPM reload, so poll `bridge_status` and re-ping after
27
+ Unity settles, then remind the operator to restart the MCP client.
28
+
29
+
30
+ For a human Search handoff, use `manage_tools(action: "editor_search", search_text: "Player", asset_type: "Prefab")`. It returns the query without UI changes; use `open_ui: true` only when opening/focusing the Search window is explicitly wanted (during a project job it returns `opened: false`, `reason: "job_busy"`; hand over the query instead). Keep structured investigation on `search_assets` / `find_references`.
@@ -0,0 +1,62 @@
1
+ # Routing and lifecycle
2
+
3
+ ## Lifecycle & scene safety
4
+
5
+
6
+ | Policy | Meaning | Tools |
7
+ | --------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
8
+ | `none` | Read-only, returns immediately | `ping`, `find_members`, `validate_edit`, `checkpoint_create`, `delta`, `find_references`, `dependencies`, `scan_paths`, `read_asset`, `search_assets`, `list_assets`, `editor_status`, `read_console`, `screenshot`, `profiler_*`, `spatial_query`, `visual_compare` |
9
+ | `editor_settle` | Mutating; bridge waits for asset refresh/serialization to finish | `apply_fix`, `reserialize` |
10
+ | `restart_then_settle` | Mutating; may trigger domain reload; bridge blocks until compile finishes (cap 60s) | `execute_csharp`, `invoke_method`, `execute_menu`, `compile_check` |
11
+ | `custom_confirmation` | Async; returns immediately, result via external completion signal you poll | `run_tests` |
12
+
13
+
14
+ > The table above is the bridge's settle-timing surface (how long it blocks before returning). For the **recovery-axis** taxonomy — `none` / `compile-reload` / `modal-dialog` / `scene-dirty` / `process-stale`, describing what to do when a call fails or stalls — read `capabilities.tools[].lifecycle` (+ `lifecycleNote`) per tool, and `capabilities.lifecycleBlock` for the full class table. See the docs §Lifecycle policy for the `compile-reload` agent limits (notably `compile_check` is batch-only and returns `editor_instance_locked` when a live Editor holds the project lock).
15
+
16
+ **Active-scene dirty guard.** Before any `restart_then_settle` op, the bridge preflights loaded scenes. If any scene has unsaved changes, the call refuses with `error.code = "scene_dirty"` + `dirtyScenes[]` + `agentNextSteps` so Unity's native save modal never interrupts. Recover by: saving first (`scene_save`), discarding (`EditorSceneManager.RestoreSavedSceneState()`), or passing `ignore_scene_dirty: true` on `execute_csharp` / `invoke_method` / `execute_menu` / `scene_create` / `scene_open` / `editor_set_state` / `build_set_target` / `build_set_defines` / `settings_set_player`.
17
+
18
+ `scene_create` / `scene_open` are guarded only in their default Single mode (it closes every open scene without saving); `mode: "additive"` keeps open scenes and skips the guard. `apply_fix`, `reserialize`, and the other scene mutators (`scene_save` / `scene_unload` / `scene_set_active` / `scene_focus` / `sceneview_set_camera`) are **not** guarded.
19
+
20
+ **Power-tool deny heuristic.** `execute_csharp` / `execute_menu` are blocked from destructive patterns by default (`EditorApplication.Exit`, `Application.Quit`, `AssetDatabase.DeleteAsset`, `BuildPipeline.BuildPlayer`, `File/Quit`, `TestRunnerApi`). Refused calls return `error.code = "denied_by_policy"` (csharp) or `"menu_blocked"` (menu) with the matched pattern + alternative. If you genuinely need one, set **both** `gate: "off"` and `confirm_bypass: true` — the bypass is audited. `execute_menu` also tells `menu_not_found` (no item at that path — fix the path) from `menu_disabled` (the item exists but its validate function rejected the current Editor state, e.g. nothing selected — change the selection/state, not the path).
21
+
22
+ **`execute_csharp` runs on Unity's main thread — never block it.** The snippet executes synchronously inline on `EditorApplication.update`, so any blocking primitive wedges the editor **unrecoverably**: `WaitOne` / `.Result` / `Thread.Sleep` / `while(!done)` waiting for a callback, and driving `TestRunnerApi` (which delivers its callbacks on the same main thread), all deadlock. The HTTP timeout fires on the worker thread and **cannot self-heal a stuck main thread** — no further editor tick runs, so the editor must be killed externally. The timeout envelope's `agentNextSteps` will tell you this; heed it (check `editor_status` / `bridge_status` before retrying, **don't** just raise `timeout_ms`). For test execution use `unity_senses_run_tests` (async, does not block). For everything else, prefer a typed tool or write the snippet to be non-blocking (fire-and-forget + poll via a follow-up call).
23
+
24
+ ## Routing rules
25
+
26
+ Treat `capabilities.routePolicy` + `batchCapable` as source of truth. `batchCapable` is about the **headless batch-spawn fallback** only (`capabilities.routing.perToolFlagMeaning` says so inline) — it does not decide what may be a nested `batch_execute` step.
27
+
28
+ - **Live is the default** — when the bridge is connected, most tools route to `POST /tools/{name}` on the Editor.
29
+ - **Batch fallback** — spawns headless Unity (`-batchmode`) **only** for `batchCapable: true` tools when the live bridge is unavailable. `find_members`, `execute_csharp`, `invoke_method`, and the allowed `execute_menu` subset can use this route; mutating power tools do not get the live gate/checkpoint flow in headless mode.
30
+ - **Exact Editor guard** — before a headless spawn, the server reads `ProjectSettings/ProjectVersion.txt` and requires that exact installed Unity. `unity_version_not_installed` / `unity_version_mismatch` mean nothing was spawned; install or select the exact Editor, then retry (no server restart needed). Use `UNITY_OPEN_MCP_ALLOW_VERSION_MISMATCH=1` only for an intentional upgrade or compatibility run because a different Unity can rewrite package/project metadata during load.
31
+ - **Senses are live-only** — `run_tests`, screenshots, profiler, console, spatial queries have no batch form.
32
+ - **Offline reads** (`list_assets`, `find_references`, `read_asset`, `search_assets`) parse the project from disk and never need Unity. Coverage: text-serialized Unity YAML (`.prefab`/`.unity`/`.asset`/`.mat`/`.controller`/`.anim`/`.playable`/`.preset`/`.spriteatlas`/`.terrainlayer`/`.vfx`) **plus** JSON assets not covered by YAML-only parsers (`.asmdef`, `.shadergraph`/`.shadersubgraph`). `read_asset` also reconstructs the full hierarchy offline, parses prefab-variant overrides (matching `prefab_get_overrides`), and surfaces `integrity[]` signals (missing refs, missing scripts, malformed JSON, orphaned prefab instances) on the response — act on them before a `validate_edit` round-trip.
33
+ - **`dependencies` is live-only** — it reuses the verify `Dependencies.Scanner` (forward) + `ReferenceGraph` (reverse), both of which call AssetDatabase. No offline form.
34
+ - **`compile_check` is always batch** — spawns a fresh headless Unity that recompiles from scratch, even when the live bridge is up.
35
+ - **Verify scans (`scan_all`, `baseline_create`, `regression_check`) are always batch** — they run in the headless verify package and are not registered on the live bridge, so even with the bridge up they spawn fresh Unity (`_route.fallbackReason: "verify_always_batch"`). Expect `editor_instance_locked` when a live Editor holds the project lock — close it and retry, or use the live `validate_edit` / `scan_paths` for a scoped health check instead.
36
+
37
+
38
+ Snippets import `Object = UnityEngine.Object` automatically unless the caller supplies an Object alias. Snippets run in a separate assembly and can access public APIs. Use explicit reflection for inspection of internals; assembly-access bypass is not supported.
39
+
40
+ ## Session-owned jobs
41
+
42
+ `unity_open_mcp_jobs` stays local and visible while Unity reloads or disconnects.
43
+ Use it only when an operation has an explicit job adapter; declarations alone do
44
+ not enable a job. `wait` observes for at most 30 seconds without owning execution.
45
+ A lost connection is not proof of failure: `orphaned` requires operation-specific
46
+ evidence before any retry. A `running` job with lifecycle `disconnected` is still
47
+ being observed through a busy or unreachable Editor; keep waiting.
48
+ `cancel_requested` is not cancellation confirmation.
49
+ Terminal records and idempotency keys survive for 30 minutes in the same server
50
+ session, but not across a server restart. Keep routing and agent metadata stable.
51
+ While a project job runs, conflicting calls return `job_busy` with `error.job`
52
+ (`jobId`, `commandId`, `phase`) and `agentNextSteps`. `execute_csharp` is refused
53
+ even with `read_only: true`: the bridge cannot verify a snippet writes nothing.
54
+ The starting agent waits with that `jobId` (others get `job_not_found`); use typed
55
+ reads meanwhile and retry once after a terminal state, never in a loop.
56
+
57
+
58
+ For project commands, exact describe precedes invoke/start. After async start,
59
+ call status and bounded wait; inspect terminal gate results even on failure or
60
+ cancellation. A job does not weaken scope, deny or dirty-scene policy. Never use
61
+ reflection to bypass an unavailable declaration. Builds/imports/packages/bakes
62
+ are unsupported job targets.
@@ -0,0 +1,41 @@
1
+ # Senses and tests
2
+
3
+ ## Agent senses (live-only, no batch fallback)
4
+
5
+ - `**unity_senses_run_tests`** — EditMode + PlayMode test runner with per-test pass/fail. Filter by assembly / namespace / class / method. Set `include_passes: false` on large suites to avoid truncation. **Never fire a second `run_tests` before the first resolves** (no concurrency guard; results interleave). Results are retrieved by the server polling a results file (NOT via `unity_senses_pull_events`). A run that is superseded by a newer run, or aborted when play mode is entered mid-EditMode-run, writes a terminal file with `status:"aborted"` + `reason` (`superseded_by_run` | `playmode_entered`) — the poll resolves to that instead of timing out.
6
+ - **Zero-match looks like "no results".** When the filter matches zero tests, `TestRunnerApi.Execute` runs nothing and the result is `status:"completed"` with `summary.total:0` and a `note` — that is a no-match filter, not a failure. `test_class` maps to NUnit `groupNames` (a partial/group match), and `assembly_name` must name a real test assembly.
7
+ - **Single-test workaround.** To run one test method deterministically (and get NUnit assertion failures verbatim), invoke the test class/method directly via `unity_open_mcp_invoke_method` instead of `run_tests`.
8
+ - **Brief reloading window after a run.** A test run (PlayMode especially) can trigger a Unity domain reload that freezes the bridge's heartbeat for a few seconds. The MCP server recognizes an in-flight run and treats that window as a normal reload rather than a dead bridge, so subsequent tools keep waiting instead of failing fast with `bridge_compile_failed`. For robustness, after `run_tests` returns `{status:"started", runId}`, poll the result file (or call `ping` / `editor_status`) once before issuing other tools — the run itself is async and the bridge may still be settling.
9
+ - **Headless / CI alternative.** `run_tests` has no MCP batch form (it needs a live Editor). For headless CI, invoke Unity's CLI directly with `-runTests`. **Omit `-quit`** — with `-quit` the editor exits after the initial asset refresh, *before* the test runner starts, so no tests run and no XML is written despite an exit code of 0. The correct invocation:
10
+ ```
11
+ Unity -batchmode -projectPath <project> -runTests -testPlatform EditMode \
12
+ -testResults out.xml -logFile run.log -nographics
13
+ ```
14
+ Exit codes: `0` = all tests passed, `2` = test failures present (NOT a process error — do not treat 2 as a crash), `3` = test runner setup failure. Known **batchmode-only** red tests that pass in the GUI editor — don't try to "fix" these, validate them in the GUI editor instead: in-process HTTP-server lifecycle (socket-disposed), GPU/SceneView (no graphic device under `-nographics`), Unity-6's removed `LogEntries.GetEntries(List<LogEntry>)` internal API (console/capture), MSBuild/Roslyn `Microsoft.Build.Utilities.Core` host assembly load, and TimeManager SerializedProperty float access.
15
+ - `**unity_senses_read_console`** — console entries via reflection. Filter `type: "error"` to confirm clean compile. `detail: "summary"` for messages only (saves tokens); `detail: "verbose"` includes Unity-internal frames.
16
+ - `**unity_senses_screenshot**` — Scene / Game / **composed** / isolated 2×2 composite of one GameObject. `view: "game"` renders `Camera.main` **only** — Screen-Space Overlay UI and multi-camera compositions are NOT in the frame; use `view: "composed"` to verify UI edits (the full player frame: all cameras by depth + Overlay canvases).
17
+ - `**unity_senses_screenshot_camera**` — render from an arbitrary world-space pose (position + rotation + fov) without moving the scene/game camera; transient camera, scene camera untouched.
18
+ - `**unity_senses_capture_inline**` — same targets as `screenshot` but returns the PNG as an inline base64 image (no temp file) for agents that don't read the filesystem.
19
+ - `**unity_senses_visual_compare**` — visual regression: capture a named reference snapshot (`action: save`) from scene / game / isolated view, then `action: compare` a later capture against it to check whether an edit visually broke anything. Returns `pixelDiffPercent`, `mismatchedPixels`, `perceptualDistance` (8×8 average-hash Hamming distance — tolerates minor anti-aliasing drift), and `match` (true when `pixelDiffPercent <= sensitivity * 100`; default `sensitivity: 0.01` = 1%). On a non-zero diff, returns an inline diff image (mismatched pixels highlighted red over the current frame). References persist under `~/.unity-open-mcp/screenshots/references/`; `action: list` / `delete` manage them. The `compare` resamples the current capture to the reference dimensions if they differ, so the diff is resolution-stable.
20
+ - `**unity_senses_screenshot_window**` — capture an Editor window (Console / Hierarchy / Inspector / Project / Scene / Game / custom). **Windows-only**: on macOS/Linux returns `not_supported` (the editor-window backbuffer is not bound during bridge dispatch, so a readback produces a blank frame) — use the OS screen-capture tool, or `screenshot {view:"composed"}` for the rendered Game view. On Windows, a blank/solid-color frame is rejected with `blank_capture` rather than reported as `ok`.
21
+ - `**unity_senses_frame_debugger**` — control Unity's Frame Debugger: `action: enable` opens the window and starts capturing, `action: disable` stops it, `action: list` returns the draw-call list (shader / pass / material / render target / vertex/index/instance counts per call). Read-only (gate off); the response reports `windowOpened` so you know Editor UI may have changed. Enable, render a frame, then call `list` to inspect draw calls.
22
+ - `**unity_senses_profiler_capture**` / `profiler_memory` / `profiler_rendering` — frame hierarchy, memory allocators, rendering env.
23
+ - `**unity_senses_profiler_capture_frame`** — single-frame deep profiler capture. Returns one (or a few, via `frame_count`) frame's full sample tree, optionally filtered by `modules` (Profiler category names, e.g. "CPU,Rendering"). Deeper than `profiler_capture`; bound output with `max_depth` (default 8) and `max_items` (default 200). If the Profiler is off, the tool enables it for the capture, then restores it to disabled once data is captured; `profilerWasEnabled` reports the PRIOR state. On a cold capture (Profiler was off and no frame is recorded yet) the tool returns `status:"warming_up"` instead of blocking — it leaves the Profiler enabled, so re-issue the call shortly to receive the frame data.
24
+ - `**unity_senses_spatial_query**` — physics-based raycast / overlap / bounds / ground_check / nearest against the live scene.
25
+ - `**unity_senses_pull_events**` — incremental console logs + editor-state transitions (compile start/stop, play-mode). Cheaper than `read_console` for "what happened since my last call"; first call opens the stream, later calls return only new events. Returns `bridge_unavailable` when the bridge is down. The stream self-heals across Unity restarts and domain reloads (the server re-reads the bridge port+token from the instance lock on every reconnect, backing off 2s→30s), so after a reload keep polling instead of assuming the stream is dead.
26
+
27
+ **Verification habit:** after any C# change, run `read_console` with `type: "error"` (or `run_tests` on the affected assembly) to confirm compile + tests pass before declaring done.
28
+
29
+ ### Return serialization (execute_csharp / invoke_method)
30
+
31
+ Results are walked by a depth-limited reflective serializer before becoming `mutation.output`:
32
+
33
+ - Structs/POCOs → JSON objects with public fields/props (`return new Vector3(1,2,3)` → `{"$type":"Vector3","x":1,"y":2,"z":3}`).
34
+ - Lists truncate to 100 items (`max_items` configurable); truncated arrays report `{"items":[...],"truncated":N}`.
35
+ - Recursion caps at depth 4 (`max_depth` configurable).
36
+ - Cycles / `UnityEngine.Object` refs never infinite-loop — back-edges become `{"$ref":"TypeName"}`, Unity objects become `{"$type":...,"name":...,"instanceId":...}`.
37
+ - **Malformed output is rejected, not trusted.** The per-member walk is try/catch'd, but an exception can still escape mid-walk (e.g. a `TypeLoadException` when a field references a missing assembly), leaving unbalanced JSON. `execute_csharp` validates the serialized output before it enters the envelope; if it isn't a balanced JSON object the call fails with `error.code = "execution_error"` and a diagnostic naming the return type. On the rare chance the body is still corrupted in transit, the MCP server surfaces `bridge_response_unparsable` instead of a fake success — either way you'll never see a `_compileVerify`-annotated result missing its `mutation` block.
38
+
39
+ ### `execute_csharp` read-only probe flag (`read_only`)
40
+
41
+ `execute_csharp` defaults to mutating and requires a non-empty `paths_hint`. For pure inspection, pass `read_only: true`: no scope, checkpoint, or validation is needed, even when the scene is dirty or a hint was supplied. This is a caller assertion, not a sandbox: never use it for writes or disruptive lifecycle transitions. Known compile/refresh, scene-switch, and play-transition calls retain protection, and the deny heuristic still applies. See the request and batch guidance below.
@@ -0,0 +1,33 @@
1
+ # YAML and offline work
2
+
3
+ ### Reserialize after direct YAML edits
4
+
5
+ When you edit `.prefab` / `.unity` / `.asset` / `.mat` / `.controller` / `.anim` directly as YAML text, run `**unity_open_mcp_reserialize`** with the touched `paths` (the `paths` array doubles as gate scope). Round-trip rewrites canonically so missing fields, wrong indentation, and stale `fileID` references surface in `gate.delta`. Whole-project reserialize is intentionally unsupported — enumerate assets you edited. Default targets asset YAML only (no `.meta` diff on body-only edits); pass `include_meta: true` only for upgrade/importer-change workflows.
6
+
7
+ Prefer a typed live tool while the Editor is reachable. Never directly edit an open or dirty scene or asset YAML. If direct editing is necessary, explicitly identify the offline-file route and confirm the target is not open/dirty. Offline work remains supported when the Editor is unavailable or the user chooses file-level repair. Preserve GUID/fileID relationships and verify the intended scene: wrong-scene edits can silently damage a different asset. Reimport/reserialize the touched files and run scoped validation before trusting the result.
8
+
9
+ ### read_asset: map, not dump
10
+
11
+ Raw Unity YAML is enormous. `read_asset` returns counts, a `cmp` table declaring repeated component sets once (referenced by `c1`/`c2` codes), and a folded `tree`. Drill down with `field_limit` + `component` / `path` / `profile=full` (or `detail=verbose`) instead of re-reading raw YAML. Session cache reuses the parsed model (`_cache: "hit"`). `profile: full` disables render-only folding; `field_limit: 0` (default) returns names only — bump it before `component` drill-down so fields are available. `page_size`/`cursor` page the `tree` rows.
12
+
13
+ Use `**search_assets`** to locate prefabs/components/GUIDs; each result tags *why* it matched so you know which `read_asset` drill-down to run next.
14
+
15
+ ### Reads that save tokens
16
+
17
+ Raw Unity data is large. Prefer the cheap, structured reads before reaching for verbose output:
18
+
19
+ - **`read_asset`** returns a folded `tree` + `cmp` table + counts, not raw YAML. Drill into a subtree with `component` / `path` + `field_limit` instead of re-reading the whole asset; the parsed model is session-cached (`_cache: "hit"`). `field_limit: 0` (default) returns field names only — bump it only for a `component` drill-down where you need values.
20
+ - **`manage_tools(action="list_groups")`** — sessions start with two default-on groups (`core` + `gate-and-verify`); activate only the other group you need so the full **250+ tool** surface stays out of the prompt. Prefer `action="activate_for"` with a free-text `intent` (or `action="suggest"` to preview) over hand-picking ids — it brings the right groups online in one call.
21
+ - **`read_console`** with `detail: "summary"` returns messages only; reserve `detail: "verbose"` for when you need Unity-internal stack frames.
22
+ - **`search_assets`** tags *why* each result matched, so you skip broad reads and go straight to the right drill-down.
23
+ - **Output profiles + paging.** The heavy tools (`read_asset`, `search_assets`, `scene_get_data`, `find_references`, `validate_edit`, `scan_paths`, `component_get`) take `profile: compact|balanced|full` (`compact` is the default — the cheap folded/counts shape). Raise to `balanced`/`full` to expand. Page large results with `page_size` + the `pagination.next_cursor` token (omit `page_size` to get the whole payload at once). For `component_get`, start compact then use `property_path` or paging to expand one subtree. For `find_references` / `validate_edit` / `scan_paths`, the compact default returns counts/groupings only — pass `profile: "balanced"` to get the per-asset / per-issue lists those tools previously returned by default. (The legacy `detail` / `max_results` / `max_nodes` caps remain as aliases.)
24
+ - **`capabilities`** before assuming tool names/schemas/route policy — cheaper than discovering a tool's real signature by trial and error.
25
+ - **Prefer typed tools** (`component_modify`, `scene_get_data`, …) over `execute_csharp` / `invoke_method` — explicit schemas, smaller request/response envelopes, same gate.
26
+
27
+ ### find_references before delete
28
+
29
+ Before deleting or moving an asset, call `**find_references*`* to see who depends on it. Offline-first (no live bridge needed for text-serialized assets).
30
+
31
+ ### dependencies: both directions in one call
32
+
33
+ When you need **both** directions (what this asset depends on AND what depends on it), or the broken-edge / cycle view, call `**dependencies*`*. It returns the forward + reverse edge sets plus the same `broken_dependency` GUIDs and `dependency_cycle` trails the `dependencies` verify rule computes — no second dependency graph is built (it reuses the verify scanner + the `find_references` reverse walker). Live bridge only (the scanners call AssetDatabase); pass `detail: "summary"` for counts only when you just need the magnitude.
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+ # unity-open-mcp MCP wrapper.
3
+ #
4
+ # Resolves UNITY_PROJECT_PATH from THIS SCRIPT'S location, so it works no
5
+ # matter what working directory the AI client spawns the MCP server in. Use it
6
+ # for clients that expand neither ${workspaceFolder} nor environment variables
7
+ # in their MCP config (for example Codex and ZCode).
8
+ #
9
+ # Committed to the repository; contains no machine-specific paths.
10
+ # Regenerate with: unity-open-mcp setup --wrapper …
11
+ #
12
+ # Override the Unity subfolder for a one-off run:
13
+ # UNITY_SUBPATH=OtherClient bash <this script>
14
+ set -euo pipefail
15
+
16
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
+ workspace_root="$(cd "${script_dir}/__WORKSPACE_FROM_SCRIPT__" && pwd)"
18
+ subpath="${UNITY_SUBPATH-__UNITY_SUBPATH__}"
19
+
20
+ if [ -n "${subpath}" ]; then
21
+ project_path="${workspace_root}/${subpath}"
22
+ else
23
+ project_path="${workspace_root}"
24
+ fi
25
+
26
+ for marker in __UNITY_ROOT_MARKERS__; do
27
+ if [ ! -d "${project_path}/${marker}" ]; then
28
+ echo "unity-open-mcp: ${project_path} is not a Unity project root (missing ${marker}/)." >&2
29
+ echo "unity-open-mcp: set UNITY_SUBPATH to the Unity folder relative to ${workspace_root}." >&2
30
+ exit 1
31
+ fi
32
+ done
33
+
34
+ export UNITY_PROJECT_PATH="${project_path}"
35
+ exec npx -y "unity-open-mcp@__UNITY_OPEN_MCP_VERSION__" "$@"
@@ -0,0 +1,8 @@
1
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ type Schema = Record<string, any>;
3
+ /** Locator names are explicit in the published contract; aliases retain their wire key. */
4
+ export declare function canonicalSchema(name: string, input: Schema): Tool["inputSchema"];
5
+ /** Validate before dispatch, including nested records; arbitrary value payloads stay opaque. */
6
+ export declare function validateSchema(value: any, schema: Schema, path?: string): string[];
7
+ export declare function wireArguments(value: any, schema: Schema, notes?: string[]): any;
8
+ export {};