From 655e8b6e46bc8ab1bdb562354b081398ae3d6e08 Mon Sep 17 00:00:00 2001 From: toxifly <88746311+toxifly@users.noreply.github.com> Date: Wed, 21 Jan 2026 20:13:02 +0800 Subject: [PATCH 1/7] fix: improve manage_scene screenshot capture --- MCPForUnity/Editor/Tools/ManageScene.cs | 162 ++++++++++-------- .../Runtime/Helpers/ScreenshotUtility.cs | 58 ++++++- docs/README-DEV-zh.md | 6 + docs/README-DEV.md | 8 +- 4 files changed, 152 insertions(+), 82 deletions(-) diff --git a/MCPForUnity/Editor/Tools/ManageScene.cs b/MCPForUnity/Editor/Tools/ManageScene.cs index e58c09424..6f383ac50 100644 --- a/MCPForUnity/Editor/Tools/ManageScene.cs +++ b/MCPForUnity/Editor/Tools/ManageScene.cs @@ -384,34 +384,27 @@ private static object CaptureScreenshot(string fileName, int? superSize) try { int resolvedSuperSize = (superSize.HasValue && superSize.Value > 0) ? superSize.Value : 1; - ScreenshotCaptureResult result; - if (Application.isPlaying) + // Best-effort: ensure Game View exists and repaints before capture. + if (!Application.isBatchMode) { - result = ScreenshotUtility.CaptureToAssetsFolder(fileName, resolvedSuperSize, ensureUniqueFileName: true); + BestEffortPrepareGameViewForScreenshot(); } - else - { - // Edit Mode path: render from the best-guess camera using RenderTexture. - Camera cam = Camera.main; - if (cam == null) - { - // Use FindObjectsOfType for Unity 2021 compatibility - var cams = UnityEngine.Object.FindObjectsOfType(); - cam = cams.FirstOrDefault(); - } - if (cam == null) - { - return new ErrorResponse("No camera found to capture screenshot in Edit Mode."); - } + ScreenshotCaptureResult result = ScreenshotUtility.CaptureToAssetsFolder(fileName, resolvedSuperSize, ensureUniqueFileName: true); - result = ScreenshotUtility.CaptureFromCameraToAssetsFolder(cam, fileName, resolvedSuperSize, ensureUniqueFileName: true); + // ScreenCapture.CaptureScreenshot is async. Import after the file actually hits disk. + if (result.IsAsync) + { + ScheduleAssetImportWhenFileExists(result.AssetsRelativePath, result.FullPath, timeoutSeconds: 30.0); + } + else + { + AssetDatabase.ImportAsset(result.AssetsRelativePath, ImportAssetOptions.ForceSynchronousImport); } - AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); - - string message = $"Screenshot captured to '{result.AssetsRelativePath}' (full: {result.FullPath})."; + string verb = result.IsAsync ? "Screenshot requested" : "Screenshot captured"; + string message = $"{verb} to '{result.AssetsRelativePath}' (full: {result.FullPath})."; return new SuccessResponse( message, @@ -420,6 +413,7 @@ private static object CaptureScreenshot(string fileName, int? superSize) path = result.AssetsRelativePath, fullPath = result.FullPath, superSize = result.SuperSize, + isAsync = result.IsAsync, } ); } @@ -429,6 +423,72 @@ private static object CaptureScreenshot(string fileName, int? superSize) } } + private static void BestEffortPrepareGameViewForScreenshot() + { + try + { + // Ensure a Game View exists and has a chance to repaint before capture. + try + { + if (!EditorApplication.ExecuteMenuItem("Window/General/Game")) + { + // Some Unity versions expose hotkey suffixes in menu paths. + EditorApplication.ExecuteMenuItem("Window/General/Game %2"); + } + } + catch { } + + try + { + var gameViewType = Type.GetType("UnityEditor.GameView,UnityEditor"); + if (gameViewType != null) + { + var window = EditorWindow.GetWindow(gameViewType); + window?.Repaint(); + } + } + catch { } + + try { SceneView.RepaintAll(); } catch { } + try { EditorApplication.QueuePlayerLoopUpdate(); } catch { } + } + catch { } + } + + private static void ScheduleAssetImportWhenFileExists(string assetsRelativePath, string fullPath, double timeoutSeconds) + { + if (string.IsNullOrWhiteSpace(assetsRelativePath) || string.IsNullOrWhiteSpace(fullPath)) + { + return; + } + + double start = EditorApplication.timeSinceStartup; + Action tick = null; + tick = () => + { + try + { + if (File.Exists(fullPath)) + { + EditorApplication.update -= tick; + AssetDatabase.ImportAsset(assetsRelativePath, ImportAssetOptions.ForceSynchronousImport); + return; + } + + if (EditorApplication.timeSinceStartup - start > timeoutSeconds) + { + EditorApplication.update -= tick; + } + } + catch + { + try { EditorApplication.update -= tick; } catch { } + } + }; + + EditorApplication.update += tick; + } + private static object GetActiveSceneInfo() { try @@ -668,7 +728,11 @@ private static object BuildGameObjectSummary(GameObject go, bool includeTransfor } } } - catch { } + catch (Exception ex) + { + try { McpLog.Debug($"[ManageScene] Failed to enumerate components for '{go.name}': {ex.Message}"); } + catch { } + } var d = new Dictionary { @@ -684,7 +748,7 @@ private static object BuildGameObjectSummary(GameObject go, bool includeTransfor { "childrenTruncated", childrenTruncated }, { "childrenCursor", childCount > 0 ? "0" : null }, { "childrenPageSizeDefault", maxChildrenPerNode }, - { "componentTypes", componentTypes }, // NEW: Lightweight component type list + { "componentTypes", componentTypes }, }; if (includeTransform && go.transform != null) @@ -721,57 +785,5 @@ private static string GetGameObjectPath(GameObject go) } } - /// - /// Recursively builds a data representation of a GameObject and its children. - /// - private static object GetGameObjectDataRecursive(GameObject go) - { - if (go == null) - return null; - - var childrenData = new List(); - foreach (Transform child in go.transform) - { - childrenData.Add(GetGameObjectDataRecursive(child.gameObject)); - } - - var gameObjectData = new Dictionary - { - { "name", go.name }, - { "activeSelf", go.activeSelf }, - { "activeInHierarchy", go.activeInHierarchy }, - { "tag", go.tag }, - { "layer", go.layer }, - { "isStatic", go.isStatic }, - { "instanceID", go.GetInstanceID() }, // Useful unique identifier - { - "transform", - new - { - position = new - { - x = go.transform.localPosition.x, - y = go.transform.localPosition.y, - z = go.transform.localPosition.z, - }, - rotation = new - { - x = go.transform.localRotation.eulerAngles.x, - y = go.transform.localRotation.eulerAngles.y, - z = go.transform.localRotation.eulerAngles.z, - }, // Euler for simplicity - scale = new - { - x = go.transform.localScale.x, - y = go.transform.localScale.y, - z = go.transform.localScale.z, - }, - } - }, - { "children", childrenData }, - }; - - return gameObjectData; - } } } diff --git a/MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs b/MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs index eb71da614..769f6de3a 100644 --- a/MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs +++ b/MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs @@ -9,21 +9,48 @@ namespace MCPForUnity.Runtime.Helpers public readonly struct ScreenshotCaptureResult { public ScreenshotCaptureResult(string fullPath, string assetsRelativePath, int superSize) + : this(fullPath, assetsRelativePath, superSize, isAsync: false) + { + } + + public ScreenshotCaptureResult(string fullPath, string assetsRelativePath, int superSize, bool isAsync) { FullPath = fullPath; AssetsRelativePath = assetsRelativePath; SuperSize = superSize; + IsAsync = isAsync; } public string FullPath { get; } public string AssetsRelativePath { get; } public int SuperSize { get; } + public bool IsAsync { get; } } public static class ScreenshotUtility { private const string ScreenshotsFolderName = "Screenshots"; + private static Camera FindBestCamera() + { + var main = Camera.main; + if (main != null) + { + return main; + } + + try + { + // Use FindObjectsOfType for Unity 2021 compatibility. + var cams = UnityEngine.Object.FindObjectsOfType(); + return cams.FirstOrDefault(); + } + catch + { + return null; + } + } + public static ScreenshotCaptureResult CaptureToAssetsFolder(string fileName = null, int superSize = 1, bool ensureUniqueFileName = true) { int size = Mathf.Max(1, superSize); @@ -39,9 +66,6 @@ public static ScreenshotCaptureResult CaptureToAssetsFolder(string fileName = nu string normalizedFullPath = fullPath.Replace('\\', '/'); - // Use only the file name to let Unity decide the final location (per CaptureScreenshot docs). - string captureName = Path.GetFileName(normalizedFullPath); - // Use Asset folder for ScreenCapture.CaptureScreenshot to ensure write to asset rather than project root string projectRoot = GetProjectRootPath(); string assetsRelativePath = normalizedFullPath; @@ -50,17 +74,27 @@ public static ScreenshotCaptureResult CaptureToAssetsFolder(string fileName = nu assetsRelativePath = assetsRelativePath.Substring(projectRoot.Length).TrimStart('/'); } + bool isAsync; #if UNITY_2022_1_OR_NEWER ScreenCapture.CaptureScreenshot(assetsRelativePath, size); + isAsync = true; #else Debug.LogWarning("ScreenCapture is supported after Unity 2022.1. Using main camera capture as fallback."); - CaptureFromCameraToAssetsFolder(Camera.main, captureName, size, false); + var cam = FindBestCamera(); + if (cam == null) + { + throw new InvalidOperationException("No camera found to capture screenshot."); + } + string captureName = Path.GetFileName(normalizedFullPath); + CaptureFromCameraToAssetsFolder(cam, captureName, size, false); + isAsync = false; #endif return new ScreenshotCaptureResult( normalizedFullPath, assetsRelativePath, - size); + size, + isAsync); } /// @@ -94,13 +128,14 @@ public static ScreenshotCaptureResult CaptureFromCameraToAssetsFolder(Camera cam RenderTexture prevRT = camera.targetTexture; RenderTexture prevActive = RenderTexture.active; var rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32); + Texture2D tex = null; try { camera.targetTexture = rt; camera.Render(); RenderTexture.active = rt; - var tex = new Texture2D(width, height, TextureFormat.RGBA32, false); + tex = new Texture2D(width, height, TextureFormat.RGBA32, false); tex.ReadPixels(new Rect(0, 0, width, height), 0, 0); tex.Apply(); @@ -112,6 +147,17 @@ public static ScreenshotCaptureResult CaptureFromCameraToAssetsFolder(Camera cam camera.targetTexture = prevRT; RenderTexture.active = prevActive; RenderTexture.ReleaseTemporary(rt); + if (tex != null) + { + if (Application.isPlaying) + { + UnityEngine.Object.Destroy(tex); + } + else + { + UnityEngine.Object.DestroyImmediate(tex); + } + } } string projectRoot = GetProjectRootPath(); diff --git a/docs/README-DEV-zh.md b/docs/README-DEV-zh.md index 269e09a0a..0fd5e187f 100644 --- a/docs/README-DEV-zh.md +++ b/docs/README-DEV-zh.md @@ -208,6 +208,12 @@ X:\UnityProject\Library\PackageCache\com.coplaydev.unity-mcp@272123cfd97e - **`max_nodes`**:默认 **1000**,限制 **1..5000** - **`include_transform`**:默认 **false** +### `manage_scene(action="screenshot")` + +- 将 PNG 保存到 `Assets/Screenshots/`。 +- Unity **2022.1+**:通过 `ScreenCapture.CaptureScreenshot` 捕获 **Game View**,因此包含 `Screen Space - Overlay` UI。注意该写入是 **异步** 的,文件/导入可能会稍后出现。 +- Unity **2021.3**:回退为将可用的 `Camera` 渲染到 `RenderTexture`(仅相机输出;不包含 `Screen Space - Overlay` UI)。 + ### `manage_gameobject(action="get_components")` - **默认行为**:仅返回 **分页的组件元数据**(`typeName`, `instanceID`)。 diff --git a/docs/README-DEV.md b/docs/README-DEV.md index c9b3d45ef..0556e81ea 100644 --- a/docs/README-DEV.md +++ b/docs/README-DEV.md @@ -199,6 +199,12 @@ Some Unity tool calls can return *very large* JSON payloads (deep hierarchies, c - **`max_nodes`**: defaults to **1000**, clamped to **1..5000** - **`include_transform`**: defaults to **false** +### `manage_scene(action="screenshot")` + +- Saves PNGs under `Assets/Screenshots/`. +- Unity **2022.1+**: captures the **Game View** via `ScreenCapture.CaptureScreenshot`, so `Screen Space - Overlay` UI is included. Note this write is **async**, so the file may appear/import a moment later. +- Unity **2021.3**: falls back to rendering the best available `Camera` into a `RenderTexture` (camera output only; `Screen Space - Overlay` UI is not included). + ### `manage_gameobject(action="get_components")` - **Default behavior**: returns **paged component metadata** only (`typeName`, `instanceID`). @@ -361,4 +367,4 @@ Tests that trigger script compilation mid-run (e.g., `DomainReloadResilienceTest - Run them first in the test suite (before backgrounding Unity) - Use the `[Explicit]` attribute to exclude them from default runs -**Note:** The MCP workflow itself is unaffected—socket messages provide external stimulus that keeps Unity responsive even when backgrounded. This limitation only affects Unity's internal test coroutine waits. \ No newline at end of file +**Note:** The MCP workflow itself is unaffected—socket messages provide external stimulus that keeps Unity responsive even when backgrounded. This limitation only affects Unity's internal test coroutine waits. From 9cbf039981a2740cefa8dd38e9705ba0b731b3af Mon Sep 17 00:00:00 2001 From: toxifly <88746311+toxifly@users.noreply.github.com> Date: Wed, 21 Jan 2026 22:37:06 +0800 Subject: [PATCH 2/7] fix: address PR review feedback for screenshot capture - Gate pre-2022 ScreenCapture fallback warning to log only once - Downgrade warning to Debug.Log to reduce log noise - Refactor path-building into shared PrepareCaptureResult() helper - Add conditional logging to catch blocks in BestEffortPrepareGameViewForScreenshot - Add timeout/failure logging to ScheduleAssetImportWhenFileExists - Fix grammar in README-DEV.md --- .claude/settings.json | 10 +-- MCPForUnity/Editor/Tools/ManageScene.cs | 76 +++++++++++++--- .../Runtime/Helpers/ScreenshotUtility.cs | 89 +++++++++---------- docs/README-DEV.md | 2 +- 4 files changed, 108 insertions(+), 69 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index bd3d33632..98ed4b398 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -5,14 +5,6 @@ "Edit(reports/**)", "MultiEdit(reports/**)" ], - "deny": [ - "Bash", - "WebFetch", - "WebSearch", - "Task", - "TodoWrite", - "NotebookEdit", - "NotebookRead" - ] + "deny": [] } } diff --git a/MCPForUnity/Editor/Tools/ManageScene.cs b/MCPForUnity/Editor/Tools/ManageScene.cs index 6f383ac50..33c571eae 100644 --- a/MCPForUnity/Editor/Tools/ManageScene.cs +++ b/MCPForUnity/Editor/Tools/ManageScene.cs @@ -436,7 +436,10 @@ private static void BestEffortPrepareGameViewForScreenshot() EditorApplication.ExecuteMenuItem("Window/General/Game %2"); } } - catch { } + catch (Exception e) + { + try { McpLog.Debug($"[ManageScene] screenshot: failed to open Game View via menu item: {e.Message}"); } catch { } + } try { @@ -447,12 +450,27 @@ private static void BestEffortPrepareGameViewForScreenshot() window?.Repaint(); } } - catch { } + catch (Exception e) + { + try { McpLog.Debug($"[ManageScene] screenshot: failed to repaint Game View: {e.Message}"); } catch { } + } + + try { SceneView.RepaintAll(); } + catch (Exception e) + { + try { McpLog.Debug($"[ManageScene] screenshot: failed to repaint Scene View: {e.Message}"); } catch { } + } - try { SceneView.RepaintAll(); } catch { } - try { EditorApplication.QueuePlayerLoopUpdate(); } catch { } + try { EditorApplication.QueuePlayerLoopUpdate(); } + catch (Exception e) + { + try { McpLog.Debug($"[ManageScene] screenshot: failed to queue player loop update: {e.Message}"); } catch { } + } + } + catch (Exception e) + { + try { McpLog.Debug($"[ManageScene] screenshot: BestEffortPrepareGameViewForScreenshot failed: {e.Message}"); } catch { } } - catch { } } private static void ScheduleAssetImportWhenFileExists(string assetsRelativePath, string fullPath, double timeoutSeconds) @@ -463,6 +481,9 @@ private static void ScheduleAssetImportWhenFileExists(string assetsRelativePath, } double start = EditorApplication.timeSinceStartup; + int failureCount = 0; + bool hasSeenFile = false; + const int maxLoggedFailures = 3; Action tick = null; tick = () => { @@ -470,19 +491,54 @@ private static void ScheduleAssetImportWhenFileExists(string assetsRelativePath, { if (File.Exists(fullPath)) { - EditorApplication.update -= tick; + hasSeenFile = true; + AssetDatabase.ImportAsset(assetsRelativePath, ImportAssetOptions.ForceSynchronousImport); + try { McpLog.Debug($"[ManageScene] Imported asset at '{assetsRelativePath}'."); } catch { } + EditorApplication.update -= tick; return; } + } + catch (Exception e) + { + failureCount++; - if (EditorApplication.timeSinceStartup - start > timeoutSeconds) + if (failureCount <= maxLoggedFailures) { - EditorApplication.update -= tick; + try + { + McpLog.Warn( + $"[ManageScene] Exception while importing asset '{assetsRelativePath}' from '{fullPath}' (attempt {failureCount}): {e}" + ); + } + catch { } } } - catch + + if (EditorApplication.timeSinceStartup - start > timeoutSeconds) { - try { EditorApplication.update -= tick; } catch { } + if (!hasSeenFile) + { + try + { + McpLog.Warn( + $"[ManageScene] Timed out waiting for file '{fullPath}' (asset: '{assetsRelativePath}') after {timeoutSeconds:F1} seconds. The asset was not imported." + ); + } + catch { } + } + else + { + try + { + McpLog.Warn( + $"[ManageScene] Timed out importing asset '{assetsRelativePath}' from '{fullPath}' after {timeoutSeconds:F1} seconds. The file existed but the asset was not imported." + ); + } + catch { } + } + + EditorApplication.update -= tick; } }; diff --git a/MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs b/MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs index 769f6de3a..70b2ebd5b 100644 --- a/MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs +++ b/MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs @@ -30,6 +30,7 @@ public ScreenshotCaptureResult(string fullPath, string assetsRelativePath, int s public static class ScreenshotUtility { private const string ScreenshotsFolderName = "Screenshots"; + private static bool s_loggedLegacyScreenCaptureFallback; private static Camera FindBestCamera() { @@ -53,48 +54,25 @@ private static Camera FindBestCamera() public static ScreenshotCaptureResult CaptureToAssetsFolder(string fileName = null, int superSize = 1, bool ensureUniqueFileName = true) { - int size = Mathf.Max(1, superSize); - string resolvedName = BuildFileName(fileName); - string folder = Path.Combine(Application.dataPath, ScreenshotsFolderName); - Directory.CreateDirectory(folder); - - string fullPath = Path.Combine(folder, resolvedName); - if (ensureUniqueFileName) - { - fullPath = EnsureUnique(fullPath); - } - - string normalizedFullPath = fullPath.Replace('\\', '/'); - - // Use Asset folder for ScreenCapture.CaptureScreenshot to ensure write to asset rather than project root - string projectRoot = GetProjectRootPath(); - string assetsRelativePath = normalizedFullPath; - if (assetsRelativePath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase)) +#if UNITY_2022_1_OR_NEWER + ScreenshotCaptureResult result = PrepareCaptureResult(fileName, superSize, ensureUniqueFileName, isAsync: true); + ScreenCapture.CaptureScreenshot(result.AssetsRelativePath, result.SuperSize); + return result; +#else + if (!s_loggedLegacyScreenCaptureFallback) { - assetsRelativePath = assetsRelativePath.Substring(projectRoot.Length).TrimStart('/'); + Debug.Log("ScreenCapture is supported after Unity 2022.1. Using camera capture as fallback."); + s_loggedLegacyScreenCaptureFallback = true; } - bool isAsync; -#if UNITY_2022_1_OR_NEWER - ScreenCapture.CaptureScreenshot(assetsRelativePath, size); - isAsync = true; -#else - Debug.LogWarning("ScreenCapture is supported after Unity 2022.1. Using main camera capture as fallback."); var cam = FindBestCamera(); if (cam == null) { throw new InvalidOperationException("No camera found to capture screenshot."); } - string captureName = Path.GetFileName(normalizedFullPath); - CaptureFromCameraToAssetsFolder(cam, captureName, size, false); - isAsync = false; -#endif - - return new ScreenshotCaptureResult( - normalizedFullPath, - assetsRelativePath, - size, - isAsync); + + return CaptureFromCameraToAssetsFolder(cam, fileName, superSize, ensureUniqueFileName); +#endif } /// @@ -107,18 +85,8 @@ public static ScreenshotCaptureResult CaptureFromCameraToAssetsFolder(Camera cam throw new ArgumentNullException(nameof(camera)); } - int size = Mathf.Max(1, superSize); - string resolvedName = BuildFileName(fileName); - string folder = Path.Combine(Application.dataPath, ScreenshotsFolderName); - Directory.CreateDirectory(folder); - - string fullPath = Path.Combine(folder, resolvedName); - if (ensureUniqueFileName) - { - fullPath = EnsureUnique(fullPath); - } - - string normalizedFullPath = fullPath.Replace('\\', '/'); + ScreenshotCaptureResult result = PrepareCaptureResult(fileName, superSize, ensureUniqueFileName, isAsync: false); + int size = result.SuperSize; int width = Mathf.Max(1, camera.pixelWidth > 0 ? camera.pixelWidth : Screen.width); int height = Mathf.Max(1, camera.pixelHeight > 0 ? camera.pixelHeight : Screen.height); @@ -140,7 +108,7 @@ public static ScreenshotCaptureResult CaptureFromCameraToAssetsFolder(Camera cam tex.Apply(); byte[] png = tex.EncodeToPNG(); - File.WriteAllBytes(normalizedFullPath, png); + File.WriteAllBytes(result.FullPath, png); } finally { @@ -160,14 +128,37 @@ public static ScreenshotCaptureResult CaptureFromCameraToAssetsFolder(Camera cam } } + return result; + } + + private static ScreenshotCaptureResult PrepareCaptureResult(string fileName, int superSize, bool ensureUniqueFileName, bool isAsync) + { + int size = Mathf.Max(1, superSize); + string resolvedName = BuildFileName(fileName); + string folder = Path.Combine(Application.dataPath, ScreenshotsFolderName); + Directory.CreateDirectory(folder); + + string fullPath = Path.Combine(folder, resolvedName); + if (ensureUniqueFileName) + { + fullPath = EnsureUnique(fullPath); + } + + string normalizedFullPath = fullPath.Replace('\\', '/'); + string assetsRelativePath = ToAssetsRelativePath(normalizedFullPath); + + return new ScreenshotCaptureResult(normalizedFullPath, assetsRelativePath, size, isAsync); + } + + private static string ToAssetsRelativePath(string normalizedFullPath) + { string projectRoot = GetProjectRootPath(); string assetsRelativePath = normalizedFullPath; if (assetsRelativePath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase)) { assetsRelativePath = assetsRelativePath.Substring(projectRoot.Length).TrimStart('/'); } - - return new ScreenshotCaptureResult(normalizedFullPath, assetsRelativePath, size); + return assetsRelativePath; } private static string BuildFileName(string fileName) diff --git a/docs/README-DEV.md b/docs/README-DEV.md index 0556e81ea..3a11a410a 100644 --- a/docs/README-DEV.md +++ b/docs/README-DEV.md @@ -202,7 +202,7 @@ Some Unity tool calls can return *very large* JSON payloads (deep hierarchies, c ### `manage_scene(action="screenshot")` - Saves PNGs under `Assets/Screenshots/`. -- Unity **2022.1+**: captures the **Game View** via `ScreenCapture.CaptureScreenshot`, so `Screen Space - Overlay` UI is included. Note this write is **async**, so the file may appear/import a moment later. +- Unity **2022.1+**: captures the **Game View** via `ScreenCapture.CaptureScreenshot`, so `Screen Space - Overlay` UI is included. This write is **async**, so the file may appear/import a moment later. - Unity **2021.3**: falls back to rendering the best available `Camera` into a `RenderTexture` (camera output only; `Screen Space - Overlay` UI is not included). ### `manage_gameobject(action="get_components")` From 20b91884130b9271ccfcdffd294503191da1dc7c Mon Sep 17 00:00:00 2001 From: toxifly <88746311+toxifly@users.noreply.github.com> Date: Thu, 22 Jan 2026 02:09:42 +0800 Subject: [PATCH 3/7] fix(unity): resolve screenshot import callback type + FindObjectsOfType deprecation --- .claude/settings.json | 10 +++++++++- .../Editor/Tools/GameObjects/ManageGameObjectCommon.cs | 7 +++++++ MCPForUnity/Editor/Tools/ManageScene.cs | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 98ed4b398..bd3d33632 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -5,6 +5,14 @@ "Edit(reports/**)", "MultiEdit(reports/**)" ], - "deny": [] + "deny": [ + "Bash", + "WebFetch", + "WebSearch", + "Task", + "TodoWrite", + "NotebookEdit", + "NotebookRead" + ] } } diff --git a/MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs b/MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs index e686145ef..8d8a36eac 100644 --- a/MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs +++ b/MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs @@ -154,9 +154,16 @@ internal static List FindObjectsInternal( } else { +#if UNITY_2023_1_OR_NEWER + var inactive = searchInactive ? FindObjectsInactive.Include : FindObjectsInactive.Exclude; + searchPoolComp = UnityEngine.Object.FindObjectsByType(componentType, inactive, FindObjectsSortMode.None) + .Cast() + .Select(c => c.gameObject); +#else searchPoolComp = UnityEngine.Object.FindObjectsOfType(componentType, searchInactive) .Cast() .Select(c => c.gameObject); +#endif } results.AddRange(searchPoolComp.Where(go => go != null)); } diff --git a/MCPForUnity/Editor/Tools/ManageScene.cs b/MCPForUnity/Editor/Tools/ManageScene.cs index 33c571eae..5f87a5ea2 100644 --- a/MCPForUnity/Editor/Tools/ManageScene.cs +++ b/MCPForUnity/Editor/Tools/ManageScene.cs @@ -484,7 +484,7 @@ private static void ScheduleAssetImportWhenFileExists(string assetsRelativePath, int failureCount = 0; bool hasSeenFile = false; const int maxLoggedFailures = 3; - Action tick = null; + EditorApplication.CallbackFunction tick = null; tick = () => { try From 2c20ddcd102ff94131875c774adfb59fe1b03f8a Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 23 Jan 2026 05:34:19 +0000 Subject: [PATCH 4/7] chore: bump version to 9.2.0 --- MCPForUnity/package.json | 2 +- Server/README.md | 2 +- Server/pyproject.toml | 2 +- manifest.json | 153 +++++++++++++++++++++++++++++++-------- 4 files changed, 125 insertions(+), 34 deletions(-) diff --git a/MCPForUnity/package.json b/MCPForUnity/package.json index cee80952d..6bf9787a6 100644 --- a/MCPForUnity/package.json +++ b/MCPForUnity/package.json @@ -1,6 +1,6 @@ { "name": "com.coplaydev.unity-mcp", - "version": "9.1.0", + "version": "9.2.0", "displayName": "MCP for Unity", "description": "A bridge that connects AI assistants to Unity via the MCP (Model Context Protocol). Allows AI clients like Claude Code, Cursor, and VSCode to directly control your Unity Editor for enhanced development workflows.\n\nFeatures automated setup wizard, cross-platform support, and seamless integration with popular AI development tools.\n\nJoin Our Discord: https://discord.gg/y4p8KfzrN4", "unity": "2021.3", diff --git a/Server/README.md b/Server/README.md index 075aceb9f..68855400b 100644 --- a/Server/README.md +++ b/Server/README.md @@ -69,7 +69,7 @@ Use this to run the latest released version from the repository. Change the vers "command": "uvx", "args": [ "--from", - "git+https://github.com/CoplayDev/unity-mcp@v9.1.0#subdirectory=Server", + "git+https://github.com/CoplayDev/unity-mcp@v9.2.0#subdirectory=Server", "mcp-for-unity", "--transport", "stdio" diff --git a/Server/pyproject.toml b/Server/pyproject.toml index b3ffa6dde..fe3d56927 100644 --- a/Server/pyproject.toml +++ b/Server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mcpforunityserver" -version = "9.1.0" +version = "9.2.0" description = "MCP for Unity Server: A Unity package for Unity Editor integration via the Model Context Protocol (MCP)." readme = "README.md" license = "MIT" diff --git a/manifest.json b/manifest.json index fac58611b..d712c0a86 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": "0.3", "name": "Unity MCP", - "version": "9.1.0", + "version": "9.2.0", "description": "AI-powered Unity Editor automation via MCP - manage GameObjects, scripts, materials, scenes, prefabs, VFX, and run tests", "author": { "name": "Coplay", @@ -20,39 +20,130 @@ "entry_point": "Server/src/main.py", "mcp_config": { "command": "uvx", - "args": ["--from", "mcpforunityserver", "mcp-for-unity"], + "args": [ + "--from", + "mcpforunityserver", + "mcp-for-unity" + ], "env": {} } }, "tools": [ - {"name": "batch_execute", "description": "Execute multiple Unity operations in a single batch"}, - {"name": "debug_request_context", "description": "Debug and inspect MCP request context"}, - {"name": "execute_custom_tool", "description": "Execute custom Unity Editor tools registered by the project"}, - {"name": "execute_menu_item", "description": "Execute Unity Editor menu items"}, - {"name": "find_gameobjects", "description": "Find GameObjects in the scene by various criteria"}, - {"name": "find_in_file", "description": "Search for content within Unity project files"}, - {"name": "manage_asset", "description": "Create, modify, search, and organize Unity assets"}, - {"name": "manage_components", "description": "Add, remove, and configure GameObject components"}, - {"name": "manage_editor", "description": "Control Unity Editor state, play mode, and preferences"}, - {"name": "manage_gameobject", "description": "Create, modify, transform, and delete GameObjects"}, - {"name": "manage_material", "description": "Create and modify Unity materials and shaders"}, - {"name": "manage_prefabs", "description": "Create, instantiate, unpack, and modify prefabs"}, - {"name": "manage_scene", "description": "Load, save, query hierarchy, and manage Unity scenes"}, - {"name": "manage_script", "description": "Create, read, and modify C# scripts"}, - {"name": "manage_scriptable_object", "description": "Create and modify ScriptableObjects"}, - {"name": "manage_shader", "description": "Work with Unity shaders"}, - {"name": "manage_vfx", "description": "Manage Visual Effects, particle systems, and trails"}, - {"name": "read_console", "description": "Read Unity Editor console output (logs, warnings, errors)"}, - {"name": "refresh_unity", "description": "Refresh Unity Editor asset database"}, - {"name": "run_tests", "description": "Run Unity Test Framework tests"}, - {"name": "get_test_job", "description": "Get status of async test job"}, - {"name": "script_apply_edits", "description": "Apply code edits to C# scripts with validation"}, - {"name": "set_active_instance", "description": "Set the active Unity Editor instance for multi-instance workflows"}, - {"name": "apply_text_edits", "description": "Apply text edits to script content"}, - {"name": "create_script", "description": "Create new C# scripts"}, - {"name": "delete_script", "description": "Delete C# scripts"}, - {"name": "validate_script", "description": "Validate C# script syntax and compilation"}, - {"name": "manage_script_capabilities", "description": "Query script management capabilities"}, - {"name": "get_sha", "description": "Get SHA hash of script content"} + { + "name": "batch_execute", + "description": "Execute multiple Unity operations in a single batch" + }, + { + "name": "debug_request_context", + "description": "Debug and inspect MCP request context" + }, + { + "name": "execute_custom_tool", + "description": "Execute custom Unity Editor tools registered by the project" + }, + { + "name": "execute_menu_item", + "description": "Execute Unity Editor menu items" + }, + { + "name": "find_gameobjects", + "description": "Find GameObjects in the scene by various criteria" + }, + { + "name": "find_in_file", + "description": "Search for content within Unity project files" + }, + { + "name": "manage_asset", + "description": "Create, modify, search, and organize Unity assets" + }, + { + "name": "manage_components", + "description": "Add, remove, and configure GameObject components" + }, + { + "name": "manage_editor", + "description": "Control Unity Editor state, play mode, and preferences" + }, + { + "name": "manage_gameobject", + "description": "Create, modify, transform, and delete GameObjects" + }, + { + "name": "manage_material", + "description": "Create and modify Unity materials and shaders" + }, + { + "name": "manage_prefabs", + "description": "Create, instantiate, unpack, and modify prefabs" + }, + { + "name": "manage_scene", + "description": "Load, save, query hierarchy, and manage Unity scenes" + }, + { + "name": "manage_script", + "description": "Create, read, and modify C# scripts" + }, + { + "name": "manage_scriptable_object", + "description": "Create and modify ScriptableObjects" + }, + { + "name": "manage_shader", + "description": "Work with Unity shaders" + }, + { + "name": "manage_vfx", + "description": "Manage Visual Effects, particle systems, and trails" + }, + { + "name": "read_console", + "description": "Read Unity Editor console output (logs, warnings, errors)" + }, + { + "name": "refresh_unity", + "description": "Refresh Unity Editor asset database" + }, + { + "name": "run_tests", + "description": "Run Unity Test Framework tests" + }, + { + "name": "get_test_job", + "description": "Get status of async test job" + }, + { + "name": "script_apply_edits", + "description": "Apply code edits to C# scripts with validation" + }, + { + "name": "set_active_instance", + "description": "Set the active Unity Editor instance for multi-instance workflows" + }, + { + "name": "apply_text_edits", + "description": "Apply text edits to script content" + }, + { + "name": "create_script", + "description": "Create new C# scripts" + }, + { + "name": "delete_script", + "description": "Delete C# scripts" + }, + { + "name": "validate_script", + "description": "Validate C# script syntax and compilation" + }, + { + "name": "manage_script_capabilities", + "description": "Query script management capabilities" + }, + { + "name": "get_sha", + "description": "Get SHA hash of script content" + } ] } From 58ccbe911146d37019475431fa812325c2e629aa Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:57:10 -0500 Subject: [PATCH 5/7] Update ManageScene.cs --- MCPForUnity/Editor/Tools/ManageScene.cs | 34 ++++++------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/MCPForUnity/Editor/Tools/ManageScene.cs b/MCPForUnity/Editor/Tools/ManageScene.cs index 5f87a5ea2..3b036d7b3 100644 --- a/MCPForUnity/Editor/Tools/ManageScene.cs +++ b/MCPForUnity/Editor/Tools/ManageScene.cs @@ -388,7 +388,7 @@ private static object CaptureScreenshot(string fileName, int? superSize) // Best-effort: ensure Game View exists and repaints before capture. if (!Application.isBatchMode) { - BestEffortPrepareGameViewForScreenshot(); + EnsureGameView(); } ScreenshotCaptureResult result = ScreenshotUtility.CaptureToAssetsFolder(fileName, resolvedSuperSize, ensureUniqueFileName: true); @@ -423,7 +423,7 @@ private static object CaptureScreenshot(string fileName, int? superSize) } } - private static void BestEffortPrepareGameViewForScreenshot() + private static void EnsureGameView() { try { @@ -477,6 +477,7 @@ private static void ScheduleAssetImportWhenFileExists(string assetsRelativePath, { if (string.IsNullOrWhiteSpace(assetsRelativePath) || string.IsNullOrWhiteSpace(fullPath)) { + McpLog.Warn("[ManageScene] ScheduleAssetImportWhenFileExists: invalid paths provided, skipping import scheduling."); return; } @@ -494,7 +495,7 @@ private static void ScheduleAssetImportWhenFileExists(string assetsRelativePath, hasSeenFile = true; AssetDatabase.ImportAsset(assetsRelativePath, ImportAssetOptions.ForceSynchronousImport); - try { McpLog.Debug($"[ManageScene] Imported asset at '{assetsRelativePath}'."); } catch { } + McpLog.Debug($"[ManageScene] Imported asset at '{assetsRelativePath}'."); EditorApplication.update -= tick; return; } @@ -505,13 +506,7 @@ private static void ScheduleAssetImportWhenFileExists(string assetsRelativePath, if (failureCount <= maxLoggedFailures) { - try - { - McpLog.Warn( - $"[ManageScene] Exception while importing asset '{assetsRelativePath}' from '{fullPath}' (attempt {failureCount}): {e}" - ); - } - catch { } + McpLog.Warn($"[ManageScene] Exception while importing asset '{assetsRelativePath}' from '{fullPath}' (attempt {failureCount}): {e}"); } } @@ -519,23 +514,11 @@ private static void ScheduleAssetImportWhenFileExists(string assetsRelativePath, { if (!hasSeenFile) { - try - { - McpLog.Warn( - $"[ManageScene] Timed out waiting for file '{fullPath}' (asset: '{assetsRelativePath}') after {timeoutSeconds:F1} seconds. The asset was not imported." - ); - } - catch { } + McpLog.Warn($"[ManageScene] Timed out waiting for file '{fullPath}' (asset: '{assetsRelativePath}') after {timeoutSeconds:F1} seconds. The asset was not imported."); } else { - try - { - McpLog.Warn( - $"[ManageScene] Timed out importing asset '{assetsRelativePath}' from '{fullPath}' after {timeoutSeconds:F1} seconds. The file existed but the asset was not imported." - ); - } - catch { } + McpLog.Warn($"[ManageScene] Timed out importing asset '{assetsRelativePath}' from '{fullPath}' after {timeoutSeconds:F1} seconds. The file existed but the asset was not imported."); } EditorApplication.update -= tick; @@ -786,8 +769,7 @@ private static object BuildGameObjectSummary(GameObject go, bool includeTransfor } catch (Exception ex) { - try { McpLog.Debug($"[ManageScene] Failed to enumerate components for '{go.name}': {ex.Message}"); } - catch { } + McpLog.Debug($"[ManageScene] Failed to enumerate components for '{go.name}': {ex.Message}"); } var d = new Dictionary From 4de0a9e2fffa7701e423546119419edaa9e285df Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:05:16 -0500 Subject: [PATCH 6/7] Update ScreenshotUtility.cs --- MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs b/MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs index 70b2ebd5b..4a001b058 100644 --- a/MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs +++ b/MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs @@ -32,7 +32,7 @@ public static class ScreenshotUtility private const string ScreenshotsFolderName = "Screenshots"; private static bool s_loggedLegacyScreenCaptureFallback; - private static Camera FindBestCamera() + private static Camera FindAvailableCamera() { var main = Camera.main; if (main != null) @@ -65,7 +65,7 @@ public static ScreenshotCaptureResult CaptureToAssetsFolder(string fileName = nu s_loggedLegacyScreenCaptureFallback = true; } - var cam = FindBestCamera(); + var cam = FindAvailableCamera(); if (cam == null) { throw new InvalidOperationException("No camera found to capture screenshot."); From e1c99c47c2f50a4323fe0ec96428c36b627f6fd6 Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:06:24 -0500 Subject: [PATCH 7/7] Update error logging in ManageScene.cs --- MCPForUnity/Editor/Tools/ManageScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MCPForUnity/Editor/Tools/ManageScene.cs b/MCPForUnity/Editor/Tools/ManageScene.cs index 3b036d7b3..2f9e397b3 100644 --- a/MCPForUnity/Editor/Tools/ManageScene.cs +++ b/MCPForUnity/Editor/Tools/ManageScene.cs @@ -469,7 +469,7 @@ private static void EnsureGameView() } catch (Exception e) { - try { McpLog.Debug($"[ManageScene] screenshot: BestEffortPrepareGameViewForScreenshot failed: {e.Message}"); } catch { } + try { McpLog.Debug($"[ManageScene] screenshot: EnsureGameView failed: {e.Message}"); } catch { } } }