Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
391a0d9
Add a decorate that wraps around the `mcp.tool` decorator.
msanatan Oct 1, 2025
5c74980
Register tools that's defined in the tools folder
msanatan Oct 1, 2025
00ccf14
Update Python tools to use new decorator
msanatan Oct 1, 2025
31ce85e
Convert script_apply_edits tool
msanatan Oct 1, 2025
5f1ab98
Convert last remaining tools with new decorator
msanatan Oct 1, 2025
0821309
Create an attribute so we can identify tools via Reflection
msanatan Oct 1, 2025
d036e36
Add attribute to all C# tools
msanatan Oct 1, 2025
c84069b
Use reflection to load tools
msanatan Oct 2, 2025
b7f2070
Initialize command registry to load tools at startup
msanatan Oct 2, 2025
2e9aa06
Update tests
msanatan Oct 2, 2025
084c27e
Move Dev docs to docs folder
msanatan Oct 2, 2025
bc5695e
Add docs for adding custom tools
msanatan Oct 2, 2025
f154e43
Update function docs for Python decorator
msanatan Oct 2, 2025
1e13517
Add working example of adding a screenshot tool
msanatan Oct 2, 2025
bf6480c
docs: update relative links in README files
msanatan Oct 2, 2025
2f87357
docs: update telemetry documentation path reference
msanatan Oct 2, 2025
8173e01
rename CursorHelp.md to docs/CURSOR_HELP.md
msanatan Oct 2, 2025
d46b0e6
docs: update CUSTOM_TOOLS.md with improved tool naming documentation …
msanatan Oct 2, 2025
7b5c156
docs: restructure development documentation and add custom tools guide
msanatan Oct 2, 2025
5a65781
Merge branch 'main' into feature/auto-tool-discovery
msanatan Oct 3, 2025
0d07efd
docs: update developer documentation and add README links
msanatan Oct 3, 2025
f79fda6
feat(tools): enhance tool registration with wrapped function assignment
msanatan Oct 3, 2025
d8fd19d
Remove AI generated code that was never used...
msanatan Oct 3, 2025
a2d76b6
feat: Rebuild MCP server installation with embedded source
msanatan Oct 3, 2025
8e7b202
Add the rebuild server step
msanatan Oct 3, 2025
1d5291e
docs: clarify tool description field requirements and client compatib…
msanatan Oct 3, 2025
7db81f1
fix: move initialization flag after tool discovery to prevent race co…
msanatan Oct 3, 2025
3d107e5
refactor: remove redundant TryParseVersion overrides in platform dete…
msanatan Oct 3, 2025
4314e2a
refactor: remove duplicate UV validation code from platform detectors
msanatan Oct 3, 2025
fe13260
Update UnityMcpBridge/Editor/Tools/CommandRegistry.cs
msanatan Oct 3, 2025
2af3413
refactor: replace WriteToConfig reflection with direct McpConfigurati…
msanatan Oct 3, 2025
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
10 changes: 7 additions & 3 deletions README-zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,15 +270,19 @@ claude mcp add UnityMCP -- "C:/Users/USERNAME/AppData/Local/Microsoft/WinGet/Lin

## 开发和贡献 🛠️

### 开发者
### 添加自定义工具

MCP for Unity 使用与 Unity 的 C# 脚本绑定的 Python MCP 服务器来实现工具功能。如果您想使用自己的工具扩展功能,请参阅 **[CUSTOM_TOOLS.md](docs/CUSTOM_TOOLS.md)** 了解如何操作。

### 贡献项目

如果您正在为 MCP for Unity 做贡献或想要测试核心更改,我们有开发工具来简化您的工作流程:

- **开发部署脚本**:快速部署和测试您对 MCP for Unity Bridge 和 Python 服务器的更改
- **自动备份系统**:具有简单回滚功能的安全测试
- **热重载工作流程**:核心开发的快速迭代周期

📖 **查看 [README-DEV.md](README-DEV.md)** 获取完整的开发设置和工作流程文档。
📖 **查看 [README-DEV.md](docs/README-DEV.md)** 获取完整的开发设置和工作流程文档。

### 贡献 🤝

Expand All @@ -299,7 +303,7 @@ Unity MCP 包含**注重隐私的匿名遥测**来帮助我们改进产品。我

- **🔒 匿名**:仅随机 UUID,无个人数据
- **🚫 轻松退出**:设置 `DISABLE_TELEMETRY=true` 环境变量
- **📖 透明**:查看 [TELEMETRY.md](TELEMETRY.md) 获取完整详情
- **📖 透明**:查看 [TELEMETRY.md](docs/TELEMETRY.md) 获取完整详情

您的隐私对我们很重要。所有遥测都是可选的,旨在尊重您的工作流程。

Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,15 +273,19 @@ On Windows, set `command` to the absolute shim, e.g. `C:\\Users\\YOU\\AppData\\L

## Development & Contributing 🛠️

### For Developers
### Adding Custom Tools

MCP for Unity uses a Python MCP Server tied with Unity's C# scripts for tools. If you'd like to extend the functionality with your own tools, learn how to do so in **[CUSTOM_TOOLS.md](docs/CUSTOM_TOOLS.md)**.

### Contributing to the Project

If you're contributing to MCP for Unity or want to test core changes, we have development tools to streamline your workflow:

- **Development Deployment Scripts**: Quickly deploy and test your changes to MCP for Unity Bridge and Python Server
- **Automatic Backup System**: Safe testing with easy rollback capabilities
- **Hot Reload Workflow**: Fast iteration cycle for core development

📖 **See [README-DEV.md](README-DEV.md)** for complete development setup and workflow documentation.
📖 **See [README-DEV.md](docs/README-DEV.md)** for complete development setup and workflow documentation.

### Contributing 🤝

Expand All @@ -302,7 +306,7 @@ Unity MCP includes **privacy-focused, anonymous telemetry** to help us improve t

- **🔒 Anonymous**: Random UUIDs only, no personal data
- **🚫 Easy opt-out**: Set `DISABLE_TELEMETRY=true` environment variable
- **📖 Transparent**: See [TELEMETRY.md](TELEMETRY.md) for full details
- **📖 Transparent**: See [TELEMETRY.md](docs/TELEMETRY.md) for full details

Your privacy matters to us. All telemetry is optional and designed to respect your workflow.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using MCPForUnity.Editor.Tools;

Expand All @@ -8,34 +11,41 @@ namespace MCPForUnityTests.Editor.Tools
public class CommandRegistryTests
{
[Test]
public void GetHandler_ThrowException_ForUnknownCommand()
public void GetHandler_ThrowsException_ForUnknownCommand()
{
var unknown = "HandleDoesNotExist";
try
{
var handler = CommandRegistry.GetHandler(unknown);
Assert.Fail("Should throw InvalidOperation for unknown handler.");
}
catch (InvalidOperationException)
{
var unknown = "nonexistent_command_that_should_not_exist";

}
catch
Assert.Throws<InvalidOperationException>(() =>
{
Assert.Fail("Should throw InvalidOperation for unknown handler.");
}
CommandRegistry.GetHandler(unknown);
}, "Should throw InvalidOperationException for unknown handler");
}

[Test]
public void GetHandler_ReturnsManageGameObjectHandler()
public void AutoDiscovery_RegistersAllBuiltInTools()
{
var handler = CommandRegistry.GetHandler("manage_gameobject");
Assert.IsNotNull(handler, "Expected a handler for manage_gameobject.");
// Verify that all expected built-in tools are registered by trying to get their handlers
var expectedTools = new[]
{
"manage_asset",
"manage_editor",
"manage_gameobject",
"manage_scene",
"manage_script",
"manage_shader",
"read_console",
"manage_menu_item",
"manage_prefabs"
};

var methodInfo = handler.Method;
Assert.AreEqual("HandleCommand", methodInfo.Name, "Handler method name should be HandleCommand.");
Assert.AreEqual(typeof(ManageGameObject), methodInfo.DeclaringType, "Handler should be declared on ManageGameObject.");
Assert.IsNull(handler.Target, "Handler should be a static method (no target instance).");
foreach (var toolName in expectedTools)
{
Assert.DoesNotThrow(() =>
{
var handler = CommandRegistry.GetHandler(toolName);
Assert.IsNotNull(handler, $"Handler for '{toolName}' should not be null");
}, $"Expected tool '{toolName}' to be auto-registered");
}
}
}
}
1 change: 1 addition & 0 deletions UnityMcpBridge/Editor/MCPForUnityBridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ public static void Start()
// Start background listener with cooperative cancellation
cts = new CancellationTokenSource();
listenerTask = Task.Run(() => ListenerLoopAsync(cts.Token));
CommandRegistry.Initialize();
EditorApplication.update += ProcessCommands;
// Ensure lifecycle events are (re)subscribed in case Stop() removed them earlier in-domain
try { AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload; } catch { }
Expand Down
131 changes: 105 additions & 26 deletions UnityMcpBridge/Editor/Tools/CommandRegistry.cs
Original file line number Diff line number Diff line change
@@ -1,50 +1,129 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Tools.MenuItems;
using MCPForUnity.Editor.Tools.Prefabs;

namespace MCPForUnity.Editor.Tools
{
/// <summary>
/// Registry for all MCP command handlers (Refactored Version)
/// Registry for all MCP command handlers via reflection.
/// </summary>
public static class CommandRegistry
{
// Maps command names (matching those called from Python via ctx.bridge.unity_editor.HandlerName)
// to the corresponding static HandleCommand method in the appropriate tool class.
private static readonly Dictionary<string, Func<JObject, object>> _handlers = new()
private static readonly Dictionary<string, Func<JObject, object>> _handlers = new();
private static bool _initialized = false;

/// <summary>
/// Initialize and auto-discover all tools marked with [McpForUnityTool]
/// </summary>
public static void Initialize()
{
{ "manage_script", ManageScript.HandleCommand },
{ "manage_scene", ManageScene.HandleCommand },
{ "manage_editor", ManageEditor.HandleCommand },
{ "manage_gameobject", ManageGameObject.HandleCommand },
{ "manage_asset", ManageAsset.HandleCommand },
{ "read_console", ReadConsole.HandleCommand },
{ "manage_menu_item", ManageMenuItem.HandleCommand },
{ "manage_shader", ManageShader.HandleCommand},
{ "manage_prefabs", ManagePrefabs.HandleCommand},
};
if (_initialized) return;
_initialized = true;

AutoDiscoverTools();
}

/// <summary>
/// Gets a command handler by name.
/// Convert PascalCase or camelCase to snake_case
/// </summary>
/// <param name="commandName">Name of the command handler (e.g., "HandleManageAsset").</param>
/// <returns>The command handler function if found, null otherwise.</returns>
public static Func<JObject, object> GetHandler(string commandName)
private static string ToSnakeCase(string name)
{
if (!_handlers.TryGetValue(commandName, out var handler))
if (string.IsNullOrEmpty(name)) return name;

// Insert underscore before uppercase letters (except first)
var s1 = Regex.Replace(name, "(.)([A-Z][a-z]+)", "$1_$2");
var s2 = Regex.Replace(s1, "([a-z0-9])([A-Z])", "$1_$2");
return s2.ToLower();
}

/// <summary>
/// Auto-discover all types with [McpForUnityTool] attribute
/// </summary>
private static void AutoDiscoverTools()
{
try
{
throw new InvalidOperationException(
$"Unknown or unsupported command type: {commandName}");
var toolTypes = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic)
.SelectMany(a =>
{
try { return a.GetTypes(); }
catch { return new Type[0]; }
})
.Where(t => t.GetCustomAttribute<McpForUnityToolAttribute>() != null);

foreach (var type in toolTypes)
{
RegisterToolType(type);
}

McpLog.Info($"Auto-discovered {_handlers.Count} tools");
}
catch (Exception ex)
{
McpLog.Error($"Failed to auto-discover MCP tools: {ex.Message}");
}
}

return handler;
private static void RegisterToolType(Type type)
{
var attr = type.GetCustomAttribute<McpForUnityToolAttribute>();

// Get command name (explicit or auto-generated)
string commandName = attr.CommandName;
if (string.IsNullOrEmpty(commandName))
{
commandName = ToSnakeCase(type.Name);
}

// Find HandleCommand method
var method = type.GetMethod(
"HandleCommand",
BindingFlags.Public | BindingFlags.Static,
null,
new[] { typeof(JObject) },
null
);

if (method == null)
{
McpLog.Warn(
$"MCP tool {type.Name} is marked with [McpForUnityTool] " +
$"but has no public static HandleCommand(JObject) method"
);
return;
}

try
{
var handler = (Func<JObject, object>)Delegate.CreateDelegate(
typeof(Func<JObject, object>),
method
);
_handlers[commandName] = handler;
}
catch (Exception ex)
{
McpLog.Error($"Failed to register tool {type.Name}: {ex.Message}");
}
}

public static void Add(string commandName, Func<JObject, object> handler)
/// <summary>
/// Get a command handler by name
/// </summary>
public static Func<JObject, object> GetHandler(string commandName)
{
_handlers.Add(commandName, handler);
if (!_handlers.TryGetValue(commandName, out var handler))
{
throw new InvalidOperationException(
$"Unknown or unsupported command type: {commandName}"
);
}
return handler;
}
}
}
1 change: 1 addition & 0 deletions UnityMcpBridge/Editor/Tools/ManageAsset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ namespace MCPForUnity.Editor.Tools
/// <summary>
/// Handles asset management operations within the Unity project.
/// </summary>
[McpForUnityTool("manage_asset")]
public static class ManageAsset
{
// --- Main Handler ---
Expand Down
1 change: 1 addition & 0 deletions UnityMcpBridge/Editor/Tools/ManageEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ namespace MCPForUnity.Editor.Tools
/// Handles operations related to controlling and querying the Unity Editor state,
/// including managing Tags and Layers.
/// </summary>
[McpForUnityTool("manage_editor")]
public static class ManageEditor
{
// Constant for starting user layer index
Expand Down
1 change: 1 addition & 0 deletions UnityMcpBridge/Editor/Tools/ManageGameObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ namespace MCPForUnity.Editor.Tools
/// <summary>
/// Handles GameObject manipulation within the current scene (CRUD, find, components).
/// </summary>
[McpForUnityTool("manage_gameobject")]
public static class ManageGameObject
{
// Shared JsonSerializer to avoid per-call allocation overhead
Expand Down
1 change: 1 addition & 0 deletions UnityMcpBridge/Editor/Tools/ManageScene.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace MCPForUnity.Editor.Tools
/// <summary>
/// Handles scene management operations like loading, saving, creating, and querying hierarchy.
/// </summary>
[McpForUnityTool("manage_scene")]
public static class ManageScene
{
private sealed class SceneCommand
Expand Down
1 change: 1 addition & 0 deletions UnityMcpBridge/Editor/Tools/ManageScript.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ namespace MCPForUnity.Editor.Tools
/// Note: Without Roslyn, the system falls back to basic structural validation.
/// Roslyn provides full C# compiler diagnostics with line numbers and detailed error messages.
/// </summary>
[McpForUnityTool("manage_script")]
public static class ManageScript
{
/// <summary>
Expand Down
1 change: 1 addition & 0 deletions UnityMcpBridge/Editor/Tools/ManageShader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace MCPForUnity.Editor.Tools
/// <summary>
/// Handles CRUD operations for shader files within the Unity project.
/// </summary>
[McpForUnityTool("manage_shader")]
public static class ManageShader
{
/// <summary>
Expand Down
37 changes: 37 additions & 0 deletions UnityMcpBridge/Editor/Tools/McpForUnityToolAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;

namespace MCPForUnity.Editor.Tools
{
/// <summary>
/// Marks a class as an MCP tool handler for auto-discovery.
/// The class must have a public static HandleCommand(JObject) method.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class McpForUnityToolAttribute : Attribute
{
/// <summary>
/// The command name used to route requests to this tool.
/// If not specified, defaults to the PascalCase class name converted to snake_case.
/// </summary>
public string CommandName { get; }

/// <summary>
/// Create an MCP tool attribute with auto-generated command name.
/// The command name will be derived from the class name (PascalCase → snake_case).
/// Example: ManageAsset → manage_asset
/// </summary>
public McpForUnityToolAttribute()
{
CommandName = null; // Will be auto-generated
}

/// <summary>
/// Create an MCP tool attribute with explicit command name.
/// </summary>
/// <param name="commandName">The command name (e.g., "manage_asset")</param>
public McpForUnityToolAttribute(string commandName)
{
CommandName = commandName;
}
}
}
11 changes: 11 additions & 0 deletions UnityMcpBridge/Editor/Tools/McpForUnityToolAttribute.cs.meta

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

Loading