diff --git a/.claude/skills/unity-mcp-skill/SKILL.md b/.claude/skills/unity-mcp-skill/SKILL.md index a48deee70..032aaaff3 100644 --- a/.claude/skills/unity-mcp-skill/SKILL.md +++ b/.claude/skills/unity-mcp-skill/SKILL.md @@ -24,20 +24,26 @@ Before applying a template: 2. Understand the scene → mcpforunity://scene/gameobject-api 3. Find what you need → find_gameobjects or resources 4. Take action → tools (manage_gameobject, create_script, script_apply_edits, apply_text_edits, validate_script, delete_script, get_sha, etc.) -5. Verify results → read_console, capture_screenshot (in manage_scene), resources +5. Verify results → read_console, manage_camera(action="screenshot"), resources ``` ## Critical Best Practices -### 1. After Writing/Editing Scripts: Always Refresh and Check Console +### 1. After Writing/Editing Scripts: Wait for Compilation and Check Console ```python # After create_script or script_apply_edits: -refresh_unity(mode="force", scope="scripts", compile="request", wait_for_ready=True) +# Both tools already trigger AssetDatabase.ImportAsset + RequestScriptCompilation automatically. +# No need to call refresh_unity — just wait for compilation to finish, then check console. + +# 1. Poll editor state until compilation completes +# Read mcpforunity://editor/state → wait until is_compiling == false + +# 2. Check for compilation errors read_console(types=["error"], count=10, include_stacktrace=True) ``` -**Why:** Unity must compile scripts before they're usable. Compilation errors block all tool execution. +**Why:** Unity must compile scripts before they're usable. `create_script` and `script_apply_edits` already trigger import and compilation automatically — calling `refresh_unity` afterward is redundant. ### 2. Use `batch_execute` for Multiple Operations @@ -55,26 +61,35 @@ batch_execute( **Max 25 commands per batch by default (configurable in Unity MCP Tools window, max 100).** Use `fail_fast=True` for dependent operations. +**Tip:** Also use `batch_execute` for discovery — batch multiple `find_gameobjects` calls instead of calling them one at a time: +```python +batch_execute(commands=[ + {"tool": "find_gameobjects", "params": {"search_term": "Camera", "search_method": "by_component"}}, + {"tool": "find_gameobjects", "params": {"search_term": "Player", "search_method": "by_tag"}}, + {"tool": "find_gameobjects", "params": {"search_term": "GameManager", "search_method": "by_name"}} +]) +``` + ### 3. Use Screenshots to Verify Visual Results ```python # Basic screenshot (saves to Assets/, returns file path only) -manage_scene(action="screenshot") +manage_camera(action="screenshot") # Inline screenshot (returns base64 PNG directly to the AI) -manage_scene(action="screenshot", include_image=True) +manage_camera(action="screenshot", include_image=True) # Use a specific camera and cap resolution for smaller payloads -manage_scene(action="screenshot", camera="MainCamera", include_image=True, max_resolution=512) +manage_camera(action="screenshot", camera="MainCamera", include_image=True, max_resolution=512) # Batch surround: captures front/back/left/right/top/bird_eye around the scene -manage_scene(action="screenshot", batch="surround", max_resolution=256) +manage_camera(action="screenshot", batch="surround", max_resolution=256) # Batch surround centered on a specific object -manage_scene(action="screenshot", batch="surround", look_at="Player", max_resolution=256) +manage_camera(action="screenshot", batch="surround", look_at="Player", max_resolution=256) # Positioned screenshot: place a temp camera and capture in one call -manage_scene(action="screenshot", look_at="Player", view_position=[0, 10, -10], max_resolution=512) +manage_camera(action="screenshot", look_at="Player", view_position=[0, 10, -10], max_resolution=512) ``` **Best practices for AI scene understanding:** @@ -87,10 +102,10 @@ manage_scene(action="screenshot", look_at="Player", view_position=[0, 10, -10], ```python # Agentic camera loop: point, shoot, analyze manage_gameobject(action="look_at", target="MainCamera", look_at_target="Player") -manage_scene(action="screenshot", camera="MainCamera", include_image=True, max_resolution=512) +manage_camera(action="screenshot", camera="MainCamera", include_image=True, max_resolution=512) # → Analyze image, decide next action -# Alternative: use manage_camera for screenshot (same underlying infrastructure) +# Screenshot from a different camera manage_camera(action="screenshot", camera="FollowCam", include_image=True, max_resolution=512) manage_camera(action="screenshot_multiview", max_resolution=480) # 6-angle contact sheet ``` @@ -157,14 +172,14 @@ uri="file:///full/path/to/file.cs" |----------|-----------|---------| | **Scene** | `manage_scene`, `find_gameobjects` | Scene operations, finding objects | | **Objects** | `manage_gameobject`, `manage_components` | Creating/modifying GameObjects | -| **Scripts** | `create_script`, `script_apply_edits`, `refresh_unity` | C# code management | -| **Assets** | `manage_asset`, `manage_prefabs` | Asset operations | +| **Scripts** | `create_script`, `script_apply_edits`, `validate_script` | C# code management (auto-refreshes on create/edit) | +| **Assets** | `manage_asset`, `manage_prefabs` | Asset operations. **Prefab instantiation** is done via `manage_gameobject(action="create", prefab_path="...")`, not `manage_prefabs`. | | **Editor** | `manage_editor`, `execute_menu_item`, `read_console` | Editor control, package deployment (`deploy_package`/`restore_package` actions) | | **Testing** | `run_tests`, `get_test_job` | Unity Test Framework | | **Batch** | `batch_execute` | Parallel/bulk operations | | **Camera** | `manage_camera` | Camera management (Unity Camera + Cinemachine). **Tier 1** (always available): create, target, lens, priority, list, screenshot. **Tier 2** (requires `com.unity.cinemachine`): brain, body/aim/noise pipeline, extensions, blending, force/release. 7 presets: follow, third_person, freelook, dolly, static, top_down, side_scroller. Resource: `mcpforunity://scene/cameras`. Use `ping` to check Cinemachine availability. See [tools-reference.md](references/tools-reference.md#camera-tools). | | **Graphics** | `manage_graphics` | Rendering and post-processing management. 33 actions across 5 groups: **Volume** (create/configure volumes and effects, URP/HDRP), **Bake** (lightmaps, light probes, reflection probes, Edit mode only), **Stats** (draw calls, batches, memory), **Pipeline** (quality levels, pipeline settings), **Features** (URP renderer features: add, remove, toggle, reorder). Resources: `mcpforunity://scene/volumes`, `mcpforunity://rendering/stats`, `mcpforunity://pipeline/renderer-features`. Use `ping` to check pipeline status. See [tools-reference.md](references/tools-reference.md#graphics-tools). | -| **Packages** | `query_packages` (read-only), `manage_packages` (mutating) | Install, remove, search, and manage Unity packages and scoped registries. `query_packages`: list installed, search registry, get info, ping, poll status. `manage_packages`: add/remove packages, embed for editing, add/remove scoped registries, force resolve. Validates identifiers, warns on git URLs, checks dependents before removal (`force=true` to override). See [tools-reference.md](references/tools-reference.md#package-tools). | +| **Packages** | `manage_packages` | Install, remove, search, and manage Unity packages and scoped registries. Query actions: list installed, search registry, get info, ping, poll status. Mutating actions: add/remove packages, embed for editing, add/remove scoped registries, force resolve. Validates identifiers, warns on git URLs, checks dependents before removal (`force=true` to override). See [tools-reference.md](references/tools-reference.md#package-tools). | | **ProBuilder** | `manage_probuilder` | 3D modeling, mesh editing, complex geometry. **When `com.unity.probuilder` is installed, prefer ProBuilder shapes over primitive GameObjects** for editable geometry, multi-material faces, or complex shapes. Supports 12 shape types, face/edge/vertex editing, smoothing, and per-face materials. See [ProBuilder Guide](references/probuilder-guide.md). | | **UI** | `manage_ui`, `batch_execute` with `manage_gameobject` + `manage_components` | **UI Toolkit**: Use `manage_ui` to create UXML/USS files, attach UIDocument, inspect visual trees. **uGUI (Canvas)**: Use `batch_execute` for Canvas, Panel, Button, Text, Slider, Toggle, Input Field. **Read `mcpforunity://project/info` first** to detect uGUI/TMP/Input System/UI Toolkit availability. (see [UI workflows](references/workflows.md#ui-creation-workflows)) | @@ -173,14 +188,14 @@ uri="file:///full/path/to/file.cs" ### Creating a New Script and Using It ```python -# 1. Create the script +# 1. Create the script (automatically triggers import + compilation) create_script( path="Assets/Scripts/PlayerController.cs", contents="using UnityEngine;\n\npublic class PlayerController : MonoBehaviour\n{\n void Update() { }\n}" ) -# 2. CRITICAL: Refresh and wait for compilation -refresh_unity(mode="force", scope="scripts", compile="request", wait_for_ready=True) +# 2. Wait for compilation to finish +# Read mcpforunity://editor/state → wait until is_compiling == false # 3. Check for compilation errors read_console(types=["error"], count=10) diff --git a/.claude/skills/unity-mcp-skill/references/tools-reference.md b/.claude/skills/unity-mcp-skill/references/tools-reference.md index 78db38b32..a052fc9f5 100644 --- a/.claude/skills/unity-mcp-skill/references/tools-reference.md +++ b/.claude/skills/unity-mcp-skill/references/tools-reference.md @@ -16,6 +16,8 @@ Complete reference for all MCP tools. Each tool includes parameters, types, and - [Editor Control Tools](#editor-control-tools) - [Testing Tools](#testing-tools) - [Camera Tools](#camera-tools) +- [Graphics Tools](#graphics-tools) +- [Package Tools](#package-tools) - [ProBuilder Tools](#probuilder-tools) --- @@ -112,7 +114,7 @@ manage_scene( ) # Screenshot (file only — saves to Assets/Screenshots/) -manage_scene(action="screenshot") +manage_camera(action="screenshot") # Screenshot with inline image (base64 PNG returned to AI) manage_scene( @@ -211,6 +213,17 @@ manage_gameobject( prefab_path="Assets/Prefabs/MyCube.prefab" ) +# Prefab instantiation — place a prefab instance in the scene +manage_gameobject( + action="create", + name="Enemy_1", + prefab_path="Assets/Prefabs/Enemy.prefab", + position=[5, 0, 3], + parent="Enemies" # optional parent GameObject +) +# Smart lookup — just the prefab name works too: +manage_gameobject(action="create", name="Enemy_2", prefab_path="Enemy", position=[10, 0, 3]) + # Modify manage_gameobject( action="modify", @@ -691,8 +704,16 @@ manage_editor(action="remove_tag", tag_name="OldTag") manage_editor(action="add_layer", layer_name="Projectiles") manage_editor(action="remove_layer", layer_name="OldLayer") + +manage_editor(action="close_prefab_stage") # Exit prefab editing mode back to main scene + +# Package deployment (no confirmation dialog — designed for LLM-driven iteration) +manage_editor(action="deploy_package") # Copy configured MCPForUnity source into installed package +manage_editor(action="restore_package") # Revert to pre-deployment backup ``` +**Deploy workflow:** Set the source path in MCP for Unity Advanced Settings first. `deploy_package` copies the source into the project's package location, creates a backup, and triggers `AssetDatabase.Refresh`. Follow with `refresh_unity(wait_for_ready=True)` to wait for recompilation. + ### execute_menu_item Execute any Unity menu item. @@ -921,6 +942,224 @@ manage_camera(action="list_cameras") --- +## Graphics Tools + +### manage_graphics + +Unified rendering and graphics management: volumes/post-processing, light baking, rendering stats, pipeline configuration, and URP renderer features. Requires URP or HDRP for volume/feature actions. Use `ping` to check pipeline status and available features. + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `action` | string | Yes | Action to perform (see categories below) | +| `target` | string | Sometimes | Target object name or instance ID | +| `effect` | string | Sometimes | Effect type name (e.g., `Bloom`, `Vignette`) | +| `properties` | dict | No | Action-specific properties to set | +| `parameters` | dict | No | Effect parameter values | +| `settings` | dict | No | Bake or pipeline settings | +| `name` | string | No | Name for created objects | +| `profile_path` | string | No | Asset path for VolumeProfile | +| `path` | string | No | Asset path (for `volume_create_profile`) | +| `position` | list[float] | No | Position [x,y,z] | + +**Actions by category:** + +**Status:** +- `ping` — Check render pipeline type, available features, and package status + +**Volume (require URP/HDRP):** +- `volume_create` — Create a Volume GameObject with optional effects. Properties: `name`, `is_global` (default true), `weight` (0-1), `priority`, `profile_path` (existing profile), `effects` (list of effect defs) +- `volume_add_effect` — Add effect override to a Volume. Params: `target` (Volume GO), `effect` (e.g., "Bloom") +- `volume_set_effect` — Set effect parameters. Params: `target`, `effect`, `parameters` (dict of param name to value) +- `volume_remove_effect` — Remove effect override. Params: `target`, `effect` +- `volume_get_info` — Get Volume details (profile, effects, parameters). Params: `target` +- `volume_set_properties` — Set Volume component properties (weight, priority, isGlobal). Params: `target`, `properties` +- `volume_list_effects` — List all available volume effects for the active pipeline +- `volume_create_profile` — Create a standalone VolumeProfile asset. Params: `path`, `effects` (optional) + +**Bake (Edit mode only):** +- `bake_start` — Start lightmap bake. Params: `async_bake` (default true) +- `bake_cancel` — Cancel in-progress bake +- `bake_status` — Check bake progress +- `bake_clear` — Clear baked lightmap data +- `bake_reflection_probe` — Bake a specific reflection probe. Params: `target` +- `bake_get_settings` — Get current Lightmapping settings +- `bake_set_settings` — Set Lightmapping settings. Params: `settings` (dict) +- `bake_create_light_probe_group` — Create a Light Probe Group. Params: `name`, `position`, `grid_size` [x,y,z], `spacing` +- `bake_create_reflection_probe` — Create a Reflection Probe. Params: `name`, `position`, `size` [x,y,z], `resolution`, `mode`, `hdr`, `box_projection` +- `bake_set_probe_positions` — Set Light Probe positions manually. Params: `target`, `positions` (array of [x,y,z]) + +**Stats:** +- `stats_get` — Get rendering counters (draw calls, batches, triangles, vertices, etc.) +- `stats_list_counters` — List all available ProfilerRecorder counters +- `stats_set_scene_debug` — Set Scene View debug/draw mode. Params: `mode` +- `stats_get_memory` — Get rendering memory usage + +**Pipeline:** +- `pipeline_get_info` — Get active render pipeline info (type, quality level, asset paths) +- `pipeline_set_quality` — Switch quality level. Params: `level` (name or index) +- `pipeline_get_settings` — Get pipeline asset settings +- `pipeline_set_settings` — Set pipeline asset settings. Params: `settings` (dict) + +**Features (URP only):** +- `feature_list` — List renderer features on the active URP renderer +- `feature_add` — Add a renderer feature. Params: `feature_type`, `name`, `material` (for full-screen effects) +- `feature_remove` — Remove a renderer feature. Params: `index` or `name` +- `feature_configure` — Set feature properties. Params: `index` or `name`, `properties` (dict) +- `feature_toggle` — Enable/disable a feature. Params: `index` or `name`, `active` (bool) +- `feature_reorder` — Reorder features. Params: `order` (list of indices) + +**Examples:** + +```python +# Check pipeline status +manage_graphics(action="ping") + +# Create a global post-processing volume with Bloom and Vignette +manage_graphics(action="volume_create", name="PostProcessing", is_global=True, + effects=[ + {"type": "Bloom", "parameters": {"intensity": 1.5, "threshold": 0.9}}, + {"type": "Vignette", "parameters": {"intensity": 0.4}} + ]) + +# Add an effect to an existing volume +manage_graphics(action="volume_add_effect", target="PostProcessing", effect="ColorAdjustments") + +# Configure effect parameters +manage_graphics(action="volume_set_effect", target="PostProcessing", + effect="ColorAdjustments", parameters={"postExposure": 0.5, "saturation": 10}) + +# Get volume info +manage_graphics(action="volume_get_info", target="PostProcessing") + +# List all available effects for the active pipeline +manage_graphics(action="volume_list_effects") + +# Create a VolumeProfile asset +manage_graphics(action="volume_create_profile", path="Assets/Settings/MyProfile.asset", + effects=[{"type": "Bloom"}, {"type": "Tonemapping"}]) + +# Start async lightmap bake +manage_graphics(action="bake_start", async_bake=True) + +# Check bake progress +manage_graphics(action="bake_status") + +# Create a Light Probe Group with a 3x2x3 grid +manage_graphics(action="bake_create_light_probe_group", name="ProbeGrid", + position=[0, 1, 0], grid_size=[3, 2, 3], spacing=2.0) + +# Create a Reflection Probe +manage_graphics(action="bake_create_reflection_probe", name="RoomProbe", + position=[0, 2, 0], size=[10, 5, 10], resolution=256, hdr=True) + +# Get rendering stats +manage_graphics(action="stats_get") + +# Get memory usage +manage_graphics(action="stats_get_memory") + +# Get pipeline info +manage_graphics(action="pipeline_get_info") + +# Switch quality level +manage_graphics(action="pipeline_set_quality", level="High") + +# List URP renderer features +manage_graphics(action="feature_list") + +# Add a full-screen renderer feature +manage_graphics(action="feature_add", feature_type="FullScreenPassRendererFeature", + name="NightVision", material="Assets/Materials/NightVision.mat") + +# Toggle a feature off +manage_graphics(action="feature_toggle", index=0, active=False) + +# Reorder features +manage_graphics(action="feature_reorder", order=[2, 0, 1]) +``` + +**Resources:** +- `mcpforunity://scene/volumes` — Lists all Volume components in the scene with their profiles and effects +- `mcpforunity://rendering/stats` — Current rendering performance counters +- `mcpforunity://pipeline/renderer-features` — URP renderer features on the active renderer + +--- + +## Package Tools + +### manage_packages + +Manage Unity packages: query, install, remove, embed, and configure registries. Install/remove trigger domain reload. + +**Query Actions (read-only):** + +| Action | Parameters | Description | +|--------|-----------|-------------| +| `list_packages` | — | List all installed packages (async, returns job_id) | +| `search_packages` | `query` | Search Unity registry by keyword (async, returns job_id) | +| `get_package_info` | `package` | Get details about a specific installed package | +| `list_registries` | — | List all scoped registries (names, URLs, scopes); immediate result | +| `ping` | — | Check package manager availability, Unity version, package count | +| `status` | `job_id` (required for list/search; optional for add/remove/embed) | Poll async job status; omit job_id to poll latest add/remove/embed job | + +**Mutating Actions:** + +| Action | Parameters | Description | +|--------|-----------|-------------| +| `add_package` | `package` | Install a package (name, name@version, git URL, or file: path) | +| `remove_package` | `package`, `force` (optional) | Remove a package; blocked if dependents exist unless `force=true` | +| `embed_package` | `package` | Copy package to local Packages/ for editing | +| `resolve_packages` | — | Force re-resolution of all packages | +| `add_registry` | `name`, `url`, `scopes` | Add a scoped registry (e.g., OpenUPM) | +| `remove_registry` | `name` or `url` | Remove a scoped registry | + +**Input validation:** +- Valid package IDs: `com.unity.inputsystem`, `com.unity.cinemachine@3.1.6` +- Git URLs: allowed with warning ("ensure this is a trusted source") +- `file:` paths: allowed with warning +- Invalid names (uppercase, missing dots): rejected + +**Example — List installed packages:** +```python +manage_packages(action="list_packages") +# Returns job_id, then poll: +manage_packages(action="status", job_id="") +``` + +**Example — Search for a package:** +```python +manage_packages(action="search_packages", query="input system") +``` + +**Example — Install a package:** +```python +manage_packages(action="add_package", package="com.unity.inputsystem") +# Poll until complete: +manage_packages(action="status", job_id="") +``` + +**Example — Remove with dependency check:** +```python +manage_packages(action="remove_package", package="com.unity.modules.ui") +# Error: "Cannot remove: 3 package(s) depend on it: ..." +manage_packages(action="remove_package", package="com.unity.modules.ui", force=True) +# Proceeds anyway +``` + +**Example — Add OpenUPM registry:** +```python +manage_packages( + action="add_registry", + name="OpenUPM", + url="https://package.openupm.com", + scopes=["com.cysharp", "com.neuecc"] +) +``` + +--- + ## ProBuilder Tools ### manage_probuilder diff --git a/.claude/skills/unity-mcp-skill/references/workflows.md b/.claude/skills/unity-mcp-skill/references/workflows.md index 829082a9d..4de0e2cf9 100644 --- a/.claude/skills/unity-mcp-skill/references/workflows.md +++ b/.claude/skills/unity-mcp-skill/references/workflows.md @@ -13,6 +13,9 @@ Common workflows and patterns for effective Unity-MCP usage. - [UI Creation Workflows](#ui-creation-workflows) - [Camera & Cinemachine Workflows](#camera--cinemachine-workflows) - [ProBuilder Workflows](#probuilder-workflows) +- [Graphics & Rendering Workflows](#graphics--rendering-workflows) +- [Package Management Workflows](#package-management-workflows) +- [Package Deployment Workflows](#package-deployment-workflows) - [Batch Operations](#batch-operations) --- @@ -113,8 +116,8 @@ manage_script( path="Assets/Scripts/MyScript.cs", contents="using UnityEngine;\n\npublic class MyScript : MonoBehaviour { ... }" ) -# Then refresh and check console -refresh_unity(mode="force", scope="scripts", compile="request", wait_for_ready=True) +# manage_script update auto-triggers import + compile — just wait and check console +# Read mcpforunity://editor/state → wait until is_compiling == false read_console(types=["error"], count=10) ``` @@ -156,7 +159,7 @@ manage_gameobject(action="modify", target="Main Camera", position=[0, 5, -10], rotation=[30, 0, 0]) # 5. Verify with screenshot -manage_scene(action="screenshot") +manage_camera(action="screenshot") # 6. Save scene manage_scene(action="save") @@ -207,7 +210,7 @@ for i in range(10): ### Create New Script and Attach ```python -# 1. Create script +# 1. Create script (automatically triggers import + compilation) create_script( path="Assets/Scripts/EnemyAI.cs", contents='''using UnityEngine; @@ -216,7 +219,7 @@ public class EnemyAI : MonoBehaviour { public float speed = 5f; public Transform target; - + void Update() { if (target != null) @@ -228,8 +231,8 @@ public class EnemyAI : MonoBehaviour }''' ) -# 2. CRITICAL: Refresh and compile -refresh_unity(mode="force", scope="scripts", compile="request", wait_for_ready=True) +# 2. Wait for compilation to finish +# Read mcpforunity://editor/state → wait until is_compiling == false # 3. Check for errors console = read_console(types=["error"], count=10) @@ -239,7 +242,7 @@ if console["messages"]: else: # 4. Attach to GameObject manage_gameobject(action="modify", target="Enemy", components_to_add=["EnemyAI"]) - + # 5. Set component properties manage_components( action="set_property", @@ -285,8 +288,8 @@ validate_script( level="standard" ) -# 5. Refresh -refresh_unity(mode="force", scope="scripts", compile="request", wait_for_ready=True) +# 5. Wait for compilation (script_apply_edits auto-triggers import + compile) +# Read mcpforunity://editor/state → wait until is_compiling == false # 6. Check console read_console(types=["error"], count=10) @@ -346,7 +349,7 @@ manage_material( ) # 3. Verify visually -manage_scene(action="screenshot") +manage_camera(action="screenshot") ``` ### Create Procedural Texture @@ -416,6 +419,35 @@ for asset in result["assets"]: print(f"Prefab: {prefab_path}, Children: {info['childCount']}") ``` +### Instantiate Prefab in Scene + +Use `manage_gameobject` (not `manage_prefabs`) to place prefab instances in the scene. + +```python +# Full path +manage_gameobject( + action="create", + name="Enemy_1", + prefab_path="Assets/Prefabs/Enemy.prefab", + position=[5, 0, 3], + parent="Enemies" +) + +# Smart lookup — just the prefab name works too +manage_gameobject(action="create", name="Enemy_2", prefab_path="Enemy", position=[10, 0, 3]) + +# Batch-spawn multiple instances +batch_execute(commands=[ + {"tool": "manage_gameobject", "params": { + "action": "create", "name": f"Enemy_{i}", + "prefab_path": "Enemy", "position": [i * 3, 0, 0], "parent": "Enemies" + }} + for i in range(5) +]) +``` + +> **Note:** `manage_prefabs` is for headless prefab editing (inspect, modify contents, create from GameObject). To *instantiate* a prefab into the scene, always use `manage_gameobject(action="create", prefab_path="...")`. + --- ## Testing Workflows @@ -485,8 +517,8 @@ public class PlayerTests }''' ) -# 2. Refresh -refresh_unity(mode="force", scope="scripts", compile="request", wait_for_ready=True) +# 2. Wait for compilation (create_script auto-triggers import + compile) +# Read mcpforunity://editor/state → wait until is_compiling == false # 3. Run test (expect pass for this simple test) result = run_tests(mode="EditMode", test_names=["PlayerTests.TestPlayerStartsAtOrigin"]) @@ -556,7 +588,7 @@ for item in hierarchy["data"]["items"]: print(f"Object {item['name']} fell through floor!") # 3. Visual verification -manage_scene(action="screenshot") +manage_camera(action="screenshot") ``` --- @@ -1585,7 +1617,7 @@ manage_probuilder(action="auto_smooth", target="Pillar1", properties={"angleThreshold": 45}) # 6. Screenshot to verify -manage_scene(action="screenshot", include_image=True, max_resolution=512) +manage_camera(action="screenshot", include_image=True, max_resolution=512) ``` ### Edit-Verify Loop Pattern @@ -1612,8 +1644,261 @@ manage_probuilder(action="delete_faces", target="Obj", properties={"faceIndices" --- +## Graphics & Rendering Workflows + +### Setting Up Post-Processing + +Add post-processing effects to a URP/HDRP scene using Volumes. + +```python +# 1. Check pipeline status and available effects +manage_graphics(action="ping") + +# 2. List available volume effects for the active pipeline +manage_graphics(action="volume_list_effects") + +# 3. Create a global post-processing volume with common effects +manage_graphics(action="volume_create", name="GlobalPostProcess", is_global=True, + effects=[ + {"type": "Bloom", "parameters": {"intensity": 1.0, "threshold": 0.9, "scatter": 0.7}}, + {"type": "Vignette", "parameters": {"intensity": 0.35}}, + {"type": "Tonemapping", "parameters": {"mode": 1}}, + {"type": "ColorAdjustments", "parameters": {"postExposure": 0.2, "contrast": 10}} + ]) + +# 4. Verify the volume was created +# Read mcpforunity://scene/volumes + +# 5. Fine-tune an effect parameter +manage_graphics(action="volume_set_effect", target="GlobalPostProcess", + effect="Bloom", parameters={"intensity": 1.5}) + +# 6. Screenshot to verify visual result +manage_camera(action="screenshot", include_image=True, max_resolution=512) +``` + +**Tips:** +- Always `ping` first to confirm URP/HDRP is active. Volumes do nothing on Built-in RP. +- Use `volume_list_effects` to discover available effect types for the active pipeline (URP and HDRP have different sets). +- Use `volume_get_info` to inspect current effect parameters before modifying. +- Create a reusable VolumeProfile asset with `volume_create_profile` and reference it via `profile_path` on multiple volumes. + +### Adding a Full-Screen Effect via Renderer Features (URP) + +Add a custom full-screen shader pass using URP Renderer Features. + +```python +# 1. Check pipeline and confirm URP +manage_graphics(action="ping") + +# 2. Create a material for the full-screen effect +manage_material(action="create", + material_path="Assets/Materials/GrayscaleEffect.mat", + shader="Shader Graphs/GrayscaleFullScreen") + +# 3. List current renderer features +manage_graphics(action="feature_list") + +# 4. Add a FullScreenPassRendererFeature with the material +manage_graphics(action="feature_add", + feature_type="FullScreenPassRendererFeature", + name="GrayscalePass", + material="Assets/Materials/GrayscaleEffect.mat") + +# 5. Verify it was added +manage_graphics(action="feature_list") + +# 6. Toggle it on/off to compare +manage_graphics(action="feature_toggle", index=0, active=False) # disable +manage_camera(action="screenshot", include_image=True, max_resolution=512) + +manage_graphics(action="feature_toggle", index=0, active=True) # re-enable +manage_camera(action="screenshot", include_image=True, max_resolution=512) + +# 7. Reorder features if needed (execution order matters) +manage_graphics(action="feature_reorder", order=[1, 0, 2]) +``` + +**Tips:** +- Renderer Features are URP-only. `feature_*` actions return an error on HDRP or Built-in RP. +- Read `mcpforunity://pipeline/renderer-features` to inspect features without modifying. +- Feature execution order affects the final image. Use `feature_reorder` to control pass ordering. + +### Configuring Light Baking + +Set up lightmaps, light probes, and reflection probes for baked GI. + +```python +# 1. Set lights to Baked or Mixed mode +manage_components(action="set_property", target="Directional Light", + component_type="Light", properties={"lightmapBakeType": 1}) # 1 = Mixed + +# 2. Mark static objects for lightmapping +manage_gameobject(action="modify", target="Environment", + component_properties={"StaticFlags": "ContributeGI"}) + +# 3. Configure lightmap settings +manage_graphics(action="bake_get_settings") +manage_graphics(action="bake_set_settings", settings={ + "lightmapper": 1, # 1 = Progressive GPU + "directSamples": 32, + "indirectSamples": 128, + "maxBounces": 4, + "lightmapResolution": 40 +}) + +# 4. Place light probes for dynamic objects +manage_graphics(action="bake_create_light_probe_group", name="MainProbeGrid", + position=[0, 1.5, 0], grid_size=[5, 3, 5], spacing=3.0) + +# 5. Place a reflection probe for an interior room +manage_graphics(action="bake_create_reflection_probe", name="RoomReflection", + position=[0, 2, 0], size=[8, 4, 8], resolution=256, + hdr=True, box_projection=True) + +# 6. Start async bake +manage_graphics(action="bake_start", async_bake=True) + +# 7. Poll bake status +manage_graphics(action="bake_status") +# Repeat until complete + +# 8. Bake the reflection probe separately if needed +manage_graphics(action="bake_reflection_probe", target="RoomReflection") + +# 9. Check rendering stats after bake +manage_graphics(action="stats_get") +``` + +**Tips:** +- Baking only works in Edit mode. If the editor is in Play mode, `bake_start` will fail. +- Use `bake_cancel` to abort a long bake. +- `bake_clear` removes all baked data (lightmaps, probes). Use before re-baking from scratch. +- For large scenes, use `async_bake=True` (default) and poll `bake_status` periodically. + +--- + +## Package Management Workflows + +### Install a Package and Verify + +```python +# 1. Check what's installed +manage_packages(action="ping") +manage_packages(action="list_packages") +# Poll status until complete +manage_packages(action="status", job_id="") + +# 2. Install the package +manage_packages(action="add_package", package="com.unity.inputsystem") +# Poll until domain reload completes +manage_packages(action="status", job_id="") + +# 3. Verify no compilation errors +read_console(types=["error"], count=10) + +# 4. Confirm it's installed +manage_packages(action="get_package_info", package="com.unity.inputsystem") +``` + +### Add OpenUPM Registry and Install Package + +```python +# 1. Add the OpenUPM scoped registry +manage_packages( + action="add_registry", + name="OpenUPM", + url="https://package.openupm.com", + scopes=["com.cysharp"] +) + +# 2. Force resolution to pick up the new registry +manage_packages(action="resolve_packages") + +# 3. Install a package from OpenUPM +manage_packages(action="add_package", package="com.cysharp.unitask") +manage_packages(action="status", job_id="") +``` + +### Safe Package Removal + +```python +# 1. Check dependencies before removing +manage_packages(action="remove_package", package="com.unity.modules.ui") +# If blocked: "Cannot remove: 3 package(s) depend on it" + +# 2. Force removal if you're sure +manage_packages(action="remove_package", package="com.unity.modules.ui", force=True) +manage_packages(action="status", job_id="") +``` + +### Install from Git URL (e.g., NuGetForUnity) + +```python +# Git URLs trigger a security warning — ensure the source is trusted +manage_packages( + action="add_package", + package="https://github.com/GlitchEnzo/NuGetForUnity.git?path=/src/NuGetForUnity" +) +manage_packages(action="status", job_id="") +``` + +--- + +## Package Deployment Workflows + +### Iterative Development Loop (Edit → Deploy → Test) + +Use `deploy_package` to copy your local MCPForUnity source into the project's installed package location. This bypasses the UI dialog and triggers recompilation automatically. + +```python +# Prerequisites: Set the MCPForUnity source path in Advanced Settings first. + +# 1. Make code changes (e.g., edit C# tools) +# script_apply_edits or create_script as needed + +# 2. Deploy the updated package (copies source → installed package, creates backup) +manage_editor(action="deploy_package") + +# 3. Wait for recompilation to finish +refresh_unity(mode="force", compile="request", wait_for_ready=True) + +# 4. Check for compilation errors +read_console(types=["error"], count=10, include_stacktrace=True) + +# 5. Test the changes +run_tests(mode="EditMode") +``` + +### Rollback After Failed Deploy + +```python +# Restore from the automatic pre-deployment backup +manage_editor(action="restore_package") + +# Wait for recompilation +refresh_unity(mode="force", compile="request", wait_for_ready=True) +``` + +--- + ## Batch Operations +### Batch Discovery (Multi-Search) + +Use `batch_execute` to search for multiple things in a single call instead of calling `find_gameobjects` repeatedly: + +```python +# Instead of 4 separate find_gameobjects calls, batch them: +batch_execute(commands=[ + {"tool": "find_gameobjects", "params": {"search_term": "Camera", "search_method": "by_component"}}, + {"tool": "find_gameobjects", "params": {"search_term": "Rigidbody", "search_method": "by_component"}}, + {"tool": "find_gameobjects", "params": {"search_term": "Player", "search_method": "by_tag"}}, + {"tool": "find_gameobjects", "params": {"search_term": "GameManager", "search_method": "by_name"}} +]) +# Returns array of results, one per command +``` + ### Mass Property Update ```python diff --git a/CLAUDE.md b/CLAUDE.md index 342484506..b6d9d72c7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,6 @@ -# CLAUDE.md - Project Overview for AI Assistants +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What This Project Is @@ -8,7 +10,7 @@ ```text AI Assistant (Claude/Cursor) - ↓ MCP Protocol (stdio/SSE) + ↓ MCP Protocol (stdio/HTTP) Python Server (Server/src/) ↓ WebSocket + HTTP Unity Editor Plugin (MCPForUnity/) @@ -20,31 +22,29 @@ Scene, Assets, Scripts - `Server/` - Python MCP server using FastMCP - `MCPForUnity/` - Unity C# Editor package -## Directory Structure +### Three Layers on the Python Side -```text -├── Server/ # Python MCP Server -│ ├── src/ -│ │ ├── cli/commands/ # Tool implementations (20 domain modules) -│ │ ├── transport/ # MCP protocol, WebSocket bridge -│ │ ├── services/ # Custom tools, resources -│ │ └── core/ # Telemetry, logging, config -│ └── tests/ # 502 Python tests -├── MCPForUnity/ # Unity Editor Package -│ └── Editor/ -│ ├── Tools/ # C# tool implementations (42 files) -│ ├── Services/ # Bridge, state management -│ ├── Helpers/ # Utilities (27 files) -│ └── Windows/ # Editor UI -├── TestProjects/UnityMCPTests/ # Unity test project (605 tests) -└── tools/ # Build/release scripts -``` +The Python server has three distinct layers. These are **not** auto-generated from each other: + +| Layer | Location | Framework | Purpose | +|-------|----------|-----------|---------| +| **MCP Tools** | `Server/src/services/tools/` | FastMCP (`@mcp_for_unity_tool`) | Exposed to AI assistants via MCP protocol | +| **CLI Commands** | `Server/src/cli/commands/` | Click (`@click.command`) | Terminal interface for developers | +| **Resources** | `Server/src/services/resources/` | FastMCP (`@mcp_for_unity_resource`) | Read-only state exposed to AI assistants | + +MCP tools call Unity via WebSocket (`send_with_unity_instance`). CLI commands call Unity via HTTP (`run_command`). Both route to the same C# `HandleCommand` methods. + +### Transport Modes + +- **Stdio**: Single-agent only. Separate Python process per client. Legacy TCP bridge to Unity. New connections stomp old ones. +- **HTTP**: Multi-agent ready. Single shared Python server. WebSocket hub at `/hub/plugin`. Session isolation via `client_id`. ## Code Philosophy ### 1. Domain Symmetry -Python CLI commands mirror C# Editor tools. Each domain (materials, prefabs, scripts, etc.) exists in both: -- `Server/src/cli/commands/materials.py` ↔ `MCPForUnity/Editor/Tools/ManageMaterial.cs` +Python MCP tools mirror C# Editor tools. Each domain exists in both: +- `Server/src/services/tools/manage_material.py` ↔ `MCPForUnity/Editor/Tools/ManageMaterial.cs` +- CLI commands (`Server/src/cli/commands/`) also mirror these but are a separate implementation. ### 2. Minimal Abstraction Avoid premature abstraction. Three similar lines of code is better than a helper that's used once. Only abstract when you have 3+ genuine use cases. @@ -53,44 +53,106 @@ Avoid premature abstraction. Three similar lines of code is better than a helper When removing functionality, delete it completely. No `_unused` renames, no `// removed` comments, no backwards-compatibility shims for internal code. ### 4. Test Coverage Required -Every new feature needs tests. We have 1100+ tests across Python and C#. Run them before PRs. +Every new feature needs tests. Run them before PRs. ### 5. Keep Tools Focused Each MCP tool does one thing well. Resist the urge to add "convenient" parameters that bloat the API surface. -### 6. Use Resources for reading. -Keep them smart and focused rather than "read everything" type resources. That way resources are quick and LLM-friendly. There are plenty of examples in the codebase to model on (gameobject, prefab, etc.) +### 6. Use Resources for Reading +Keep them smart and focused rather than "read everything" type resources. Resources should be quick and LLM-friendly. ## Key Patterns -### Parameter Handling (C#) -Use `ToolParams` for consistent parameter validation: -```csharp -var p = new ToolParams(parameters); -var pageSize = p.GetInt("page_size", "pageSize") ?? 50; -var name = p.RequireString("name"); +### Python MCP Tool Registration +Tools in `Server/src/services/tools/` are auto-discovered. Use the `@mcp_for_unity_tool` decorator: +```python +from services.registry import mcp_for_unity_tool + +@mcp_for_unity_tool( + description="Does something in Unity.", + group="core", # core (default), vfx, animation, ui, scripting_ext, testing, probuilder +) +async def manage_something( + ctx: Context, + action: Annotated[Literal["create", "delete"], "Action to perform"], +) -> dict[str, Any]: + unity_instance = await get_unity_instance_from_context(ctx) + params = {"action": action} + response = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "manage_something", params) + return response ``` -### Error Handling (Python CLI) -Use the `@handle_unity_errors` decorator: +The `group` parameter controls tool visibility. Only `"core"` is enabled by default. Non-core groups (vfx, animation, etc.) start disabled and are toggled via `manage_tools`. + +### Python CLI Error Handling +CLI commands (not MCP tools) use the `@handle_unity_errors` decorator: ```python @handle_unity_errors async def my_command(ctx, ...): result = await call_unity_tool(...) ``` +### C# Tool Registration +Tools are auto-discovered by `CommandRegistry` via reflection. Use the `[McpForUnityTool]` attribute: +```csharp +[McpForUnityTool("manage_something", AutoRegister = false, Group = "core")] +public static class ManageSomething +{ + // Sync handler (most tools): + public static object HandleCommand(JObject @params) + { + var p = new ToolParams(@params); + // ... + return new SuccessResponse("Done.", new { data = result }); + } + + // OR async handler (for long-running operations like play-test, refresh, batch): + public static async Task HandleCommand(JObject @params) + { + // CommandRegistry detects Task return type automatically + await SomeAsyncOperation(); + return new SuccessResponse("Done."); + } +} +``` + +Async handlers use `EditorApplication.update` polling with `TaskCompletionSource` — see `RefreshUnity.cs` for the canonical pattern. + +### C# Parameter Handling +Use `ToolParams` for consistent parameter validation: +```csharp +var p = new ToolParams(parameters); +var pageSize = p.GetInt("page_size", "pageSize") ?? 50; +var name = p.RequireString("name"); +``` + +### C# Resources +Resources use `[McpForUnityResource]` and follow the same `HandleCommand` pattern as tools. They provide read-only state to AI assistants. + ### Paging Large Results Always page results that could be large (hierarchies, components, search results): - Use `page_size` and `cursor` parameters - Return `next_cursor` when more results exist -## Common Tasks +### Composing Tools Internally (C#) +Use `CommandRegistry.InvokeCommandAsync` to call other tools from within a handler: +```csharp +var result = await CommandRegistry.InvokeCommandAsync("read_console", consoleParams); +``` + +## Commands ### Running Tests ```bash -# Python +# Python (all tests) cd Server && uv run pytest tests/ -v +# Python (single test file) +cd Server && uv run pytest tests/test_manage_material.py -v + +# Python (single test by name) +cd Server && uv run pytest tests/ -k "test_create_material" -v + # Unity - open TestProjects/UnityMCPTests in Unity, use Test Runner window ``` @@ -98,12 +160,14 @@ cd Server && uv run pytest tests/ -v 1. Set **Server Source Override** in MCP for Unity Advanced Settings to your local `Server/` path 2. Enable **Dev Mode** checkbox to force fresh installs 3. Use `mcp_source.py` to switch Unity package sources -4. Test on Windows and Mac if possible, and multiple clients (Claude Desktop and Claude Code are tricky for configuration as of this writing) +4. Test on Windows and Mac if possible, and multiple clients (Claude Desktop and Claude Code are tricky for configuration as of this writing) ### Adding a New Tool -1. Add Python command in `Server/src/cli/commands/.py` -2. Add C# implementation in `MCPForUnity/Editor/Tools/Manage.cs` -3. Add tests in both `Server/tests/` and `TestProjects/UnityMCPTests/Assets/Tests/` +1. Add Python MCP tool in `Server/src/services/tools/manage_.py` using `@mcp_for_unity_tool` +2. Add Python CLI commands in `Server/src/cli/commands/.py` using Click +3. Add C# implementation in `MCPForUnity/Editor/Tools/Manage.cs` with `[McpForUnityTool]` +4. Add Python tests in `Server/tests/test_manage_.py` +5. Add Unity tests in `TestProjects/UnityMCPTests/Assets/Tests/` ## What Not To Do diff --git a/MCPForUnity/Editor/Helpers/ComponentOps.cs b/MCPForUnity/Editor/Helpers/ComponentOps.cs index 3598c6209..1d726dc0a 100644 --- a/MCPForUnity/Editor/Helpers/ComponentOps.cs +++ b/MCPForUnity/Editor/Helpers/ComponentOps.cs @@ -466,6 +466,25 @@ private static bool SetViaSerializedProperty(Component component, string propert return false; so.ApplyModifiedProperties(); + + // Readback verification for ObjectReference — these can silently fail + if (prop.propertyType == SerializedPropertyType.ObjectReference + && value != null + && !(value is JValue jv && jv.Type == JTokenType.Null)) + { + so.Update(); + var verifyProp = so.FindProperty(propertyName) + ?? so.FindProperty(normalizedName); + if (verifyProp != null + && verifyProp.propertyType == SerializedPropertyType.ObjectReference + && verifyProp.objectReferenceValue == null) + { + error = $"Property '{propertyName}' was set but the object reference did not persist. " + + "Check that the referenced object exists and is the correct type."; + return false; + } + } + return true; } @@ -522,17 +541,17 @@ private static bool SetSerializedPropertyRecursive(SerializedProperty prop, JTok switch (prop.propertyType) { case SerializedPropertyType.Integer: - int intVal = ParamCoercion.CoerceInt(value, int.MinValue); - if (intVal == int.MinValue && value?.Type != JTokenType.Integer) + if (value == null || value.Type == JTokenType.Null + || (value.Type != JTokenType.Integer && value.Type != JTokenType.Float + && !long.TryParse(value.ToString(), out _))) { - if (value == null || value.Type == JTokenType.Null || - (value.Type == JTokenType.String && !int.TryParse(value.ToString(), out _))) - { - error = "Expected integer value."; - return false; - } + error = "Expected integer value."; + return false; } - prop.intValue = intVal; + if (prop.type == "long") + prop.longValue = ParamCoercion.CoerceLong(value, 0); + else + prop.intValue = ParamCoercion.CoerceInt(value, 0); return true; case SerializedPropertyType.Boolean: diff --git a/MCPForUnity/Editor/Helpers/ParamCoercion.cs b/MCPForUnity/Editor/Helpers/ParamCoercion.cs index d19d7bf46..38a8e32d7 100644 --- a/MCPForUnity/Editor/Helpers/ParamCoercion.cs +++ b/MCPForUnity/Editor/Helpers/ParamCoercion.cs @@ -44,6 +44,37 @@ public static int CoerceInt(JToken token, int defaultValue) return defaultValue; } + /// + /// Coerces a JToken to a long value, handling strings and floats. + /// + public static long CoerceLong(JToken token, long defaultValue) + { + if (token == null || token.Type == JTokenType.Null) + return defaultValue; + + try + { + if (token.Type == JTokenType.Integer) + return token.Value(); + + var s = token.ToString().Trim(); + if (s.Length == 0) + return defaultValue; + + if (long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var l)) + return l; + + if (double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var d)) + return (long)d; + } + catch + { + // Swallow and return default + } + + return defaultValue; + } + /// /// Coerces a JToken to a nullable integer value. /// Returns null if token is null, empty, or cannot be parsed. diff --git a/MCPForUnity/Editor/Tools/GameObjects/ManageGameObject.cs b/MCPForUnity/Editor/Tools/GameObjects/ManageGameObject.cs index d7a1b47bb..9bf16c704 100644 --- a/MCPForUnity/Editor/Tools/GameObjects/ManageGameObject.cs +++ b/MCPForUnity/Editor/Tools/GameObjects/ManageGameObject.cs @@ -80,7 +80,7 @@ public static object HandleCommand(JObject @params) return new ErrorResponse( $"Target '{targetPath}' is a prefab asset. " + $"Use 'manage_asset' with action='modify' for prefab asset modifications, " + - $"or 'manage_prefabs' with action='open_stage' to edit the prefab in isolation mode." + $"or 'manage_prefabs' with action='modify_contents' to edit the prefab headlessly, or 'manage_editor' with action='close_prefab_stage' to exit prefab editing mode." ); } // --- End Prefab Asset Check --- diff --git a/MCPForUnity/Editor/Tools/Graphics/GraphicsHelpers.cs b/MCPForUnity/Editor/Tools/Graphics/GraphicsHelpers.cs index cd5093899..dcb1ef346 100644 --- a/MCPForUnity/Editor/Tools/Graphics/GraphicsHelpers.cs +++ b/MCPForUnity/Editor/Tools/Graphics/GraphicsHelpers.cs @@ -146,7 +146,7 @@ internal static object ReadSerializedValue(SerializedProperty prop) return prop.propertyType switch { SerializedPropertyType.Boolean => prop.boolValue, - SerializedPropertyType.Integer => prop.intValue, + SerializedPropertyType.Integer => prop.type == "long" ? prop.longValue : (object)prop.intValue, SerializedPropertyType.Float => prop.floatValue, SerializedPropertyType.String => prop.stringValue, SerializedPropertyType.Enum => prop.enumValueIndex < prop.enumNames.Length @@ -177,7 +177,10 @@ internal static bool SetSerializedValue(SerializedProperty prop, JToken value) prop.boolValue = ParamCoercion.CoerceBool(value, false); return true; case SerializedPropertyType.Integer: - prop.intValue = ParamCoercion.CoerceInt(value, 0); + if (prop.type == "long") + prop.longValue = ParamCoercion.CoerceLong(value, 0); + else + prop.intValue = ParamCoercion.CoerceInt(value, 0); return true; case SerializedPropertyType.Float: prop.floatValue = ParamCoercion.CoerceFloat(value, 0f); diff --git a/MCPForUnity/Editor/Tools/ManageEditor.cs b/MCPForUnity/Editor/Tools/ManageEditor.cs index 20b298169..5bfd1972d 100644 --- a/MCPForUnity/Editor/Tools/ManageEditor.cs +++ b/MCPForUnity/Editor/Tools/ManageEditor.cs @@ -3,6 +3,7 @@ using MCPForUnity.Editor.Services; using Newtonsoft.Json.Linq; using UnityEditor; +using UnityEditor.SceneManagement; using UnityEditorInternal; // Required for tag management namespace MCPForUnity.Editor.Tools @@ -45,7 +46,6 @@ public static object HandleCommand(JObject @params) // Parameters for specific actions string tagName = p.Get("tagName"); string layerName = p.Get("layerName"); - bool waitForCompletion = p.GetBool("waitForCompletion", false); // Route action switch (action) @@ -135,6 +135,10 @@ public static object HandleCommand(JObject @params) // // Handle string name or int index // return SetQualityLevel(@params["qualityLevel"]); + // Prefab Stage + case "close_prefab_stage": + return ClosePrefabStage(); + // Package Deployment case "deploy_package": return DeployPackage(); @@ -143,7 +147,7 @@ public static object HandleCommand(JObject @params) default: return new ErrorResponse( - $"Unknown action: '{action}'. Supported actions: play, pause, stop, set_active_tool, add_tag, remove_tag, add_layer, remove_layer, deploy_package, restore_package. Use MCP resources for reading editor state, project info, tags, layers, selection, windows, prefab stage, and active tool." + $"Unknown action: '{action}'. Supported actions: play, pause, stop, set_active_tool, add_tag, remove_tag, add_layer, remove_layer, close_prefab_stage, deploy_package, restore_package. Use MCP resources for reading editor state, project info, tags, layers, selection, windows, prefab stage, and active tool." ); } } @@ -363,6 +367,28 @@ private static object RemoveLayer(string layerName) } } + // --- Prefab Stage Methods --- + + private static object ClosePrefabStage() + { + try + { + var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); + if (prefabStage == null) + { + return new SuccessResponse("Not currently in prefab editing mode."); + } + + string prefabPath = prefabStage.assetPath; + StageUtility.GoToMainStage(); + return new SuccessResponse($"Exited prefab stage for '{prefabPath}'.", new { prefabPath }); + } + catch (Exception e) + { + return new ErrorResponse($"Error closing prefab stage: {e.Message}"); + } + } + // --- Package Deployment Methods --- private static object DeployPackage() diff --git a/MCPForUnity/Editor/Tools/ManageMaterial.cs b/MCPForUnity/Editor/Tools/ManageMaterial.cs index 4825aa2c0..ecfb360b0 100644 --- a/MCPForUnity/Editor/Tools/ManageMaterial.cs +++ b/MCPForUnity/Editor/Tools/ManageMaterial.cs @@ -258,7 +258,7 @@ private static object SetRendererColor(JObject @params) } int slot = p.GetInt("slot") ?? 0; - string mode = p.Get("mode", "create_unique"); + string mode = p.Get("mode", "property_block"); Color color; try @@ -367,6 +367,22 @@ private static object SetRendererColor(JObject @params) return new ErrorResponse($"Unknown mode: {mode}"); } + private static void EnsureAssetFolderExists(string assetFolderPath) + { + if (AssetDatabase.IsValidFolder(assetFolderPath)) + return; + + string[] parts = assetFolderPath.Replace('\\', '/').Split('/'); + string current = parts[0]; // "Assets" + for (int i = 1; i < parts.Length; i++) + { + string next = current + "/" + parts[i]; + if (!AssetDatabase.IsValidFolder(next)) + AssetDatabase.CreateFolder(current, parts[i]); + current = next; + } + } + private static void SetColorProperties(Material mat, Color color) { bool wrote = false; @@ -395,7 +411,7 @@ private static object CreateUniqueAndAssign(Renderer renderer, GameObject go, Co // live next to the scene/generation folder instead of a global dump. string materialFolder = "Assets/Materials"; var scene = go.scene; - if (scene.IsValid() && !string.IsNullOrEmpty(scene.path)) + if (scene.IsValid() && !string.IsNullOrEmpty(scene.path) && scene.path.StartsWith("Assets/")) { string sceneDir = System.IO.Path.GetDirectoryName(scene.path).Replace("\\", "/"); materialFolder = $"{sceneDir}/Materials"; @@ -408,13 +424,8 @@ private static object CreateUniqueAndAssign(Renderer renderer, GameObject go, Co return new ErrorResponse($"Invalid GameObject name '{go.name}' — cannot build a safe material path."); } - // Ensure the Materials directory exists - if (!AssetDatabase.IsValidFolder(materialFolder)) - { - string parentDir = System.IO.Path.GetDirectoryName(materialFolder).Replace("\\", "/"); - string folderName = System.IO.Path.GetFileName(materialFolder); - AssetDatabase.CreateFolder(parentDir, folderName); - } + // Ensure the Materials directory exists (recursive) + EnsureAssetFolderExists(materialFolder); Material existing = AssetDatabase.LoadAssetAtPath(matPath); if (existing != null) diff --git a/MCPForUnity/Editor/Tools/ManageScriptableObject.cs b/MCPForUnity/Editor/Tools/ManageScriptableObject.cs index b5a4d78f3..cdbec8a65 100644 --- a/MCPForUnity/Editor/Tools/ManageScriptableObject.cs +++ b/MCPForUnity/Editor/Tools/ManageScriptableObject.cs @@ -924,20 +924,22 @@ private static bool TrySetValueRecursive(SerializedProperty prop, JToken valueTo switch (prop.propertyType) { case SerializedPropertyType.Integer: - // Use ParamCoercion for robust int parsing - int intVal = ParamCoercion.CoerceInt(valueToken, int.MinValue); - if (intVal == int.MinValue && valueToken?.Type != JTokenType.Integer) + if (valueToken == null || valueToken.Type == JTokenType.Null) { - // Double-check: if it's actually int.MinValue or failed to parse - if (valueToken == null || valueToken.Type == JTokenType.Null || - (valueToken.Type == JTokenType.String && !int.TryParse(valueToken.ToString(), out _))) - { - message = "Expected integer value."; - return false; - } + message = "Expected integer value."; + return false; } - prop.intValue = intVal; - message = "Set int."; + if (valueToken.Type != JTokenType.Integer && valueToken.Type != JTokenType.Float + && !long.TryParse(valueToken.ToString(), out _)) + { + message = "Expected integer value."; + return false; + } + if (prop.type == "long") + prop.longValue = ParamCoercion.CoerceLong(valueToken, 0); + else + prop.intValue = ParamCoercion.CoerceInt(valueToken, 0); + message = prop.type == "long" ? "Set long." : "Set int."; return true; case SerializedPropertyType.Boolean: diff --git a/MCPForUnity/Editor/Tools/ManageUI.cs b/MCPForUnity/Editor/Tools/ManageUI.cs index 1d310dbf4..481e47af9 100644 --- a/MCPForUnity/Editor/Tools/ManageUI.cs +++ b/MCPForUnity/Editor/Tools/ManageUI.cs @@ -181,6 +181,7 @@ private static object CreateFile(JObject @params) { return new ErrorResponse($"UXML validation failed — file was NOT written. {xmlError}"); } + contents = EnsureEditorExtensionMode(contents); } File.WriteAllText(fullPath, contents, Utf8NoBom); @@ -272,6 +273,7 @@ private static object UpdateFile(JObject @params) { return new ErrorResponse($"UXML validation failed — file was NOT updated. {xmlError}"); } + contents = EnsureEditorExtensionMode(contents); } File.WriteAllText(fullPath, contents, Utf8NoBom); @@ -957,8 +959,8 @@ private static object RenderUI(JObject @params) if (s_pendingCaptureStarted) { return new ErrorResponse( - "A play-mode screen capture is already in progress. " - + "Call render_ui again after the current capture completes."); + "Cannot capture: another capture is already in progress.", + new { retry_after_ms = 100, reason = "capture_in_progress" }); } s_pendingCaptureDone = false; @@ -1846,6 +1848,34 @@ private static string GetDecodedContents(ToolParams p) /// Uses XmlParserContext to pre-declare common UXML namespace prefixes /// (ui, uie, engine, editor) since Unity's parser is more lenient than System.Xml. /// + + /// + /// Ensures the root UXML element has editor-extension-mode attribute. + /// UI Builder requires this to open the file. Injects "False" if missing. + /// + private static string EnsureEditorExtensionMode(string contents) + { + if (contents.Contains("editor-extension-mode")) + return contents; + + int idx = contents.IndexOf("', idx); + if (closeTag < 0) + return contents; + + bool selfClosing = contents[closeTag - 1] == '/'; + int insertPos = selfClosing ? closeTag - 1 : closeTag; + + return contents.Substring(0, insertPos) + + " editor-extension-mode=\"False\"" + + contents.Substring(insertPos); + } + private static string ValidateUxmlContent(string contents, List warnings) { if (string.IsNullOrWhiteSpace(contents)) diff --git a/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs b/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs index 10ebf1677..9a9069783 100644 --- a/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs +++ b/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using MCPForUnity.Editor.Helpers; using Newtonsoft.Json.Linq; using UnityEditor; @@ -125,7 +126,10 @@ private static object CreatePrefabFromGameObject(JObject @params) } } - // 7. Create the prefab + // 7. Persist any runtime-only materials so they survive prefab serialization + var persistResult = PersistRuntimeMaterials(sourceObject, finalPath); + + // 8. Create the prefab try { GameObject result = CreatePrefabAsset(sourceObject, finalPath, replaceExisting); @@ -135,7 +139,7 @@ private static object CreatePrefabFromGameObject(JObject @params) return new ErrorResponse($"Failed to create prefab asset at '{finalPath}'."); } - // 8. Select the newly created instance + // 9. Select the newly created instance Selection.activeGameObject = result; return new SuccessResponse( @@ -148,7 +152,8 @@ private static object CreatePrefabFromGameObject(JObject @params) wasUnlinked = unlinkIfInstance && objectValidation.shouldUnlink, wasReplaced = replaceExisting && fileExistedAtPath, componentCount = result.GetComponents().Length, - childCount = result.transform.childCount + childCount = result.transform.childCount, + materialsPersisted = persistResult.count } ); } @@ -263,6 +268,148 @@ private static GameObject CreatePrefabAsset(GameObject sourceObject, string path return result; } + /// + /// Scans all Renderers in the hierarchy and persists any runtime-only materials + /// (MaterialPropertyBlock overrides or in-memory instances from renderer.material) + /// as .mat assets so they survive prefab serialization. + /// + private static (int count, List paths) PersistRuntimeMaterials(GameObject root, string prefabPath) + { + var renderers = root.GetComponentsInChildren(true); + var persistedPaths = new List(); + string prefabDir = Path.GetDirectoryName(prefabPath).Replace("\\", "/"); + string materialsFolder = $"{prefabDir}/Materials"; + + foreach (var renderer in renderers) + { + Material[] sharedMats = renderer.sharedMaterials; + bool changed = false; + + for (int slot = 0; slot < sharedMats.Length; slot++) + { + Material mat = sharedMats[slot]; + + // Case 1: Material is null but a property block has color data — + // this happens after instance mode severs the asset link. + // Case 2: Material exists but is not a persistent asset (runtime instance). + bool isRuntimeInstance = mat != null && !EditorUtility.IsPersistent(mat); + bool isNullWithPropertyBlock = mat == null && HasPropertyBlockColors(renderer, slot); + bool isNullMaterial = mat == null && !isNullWithPropertyBlock; + + if (!isRuntimeInstance && !isNullWithPropertyBlock) + continue; + + // Derive a unique asset path from the GameObject name and slot + string goName = renderer.gameObject.name.Replace(" ", "_"); + string suffix = slot > 0 ? $"_slot{slot}" : ""; + string matPath = $"{materialsFolder}/{goName}{suffix}_mat.mat"; + matPath = AssetPathUtility.SanitizeAssetPath(matPath); + if (matPath == null) + { + McpLog.Warn($"[ManagePrefabs] Could not build safe material path for '{renderer.gameObject.name}', skipping."); + continue; + } + + // Ensure the Materials directory exists (recursive) + EnsureAssetFolderExists(materialsFolder); + + Material persisted = AssetDatabase.LoadAssetAtPath(matPath); + if (persisted == null) + { + // Create a new material with the correct shader for the active pipeline + Shader shader = isRuntimeInstance && mat.shader != null + ? mat.shader + : RenderPipelineUtility.ResolveShader("Standard"); + persisted = new Material(shader); + AssetDatabase.CreateAsset(persisted, matPath); + } + + // Copy properties from the runtime instance if available + if (isRuntimeInstance) + { + persisted.CopyPropertiesFromMaterial(mat); + EditorUtility.SetDirty(persisted); + } + else if (isNullWithPropertyBlock) + { + // Extract color from the property block and apply to the new material + ApplyPropertyBlockToMaterial(renderer, slot, persisted); + EditorUtility.SetDirty(persisted); + } + + sharedMats[slot] = persisted; + changed = true; + persistedPaths.Add(matPath); + McpLog.Info($"[ManagePrefabs] Persisted runtime material for '{renderer.gameObject.name}' slot {slot} → {matPath}"); + } + + if (changed) + { + Undo.RecordObject(renderer, "Persist runtime materials for prefab"); + renderer.sharedMaterials = sharedMats; + // Clear any property blocks now that the material is persisted + for (int slot = 0; slot < sharedMats.Length; slot++) + { + renderer.SetPropertyBlock(null, slot); + } + EditorUtility.SetDirty(renderer); + } + } + + if (persistedPaths.Count > 0) + { + AssetDatabase.SaveAssets(); + McpLog.Info($"[ManagePrefabs] Persisted {persistedPaths.Count} runtime material(s) before prefab save."); + } + + return (persistedPaths.Count, persistedPaths); + } + + /// + /// Recursively creates the folder hierarchy for the given asset path if it doesn't exist. + /// + private static void EnsureAssetFolderExists(string assetFolderPath) + { + if (AssetDatabase.IsValidFolder(assetFolderPath)) + return; + + string[] parts = assetFolderPath.Replace('\\', '/').Split('/'); + string current = parts[0]; // "Assets" + for (int i = 1; i < parts.Length; i++) + { + string next = current + "/" + parts[i]; + if (!AssetDatabase.IsValidFolder(next)) + AssetDatabase.CreateFolder(current, parts[i]); + current = next; + } + } + + private static bool HasPropertyBlockColors(Renderer renderer, int slot) + { + MaterialPropertyBlock block = new MaterialPropertyBlock(); + renderer.GetPropertyBlock(block, slot); + return !block.isEmpty; + } + + /// + /// Extracts color properties from a MaterialPropertyBlock and applies them to a material. + /// + private static void ApplyPropertyBlockToMaterial(Renderer renderer, int slot, Material mat) + { + MaterialPropertyBlock block = new MaterialPropertyBlock(); + renderer.GetPropertyBlock(block, slot); + + // Try the standard color property names + string[] colorProps = { "_BaseColor", "_Color" }; + foreach (string prop in colorProps) + { + if (mat.HasProperty(prop) && block.HasColor(prop)) + { + mat.SetColor(prop, block.GetColor(prop)); + } + } + } + #endregion /// diff --git a/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.uxml b/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.uxml index c5c572074..c4699d043 100644 --- a/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.uxml +++ b/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.uxml @@ -42,7 +42,7 @@ - + diff --git a/Server/src/cli/commands/material.py b/Server/src/cli/commands/material.py index 2bcd4315a..53c8376fb 100644 --- a/Server/src/cli/commands/material.py +++ b/Server/src/cli/commands/material.py @@ -209,8 +209,8 @@ def assign(material_path: str, target: str, search_method: Optional[str], slot: @click.option( "--mode", "-m", type=click.Choice(["shared", "instance", "property_block", "create_unique"]), - default="create_unique", - help="Modification mode (default: create_unique — persistent per-object material)." + default="property_block", + help="Modification mode (default: property_block — use create_unique for persistent per-object material)." ) @handle_unity_errors def set_renderer_color(target: str, r: float, g: float, b: float, a: float, search_method: Optional[str], mode: str): diff --git a/Server/src/services/tools/manage_editor.py b/Server/src/services/tools/manage_editor.py index ead5107d8..8f44a0764 100644 --- a/Server/src/services/tools/manage_editor.py +++ b/Server/src/services/tools/manage_editor.py @@ -8,20 +8,16 @@ from services.tools import get_unity_instance_from_context from transport.unity_transport import send_with_unity_instance from transport.legacy.unity_connection import async_send_command_with_retry -from services.tools.utils import coerce_bool - @mcp_for_unity_tool( - description="Controls and queries the Unity editor's state and settings. Tip: pass booleans as true/false; if your client only sends strings, 'true'/'false' are accepted. Read-only actions: telemetry_status, telemetry_ping. Modifying actions: play, pause, stop, set_active_tool, add_tag, remove_tag, add_layer, remove_layer, deploy_package, restore_package. deploy_package copies the configured MCPForUnity source folder into the project's installed package location (triggers recompile, no confirmation dialog). restore_package reverts to the pre-deployment backup.", + description="Controls and queries the Unity editor's state and settings. Read-only actions: telemetry_status, telemetry_ping. Modifying actions: play, pause, stop, set_active_tool, add_tag, remove_tag, add_layer, remove_layer, close_prefab_stage, deploy_package, restore_package. deploy_package copies the configured MCPForUnity source folder into the project's installed package location (triggers recompile, no confirmation dialog). restore_package reverts to the pre-deployment backup.", annotations=ToolAnnotations( title="Manage Editor", ), ) async def manage_editor( ctx: Context, - action: Annotated[Literal["telemetry_status", "telemetry_ping", "play", "pause", "stop", "set_active_tool", "add_tag", "remove_tag", "add_layer", "remove_layer", "deploy_package", "restore_package"], "Get and update the Unity Editor state. deploy_package copies the configured MCPForUnity source into the project's package location (triggers recompile). restore_package reverts the last deployment from backup."], - wait_for_completion: Annotated[bool | str, - "Optional. If True, waits for certain actions (accepts true/false or 'true'/'false')"] | None = None, + action: Annotated[Literal["telemetry_status", "telemetry_ping", "play", "pause", "stop", "set_active_tool", "add_tag", "remove_tag", "add_layer", "remove_layer", "close_prefab_stage", "deploy_package", "restore_package"], "Get and update the Unity Editor state. close_prefab_stage exits prefab editing mode and returns to the main scene stage. deploy_package copies the configured MCPForUnity source into the project's package location (triggers recompile). restore_package reverts the last deployment from backup."], tool_name: Annotated[str, "Tool name when setting active tool"] | None = None, tag_name: Annotated[str, @@ -32,8 +28,6 @@ async def manage_editor( # Get active instance from request state (injected by middleware) unity_instance = await get_unity_instance_from_context(ctx) - wait_for_completion = coerce_bool(wait_for_completion) - try: # Diagnostics: quick telemetry checks if action == "telemetry_status": @@ -45,7 +39,6 @@ async def manage_editor( # Prepare parameters, removing None values params = { "action": action, - "waitForCompletion": wait_for_completion, "toolName": tool_name, "tagName": tag_name, "layerName": layer_name, diff --git a/Server/src/services/tools/manage_material.py b/Server/src/services/tools/manage_material.py index 1e4d535a5..7ee3356b0 100644 --- a/Server/src/services/tools/manage_material.py +++ b/Server/src/services/tools/manage_material.py @@ -59,7 +59,7 @@ async def manage_material( "by_layer", "by_component"], "Search method for target"] | None = None, slot: Annotated[int, "Material slot index (0-based)"] | None = None, mode: Annotated[Literal["shared", "instance", "property_block", "create_unique"], - "Assignment/modification mode (default: create_unique — creates a persistent per-object material)"] | None = None, + "Assignment/modification mode; behavior when omitted is action-specific on the Unity side."] | None = None, ) -> dict[str, Any]: unity_instance = await get_unity_instance_from_context(ctx) diff --git a/unity-mcp-skill/SKILL.md b/unity-mcp-skill/SKILL.md index 83a8824a6..032aaaff3 100644 --- a/unity-mcp-skill/SKILL.md +++ b/unity-mcp-skill/SKILL.md @@ -29,15 +29,21 @@ Before applying a template: ## Critical Best Practices -### 1. After Writing/Editing Scripts: Always Refresh and Check Console +### 1. After Writing/Editing Scripts: Wait for Compilation and Check Console ```python # After create_script or script_apply_edits: -refresh_unity(mode="force", scope="scripts", compile="request", wait_for_ready=True) +# Both tools already trigger AssetDatabase.ImportAsset + RequestScriptCompilation automatically. +# No need to call refresh_unity — just wait for compilation to finish, then check console. + +# 1. Poll editor state until compilation completes +# Read mcpforunity://editor/state → wait until is_compiling == false + +# 2. Check for compilation errors read_console(types=["error"], count=10, include_stacktrace=True) ``` -**Why:** Unity must compile scripts before they're usable. Compilation errors block all tool execution. +**Why:** Unity must compile scripts before they're usable. `create_script` and `script_apply_edits` already trigger import and compilation automatically — calling `refresh_unity` afterward is redundant. ### 2. Use `batch_execute` for Multiple Operations @@ -55,6 +61,15 @@ batch_execute( **Max 25 commands per batch by default (configurable in Unity MCP Tools window, max 100).** Use `fail_fast=True` for dependent operations. +**Tip:** Also use `batch_execute` for discovery — batch multiple `find_gameobjects` calls instead of calling them one at a time: +```python +batch_execute(commands=[ + {"tool": "find_gameobjects", "params": {"search_term": "Camera", "search_method": "by_component"}}, + {"tool": "find_gameobjects", "params": {"search_term": "Player", "search_method": "by_tag"}}, + {"tool": "find_gameobjects", "params": {"search_term": "GameManager", "search_method": "by_name"}} +]) +``` + ### 3. Use Screenshots to Verify Visual Results ```python @@ -157,8 +172,8 @@ uri="file:///full/path/to/file.cs" |----------|-----------|---------| | **Scene** | `manage_scene`, `find_gameobjects` | Scene operations, finding objects | | **Objects** | `manage_gameobject`, `manage_components` | Creating/modifying GameObjects | -| **Scripts** | `create_script`, `script_apply_edits`, `refresh_unity` | C# code management | -| **Assets** | `manage_asset`, `manage_prefabs` | Asset operations | +| **Scripts** | `create_script`, `script_apply_edits`, `validate_script` | C# code management (auto-refreshes on create/edit) | +| **Assets** | `manage_asset`, `manage_prefabs` | Asset operations. **Prefab instantiation** is done via `manage_gameobject(action="create", prefab_path="...")`, not `manage_prefabs`. | | **Editor** | `manage_editor`, `execute_menu_item`, `read_console` | Editor control, package deployment (`deploy_package`/`restore_package` actions) | | **Testing** | `run_tests`, `get_test_job` | Unity Test Framework | | **Batch** | `batch_execute` | Parallel/bulk operations | @@ -173,14 +188,14 @@ uri="file:///full/path/to/file.cs" ### Creating a New Script and Using It ```python -# 1. Create the script +# 1. Create the script (automatically triggers import + compilation) create_script( path="Assets/Scripts/PlayerController.cs", contents="using UnityEngine;\n\npublic class PlayerController : MonoBehaviour\n{\n void Update() { }\n}" ) -# 2. CRITICAL: Refresh and wait for compilation -refresh_unity(mode="force", scope="scripts", compile="request", wait_for_ready=True) +# 2. Wait for compilation to finish +# Read mcpforunity://editor/state → wait until is_compiling == false # 3. Check for compilation errors read_console(types=["error"], count=10) diff --git a/unity-mcp-skill/references/tools-reference.md b/unity-mcp-skill/references/tools-reference.md index 749a517f6..a052fc9f5 100644 --- a/unity-mcp-skill/references/tools-reference.md +++ b/unity-mcp-skill/references/tools-reference.md @@ -213,6 +213,17 @@ manage_gameobject( prefab_path="Assets/Prefabs/MyCube.prefab" ) +# Prefab instantiation — place a prefab instance in the scene +manage_gameobject( + action="create", + name="Enemy_1", + prefab_path="Assets/Prefabs/Enemy.prefab", + position=[5, 0, 3], + parent="Enemies" # optional parent GameObject +) +# Smart lookup — just the prefab name works too: +manage_gameobject(action="create", name="Enemy_2", prefab_path="Enemy", position=[10, 0, 3]) + # Modify manage_gameobject( action="modify", @@ -694,6 +705,8 @@ manage_editor(action="remove_tag", tag_name="OldTag") manage_editor(action="add_layer", layer_name="Projectiles") manage_editor(action="remove_layer", layer_name="OldLayer") +manage_editor(action="close_prefab_stage") # Exit prefab editing mode back to main scene + # Package deployment (no confirmation dialog — designed for LLM-driven iteration) manage_editor(action="deploy_package") # Copy configured MCPForUnity source into installed package manage_editor(action="restore_package") # Revert to pre-deployment backup diff --git a/unity-mcp-skill/references/workflows.md b/unity-mcp-skill/references/workflows.md index 6950a2f60..4de0e2cf9 100644 --- a/unity-mcp-skill/references/workflows.md +++ b/unity-mcp-skill/references/workflows.md @@ -116,8 +116,8 @@ manage_script( path="Assets/Scripts/MyScript.cs", contents="using UnityEngine;\n\npublic class MyScript : MonoBehaviour { ... }" ) -# Then refresh and check console -refresh_unity(mode="force", scope="scripts", compile="request", wait_for_ready=True) +# manage_script update auto-triggers import + compile — just wait and check console +# Read mcpforunity://editor/state → wait until is_compiling == false read_console(types=["error"], count=10) ``` @@ -210,7 +210,7 @@ for i in range(10): ### Create New Script and Attach ```python -# 1. Create script +# 1. Create script (automatically triggers import + compilation) create_script( path="Assets/Scripts/EnemyAI.cs", contents='''using UnityEngine; @@ -219,7 +219,7 @@ public class EnemyAI : MonoBehaviour { public float speed = 5f; public Transform target; - + void Update() { if (target != null) @@ -231,8 +231,8 @@ public class EnemyAI : MonoBehaviour }''' ) -# 2. CRITICAL: Refresh and compile -refresh_unity(mode="force", scope="scripts", compile="request", wait_for_ready=True) +# 2. Wait for compilation to finish +# Read mcpforunity://editor/state → wait until is_compiling == false # 3. Check for errors console = read_console(types=["error"], count=10) @@ -242,7 +242,7 @@ if console["messages"]: else: # 4. Attach to GameObject manage_gameobject(action="modify", target="Enemy", components_to_add=["EnemyAI"]) - + # 5. Set component properties manage_components( action="set_property", @@ -288,8 +288,8 @@ validate_script( level="standard" ) -# 5. Refresh -refresh_unity(mode="force", scope="scripts", compile="request", wait_for_ready=True) +# 5. Wait for compilation (script_apply_edits auto-triggers import + compile) +# Read mcpforunity://editor/state → wait until is_compiling == false # 6. Check console read_console(types=["error"], count=10) @@ -419,6 +419,35 @@ for asset in result["assets"]: print(f"Prefab: {prefab_path}, Children: {info['childCount']}") ``` +### Instantiate Prefab in Scene + +Use `manage_gameobject` (not `manage_prefabs`) to place prefab instances in the scene. + +```python +# Full path +manage_gameobject( + action="create", + name="Enemy_1", + prefab_path="Assets/Prefabs/Enemy.prefab", + position=[5, 0, 3], + parent="Enemies" +) + +# Smart lookup — just the prefab name works too +manage_gameobject(action="create", name="Enemy_2", prefab_path="Enemy", position=[10, 0, 3]) + +# Batch-spawn multiple instances +batch_execute(commands=[ + {"tool": "manage_gameobject", "params": { + "action": "create", "name": f"Enemy_{i}", + "prefab_path": "Enemy", "position": [i * 3, 0, 0], "parent": "Enemies" + }} + for i in range(5) +]) +``` + +> **Note:** `manage_prefabs` is for headless prefab editing (inspect, modify contents, create from GameObject). To *instantiate* a prefab into the scene, always use `manage_gameobject(action="create", prefab_path="...")`. + --- ## Testing Workflows @@ -488,8 +517,8 @@ public class PlayerTests }''' ) -# 2. Refresh -refresh_unity(mode="force", scope="scripts", compile="request", wait_for_ready=True) +# 2. Wait for compilation (create_script auto-triggers import + compile) +# Read mcpforunity://editor/state → wait until is_compiling == false # 3. Run test (expect pass for this simple test) result = run_tests(mode="EditMode", test_names=["PlayerTests.TestPlayerStartsAtOrigin"]) @@ -1855,6 +1884,21 @@ refresh_unity(mode="force", compile="request", wait_for_ready=True) ## Batch Operations +### Batch Discovery (Multi-Search) + +Use `batch_execute` to search for multiple things in a single call instead of calling `find_gameobjects` repeatedly: + +```python +# Instead of 4 separate find_gameobjects calls, batch them: +batch_execute(commands=[ + {"tool": "find_gameobjects", "params": {"search_term": "Camera", "search_method": "by_component"}}, + {"tool": "find_gameobjects", "params": {"search_term": "Rigidbody", "search_method": "by_component"}}, + {"tool": "find_gameobjects", "params": {"search_term": "Player", "search_method": "by_tag"}}, + {"tool": "find_gameobjects", "params": {"search_term": "GameManager", "search_method": "by_name"}} +]) +# Returns array of results, one per command +``` + ### Mass Property Update ```python