diff --git a/CLAUDE.md b/CLAUDE.md
index d95638a4f..815a255ec 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -70,7 +70,7 @@ from services.registry import mcp_for_unity_tool
@mcp_for_unity_tool(
description="Does something in Unity.",
- group="core", # core (default), vfx, animation, ui, scripting_ext, testing, probuilder, profiling, docs
+ group="core", # core (default), vfx, animation, ui, scripting_ext, testing, probuilder, profiling, docs, asset_gen
)
async def manage_something(
ctx: Context,
diff --git a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs
index 6f7ca9f04..01e72b18f 100644
--- a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs
+++ b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs
@@ -86,5 +86,12 @@ internal static class EditorPrefKeys
internal const string AssetGenOutputRoot = "MCPForUnity.AssetGen.OutputRoot";
internal const string AssetGenAutoNormalize = "MCPForUnity.AssetGen.AutoNormalize";
internal const string AssetGenProviderEnabledPrefix = "MCPForUnity.AssetGen.Enabled.";
+
+ // Blender Bridge (Asset Gen tab). Machine-specific, non-secret. An empty ForkPath means the
+ // addon sync / update-check features are not configured; import and screenshot still work.
+ internal const string BlenderHost = "MCPForUnity.Blender.Host";
+ internal const string BlenderPort = "MCPForUnity.Blender.Port";
+ internal const string BlenderForkPath = "MCPForUnity.Blender.ForkPath";
+ internal const string BlenderAddonsDir = "MCPForUnity.Blender.AddonsDir";
}
}
diff --git a/MCPForUnity/Editor/Helpers/BlenderBridgePrefs.cs b/MCPForUnity/Editor/Helpers/BlenderBridgePrefs.cs
new file mode 100644
index 000000000..c3af3662f
--- /dev/null
+++ b/MCPForUnity/Editor/Helpers/BlenderBridgePrefs.cs
@@ -0,0 +1,93 @@
+using System.IO;
+using MCPForUnity.Editor.Constants;
+using MCPForUnity.Editor.Services.Blender;
+using UnityEditor;
+
+namespace MCPForUnity.Editor.Helpers
+{
+ ///
+ /// Per-user, NON-SECRET configuration for the Blender Bridge (Asset Gen tab): where the
+ /// BlenderMCP addon socket listens, where the user's blender-mcp checkout lives, and where
+ /// Blender keeps its user addons. Nothing here is required for the bridge to talk to Blender;
+ /// the checkout path only unlocks addon sync and update checks. EditorPrefs is main-thread
+ /// only, so callers capture what they need (e.g. ) before going async.
+ ///
+ public static class BlenderBridgePrefs
+ {
+ public const string DefaultHost = "127.0.0.1";
+ public const int DefaultPort = 9876;
+ public const string AddonFileName = "addon.py";
+
+ public static string Host
+ {
+ get => EditorPrefs.GetString(EditorPrefKeys.BlenderHost, DefaultHost);
+ set => SetOrDelete(EditorPrefKeys.BlenderHost, value);
+ }
+
+ public static int Port
+ {
+ get => EditorPrefs.GetInt(EditorPrefKeys.BlenderPort, DefaultPort);
+ set => EditorPrefs.SetInt(EditorPrefKeys.BlenderPort, value > 0 && value <= 65535 ? value : DefaultPort);
+ }
+
+ /// Host and port as one value, safe to hand to a background thread.
+ public static BlenderEndpoint Endpoint => new BlenderEndpoint(Host, Port);
+
+ /// Local checkout of the blender-mcp repository (contains addon.py). Empty = not configured.
+ public static string ForkPath
+ {
+ get => NormalizePath(EditorPrefs.GetString(EditorPrefKeys.BlenderForkPath, string.Empty));
+ set => SetOrDelete(EditorPrefKeys.BlenderForkPath, NormalizePath(value));
+ }
+
+ ///
+ /// Blender's user addons directory. Empty = auto-detect the newest
+ /// <user config>/Blender/<version>/scripts/addons that already has the addon installed.
+ ///
+ public static string AddonsDirOverride
+ {
+ get => NormalizePath(EditorPrefs.GetString(EditorPrefKeys.BlenderAddonsDir, string.Empty));
+ set => SetOrDelete(EditorPrefKeys.BlenderAddonsDir, NormalizePath(value));
+ }
+
+ public static bool IsForkConfigured => !string.IsNullOrEmpty(ForkPath);
+
+ public static string ForkAddonPath => IsForkConfigured ? ForkPath + "/" + AddonFileName : null;
+
+ /// The override when set, otherwise the newest detected user addons folder; null if none.
+ public static string ResolveAddonsDir()
+ {
+ string o = AddonsDirOverride;
+ return !string.IsNullOrEmpty(o) ? o : BlenderDetection.FindUserAddonsDir(AddonFileName);
+ }
+
+ public static string InstalledAddonPath
+ {
+ get
+ {
+ string dir = ResolveAddonsDir();
+ return string.IsNullOrEmpty(dir) ? null : dir + "/" + AddonFileName;
+ }
+ }
+
+ /// True when the checkout path points at a folder that actually contains addon.py.
+ public static bool IsValidForkPath(string path)
+ {
+ string p = NormalizePath(path);
+ return !string.IsNullOrEmpty(p) && File.Exists(Path.Combine(p, AddonFileName));
+ }
+
+ /// Trims, converts backslashes to slashes and drops a trailing slash.
+ internal static string NormalizePath(string value)
+ {
+ return (value ?? string.Empty).Trim().Replace('\\', '/').TrimEnd('/');
+ }
+
+ /// Stores a string pref, deleting the key when the value is blank so defaults apply.
+ private static void SetOrDelete(string key, string value)
+ {
+ if (string.IsNullOrWhiteSpace(value)) EditorPrefs.DeleteKey(key);
+ else EditorPrefs.SetString(key, value.Trim());
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Helpers/BlenderBridgePrefs.cs.meta b/MCPForUnity/Editor/Helpers/BlenderBridgePrefs.cs.meta
new file mode 100644
index 000000000..5dafa8364
--- /dev/null
+++ b/MCPForUnity/Editor/Helpers/BlenderBridgePrefs.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 029b420c97f34c738e98b875fe56bf73
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Helpers/BlenderDetection.cs b/MCPForUnity/Editor/Helpers/BlenderDetection.cs
index 6af74e344..9e11ee000 100644
--- a/MCPForUnity/Editor/Helpers/BlenderDetection.cs
+++ b/MCPForUnity/Editor/Helpers/BlenderDetection.cs
@@ -1,14 +1,16 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
///
- /// Best-effort detection of a locally installed Blender application, for the Asset Gen tab's
- /// "Blender → Unity handoff" hint. This finds the Blender APP only — it cannot tell whether the
- /// BlenderMCP server is configured in the user's AI client (that lives outside Unity).
+ /// Best-effort detection of a locally installed Blender application and of Blender's per-user
+ /// addons folders, for the Asset Gen tab's Blender Bridge. This finds the Blender APP and its
+ /// config folders only — whether the BlenderMCP addon is running is a socket question answered
+ /// by the bridge itself.
///
internal static class BlenderDetection
{
@@ -76,5 +78,84 @@ internal static IEnumerable CandidatePaths()
}
return list;
}
+
+ ///
+ /// Blender's per-user config roots (the folder that holds one subfolder per Blender version):
+ /// %APPDATA%/Blender Foundation/Blender on Windows, ~/Library/Application Support/Blender on
+ /// macOS, $XDG_CONFIG_HOME/blender or ~/.config/blender on Linux.
+ ///
+ internal static IEnumerable UserConfigRoots()
+ {
+ var list = new List();
+ switch (Application.platform)
+ {
+ case RuntimePlatform.WindowsEditor:
+ string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+ if (!string.IsNullOrEmpty(appData))
+ list.Add(Path.Combine(appData, "Blender Foundation", "Blender"));
+ break;
+ case RuntimePlatform.OSXEditor:
+ string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ if (!string.IsNullOrEmpty(home))
+ list.Add(Path.Combine(home, "Library", "Application Support", "Blender"));
+ break;
+ case RuntimePlatform.LinuxEditor:
+ string xdg = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
+ string linuxHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ if (!string.IsNullOrEmpty(xdg)) list.Add(Path.Combine(xdg, "blender"));
+ else if (!string.IsNullOrEmpty(linuxHome)) list.Add(Path.Combine(linuxHome, ".config", "blender"));
+ break;
+ }
+ return list;
+ }
+
+ /// <root>/<X.Y>/scripts/addons for every versioned config folder, newest version first.
+ internal static IEnumerable UserAddonsDirs()
+ {
+ var found = new List<(Version Ver, string Dir)>();
+ foreach (string root in UserConfigRoots())
+ {
+ try
+ {
+ if (!Directory.Exists(root)) continue;
+ foreach (string d in Directory.GetDirectories(root))
+ {
+ Version v = ParseVersion(Path.GetFileName(d));
+ if (v != null) found.Add((v, Path.Combine(d, "scripts", "addons")));
+ }
+ }
+ catch { /* unreadable dir; ignore */ }
+ }
+ return found.OrderByDescending(x => x.Ver).Select(x => x.Dir.Replace('\\', '/')).ToList();
+ }
+
+ /// Newest user addons dir that already contains , else the newest one, else null.
+ internal static string FindUserAddonsDir(string fileName)
+ {
+ try { return PickAddonsDir(UserAddonsDirs(), File.Exists, fileName); }
+ catch { return null; }
+ }
+
+ /// Pure core of . Testable.
+ internal static string PickAddonsDir(IEnumerable dirsNewestFirst, Func fileExists, string fileName)
+ {
+ if (dirsNewestFirst == null) return null;
+ string first = null;
+ foreach (string d in dirsNewestFirst)
+ {
+ if (string.IsNullOrEmpty(d)) continue;
+ first ??= d;
+ if (!string.IsNullOrEmpty(fileName) && fileExists != null && fileExists(d.TrimEnd('/') + "/" + fileName))
+ return d;
+ }
+ return first;
+ }
+
+ /// Parses a Blender version folder name ("4.2", "5.2") into a comparable Version; null if it is not one.
+ internal static Version ParseVersion(string name)
+ {
+ if (string.IsNullOrEmpty(name)) return null;
+ return Version.TryParse(name.Contains('.') ? name : name + ".0", out Version v) ? v : null;
+ }
}
}
diff --git a/MCPForUnity/Editor/MenuItems/BlenderBridgeMenu.cs b/MCPForUnity/Editor/MenuItems/BlenderBridgeMenu.cs
new file mode 100644
index 000000000..efb888d18
--- /dev/null
+++ b/MCPForUnity/Editor/MenuItems/BlenderBridgeMenu.cs
@@ -0,0 +1,64 @@
+using System.Threading.Tasks;
+using MCPForUnity.Editor.Constants;
+using MCPForUnity.Editor.Helpers;
+using MCPForUnity.Editor.Tools.Blender;
+using MCPForUnity.Editor.Windows;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using UnityEditor;
+
+namespace MCPForUnity.Editor.MenuItems
+{
+ ///
+ /// Menu entry points that drive the same code path as the blender_bridge tool, so the
+ /// Blender handoff works without any AI client attached. Settings live in the Generative tab.
+ /// Actions are awaited so the editor stays responsive while Blender exports.
+ ///
+ public static class BlenderBridgeMenu
+ {
+ private const string Root = ProductInfo.MenuRoot + "/Blender Bridge/";
+
+ /// Exports Blender's current selection as GLB and places it in the open scene.
+ [MenuItem(Root + "Import Selection From Blender (GLB)", priority = 20)]
+ private static async void ImportSelection()
+ {
+ await RunAsync(new JObject { ["action"] = "import_model", ["selection_only"] = true, ["format"] = "glb" });
+ }
+
+ /// Exports Blender's whole scene as GLB and places it in the open scene.
+ [MenuItem(Root + "Import Whole Scene From Blender (GLB)", priority = 21)]
+ private static async void ImportScene()
+ {
+ await RunAsync(new JObject { ["action"] = "import_model", ["format"] = "glb", ["name"] = "BlenderScene" });
+ }
+
+ /// Captures Blender's viewport and reveals the PNG.
+ [MenuItem(Root + "Blender Viewport Screenshot", priority = 22)]
+ private static async void Screenshot()
+ {
+ JObject r = await RunAsync(new JObject { ["action"] = "screenshot" });
+ string path = r?["data"]?["path"]?.ToString();
+ if (!string.IsNullOrEmpty(path)) EditorUtility.RevealInFinder(path);
+ }
+
+ /// Opens the MCP for Unity window; the Blender Bridge panel is in its Generative tab.
+ [MenuItem(Root + "Settings...", priority = 40)]
+ private static void OpenSettings()
+ {
+ MCPForUnityEditorWindow.ShowWindow();
+ McpLog.Info("Blender Bridge settings are in the Generative tab of the MCP for Unity window.");
+ }
+
+ /// Runs one bridge action and logs its full result.
+ private static async Task RunAsync(JObject parameters)
+ {
+ object result = await BlenderBridgeTool.HandleCommand(parameters);
+ JObject json = JObject.FromObject(result);
+ bool ok = json.Value("success") ?? false;
+ string text = json.ToString(Formatting.Indented);
+ if (ok) McpLog.Info($"[Blender Bridge] {parameters["action"]}: {json["message"]}\n{text}");
+ else McpLog.Error($"[Blender Bridge] {parameters["action"]} failed: {json["error"]}\n{text}");
+ return json;
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/MenuItems/BlenderBridgeMenu.cs.meta b/MCPForUnity/Editor/MenuItems/BlenderBridgeMenu.cs.meta
new file mode 100644
index 000000000..b490058fb
--- /dev/null
+++ b/MCPForUnity/Editor/MenuItems/BlenderBridgeMenu.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: cf5a30eff9b34262b2e15cc23f196d12
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Services/Blender.meta b/MCPForUnity/Editor/Services/Blender.meta
new file mode 100644
index 000000000..c2029bf05
--- /dev/null
+++ b/MCPForUnity/Editor/Services/Blender.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5171d93ea027470b96c8930306ca0c60
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Services/Blender/BlenderSocketClient.cs b/MCPForUnity/Editor/Services/Blender/BlenderSocketClient.cs
new file mode 100644
index 000000000..18caa245c
--- /dev/null
+++ b/MCPForUnity/Editor/Services/Blender/BlenderSocketClient.cs
@@ -0,0 +1,171 @@
+using System;
+using System.IO;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace MCPForUnity.Editor.Services.Blender
+{
+ /// Where the BlenderMCP addon socket listens. Captured on the main thread so I/O can run elsewhere.
+ public readonly struct BlenderEndpoint
+ {
+ public string Host { get; }
+ public int Port { get; }
+
+ public BlenderEndpoint(string host, int port)
+ {
+ Host = host;
+ Port = port;
+ }
+
+ /// Formats the endpoint as host:port.
+ public override string ToString() => $"{Host}:{Port}";
+ }
+
+ ///
+ /// Minimal client for the BlenderMCP addon socket. Protocol: one JSON object
+ /// {"type": ..., "params": {...}} per request and one JSON object
+ /// {"status": "success"|"error", "result"|"message": ...} back, with no length framing —
+ /// the addon parses whenever the accumulated bytes form valid JSON, so this does the same.
+ /// A fresh connection is used per command so it never interleaves with the AI client's
+ /// own BlenderMCP server talking to the same addon. The synchronous methods block and touch
+ /// no Unity API, so callers run them through the *Async wrappers off the editor thread.
+ ///
+ public static class BlenderSocketClient
+ {
+ private const int ConnectTimeoutSeconds = 3;
+
+ /// Sends one command and blocks until the addon answers or the timeout elapses.
+ public static JToken Send(BlenderEndpoint endpoint, string type, JObject @params = null, int timeoutSeconds = 60)
+ {
+ var request = new JObject { ["type"] = type, ["params"] = @params ?? new JObject() };
+ byte[] payload = Encoding.UTF8.GetBytes(request.ToString(Formatting.None));
+
+ using var client = new TcpClient();
+ IAsyncResult connect = client.BeginConnect(endpoint.Host, endpoint.Port, null, null);
+ if (!connect.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(ConnectTimeoutSeconds)) || !client.Connected)
+ {
+ throw new BlenderUnavailableException(
+ $"Blender addon not reachable at {endpoint}. Start Blender and press " +
+ "'Connect to MCP server' in the BlenderMCP sidebar (N panel).");
+ }
+ client.EndConnect(connect);
+
+ using NetworkStream stream = client.GetStream();
+ stream.ReadTimeout = Math.Max(1, timeoutSeconds) * 1000;
+ stream.Write(payload, 0, payload.Length);
+ stream.Flush();
+
+ var buffer = new MemoryStream();
+ var chunk = new byte[65536];
+ DateTime deadline = DateTime.UtcNow.AddSeconds(timeoutSeconds);
+
+ while (true)
+ {
+ int n;
+ try
+ {
+ n = stream.Read(chunk, 0, chunk.Length);
+ }
+ catch (IOException e)
+ {
+ throw new TimeoutException(
+ $"Timed out after {timeoutSeconds}s waiting for Blender to answer '{type}'.", e);
+ }
+
+ if (n <= 0) break;
+ buffer.Write(chunk, 0, n);
+
+ if (TryParseResponse(buffer.GetBuffer(), (int)buffer.Length, out JObject parsed)) return Unwrap(parsed, type);
+ if (DateTime.UtcNow > deadline)
+ throw new TimeoutException($"Timed out after {timeoutSeconds}s waiting for Blender to answer '{type}'.");
+ }
+
+ if (TryParseResponse(buffer.GetBuffer(), (int)buffer.Length, out JObject final)) return Unwrap(final, type);
+ throw new IOException($"Blender closed the connection before a complete response to '{type}' arrived.");
+ }
+
+ /// Runs on the thread pool so the editor stays responsive.
+ public static Task SendAsync(BlenderEndpoint endpoint, string type, JObject @params = null, int timeoutSeconds = 60)
+ {
+ return Task.Run(() => Send(endpoint, type, @params, timeoutSeconds));
+ }
+
+ /// Runs Python inside Blender (blocking) and returns its captured stdout.
+ public static string RunPython(BlenderEndpoint endpoint, string code, int timeoutSeconds = 120)
+ {
+ JToken result = Send(endpoint, "execute_code", new JObject { ["code"] = code }, timeoutSeconds);
+ return result?["result"]?.ToString() ?? string.Empty;
+ }
+
+ /// Runs on the thread pool.
+ public static Task RunPythonAsync(BlenderEndpoint endpoint, string code, int timeoutSeconds = 120)
+ {
+ return Task.Run(() => RunPython(endpoint, code, timeoutSeconds));
+ }
+
+ /// Probes the addon with get_scene_info; returns whether it answered and the error text if not.
+ public static (bool Ok, string Error) Probe(BlenderEndpoint endpoint, int timeoutSeconds = 10)
+ {
+ try
+ {
+ Send(endpoint, "get_scene_info", null, timeoutSeconds);
+ return (true, null);
+ }
+ catch (Exception e)
+ {
+ return (false, e.Message);
+ }
+ }
+
+ /// Runs on the thread pool.
+ public static Task<(bool Ok, string Error)> ProbeAsync(BlenderEndpoint endpoint, int timeoutSeconds = 10)
+ {
+ return Task.Run(() => Probe(endpoint, timeoutSeconds));
+ }
+
+ /// True once the bytes received so far form one complete JSON object.
+ internal static bool TryParseResponse(byte[] buffer, int length, out JObject obj)
+ {
+ try
+ {
+ obj = JObject.Parse(Encoding.UTF8.GetString(buffer, 0, length));
+ return true;
+ }
+ catch
+ {
+ obj = null;
+ return false;
+ }
+ }
+
+ ///
+ /// Returns the addon's "result" payload for a success response, throws
+ /// for an error response, and rejects anything else
+ /// so a malformed reply never surfaces as a successful command with a null payload.
+ ///
+ internal static JToken Unwrap(JObject response, string type)
+ {
+ string status = (string)response?["status"];
+ if (string.Equals(status, "success", StringComparison.OrdinalIgnoreCase))
+ return response["result"];
+ if (string.Equals(status, "error", StringComparison.OrdinalIgnoreCase))
+ throw new BlenderCommandException($"Blender '{type}' failed: {(string)response["message"] ?? "unknown error"}");
+ throw new InvalidDataException($"Blender returned an invalid status '{status ?? "(none)"}' for '{type}'.");
+ }
+ }
+
+ /// The addon socket could not be reached (Blender closed or the addon not connected).
+ public class BlenderUnavailableException : Exception
+ {
+ public BlenderUnavailableException(string message) : base(message) { }
+ }
+
+ /// The addon accepted the command but reported an error while running it.
+ public class BlenderCommandException : Exception
+ {
+ public BlenderCommandException(string message) : base(message) { }
+ }
+}
diff --git a/MCPForUnity/Editor/Services/Blender/BlenderSocketClient.cs.meta b/MCPForUnity/Editor/Services/Blender/BlenderSocketClient.cs.meta
new file mode 100644
index 000000000..73c1144e0
--- /dev/null
+++ b/MCPForUnity/Editor/Services/Blender/BlenderSocketClient.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6c83ed8aaae34a829fc59df7c1a53948
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Tools/Blender.meta b/MCPForUnity/Editor/Tools/Blender.meta
new file mode 100644
index 000000000..2f3497027
--- /dev/null
+++ b/MCPForUnity/Editor/Tools/Blender.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d61fccfc880e4bd38412f8705fc73104
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Tools/Blender/BlenderBridgeTool.cs b/MCPForUnity/Editor/Tools/Blender/BlenderBridgeTool.cs
new file mode 100644
index 000000000..d825a4593
--- /dev/null
+++ b/MCPForUnity/Editor/Tools/Blender/BlenderBridgeTool.cs
@@ -0,0 +1,927 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+using MCPForUnity.Editor.Helpers;
+using MCPForUnity.Editor.Security;
+using MCPForUnity.Editor.Services.Blender;
+using MCPForUnity.Editor.Tools.AssetGen;
+using MCPForUnity.Runtime.Helpers;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+
+namespace MCPForUnity.Editor.Tools.Blender
+{
+ ///
+ /// Lets the Unity Editor talk straight to the BlenderMCP addon socket, so a Blender → Unity
+ /// handoff is one call (export → import through the shared model pipeline → place → normalize)
+ /// instead of an AI-orchestrated multi-step dance. Also drives the Blender Bridge panel in the
+ /// Asset Gen tab and the Window/MCP for Unity/Blender Bridge menu. Carries no API keys.
+ /// Socket and git work runs on the thread pool; Unity API calls happen after the await, back on
+ /// the editor thread (the bridge awaits handlers on Unity's synchronization context).
+ ///
+ [McpForUnityTool("blender_bridge", AutoRegister = false, Group = "asset_gen",
+ Description = "Bridge to a running Blender with the BlenderMCP addon: status, scene/object info, viewport " +
+ "screenshot, run Python in Blender, import a model (export → import → place → normalize), " +
+ "check the blender-mcp checkout for updates, and sync its addon into Blender.")]
+ public static class BlenderBridgeTool
+ {
+ private static readonly string[] ValidActions =
+ { "status", "scene_info", "object_info", "screenshot", "run_python", "import_model", "compare_screenshot", "setup_bloom", "check_updates", "sync_addon" };
+
+ private const string NotConfiguredMessage =
+ "The blender-mcp checkout is not set. Open Window > MCP for Unity > Generative > Blender Bridge " +
+ "and pick the folder that contains addon.py.";
+
+ /// Entry point for the bridge: validates parameters, dispatches by action, and never throws.
+ public static async Task