Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions MCPForUnity/Editor/Tools/Graphics/SkyboxOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ---------------------------------------------------------------
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -301,7 +317,7 @@ public static object SetReflection(JObject @params)
{
var cubemap = AssetDatabase.LoadAssetAtPath<Texture>(cubemapPath);
if (cubemap != null)
RenderSettings.customReflectionTexture = cubemap;
CustomReflectionTexture = cubemap;
else
return new ErrorResponse($"Cubemap not found at '{cubemapPath}'.");
}
Expand All @@ -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
}
};
Expand Down
16 changes: 16 additions & 0 deletions MCPForUnity/Editor/Tools/ManageScript.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -2752,6 +2753,21 @@ private static void CheckDuplicateMethodSignatures(string contents, System.Colle
}
}

private static readonly System.Collections.Generic.HashSet<string> CSharpKeywords =
new System.Collections.Generic.HashSet<string>(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);

/// <summary>
/// Validates semantic rules and common coding issues
/// </summary>
Expand Down
196 changes: 196 additions & 0 deletions Server/src/cli/CLI_USAGE_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <job_id>

# 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
Expand All @@ -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"}'
```

---
Expand Down
4 changes: 2 additions & 2 deletions Server/src/services/tools/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
42 changes: 41 additions & 1 deletion Server/src/transport/plugin_hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Comment on lines +429 to 431
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential deadlock due to lock-order inversion between PluginHub’s _lock and PluginRegistry’s internal _lock: _handle_register awaits registry.register(...) before acquiring PluginHub._lock, while on_disconnect acquires PluginHub._lock and then awaits registry.unregister(...) (which takes the registry lock). If these happen concurrently, each task can end up waiting on the other lock. To avoid this, enforce a consistent acquisition order (e.g., never await PluginRegistry methods while holding PluginHub._lock, or acquire PluginHub._lock first in _handle_register and keep registry calls within that order).

Suggested change
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:
evicted_ws = None
async with lock:
session, evicted_session_id = await registry.register(
session_id,
project_name,
project_hash,
unity_version,
project_path,
user_id=user_id,
)

Copilot uses AI. Check for mistakes.
# 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()
Expand All @@ -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:
Expand Down
Loading