Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions MCPForUnity/Editor/Constants/EditorPrefKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
93 changes: 93 additions & 0 deletions MCPForUnity/Editor/Helpers/BlenderBridgePrefs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.IO;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Services.Blender;
using UnityEditor;

namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// 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. <see cref="Endpoint"/>) before going async.
/// </summary>
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);
}

/// <summary>Host and port as one value, safe to hand to a background thread.</summary>
public static BlenderEndpoint Endpoint => new BlenderEndpoint(Host, Port);

/// <summary>Local checkout of the blender-mcp repository (contains addon.py). Empty = not configured.</summary>
public static string ForkPath
{
get => NormalizePath(EditorPrefs.GetString(EditorPrefKeys.BlenderForkPath, string.Empty));
set => SetOrDelete(EditorPrefKeys.BlenderForkPath, NormalizePath(value));
}

/// <summary>
/// Blender's user addons directory. Empty = auto-detect the newest
/// &lt;user config&gt;/Blender/&lt;version&gt;/scripts/addons that already has the addon installed.
/// </summary>
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;

/// <summary>The override when set, otherwise the newest detected user addons folder; null if none.</summary>
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;
}
}

/// <summary>True when the checkout path points at a folder that actually contains addon.py.</summary>
public static bool IsValidForkPath(string path)
{
string p = NormalizePath(path);
return !string.IsNullOrEmpty(p) && File.Exists(Path.Combine(p, AddonFileName));
}

/// <summary>Trims, converts backslashes to slashes and drops a trailing slash.</summary>
internal static string NormalizePath(string value)
{
return (value ?? string.Empty).Trim().Replace('\\', '/').TrimEnd('/');
}

/// <summary>Stores a string pref, deleting the key when the value is blank so defaults apply.</summary>
private static void SetOrDelete(string key, string value)
{
if (string.IsNullOrWhiteSpace(value)) EditorPrefs.DeleteKey(key);
else EditorPrefs.SetString(key, value.Trim());
}
}
}
11 changes: 11 additions & 0 deletions MCPForUnity/Editor/Helpers/BlenderBridgePrefs.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

87 changes: 84 additions & 3 deletions MCPForUnity/Editor/Helpers/BlenderDetection.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;

namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// 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.
/// </summary>
internal static class BlenderDetection
{
Expand Down Expand Up @@ -76,5 +78,84 @@ internal static IEnumerable<string> CandidatePaths()
}
return list;
}

/// <summary>
/// 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.
/// </summary>
internal static IEnumerable<string> UserConfigRoots()
{
var list = new List<string>();
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;
}

/// <summary>&lt;root&gt;/&lt;X.Y&gt;/scripts/addons for every versioned config folder, newest version first.</summary>
internal static IEnumerable<string> 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();
}

/// <summary>Newest user addons dir that already contains <paramref name="fileName"/>, else the newest one, else null.</summary>
internal static string FindUserAddonsDir(string fileName)
{
try { return PickAddonsDir(UserAddonsDirs(), File.Exists, fileName); }
catch { return null; }
}

/// <summary>Pure core of <see cref="FindUserAddonsDir"/>. Testable.</summary>
internal static string PickAddonsDir(IEnumerable<string> dirsNewestFirst, Func<string, bool> 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;
}

/// <summary>Parses a Blender version folder name ("4.2", "5.2") into a comparable Version; null if it is not one.</summary>
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;
}
}
}
64 changes: 64 additions & 0 deletions MCPForUnity/Editor/MenuItems/BlenderBridgeMenu.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
public static class BlenderBridgeMenu
{
private const string Root = ProductInfo.MenuRoot + "/Blender Bridge/";

/// <summary>Exports Blender's current selection as GLB and places it in the open scene.</summary>
[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" });
}

/// <summary>Exports Blender's whole scene as GLB and places it in the open scene.</summary>
[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" });
}

/// <summary>Captures Blender's viewport and reveals the PNG.</summary>
[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);
}

/// <summary>Opens the MCP for Unity window; the Blender Bridge panel is in its Generative tab.</summary>
[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.");
}

/// <summary>Runs one bridge action and logs its full result.</summary>
private static async Task<JObject> RunAsync(JObject parameters)
{
object result = await BlenderBridgeTool.HandleCommand(parameters);
JObject json = JObject.FromObject(result);
bool ok = json.Value<bool?>("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;
}
}
}
11 changes: 11 additions & 0 deletions MCPForUnity/Editor/MenuItems/BlenderBridgeMenu.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions MCPForUnity/Editor/Services/Blender.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading