diff --git a/MCPForUnity/Editor/Dependencies/UvInstaller.cs b/MCPForUnity/Editor/Dependencies/UvInstaller.cs
new file mode 100644
index 000000000..002ae69e0
--- /dev/null
+++ b/MCPForUnity/Editor/Dependencies/UvInstaller.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Runtime.InteropServices;
+using MCPForUnity.Editor.Helpers;
+
+namespace MCPForUnity.Editor.Dependencies
+{
+ ///
+ /// Runs the official uv installer for the current platform. UI-agnostic: build the command,
+ /// run it (off the main thread), and report the outcome. The caller owns confirmation and
+ /// re-checking dependencies afterwards.
+ /// Installer reference: https://docs.astral.sh/uv/getting-started/installation/
+ ///
+ public static class UvInstaller
+ {
+ public readonly struct UvInstallResult
+ {
+ public readonly bool Success;
+ public readonly string Output;
+
+ public UvInstallResult(bool success, string output)
+ {
+ Success = success;
+ Output = output ?? string.Empty;
+ }
+ }
+
+ /// One-click install is available on Windows, macOS and Linux.
+ public static bool IsSupported =>
+ RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ||
+ RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ||
+ RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+
+ ///
+ /// Build the installer invocation for the current platform. Pure and testable — the
+ /// returned command is exactly what executes and what the confirm
+ /// dialog shows the user.
+ ///
+ public static (string file, string arguments) BuildInstallCommand()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return ("powershell",
+ "-NoProfile -ExecutionPolicy ByPass -c \"irm https://astral.sh/uv/install.ps1 | iex\"");
+ }
+
+ // macOS and Linux share the POSIX shell installer.
+ return ("/bin/sh", "-c \"curl -LsSf https://astral.sh/uv/install.sh | sh\"");
+ }
+
+ /// Human-readable one-line description of the command that will run.
+ public static string DescribeCommand()
+ {
+ var (file, arguments) = BuildInstallCommand();
+ return $"{file} {arguments}";
+ }
+
+ ///
+ /// Run the installer and return the outcome. Blocks until the installer exits or the
+ /// timeout elapses, so call it off the main thread (e.g. via Task.Run).
+ ///
+ public static UvInstallResult Run(int timeoutMs = 180000)
+ {
+ try
+ {
+ var (file, arguments) = BuildInstallCommand();
+ bool ok = ExecPath.TryRun(file, arguments, null, out string stdout, out string stderr, timeoutMs);
+ return new UvInstallResult(ok, Combine(stdout, stderr));
+ }
+ catch (Exception ex)
+ {
+ return new UvInstallResult(false, ex.Message);
+ }
+ }
+
+ private static string Combine(string stdout, string stderr)
+ {
+ string outText = (stdout ?? string.Empty).Trim();
+ string errText = (stderr ?? string.Empty).Trim();
+ string combined =
+ string.IsNullOrEmpty(errText) ? outText :
+ string.IsNullOrEmpty(outText) ? errText :
+ outText + "\n" + errText;
+
+ // Keep dialogs readable — echo only the tail of long installer output.
+ const int max = 1500;
+ if (combined.Length > max)
+ {
+ combined = "…" + combined.Substring(combined.Length - max);
+ }
+ return combined;
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Dependencies/UvInstaller.cs.meta b/MCPForUnity/Editor/Dependencies/UvInstaller.cs.meta
new file mode 100644
index 000000000..d3fe2b3c1
--- /dev/null
+++ b/MCPForUnity/Editor/Dependencies/UvInstaller.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 55a5a123f9454b75906bdb5b7d008990
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Helpers/ExecPath.cs b/MCPForUnity/Editor/Helpers/ExecPath.cs
index 350689821..412542139 100644
--- a/MCPForUnity/Editor/Helpers/ExecPath.cs
+++ b/MCPForUnity/Editor/Helpers/ExecPath.cs
@@ -34,6 +34,7 @@ internal static string ResolveClaude()
"/opt/homebrew/bin/claude",
"/usr/local/bin/claude",
Path.Combine(home, ".local", "bin", "claude"),
+ Path.Combine(home, ".claude", "local", "claude"), // `claude migrate-installer` location
};
foreach (string c in candidates) { if (File.Exists(c)) return c; }
// Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude
@@ -59,6 +60,7 @@ internal static string ResolveClaude()
Path.Combine(localAppData, "Programs", "claude", "claude.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "claude", "claude.exe"),
Path.Combine(home, ".local", "bin", "claude.exe"),
+ Path.Combine(home, ".claude", "local", "claude.exe"), // `claude migrate-installer` location
// npm global locations (.cmd preferred for non-interactive processes)
Path.Combine(appData, "npm", "claude.cmd"),
Path.Combine(localAppData, "npm", "claude.cmd"),
@@ -81,6 +83,7 @@ internal static string ResolveClaude()
"/usr/local/bin/claude",
"/usr/bin/claude",
Path.Combine(home, ".local", "bin", "claude"),
+ Path.Combine(home, ".claude", "local", "claude"), // `claude migrate-installer` location
};
foreach (string c in candidates) { if (File.Exists(c)) return c; }
// Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude
diff --git a/MCPForUnity/Editor/Services/PathResolverService.cs b/MCPForUnity/Editor/Services/PathResolverService.cs
index 94adb88e5..c233d8680 100644
--- a/MCPForUnity/Editor/Services/PathResolverService.cs
+++ b/MCPForUnity/Editor/Services/PathResolverService.cs
@@ -107,60 +107,10 @@ public string GetClaudeCliPath()
return null;
}
- // No override - use platform-specific discovery
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- string[] candidates = new[]
- {
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "claude", "claude.exe"),
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "claude", "claude.exe"),
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", "claude.exe"),
- };
-
- foreach (var c in candidates)
- {
- if (File.Exists(c)) return c;
- }
-
-#if UNITY_EDITOR_WIN
- // Fall back to PATH search (handles non-standard install locations and npm shims)
- foreach (var name in new[] { "claude.exe", "claude.cmd", "claude.ps1" })
- {
- string fromPath = ExecPath.FindInPathWindows(name);
- if (!string.IsNullOrEmpty(fromPath)) return fromPath;
- }
-#endif
- }
- else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
- {
- string[] candidates = new[]
- {
- "/opt/homebrew/bin/claude",
- "/usr/local/bin/claude",
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", "claude")
- };
-
- foreach (var c in candidates)
- {
- if (File.Exists(c)) return c;
- }
- }
- else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
- {
- string[] candidates = new[]
- {
- "/usr/bin/claude",
- "/usr/local/bin/claude",
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", "claude")
- };
-
- foreach (var c in candidates)
- {
- if (File.Exists(c)) return c;
- }
- }
-
- return null;
+ // No override: delegate to the shared discovery in ExecPath, which covers the
+ // native-installer, npm, NVM and PATH-scan locations for every platform. Kept in
+ // one place so both call sites (this and ExecPath's own callers) stay in sync.
+ return ExecPath.ResolveClaude();
}
public bool IsPythonDetected()
diff --git a/MCPForUnity/Editor/Windows/Components/Branding.meta b/MCPForUnity/Editor/Windows/Components/Branding.meta
new file mode 100644
index 000000000..1dd751339
--- /dev/null
+++ b/MCPForUnity/Editor/Windows/Components/Branding.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d20507fd66884816930ea8f431fd6ce8
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Windows/Components/Branding/OceanMark.cs b/MCPForUnity/Editor/Windows/Components/Branding/OceanMark.cs
new file mode 100644
index 000000000..c84346d59
--- /dev/null
+++ b/MCPForUnity/Editor/Windows/Components/Branding/OceanMark.cs
@@ -0,0 +1,163 @@
+using UnityEngine;
+using UnityEngine.UIElements;
+
+namespace MCPForUnity.Editor.Windows.Components.Branding
+{
+ ///
+ /// The MCP for Unity "Ocean split-cube" brand mark.
+ ///
+ /// On Unity 2022.1+ it is drawn as resolution-independent vector geometry via UI Toolkit's
+ /// Painter2D — crisp at any DPI, theme-independent (fixed brand colors matching the source
+ /// SVG), and with no package dependency. Painter2D does not exist on the 2021.3 floor, so
+ /// there we fall back to the raster brand icon (package-icon.png) that ships with the package.
+ ///
+ /// Geometry is transcribed from website/static/img/logo-mark.svg (viewBox "30 30 140 140"),
+ /// which remains the human-facing source of truth. Size the element via USS (width/height);
+ /// the drawing scales to the resolved content box.
+ ///
+ public sealed class OceanMark : VisualElement
+ {
+ public OceanMark()
+ {
+#if UNITY_2022_1_OR_NEWER
+ generateVisualContent += OnGenerateVisualContent;
+ RegisterCallback(_ => MarkDirtyRepaint());
+#else
+ ApplyRasterFallback();
+#endif
+ pickingMode = PickingMode.Ignore; // purely decorative
+ }
+
+#if UNITY_2022_1_OR_NEWER
+ // The source SVG viewBox spans 30..170 on both axes (a 140-unit square).
+ private const float SvgOrigin = 30f;
+ private const float SvgSize = 140f;
+
+ // Brand palette (hex values lifted directly from logo-mark.svg).
+ private static readonly Color LeftFill = FromHex(0x2563EB); // blue cube faces
+ private static readonly Color LeftStroke = FromHex(0x60A5FA); // blue cube outline
+ private static readonly Color RightFill = FromHex(0x0D9488); // teal cube faces
+ private static readonly Color RightStroke = FromHex(0x2DD4BF);// teal cube outline
+ private static readonly Color Bridge = FromHex(0x22D3EE); // cyan bridge
+
+ private void OnGenerateVisualContent(MeshGenerationContext mgc)
+ {
+ Rect rect = contentRect;
+ float box = Mathf.Min(rect.width, rect.height);
+ if (box <= 1f) return; // not laid out yet / too small to draw
+
+ float scale = box / SvgSize;
+ Vector2 P(float x, float y) => MapSvgPoint(x, y, rect);
+ float S(float w) => w * scale;
+
+ Painter2D p = mgc.painter2D;
+ p.lineJoin = LineJoin.Round;
+ p.lineCap = LineCap.Round;
+
+ // --- Left cube (blue) ---
+ FillPoly(p, WithAlpha(LeftFill, 0.22f), P(94, 40), P(42, 70), P(94, 100));
+ FillPoly(p, WithAlpha(LeftFill, 0.12f), P(42, 70), P(42, 130), P(94, 160), P(94, 100));
+ StrokePath(p, LeftStroke, S(3f), true, P(94, 40), P(42, 70), P(42, 130), P(94, 160));
+ StrokePath(p, WithAlpha(LeftStroke, 0.65f), S(1.8f), false, P(42, 70), P(94, 100));
+
+ // --- Right cube (teal) ---
+ FillPoly(p, WithAlpha(RightFill, 0.22f), P(106, 40), P(158, 70), P(106, 100));
+ FillPoly(p, WithAlpha(RightFill, 0.12f), P(158, 70), P(158, 130), P(106, 160), P(106, 100));
+ StrokePath(p, RightStroke, S(3f), true, P(106, 40), P(158, 70), P(158, 130), P(106, 160));
+ StrokePath(p, WithAlpha(RightStroke, 0.65f), S(1.8f), false, P(158, 70), P(106, 100));
+
+ // --- Bridge rungs (cyan) ---
+ StrokePath(p, Bridge, S(3f), false, P(94, 70), P(106, 70));
+ StrokePath(p, Bridge, S(3f), false, P(94, 100), P(106, 100));
+ StrokePath(p, Bridge, S(3f), false, P(94, 130), P(106, 130));
+
+ // --- Bridge dots (cyan) ---
+ float dot = S(2.2f);
+ FillDot(p, Bridge, P(94, 70), dot);
+ FillDot(p, Bridge, P(106, 70), dot);
+ FillDot(p, Bridge, P(94, 100), dot);
+ FillDot(p, Bridge, P(106, 100), dot);
+ FillDot(p, Bridge, P(94, 130), dot);
+ FillDot(p, Bridge, P(106, 130), dot);
+ }
+
+ private static void FillPoly(Painter2D p, Color color, params Vector2[] pts)
+ {
+ p.fillColor = color;
+ p.BeginPath();
+ p.MoveTo(pts[0]);
+ for (int i = 1; i < pts.Length; i++) p.LineTo(pts[i]);
+ p.ClosePath();
+ p.Fill();
+ }
+
+ private static void StrokePath(Painter2D p, Color color, float width, bool closed, params Vector2[] pts)
+ {
+ p.strokeColor = color;
+ p.lineWidth = width;
+ p.BeginPath();
+ p.MoveTo(pts[0]);
+ for (int i = 1; i < pts.Length; i++) p.LineTo(pts[i]);
+ if (closed) p.ClosePath();
+ p.Stroke();
+ }
+
+ private static void FillDot(Painter2D p, Color color, Vector2 center, float radius)
+ {
+ if (radius <= 0.01f) return;
+ p.fillColor = color;
+ p.BeginPath();
+ p.Arc(center, radius, new Angle(0f, AngleUnit.Degree), new Angle(360f, AngleUnit.Degree));
+ p.Fill();
+ }
+
+ private static Color WithAlpha(Color c, float a)
+ {
+ c.a = a;
+ return c;
+ }
+
+ /// Map a point from SVG viewBox space into the element's content rect (square, centered).
+ private static Vector2 MapSvgPoint(float svgX, float svgY, Rect content)
+ {
+ float box = Mathf.Min(content.width, content.height);
+ float scale = box / SvgSize;
+ float offsetX = (content.width - box) * 0.5f;
+ float offsetY = (content.height - box) * 0.5f;
+ return new Vector2(
+ offsetX + (svgX - SvgOrigin) * scale,
+ offsetY + (svgY - SvgOrigin) * scale);
+ }
+
+ /// Convert a 0xRRGGBB literal into an opaque Color.
+ private static Color FromHex(int rgb) => new Color(
+ ((rgb >> 16) & 0xFF) / 255f,
+ ((rgb >> 8) & 0xFF) / 255f,
+ (rgb & 0xFF) / 255f,
+ 1f);
+#else
+ // Painter2D is unavailable before Unity 2022.1 — show the raster brand icon instead.
+ private void ApplyRasterFallback()
+ {
+ Texture2D tex = LoadBrandTexture();
+ if (tex == null) return;
+ style.backgroundImage = new StyleBackground(tex);
+ style.unityBackgroundScaleMode = ScaleMode.ScaleToFit;
+ }
+
+ private static Texture2D LoadBrandTexture()
+ {
+ try
+ {
+ string root = MCPForUnity.Editor.Helpers.AssetPathUtility.GetMcpPackageRootPath();
+ return UnityEditor.AssetDatabase.LoadAssetAtPath($"{root}/package-icon.png");
+ }
+ catch (System.Exception ex)
+ {
+ MCPForUnity.Editor.Helpers.McpLog.Warn($"OceanMark: failed to load brand icon fallback: {ex.Message}");
+ return null;
+ }
+ }
+#endif
+ }
+}
diff --git a/MCPForUnity/Editor/Windows/Components/Branding/OceanMark.cs.meta b/MCPForUnity/Editor/Windows/Components/Branding/OceanMark.cs.meta
new file mode 100644
index 000000000..3526141e2
--- /dev/null
+++ b/MCPForUnity/Editor/Windows/Components/Branding/OceanMark.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a5a6cc86fe9844309b97362a718c3edd
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Windows/Components/Common.uss b/MCPForUnity/Editor/Windows/Components/Common.uss
index ecffd6a9c..c380799d7 100644
--- a/MCPForUnity/Editor/Windows/Components/Common.uss
+++ b/MCPForUnity/Editor/Windows/Components/Common.uss
@@ -170,6 +170,23 @@
-unity-text-align: middle-left;
}
+/* At narrow widths the connection status label (e.g. "Session Active (Project)") must wrap/shrink
+ instead of sliding under the toggle button. Scoped to the connection section so other shared
+ .status-text rows are unaffected; uses wrapping (not ellipsis) so the text stays fully readable. */
+#connection-section .status-container {
+ flex-shrink: 1;
+ min-width: 0;
+}
+
+#connection-section .status-text {
+ flex-shrink: 1;
+ white-space: normal;
+}
+
+#connection-section #connection-toggle {
+ flex-shrink: 0;
+}
+
.status-indicator-small {
width: 8px;
height: 8px;
diff --git a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
index bd1e05844..085593905 100644
--- a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
+++ b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
@@ -7,6 +7,7 @@
using MCPForUnity.Editor.Services;
using MCPForUnity.Editor.Windows.Components.Advanced;
using MCPForUnity.Editor.Windows.Components.AssetGen;
+using MCPForUnity.Editor.Windows.Components.Branding;
using MCPForUnity.Editor.Windows.Components.ClientConfig;
using MCPForUnity.Editor.Windows.Components.Connection;
using MCPForUnity.Editor.Windows.Components.Resources;
@@ -185,6 +186,15 @@ public void CreateGUI()
rootVisualElement.styleSheets.Add(commonStyleSheet);
}
+ // Embed the Ocean brand mark at the left of the header bar
+ var headerLeft = rootVisualElement.Q("header-left");
+ if (headerLeft != null && headerLeft.Q() == null)
+ {
+ var logo = new OceanMark { name = "header-logo" };
+ logo.AddToClassList("header-logo");
+ headerLeft.Insert(0, logo);
+ }
+
// Cache UI elements
versionLabel = rootVisualElement.Q