diff --git a/MCPForUnity/Editor/Tools/ManageScene.cs b/MCPForUnity/Editor/Tools/ManageScene.cs
index 150055e17..eb41a8fb8 100644
--- a/MCPForUnity/Editor/Tools/ManageScene.cs
+++ b/MCPForUnity/Editor/Tools/ManageScene.cs
@@ -3,6 +3,7 @@
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 @@ private sealed class SceneCommand
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 @@ private static SceneCommand ToSceneCommand(JObject p)
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 @@ public static object HandleCommand(JObject @params)
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."
);
}
}
+ ///
+ /// 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.
+ ///
+ 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 @@ private static object SaveScene(string fullPath, string relativePath)
}
}
+ 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();
+ 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
diff --git a/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs b/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs
index b6fc2b4a9..a19f3fd6b 100644
--- a/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs
+++ b/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs
@@ -4,6 +4,7 @@
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 @@ private VisualElement CreateToolRow(ToolMetadata tool)
row.Add(parametersLabel);
}
+ if (IsManageSceneTool(tool))
+ {
+ row.Add(CreateManageSceneActions());
+ }
+
return row;
}
@@ -258,6 +264,47 @@ private void AddInfoLabel(string message)
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 @@ private static Label CreateTag(string text)
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;
}
}
diff --git a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
index 1a81947e4..d1932b191 100644
--- a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
+++ b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
@@ -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 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("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(
- $"{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(
- $"{basePath}/Editor/Windows/MCPForUnityEditorWindow.uss"
- );
- if (mainStyleSheet != null)
- {
- rootVisualElement.styleSheets.Add(mainStyleSheet);
- }
-
- // Load common USS
- var commonStyleSheet = AssetDatabase.LoadAssetAtPath(
- $"{basePath}/Editor/Windows/Components/Common.uss"
- );
- if (commonStyleSheet != null)
- {
- rootVisualElement.styleSheets.Add(commonStyleSheet);
- }
-
- settingsPanel = rootVisualElement.Q("settings-panel");
- toolsPanel = rootVisualElement.Q("tools-panel");
- var settingsContainer = rootVisualElement.Q("settings-container");
- var toolsContainer = rootVisualElement.Q("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(
- $"{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(
- $"{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(
- $"{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(
- $"{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("settings-tab");
- toolsTabToggle = rootVisualElement.Q("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 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("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(
+ $"{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(
+ $"{basePath}/Editor/Windows/MCPForUnityEditorWindow.uss"
+ );
+ if (mainStyleSheet != null)
+ {
+ rootVisualElement.styleSheets.Add(mainStyleSheet);
+ }
+
+ // Load common USS
+ var commonStyleSheet = AssetDatabase.LoadAssetAtPath(
+ $"{basePath}/Editor/Windows/Components/Common.uss"
+ );
+ if (commonStyleSheet != null)
+ {
+ rootVisualElement.styleSheets.Add(commonStyleSheet);
+ }
+
+ settingsPanel = rootVisualElement.Q("settings-panel");
+ toolsPanel = rootVisualElement.Q("tools-panel");
+ var settingsContainer = rootVisualElement.Q("settings-container");
+ var toolsContainer = rootVisualElement.Q("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(
+ $"{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(
+ $"{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(
+ $"{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(
+ $"{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("settings-tab");
+ toolsTabToggle = rootVisualElement.Q("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}");
+ }
+ };
+ }
+ }
+}
diff --git a/MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs b/MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs
new file mode 100644
index 000000000..81cefa31d
--- /dev/null
+++ b/MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs
@@ -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);
+ }
+
+ ///
+ /// Captures a screenshot from a specific camera by rendering into a temporary RenderTexture (works in Edit Mode).
+ ///
+ 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;
+ }
+ }
+}
diff --git a/Server/src/services/tools/manage_scene.py b/Server/src/services/tools/manage_scene.py
index 59d6ae9c1..5cb164a88 100644
--- a/Server/src/services/tools/manage_scene.py
+++ b/Server/src/services/tools/manage_scene.py
@@ -12,11 +12,21 @@
)
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 @@ def _coerce_int(value, default=None):
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)
diff --git a/deploy-dev.bat b/deploy-dev.bat
index 866ae2135..a99115bba 100644
--- a/deploy-dev.bat
+++ b/deploy-dev.bat
@@ -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 ===============================================