diff --git a/MCPForUnity/Editor/Tools/Graphics/SkyboxOps.cs b/MCPForUnity/Editor/Tools/Graphics/SkyboxOps.cs index 4ae2dbfe8..ec681e448 100644 --- a/MCPForUnity/Editor/Tools/Graphics/SkyboxOps.cs +++ b/MCPForUnity/Editor/Tools/Graphics/SkyboxOps.cs @@ -10,6 +10,22 @@ namespace MCPForUnity.Editor.Tools.Graphics { internal static class SkyboxOps { + static Texture CustomReflectionTexture + { + get => +#if UNITY_2022_1_OR_NEWER + RenderSettings.customReflectionTexture; +#else + RenderSettings.customReflection; +#endif + set { +#if UNITY_2022_1_OR_NEWER + RenderSettings.customReflectionTexture = value; +#else + RenderSettings.customReflection = value; +#endif + } + } // --------------------------------------------------------------- // skybox_get — read all environment settings // --------------------------------------------------------------- @@ -70,8 +86,8 @@ public static object GetEnvironment(JObject @params) bounces = RenderSettings.reflectionBounces, mode = RenderSettings.defaultReflectionMode.ToString(), resolution = RenderSettings.defaultReflectionResolution, - customCubemap = RenderSettings.customReflectionTexture != null - ? AssetDatabase.GetAssetPath(RenderSettings.customReflectionTexture) + customCubemap = CustomReflectionTexture != null + ? AssetDatabase.GetAssetPath(CustomReflectionTexture) : null }, sun = sun != null @@ -301,7 +317,7 @@ public static object SetReflection(JObject @params) { var cubemap = AssetDatabase.LoadAssetAtPath(cubemapPath); if (cubemap != null) - RenderSettings.customReflectionTexture = cubemap; + CustomReflectionTexture = cubemap; else return new ErrorResponse($"Cubemap not found at '{cubemapPath}'."); } @@ -318,8 +334,8 @@ public static object SetReflection(JObject @params) bounces = RenderSettings.reflectionBounces, mode = RenderSettings.defaultReflectionMode.ToString(), resolution = RenderSettings.defaultReflectionResolution, - customCubemap = RenderSettings.customReflectionTexture != null - ? AssetDatabase.GetAssetPath(RenderSettings.customReflectionTexture) + customCubemap = CustomReflectionTexture != null + ? AssetDatabase.GetAssetPath(CustomReflectionTexture) : null } }; diff --git a/MCPForUnity/Editor/Tools/ManageScript.cs b/MCPForUnity/Editor/Tools/ManageScript.cs index 4ebf1b06b..7f1edf5b4 100644 --- a/MCPForUnity/Editor/Tools/ManageScript.cs +++ b/MCPForUnity/Editor/Tools/ManageScript.cs @@ -2741,6 +2741,7 @@ private static void CheckDuplicateMethodSignatures(string contents, System.Colle foreach (Match sm in sigMatches) { string methodName = sm.Groups[1].Value; + if (IsCSharpKeyword(methodName)) continue; int paramCount = CountTopLevelParams(sm.Groups[2].Value); string paramTypes = ExtractParamTypes(sm.Groups[2].Value); string containingType = containingTypeArr[sm.Index]; @@ -2752,6 +2753,21 @@ private static void CheckDuplicateMethodSignatures(string contents, System.Colle } } + private static readonly System.Collections.Generic.HashSet CSharpKeywords = + new System.Collections.Generic.HashSet(System.StringComparer.Ordinal) + { + "if", "else", "for", "foreach", "while", "do", "switch", "case", + "try", "catch", "finally", "throw", "return", "yield", "await", + "lock", "using", "fixed", "checked", "unchecked", "typeof", "sizeof", + "nameof", "default", "new", "stackalloc", "when", "in", "is", "as", + "ref", "out", "params", "this", "base", "null", "true", "false", + "get", "set", "var", "dynamic", "where", "from", "select", "group", + "into", "orderby", "join", "let", "on", "equals", "by", "ascending", + "descending" + }; + + private static bool IsCSharpKeyword(string name) => CSharpKeywords.Contains(name); + /// /// Validates semantic rules and common coding issues /// diff --git a/Server/src/cli/CLI_USAGE_GUIDE.md b/Server/src/cli/CLI_USAGE_GUIDE.md index dfe0e3666..75d1cb8f6 100644 --- a/Server/src/cli/CLI_USAGE_GUIDE.md +++ b/Server/src/cli/CLI_USAGE_GUIDE.md @@ -604,12 +604,205 @@ unity-mcp animation set-parameter "Character" "IsRunning" true --type bool unity-mcp animation set-parameter "Character" "Jump" "" --type trigger ``` +### Camera Commands + +```bash +# Check Cinemachine availability +unity-mcp camera ping + +# List all cameras in scene +unity-mcp camera list + +# Create cameras (plain or with Cinemachine presets) +unity-mcp camera create # Basic camera +unity-mcp camera create --name "FollowCam" --preset follow --follow "Player" --look-at "Player" +unity-mcp camera create --preset third_person --follow "Player" --fov 50 +unity-mcp camera create --preset dolly --look-at "Player" +unity-mcp camera create --preset top_down --follow "Player" +unity-mcp camera create --preset side_scroller --follow "Player" +unity-mcp camera create --preset static --fov 40 + +# Set targets on existing camera +unity-mcp camera set-target "FollowCam" --follow "Player" --look-at "Enemy" + +# Lens settings +unity-mcp camera set-lens "MainCam" --fov 60 --near 0.1 --far 1000 +unity-mcp camera set-lens "OrthoCamera" --ortho-size 10 + +# Priority (higher = preferred by CinemachineBrain) +unity-mcp camera set-priority "FollowCam" --priority 15 + +# Cinemachine Body/Aim/Noise configuration +unity-mcp camera set-body "FollowCam" --body-type "CinemachineFollow" +unity-mcp camera set-body "FollowCam" --body-type "CinemachineFollow" --props '{"TrackerSettings": {"BindingMode": 1}}' +unity-mcp camera set-aim "FollowCam" --aim-type "CinemachineRotationComposer" +unity-mcp camera set-noise "FollowCam" --amplitude 1.5 --frequency 0.5 + +# Extensions +unity-mcp camera add-extension "FollowCam" CinemachineConfiner3D +unity-mcp camera remove-extension "FollowCam" CinemachineConfiner3D + +# Brain (ensure Brain exists on main camera, set default blend) +unity-mcp camera ensure-brain +unity-mcp camera ensure-brain --blend-style "EaseInOut" --blend-duration 1.5 +unity-mcp camera brain-status +unity-mcp camera set-blend --style "Cut" --duration 0 + +# Force/release camera override +unity-mcp camera force "FollowCam" +unity-mcp camera release + +# Screenshots +unity-mcp camera screenshot +unity-mcp camera screenshot --file-name "my_capture" --super-size 2 +unity-mcp camera screenshot --camera-ref "SecondCamera" --include-image +unity-mcp camera screenshot --max-resolution 256 +unity-mcp camera screenshot --batch surround --max-resolution 256 +unity-mcp camera screenshot --batch orbit --look-at "Player" +unity-mcp camera screenshot-multiview --look-at "Player" --max-resolution 480 +``` + +### Graphics Commands + +```bash +# Check graphics system status +unity-mcp graphics ping + +# --- Volumes --- +# Create a Volume (global or local) +unity-mcp graphics volume-create --name "PostProcessing" --global +unity-mcp graphics volume-create --name "LocalFog" --local --weight 0.8 --priority 1 + +# Add/remove/configure effects on a Volume +unity-mcp graphics volume-add-effect --target "PostProcessing" --effect "Bloom" +unity-mcp graphics volume-set-effect --target "PostProcessing" --effect "Bloom" -p intensity 1.5 -p threshold 0.9 +unity-mcp graphics volume-remove-effect --target "PostProcessing" --effect "Bloom" +unity-mcp graphics volume-info --target "PostProcessing" +unity-mcp graphics volume-set-properties --target "PostProcessing" --weight 0.5 --priority 2 --local +unity-mcp graphics volume-list-effects +unity-mcp graphics volume-create-profile --path "Assets/Profiles/MyProfile.asset" --name "MyProfile" + +# --- Render Pipeline --- +unity-mcp graphics pipeline-info +unity-mcp graphics pipeline-settings +unity-mcp graphics pipeline-set-quality --level "High" +unity-mcp graphics pipeline-set-settings -s renderScale 1.5 -s msaaSampleCount 4 + +# --- Light Baking --- +unity-mcp graphics bake-start +unity-mcp graphics bake-start --sync # Wait for completion +unity-mcp graphics bake-status +unity-mcp graphics bake-cancel +unity-mcp graphics bake-clear +unity-mcp graphics bake-settings +unity-mcp graphics bake-set-settings -s lightmapResolution 64 -s directSamples 32 +unity-mcp graphics bake-reflection-probe --target "ReflectionProbe1" +unity-mcp graphics bake-create-probes --name "LightProbes" --spacing 5 +unity-mcp graphics bake-create-reflection --name "ReflProbe" --resolution 512 --mode Realtime + +# --- Rendering Stats --- +unity-mcp graphics stats +unity-mcp graphics stats-memory +unity-mcp graphics stats-debug-mode --mode "Wireframe" + +# --- URP Renderer Features --- +unity-mcp graphics feature-list +unity-mcp graphics feature-add --type "ScreenSpaceAmbientOcclusion" --name "SSAO" +unity-mcp graphics feature-remove --name "SSAO" +unity-mcp graphics feature-configure --name "SSAO" -p Intensity 1.5 -p Radius 0.3 +unity-mcp graphics feature-reorder --order "0,2,1,3" +unity-mcp graphics feature-toggle --name "SSAO" --active +unity-mcp graphics feature-toggle --name "SSAO" --inactive + +# --- Skybox & Environment --- +unity-mcp graphics skybox-info +unity-mcp graphics skybox-set-material --material "Assets/Materials/NightSky.mat" +unity-mcp graphics skybox-set-properties -p _Tint "0.5,0.5,1,1" -p _Exposure 1.2 +unity-mcp graphics skybox-set-ambient --mode Flat --color "0.2,0.2,0.3" +unity-mcp graphics skybox-set-ambient --mode Trilight --color "0.4,0.6,0.8" --equator-color "0.3,0.3,0.3" --ground-color "0.1,0.1,0.1" +unity-mcp graphics skybox-set-fog --enable --mode ExponentialSquared --color "0.7,0.8,0.9" --density 0.02 +unity-mcp graphics skybox-set-fog --disable +unity-mcp graphics skybox-set-reflection --intensity 1.0 --bounces 2 --mode Custom --resolution 256 +unity-mcp graphics skybox-set-sun --target "DirectionalLight" +``` + +### Package Commands + +```bash +# Check package manager status +unity-mcp packages ping + +# List installed packages +unity-mcp packages list + +# Search Unity registry +unity-mcp packages search "cinemachine" +unity-mcp packages search "probuilder" + +# Get package details +unity-mcp packages info "com.unity.cinemachine" + +# Install / remove packages +unity-mcp packages add "com.unity.cinemachine" +unity-mcp packages add "com.unity.cinemachine@4.1.1" +unity-mcp packages remove "com.unity.cinemachine" +unity-mcp packages remove "com.unity.cinemachine" --force # Skip confirmation + +# Embed package for local editing +unity-mcp packages embed "com.unity.cinemachine" + +# Force package re-resolution +unity-mcp packages resolve + +# Check async operation status +unity-mcp packages status + +# Scoped registries +unity-mcp packages list-registries +unity-mcp packages add-registry "My Registry" --url "https://registry.example.com" -s "com.example" +unity-mcp packages remove-registry "My Registry" +``` + +### Texture Commands + +```bash +# Create procedural textures +unity-mcp texture create "Assets/Textures/Red.png" --width 128 --height 128 --color "1,0,0,1" +unity-mcp texture create "Assets/Textures/Check.png" --pattern checkerboard --palette "1,0,0,1;0,0,1,1" +unity-mcp texture create "Assets/Textures/Brick.png" --width 256 --height 256 --pattern brick +unity-mcp texture create "Assets/Textures/Grid.png" --pattern grid --width 512 --height 512 + +# Available patterns: checkerboard, stripes, stripes_h, stripes_v, stripes_diag, dots, grid, brick + +# Create from image file +unity-mcp texture create "Assets/Textures/Photo.png" --image-path "/path/to/source.png" + +# Create with custom import settings +unity-mcp texture create "Assets/Textures/Normal.png" --import-settings '{"textureType": "NormalMap", "filterMode": "Trilinear"}' + +# Create sprites (auto-configures import settings for 2D) +unity-mcp texture sprite "Assets/Sprites/Player.png" --width 32 --height 32 --color "0,0.5,1,1" +unity-mcp texture sprite "Assets/Sprites/Tile.png" --pattern checkerboard --ppu 16 --pivot "0.5,0" + +# Modify existing texture pixels +unity-mcp texture modify "Assets/Textures/Existing.png" --set-pixels '{"x":0,"y":0,"width":16,"height":16,"color":[1,0,0,1]}' + +# Delete texture +unity-mcp texture delete "Assets/Textures/Old.png" +unity-mcp texture delete "Assets/Textures/Old.png" --force +``` + ### Code Commands ```bash # Read source files unity-mcp code read "Assets/Scripts/Player.cs" unity-mcp code read "Assets/Scripts/Player.cs" --start-line 10 --line-count 20 + +# Search with regex +unity-mcp code search "class.*Player" "Assets/Scripts/Player.cs" +unity-mcp code search "TODO|FIXME" "Assets/Scripts/Utils.cs" +unity-mcp code search "void Update" "Assets/Scripts/Game.cs" --max-results 20 ``` ### Raw Commands @@ -622,6 +815,9 @@ unity-mcp raw manage_scene '{"action": "get_active"}' unity-mcp raw manage_gameobject '{"action": "create", "name": "Test"}' unity-mcp raw manage_components '{"action": "add", "target": "Test", "componentType": "Rigidbody"}' unity-mcp raw manage_editor '{"action": "play"}' +unity-mcp raw manage_camera '{"action": "screenshot", "include_image": true}' +unity-mcp raw manage_graphics '{"action": "volume_get_info", "target": "PostProcessing"}' +unity-mcp raw manage_packages '{"action": "list_packages"}' ``` --- diff --git a/Server/src/services/tools/utils.py b/Server/src/services/tools/utils.py index b7c1a3e4d..aea7323b4 100644 --- a/Server/src/services/tools/utils.py +++ b/Server/src/services/tools/utils.py @@ -406,7 +406,7 @@ def extract_screenshot_images(response: dict[str, Any]) -> "ToolResult | None": """If a Unity response contains inline base64 images, return a ToolResult with TextContent + ImageContent blocks. Returns None for normal text-only responses. - Shared by manage_scene and manage_camera screenshot handling. + Shared screenshot handling (used by manage_camera). """ from fastmcp.server.server import ToolResult from mcp.types import TextContent, ImageContent @@ -476,7 +476,7 @@ def build_screenshot_params( """Populate screenshot-related keys in *params* dict. Returns an error dict if validation fails, or None on success. - Shared by manage_scene and manage_camera screenshot handling. + Shared screenshot handling (used by manage_camera). """ if screenshot_file_name: params["fileName"] = screenshot_file_name diff --git a/Server/src/transport/plugin_hub.py b/Server/src/transport/plugin_hub.py index 4821f8e1b..1a71f30a7 100644 --- a/Server/src/transport/plugin_hub.py +++ b/Server/src/transport/plugin_hub.py @@ -426,8 +426,37 @@ async def _handle_register(self, websocket: WebSocket, payload: RegisterMessage) response = RegisteredMessage(session_id=session_id) await websocket.send_json(response.model_dump()) - session = await registry.register(session_id, project_name, project_hash, unity_version, project_path, user_id=user_id) + session, evicted_session_id = await registry.register(session_id, project_name, project_hash, unity_version, project_path, user_id=user_id) + evicted_ws = None async with lock: + # Clean up the evicted session's connection, ping loop, and pending commands + # so they don't linger as orphans after a domain-reload reconnection race. + if evicted_session_id: + evicted_ws = cls._connections.pop(evicted_session_id, None) + old_ping = cls._ping_tasks.pop(evicted_session_id, None) + if old_ping and not old_ping.done(): + old_ping.cancel() + cls._last_pong.pop(evicted_session_id, None) + cancelled_commands = [] + for command_id, entry in list(cls._pending.items()): + if entry.get("session_id") == evicted_session_id: + future = entry.get("future") + if future and not future.done(): + future.set_exception( + PluginDisconnectedError( + f"Unity plugin session {evicted_session_id} superseded by {session_id}" + ) + ) + cancelled_commands.append(command_id) + cls._pending.pop(command_id, None) + if cancelled_commands: + logger.info( + "Evicted session %s: cancelled pending commands %s", + evicted_session_id, + cancelled_commands, + ) + logger.info(f"Evicted previous session {evicted_session_id} for same instance") + cls._connections[session.session_id] = websocket # Initialize last pong time and start ping loop for this session cls._last_pong[session_id] = time.monotonic() @@ -439,6 +468,17 @@ async def _handle_register(self, websocket: WebSocket, payload: RegisterMessage) ping_task = asyncio.create_task(cls._ping_loop(session_id, websocket)) cls._ping_tasks[session_id] = ping_task + # Close evicted WebSocket outside the lock to avoid blocking + if evicted_ws is not None: + try: + await evicted_ws.close(code=1001) + except Exception: + logger.debug( + "Failed to close evicted WebSocket for session %s", + evicted_session_id, + exc_info=True, + ) + if user_id: logger.info(f"Plugin registered: {project_name} ({project_hash}) for user {user_id}") else: diff --git a/Server/src/transport/plugin_registry.py b/Server/src/transport/plugin_registry.py index 5f8f75b29..a8b34ec5b 100644 --- a/Server/src/transport/plugin_registry.py +++ b/Server/src/transport/plugin_registry.py @@ -55,12 +55,16 @@ async def register( unity_version: str, project_path: str | None = None, user_id: str | None = None, - ) -> PluginSession: + ) -> tuple[PluginSession, str | None]: """Register (or replace) a plugin session. If an existing session already claims the same ``project_hash`` (and ``user_id`` in remote-hosted mode) it will be replaced, ensuring that reconnect scenarios always map to the latest WebSocket connection. + + Returns: + A tuple of (new_session, evicted_session_id). The evicted ID is None + when no previous session was replaced. """ if config.http_remote_hosted and not user_id: raise ValueError("user_id is required in remote-hosted mode") @@ -79,6 +83,7 @@ async def register( ) # Remove old mapping for this hash if it existed under a different session + evicted_session_id: str | None = None if user_id: # Remote-hosted mode: use composite key (user_id, project_hash) composite_key = (user_id, project_hash) @@ -86,16 +91,18 @@ async def register( composite_key) if previous_session_id and previous_session_id != session_id: self._sessions.pop(previous_session_id, None) + evicted_session_id = previous_session_id self._user_hash_to_session[composite_key] = session_id else: # Local mode: use project_hash only previous_session_id = self._hash_to_session.get(project_hash) if previous_session_id and previous_session_id != session_id: self._sessions.pop(previous_session_id, None) + evicted_session_id = previous_session_id self._hash_to_session[project_hash] = session_id self._sessions[session_id] = session - return session + return session, evicted_session_id async def touch(self, session_id: str) -> None: """Update the ``connected_at`` timestamp when a heartbeat is received.""" diff --git a/Server/tests/integration/test_plugin_registry_user_isolation.py b/Server/tests/integration/test_plugin_registry_user_isolation.py index 2ce0aa047..009fb956d 100644 --- a/Server/tests/integration/test_plugin_registry_user_isolation.py +++ b/Server/tests/integration/test_plugin_registry_user_isolation.py @@ -10,7 +10,7 @@ class TestRegistryUserIsolation: @pytest.mark.asyncio async def test_register_with_user_id_stores_composite_key(self): registry = PluginRegistry() - session = await registry.register( + session, _ = await registry.register( "sess-1", "MyProject", "hash1", "2022.3", user_id="user-A" ) assert session.user_id == "user-A" @@ -29,15 +29,35 @@ async def test_get_session_id_by_hash(self): not_found = await registry.get_session_id_by_hash("h1", "uB") assert not_found is None + @pytest.mark.asyncio + async def test_register_same_user_same_hash_evicts_previous_session(self): + """Same user + project_hash: second registration evicts the first session.""" + registry = PluginRegistry() + + first_session, first_evicted = await registry.register( + "sess-1", "MyProject", "hash1", "2022.3", user_id="user-A" + ) + assert first_session.session_id == "sess-1" + assert first_evicted is None + + second_session, second_evicted = await registry.register( + "sess-2", "MyProject", "hash1", "2022.3", user_id="user-A" + ) + assert second_session.session_id == "sess-2" + assert second_evicted == "sess-1" + @pytest.mark.asyncio async def test_cross_user_isolation_same_hash(self): """Two users registering with the same project_hash get independent sessions.""" registry = PluginRegistry() - sess_a = await registry.register("sA", "Proj", "hash1", "2022", user_id="userA") - sess_b = await registry.register("sB", "Proj", "hash1", "2022", user_id="userB") + sess_a, evicted_a = await registry.register("sA", "Proj", "hash1", "2022", user_id="userA") + sess_b, evicted_b = await registry.register("sB", "Proj", "hash1", "2022", user_id="userB") assert sess_a.session_id == "sA" assert sess_b.session_id == "sB" + # Different users should not evict each other's sessions + assert evicted_a is None + assert evicted_b is None # Each user resolves to their own session assert await registry.get_session_id_by_hash("hash1", "userA") == "sA" diff --git a/Server/tests/test_transport_characterization.py b/Server/tests/test_transport_characterization.py index f8b04b65e..45e35e6a8 100644 --- a/Server/tests/test_transport_characterization.py +++ b/Server/tests/test_transport_characterization.py @@ -816,7 +816,7 @@ async def test_registry_registers_session(self, plugin_registry): Current behavior: register() creates a new PluginSession and stores it by session_id and project_hash. """ - session = await plugin_registry.register( + session, _ = await plugin_registry.register( session_id="sess-abc", project_name="TestProject", project_hash="hash123", @@ -904,7 +904,7 @@ async def test_registry_touch_updates_connected_at(self, plugin_registry): """ Current behavior: touch() updates the connected_at timestamp on heartbeat. """ - session = await plugin_registry.register( + session, _ = await plugin_registry.register( session_id="sess-y", project_name="Project", project_hash="hash-y", diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptValidationTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptValidationTests.cs index fb0c09593..259feb4ca 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptValidationTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptValidationTests.cs @@ -286,5 +286,38 @@ public void Process(Dictionary other) { } Assert.IsTrue(HasDuplicateMethodError(errors), "Generic param duplicates with different names should be flagged"); } + + // --- Keyword false positive tests --- + + [Test] + public void DuplicateDetection_CSharpKeywords_NotMatchedAsMethods() + { + string code = @"using UnityEngine; +public class Foo : MonoBehaviour +{ + public void Update() + { + if (true) { } + if (true) { } + for (int i = 0; i < 10; i++) { } + for (int j = 0; j < 5; j++) { } + while (true) { break; } + while (false) { break; } + foreach (var x in new int[0]) { } + foreach (var y in new int[0]) { } + switch (0) { default: break; } + switch (1) { default: break; } + lock (this) { } + lock (this) { } + using (var d = new System.IO.MemoryStream()) { } + using (var e = new System.IO.MemoryStream()) { } + typeof(int); + typeof(string); + } +}"; + var errors = CallValidateScriptSyntaxUnity(code); + Assert.IsFalse(HasDuplicateMethodError(errors), + "C# keywords (if, for, while, etc.) should not be matched as duplicate methods"); + } } } diff --git a/docs/guides/CLI_EXAMPLE.md b/docs/guides/CLI_EXAMPLE.md index dd9c3d731..6de104ed7 100644 --- a/docs/guides/CLI_EXAMPLE.md +++ b/docs/guides/CLI_EXAMPLE.md @@ -145,6 +145,71 @@ unity-mcp vfx line create-circle "Name" --radius N unity-mcp vfx trail info|set-time|clear "Name" [time] ``` +**Camera Operations** +```bash +unity-mcp camera ping # Check Cinemachine +unity-mcp camera list # List all cameras +unity-mcp camera create --name "Cam" --preset follow --follow "Player" +unity-mcp camera set-target "Cam" --follow "Player" --look-at "Enemy" +unity-mcp camera set-lens "Cam" --fov 60 --near 0.1 +unity-mcp camera set-priority "Cam" --priority 15 +unity-mcp camera set-body "Cam" --body-type "CinemachineFollow" +unity-mcp camera set-aim "Cam" --aim-type "CinemachineRotationComposer" +unity-mcp camera set-noise "Cam" --amplitude 1.5 --frequency 0.5 +unity-mcp camera ensure-brain --blend-style "EaseInOut" --blend-duration 1.5 +unity-mcp camera force "Cam" # Force Brain to use camera +unity-mcp camera release # Release override +``` + +**Graphics Operations** +```bash +# Volumes +unity-mcp graphics volume-create --name "PostFX" --global +unity-mcp graphics volume-add-effect --target "PostFX" --effect "Bloom" +unity-mcp graphics volume-set-effect --target "PostFX" --effect "Bloom" -p intensity 1.5 +unity-mcp graphics volume-info --target "PostFX" +# Pipeline +unity-mcp graphics pipeline-info +unity-mcp graphics pipeline-set-quality --level "High" +# Baking +unity-mcp graphics bake-start [--sync] +unity-mcp graphics bake-status +unity-mcp graphics bake-create-probes --spacing 5 +# Stats +unity-mcp graphics stats +unity-mcp graphics stats-memory +# URP Features +unity-mcp graphics feature-list +unity-mcp graphics feature-add --type "ScreenSpaceAmbientOcclusion" +# Skybox +unity-mcp graphics skybox-info +unity-mcp graphics skybox-set-fog --enable --mode ExponentialSquared --density 0.02 +unity-mcp graphics skybox-set-sun --target "DirectionalLight" +``` + +**Package Operations** +```bash +unity-mcp packages list # List installed +unity-mcp packages search "cinemachine" # Search registry +unity-mcp packages info "com.unity.cinemachine" # Details +unity-mcp packages add "com.unity.cinemachine" # Install +unity-mcp packages add "com.unity.cinemachine@4.1.1" # Specific version +unity-mcp packages remove "com.unity.cinemachine" [--force] +unity-mcp packages embed "com.unity.cinemachine" # Local editing +unity-mcp packages resolve # Re-resolve +unity-mcp packages list-registries +unity-mcp packages add-registry "Name" --url URL -s "com.example" +``` + +**Texture Operations** +```bash +unity-mcp texture create "Assets/Textures/Red.png" --color "1,0,0,1" +unity-mcp texture create "Assets/Textures/Check.png" --pattern checkerboard --width 256 --height 256 +unity-mcp texture sprite "Assets/Sprites/Player.png" --width 32 --height 32 --ppu 16 +unity-mcp texture modify "Assets/Textures/Img.png" --set-pixels '{"x":0,"y":0,"width":16,"height":16,"color":[1,0,0,1]}' +unity-mcp texture delete "Assets/Textures/Old.png" [--force] +``` + **Lighting & UI** ```bash unity-mcp lighting create "Name" --type Point|Spot [--intensity N] [--position X Y Z] @@ -164,6 +229,9 @@ unity-mcp batch template > commands.json ```bash unity-mcp raw tool_name 'JSON_params' unity-mcp raw manage_scene '{"action":"get_active"}' +unity-mcp raw manage_camera '{"action":"screenshot","include_image":true}' +unity-mcp raw manage_graphics '{"action":"volume_get_info","target":"PostProcessing"}' +unity-mcp raw manage_packages '{"action":"list_packages"}' ``` ### Note on MCP Server diff --git a/docs/guides/CLI_USAGE.md b/docs/guides/CLI_USAGE.md index 81bdc6f09..1c9601363 100644 --- a/docs/guides/CLI_USAGE.md +++ b/docs/guides/CLI_USAGE.md @@ -185,7 +185,7 @@ unity-mcp custom_tool list |--------|------|-------------| | `--filename, -f` | string | Output filename (default: timestamp-based) | | `--supersize, -s` | int | Resolution multiplier 1–4 for file-saved screenshots | -| `--camera, -c` | string | Camera name/path/ID (default: Camera.main) | +| `--camera-ref` | string | Camera name/path/ID (default: Camera.main) | | `--include-image` | flag | Return base64 PNG inline in the response | | `--max-resolution, -r` | int | Max longest-edge pixels (default 640) | | `--batch, -b` | string | `surround` (6 angles) or `orbit` (configurable grid) | @@ -362,6 +362,101 @@ unity-mcp ui create-button "StartBtn" --parent "MainCanvas" --text "Start" unity-mcp ui create-image "Background" --parent "MainCanvas" ``` +### Camera Operations + +```bash +unity-mcp camera ping # Check Cinemachine availability +unity-mcp camera list # List all cameras +unity-mcp camera create --name "Cam" --preset follow --follow "Player" +unity-mcp camera set-target "Cam" --follow "Player" --look-at "Enemy" +unity-mcp camera set-lens "Cam" --fov 60 --near 0.1 --far 1000 +unity-mcp camera set-priority "Cam" --priority 15 +unity-mcp camera set-body "Cam" --body-type "CinemachineFollow" +unity-mcp camera set-aim "Cam" --aim-type "CinemachineRotationComposer" +unity-mcp camera set-noise "Cam" --amplitude 1.5 --frequency 0.5 +unity-mcp camera add-extension "Cam" CinemachineConfiner3D +unity-mcp camera ensure-brain --blend-style "EaseInOut" --blend-duration 1.5 +unity-mcp camera brain-status +unity-mcp camera force "Cam" # Force Brain to use camera +unity-mcp camera release # Release override +unity-mcp camera screenshot --file-name "capture" --super-size 2 +unity-mcp camera screenshot --batch orbit --look-at "Player" --max-resolution 256 +unity-mcp camera screenshot-multiview --look-at "Player" --max-resolution 480 +``` + +### Graphics Operations + +```bash +# Volumes +unity-mcp graphics volume-create --name "PostFX" --global +unity-mcp graphics volume-add-effect --target "PostFX" --effect "Bloom" +unity-mcp graphics volume-set-effect --target "PostFX" --effect "Bloom" -p intensity 1.5 +unity-mcp graphics volume-info --target "PostFX" +unity-mcp graphics volume-list-effects + +# Render Pipeline +unity-mcp graphics pipeline-info +unity-mcp graphics pipeline-set-quality --level "High" +unity-mcp graphics pipeline-set-settings -s renderScale 1.5 + +# Light Baking +unity-mcp graphics bake-start [--sync] +unity-mcp graphics bake-status +unity-mcp graphics bake-cancel +unity-mcp graphics bake-settings +unity-mcp graphics bake-create-probes --spacing 5 +unity-mcp graphics bake-create-reflection --resolution 512 + +# Stats & Debug +unity-mcp graphics stats +unity-mcp graphics stats-memory +unity-mcp graphics stats-debug-mode --mode "Wireframe" + +# URP Renderer Features +unity-mcp graphics feature-list +unity-mcp graphics feature-add --type "ScreenSpaceAmbientOcclusion" +unity-mcp graphics feature-configure --name "SSAO" -p Intensity 1.5 +unity-mcp graphics feature-toggle --name "SSAO" --active|--inactive + +# Skybox & Environment +unity-mcp graphics skybox-info +unity-mcp graphics skybox-set-material --material "Assets/Materials/Sky.mat" +unity-mcp graphics skybox-set-ambient --mode Flat --color "0.2,0.2,0.3" +unity-mcp graphics skybox-set-fog --enable --mode ExponentialSquared --density 0.02 +unity-mcp graphics skybox-set-reflection --intensity 1.0 --bounces 2 +unity-mcp graphics skybox-set-sun --target "DirectionalLight" +``` + +### Package Operations + +```bash +unity-mcp packages ping # Check package manager +unity-mcp packages list # List installed packages +unity-mcp packages search "cinemachine" # Search registry +unity-mcp packages info "com.unity.cinemachine" # Package details +unity-mcp packages add "com.unity.cinemachine" # Install package +unity-mcp packages add "com.unity.cinemachine@4.1.1" # Specific version +unity-mcp packages remove "com.unity.cinemachine" [--force] +unity-mcp packages embed "com.unity.cinemachine" # Embed for local editing +unity-mcp packages resolve # Force re-resolution +unity-mcp packages status # Check async op +unity-mcp packages list-registries +unity-mcp packages add-registry "Name" --url URL -s "com.example" +unity-mcp packages remove-registry "Name" +``` + +### Texture Operations + +```bash +unity-mcp texture create "Assets/Textures/Red.png" --color "1,0,0,1" +unity-mcp texture create "Assets/Textures/Check.png" --pattern checkerboard --width 256 --height 256 +unity-mcp texture create "Assets/Textures/Img.png" --image-path "/path/to/source.png" +unity-mcp texture sprite "Assets/Sprites/Player.png" --width 32 --height 32 --ppu 16 +unity-mcp texture modify "Assets/Textures/Img.png" --set-pixels '{"x":0,"y":0,"width":16,"height":16,"color":[1,0,0,1]}' +unity-mcp texture delete "Assets/Textures/Old.png" [--force] +# Patterns: checkerboard, stripes, stripes_h, stripes_v, stripes_diag, dots, grid, brick +``` + ### Raw Commands For any MCP tool not covered by dedicated commands: @@ -369,6 +464,9 @@ For any MCP tool not covered by dedicated commands: ```bash unity-mcp raw manage_scene '{"action": "get_hierarchy", "max_nodes": 100}' unity-mcp raw read_console '{"count": 20}' +unity-mcp raw manage_camera '{"action": "screenshot", "include_image": true}' +unity-mcp raw manage_graphics '{"action": "volume_get_info", "target": "PostProcessing"}' +unity-mcp raw manage_packages '{"action": "list_packages"}' ``` --- @@ -388,11 +486,15 @@ unity-mcp raw read_console '{"count": 20}' | `asset` | `search`, `info`, `create`, `delete`, `duplicate`, `move`, `rename`, `import`, `mkdir` | | `prefab` | `open`, `close`, `save`, `create` | | `material` | `info`, `create`, `set-color`, `set-property`, `assign`, `set-renderer-color` | +| `camera` | `ping`, `list`, `create`, `set-target`, `set-lens`, `set-priority`, `set-body`, `set-aim`, `set-noise`, `add-extension`, `remove-extension`, `ensure-brain`, `brain-status`, `set-blend`, `force`, `release`, `screenshot`, `screenshot-multiview` | +| `graphics` | `ping`, `volume-create`, `volume-add-effect`, `volume-set-effect`, `volume-remove-effect`, `volume-info`, `volume-set-properties`, `volume-list-effects`, `volume-create-profile`, `pipeline-info`, `pipeline-settings`, `pipeline-set-quality`, `pipeline-set-settings`, `bake-start`, `bake-cancel`, `bake-status`, `bake-clear`, `bake-settings`, `bake-set-settings`, `bake-reflection-probe`, `bake-create-probes`, `bake-create-reflection`, `stats`, `stats-memory`, `stats-debug-mode`, `feature-list`, `feature-add`, `feature-remove`, `feature-configure`, `feature-reorder`, `feature-toggle`, `skybox-info`, `skybox-set-material`, `skybox-set-properties`, `skybox-set-ambient`, `skybox-set-fog`, `skybox-set-reflection`, `skybox-set-sun` | +| `packages` | `ping`, `list`, `search`, `info`, `add`, `remove`, `embed`, `resolve`, `status`, `list-registries`, `add-registry`, `remove-registry` | +| `texture` | `create`, `sprite`, `modify`, `delete` | | `vfx particle` | `info`, `play`, `stop`, `pause`, `restart`, `clear` | | `vfx line` | `info`, `set-positions`, `create-line`, `create-circle`, `clear` | | `vfx trail` | `info`, `set-time`, `clear` | | `vfx` | `raw` (access all 60+ actions) | -| `probuilder` | `create-shape`, `create-poly`, `info`, `raw` (access all 21 actions) | +| `probuilder` | `create-shape`, `create-poly`, `info`, `raw` (access all 35+ actions) | | `batch` | `run`, `inline`, `template` | | `animation` | `play`, `set-parameter` | | `audio` | `play`, `stop`, `volume` | diff --git a/unity-mcp-skill/SKILL.md b/unity-mcp-skill/SKILL.md index aa780bd06..83a8824a6 100644 --- a/unity-mcp-skill/SKILL.md +++ b/unity-mcp-skill/SKILL.md @@ -24,7 +24,7 @@ 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 @@ -90,7 +90,7 @@ manage_gameobject(action="look_at", target="MainCamera", look_at_target="Player" 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 ```