diff --git a/CLAUDE.md b/CLAUDE.md
index 11c17935e..8b0c0e395 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -140,6 +140,11 @@ Use `CommandRegistry.InvokeCommandAsync` to call other tools from within a handl
var result = await CommandRegistry.InvokeCommandAsync("read_console", consoleParams);
```
+### Unity API Compatibility Shims
+We support a wide Unity version range (2021+ → 6.x → CoreCLR 6.8). When an API is renamed, deprecated, or removed across versions, **don't sprinkle `#if UNITY_x_y_OR_NEWER` at every call site** — add a shim in `MCPForUnity/Runtime/Helpers/Unity*Compat.cs` and route every caller through it.
+
+The catalog of active shims, the policy for when to add one, what does NOT belong in a shim, and the reflection-cache pattern all live in **`MCPForUnity/Runtime/Helpers/UnityCompatShims.cs`** — the XML doc on that empty marker class is the source of truth and ships inside the UPM package, so end-users can `F12`/Go-to-definition into it. Sources for current deprecations: Unity 6.x upgrade guides and the [CoreCLR 2026 thread](https://discussions.unity.com/t/path-to-coreclr-2026-upgrade-guide/1714279).
+
## Commands
### Running Tests
diff --git a/MCPForUnity/Editor/Helpers/EditorWindowScreenshotUtility.cs b/MCPForUnity/Editor/Helpers/EditorWindowScreenshotUtility.cs
index 229b9336c..ee688718b 100644
--- a/MCPForUnity/Editor/Helpers/EditorWindowScreenshotUtility.cs
+++ b/MCPForUnity/Editor/Helpers/EditorWindowScreenshotUtility.cs
@@ -16,7 +16,6 @@ namespace MCPForUnity.Editor.Helpers
///
internal static class EditorWindowScreenshotUtility
{
- private const string ScreenshotsFolderName = "Screenshots";
// Keep capture synchronous so callers can immediately return the screenshot payload.
// The short sleep gives Unity a chance to flush repaint work before GrabPixels reads the viewport.
private const int RepaintSettlingDelayMs = 75;
@@ -40,7 +39,7 @@ internal static class EditorWindowScreenshotUtility
/// Maximum edge length for the inline image payload.
/// Captured viewport width in pixels.
/// Captured viewport height in pixels.
- public static ScreenshotCaptureResult CaptureSceneViewViewportToAssets(
+ public static ScreenshotCaptureResult CaptureSceneViewViewportToProject(
SceneView sceneView,
string fileName,
int superSize,
@@ -48,7 +47,8 @@ public static ScreenshotCaptureResult CaptureSceneViewViewportToAssets(
bool includeImage,
int maxResolution,
out int viewportWidth,
- out int viewportHeight)
+ out int viewportHeight,
+ string folderOverride = null)
{
if (sceneView == null)
throw new ArgumentNullException(nameof(sceneView));
@@ -70,7 +70,7 @@ public static ScreenshotCaptureResult CaptureSceneViewViewportToAssets(
{
captured = CaptureViewRect(sceneView, viewportRectPixels);
- var result = PrepareCaptureResult(fileName, effectiveSuperSize, ensureUniqueFileName);
+ var result = PrepareCaptureResult(fileName, effectiveSuperSize, ensureUniqueFileName, folderOverride);
byte[] png = captured.EncodeToPNG();
File.WriteAllBytes(result.FullPath, png);
@@ -97,7 +97,7 @@ public static ScreenshotCaptureResult CaptureSceneViewViewportToAssets(
return new ScreenshotCaptureResult(
result.FullPath,
- result.AssetsRelativePath,
+ result.ProjectRelativePath,
result.SuperSize,
false,
imageBase64,
@@ -317,11 +317,11 @@ private static void FlipTextureVertically(Texture2D texture)
texture.Apply();
}
- private static ScreenshotCaptureResult PrepareCaptureResult(string fileName, int superSize, bool ensureUniqueFileName)
+ private static ScreenshotCaptureResult PrepareCaptureResult(string fileName, int superSize, bool ensureUniqueFileName, string folderOverride)
{
int size = Mathf.Max(1, superSize);
string resolvedName = BuildFileName(fileName);
- string folder = Path.Combine(Application.dataPath, ScreenshotsFolderName);
+ string folder = ScreenshotUtility.ResolveFolderAbsolute(folderOverride);
Directory.CreateDirectory(folder);
string fullPath = Path.Combine(folder, resolvedName);
@@ -331,8 +331,12 @@ private static ScreenshotCaptureResult PrepareCaptureResult(string fileName, int
}
string normalizedFullPath = fullPath.Replace('\\', '/');
- string assetsRelativePath = "Assets/" + normalizedFullPath.Substring(Application.dataPath.Length).TrimStart('/');
- return new ScreenshotCaptureResult(normalizedFullPath, assetsRelativePath, size, false);
+ string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")).Replace('\\', '/');
+ string normalizedRoot = projectRoot.EndsWith("/") ? projectRoot : projectRoot + "/";
+ string projectRelativePath = normalizedFullPath.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase)
+ ? normalizedFullPath.Substring(normalizedRoot.Length)
+ : normalizedFullPath;
+ return new ScreenshotCaptureResult(normalizedFullPath, projectRelativePath, size, false);
}
private static string BuildFileName(string fileName)
diff --git a/MCPForUnity/Editor/Helpers/ScreenshotPreferences.cs b/MCPForUnity/Editor/Helpers/ScreenshotPreferences.cs
new file mode 100644
index 000000000..b12c97449
--- /dev/null
+++ b/MCPForUnity/Editor/Helpers/ScreenshotPreferences.cs
@@ -0,0 +1,49 @@
+using MCPForUnity.Runtime.Helpers;
+using UnityEditor;
+
+namespace MCPForUnity.Editor.Helpers
+{
+ ///
+ /// Per-user EditorPrefs override for the default screenshot output folder.
+ /// Resolution priority used by callers:
+ /// 1. Per-call output_folder tool parameter
+ /// 2. (this preference)
+ /// 3. built-in fallback
+ ///
+ public static class ScreenshotPreferences
+ {
+ public const string EditorPrefsKey = "MCPForUnity_ScreenshotsFolder";
+
+ ///
+ /// User-configured default folder, or empty string when unset.
+ /// Stored as a project-relative path (e.g. "Assets/Screenshots", "Captures").
+ ///
+ public static string DefaultFolder
+ {
+ get => EditorPrefs.GetString(EditorPrefsKey, string.Empty);
+ set
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ EditorPrefs.DeleteKey(EditorPrefsKey);
+ }
+ else
+ {
+ EditorPrefs.SetString(EditorPrefsKey, value.Trim());
+ }
+ }
+ }
+
+ ///
+ /// Resolves the effective folder: caller override → user pref → built-in default.
+ /// Returns a project-relative path string suitable for .
+ ///
+ public static string Resolve(string callerOverride)
+ {
+ if (!string.IsNullOrWhiteSpace(callerOverride)) return callerOverride.Trim();
+ string pref = DefaultFolder;
+ if (!string.IsNullOrWhiteSpace(pref)) return pref;
+ return ScreenshotUtility.DefaultFolder;
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Helpers/ScreenshotPreferences.cs.meta b/MCPForUnity/Editor/Helpers/ScreenshotPreferences.cs.meta
new file mode 100644
index 000000000..c1668314b
--- /dev/null
+++ b/MCPForUnity/Editor/Helpers/ScreenshotPreferences.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7c4f1a8e9b3d4d2eaf5c1d7b9e2f4a6c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Helpers/UnityTypeResolver.cs b/MCPForUnity/Editor/Helpers/UnityTypeResolver.cs
index feb0b5c7a..8a6f80169 100644
--- a/MCPForUnity/Editor/Helpers/UnityTypeResolver.cs
+++ b/MCPForUnity/Editor/Helpers/UnityTypeResolver.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
+using MCPForUnity.Runtime.Helpers;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
@@ -150,7 +151,7 @@ private static void Cache(Type t)
private static List FindCandidates(string query, Type requiredBaseType)
{
bool isShort = !query.Contains('.');
- var loaded = AppDomain.CurrentDomain.GetAssemblies();
+ var loaded = UnityAssembliesCompat.GetLoadedAssemblies();
#if UNITY_EDITOR
// Names of Player (runtime) script assemblies
diff --git a/MCPForUnity/Editor/Tools/Animation/ClipCreate.cs b/MCPForUnity/Editor/Tools/Animation/ClipCreate.cs
index 47a386c96..b091dce37 100644
--- a/MCPForUnity/Editor/Tools/Animation/ClipCreate.cs
+++ b/MCPForUnity/Editor/Tools/Animation/ClipCreate.cs
@@ -4,6 +4,7 @@
using System.Linq;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Helpers;
+using MCPForUnity.Runtime.Helpers;
using UnityEditor;
using UnityEngine;
@@ -491,7 +492,7 @@ private static Type ResolveType(string typeName)
if (type != null) return type;
// Fallback: search all loaded assemblies
- foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
+ foreach (var assembly in UnityAssembliesCompat.GetLoadedAssemblies())
{
type = assembly.GetType(typeName);
if (type != null) return type;
diff --git a/MCPForUnity/Editor/Tools/CommandRegistry.cs b/MCPForUnity/Editor/Tools/CommandRegistry.cs
index ca39ea51c..c22ab00bb 100644
--- a/MCPForUnity/Editor/Tools/CommandRegistry.cs
+++ b/MCPForUnity/Editor/Tools/CommandRegistry.cs
@@ -5,6 +5,7 @@
using System.Threading.Tasks;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Resources;
+using MCPForUnity.Runtime.Helpers;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@@ -59,7 +60,7 @@ private static void AutoDiscoverCommands()
{
try
{
- var allTypes = AppDomain.CurrentDomain.GetAssemblies()
+ var allTypes = UnityAssembliesCompat.GetLoadedAssemblies()
.Where(a => !a.IsDynamic)
.SelectMany(a =>
{
diff --git a/MCPForUnity/Editor/Tools/ExecuteCode.cs b/MCPForUnity/Editor/Tools/ExecuteCode.cs
index c393e6660..d53c04e28 100644
--- a/MCPForUnity/Editor/Tools/ExecuteCode.cs
+++ b/MCPForUnity/Editor/Tools/ExecuteCode.cs
@@ -6,6 +6,7 @@
using System.Reflection;
using System.Text;
using MCPForUnity.Editor.Helpers;
+using MCPForUnity.Runtime.Helpers;
using Microsoft.CSharp;
using Newtonsoft.Json.Linq;
using UnityEngine;
@@ -360,7 +361,7 @@ private static string[] ResolveAssemblyPaths()
{
var paths = new HashSet(StringComparer.OrdinalIgnoreCase);
- foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
+ foreach (var assembly in UnityAssembliesCompat.GetLoadedAssemblies())
{
try
{
diff --git a/MCPForUnity/Editor/Tools/ManageScene.cs b/MCPForUnity/Editor/Tools/ManageScene.cs
index 7567a78a2..168167852 100644
--- a/MCPForUnity/Editor/Tools/ManageScene.cs
+++ b/MCPForUnity/Editor/Tools/ManageScene.cs
@@ -32,6 +32,7 @@ private sealed class SceneCommand
public string captureSource { get; set; } // "game_view" (default) or "scene_view"
public bool? includeImage { get; set; }
public int? maxResolution { get; set; }
+ public string outputFolder { get; set; } // optional override; null falls back to user pref / Assets/Screenshots
public string batch { get; set; } // "surround" or "orbit" for multi-angle batch capture
public JToken viewTarget { get; set; } // GO reference or [x,y,z] to focus on before capture
public Vector3? viewPosition { get; set; } // camera position for view-based capture
@@ -109,6 +110,7 @@ private static SceneCommand ToSceneCommand(JObject p)
captureSource = toolParams.Get("capture_source"),
includeImage = ParamCoercion.CoerceBoolNullable(p["includeImage"] ?? p["include_image"]),
maxResolution = ParamCoercion.CoerceIntNullable(p["maxResolution"] ?? p["max_resolution"]),
+ outputFolder = (p["outputFolder"] ?? p["output_folder"])?.ToString(),
batch = (p["batch"])?.ToString(),
viewTarget = p["viewTarget"] ?? p["view_target"],
viewPosition = VectorParsing.ParseVector3(p["viewPosition"] ?? p["view_position"]),
@@ -609,16 +611,27 @@ private static object CaptureScreenshot(SceneCommand cmd)
if (!Application.isBatchMode) EnsureGameView();
- ScreenshotCaptureResult result = ScreenshotUtility.CaptureFromCameraToAssetsFolder(
- targetCamera, fileName, resolvedSuperSize, ensureUniqueFileName: true,
- includeImage: includeImage, maxResolution: maxResolution);
+ string folderOverride = ScreenshotPreferences.Resolve(cmd.outputFolder);
+ ScreenshotCaptureResult result;
+ try
+ {
+ result = ScreenshotUtility.CaptureFromCameraToProjectFolder(
+ targetCamera, fileName, resolvedSuperSize, ensureUniqueFileName: true,
+ includeImage: includeImage, maxResolution: maxResolution,
+ folderOverride: folderOverride);
+ }
+ catch (InvalidOperationException ex)
+ {
+ return new ErrorResponse(ex.Message);
+ }
- AssetDatabase.ImportAsset(result.AssetsRelativePath, ImportAssetOptions.ForceSynchronousImport);
- string message = $"Screenshot captured to '{result.AssetsRelativePath}' (camera: {targetCamera.name}).";
+ if (ScreenshotUtility.IsUnderAssets(result.ProjectRelativePath))
+ AssetDatabase.ImportAsset(result.ProjectRelativePath, ImportAssetOptions.ForceSynchronousImport);
+ string message = $"Screenshot captured to '{result.ProjectRelativePath}' (camera: {targetCamera.name}).";
var data = new Dictionary
{
- { "path", result.AssetsRelativePath },
+ { "path", result.ProjectRelativePath },
{ "fullPath", result.FullPath },
{ "superSize", result.SuperSize },
{ "isAsync", false },
@@ -662,19 +675,33 @@ private static object CaptureScreenshot(SceneCommand cmd)
if (!Application.isBatchMode) EnsureGameView();
- ScreenshotCaptureResult defaultResult = ScreenshotUtility.CaptureToAssetsFolder(fileName, resolvedSuperSize, ensureUniqueFileName: true);
+ string defaultFolderOverride = ScreenshotPreferences.Resolve(cmd.outputFolder);
+ ScreenshotCaptureResult defaultResult;
+ try
+ {
+ defaultResult = ScreenshotUtility.CaptureToProjectFolder(
+ fileName, resolvedSuperSize, ensureUniqueFileName: true,
+ folderOverride: defaultFolderOverride);
+ }
+ catch (InvalidOperationException ex)
+ {
+ return new ErrorResponse(ex.Message);
+ }
- if (defaultResult.IsAsync)
- ScheduleAssetImportWhenFileExists(defaultResult.AssetsRelativePath, defaultResult.FullPath, timeoutSeconds: 30.0);
- else
- AssetDatabase.ImportAsset(defaultResult.AssetsRelativePath, ImportAssetOptions.ForceSynchronousImport);
+ if (ScreenshotUtility.IsUnderAssets(defaultResult.ProjectRelativePath))
+ {
+ if (defaultResult.IsAsync)
+ ScheduleAssetImportWhenFileExists(defaultResult.ProjectRelativePath, defaultResult.FullPath, timeoutSeconds: 30.0);
+ else
+ AssetDatabase.ImportAsset(defaultResult.ProjectRelativePath, ImportAssetOptions.ForceSynchronousImport);
+ }
string verb = defaultResult.IsAsync ? "Screenshot requested" : "Screenshot captured";
return new SuccessResponse(
- $"{verb} to '{defaultResult.AssetsRelativePath}'.",
+ $"{verb} to '{defaultResult.ProjectRelativePath}'.",
new
{
- path = defaultResult.AssetsRelativePath,
+ path = defaultResult.ProjectRelativePath,
fullPath = defaultResult.FullPath,
superSize = defaultResult.SuperSize,
isAsync = defaultResult.IsAsync,
@@ -718,22 +745,35 @@ private static object CaptureSceneViewScreenshot(
try
{
- ScreenshotCaptureResult result = EditorWindowScreenshotUtility.CaptureSceneViewViewportToAssets(
- sceneView,
- fileName,
- resolvedSuperSize,
- ensureUniqueFileName: true,
- includeImage: includeImage,
- maxResolution: maxResolution,
- out int viewportWidth,
- out int viewportHeight);
-
- AssetDatabase.ImportAsset(result.AssetsRelativePath, ImportAssetOptions.ForceSynchronousImport);
+ string sceneViewFolderOverride = ScreenshotPreferences.Resolve(cmd.outputFolder);
+ ScreenshotCaptureResult result;
+ int viewportWidth;
+ int viewportHeight;
+ try
+ {
+ result = EditorWindowScreenshotUtility.CaptureSceneViewViewportToProject(
+ sceneView,
+ fileName,
+ resolvedSuperSize,
+ ensureUniqueFileName: true,
+ includeImage: includeImage,
+ maxResolution: maxResolution,
+ out viewportWidth,
+ out viewportHeight,
+ folderOverride: sceneViewFolderOverride);
+ }
+ catch (InvalidOperationException ex) when (ex.Message.StartsWith("Screenshot folder", StringComparison.Ordinal))
+ {
+ return new ErrorResponse(ex.Message);
+ }
+
+ if (ScreenshotUtility.IsUnderAssets(result.ProjectRelativePath))
+ AssetDatabase.ImportAsset(result.ProjectRelativePath, ImportAssetOptions.ForceSynchronousImport);
string sceneViewName = sceneView.titleContent?.text ?? "Scene";
var data = new Dictionary
{
- { "path", result.AssetsRelativePath },
+ { "path", result.ProjectRelativePath },
{ "fullPath", result.FullPath },
{ "superSize", result.SuperSize },
{ "isAsync", false },
@@ -758,7 +798,7 @@ private static object CaptureSceneViewScreenshot(
}
return new SuccessResponse(
- $"Scene View screenshot captured to '{result.AssetsRelativePath}' (scene view: {sceneViewName}).",
+ $"Scene View screenshot captured to '{result.ProjectRelativePath}' (scene view: {sceneViewName}).",
data);
}
catch (Exception e)
@@ -879,14 +919,14 @@ private static object CaptureSurroundBatch(SceneCommand cmd)
var (compositeB64, compW, compH) = ScreenshotUtility.ComposeContactSheet(tiles, tileLabels);
- string screenshotsFolder = Path.Combine(Application.dataPath, "Screenshots");
+ string outputFolder = ResolveAbsoluteOutputFolder(cmd.outputFolder);
return new SuccessResponse(
$"Captured {shotMeta.Count} multi-angle screenshots as contact sheet ({compW}x{compH}). Scene bounds center: ({center.x:F1}, {center.y:F1}, {center.z:F1}), radius: {radius:F1}.",
new
{
sceneCenter = new[] { center.x, center.y, center.z },
sceneRadius = radius,
- screenshotsFolder = screenshotsFolder,
+ outputFolder = outputFolder,
imageBase64 = compositeB64,
imageWidth = compW,
imageHeight = compH,
@@ -1026,7 +1066,7 @@ private static object CaptureOrbitBatch(SceneCommand cmd)
// Compose all tiles into a single contact-sheet grid image
var (compositeB64, compW, compH) = ScreenshotUtility.ComposeContactSheet(tiles, tileLabels);
- string screenshotsFolder = Path.Combine(Application.dataPath, "Screenshots");
+ string outputFolder = ResolveAbsoluteOutputFolder(cmd.outputFolder);
return new SuccessResponse(
$"Captured {shotMeta.Count} orbit screenshots as contact sheet ({compW}x{compH}, {azimuthCount} azimuths x {elevations.Length} elevations). Center: ({center.x:F1}, {center.y:F1}, {center.z:F1}), radius: {radius:F1}.",
new
@@ -1036,7 +1076,7 @@ private static object CaptureOrbitBatch(SceneCommand cmd)
orbitAngles = azimuthCount,
orbitElevations = elevations,
orbitFov = fov,
- screenshotsFolder = screenshotsFolder,
+ outputFolder = outputFolder,
imageBase64 = compositeB64,
imageWidth = compW,
imageHeight = compH,
@@ -1057,7 +1097,8 @@ private static object CaptureOrbitBatch(SceneCommand cmd)
///
/// Captures a single screenshot from a temporary camera placed at view_position and aimed at view_target.
- /// Returns inline base64 PNG and also saves the image to Assets/Screenshots/.
+ /// Returns inline base64 PNG and also saves the image to the resolved screenshot folder
+ /// (caller's output_folder override -> ScreenshotPreferences.DefaultFolder -> built-in Assets/Screenshots).
///
private static object CapturePositionedScreenshot(SceneCommand cmd)
{
@@ -1118,13 +1159,23 @@ private static object CapturePositionedScreenshot(SceneCommand cmd)
var (b64, w, h) = ScreenshotUtility.RenderCameraToBase64(tempCam, maxRes);
- // Save to disk
- string screenshotsFolder = Path.Combine(Application.dataPath, "Screenshots");
- Directory.CreateDirectory(screenshotsFolder);
+ // Resolve output folder (per-call override → user pref → built-in default).
+ string resolvedFolderSpec = ScreenshotPreferences.Resolve(cmd.outputFolder);
+ string folderAbsolute;
+ try
+ {
+ folderAbsolute = ScreenshotUtility.ResolveFolderAbsolute(resolvedFolderSpec);
+ }
+ catch (InvalidOperationException ex)
+ {
+ return new ErrorResponse(ex.Message);
+ }
+ Directory.CreateDirectory(folderAbsolute);
+
string fileName = !string.IsNullOrEmpty(cmd.fileName)
? (cmd.fileName.EndsWith(".png", System.StringComparison.OrdinalIgnoreCase) ? cmd.fileName : cmd.fileName + ".png")
: $"screenshot-{DateTime.Now:yyyyMMdd-HHmmss}.png";
- string fullPath = Path.Combine(screenshotsFolder, fileName);
+ string fullPath = Path.Combine(folderAbsolute, fileName);
// Ensure unique filename
if (File.Exists(fullPath))
{
@@ -1133,15 +1184,22 @@ private static object CapturePositionedScreenshot(SceneCommand cmd)
int counter = 1;
while (File.Exists(fullPath))
{
- fullPath = Path.Combine(screenshotsFolder, $"{baseName}_{counter}{ext}");
+ fullPath = Path.Combine(folderAbsolute, $"{baseName}_{counter}{ext}");
counter++;
}
}
byte[] pngBytes = System.Convert.FromBase64String(b64);
File.WriteAllBytes(fullPath, pngBytes);
- string assetsRelativePath = "Assets/Screenshots/" + Path.GetFileName(fullPath);
- AssetDatabase.ImportAsset(assetsRelativePath, ImportAssetOptions.ForceSynchronousImport);
+ string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")).Replace('\\', '/');
+ string normalizedFull = fullPath.Replace('\\', '/');
+ string normalizedRoot = projectRoot.EndsWith("/") ? projectRoot : projectRoot + "/";
+ string projectRelativePath = normalizedFull.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase)
+ ? normalizedFull.Substring(normalizedRoot.Length)
+ : normalizedFull;
+
+ if (ScreenshotUtility.IsUnderAssets(projectRelativePath))
+ AssetDatabase.ImportAsset(projectRelativePath, ImportAssetOptions.ForceSynchronousImport);
var data = new Dictionary
{
@@ -1149,14 +1207,15 @@ private static object CapturePositionedScreenshot(SceneCommand cmd)
{ "imageWidth", w },
{ "imageHeight", h },
{ "viewPosition", new[] { camPos.x, camPos.y, camPos.z } },
- { "screenshotsFolder", screenshotsFolder },
- { "path", assetsRelativePath },
+ { "outputFolder", folderAbsolute.Replace('\\', '/') },
+ { "path", projectRelativePath },
+ { "fullPath", normalizedFull },
};
if (targetPos.HasValue)
data["viewTarget"] = new[] { targetPos.Value.x, targetPos.Value.y, targetPos.Value.z };
return new SuccessResponse(
- $"Positioned screenshot captured (max {maxRes}px) and saved to '{assetsRelativePath}'.",
+ $"Positioned screenshot captured (max {maxRes}px) and saved to '{projectRelativePath}'.",
data
);
}
@@ -1171,6 +1230,17 @@ private static object CapturePositionedScreenshot(SceneCommand cmd)
}
}
+ ///
+ /// Resolves the per-call/per-pref/built-in screenshot folder spec to an absolute path.
+ /// Propagates validation errors from
+ /// so callers can surface them rather than silently writing somewhere else.
+ ///
+ private static string ResolveAbsoluteOutputFolder(string callerOverride)
+ {
+ string spec = ScreenshotPreferences.Resolve(callerOverride);
+ return ScreenshotUtility.ResolveFolderAbsolute(spec).Replace('\\', '/');
+ }
+
private static string GetDirectionLabel(float azimuthDeg)
{
float a = ((azimuthDeg % 360f) + 360f) % 360f;
@@ -1457,7 +1527,6 @@ private static void ScheduleAssetImportWhenFileExists(string assetsRelativePath,
if (File.Exists(fullPath))
{
hasSeenFile = true;
-
AssetDatabase.ImportAsset(assetsRelativePath, ImportAssetOptions.ForceSynchronousImport);
McpLog.Debug($"[ManageScene] Imported asset at '{assetsRelativePath}'.");
EditorApplication.update -= tick;
@@ -1467,7 +1536,6 @@ private static void ScheduleAssetImportWhenFileExists(string assetsRelativePath,
catch (Exception e)
{
failureCount++;
-
if (failureCount <= maxLoggedFailures)
{
McpLog.Warn($"[ManageScene] Exception while importing asset '{assetsRelativePath}' from '{fullPath}' (attempt {failureCount}): {e}");
@@ -1477,14 +1545,9 @@ private static void ScheduleAssetImportWhenFileExists(string assetsRelativePath,
if (EditorApplication.timeSinceStartup - start > timeoutSeconds)
{
if (!hasSeenFile)
- {
McpLog.Warn($"[ManageScene] Timed out waiting for file '{fullPath}' (asset: '{assetsRelativePath}') after {timeoutSeconds:F1} seconds. The asset was not imported.");
- }
else
- {
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;
}
};
@@ -1492,6 +1555,7 @@ private static void ScheduleAssetImportWhenFileExists(string assetsRelativePath,
EditorApplication.update += tick;
}
+
// ── Multi-scene editing ────────────────────────────────────────────
private static object LoadSceneAdditive(string scenePath)
diff --git a/MCPForUnity/Editor/Tools/ManageUI.cs b/MCPForUnity/Editor/Tools/ManageUI.cs
index ec1a57c81..f67bcff40 100644
--- a/MCPForUnity/Editor/Tools/ManageUI.cs
+++ b/MCPForUnity/Editor/Tools/ManageUI.cs
@@ -860,12 +860,24 @@ private static object RenderUI(JObject @params)
bool includeImage = p.GetBool("include_image") || p.GetBool("includeImage");
int maxResolution = p.GetInt("max_resolution") ?? p.GetInt("maxResolution") ?? 640;
string fileName = p.Get("file_name") ?? p.Get("fileName");
+ string outputFolderOverride = p.Get("output_folder") ?? p.Get("outputFolder");
if (string.IsNullOrEmpty(target) && string.IsNullOrEmpty(uxmlPath))
{
return new ErrorResponse("Either 'target' (GameObject with UIDocument) or 'path' (UXML asset path) is required.");
}
+ string resolvedFolderSpec = ScreenshotPreferences.Resolve(outputFolderOverride);
+ string resolvedFolderAbs;
+ try
+ {
+ resolvedFolderAbs = ScreenshotUtility.ResolveFolderAbsolute(resolvedFolderSpec);
+ }
+ catch (InvalidOperationException ex)
+ {
+ return new ErrorResponse(ex.Message);
+ }
+
// ── Play-mode capture via ScreenCapture coroutine ──────────────────────
// PanelSettings.targetTexture is read in the same frame it is assigned,
// so the RT is always blank in a synchronous tool call. In play mode we
@@ -882,11 +894,10 @@ private static object RenderUI(JObject @params)
if (!resolvedPlayName.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
resolvedPlayName += ".png";
- string playFolder = Path.Combine(Application.dataPath, "Screenshots");
- Directory.CreateDirectory(playFolder);
- string playFullPath = Path.Combine(playFolder, resolvedPlayName).Replace('\\', '/');
+ Directory.CreateDirectory(resolvedFolderAbs);
+ string playFullPath = Path.Combine(resolvedFolderAbs, resolvedPlayName).Replace('\\', '/');
playFullPath = EnsureUniqueFilePath(playFullPath);
- string playAssetsRelPath = "Assets/Screenshots/" + Path.GetFileName(playFullPath);
+ string playProjectRelPath = ScreenshotUtility.ToProjectRelativePath(playFullPath);
// ── Case 1: capture is ready ──────────────────────────────────────
if (s_pendingCaptureDone && s_pendingCaptureTex != null)
@@ -901,11 +912,12 @@ private static object RenderUI(JObject @params)
UnityEngine.Object.DestroyImmediate(captureTex);
File.WriteAllBytes(playFullPath, capturePng);
- AssetDatabase.ImportAsset(playAssetsRelPath, ImportAssetOptions.ForceSynchronousImport);
+ if (ScreenshotUtility.IsUnderAssets(playProjectRelPath))
+ AssetDatabase.ImportAsset(playProjectRelPath, ImportAssetOptions.ForceSynchronousImport);
var playData = new Dictionary
{
- { "path", playAssetsRelPath },
+ { "path", playProjectRelPath },
{ "fullPath", playFullPath },
{ "width", captureW },
{ "height", captureH },
@@ -944,7 +956,7 @@ private static object RenderUI(JObject @params)
}
}
- return new SuccessResponse($"UI render saved to '{playAssetsRelPath}'.", playData);
+ return new SuccessResponse($"UI render saved to '{playProjectRelPath}'.", playData);
}
// ── Case 2: start a new capture ───────────────────────────────────
@@ -1134,20 +1146,20 @@ private static object RenderUI(JObject @params)
if (!resolvedName.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
resolvedName += ".png";
- string folder = Path.Combine(Application.dataPath, "Screenshots");
- Directory.CreateDirectory(folder);
- string fullPath = Path.Combine(folder, resolvedName).Replace('\\', '/');
+ Directory.CreateDirectory(resolvedFolderAbs);
+ string fullPath = Path.Combine(resolvedFolderAbs, resolvedName).Replace('\\', '/');
fullPath = EnsureUniqueFilePath(fullPath);
byte[] png = tex.EncodeToPNG();
File.WriteAllBytes(fullPath, png);
- string assetsRelPath = "Assets/Screenshots/" + Path.GetFileName(fullPath);
- AssetDatabase.ImportAsset(assetsRelPath, ImportAssetOptions.ForceSynchronousImport);
+ string projectRelPath = ScreenshotUtility.ToProjectRelativePath(fullPath);
+ if (ScreenshotUtility.IsUnderAssets(projectRelPath))
+ AssetDatabase.ImportAsset(projectRelPath, ImportAssetOptions.ForceSynchronousImport);
var data = new Dictionary
{
- { "path", assetsRelPath },
+ { "path", projectRelPath },
{ "fullPath", fullPath },
{ "width", width },
{ "height", height },
@@ -1191,10 +1203,10 @@ private static object RenderUI(JObject @params)
UnityEngine.Object.DestroyImmediate(tex);
string msg = hasContent
- ? $"UI rendered to '{assetsRelPath}'."
+ ? $"UI rendered to '{projectRelPath}'."
: rtJustAssigned
? $"RenderTexture assigned to PanelSettings. Call render_ui again to capture the rendered content."
- : $"UI render saved to '{assetsRelPath}' (no visible content detected).";
+ : $"UI render saved to '{projectRelPath}' (no visible content detected).";
return new SuccessResponse(msg, data);
}
diff --git a/MCPForUnity/Editor/Tools/Physics/PhysicsSettingsOps.cs b/MCPForUnity/Editor/Tools/Physics/PhysicsSettingsOps.cs
index 69bc247e6..52af8145d 100644
--- a/MCPForUnity/Editor/Tools/Physics/PhysicsSettingsOps.cs
+++ b/MCPForUnity/Editor/Tools/Physics/PhysicsSettingsOps.cs
@@ -3,6 +3,7 @@
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Helpers;
+using MCPForUnity.Runtime.Helpers;
namespace MCPForUnity.Editor.Tools.Physics
{
@@ -12,12 +13,7 @@ public static object Ping(JObject @params)
{
var gravity3d = UnityEngine.Physics.gravity;
var gravity2d = Physics2D.gravity;
-
-#if UNITY_2022_2_OR_NEWER
- var simMode = UnityEngine.Physics.simulationMode.ToString();
-#else
- var simMode = UnityEngine.Physics.autoSimulation ? "FixedUpdate" : "Script";
-#endif
+ var simMode = UnityPhysicsCompat.GetPhysicsSimulationMode().ToString();
return new
{
@@ -59,7 +55,7 @@ public static object GetSettings(JObject @params)
queriesHitTriggers = Physics2D.queriesHitTriggers,
queriesStartInColliders = Physics2D.queriesStartInColliders,
callbacksOnDisable = Physics2D.callbacksOnDisable,
- autoSyncTransforms = Physics2D.autoSyncTransforms
+ autoSyncTransforms = UnityPhysicsCompat.GetPhysics2DAutoSyncTransforms()
}
};
}
@@ -68,11 +64,7 @@ public static object GetSettings(JObject @params)
return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'.");
var g3 = UnityEngine.Physics.gravity;
-#if UNITY_2022_2_OR_NEWER
- var simMode = UnityEngine.Physics.simulationMode.ToString();
-#else
- var simMode = UnityEngine.Physics.autoSimulation ? "FixedUpdate" : "Script";
-#endif
+ var simMode = UnityPhysicsCompat.GetPhysicsSimulationMode().ToString();
return new
{
@@ -90,7 +82,8 @@ public static object GetSettings(JObject @params)
defaultMaxAngularSpeed = UnityEngine.Physics.defaultMaxAngularSpeed,
queriesHitTriggers = UnityEngine.Physics.queriesHitTriggers,
queriesHitBackfaces = UnityEngine.Physics.queriesHitBackfaces,
- simulationMode = simMode
+ simulationMode = simMode,
+ autoSyncTransforms = UnityPhysicsCompat.GetPhysicsAutoSyncTransforms()
}
};
}
@@ -118,7 +111,8 @@ public static object SetSettings(JObject @params)
"gravity", "defaultcontactoffset", "sleepthreshold",
"defaultsolveriterations", "defaultsolvervelocityiterations",
"bouncethreshold", "defaultmaxangularspeed",
- "querieshittriggers", "querieshitbackfaces", "simulationmode"
+ "querieshittriggers", "querieshitbackfaces", "simulationmode",
+ "autosynctransforms"
};
private static object SetSettings3D(JObject settings)
@@ -184,32 +178,28 @@ private static object SetSettings3D(JObject settings)
changed.Add("queriesHitBackfaces");
break;
case "simulationmode":
-#if UNITY_2022_2_OR_NEWER
{
string modeStr = prop.Value.ToString();
- if (System.Enum.TryParse(modeStr, true, out var mode))
+ if (!System.Enum.TryParse(modeStr, true, out var mode)
+ || mode == UnityPhysicsCompat.SimulationMode.Unknown)
{
- UnityEngine.Physics.simulationMode = mode;
- changed.Add("simulationMode");
+ return new ErrorResponse(
+ $"Invalid simulationMode: '{modeStr}'. Valid: FixedUpdate, Update, Script.");
}
- else
+ if (!UnityPhysicsCompat.TrySetPhysicsSimulationMode(mode))
{
return new ErrorResponse(
- $"Invalid simulationMode: '{modeStr}'. Valid: FixedUpdate, Update, Script.");
+ $"simulationMode '{modeStr}' is not supported on this Unity version.");
}
- break;
- }
-#else
- {
- string modeStr = prop.Value.ToString().ToLowerInvariant();
- if (modeStr == "fixedupdate") UnityEngine.Physics.autoSimulation = true;
- else if (modeStr == "script") UnityEngine.Physics.autoSimulation = false;
- else return new ErrorResponse(
- $"Invalid simulationMode: '{prop.Value}'. Valid: FixedUpdate, Script.");
changed.Add("simulationMode");
break;
}
-#endif
+ case "autosynctransforms":
+ if (UnityPhysicsCompat.TrySetPhysicsAutoSyncTransforms(prop.Value.Value()))
+ {
+ changed.Add("autoSyncTransforms");
+ }
+ break;
}
}
@@ -281,8 +271,10 @@ private static object SetSettings2D(JObject settings)
changed.Add("callbacksOnDisable");
break;
case "autosynctransforms":
- Physics2D.autoSyncTransforms = prop.Value.Value();
- changed.Add("autoSyncTransforms");
+ if (UnityPhysicsCompat.TrySetPhysics2DAutoSyncTransforms(prop.Value.Value()))
+ {
+ changed.Add("autoSyncTransforms");
+ }
break;
}
}
diff --git a/MCPForUnity/Editor/Tools/Physics/PhysicsSimulationOps.cs b/MCPForUnity/Editor/Tools/Physics/PhysicsSimulationOps.cs
index 8bf4d752a..fc922110f 100644
--- a/MCPForUnity/Editor/Tools/Physics/PhysicsSimulationOps.cs
+++ b/MCPForUnity/Editor/Tools/Physics/PhysicsSimulationOps.cs
@@ -29,10 +29,9 @@ public static object SimulateStep(JObject @params)
else
{
UnityEngine.Physics.SyncTransforms();
-#if UNITY_2022_2_OR_NEWER
- var prevMode = UnityEngine.Physics.simulationMode;
- if (prevMode != SimulationMode.Script)
- UnityEngine.Physics.simulationMode = SimulationMode.Script;
+ var prevMode = UnityPhysicsCompat.GetPhysicsSimulationMode();
+ if (prevMode != UnityPhysicsCompat.SimulationMode.Script)
+ UnityPhysicsCompat.TrySetPhysicsSimulationMode(UnityPhysicsCompat.SimulationMode.Script);
try
{
for (int i = 0; i < steps; i++)
@@ -40,22 +39,12 @@ public static object SimulateStep(JObject @params)
}
finally
{
- UnityEngine.Physics.simulationMode = prevMode;
- }
-#else
- bool wasAuto = UnityEngine.Physics.autoSimulation;
- if (wasAuto)
- UnityEngine.Physics.autoSimulation = false;
- try
- {
- for (int i = 0; i < steps; i++)
- UnityEngine.Physics.Simulate(stepSize);
- }
- finally
- {
- UnityEngine.Physics.autoSimulation = wasAuto;
+ if (prevMode != UnityPhysicsCompat.SimulationMode.Unknown
+ && prevMode != UnityPhysicsCompat.SimulationMode.Script)
+ {
+ UnityPhysicsCompat.TrySetPhysicsSimulationMode(prevMode);
+ }
}
-#endif
}
// Collect rigidbody states after simulation
diff --git a/MCPForUnity/Editor/Tools/UnityReflect.cs b/MCPForUnity/Editor/Tools/UnityReflect.cs
index 0af4f6187..06aee0d7f 100644
--- a/MCPForUnity/Editor/Tools/UnityReflect.cs
+++ b/MCPForUnity/Editor/Tools/UnityReflect.cs
@@ -6,6 +6,7 @@
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using MCPForUnity.Editor.Helpers;
+using MCPForUnity.Runtime.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
@@ -81,7 +82,7 @@ private static Dictionary GetAssemblyTypeCache()
return _assemblyTypeCache;
_assemblyTypeCache = new Dictionary();
- foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
+ foreach (var asm in UnityAssembliesCompat.GetLoadedAssemblies())
{
try
{
diff --git a/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs b/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs
index eec0f9a74..8a4c98c16 100644
--- a/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs
+++ b/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs
@@ -30,6 +30,9 @@ public class McpAdvancedSection
private Toggle devModeForceRefreshToggle;
private Toggle allowLanHttpBindToggle;
private Toggle allowInsecureRemoteHttpToggle;
+ private TextField screenshotsFolderOverride;
+ private Button browseScreenshotsFolderButton;
+ private Button clearScreenshotsFolderButton;
private TextField deploySourcePath;
private Button browseDeploySourceButton;
private Button clearDeploySourceButton;
@@ -73,6 +76,9 @@ private void CacheUIElements()
devModeForceRefreshToggle = Root.Q("dev-mode-force-refresh-toggle");
allowLanHttpBindToggle = Root.Q("allow-lan-http-bind-toggle");
allowInsecureRemoteHttpToggle = Root.Q("allow-insecure-remote-http-toggle");
+ screenshotsFolderOverride = Root.Q("screenshots-folder-override");
+ browseScreenshotsFolderButton = Root.Q