diff --git a/.claude/skills/unity-mcp-skill/SKILL.md b/.claude/skills/unity-mcp-skill/SKILL.md
index 032aaaff3..12cf41c22 100644
--- a/.claude/skills/unity-mcp-skill/SKILL.md
+++ b/.claude/skills/unity-mcp-skill/SKILL.md
@@ -86,18 +86,24 @@ manage_camera(action="screenshot", camera="MainCamera", include_image=True, max_
manage_camera(action="screenshot", batch="surround", max_resolution=256)
# Batch surround centered on a specific object
-manage_camera(action="screenshot", batch="surround", look_at="Player", max_resolution=256)
+manage_camera(action="screenshot", batch="surround", view_target="Player", max_resolution=256)
# Positioned screenshot: place a temp camera and capture in one call
-manage_camera(action="screenshot", look_at="Player", view_position=[0, 10, -10], max_resolution=512)
+manage_camera(action="screenshot", view_target="Player", view_position=[0, 10, -10], max_resolution=512)
+
+# Scene View screenshot: capture what the developer sees in the editor
+manage_camera(action="screenshot", capture_source="scene_view", include_image=True)
+
+# Scene View framed on a specific object
+manage_camera(action="screenshot", capture_source="scene_view", view_target="Canvas", include_image=True)
```
**Best practices for AI scene understanding:**
- Use `include_image=True` when you need to *see* the scene, not just save a file.
- Use `batch="surround"` for a comprehensive overview (6 angles, one command).
-- Use `look_at`/`view_position` to capture from a specific viewpoint without needing a scene camera.
+- Use `view_target`/`view_position` to capture from a specific viewpoint without needing a scene camera.
+- Use `capture_source="scene_view"` to see the editor viewport (gizmos, wireframes, grid).
- Keep `max_resolution` at 256–512 to balance quality vs. token cost.
-- Combine with `look_at` on `manage_gameobject` to orient a game camera before capturing.
```python
# Agentic camera loop: point, shoot, analyze
@@ -105,9 +111,11 @@ manage_gameobject(action="look_at", target="MainCamera", look_at_target="Player"
manage_camera(action="screenshot", camera="MainCamera", include_image=True, max_resolution=512)
# → Analyze image, decide next action
-# Screenshot from a different camera
-manage_camera(action="screenshot", camera="FollowCam", include_image=True, max_resolution=512)
-manage_camera(action="screenshot_multiview", max_resolution=480) # 6-angle contact sheet
+# Multi-view screenshot (6-angle contact sheet)
+manage_camera(action="screenshot_multiview", max_resolution=480)
+
+# Scene View for editor-level inspection (shows gizmos, debug overlays, etc.)
+manage_camera(action="screenshot", capture_source="scene_view", view_target="Player", include_image=True)
```
### 4. Check Console After Major Changes
diff --git a/.claude/skills/unity-mcp-skill/references/tools-reference.md b/.claude/skills/unity-mcp-skill/references/tools-reference.md
index a052fc9f5..c7fd7f0a0 100644
--- a/.claude/skills/unity-mcp-skill/references/tools-reference.md
+++ b/.claude/skills/unity-mcp-skill/references/tools-reference.md
@@ -136,7 +136,7 @@ manage_scene(
manage_scene(
action="screenshot",
batch="surround",
- look_at="Player", # str|int|list[float] - center surround on this target
+ view_target="Player", # str|int|list[float] - center surround on this target
max_resolution=256
)
@@ -144,7 +144,7 @@ manage_scene(
manage_scene(
action="screenshot",
batch="orbit", # str - "orbit" for configurable angle grid
- look_at="Player", # str|int|list[float] - target to orbit around
+ view_target="Player", # str|int|list[float] - target to orbit around
orbit_angles=8, # int, default 8 - number of azimuth steps
orbit_elevations=[0, 30], # list[float], default [0, 30, -15] - vertical angles in degrees
orbit_distance=10, # float, optional - camera distance (auto-fit if omitted)
@@ -156,9 +156,9 @@ manage_scene(
# Positioned screenshot (temp camera at viewpoint, no file saved)
manage_scene(
action="screenshot",
- look_at="Enemy", # str|int|list[float] - target to aim at
+ view_target="Enemy", # str|int|list[float] - target to aim at
view_position=[0, 10, -10], # list[float], optional - camera position
- view_rotation=[45, 0, 0], # list[float], optional - euler angles (overrides look_at aim)
+ view_rotation=[45, 0, 0], # list[float], optional - euler angles (overrides view_target aim)
max_resolution=512
)
@@ -836,13 +836,14 @@ Unified camera management (Unity Camera + Cinemachine). Works without Cinemachin
| Parameter | Type | Description |
|-----------|------|-------------|
-| `camera` | string | Camera to capture from (defaults to Camera.main) |
+| `capture_source` | string | `"game_view"` (default) or `"scene_view"` (editor viewport) |
+| `view_target` | string\|int\|list | Target to focus on (GO name/path/ID or [x,y,z]). game_view: aims camera; scene_view: frames viewport |
+| `camera` | string | Camera to capture from (defaults to Camera.main). game_view only |
| `include_image` | bool | Return base64 PNG inline (default false) |
| `max_resolution` | int | Downscale cap in px (default 640) |
-| `batch` | string | `"surround"` (6 angles) or `"orbit"` (configurable grid) |
-| `look_at` | string\|int\|list | Target to aim at (GO name/path/ID or [x,y,z]) |
-| `view_position` | list[float] | World position [x,y,z] to place camera |
-| `view_rotation` | list[float] | Euler rotation [x,y,z] (overrides look_at) |
+| `batch` | string | `"surround"` (6 angles) or `"orbit"` (configurable grid). game_view only |
+| `view_position` | list[float] | World position [x,y,z] to place camera. game_view only |
+| `view_rotation` | list[float] | Euler rotation [x,y,z] (overrides view_target). game_view only |
**Actions by category:**
@@ -873,7 +874,7 @@ Unified camera management (Unity Camera + Cinemachine). Works without Cinemachin
- `release_override` — Release camera override
**Capture:**
-- `screenshot` — Capture from a camera. Supports inline base64, batch surround/orbit, positioned capture.
+- `screenshot` — Capture screenshot. Supports `capture_source="game_view"` (default, camera-based) or `"scene_view"` (editor viewport). game_view supports inline base64, batch surround/orbit, positioned capture. scene_view supports `view_target` for framing.
- `screenshot_multiview` — Shorthand for screenshot with batch='surround' and include_image=true.
**Examples:**
@@ -924,9 +925,15 @@ manage_camera(action="add_extension", target="FollowCam", properties={
"extensionType": "CinemachineDeoccluder"
})
-# Screenshot from a specific camera
+# Screenshot from a specific camera (game_view, default)
manage_camera(action="screenshot", camera="FollowCam", include_image=True, max_resolution=512)
+# Scene View screenshot (captures editor viewport — gizmos, wireframes, grid)
+manage_camera(action="screenshot", capture_source="scene_view", include_image=True)
+
+# Scene View screenshot framed on a specific object
+manage_camera(action="screenshot", capture_source="scene_view", view_target="Canvas", include_image=True)
+
# Multi-view screenshot (6-angle contact sheet)
manage_camera(action="screenshot_multiview", max_resolution=480)
diff --git a/.claude/skills/unity-mcp-skill/references/workflows.md b/.claude/skills/unity-mcp-skill/references/workflows.md
index 4de0e2cf9..acf7fee65 100644
--- a/.claude/skills/unity-mcp-skill/references/workflows.md
+++ b/.claude/skills/unity-mcp-skill/references/workflows.md
@@ -1560,6 +1560,26 @@ manage_camera(action="list_cameras")
manage_camera(action="screenshot_multiview", max_resolution=480)
```
+### Scene View Screenshot Workflow
+
+Use `capture_source="scene_view"` to capture the editor's Scene View viewport — useful for seeing gizmos, wireframes, grid, debug overlays, and objects without cameras.
+
+```python
+# 1. Capture the Scene View as-is
+manage_camera(action="screenshot", capture_source="scene_view", include_image=True)
+
+# 2. Frame on a specific object first, then capture
+manage_camera(action="screenshot", capture_source="scene_view",
+ view_target="Player", include_image=True, max_resolution=512)
+
+# 3. Frame on UI Canvas (RectTransform bounds are supported)
+manage_camera(action="screenshot", capture_source="scene_view",
+ view_target="Canvas", include_image=True)
+
+# Limitations: scene_view does not support batch, view_position, view_rotation, or camera selection.
+# Use capture_source="game_view" (default) for those features.
+```
+
---
## ProBuilder Workflows
diff --git a/MCPForUnity/Editor/Helpers/EditorWindowScreenshotUtility.cs b/MCPForUnity/Editor/Helpers/EditorWindowScreenshotUtility.cs
new file mode 100644
index 000000000..229b9336c
--- /dev/null
+++ b/MCPForUnity/Editor/Helpers/EditorWindowScreenshotUtility.cs
@@ -0,0 +1,423 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Runtime.ExceptionServices;
+using System.Threading;
+using MCPForUnity.Runtime.Helpers;
+using UnityEditor;
+using UnityEngine;
+
+namespace MCPForUnity.Editor.Helpers
+{
+ ///
+ /// Captures the pixels currently displayed in an editor window viewport.
+ /// Uses the editor view's own pixel grab path instead of re-rendering through a Camera.
+ ///
+ 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;
+ private static readonly HashSet WindowsReservedNames = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "CON", "PRN", "AUX", "NUL",
+ "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
+ "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
+ };
+
+ ///
+ /// Captures the active Scene View viewport to a PNG asset.
+ ///
+ /// Scene View window to capture.
+ /// Optional file name, defaulting to a timestamped PNG.
+ ///
+ /// Preserved in the result for API consistency, but Scene View capture always uses the current viewport resolution.
+ ///
+ /// If true, appends a suffix instead of overwriting an existing file.
+ /// If true, includes a base64 PNG in the returned result.
+ /// Maximum edge length for the inline image payload.
+ /// Captured viewport width in pixels.
+ /// Captured viewport height in pixels.
+ public static ScreenshotCaptureResult CaptureSceneViewViewportToAssets(
+ SceneView sceneView,
+ string fileName,
+ int superSize,
+ bool ensureUniqueFileName,
+ bool includeImage,
+ int maxResolution,
+ out int viewportWidth,
+ out int viewportHeight)
+ {
+ if (sceneView == null)
+ throw new ArgumentNullException(nameof(sceneView));
+
+ int effectiveSuperSize = NormalizeSceneViewSuperSize(superSize);
+
+ FocusAndRepaint(sceneView);
+
+ Rect viewportRectPixels = GetSceneViewViewportPixelRect(sceneView);
+ viewportWidth = Mathf.RoundToInt(viewportRectPixels.width);
+ viewportHeight = Mathf.RoundToInt(viewportRectPixels.height);
+
+ if (viewportWidth <= 0 || viewportHeight <= 0)
+ throw new InvalidOperationException("Captured Scene view viewport is empty.");
+
+ Texture2D captured = null;
+ Texture2D downscaled = null;
+ try
+ {
+ captured = CaptureViewRect(sceneView, viewportRectPixels);
+
+ var result = PrepareCaptureResult(fileName, effectiveSuperSize, ensureUniqueFileName);
+ byte[] png = captured.EncodeToPNG();
+ File.WriteAllBytes(result.FullPath, png);
+
+ if (includeImage)
+ {
+ int targetMax = maxResolution > 0 ? maxResolution : 640;
+ string imageBase64;
+ int imageWidth;
+ int imageHeight;
+
+ if (captured.width > targetMax || captured.height > targetMax)
+ {
+ downscaled = ScreenshotUtility.DownscaleTexture(captured, targetMax);
+ imageBase64 = Convert.ToBase64String(downscaled.EncodeToPNG());
+ imageWidth = downscaled.width;
+ imageHeight = downscaled.height;
+ }
+ else
+ {
+ imageBase64 = Convert.ToBase64String(png);
+ imageWidth = captured.width;
+ imageHeight = captured.height;
+ }
+
+ return new ScreenshotCaptureResult(
+ result.FullPath,
+ result.AssetsRelativePath,
+ result.SuperSize,
+ false,
+ imageBase64,
+ imageWidth,
+ imageHeight);
+ }
+
+ return result;
+ }
+ finally
+ {
+ DestroyTexture(captured);
+ DestroyTexture(downscaled);
+ }
+ }
+
+ private static void FocusAndRepaint(SceneView sceneView)
+ {
+ try
+ {
+ sceneView.Focus();
+ }
+ catch (Exception ex)
+ {
+ McpLog.Debug($"[EditorWindowScreenshotUtility] SceneView focus failed: {ex.Message}");
+ }
+
+ try
+ {
+ sceneView.Repaint();
+ InvokeMethodIfExists(sceneView, "RepaintImmediately");
+ SceneView.RepaintAll();
+ UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
+ EditorApplication.QueuePlayerLoopUpdate();
+ Thread.Sleep(RepaintSettlingDelayMs);
+ }
+ catch (Exception ex)
+ {
+ McpLog.Debug($"[EditorWindowScreenshotUtility] SceneView repaint failed: {ex.Message}");
+ }
+ }
+
+ private static Rect GetSceneViewViewportPixelRect(SceneView sceneView)
+ {
+ float pixelsPerPoint = EditorGUIUtility.pixelsPerPoint;
+ Rect viewportLocalPoints = GetViewportLocalRectPoints(sceneView, pixelsPerPoint);
+ if (viewportLocalPoints.width <= 0f || viewportLocalPoints.height <= 0f)
+ throw new InvalidOperationException("Failed to resolve Scene view viewport rect.");
+
+ return new Rect(
+ Mathf.Round(viewportLocalPoints.x * pixelsPerPoint),
+ Mathf.Round(viewportLocalPoints.y * pixelsPerPoint),
+ Mathf.Round(viewportLocalPoints.width * pixelsPerPoint),
+ Mathf.Round(viewportLocalPoints.height * pixelsPerPoint));
+ }
+
+ private static Rect GetViewportLocalRectPoints(SceneView sceneView, float pixelsPerPoint)
+ {
+ Rect? cameraViewport = GetRectProperty(sceneView, "cameraViewport");
+ if (cameraViewport.HasValue && cameraViewport.Value.width > 0f && cameraViewport.Value.height > 0f)
+ {
+ return cameraViewport.Value;
+ }
+
+ Camera camera = sceneView.camera;
+ if (camera == null)
+ throw new InvalidOperationException("Active Scene View has no camera to derive viewport size from.");
+
+ float viewportWidth = camera.pixelWidth / Mathf.Max(0.0001f, pixelsPerPoint);
+ float viewportHeight = camera.pixelHeight / Mathf.Max(0.0001f, pixelsPerPoint);
+ Rect windowRect = sceneView.position;
+
+ return new Rect(
+ 0f,
+ Mathf.Max(0f, windowRect.height - viewportHeight),
+ Mathf.Min(windowRect.width, viewportWidth),
+ Mathf.Min(windowRect.height, viewportHeight));
+ }
+
+ private static Texture2D CaptureViewRect(SceneView sceneView, Rect viewportRectPixels)
+ {
+ object hostView = GetHostView(sceneView);
+ if (hostView == null)
+ throw new InvalidOperationException("Failed to resolve Scene view host view.");
+
+ // GrabPixels is an internal extern on GUIView (parent of HostView), present since at least Unity 2021.1.
+ // See: UnityCsReference/Editor/Mono/GUIView.bindings.cs — `internal extern void GrabPixels(RenderTexture, Rect)`
+ // If Unity removes this, the MissingMethodException below keeps the failure explicit.
+ MethodInfo grabPixels = hostView.GetType().GetMethod(
+ "GrabPixels",
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
+ null,
+ new[] { typeof(RenderTexture), typeof(Rect) },
+ null);
+
+ if (grabPixels == null)
+ throw new MissingMethodException($"{hostView.GetType().FullName}.GrabPixels(RenderTexture, Rect)");
+
+ int width = Mathf.RoundToInt(viewportRectPixels.width);
+ int height = Mathf.RoundToInt(viewportRectPixels.height);
+
+ RenderTexture rt = null;
+ RenderTexture previousActive = RenderTexture.active;
+ try
+ {
+ rt = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32)
+ {
+ antiAliasing = 1,
+ filterMode = FilterMode.Bilinear,
+ hideFlags = HideFlags.HideAndDontSave,
+ };
+ rt.Create();
+
+ grabPixels.Invoke(hostView, new object[] { rt, viewportRectPixels });
+
+ RenderTexture.active = rt;
+ var texture = new Texture2D(width, height, TextureFormat.RGBA32, false);
+ texture.ReadPixels(new Rect(0, 0, width, height), 0, 0);
+ texture.Apply();
+ FlipTextureVertically(texture);
+ return texture;
+ }
+ catch (TargetInvocationException ex)
+ {
+ ExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw();
+ throw;
+ }
+ finally
+ {
+ RenderTexture.active = previousActive;
+ if (rt != null)
+ {
+ rt.Release();
+ UnityEngine.Object.DestroyImmediate(rt);
+ }
+ }
+ }
+
+ private static object GetHostView(EditorWindow window)
+ {
+ if (window == null)
+ return null;
+
+ Type windowType = typeof(EditorWindow);
+ FieldInfo parentField = windowType.GetField("m_Parent", BindingFlags.Instance | BindingFlags.NonPublic);
+ if (parentField != null)
+ {
+ object parent = parentField.GetValue(window);
+ if (parent != null)
+ return parent;
+ }
+
+ PropertyInfo hostViewProperty = windowType.GetProperty("hostView", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ return hostViewProperty?.GetValue(window, null);
+ }
+
+ private static Rect? GetRectProperty(object instance, string propertyName)
+ {
+ if (instance == null)
+ return null;
+
+ Type type = instance.GetType();
+ PropertyInfo property = type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ if (property == null || property.PropertyType != typeof(Rect))
+ return null;
+
+ try
+ {
+ return (Rect)property.GetValue(instance, null);
+ }
+ catch (Exception ex)
+ {
+ McpLog.Debug($"[EditorWindowScreenshotUtility] Failed to read rect property '{propertyName}': {ex.Message}");
+ return null;
+ }
+ }
+
+ private static void InvokeMethodIfExists(object instance, string methodName)
+ {
+ if (instance == null)
+ return;
+
+ MethodInfo method = instance.GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ if (method == null || method.GetParameters().Length != 0)
+ return;
+
+ try
+ {
+ method.Invoke(instance, null);
+ }
+ catch (Exception ex)
+ {
+ McpLog.Debug($"[EditorWindowScreenshotUtility] Best-effort invoke of '{methodName}' failed: {ex.Message}");
+ }
+ }
+
+ private static void FlipTextureVertically(Texture2D texture)
+ {
+ if (texture == null)
+ return;
+
+ int width = texture.width;
+ int height = texture.height;
+ Color32[] pixels = texture.GetPixels32();
+ var temp = new Color32[width];
+
+ for (int y = 0; y < height / 2; y++)
+ {
+ int topRow = y * width;
+ int bottomRow = (height - 1 - y) * width;
+ Array.Copy(pixels, topRow, temp, 0, width);
+ Array.Copy(pixels, bottomRow, pixels, topRow, width);
+ Array.Copy(temp, 0, pixels, bottomRow, width);
+ }
+
+ texture.SetPixels32(pixels);
+ texture.Apply();
+ }
+
+ private static ScreenshotCaptureResult PrepareCaptureResult(string fileName, int superSize, bool ensureUniqueFileName)
+ {
+ int size = Mathf.Max(1, superSize);
+ string resolvedName = BuildFileName(fileName);
+ string folder = Path.Combine(Application.dataPath, ScreenshotsFolderName);
+ Directory.CreateDirectory(folder);
+
+ string fullPath = Path.Combine(folder, resolvedName);
+ if (ensureUniqueFileName)
+ {
+ fullPath = EnsureUnique(fullPath);
+ }
+
+ string normalizedFullPath = fullPath.Replace('\\', '/');
+ string assetsRelativePath = "Assets/" + normalizedFullPath.Substring(Application.dataPath.Length).TrimStart('/');
+ return new ScreenshotCaptureResult(normalizedFullPath, assetsRelativePath, size, false);
+ }
+
+ private static string BuildFileName(string fileName)
+ {
+ string baseName = string.IsNullOrWhiteSpace(fileName)
+ ? $"screenshot-{DateTime.Now:yyyyMMdd-HHmmss}.png"
+ : SanitizeFileName(fileName);
+
+ if (!baseName.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
+ baseName += ".png";
+
+ return baseName;
+ }
+
+ private static int NormalizeSceneViewSuperSize(int superSize)
+ {
+ if (superSize > 1)
+ {
+ McpLog.Warn("[EditorWindowScreenshotUtility] Scene View capture ignores superSize and uses the displayed viewport resolution.");
+ return 1;
+ }
+
+ return Mathf.Max(1, superSize);
+ }
+
+ private static string SanitizeFileName(string fileName)
+ {
+ string trimmed = (fileName ?? string.Empty).Trim();
+ if (string.IsNullOrEmpty(trimmed))
+ return $"screenshot-{DateTime.Now:yyyyMMdd-HHmmss}.png";
+
+ string candidate = trimmed;
+ string normalizedSeparators = candidate.Replace('\\', '/');
+ if (Path.IsPathRooted(candidate) || normalizedSeparators.Contains("/") || normalizedSeparators.Contains(".."))
+ {
+ string[] pathParts = normalizedSeparators.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
+ candidate = pathParts.Length > 0 ? pathParts[pathParts.Length - 1] : string.Empty;
+ }
+
+ if (string.IsNullOrWhiteSpace(candidate) || candidate == "." || candidate == "..")
+ candidate = $"screenshot-{DateTime.Now:yyyyMMdd-HHmmss}.png";
+
+ char[] invalidChars = Path.GetInvalidFileNameChars();
+ foreach (char invalidChar in invalidChars)
+ {
+ candidate = candidate.Replace(invalidChar, '_');
+ }
+
+ string extension = Path.GetExtension(candidate);
+ string stem = Path.GetFileNameWithoutExtension(candidate);
+ extension = extension.TrimEnd(' ', '.');
+ stem = stem.TrimEnd(' ', '.');
+ if (WindowsReservedNames.Contains(stem))
+ {
+ candidate = $"_{stem}{extension}";
+ }
+
+ return candidate;
+ }
+
+ private static string EnsureUnique(string fullPath)
+ {
+ if (!File.Exists(fullPath))
+ return fullPath;
+
+ string directory = Path.GetDirectoryName(fullPath) ?? string.Empty;
+ string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fullPath);
+ string extension = Path.GetExtension(fullPath);
+
+ for (int i = 1; i < 10000; i++)
+ {
+ string candidate = Path.Combine(directory, $"{fileNameWithoutExtension}-{i}{extension}");
+ if (!File.Exists(candidate))
+ return candidate;
+ }
+
+ throw new IOException($"Could not generate a unique screenshot filename for '{fullPath}'.");
+ }
+
+ private static void DestroyTexture(Texture2D texture)
+ {
+ if (texture == null)
+ return;
+
+ UnityEngine.Object.DestroyImmediate(texture);
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Helpers/EditorWindowScreenshotUtility.cs.meta b/MCPForUnity/Editor/Helpers/EditorWindowScreenshotUtility.cs.meta
new file mode 100644
index 000000000..d120907fe
--- /dev/null
+++ b/MCPForUnity/Editor/Helpers/EditorWindowScreenshotUtility.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b73350febfd6534436726d19b4d270fd
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Tools/ManageScene.cs b/MCPForUnity/Editor/Tools/ManageScene.cs
index b1810df84..cdae1f22d 100644
--- a/MCPForUnity/Editor/Tools/ManageScene.cs
+++ b/MCPForUnity/Editor/Tools/ManageScene.cs
@@ -29,10 +29,11 @@ private sealed class SceneCommand
// screenshot: camera selection, inline image, batch, view positioning
public string camera { get; set; }
+ public string captureSource { get; set; } // "game_view" (default) or "scene_view"
public bool? includeImage { get; set; }
public int? maxResolution { get; set; }
public string batch { get; set; } // "surround" or "orbit" for multi-angle batch capture
- public JToken lookAt { get; set; } // GO reference or [x,y,z] to aim at before 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
public Vector3? viewRotation { get; set; } // euler rotation for view-based capture
@@ -84,6 +85,7 @@ private static float[] ParseFloatArray(JToken token)
private static SceneCommand ToSceneCommand(JObject p)
{
if (p == null) return new SceneCommand();
+ var toolParams = new ToolParams(p);
return new SceneCommand
{
action = (p["action"]?.ToString() ?? string.Empty).Trim().ToLowerInvariant(),
@@ -95,10 +97,11 @@ private static SceneCommand ToSceneCommand(JObject p)
// screenshot: camera selection, inline image, batch, view positioning
camera = (p["camera"])?.ToString(),
+ captureSource = toolParams.Get("capture_source"),
includeImage = ParamCoercion.CoerceBoolNullable(p["includeImage"] ?? p["include_image"]),
maxResolution = ParamCoercion.CoerceIntNullable(p["maxResolution"] ?? p["max_resolution"]),
batch = (p["batch"])?.ToString(),
- lookAt = p["lookAt"] ?? p["look_at"],
+ viewTarget = p["viewTarget"] ?? p["view_target"],
viewPosition = VectorParsing.ParseVector3(p["viewPosition"] ?? p["view_position"]),
viewRotation = VectorParsing.ParseVector3(p["viewRotation"] ?? p["view_rotation"]),
@@ -109,7 +112,7 @@ private static SceneCommand ToSceneCommand(JObject p)
orbitFov = ParamCoercion.CoerceFloatNullable(p["orbitFov"] ?? p["orbit_fov"]),
// scene_view_frame
- sceneViewTarget = p["sceneViewTarget"] ?? p["scene_view_target"],
+ sceneViewTarget = toolParams.GetRaw("scene_view_target"),
// get_hierarchy paging + safety
parent = p["parent"],
@@ -244,6 +247,16 @@ public static object ExecuteScreenshot(string fileName = null, int? superSize =
/// Captures a 6-angle contact-sheet around the scene bounds centre.
/// Public so the tools UI can reuse the same logic.
///
+ ///
+ /// Captures the active Scene View viewport to a PNG asset.
+ /// Public so the tools UI can reuse the same logic.
+ ///
+ public static object ExecuteSceneViewScreenshot(string fileName = null)
+ {
+ var cmd = new SceneCommand { fileName = fileName ?? string.Empty };
+ return CaptureSceneViewScreenshot(cmd, cmd.fileName, 1, false, 0);
+ }
+
public static object ExecuteMultiviewScreenshot(int maxResolution = 480)
{
var cmd = new SceneCommand { maxResolution = maxResolution };
@@ -433,6 +446,46 @@ private static object CaptureScreenshot(SceneCommand cmd)
{
try
{
+ string fileName = cmd.fileName;
+ int resolvedSuperSize = (cmd.superSize.HasValue && cmd.superSize.Value > 0) ? cmd.superSize.Value : 1;
+ bool includeImage = cmd.includeImage ?? false;
+ int maxResolution = cmd.maxResolution ?? 0; // 0 = let ScreenshotUtility default to 640
+ string cameraRef = cmd.camera;
+ string captureSource = string.IsNullOrWhiteSpace(cmd.captureSource)
+ ? "game_view"
+ : cmd.captureSource.Trim().ToLowerInvariant();
+
+ if (captureSource != "game_view" && captureSource != "scene_view")
+ {
+ return new ErrorResponse(
+ $"Invalid capture_source '{cmd.captureSource}'. Valid values: 'game_view', 'scene_view'.");
+ }
+
+ if (captureSource == "scene_view")
+ {
+ if (resolvedSuperSize > 1)
+ {
+ return new ErrorResponse(
+ "capture_source='scene_view' does not support super_size above 1. Remove 'super_size' or use capture_source='game_view'.");
+ }
+ if (!string.IsNullOrEmpty(cmd.batch))
+ {
+ return new ErrorResponse(
+ "capture_source='scene_view' does not support batch modes. Use capture_source='game_view' for batch capture.");
+ }
+ if (cmd.viewPosition.HasValue || cmd.viewRotation.HasValue)
+ {
+ return new ErrorResponse(
+ "capture_source='scene_view' does not support view_position/view_rotation. Use view_target to frame a Scene View object.");
+ }
+ if (!string.IsNullOrEmpty(cameraRef))
+ {
+ return new ErrorResponse(
+ "capture_source='scene_view' does not support camera selection. Remove 'camera' or use capture_source='game_view'.");
+ }
+ return CaptureSceneViewScreenshot(cmd, fileName, resolvedSuperSize, includeImage, maxResolution);
+ }
+
// Batch capture (e.g., "surround" for 6 angles around the scene)
if (!string.IsNullOrEmpty(cmd.batch))
{
@@ -443,18 +496,12 @@ private static object CaptureScreenshot(SceneCommand cmd)
return new ErrorResponse($"Unknown batch mode: '{cmd.batch}'. Valid modes: 'surround', 'orbit'.");
}
- // Positioned view-based capture (creates temp camera at view_position, aimed at look_at)
- if ((cmd.lookAt != null && cmd.lookAt.Type != JTokenType.Null) || cmd.viewPosition.HasValue)
+ // Positioned view-based capture (creates temp camera at view_position, aimed at view_target)
+ if ((cmd.viewTarget != null && cmd.viewTarget.Type != JTokenType.Null) || cmd.viewPosition.HasValue)
{
return CapturePositionedScreenshot(cmd);
}
- string fileName = cmd.fileName;
- int resolvedSuperSize = (cmd.superSize.HasValue && cmd.superSize.Value > 0) ? cmd.superSize.Value : 1;
- bool includeImage = cmd.includeImage ?? false;
- int maxResolution = cmd.maxResolution ?? 0; // 0 = let ScreenshotUtility default to 640
- string cameraRef = cmd.camera;
-
// Batch mode warning
if (Application.isBatchMode)
{
@@ -510,6 +557,7 @@ private static object CaptureScreenshot(SceneCommand cmd)
{ "superSize", result.SuperSize },
{ "isAsync", false },
{ "camera", targetCamera.name },
+ { "captureSource", "game_view" },
};
if (includeImage && result.ImageBase64 != null)
{
@@ -568,6 +616,7 @@ private static object CaptureScreenshot(SceneCommand cmd)
fullPath = defaultResult.FullPath,
superSize = defaultResult.SuperSize,
isAsync = defaultResult.IsAsync,
+ captureSource = "game_view",
}
);
}
@@ -577,8 +626,87 @@ private static object CaptureScreenshot(SceneCommand cmd)
}
}
+ private static object CaptureSceneViewScreenshot(
+ SceneCommand cmd,
+ string fileName,
+ int resolvedSuperSize,
+ bool includeImage,
+ int maxResolution)
+ {
+ if (Application.isBatchMode)
+ {
+ return new ErrorResponse("capture_source='scene_view' is not supported in batch mode.");
+ }
+
+ var sceneView = SceneView.lastActiveSceneView;
+ if (sceneView == null)
+ {
+ return new ErrorResponse(
+ "No active Scene View found. Open a Scene View window first, then retry screenshot with capture_source='scene_view'.");
+ }
+
+ if (cmd.viewTarget != null && cmd.viewTarget.Type != JTokenType.Null)
+ {
+ var frameResult = FrameSceneView(new SceneCommand { sceneViewTarget = cmd.viewTarget });
+ if (frameResult is ErrorResponse)
+ {
+ return frameResult;
+ }
+ }
+
+ 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 sceneViewName = sceneView.titleContent?.text ?? "Scene";
+
+ var data = new Dictionary
+ {
+ { "path", result.AssetsRelativePath },
+ { "fullPath", result.FullPath },
+ { "superSize", result.SuperSize },
+ { "isAsync", false },
+ { "camera", sceneView.camera != null ? sceneView.camera.name : "SceneCamera" },
+ { "captureSource", "scene_view" },
+ { "captureMode", "scene_view_viewport" },
+ { "sceneViewName", sceneViewName },
+ { "viewportWidth", viewportWidth },
+ { "viewportHeight", viewportHeight },
+ };
+
+ if (cmd.viewTarget != null && cmd.viewTarget.Type != JTokenType.Null)
+ {
+ data["viewTarget"] = cmd.viewTarget;
+ }
+
+ if (includeImage && result.ImageBase64 != null)
+ {
+ data["imageBase64"] = result.ImageBase64;
+ data["imageWidth"] = result.ImageWidth;
+ data["imageHeight"] = result.ImageHeight;
+ }
+
+ return new SuccessResponse(
+ $"Scene View screenshot captured to '{result.AssetsRelativePath}' (scene view: {sceneViewName}).",
+ data);
+ }
+ catch (Exception e)
+ {
+ return new ErrorResponse($"Error capturing Scene View screenshot: {e.Message}");
+ }
+ }
+
///
- /// Captures screenshots from 6 angles around scene bounds (or a look_at target) for AI scene understanding.
+ /// Captures screenshots from 6 angles around scene bounds (or a view_target) for AI scene understanding.
/// Does NOT save to disk — returns all images as inline base64 PNGs. Always uses camera-based capture.
///
private static object CaptureSurroundBatch(SceneCommand cmd)
@@ -590,24 +718,24 @@ private static object CaptureSurroundBatch(SceneCommand cmd)
Vector3 center;
float radius;
- // If look_at is provided, center on that target instead of scene bounds
- if (cmd.lookAt != null && cmd.lookAt.Type != JTokenType.Null)
+ // If view_target is provided, center on that target instead of scene bounds
+ if (cmd.viewTarget != null && cmd.viewTarget.Type != JTokenType.Null)
{
- var lookAtPos = VectorParsing.ParseVector3(cmd.lookAt);
- if (lookAtPos.HasValue)
+ var targetPos3 = VectorParsing.ParseVector3(cmd.viewTarget);
+ if (targetPos3.HasValue)
{
- center = lookAtPos.Value;
+ center = targetPos3.Value;
radius = 5f;
}
else
{
- Scene lookAtScene = EditorSceneManager.GetActiveScene();
- var lookAtGo = ResolveGameObject(cmd.lookAt, lookAtScene);
- if (lookAtGo == null)
- return new ErrorResponse($"look_at target '{cmd.lookAt}' not found for batch capture.");
+ Scene targetScene = EditorSceneManager.GetActiveScene();
+ var targetGo = ResolveGameObject(cmd.viewTarget, targetScene);
+ if (targetGo == null)
+ return new ErrorResponse($"view_target '{cmd.viewTarget}' not found for batch capture.");
- Bounds targetBounds = new Bounds(lookAtGo.transform.position, Vector3.zero);
- foreach (var r in lookAtGo.GetComponentsInChildren())
+ Bounds targetBounds = new Bounds(targetGo.transform.position, Vector3.zero);
+ foreach (var r in targetGo.GetComponentsInChildren())
{
if (r != null && r.gameObject.activeInHierarchy) targetBounds.Encapsulate(r.bounds);
}
@@ -736,24 +864,24 @@ private static object CaptureOrbitBatch(SceneCommand cmd)
Vector3 center;
float radius;
- // Resolve center and radius from look_at target or scene bounds
- if (cmd.lookAt != null && cmd.lookAt.Type != JTokenType.Null)
+ // Resolve center and radius from view_target or scene bounds
+ if (cmd.viewTarget != null && cmd.viewTarget.Type != JTokenType.Null)
{
- var lookAtPos = VectorParsing.ParseVector3(cmd.lookAt);
- if (lookAtPos.HasValue)
+ var targetPos3 = VectorParsing.ParseVector3(cmd.viewTarget);
+ if (targetPos3.HasValue)
{
- center = lookAtPos.Value;
+ center = targetPos3.Value;
radius = cmd.orbitDistance ?? 5f;
}
else
{
- Scene lookAtScene = EditorSceneManager.GetActiveScene();
- var lookAtGo = ResolveGameObject(cmd.lookAt, lookAtScene);
- if (lookAtGo == null)
- return new ErrorResponse($"look_at target '{cmd.lookAt}' not found for orbit capture.");
+ Scene targetScene = EditorSceneManager.GetActiveScene();
+ var targetGo = ResolveGameObject(cmd.viewTarget, targetScene);
+ if (targetGo == null)
+ return new ErrorResponse($"view_target '{cmd.viewTarget}' not found for orbit capture.");
- Bounds targetBounds = new Bounds(lookAtGo.transform.position, Vector3.zero);
- foreach (var r in lookAtGo.GetComponentsInChildren())
+ Bounds targetBounds = new Bounds(targetGo.transform.position, Vector3.zero);
+ foreach (var r in targetGo.GetComponentsInChildren())
{
if (r != null && r.gameObject.activeInHierarchy) targetBounds.Encapsulate(r.bounds);
}
@@ -874,7 +1002,7 @@ private static object CaptureOrbitBatch(SceneCommand cmd)
}
///
- /// Captures a single screenshot from a temporary camera placed at view_position and aimed at look_at.
+ /// 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/.
///
private static object CapturePositionedScreenshot(SceneCommand cmd)
@@ -885,9 +1013,9 @@ private static object CapturePositionedScreenshot(SceneCommand cmd)
// Resolve where to aim
Vector3? targetPos = null;
- if (cmd.lookAt != null && cmd.lookAt.Type != JTokenType.Null)
+ if (cmd.viewTarget != null && cmd.viewTarget.Type != JTokenType.Null)
{
- var parsedPos = VectorParsing.ParseVector3(cmd.lookAt);
+ var parsedPos = VectorParsing.ParseVector3(cmd.viewTarget);
if (parsedPos.HasValue)
{
targetPos = parsedPos.Value;
@@ -895,10 +1023,10 @@ private static object CapturePositionedScreenshot(SceneCommand cmd)
else
{
Scene activeScene = EditorSceneManager.GetActiveScene();
- var lookAtGo = ResolveGameObject(cmd.lookAt, activeScene);
- if (lookAtGo == null)
- return new ErrorResponse($"look_at target '{cmd.lookAt}' not found.");
- targetPos = lookAtGo.transform.position;
+ var resolvedGo = ResolveGameObject(cmd.viewTarget, activeScene);
+ if (resolvedGo == null)
+ return new ErrorResponse($"view_target '{cmd.viewTarget}' not found.");
+ targetPos = resolvedGo.transform.position;
}
}
@@ -910,12 +1038,12 @@ private static object CapturePositionedScreenshot(SceneCommand cmd)
}
else if (targetPos.HasValue)
{
- // Default: offset from look_at target
+ // Default: offset from view_target
camPos = targetPos.Value + new Vector3(0, 2, -5);
}
else
{
- return new ErrorResponse("Provide 'look_at' or 'view_position' for a positioned screenshot.");
+ return new ErrorResponse("Provide 'view_target' or 'view_position' for a positioned screenshot.");
}
// Create temporary camera
@@ -971,7 +1099,7 @@ private static object CapturePositionedScreenshot(SceneCommand cmd)
{ "path", assetsRelativePath },
};
if (targetPos.HasValue)
- data["lookAt"] = new[] { targetPos.Value.x, targetPos.Value.y, targetPos.Value.z };
+ 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}'.",
@@ -1065,30 +1193,7 @@ private static object FrameSceneView(SceneCommand cmd)
return new ErrorResponse($"Target GameObject '{cmd.sceneViewTarget}' not found for scene_view_frame.");
}
- // Calculate bounds from renderers, colliders, or transform
- Bounds bounds = new Bounds(target.transform.position, Vector3.zero);
- var renderers = target.GetComponentsInChildren();
- if (renderers.Length > 0)
- {
- bounds = renderers[0].bounds;
- for (int i = 1; i < renderers.Length; i++)
- bounds.Encapsulate(renderers[i].bounds);
- }
- else
- {
- var colliders = target.GetComponentsInChildren();
- if (colliders.Length > 0)
- {
- bounds = colliders[0].bounds;
- for (int i = 1; i < colliders.Length; i++)
- bounds.Encapsulate(colliders[i].bounds);
- }
- else
- {
- bounds = new Bounds(target.transform.position, Vector3.one);
- }
- }
-
+ Bounds bounds = CalculateFrameBounds(target);
sceneView.Frame(bounds, false);
return new SuccessResponse($"Scene View framed on '{target.name}'.", new { target = target.name });
}
@@ -1118,6 +1223,124 @@ private static object FrameSceneView(SceneCommand cmd)
}
}
+ private static Bounds CalculateFrameBounds(GameObject target)
+ {
+ if (target == null)
+ return new Bounds(Vector3.zero, Vector3.one);
+
+ if (TryGetRectTransformBounds(target, out Bounds rectBounds))
+ return rectBounds;
+
+ if (TryGetRendererBounds(target, out Bounds rendererBounds))
+ return rendererBounds;
+
+ if (TryGetColliderBounds(target, out Bounds colliderBounds))
+ return colliderBounds;
+
+ return new Bounds(target.transform.position, Vector3.one);
+ }
+
+ private static bool TryGetRectTransformBounds(GameObject target, out Bounds bounds)
+ {
+ bounds = default(Bounds);
+ var rectTransforms = target.GetComponentsInChildren(true);
+ bool hasBounds = false;
+ var corners = new Vector3[4];
+
+ foreach (var rectTransform in rectTransforms)
+ {
+ if (rectTransform == null || !rectTransform.gameObject.activeInHierarchy)
+ continue;
+
+ rectTransform.GetWorldCorners(corners);
+ for (int i = 0; i < corners.Length; i++)
+ {
+ if (!hasBounds)
+ {
+ bounds = new Bounds(corners[i], Vector3.zero);
+ hasBounds = true;
+ }
+ else
+ {
+ bounds.Encapsulate(corners[i]);
+ }
+ }
+ }
+
+ if (!hasBounds)
+ return false;
+
+ if (bounds.size.sqrMagnitude < 0.0001f)
+ bounds.Expand(1f);
+
+ return true;
+ }
+
+ private static bool TryGetRendererBounds(GameObject target, out Bounds bounds)
+ {
+ bounds = default(Bounds);
+ var renderers = target.GetComponentsInChildren(true);
+ bool hasBounds = false;
+ foreach (var renderer in renderers)
+ {
+ if (renderer == null || !renderer.gameObject.activeInHierarchy)
+ continue;
+
+ if (!hasBounds)
+ {
+ bounds = renderer.bounds;
+ hasBounds = true;
+ }
+ else
+ {
+ bounds.Encapsulate(renderer.bounds);
+ }
+ }
+
+ return hasBounds;
+ }
+
+ private static bool TryGetColliderBounds(GameObject target, out Bounds bounds)
+ {
+ bounds = default(Bounds);
+ var colliders = target.GetComponentsInChildren(true);
+ bool hasBounds = false;
+ foreach (var collider in colliders)
+ {
+ if (collider == null || !collider.gameObject.activeInHierarchy)
+ continue;
+
+ if (!hasBounds)
+ {
+ bounds = collider.bounds;
+ hasBounds = true;
+ }
+ else
+ {
+ bounds.Encapsulate(collider.bounds);
+ }
+ }
+
+ var colliders2D = target.GetComponentsInChildren(true);
+ foreach (var collider in colliders2D)
+ {
+ if (collider == null || !collider.gameObject.activeInHierarchy)
+ continue;
+
+ if (!hasBounds)
+ {
+ bounds = collider.bounds;
+ hasBounds = true;
+ }
+ else
+ {
+ bounds.Encapsulate(collider.bounds);
+ }
+ }
+
+ return hasBounds;
+ }
+
private static void EnsureGameView()
{
try
diff --git a/MCPForUnity/Editor/Windows/Components/Common.uss b/MCPForUnity/Editor/Windows/Components/Common.uss
index 8c63aa0d7..24a69d01c 100644
--- a/MCPForUnity/Editor/Windows/Components/Common.uss
+++ b/MCPForUnity/Editor/Windows/Components/Common.uss
@@ -335,10 +335,14 @@
.tool-action-button {
flex-grow: 1;
+ flex-shrink: 1;
flex-basis: 0;
min-width: 0;
height: 26px;
margin-bottom: 4px;
+ overflow: hidden;
+ -unity-text-overflow-position: end;
+ text-overflow: ellipsis;
}
.tool-category-container {
diff --git a/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs b/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs
index 1f9651447..07dfd3ea7 100644
--- a/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs
+++ b/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs
@@ -590,28 +590,44 @@ private VisualElement CreateManageSceneActions()
var actions = new VisualElement();
actions.AddToClassList("tool-item-actions");
- var screenshotButton = new Button(OnManageSceneScreenshotClicked)
+ var gameViewButton = new Button(OnManageSceneScreenshotClicked)
{
- text = "Capture Screenshot"
+ text = "Game View"
};
- screenshotButton.AddToClassList("tool-action-button");
- screenshotButton.style.marginTop = 4;
- screenshotButton.tooltip = "Capture a screenshot to Assets/Screenshots via manage_camera.";
+ gameViewButton.AddToClassList("tool-action-button");
+ gameViewButton.style.marginTop = 4;
+ gameViewButton.tooltip = "Capture a game camera screenshot to Assets/Screenshots.";
+
+ var sceneViewButton = new Button(OnSceneViewScreenshotClicked)
+ {
+ text = "Scene View"
+ };
+ sceneViewButton.AddToClassList("tool-action-button");
+ sceneViewButton.style.marginTop = 4;
+ sceneViewButton.style.marginLeft = 4;
+ sceneViewButton.tooltip = "Capture the active Scene View viewport to Assets/Screenshots.";
var multiviewButton = new Button(OnManageSceneMultiviewClicked)
{
- text = "Capture Multiview"
+ text = "Multiview"
};
multiviewButton.AddToClassList("tool-action-button");
multiviewButton.style.marginTop = 4;
multiviewButton.style.marginLeft = 4;
multiviewButton.tooltip = "Capture a 6-angle contact sheet around the scene centre and save to Assets/Screenshots.";
+ var captureLabel = new Label("Capture:");
+ captureLabel.style.marginTop = 6;
+ captureLabel.style.unityFontStyleAndWeight = UnityEngine.FontStyle.Normal;
+
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
- row.Add(screenshotButton);
+ row.style.flexWrap = Wrap.Wrap;
+ row.Add(gameViewButton);
+ row.Add(sceneViewButton);
row.Add(multiviewButton);
+ actions.Add(captureLabel);
actions.Add(row);
return actions;
}
@@ -686,6 +702,30 @@ private void OnManageSceneScreenshotClicked()
}
}
+ private void OnSceneViewScreenshotClicked()
+ {
+ try
+ {
+ var response = ManageScene.ExecuteSceneViewScreenshot();
+ 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("Scene View screenshot capture requested.");
+ }
+ }
+ catch (Exception ex)
+ {
+ McpLog.Error($"Failed to capture Scene View screenshot: {ex.Message}");
+ }
+ }
+
private void OnManageSceneMultiviewClicked()
{
try
diff --git a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
index ef7624b8c..bf41353a6 100644
--- a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
+++ b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
@@ -382,7 +382,8 @@ private void CheckForPackageUpdates()
var result = MCPServiceLocator.Updates.CheckForUpdate(currentVersion);
if (result.CheckSucceeded && result.UpdateAvailable && !string.IsNullOrEmpty(result.LatestVersion))
{
- updateNotificationText.text = $"Newer version available: v{result.LatestVersion} (current v{currentVersion})";
+ updateNotificationText.text = $"Update available: v{result.LatestVersion} (current: v{currentVersion})";
+ updateNotificationText.tooltip = $"Latest version: v{result.LatestVersion}\nCurrent version: v{currentVersion}";
updateNotification.AddToClassList("visible");
}
else
diff --git a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.uss b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.uss
index a374a6331..212d943aa 100644
--- a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.uss
+++ b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.uss
@@ -40,12 +40,17 @@
/* Update Notification */
.update-notification {
display: none;
- padding: 8px 16px;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ padding: 6px 12px;
margin: 0px 12px;
background-color: rgba(100, 200, 100, 0.15);
border-radius: 4px;
border-width: 1px;
border-color: rgba(100, 200, 100, 0.3);
+ flex-shrink: 0;
}
.update-notification.visible {
@@ -56,6 +61,9 @@
font-size: 11px;
color: rgba(100, 200, 100, 1);
white-space: normal;
+ flex-shrink: 1;
+ overflow: hidden;
+ -unity-text-align: middle-center;
}
/* Tabs */
diff --git a/Server/src/cli/CLI_USAGE_GUIDE.md b/Server/src/cli/CLI_USAGE_GUIDE.md
index 75d1cb8f6..4c6413eae 100644
--- a/Server/src/cli/CLI_USAGE_GUIDE.md
+++ b/Server/src/cli/CLI_USAGE_GUIDE.md
@@ -658,8 +658,9 @@ unity-mcp camera screenshot --file-name "my_capture" --super-size 2
unity-mcp camera screenshot --camera-ref "SecondCamera" --include-image
unity-mcp camera screenshot --max-resolution 256
unity-mcp camera screenshot --batch surround --max-resolution 256
-unity-mcp camera screenshot --batch orbit --look-at "Player"
-unity-mcp camera screenshot-multiview --look-at "Player" --max-resolution 480
+unity-mcp camera screenshot --batch orbit --view-target "Player"
+unity-mcp camera screenshot --capture-source scene_view --view-target "Canvas" --include-image
+unity-mcp camera screenshot-multiview --view-target "Player" --max-resolution 480
```
### Graphics Commands
diff --git a/Server/src/cli/commands/camera.py b/Server/src/cli/commands/camera.py
index 2e1cad659..b97a07700 100644
--- a/Server/src/cli/commands/camera.py
+++ b/Server/src/cli/commands/camera.py
@@ -481,18 +481,23 @@ def release_override():
@click.option("--super-size", type=int, default=None, help="Supersize multiplier.")
@click.option("--include-image/--no-include-image", default=None, help="Return inline base64 PNG.")
@click.option("--max-resolution", type=int, default=None, help="Max resolution for inline image.")
+@click.option("--capture-source", default=None,
+ type=click.Choice(["game_view", "scene_view"], case_sensitive=False),
+ help="Capture source: game_view (default) or scene_view.")
@click.option("--batch", default=None, type=click.Choice(["surround", "orbit"]),
help="Batch capture mode.")
-@click.option("--look-at", default=None, help="Target to aim at (name/path/ID or [x,y,z]).")
+@click.option("--view-target", default=None,
+ help="Target to focus on (name/path/ID or [x,y,z]). Aims camera (game_view) or frames Scene View (scene_view).")
@handle_unity_errors
-def screenshot(camera_ref, file_name, super_size, include_image, max_resolution, batch, look_at):
+def screenshot(camera_ref, file_name, super_size, include_image, max_resolution, capture_source, batch, view_target):
"""Capture a screenshot from a camera.
\b
Examples:
unity-mcp camera screenshot
unity-mcp camera screenshot --camera-ref "CM FollowCam" --include-image --max-resolution 512
- unity-mcp camera screenshot --batch surround --look-at Player
+ unity-mcp camera screenshot --capture-source scene_view --view-target Canvas --include-image
+ unity-mcp camera screenshot --batch surround --view-target Player
"""
config = get_config()
params: dict[str, Any] = {"action": "screenshot"}
@@ -506,31 +511,33 @@ def screenshot(camera_ref, file_name, super_size, include_image, max_resolution,
params["includeImage"] = include_image
if max_resolution is not None:
params["maxResolution"] = max_resolution
+ if capture_source:
+ params["captureSource"] = capture_source
if batch:
params["batch"] = batch
- if look_at:
- params["lookAt"] = look_at
+ if view_target:
+ params["viewTarget"] = view_target
result = run_command(config, "manage_camera", params)
format_output(result, config)
@camera.command("screenshot-multiview")
@click.option("--max-resolution", type=int, default=None, help="Max resolution per tile.")
-@click.option("--look-at", default=None, help="Center target for the multiview capture.")
+@click.option("--view-target", default=None, help="Center target for the multiview capture.")
@handle_unity_errors
-def screenshot_multiview(max_resolution, look_at):
+def screenshot_multiview(max_resolution, view_target):
"""Capture a 6-angle contact sheet around the scene.
\b
Examples:
unity-mcp camera screenshot-multiview
- unity-mcp camera screenshot-multiview --look-at Player --max-resolution 480
+ unity-mcp camera screenshot-multiview --view-target Player --max-resolution 480
"""
config = get_config()
params: dict[str, Any] = {"action": "screenshot_multiview"}
if max_resolution is not None:
params["maxResolution"] = max_resolution
- if look_at:
- params["lookAt"] = look_at
+ if view_target:
+ params["viewTarget"] = view_target
result = run_command(config, "manage_camera", params)
format_output(result, config)
diff --git a/Server/src/services/tools/manage_camera.py b/Server/src/services/tools/manage_camera.py
index 035fd2a86..41004f05e 100644
--- a/Server/src/services/tools/manage_camera.py
+++ b/Server/src/services/tools/manage_camera.py
@@ -63,7 +63,8 @@
"CAPTURE:\n"
"- screenshot: Capture from a camera. Supports include_image=true for inline base64 PNG, "
"batch='surround' for 6-angle contact sheet, batch='orbit' for configurable grid, "
- "look_at/view_position for positioned capture.\n"
+ "view_target/view_position for positioned capture, and capture_source='scene_view' to capture "
+ "the active Unity Scene View viewport.\n"
"- screenshot_multiview: Shorthand for screenshot with batch='surround' and include_image=true."
),
annotations=ToolAnnotations(
@@ -94,14 +95,18 @@ async def manage_camera(
"If true, return screenshot as inline base64 PNG. Default false."] = None,
max_resolution: Annotated[int | str | None,
"Max resolution (longest edge px) for inline image. Default 640."] = None,
+ capture_source: Annotated[Literal["game_view", "scene_view"] | None,
+ "Screenshot source. 'game_view' (default) captures the game/camera path; "
+ "'scene_view' captures the active Unity Scene View viewport."] = None,
batch: Annotated[str | None,
"Batch capture mode: 'surround' (6 angles) or 'orbit' (configurable grid)."] = None,
- look_at: Annotated[str | int | list[float] | None,
- "Target to aim camera at. GameObject name/path/ID or [x,y,z]."] = None,
+ view_target: Annotated[str | int | list[float] | None,
+ "Target to focus on. GameObject name/path/ID or [x,y,z]. "
+ "For game_view: aims camera at target. For scene_view: frames the Scene View on the target."] = None,
view_position: Annotated[list[float] | str | None,
"World position [x,y,z] to place camera for positioned capture."] = None,
view_rotation: Annotated[list[float] | str | None,
- "Euler rotation [x,y,z] for camera. Overrides look_at if both provided."] = None,
+ "Euler rotation [x,y,z] for camera. Overrides view_target if both provided."] = None,
orbit_angles: Annotated[int | str | None,
"Number of azimuth samples for batch='orbit' (default 8, max 36)."] = None,
orbit_elevations: Annotated[list[float] | str | None,
@@ -154,8 +159,9 @@ async def manage_camera(
camera=camera,
include_image=include_image,
max_resolution=max_resolution,
+ capture_source=capture_source,
batch=batch,
- look_at=look_at,
+ view_target=view_target,
orbit_angles=orbit_angles,
orbit_elevations=orbit_elevations,
orbit_distance=orbit_distance,
diff --git a/Server/src/services/tools/utils.py b/Server/src/services/tools/utils.py
index aea7323b4..86e483519 100644
--- a/Server/src/services/tools/utils.py
+++ b/Server/src/services/tools/utils.py
@@ -464,8 +464,9 @@ def build_screenshot_params(
camera: str | None = None,
include_image: bool | str | None = None,
max_resolution: int | str | None = None,
+ capture_source: str | None = None,
batch: str | None = None,
- look_at: str | int | list[float] | None = None,
+ view_target: str | int | list[float] | None = None,
orbit_angles: int | str | None = None,
orbit_elevations: list[float] | str | None = None,
orbit_distance: float | str | None = None,
@@ -493,10 +494,18 @@ def build_screenshot_params(
if coerced_max_resolution <= 0:
return {"success": False, "message": "max_resolution must be a positive integer."}
params["maxResolution"] = coerced_max_resolution
+ if capture_source is not None:
+ normalized_capture_source = str(capture_source).strip().lower()
+ if normalized_capture_source not in {"game_view", "scene_view"}:
+ return {
+ "success": False,
+ "message": "capture_source must be either 'game_view' or 'scene_view'.",
+ }
+ params["captureSource"] = normalized_capture_source
if batch:
params["batch"] = batch
- if look_at is not None:
- params["lookAt"] = look_at
+ if view_target is not None:
+ params["viewTarget"] = view_target
# Orbit params
coerced_orbit_angles = coerce_int(orbit_angles, default=None)
@@ -533,5 +542,26 @@ def build_screenshot_params(
if err:
return {"success": False, "message": err}
params["viewRotation"] = vec
+ if params.get("captureSource") == "scene_view":
+ if coerced_super_size is not None and coerced_super_size > 1:
+ return {
+ "success": False,
+ "message": "capture_source='scene_view' does not support super_size above 1.",
+ }
+ if batch:
+ return {
+ "success": False,
+ "message": "capture_source='scene_view' does not support batch modes.",
+ }
+ if view_position is not None or view_rotation is not None:
+ return {
+ "success": False,
+ "message": "capture_source='scene_view' does not support view_position/view_rotation.",
+ }
+ if camera:
+ return {
+ "success": False,
+ "message": "capture_source='scene_view' does not support camera selection.",
+ }
return None
diff --git a/Server/tests/test_cli.py b/Server/tests/test_cli.py
index baaf8e4d4..976b03cad 100644
--- a/Server/tests/test_cli.py
+++ b/Server/tests/test_cli.py
@@ -457,6 +457,25 @@ def test_scene_create(self, runner, mock_unity_response):
assert result.exit_code == 0
+class TestCameraCommands:
+ """Tests for Camera CLI commands."""
+
+ def test_camera_screenshot_scene_view(self, runner, mock_unity_response):
+ with patch("cli.commands.camera.run_command", return_value=mock_unity_response) as mock_run:
+ result = runner.invoke(cli, [
+ "camera", "screenshot",
+ "--capture-source", "scene_view",
+ "--view-target", "Canvas",
+ "--include-image",
+ ])
+ assert result.exit_code == 0
+ mock_run.assert_called_once()
+ params = mock_run.call_args[0][2]
+ assert params["captureSource"] == "scene_view"
+ assert params["viewTarget"] == "Canvas"
+ assert params["includeImage"] is True
+
+
# =============================================================================
# Asset Command Tests
diff --git a/Server/tests/test_manage_camera.py b/Server/tests/test_manage_camera.py
index 80af04a46..4919a520c 100644
--- a/Server/tests/test_manage_camera.py
+++ b/Server/tests/test_manage_camera.py
@@ -354,13 +354,13 @@ def test_screenshot_batch_surround(mock_unity):
SimpleNamespace(),
action="screenshot",
batch="surround",
- look_at="Player",
+ view_target="Player",
include_image=True,
)
)
assert result["success"] is True
assert mock_unity["params"]["batch"] == "surround"
- assert mock_unity["params"]["lookAt"] == "Player"
+ assert mock_unity["params"]["viewTarget"] == "Player"
def test_screenshot_batch_orbit(mock_unity):
@@ -389,13 +389,69 @@ def test_screenshot_positioned(mock_unity):
SimpleNamespace(),
action="screenshot",
view_position=[5.0, 3.0, -10.0],
- look_at="Player",
+ view_target="Player",
include_image=True,
)
)
assert result["success"] is True
assert mock_unity["params"]["viewPosition"] == [5.0, 3.0, -10.0]
- assert mock_unity["params"]["lookAt"] == "Player"
+ assert mock_unity["params"]["viewTarget"] == "Player"
+
+
+def test_screenshot_scene_view_capture_params(mock_unity):
+ result = asyncio.run(
+ manage_camera(
+ SimpleNamespace(),
+ action="screenshot",
+ capture_source="scene_view",
+ view_target="Canvas",
+ include_image=True,
+ )
+ )
+ assert result["success"] is True
+ assert mock_unity["params"]["captureSource"] == "scene_view"
+ assert mock_unity["params"]["viewTarget"] == "Canvas"
+ assert mock_unity["params"]["includeImage"] is True
+
+
+def test_screenshot_invalid_capture_source(mock_unity):
+ result = asyncio.run(
+ manage_camera(
+ SimpleNamespace(),
+ action="screenshot",
+ capture_source="editor_view",
+ )
+ )
+ assert result["success"] is False
+ assert "capture_source must be either" in result["message"]
+ assert "params" not in mock_unity
+
+
+def test_screenshot_scene_view_rejects_batch_in_python(mock_unity):
+ result = asyncio.run(
+ manage_camera(
+ SimpleNamespace(),
+ action="screenshot",
+ capture_source="scene_view",
+ batch="surround",
+ )
+ )
+ assert result["success"] is False
+ assert "does not support batch modes" in result["message"]
+ assert "params" not in mock_unity
+
+
+def test_screenshot_view_target_works_without_capture_source(mock_unity):
+ """view_target should work for both game_view and scene_view (no rejection)."""
+ result = asyncio.run(
+ manage_camera(
+ SimpleNamespace(),
+ action="screenshot",
+ view_target="Player",
+ )
+ )
+ assert result["success"] is True
+ assert mock_unity["params"]["viewTarget"] == "Player"
def test_screenshot_multiview_sends_action(mock_unity):
diff --git a/Server/uv.lock b/Server/uv.lock
index 8afc08289..210dfaa1f 100644
--- a/Server/uv.lock
+++ b/Server/uv.lock
@@ -858,7 +858,7 @@ wheels = [
[[package]]
name = "mcpforunityserver"
-version = "9.5.2"
+version = "9.5.3"
source = { editable = "." }
dependencies = [
{ name = "click" },
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageSceneHierarchyPagingTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageSceneHierarchyPagingTests.cs
index c0066a89f..412c52a7c 100644
--- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageSceneHierarchyPagingTests.cs
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageSceneHierarchyPagingTests.cs
@@ -1,4 +1,5 @@
using NUnit.Framework;
+using System.Reflection;
using UnityEngine;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Tools;
@@ -101,5 +102,104 @@ public void GetHierarchy_PaginatesRoots_AndSupportsChildrenPaging()
Assert.IsNotNull(childItems);
Assert.AreEqual(7, childItems.Count);
}
+
+ [Test]
+ public void Screenshot_SceneViewRejectsSupersizeAboveOne()
+ {
+ var raw = ManageScene.HandleCommand(new JObject
+ {
+ ["action"] = "screenshot",
+ ["captureSource"] = "scene_view",
+ ["superSize"] = 2,
+ });
+ var response = raw as JObject ?? JObject.FromObject(raw);
+
+ Assert.IsFalse(response.Value("success"), response.ToString());
+ StringAssert.Contains("does not support super_size above 1", response.Value("message"));
+ }
+
+ [Test]
+ public void EditorWindowScreenshotUtility_SanitizesFileName()
+ {
+ var helperType = typeof(ManageScene).Assembly.GetType("MCPForUnity.Editor.Helpers.EditorWindowScreenshotUtility");
+ Assert.IsNotNull(helperType, "Expected EditorWindowScreenshotUtility type.");
+
+ var sanitizeMethod = helperType.GetMethod("SanitizeFileName", BindingFlags.NonPublic | BindingFlags.Static);
+ Assert.IsNotNull(sanitizeMethod, "Expected SanitizeFileName helper.");
+
+ string sanitized = (string)sanitizeMethod.Invoke(null, new object[] { "../evil/path/shot" });
+ Assert.AreEqual("shot", sanitized);
+ Assert.IsFalse(sanitized.Contains("/"));
+ Assert.IsFalse(sanitized.Contains("\\"));
+ Assert.IsFalse(sanitized.Contains(".."));
+
+ string[] reservedInputs = { "CON", "NUL", "PRN", "AUX", "../CON.txt", "folder/COM1.log", "nested\\LPT9", "CON ", "NUL." };
+ foreach (string input in reservedInputs)
+ {
+ sanitized = (string)sanitizeMethod.Invoke(null, new object[] { input });
+ string sanitizedStem = System.IO.Path.GetFileNameWithoutExtension(sanitized);
+ Assert.IsFalse(
+ string.Equals(sanitizedStem, "CON", System.StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(sanitizedStem, "NUL", System.StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(sanitizedStem, "PRN", System.StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(sanitizedStem, "AUX", System.StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(sanitizedStem, "COM1", System.StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(sanitizedStem, "LPT9", System.StringComparison.OrdinalIgnoreCase),
+ $"Expected reserved device name to be sanitized for input '{input}', got '{sanitized}'.");
+ Assert.IsFalse(sanitized.Contains("/"));
+ Assert.IsFalse(sanitized.Contains("\\"));
+ Assert.IsFalse(sanitized.Contains(".."));
+ }
+ }
+
+ [Test]
+ public void EditorWindowScreenshotUtility_ClampsSceneViewSupersizeToOne()
+ {
+ var helperType = typeof(ManageScene).Assembly.GetType("MCPForUnity.Editor.Helpers.EditorWindowScreenshotUtility");
+ Assert.IsNotNull(helperType, "Expected EditorWindowScreenshotUtility type.");
+
+ var normalizeMethod = helperType.GetMethod("NormalizeSceneViewSuperSize", BindingFlags.NonPublic | BindingFlags.Static);
+ Assert.IsNotNull(normalizeMethod, "Expected NormalizeSceneViewSuperSize helper.");
+
+ int normalized = (int)normalizeMethod.Invoke(null, new object[] { 4 });
+ Assert.AreEqual(1, normalized);
+
+ normalized = (int)normalizeMethod.Invoke(null, new object[] { 0 });
+ Assert.AreEqual(1, normalized);
+ }
+
+ [Test]
+ public void Screenshot_ViewTargetAcceptedForGameView()
+ {
+ // view_target should be accepted for game_view (positioned capture path).
+ // It will fail to resolve a non-existent GO, but should NOT reject the parameter itself.
+ var raw = ManageScene.HandleCommand(new JObject
+ {
+ ["action"] = "screenshot",
+ ["viewTarget"] = "NonExistentObject",
+ });
+ var response = raw as JObject ?? JObject.FromObject(raw);
+
+ // Should attempt positioned capture and fail to resolve the GO — not reject the param
+ Assert.IsFalse(response.Value("success"), response.ToString());
+ StringAssert.Contains("not found", response.Value("message"));
+ }
+
+ [Test]
+ public void CalculateFrameBounds_UsesCollider2D()
+ {
+ var helperType = typeof(ManageScene).GetMethod("CalculateFrameBounds", BindingFlags.NonPublic | BindingFlags.Static);
+ Assert.IsNotNull(helperType, "Expected CalculateFrameBounds helper.");
+
+ var root = new GameObject("HS_2D");
+ _created.Add(root);
+ var collider = root.AddComponent();
+ collider.size = new Vector2(4f, 2f);
+ collider.offset = new Vector2(1f, -1f);
+
+ Bounds bounds = (Bounds)helperType.Invoke(null, new object[] { root });
+ Assert.Greater(bounds.size.x, 0.1f);
+ Assert.Greater(bounds.size.y, 0.1f);
+ }
}
}
diff --git a/docs/guides/CLI_EXAMPLE.md b/docs/guides/CLI_EXAMPLE.md
index 6de104ed7..dbddaee81 100644
--- a/docs/guides/CLI_EXAMPLE.md
+++ b/docs/guides/CLI_EXAMPLE.md
@@ -77,8 +77,9 @@ unity-mcp --format json scene hierarchy
unity-mcp camera screenshot --file-name "capture"
unity-mcp camera screenshot --camera-ref "MainCam" --include-image --max-resolution 512
unity-mcp camera screenshot --batch surround --max-resolution 256
-unity-mcp camera screenshot --batch orbit --look-at "Player"
-unity-mcp camera screenshot-multiview --look-at "Player" --max-resolution 480
+unity-mcp camera screenshot --batch orbit --view-target "Player"
+unity-mcp camera screenshot --capture-source scene_view --view-target "Canvas" --include-image
+unity-mcp camera screenshot-multiview --view-target "Player" --max-resolution 480
```
**GameObject Operations**
diff --git a/docs/guides/CLI_USAGE.md b/docs/guides/CLI_USAGE.md
index 1c9601363..0c610503f 100644
--- a/docs/guides/CLI_USAGE.md
+++ b/docs/guides/CLI_USAGE.md
@@ -73,8 +73,9 @@ unity-mcp camera screenshot
unity-mcp camera screenshot --file-name "level_preview"
unity-mcp camera screenshot --camera-ref "SecondCamera" --include-image
unity-mcp camera screenshot --batch surround --max-resolution 256
-unity-mcp camera screenshot --batch orbit --look-at "Player"
-unity-mcp camera screenshot-multiview --look-at "Player" --max-resolution 480
+unity-mcp camera screenshot --batch orbit --view-target "Player"
+unity-mcp camera screenshot --capture-source scene_view --view-target "Canvas" --include-image
+unity-mcp camera screenshot-multiview --view-target "Player" --max-resolution 480
```
### GameObject Operations
@@ -189,9 +190,10 @@ unity-mcp custom_tool list
| `--include-image` | flag | Return base64 PNG inline in the response |
| `--max-resolution, -r` | int | Max longest-edge pixels (default 640) |
| `--batch, -b` | string | `surround` (6 angles) or `orbit` (configurable grid) |
-| `--look-at` | string | Target: GameObject name/path/ID, or `x,y,z` world position |
-| `--view-position` | string | Camera position as `x,y,z` (positioned screenshot) |
-| `--view-rotation` | string | Camera euler rotation as `x,y,z` (positioned screenshot) |
+| `--capture-source` | string | `game_view` (default) or `scene_view` (editor viewport) |
+| `--view-target` | string | Target to focus on: GO name/path/ID, or `x,y,z`. Aims camera (game_view) or frames viewport (scene_view) |
+| `--view-position` | string | Camera position as `x,y,z` (positioned screenshot, game_view only) |
+| `--view-rotation` | string | Camera euler rotation as `x,y,z` (positioned screenshot, game_view only) |
| `--orbit-angles` | int | Number of azimuth steps around target (default 8) |
| `--orbit-elevations` | string | Vertical angles as JSON array, e.g. `[0,30,-15]` (default `[0, 30, -15]`) |
| `--orbit-distance` | float | Camera distance from target in world units (auto-fit if omitted) |
@@ -380,8 +382,9 @@ unity-mcp camera brain-status
unity-mcp camera force "Cam" # Force Brain to use camera
unity-mcp camera release # Release override
unity-mcp camera screenshot --file-name "capture" --super-size 2
-unity-mcp camera screenshot --batch orbit --look-at "Player" --max-resolution 256
-unity-mcp camera screenshot-multiview --look-at "Player" --max-resolution 480
+unity-mcp camera screenshot --batch orbit --view-target "Player" --max-resolution 256
+unity-mcp camera screenshot --capture-source scene_view --view-target "Canvas" --include-image
+unity-mcp camera screenshot-multiview --view-target "Player" --max-resolution 480
```
### Graphics Operations
diff --git a/unity-mcp-skill/SKILL.md b/unity-mcp-skill/SKILL.md
index 032aaaff3..12cf41c22 100644
--- a/unity-mcp-skill/SKILL.md
+++ b/unity-mcp-skill/SKILL.md
@@ -86,18 +86,24 @@ manage_camera(action="screenshot", camera="MainCamera", include_image=True, max_
manage_camera(action="screenshot", batch="surround", max_resolution=256)
# Batch surround centered on a specific object
-manage_camera(action="screenshot", batch="surround", look_at="Player", max_resolution=256)
+manage_camera(action="screenshot", batch="surround", view_target="Player", max_resolution=256)
# Positioned screenshot: place a temp camera and capture in one call
-manage_camera(action="screenshot", look_at="Player", view_position=[0, 10, -10], max_resolution=512)
+manage_camera(action="screenshot", view_target="Player", view_position=[0, 10, -10], max_resolution=512)
+
+# Scene View screenshot: capture what the developer sees in the editor
+manage_camera(action="screenshot", capture_source="scene_view", include_image=True)
+
+# Scene View framed on a specific object
+manage_camera(action="screenshot", capture_source="scene_view", view_target="Canvas", include_image=True)
```
**Best practices for AI scene understanding:**
- Use `include_image=True` when you need to *see* the scene, not just save a file.
- Use `batch="surround"` for a comprehensive overview (6 angles, one command).
-- Use `look_at`/`view_position` to capture from a specific viewpoint without needing a scene camera.
+- Use `view_target`/`view_position` to capture from a specific viewpoint without needing a scene camera.
+- Use `capture_source="scene_view"` to see the editor viewport (gizmos, wireframes, grid).
- Keep `max_resolution` at 256–512 to balance quality vs. token cost.
-- Combine with `look_at` on `manage_gameobject` to orient a game camera before capturing.
```python
# Agentic camera loop: point, shoot, analyze
@@ -105,9 +111,11 @@ manage_gameobject(action="look_at", target="MainCamera", look_at_target="Player"
manage_camera(action="screenshot", camera="MainCamera", include_image=True, max_resolution=512)
# → Analyze image, decide next action
-# Screenshot from a different camera
-manage_camera(action="screenshot", camera="FollowCam", include_image=True, max_resolution=512)
-manage_camera(action="screenshot_multiview", max_resolution=480) # 6-angle contact sheet
+# Multi-view screenshot (6-angle contact sheet)
+manage_camera(action="screenshot_multiview", max_resolution=480)
+
+# Scene View for editor-level inspection (shows gizmos, debug overlays, etc.)
+manage_camera(action="screenshot", capture_source="scene_view", view_target="Player", include_image=True)
```
### 4. Check Console After Major Changes
diff --git a/unity-mcp-skill/references/tools-reference.md b/unity-mcp-skill/references/tools-reference.md
index a052fc9f5..c7fd7f0a0 100644
--- a/unity-mcp-skill/references/tools-reference.md
+++ b/unity-mcp-skill/references/tools-reference.md
@@ -136,7 +136,7 @@ manage_scene(
manage_scene(
action="screenshot",
batch="surround",
- look_at="Player", # str|int|list[float] - center surround on this target
+ view_target="Player", # str|int|list[float] - center surround on this target
max_resolution=256
)
@@ -144,7 +144,7 @@ manage_scene(
manage_scene(
action="screenshot",
batch="orbit", # str - "orbit" for configurable angle grid
- look_at="Player", # str|int|list[float] - target to orbit around
+ view_target="Player", # str|int|list[float] - target to orbit around
orbit_angles=8, # int, default 8 - number of azimuth steps
orbit_elevations=[0, 30], # list[float], default [0, 30, -15] - vertical angles in degrees
orbit_distance=10, # float, optional - camera distance (auto-fit if omitted)
@@ -156,9 +156,9 @@ manage_scene(
# Positioned screenshot (temp camera at viewpoint, no file saved)
manage_scene(
action="screenshot",
- look_at="Enemy", # str|int|list[float] - target to aim at
+ view_target="Enemy", # str|int|list[float] - target to aim at
view_position=[0, 10, -10], # list[float], optional - camera position
- view_rotation=[45, 0, 0], # list[float], optional - euler angles (overrides look_at aim)
+ view_rotation=[45, 0, 0], # list[float], optional - euler angles (overrides view_target aim)
max_resolution=512
)
@@ -836,13 +836,14 @@ Unified camera management (Unity Camera + Cinemachine). Works without Cinemachin
| Parameter | Type | Description |
|-----------|------|-------------|
-| `camera` | string | Camera to capture from (defaults to Camera.main) |
+| `capture_source` | string | `"game_view"` (default) or `"scene_view"` (editor viewport) |
+| `view_target` | string\|int\|list | Target to focus on (GO name/path/ID or [x,y,z]). game_view: aims camera; scene_view: frames viewport |
+| `camera` | string | Camera to capture from (defaults to Camera.main). game_view only |
| `include_image` | bool | Return base64 PNG inline (default false) |
| `max_resolution` | int | Downscale cap in px (default 640) |
-| `batch` | string | `"surround"` (6 angles) or `"orbit"` (configurable grid) |
-| `look_at` | string\|int\|list | Target to aim at (GO name/path/ID or [x,y,z]) |
-| `view_position` | list[float] | World position [x,y,z] to place camera |
-| `view_rotation` | list[float] | Euler rotation [x,y,z] (overrides look_at) |
+| `batch` | string | `"surround"` (6 angles) or `"orbit"` (configurable grid). game_view only |
+| `view_position` | list[float] | World position [x,y,z] to place camera. game_view only |
+| `view_rotation` | list[float] | Euler rotation [x,y,z] (overrides view_target). game_view only |
**Actions by category:**
@@ -873,7 +874,7 @@ Unified camera management (Unity Camera + Cinemachine). Works without Cinemachin
- `release_override` — Release camera override
**Capture:**
-- `screenshot` — Capture from a camera. Supports inline base64, batch surround/orbit, positioned capture.
+- `screenshot` — Capture screenshot. Supports `capture_source="game_view"` (default, camera-based) or `"scene_view"` (editor viewport). game_view supports inline base64, batch surround/orbit, positioned capture. scene_view supports `view_target` for framing.
- `screenshot_multiview` — Shorthand for screenshot with batch='surround' and include_image=true.
**Examples:**
@@ -924,9 +925,15 @@ manage_camera(action="add_extension", target="FollowCam", properties={
"extensionType": "CinemachineDeoccluder"
})
-# Screenshot from a specific camera
+# Screenshot from a specific camera (game_view, default)
manage_camera(action="screenshot", camera="FollowCam", include_image=True, max_resolution=512)
+# Scene View screenshot (captures editor viewport — gizmos, wireframes, grid)
+manage_camera(action="screenshot", capture_source="scene_view", include_image=True)
+
+# Scene View screenshot framed on a specific object
+manage_camera(action="screenshot", capture_source="scene_view", view_target="Canvas", include_image=True)
+
# Multi-view screenshot (6-angle contact sheet)
manage_camera(action="screenshot_multiview", max_resolution=480)
diff --git a/unity-mcp-skill/references/workflows.md b/unity-mcp-skill/references/workflows.md
index 4de0e2cf9..acf7fee65 100644
--- a/unity-mcp-skill/references/workflows.md
+++ b/unity-mcp-skill/references/workflows.md
@@ -1560,6 +1560,26 @@ manage_camera(action="list_cameras")
manage_camera(action="screenshot_multiview", max_resolution=480)
```
+### Scene View Screenshot Workflow
+
+Use `capture_source="scene_view"` to capture the editor's Scene View viewport — useful for seeing gizmos, wireframes, grid, debug overlays, and objects without cameras.
+
+```python
+# 1. Capture the Scene View as-is
+manage_camera(action="screenshot", capture_source="scene_view", include_image=True)
+
+# 2. Frame on a specific object first, then capture
+manage_camera(action="screenshot", capture_source="scene_view",
+ view_target="Player", include_image=True, max_resolution=512)
+
+# 3. Frame on UI Canvas (RectTransform bounds are supported)
+manage_camera(action="screenshot", capture_source="scene_view",
+ view_target="Canvas", include_image=True)
+
+# Limitations: scene_view does not support batch, view_position, view_rotation, or camera selection.
+# Use capture_source="game_view" (default) for those features.
+```
+
---
## ProBuilder Workflows