[FEATURE] Camera Capture (#449)

* Updates on Camera Capture Feature

* Enable Camera Capture through both play and editor mode
Notes: Because the standard ScreenCapture.CaptureScreenshot does not work in editor mode, so we use ScreenCapture.CaptureScreenshotIntoRenderTexture to enable it during play mode.

* The user can access the camera access through the tool menu or through direct LLM calling. Both tested on Windows with Claude Desktop.

* Minor changes

nitpicking changes
main
Shutong Wu 2025-12-09 19:00:30 -05:00 committed by GitHub
parent 8a17cde29e
commit 97b85749b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 707 additions and 393 deletions

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using MCPForUnity.Editor.Helpers; // For Response class
using MCPForUnity.Runtime.Helpers; // For ScreenshotUtility
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
@ -23,6 +24,8 @@ namespace MCPForUnity.Editor.Tools
public string name { get; set; } = string.Empty;
public string path { get; set; } = string.Empty;
public int? buildIndex { get; set; }
public string fileName { get; set; } = string.Empty;
public int? superSize { get; set; }
}
private static SceneCommand ToSceneCommand(JObject p)
@ -42,7 +45,9 @@ namespace MCPForUnity.Editor.Tools
action = (p["action"]?.ToString() ?? string.Empty).Trim().ToLowerInvariant(),
name = p["name"]?.ToString() ?? string.Empty,
path = p["path"]?.ToString() ?? string.Empty,
buildIndex = BI(p["buildIndex"] ?? p["build_index"])
buildIndex = BI(p["buildIndex"] ?? p["build_index"]),
fileName = (p["fileName"] ?? p["filename"])?.ToString() ?? string.Empty,
superSize = BI(p["superSize"] ?? p["super_size"] ?? p["supersize"])
};
}
@ -142,14 +147,26 @@ namespace MCPForUnity.Editor.Tools
return ga;
case "get_build_settings":
return GetBuildSettingsScenes();
case "screenshot":
return CaptureScreenshot(cmd.fileName, cmd.superSize);
// Add cases for modifying build settings, additive loading, unloading etc.
default:
return new ErrorResponse(
$"Unknown action: '{action}'. Valid actions: create, load, save, get_hierarchy, get_active, get_build_settings."
$"Unknown action: '{action}'. Valid actions: create, load, save, get_hierarchy, get_active, get_build_settings, screenshot."
);
}
}
/// <summary>
/// Captures a screenshot to Assets/Screenshots and returns a response payload.
/// Public so the tools UI can reuse the same logic without duplicating parameters.
/// Available in both Edit Mode and Play Mode.
/// </summary>
public static object ExecuteScreenshot(string fileName = null, int? superSize = null)
{
return CaptureScreenshot(fileName, superSize);
}
private static object CreateScene(string fullPath, string relativePath)
{
if (File.Exists(fullPath))
@ -329,6 +346,55 @@ namespace MCPForUnity.Editor.Tools
}
}
private static object CaptureScreenshot(string fileName, int? superSize)
{
try
{
int resolvedSuperSize = (superSize.HasValue && superSize.Value > 0) ? superSize.Value : 1;
ScreenshotCaptureResult result;
if (Application.isPlaying)
{
result = ScreenshotUtility.CaptureToAssetsFolder(fileName, resolvedSuperSize, ensureUniqueFileName: true);
}
else
{
// Edit Mode path: render from the best-guess camera using RenderTexture.
Camera cam = Camera.main;
if (cam == null)
{
var cams = UnityEngine.Object.FindObjectsOfType<Camera>();
cam = cams.FirstOrDefault();
}
if (cam == null)
{
return new ErrorResponse("No camera found to capture screenshot in Edit Mode.");
}
result = ScreenshotUtility.CaptureFromCameraToAssetsFolder(cam, fileName, resolvedSuperSize, ensureUniqueFileName: true);
}
AssetDatabase.Refresh();
string message = $"Screenshot captured to '{result.AssetsRelativePath}' (full: {result.FullPath}).";
return new SuccessResponse(
message,
new
{
path = result.AssetsRelativePath,
fullPath = result.FullPath,
superSize = result.SuperSize,
}
);
}
catch (Exception e)
{
return new ErrorResponse($"Error capturing screenshot: {e.Message}");
}
}
private static object GetActiveSceneInfo()
{
try

View File

@ -4,6 +4,7 @@ using System.Linq;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
using MCPForUnity.Editor.Tools;
using UnityEditor;
using UnityEngine.UIElements;
@ -199,6 +200,11 @@ namespace MCPForUnity.Editor.Windows.Components.Tools
row.Add(parametersLabel);
}
if (IsManageSceneTool(tool))
{
row.Add(CreateManageSceneActions());
}
return row;
}
@ -258,6 +264,47 @@ namespace MCPForUnity.Editor.Windows.Components.Tools
categoryContainer?.Add(label);
}
private VisualElement CreateManageSceneActions()
{
var actions = new VisualElement();
actions.AddToClassList("tool-item-actions");
var screenshotButton = new Button(OnManageSceneScreenshotClicked)
{
text = "Capture Screenshot"
};
screenshotButton.AddToClassList("tool-action-button");
screenshotButton.style.marginTop = 4;
screenshotButton.tooltip = "Capture a screenshot to Assets/Screenshots via manage_scene.";
actions.Add(screenshotButton);
return actions;
}
private void OnManageSceneScreenshotClicked()
{
try
{
var response = ManageScene.ExecuteScreenshot();
if (response is SuccessResponse success && !string.IsNullOrWhiteSpace(success.Message))
{
McpLog.Info(success.Message);
}
else if (response is ErrorResponse error && !string.IsNullOrWhiteSpace(error.Error))
{
McpLog.Error(error.Error);
}
else
{
McpLog.Info("Screenshot capture requested.");
}
}
catch (Exception ex)
{
McpLog.Error($"Failed to capture screenshot: {ex.Message}");
}
}
private static Label CreateTag(string text)
{
var tag = new Label(text);
@ -265,6 +312,8 @@ namespace MCPForUnity.Editor.Windows.Components.Tools
return tag;
}
private static bool IsManageSceneTool(ToolMetadata tool) => string.Equals(tool?.Name, "manage_scene", StringComparison.OrdinalIgnoreCase);
private static bool IsBuiltIn(ToolMetadata tool) => tool?.IsBuiltIn ?? false;
}
}

View File

@ -1,363 +1,363 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
using MCPForUnity.Editor.Windows.Components.ClientConfig;
using MCPForUnity.Editor.Windows.Components.Connection;
using MCPForUnity.Editor.Windows.Components.Settings;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Windows.Components.Tools;
namespace MCPForUnity.Editor.Windows
{
public class MCPForUnityEditorWindow : EditorWindow
{
// Section controllers
private McpSettingsSection settingsSection;
private McpConnectionSection connectionSection;
private McpClientConfigSection clientConfigSection;
private McpToolsSection toolsSection;
private ToolbarToggle settingsTabToggle;
private ToolbarToggle toolsTabToggle;
private VisualElement settingsPanel;
private VisualElement toolsPanel;
private static readonly HashSet<MCPForUnityEditorWindow> OpenWindows = new();
private bool guiCreated = false;
private double lastRefreshTime = 0;
private const double RefreshDebounceSeconds = 0.5;
private enum ActivePanel
{
Settings,
Tools
}
internal static void CloseAllWindows()
{
var windows = OpenWindows.Where(window => window != null).ToArray();
foreach (var window in windows)
{
window.Close();
}
}
public static void ShowWindow()
{
var window = GetWindow<MCPForUnityEditorWindow>("MCP For Unity");
window.minSize = new Vector2(500, 600);
}
// Helper to check and manage open windows from other classes
public static bool HasAnyOpenWindow()
{
return OpenWindows.Count > 0;
}
public static void CloseAllOpenWindows()
{
if (OpenWindows.Count == 0)
return;
// Copy to array to avoid modifying the collection while iterating
var arr = new MCPForUnityEditorWindow[OpenWindows.Count];
OpenWindows.CopyTo(arr);
foreach (var window in arr)
{
try
{
window?.Close();
}
catch (Exception ex)
{
McpLog.Warn($"Error closing MCP window: {ex.Message}");
}
}
}
public void CreateGUI()
{
// Guard against repeated CreateGUI calls (e.g., domain reloads)
if (guiCreated)
return;
string basePath = AssetPathUtility.GetMcpPackageRootPath();
// Load main window UXML
var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/MCPForUnityEditorWindow.uxml"
);
if (visualTree == null)
{
McpLog.Error(
$"Failed to load UXML at: {basePath}/Editor/Windows/MCPForUnityEditorWindow.uxml"
);
return;
}
visualTree.CloneTree(rootVisualElement);
// Load main window USS
var mainStyleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(
$"{basePath}/Editor/Windows/MCPForUnityEditorWindow.uss"
);
if (mainStyleSheet != null)
{
rootVisualElement.styleSheets.Add(mainStyleSheet);
}
// Load common USS
var commonStyleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(
$"{basePath}/Editor/Windows/Components/Common.uss"
);
if (commonStyleSheet != null)
{
rootVisualElement.styleSheets.Add(commonStyleSheet);
}
settingsPanel = rootVisualElement.Q<VisualElement>("settings-panel");
toolsPanel = rootVisualElement.Q<VisualElement>("tools-panel");
var settingsContainer = rootVisualElement.Q<VisualElement>("settings-container");
var toolsContainer = rootVisualElement.Q<VisualElement>("tools-container");
if (settingsPanel == null || toolsPanel == null)
{
McpLog.Error("Failed to find tab panels in UXML");
return;
}
if (settingsContainer == null)
{
McpLog.Error("Failed to find settings-container in UXML");
return;
}
if (toolsContainer == null)
{
McpLog.Error("Failed to find tools-container in UXML");
return;
}
SetupTabs();
// Load and initialize Settings section
var settingsTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/Components/Settings/McpSettingsSection.uxml"
);
if (settingsTree != null)
{
var settingsRoot = settingsTree.Instantiate();
settingsContainer.Add(settingsRoot);
settingsSection = new McpSettingsSection(settingsRoot);
settingsSection.OnGitUrlChanged += () =>
clientConfigSection?.UpdateManualConfiguration();
settingsSection.OnHttpServerCommandUpdateRequested += () =>
connectionSection?.UpdateHttpServerCommandDisplay();
}
// Load and initialize Connection section
var connectionTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/Components/Connection/McpConnectionSection.uxml"
);
if (connectionTree != null)
{
var connectionRoot = connectionTree.Instantiate();
settingsContainer.Add(connectionRoot);
connectionSection = new McpConnectionSection(connectionRoot);
connectionSection.OnManualConfigUpdateRequested += () =>
clientConfigSection?.UpdateManualConfiguration();
}
// Load and initialize Client Configuration section
var clientConfigTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/Components/ClientConfig/McpClientConfigSection.uxml"
);
if (clientConfigTree != null)
{
var clientConfigRoot = clientConfigTree.Instantiate();
settingsContainer.Add(clientConfigRoot);
clientConfigSection = new McpClientConfigSection(clientConfigRoot);
}
// Load and initialize Tools section
var toolsTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/Components/Tools/McpToolsSection.uxml"
);
if (toolsTree != null)
{
var toolsRoot = toolsTree.Instantiate();
toolsContainer.Add(toolsRoot);
toolsSection = new McpToolsSection(toolsRoot);
toolsSection.Refresh();
}
else
{
McpLog.Warn("Failed to load tools section UXML. Tool configuration will be unavailable.");
}
guiCreated = true;
// Initial updates
RefreshAllData();
}
private void OnEnable()
{
EditorApplication.update += OnEditorUpdate;
OpenWindows.Add(this);
}
private void OnDisable()
{
EditorApplication.update -= OnEditorUpdate;
OpenWindows.Remove(this);
guiCreated = false;
}
private void OnFocus()
{
// Only refresh data if UI is built
if (rootVisualElement == null || rootVisualElement.childCount == 0)
return;
RefreshAllData();
}
private void OnEditorUpdate()
{
if (rootVisualElement == null || rootVisualElement.childCount == 0)
return;
connectionSection?.UpdateConnectionStatus();
}
private void RefreshAllData()
{
// Debounce rapid successive calls (e.g., from OnFocus being called multiple times)
double currentTime = EditorApplication.timeSinceStartup;
if (currentTime - lastRefreshTime < RefreshDebounceSeconds)
{
return;
}
lastRefreshTime = currentTime;
connectionSection?.UpdateConnectionStatus();
if (MCPServiceLocator.Bridge.IsRunning)
{
_ = connectionSection?.VerifyBridgeConnectionAsync();
}
settingsSection?.UpdatePathOverrides();
clientConfigSection?.RefreshSelectedClient();
}
private void SetupTabs()
{
settingsTabToggle = rootVisualElement.Q<ToolbarToggle>("settings-tab");
toolsTabToggle = rootVisualElement.Q<ToolbarToggle>("tools-tab");
settingsPanel?.RemoveFromClassList("hidden");
toolsPanel?.RemoveFromClassList("hidden");
if (settingsTabToggle != null)
{
settingsTabToggle.RegisterValueChangedCallback(evt =>
{
if (!evt.newValue)
{
if (toolsTabToggle != null && !toolsTabToggle.value)
{
settingsTabToggle.SetValueWithoutNotify(true);
}
return;
}
SwitchPanel(ActivePanel.Settings);
});
}
if (toolsTabToggle != null)
{
toolsTabToggle.RegisterValueChangedCallback(evt =>
{
if (!evt.newValue)
{
if (settingsTabToggle != null && !settingsTabToggle.value)
{
toolsTabToggle.SetValueWithoutNotify(true);
}
return;
}
SwitchPanel(ActivePanel.Tools);
});
}
var savedPanel = EditorPrefs.GetString(EditorPrefKeys.EditorWindowActivePanel, ActivePanel.Settings.ToString());
if (!Enum.TryParse(savedPanel, out ActivePanel initialPanel))
{
initialPanel = ActivePanel.Settings;
}
SwitchPanel(initialPanel);
}
private void SwitchPanel(ActivePanel panel)
{
bool showSettings = panel == ActivePanel.Settings;
if (settingsPanel != null)
{
settingsPanel.style.display = showSettings ? DisplayStyle.Flex : DisplayStyle.None;
}
if (toolsPanel != null)
{
toolsPanel.style.display = showSettings ? DisplayStyle.None : DisplayStyle.Flex;
}
settingsTabToggle?.SetValueWithoutNotify(showSettings);
toolsTabToggle?.SetValueWithoutNotify(!showSettings);
EditorPrefs.SetString(EditorPrefKeys.EditorWindowActivePanel, panel.ToString());
}
internal static void RequestHealthVerification()
{
foreach (var window in OpenWindows)
{
window?.ScheduleHealthCheck();
}
}
private void ScheduleHealthCheck()
{
EditorApplication.delayCall += async () =>
{
// Ensure window and components are still valid before execution
if (this == null || connectionSection == null)
{
return;
}
try
{
await connectionSection.VerifyBridgeConnectionAsync();
}
catch (Exception ex)
{
// Log but don't crash if verification fails during cleanup
McpLog.Warn($"Health check verification failed: {ex.Message}");
}
};
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
using MCPForUnity.Editor.Windows.Components.ClientConfig;
using MCPForUnity.Editor.Windows.Components.Connection;
using MCPForUnity.Editor.Windows.Components.Settings;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Windows.Components.Tools;
namespace MCPForUnity.Editor.Windows
{
public class MCPForUnityEditorWindow : EditorWindow
{
// Section controllers
private McpSettingsSection settingsSection;
private McpConnectionSection connectionSection;
private McpClientConfigSection clientConfigSection;
private McpToolsSection toolsSection;
private ToolbarToggle settingsTabToggle;
private ToolbarToggle toolsTabToggle;
private VisualElement settingsPanel;
private VisualElement toolsPanel;
private static readonly HashSet<MCPForUnityEditorWindow> OpenWindows = new();
private bool guiCreated = false;
private double lastRefreshTime = 0;
private const double RefreshDebounceSeconds = 0.5;
private enum ActivePanel
{
Settings,
Tools
}
internal static void CloseAllWindows()
{
var windows = OpenWindows.Where(window => window != null).ToArray();
foreach (var window in windows)
{
window.Close();
}
}
public static void ShowWindow()
{
var window = GetWindow<MCPForUnityEditorWindow>("MCP For Unity");
window.minSize = new Vector2(500, 600);
}
// Helper to check and manage open windows from other classes
public static bool HasAnyOpenWindow()
{
return OpenWindows.Count > 0;
}
public static void CloseAllOpenWindows()
{
if (OpenWindows.Count == 0)
return;
// Copy to array to avoid modifying the collection while iterating
var arr = new MCPForUnityEditorWindow[OpenWindows.Count];
OpenWindows.CopyTo(arr);
foreach (var window in arr)
{
try
{
window?.Close();
}
catch (Exception ex)
{
McpLog.Warn($"Error closing MCP window: {ex.Message}");
}
}
}
public void CreateGUI()
{
// Guard against repeated CreateGUI calls (e.g., domain reloads)
if (guiCreated)
return;
string basePath = AssetPathUtility.GetMcpPackageRootPath();
// Load main window UXML
var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/MCPForUnityEditorWindow.uxml"
);
if (visualTree == null)
{
McpLog.Error(
$"Failed to load UXML at: {basePath}/Editor/Windows/MCPForUnityEditorWindow.uxml"
);
return;
}
visualTree.CloneTree(rootVisualElement);
// Load main window USS
var mainStyleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(
$"{basePath}/Editor/Windows/MCPForUnityEditorWindow.uss"
);
if (mainStyleSheet != null)
{
rootVisualElement.styleSheets.Add(mainStyleSheet);
}
// Load common USS
var commonStyleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(
$"{basePath}/Editor/Windows/Components/Common.uss"
);
if (commonStyleSheet != null)
{
rootVisualElement.styleSheets.Add(commonStyleSheet);
}
settingsPanel = rootVisualElement.Q<VisualElement>("settings-panel");
toolsPanel = rootVisualElement.Q<VisualElement>("tools-panel");
var settingsContainer = rootVisualElement.Q<VisualElement>("settings-container");
var toolsContainer = rootVisualElement.Q<VisualElement>("tools-container");
if (settingsPanel == null || toolsPanel == null)
{
McpLog.Error("Failed to find tab panels in UXML");
return;
}
if (settingsContainer == null)
{
McpLog.Error("Failed to find settings-container in UXML");
return;
}
if (toolsContainer == null)
{
McpLog.Error("Failed to find tools-container in UXML");
return;
}
SetupTabs();
// Load and initialize Settings section
var settingsTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/Components/Settings/McpSettingsSection.uxml"
);
if (settingsTree != null)
{
var settingsRoot = settingsTree.Instantiate();
settingsContainer.Add(settingsRoot);
settingsSection = new McpSettingsSection(settingsRoot);
settingsSection.OnGitUrlChanged += () =>
clientConfigSection?.UpdateManualConfiguration();
settingsSection.OnHttpServerCommandUpdateRequested += () =>
connectionSection?.UpdateHttpServerCommandDisplay();
}
// Load and initialize Connection section
var connectionTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/Components/Connection/McpConnectionSection.uxml"
);
if (connectionTree != null)
{
var connectionRoot = connectionTree.Instantiate();
settingsContainer.Add(connectionRoot);
connectionSection = new McpConnectionSection(connectionRoot);
connectionSection.OnManualConfigUpdateRequested += () =>
clientConfigSection?.UpdateManualConfiguration();
}
// Load and initialize Client Configuration section
var clientConfigTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/Components/ClientConfig/McpClientConfigSection.uxml"
);
if (clientConfigTree != null)
{
var clientConfigRoot = clientConfigTree.Instantiate();
settingsContainer.Add(clientConfigRoot);
clientConfigSection = new McpClientConfigSection(clientConfigRoot);
}
// Load and initialize Tools section
var toolsTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/Components/Tools/McpToolsSection.uxml"
);
if (toolsTree != null)
{
var toolsRoot = toolsTree.Instantiate();
toolsContainer.Add(toolsRoot);
toolsSection = new McpToolsSection(toolsRoot);
toolsSection.Refresh();
}
else
{
McpLog.Warn("Failed to load tools section UXML. Tool configuration will be unavailable.");
}
guiCreated = true;
// Initial updates
RefreshAllData();
}
private void OnEnable()
{
EditorApplication.update += OnEditorUpdate;
OpenWindows.Add(this);
}
private void OnDisable()
{
EditorApplication.update -= OnEditorUpdate;
OpenWindows.Remove(this);
guiCreated = false;
}
private void OnFocus()
{
// Only refresh data if UI is built
if (rootVisualElement == null || rootVisualElement.childCount == 0)
return;
RefreshAllData();
}
private void OnEditorUpdate()
{
if (rootVisualElement == null || rootVisualElement.childCount == 0)
return;
connectionSection?.UpdateConnectionStatus();
}
private void RefreshAllData()
{
// Debounce rapid successive calls (e.g., from OnFocus being called multiple times)
double currentTime = EditorApplication.timeSinceStartup;
if (currentTime - lastRefreshTime < RefreshDebounceSeconds)
{
return;
}
lastRefreshTime = currentTime;
connectionSection?.UpdateConnectionStatus();
if (MCPServiceLocator.Bridge.IsRunning)
{
_ = connectionSection?.VerifyBridgeConnectionAsync();
}
settingsSection?.UpdatePathOverrides();
clientConfigSection?.RefreshSelectedClient();
}
private void SetupTabs()
{
settingsTabToggle = rootVisualElement.Q<ToolbarToggle>("settings-tab");
toolsTabToggle = rootVisualElement.Q<ToolbarToggle>("tools-tab");
settingsPanel?.RemoveFromClassList("hidden");
toolsPanel?.RemoveFromClassList("hidden");
if (settingsTabToggle != null)
{
settingsTabToggle.RegisterValueChangedCallback(evt =>
{
if (!evt.newValue)
{
if (toolsTabToggle != null && !toolsTabToggle.value)
{
settingsTabToggle.SetValueWithoutNotify(true);
}
return;
}
SwitchPanel(ActivePanel.Settings);
});
}
if (toolsTabToggle != null)
{
toolsTabToggle.RegisterValueChangedCallback(evt =>
{
if (!evt.newValue)
{
if (settingsTabToggle != null && !settingsTabToggle.value)
{
toolsTabToggle.SetValueWithoutNotify(true);
}
return;
}
SwitchPanel(ActivePanel.Tools);
});
}
var savedPanel = EditorPrefs.GetString(EditorPrefKeys.EditorWindowActivePanel, ActivePanel.Settings.ToString());
if (!Enum.TryParse(savedPanel, out ActivePanel initialPanel))
{
initialPanel = ActivePanel.Settings;
}
SwitchPanel(initialPanel);
}
private void SwitchPanel(ActivePanel panel)
{
bool showSettings = panel == ActivePanel.Settings;
if (settingsPanel != null)
{
settingsPanel.style.display = showSettings ? DisplayStyle.Flex : DisplayStyle.None;
}
if (toolsPanel != null)
{
toolsPanel.style.display = showSettings ? DisplayStyle.None : DisplayStyle.Flex;
}
settingsTabToggle?.SetValueWithoutNotify(showSettings);
toolsTabToggle?.SetValueWithoutNotify(!showSettings);
EditorPrefs.SetString(EditorPrefKeys.EditorWindowActivePanel, panel.ToString());
}
internal static void RequestHealthVerification()
{
foreach (var window in OpenWindows)
{
window?.ScheduleHealthCheck();
}
}
private void ScheduleHealthCheck()
{
EditorApplication.delayCall += async () =>
{
// Ensure window and components are still valid before execution
if (this == null || connectionSection == null)
{
return;
}
try
{
await connectionSection.VerifyBridgeConnectionAsync();
}
catch (Exception ex)
{
// Log but don't crash if verification fails during cleanup
McpLog.Warn($"Health check verification failed: {ex.Message}");
}
};
}
}
}

View File

@ -0,0 +1,181 @@
using System;
using System.IO;
using System.Linq;
using UnityEngine;
namespace MCPForUnity.Runtime.Helpers
//The reason for having another Runtime Utilities in additional to Editor Utilities is to avoid Editor-only dependencies in this runtime code.
{
public readonly struct ScreenshotCaptureResult
{
public ScreenshotCaptureResult(string fullPath, string assetsRelativePath, int superSize)
{
FullPath = fullPath;
AssetsRelativePath = assetsRelativePath;
SuperSize = superSize;
}
public string FullPath { get; }
public string AssetsRelativePath { get; }
public int SuperSize { get; }
}
public static class ScreenshotUtility
{
private const string ScreenshotsFolderName = "Screenshots";
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 only the file name to let Unity decide the final location (per CaptureScreenshot docs).
string captureName = Path.GetFileName(normalizedFullPath);
ScreenCapture.CaptureScreenshot(captureName, size);
Debug.Log($"Screenshot requested: file='{captureName}' intendedFullPath='{normalizedFullPath}' persistentDataPath='{Application.persistentDataPath}'");
string projectRoot = GetProjectRootPath();
string assetsRelativePath = normalizedFullPath;
if (assetsRelativePath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase))
{
assetsRelativePath = assetsRelativePath.Substring(projectRoot.Length).TrimStart('/');
}
return new ScreenshotCaptureResult(
normalizedFullPath,
assetsRelativePath,
size);
}
/// <summary>
/// Captures a screenshot from a specific camera by rendering into a temporary RenderTexture (works in Edit Mode).
/// </summary>
public static ScreenshotCaptureResult CaptureFromCameraToAssetsFolder(Camera camera, string fileName = null, int superSize = 1, bool ensureUniqueFileName = true)
{
if (camera == null)
{
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('\\', '/');
int width = Mathf.Max(1, camera.pixelWidth > 0 ? camera.pixelWidth : Screen.width);
int height = Mathf.Max(1, camera.pixelHeight > 0 ? camera.pixelHeight : Screen.height);
width *= size;
height *= size;
RenderTexture prevRT = camera.targetTexture;
RenderTexture prevActive = RenderTexture.active;
var rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32);
try
{
camera.targetTexture = rt;
camera.Render();
RenderTexture.active = rt;
var tex = new Texture2D(width, height, TextureFormat.RGBA32, false);
tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
tex.Apply();
byte[] png = tex.EncodeToPNG();
File.WriteAllBytes(normalizedFullPath, png);
}
finally
{
camera.targetTexture = prevRT;
RenderTexture.active = prevActive;
RenderTexture.ReleaseTemporary(rt);
}
string projectRoot = GetProjectRootPath();
string assetsRelativePath = normalizedFullPath;
if (assetsRelativePath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase))
{
assetsRelativePath = assetsRelativePath.Substring(projectRoot.Length).TrimStart('/');
}
return new ScreenshotCaptureResult(normalizedFullPath, assetsRelativePath, size);
}
private static string BuildFileName(string fileName)
{
string name = string.IsNullOrWhiteSpace(fileName)
? $"screenshot-{DateTime.Now:yyyyMMdd-HHmmss}"
: fileName.Trim();
name = SanitizeFileName(name);
if (!name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) &&
!name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) &&
!name.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase))
{
name += ".png";
}
return name;
}
private static string SanitizeFileName(string fileName)
{
var invalidChars = Path.GetInvalidFileNameChars();
string cleaned = new string(fileName.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray());
return string.IsNullOrWhiteSpace(cleaned) ? "screenshot" : cleaned;
}
private static string EnsureUnique(string path)
{
if (!File.Exists(path))
{
return path;
}
string directory = Path.GetDirectoryName(path) ?? string.Empty;
string baseName = Path.GetFileNameWithoutExtension(path);
string extension = Path.GetExtension(path);
int counter = 1;
string candidate;
do
{
candidate = Path.Combine(directory, $"{baseName}-{counter}{extension}");
counter++;
} while (File.Exists(candidate));
return candidate;
}
private static string GetProjectRootPath()
{
string root = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
root = root.Replace('\\', '/');
if (!root.EndsWith("/", StringComparison.Ordinal))
{
root += "/";
}
return root;
}
}
}

View File

@ -12,11 +12,21 @@ from transport.legacy.unity_connection import async_send_command_with_retry
)
async def manage_scene(
ctx: Context,
action: Annotated[Literal["create", "load", "save", "get_hierarchy", "get_active", "get_build_settings"], "Perform CRUD operations on Unity scenes."],
action: Annotated[Literal[
"create",
"load",
"save",
"get_hierarchy",
"get_active",
"get_build_settings",
"screenshot",
], "Perform CRUD operations on Unity scenes, and capture a screenshot."],
name: Annotated[str, "Scene name."] | None = None,
path: Annotated[str, "Scene path."] | None = None,
build_index: Annotated[int | str,
"Unity build index (quote as string, e.g., '0')."] | None = None,
screenshot_file_name: Annotated[str, "Screenshot file name (optional). Defaults to timestamp when omitted."] | None = None,
screenshot_super_size: Annotated[int | str, "Screenshot supersize multiplier (integer ≥1). Optional." ] | None = None,
) -> dict[str, Any]:
# Get active instance from session state
# Removed session_state import
@ -39,14 +49,19 @@ async def manage_scene(
return default
coerced_build_index = _coerce_int(build_index, default=None)
coerced_super_size = _coerce_int(screenshot_super_size, default=None)
params = {"action": action}
params: dict[str, Any] = {"action": action}
if name:
params["name"] = name
if path:
params["path"] = path
if coerced_build_index is not None:
params["buildIndex"] = coerced_build_index
if screenshot_file_name:
params["fileName"] = screenshot_file_name
if coerced_super_size is not None:
params["superSize"] = coerced_super_size
# Use centralized retry helper with instance routing
response = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "manage_scene", params)

View File

@ -28,12 +28,8 @@ if "%PACKAGE_CACHE_PATH%"=="" (
exit /b 1
)
:: Server installation path (with default)
echo.
echo Server Installation Path:
echo Default: %DEFAULT_SERVER_PATH%
set /p "SERVER_PATH=Enter server path (or press Enter for default): "
if "%SERVER_PATH%"=="" set "SERVER_PATH=%DEFAULT_SERVER_PATH%"
rem Server installation path prompt disabled (server deploy skipped)
set "SERVER_PATH="
:: Backup location (with default)
echo.
@ -54,24 +50,12 @@ if not exist "%BRIDGE_SOURCE%" (
exit /b 1
)
if not exist "%SERVER_SOURCE%" (
echo Error: Server source not found: %SERVER_SOURCE%
pause
exit /b 1
)
if not exist "%PACKAGE_CACHE_PATH%" (
echo Error: Package cache path not found: %PACKAGE_CACHE_PATH%
pause
exit /b 1
)
if not exist "%SERVER_PATH%" (
echo Error: Server installation path not found: %SERVER_PATH%
pause
exit /b 1
)
:: Create backup directory
if not exist "%BACKUP_DIR%" (
echo Creating backup directory: %BACKUP_DIR%
@ -103,16 +87,27 @@ if exist "%PACKAGE_CACHE_PATH%\Editor" (
)
)
if exist "%SERVER_PATH%" (
echo Backing up Python Server files...
xcopy "%SERVER_PATH%\*" "%BACKUP_SUBDIR%\PythonServer\" /E /I /Y > nul
if exist "%PACKAGE_CACHE_PATH%\Runtime" (
echo Backing up Unity Runtime files...
xcopy "%PACKAGE_CACHE_PATH%\Runtime" "%BACKUP_SUBDIR%\UnityBridge\Runtime\" /E /I /Y > nul
if !errorlevel! neq 0 (
echo Error: Failed to backup Python Server files
echo Error: Failed to backup Unity Runtime files
pause
exit /b 1
)
)
rem Server backup skipped (deprecated legacy deploy)
rem if exist "%SERVER_PATH%" (
rem echo Backing up Python Server files...
rem xcopy "%SERVER_PATH%\*" "%BACKUP_SUBDIR%\PythonServer\" /E /I /Y > nul
rem if !errorlevel! neq 0 (
rem echo Error: Failed to backup Python Server files
rem pause
rem exit /b 1
rem )
rem )
:: Deploy Unity Bridge
echo.
echo Deploying Unity Bridge code...
@ -123,15 +118,23 @@ if !errorlevel! neq 0 (
exit /b 1
)
:: Deploy Python Server
echo Deploying Python Server code...
xcopy "%SERVER_SOURCE%\*" "%SERVER_PATH%\" /E /Y > nul
echo Deploying Unity Runtime code...
xcopy "%BRIDGE_SOURCE%\Runtime\*" "%PACKAGE_CACHE_PATH%\Runtime\" /E /Y > nul
if !errorlevel! neq 0 (
echo Error: Failed to deploy Python Server code
echo Error: Failed to deploy Unity Runtime code
pause
exit /b 1
)
rem Deploy Python Server (disabled; server no longer deployed this way)
rem echo Deploying Python Server code...
rem xcopy "%SERVER_SOURCE%\*" "%SERVER_PATH%\" /E /Y > nul
rem if !errorlevel! neq 0 (
rem echo Error: Failed to deploy Python Server code
rem pause
rem exit /b 1
rem )
:: Success
echo.
echo ===============================================