Skip to content

Commit 8e122e3

Browse files
committed
Plugin eviction, CLI camera/graphics, minor fixes
Handle plugin session evictions and extend CLI/docs; misc Unity tooling fixes. - Server: PluginRegistry.register now returns (session, evicted_session_id). PluginHub consumes the evicted id to cancel ping tasks, clear pending commands, remove state, and close the old WebSocket to avoid orphaned connections after reconnection races. Tests updated to unpack the returned tuple. - CLI/docs: Add extensive camera, graphics, packages, and texture command examples and raw command snippets across CLI guides and examples; update command reference tables. - Unity editor tools: SkyboxOps adds a CustomReflectionTexture accessor to handle Unity API differences (RenderSettings.customReflection vs customReflectionTexture). ManageScript now ignores C# keywords when detecting duplicate methods to prevent false positives; added unit test to cover keyword cases. - Misc: Small docstring tweaks in services/tools/utils.py and skill docs updated to reference manage_camera screenshots.
1 parent ad998f1 commit 8e122e3

12 files changed

Lines changed: 484 additions & 18 deletions

File tree

MCPForUnity/Editor/Tools/Graphics/SkyboxOps.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@ namespace MCPForUnity.Editor.Tools.Graphics
1010
{
1111
internal static class SkyboxOps
1212
{
13+
static Texture CustomReflectionTexture
14+
{
15+
get =>
16+
#if UNITY_2022_1_OR_NEWER
17+
RenderSettings.customReflectionTexture;
18+
#else
19+
RenderSettings.customReflection;
20+
#endif
21+
set {
22+
#if UNITY_2022_1_OR_NEWER
23+
RenderSettings.customReflectionTexture = value;
24+
#else
25+
RenderSettings.customReflection = value;
26+
#endif
27+
}
28+
}
1329
// ---------------------------------------------------------------
1430
// skybox_get — read all environment settings
1531
// ---------------------------------------------------------------
@@ -70,8 +86,8 @@ public static object GetEnvironment(JObject @params)
7086
bounces = RenderSettings.reflectionBounces,
7187
mode = RenderSettings.defaultReflectionMode.ToString(),
7288
resolution = RenderSettings.defaultReflectionResolution,
73-
customCubemap = RenderSettings.customReflectionTexture != null
74-
? AssetDatabase.GetAssetPath(RenderSettings.customReflectionTexture)
89+
customCubemap = CustomReflectionTexture != null
90+
? AssetDatabase.GetAssetPath(CustomReflectionTexture)
7591
: null
7692
},
7793
sun = sun != null
@@ -301,7 +317,7 @@ public static object SetReflection(JObject @params)
301317
{
302318
var cubemap = AssetDatabase.LoadAssetAtPath<Texture>(cubemapPath);
303319
if (cubemap != null)
304-
RenderSettings.customReflectionTexture = cubemap;
320+
CustomReflectionTexture = cubemap;
305321
else
306322
return new ErrorResponse($"Cubemap not found at '{cubemapPath}'.");
307323
}
@@ -318,8 +334,8 @@ public static object SetReflection(JObject @params)
318334
bounces = RenderSettings.reflectionBounces,
319335
mode = RenderSettings.defaultReflectionMode.ToString(),
320336
resolution = RenderSettings.defaultReflectionResolution,
321-
customCubemap = RenderSettings.customReflectionTexture != null
322-
? AssetDatabase.GetAssetPath(RenderSettings.customReflectionTexture)
337+
customCubemap = CustomReflectionTexture != null
338+
? AssetDatabase.GetAssetPath(CustomReflectionTexture)
323339
: null
324340
}
325341
};

MCPForUnity/Editor/Tools/ManageScript.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2741,6 +2741,7 @@ private static void CheckDuplicateMethodSignatures(string contents, System.Colle
27412741
foreach (Match sm in sigMatches)
27422742
{
27432743
string methodName = sm.Groups[1].Value;
2744+
if (IsCSharpKeyword(methodName)) continue;
27442745
int paramCount = CountTopLevelParams(sm.Groups[2].Value);
27452746
string paramTypes = ExtractParamTypes(sm.Groups[2].Value);
27462747
string containingType = containingTypeArr[sm.Index];
@@ -2752,6 +2753,21 @@ private static void CheckDuplicateMethodSignatures(string contents, System.Colle
27522753
}
27532754
}
27542755

2756+
private static readonly System.Collections.Generic.HashSet<string> CSharpKeywords =
2757+
new System.Collections.Generic.HashSet<string>(System.StringComparer.Ordinal)
2758+
{
2759+
"if", "else", "for", "foreach", "while", "do", "switch", "case",
2760+
"try", "catch", "finally", "throw", "return", "yield", "await",
2761+
"lock", "using", "fixed", "checked", "unchecked", "typeof", "sizeof",
2762+
"nameof", "default", "new", "stackalloc", "when", "in", "is", "as",
2763+
"ref", "out", "params", "this", "base", "null", "true", "false",
2764+
"get", "set", "var", "dynamic", "where", "from", "select", "group",
2765+
"into", "orderby", "join", "let", "on", "equals", "by", "ascending",
2766+
"descending"
2767+
};
2768+
2769+
private static bool IsCSharpKeyword(string name) => CSharpKeywords.Contains(name);
2770+
27552771
/// <summary>
27562772
/// Validates semantic rules and common coding issues
27572773
/// </summary>

Server/src/cli/CLI_USAGE_GUIDE.md

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,12 +604,205 @@ unity-mcp animation set-parameter "Character" "IsRunning" true --type bool
604604
unity-mcp animation set-parameter "Character" "Jump" "" --type trigger
605605
```
606606

607+
### Camera Commands
608+
609+
```bash
610+
# Check Cinemachine availability
611+
unity-mcp camera ping
612+
613+
# List all cameras in scene
614+
unity-mcp camera list
615+
616+
# Create cameras (plain or with Cinemachine presets)
617+
unity-mcp camera create # Basic camera
618+
unity-mcp camera create --name "FollowCam" --preset follow --follow "Player" --look-at "Player"
619+
unity-mcp camera create --preset third_person --follow "Player" --fov 50
620+
unity-mcp camera create --preset dolly --look-at "Player"
621+
unity-mcp camera create --preset top_down --follow "Player"
622+
unity-mcp camera create --preset side_scroller --follow "Player"
623+
unity-mcp camera create --preset static --fov 40
624+
625+
# Set targets on existing camera
626+
unity-mcp camera set-target "FollowCam" --follow "Player" --look-at "Enemy"
627+
628+
# Lens settings
629+
unity-mcp camera set-lens "MainCam" --fov 60 --near 0.1 --far 1000
630+
unity-mcp camera set-lens "OrthoCamera" --ortho-size 10
631+
632+
# Priority (higher = preferred by CinemachineBrain)
633+
unity-mcp camera set-priority "FollowCam" --priority 15
634+
635+
# Cinemachine Body/Aim/Noise configuration
636+
unity-mcp camera set-body "FollowCam" --body-type "CinemachineFollow"
637+
unity-mcp camera set-body "FollowCam" --body-type "CinemachineFollow" --props '{"TrackerSettings": {"BindingMode": 1}}'
638+
unity-mcp camera set-aim "FollowCam" --aim-type "CinemachineRotationComposer"
639+
unity-mcp camera set-noise "FollowCam" --amplitude 1.5 --frequency 0.5
640+
641+
# Extensions
642+
unity-mcp camera add-extension "FollowCam" CinemachineConfiner3D
643+
unity-mcp camera remove-extension "FollowCam" CinemachineConfiner3D
644+
645+
# Brain (ensure Brain exists on main camera, set default blend)
646+
unity-mcp camera ensure-brain
647+
unity-mcp camera ensure-brain --blend-style "EaseInOut" --blend-duration 1.5
648+
unity-mcp camera brain-status
649+
unity-mcp camera set-blend --style "Cut" --duration 0
650+
651+
# Force/release camera override
652+
unity-mcp camera force "FollowCam"
653+
unity-mcp camera release
654+
655+
# Screenshots
656+
unity-mcp camera screenshot
657+
unity-mcp camera screenshot --file-name "my_capture" --super-size 2
658+
unity-mcp camera screenshot --camera-ref "SecondCamera" --include-image
659+
unity-mcp camera screenshot --max-resolution 256
660+
unity-mcp camera screenshot --batch surround --max-resolution 256
661+
unity-mcp camera screenshot --batch orbit --look-at "Player"
662+
unity-mcp camera screenshot-multiview --look-at "Player" --max-resolution 480
663+
```
664+
665+
### Graphics Commands
666+
667+
```bash
668+
# Check graphics system status
669+
unity-mcp graphics ping
670+
671+
# --- Volumes ---
672+
# Create a Volume (global or local)
673+
unity-mcp graphics volume-create --name "PostProcessing" --global
674+
unity-mcp graphics volume-create --name "LocalFog" --local --weight 0.8 --priority 1
675+
676+
# Add/remove/configure effects on a Volume
677+
unity-mcp graphics volume-add-effect --target "PostProcessing" --effect "Bloom"
678+
unity-mcp graphics volume-set-effect --target "PostProcessing" --effect "Bloom" -p intensity 1.5 -p threshold 0.9
679+
unity-mcp graphics volume-remove-effect --target "PostProcessing" --effect "Bloom"
680+
unity-mcp graphics volume-info --target "PostProcessing"
681+
unity-mcp graphics volume-set-properties --target "PostProcessing" --weight 0.5 --priority 2 --local
682+
unity-mcp graphics volume-list-effects
683+
unity-mcp graphics volume-create-profile --path "Assets/Profiles/MyProfile.asset" --name "MyProfile"
684+
685+
# --- Render Pipeline ---
686+
unity-mcp graphics pipeline-info
687+
unity-mcp graphics pipeline-settings
688+
unity-mcp graphics pipeline-set-quality --level "High"
689+
unity-mcp graphics pipeline-set-settings -s renderScale 1.5 -s msaaSampleCount 4
690+
691+
# --- Light Baking ---
692+
unity-mcp graphics bake-start
693+
unity-mcp graphics bake-start --sync # Wait for completion
694+
unity-mcp graphics bake-status
695+
unity-mcp graphics bake-cancel
696+
unity-mcp graphics bake-clear
697+
unity-mcp graphics bake-settings
698+
unity-mcp graphics bake-set-settings -s lightmapResolution 64 -s directSamples 32
699+
unity-mcp graphics bake-reflection-probe --target "ReflectionProbe1"
700+
unity-mcp graphics bake-create-probes --name "LightProbes" --spacing 5
701+
unity-mcp graphics bake-create-reflection --name "ReflProbe" --resolution 512 --mode Realtime
702+
703+
# --- Rendering Stats ---
704+
unity-mcp graphics stats
705+
unity-mcp graphics stats-memory
706+
unity-mcp graphics stats-debug-mode --mode "Wireframe"
707+
708+
# --- URP Renderer Features ---
709+
unity-mcp graphics feature-list
710+
unity-mcp graphics feature-add --type "ScreenSpaceAmbientOcclusion" --name "SSAO"
711+
unity-mcp graphics feature-remove --name "SSAO"
712+
unity-mcp graphics feature-configure --name "SSAO" -p Intensity 1.5 -p Radius 0.3
713+
unity-mcp graphics feature-reorder --order "0,2,1,3"
714+
unity-mcp graphics feature-toggle --name "SSAO" --active
715+
unity-mcp graphics feature-toggle --name "SSAO" --inactive
716+
717+
# --- Skybox & Environment ---
718+
unity-mcp graphics skybox-info
719+
unity-mcp graphics skybox-set-material --material "Assets/Materials/NightSky.mat"
720+
unity-mcp graphics skybox-set-properties -p _Tint "0.5,0.5,1,1" -p _Exposure 1.2
721+
unity-mcp graphics skybox-set-ambient --mode Flat --color "0.2,0.2,0.3"
722+
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"
723+
unity-mcp graphics skybox-set-fog --enable --mode ExponentialSquared --color "0.7,0.8,0.9" --density 0.02
724+
unity-mcp graphics skybox-set-fog --disable
725+
unity-mcp graphics skybox-set-reflection --intensity 1.0 --bounces 2 --mode Custom --resolution 256
726+
unity-mcp graphics skybox-set-sun --target "DirectionalLight"
727+
```
728+
729+
### Package Commands
730+
731+
```bash
732+
# Check package manager status
733+
unity-mcp packages ping
734+
735+
# List installed packages
736+
unity-mcp packages list
737+
738+
# Search Unity registry
739+
unity-mcp packages search "cinemachine"
740+
unity-mcp packages search "probuilder"
741+
742+
# Get package details
743+
unity-mcp packages info "com.unity.cinemachine"
744+
745+
# Install / remove packages
746+
unity-mcp packages add "com.unity.cinemachine"
747+
unity-mcp packages add "com.unity.cinemachine@4.1.1"
748+
unity-mcp packages remove "com.unity.cinemachine"
749+
unity-mcp packages remove "com.unity.cinemachine" --force # Skip confirmation
750+
751+
# Embed package for local editing
752+
unity-mcp packages embed "com.unity.cinemachine"
753+
754+
# Force package re-resolution
755+
unity-mcp packages resolve
756+
757+
# Check async operation status
758+
unity-mcp packages status <job_id>
759+
760+
# Scoped registries
761+
unity-mcp packages list-registries
762+
unity-mcp packages add-registry "My Registry" --url "https://registry.example.com" -s "com.example"
763+
unity-mcp packages remove-registry "My Registry"
764+
```
765+
766+
### Texture Commands
767+
768+
```bash
769+
# Create procedural textures
770+
unity-mcp texture create "Assets/Textures/Red.png" --width 128 --height 128 --color "1,0,0,1"
771+
unity-mcp texture create "Assets/Textures/Check.png" --pattern checkerboard --palette "1,0,0,1;0,0,1,1"
772+
unity-mcp texture create "Assets/Textures/Brick.png" --width 256 --height 256 --pattern brick
773+
unity-mcp texture create "Assets/Textures/Grid.png" --pattern grid --width 512 --height 512
774+
775+
# Available patterns: checkerboard, stripes, stripes_h, stripes_v, stripes_diag, dots, grid, brick
776+
777+
# Create from image file
778+
unity-mcp texture create "Assets/Textures/Photo.png" --image-path "/path/to/source.png"
779+
780+
# Create with custom import settings
781+
unity-mcp texture create "Assets/Textures/Normal.png" --import-settings '{"textureType": "NormalMap", "filterMode": "Trilinear"}'
782+
783+
# Create sprites (auto-configures import settings for 2D)
784+
unity-mcp texture sprite "Assets/Sprites/Player.png" --width 32 --height 32 --color "0,0.5,1,1"
785+
unity-mcp texture sprite "Assets/Sprites/Tile.png" --pattern checkerboard --ppu 16 --pivot "0.5,0"
786+
787+
# Modify existing texture pixels
788+
unity-mcp texture modify "Assets/Textures/Existing.png" --set-pixels '{"x":0,"y":0,"width":16,"height":16,"color":[1,0,0,1]}'
789+
790+
# Delete texture
791+
unity-mcp texture delete "Assets/Textures/Old.png"
792+
unity-mcp texture delete "Assets/Textures/Old.png" --force
793+
```
794+
607795
### Code Commands
608796

609797
```bash
610798
# Read source files
611799
unity-mcp code read "Assets/Scripts/Player.cs"
612800
unity-mcp code read "Assets/Scripts/Player.cs" --start-line 10 --line-count 20
801+
802+
# Search with regex
803+
unity-mcp code search "class.*Player" "Assets/Scripts/Player.cs"
804+
unity-mcp code search "TODO|FIXME" "Assets/Scripts/Utils.cs"
805+
unity-mcp code search "void Update" "Assets/Scripts/Game.cs" --max-results 20
613806
```
614807

615808
### Raw Commands
@@ -622,6 +815,9 @@ unity-mcp raw manage_scene '{"action": "get_active"}'
622815
unity-mcp raw manage_gameobject '{"action": "create", "name": "Test"}'
623816
unity-mcp raw manage_components '{"action": "add", "target": "Test", "componentType": "Rigidbody"}'
624817
unity-mcp raw manage_editor '{"action": "play"}'
818+
unity-mcp raw manage_camera '{"action": "screenshot", "include_image": true}'
819+
unity-mcp raw manage_graphics '{"action": "volume_info", "target": "PostProcess"}'
820+
unity-mcp raw manage_packages '{"action": "list"}'
625821
```
626822

627823
---

Server/src/services/tools/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ def extract_screenshot_images(response: dict[str, Any]) -> "ToolResult | None":
406406
"""If a Unity response contains inline base64 images, return a ToolResult
407407
with TextContent + ImageContent blocks. Returns None for normal text-only responses.
408408
409-
Shared by manage_scene and manage_camera screenshot handling.
409+
Shared screenshot handling (used by manage_camera).
410410
"""
411411
from fastmcp.server.server import ToolResult
412412
from mcp.types import TextContent, ImageContent
@@ -476,7 +476,7 @@ def build_screenshot_params(
476476
"""Populate screenshot-related keys in *params* dict. Returns an error dict
477477
if validation fails, or None on success.
478478
479-
Shared by manage_scene and manage_camera screenshot handling.
479+
Shared screenshot handling (used by manage_camera).
480480
"""
481481
if screenshot_file_name:
482482
params["fileName"] = screenshot_file_name

Server/src/transport/plugin_hub.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,8 +426,29 @@ async def _handle_register(self, websocket: WebSocket, payload: RegisterMessage)
426426
response = RegisteredMessage(session_id=session_id)
427427
await websocket.send_json(response.model_dump())
428428

429-
session = await registry.register(session_id, project_name, project_hash, unity_version, project_path, user_id=user_id)
429+
session, evicted_session_id = await registry.register(session_id, project_name, project_hash, unity_version, project_path, user_id=user_id)
430+
evicted_ws = None
430431
async with lock:
432+
# Clean up the evicted session's connection, ping loop, and pending commands
433+
# so they don't linger as orphans after a domain-reload reconnection race.
434+
if evicted_session_id:
435+
evicted_ws = cls._connections.pop(evicted_session_id, None)
436+
old_ping = cls._ping_tasks.pop(evicted_session_id, None)
437+
if old_ping and not old_ping.done():
438+
old_ping.cancel()
439+
cls._last_pong.pop(evicted_session_id, None)
440+
for command_id, entry in list(cls._pending.items()):
441+
if entry.get("session_id") == evicted_session_id:
442+
future = entry.get("future") if isinstance(entry, dict) else None
443+
if future and not future.done():
444+
future.set_exception(
445+
PluginDisconnectedError(
446+
f"Unity plugin session {evicted_session_id} superseded by {session_id}"
447+
)
448+
)
449+
cls._pending.pop(command_id, None)
450+
logger.info(f"Evicted previous session {evicted_session_id} for same instance")
451+
431452
cls._connections[session.session_id] = websocket
432453
# Initialize last pong time and start ping loop for this session
433454
cls._last_pong[session_id] = time.monotonic()
@@ -439,6 +460,13 @@ async def _handle_register(self, websocket: WebSocket, payload: RegisterMessage)
439460
ping_task = asyncio.create_task(cls._ping_loop(session_id, websocket))
440461
cls._ping_tasks[session_id] = ping_task
441462

463+
# Close evicted WebSocket outside the lock to avoid blocking
464+
if evicted_ws is not None:
465+
try:
466+
await evicted_ws.close(code=1001)
467+
except Exception:
468+
pass
469+
442470
if user_id:
443471
logger.info(f"Plugin registered: {project_name} ({project_hash}) for user {user_id}")
444472
else:

0 commit comments

Comments
 (0)