Skip to content

Commit 5488af2

Browse files
Make it easier to add tools (#301)
* Add a decorate that wraps around the `mcp.tool` decorator. This will allow us to more easily collect tools * Register tools that's defined in the tools folder * Update Python tools to use new decorator * Convert script_apply_edits tool * Convert last remaining tools with new decorator * Create an attribute so we can identify tools via Reflection * Add attribute to all C# tools * Use reflection to load tools * Initialize command registry to load tools at startup * Update tests * Move Dev docs to docs folder * Add docs for adding custom tools * Update function docs for Python decorator * Add working example of adding a screenshot tool * docs: update relative links in README files Updated the relative links in both README-DEV.md and README-DEV-zh.md to use direct filenames instead of paths relative to the docs directory, improving link correctness when files are accessed from the root directory. * docs: update telemetry documentation path reference Updated the link to TELEMETRY.md in README.md to point to the new docs/ directory location to ensure users can access the telemetry documentation correctly. Also moved the TELEMETRY.md file to the docs/ directory as part of the documentation restructuring. * rename CursorHelp.md to docs/CURSOR_HELP.md Moved the CursorHelp.md file to the docs directory to better organize documentation files and improve project structure. * docs: update CUSTOM_TOOLS.md with improved tool naming documentation and path corrections - Clarified that the `name` argument in `@mcp_for_unity_tool` decorator is optional and defaults to the function name - Added documentation about using all FastMCP `mcp.tool` function decorator options - Updated class naming documentation to mention snake_case conversion by default - Corrected Python file path from `tools/screenshot_tool.py` to `UnityMcpServer~/src/tools/screenshot_tool.py` - Enhanced documentation for tool discovery and usage examples * docs: restructure development documentation and add custom tools guide Rearranged the development section in README.md to better organize the documentation flow. Added a dedicated section for "Adding Custom Tools" with a link to the new CUSTOM_TOOLS.md file, and renamed the previous "For Developers" section to "Contributing to the Project" to better reflect its content. This improves discoverability and organization of the development setup documentation. * docs: update developer documentation and add README links - Added links to developer READMEs in CUSTOM_TOOLS.md to guide users to the appropriate documentation - Fixed typo in README-DEV.md ("roote" → "root") for improved clarity - These changes improve the developer experience by providing better documentation navigation and correcting technical inaccuracies * feat(tools): enhance tool registration with wrapped function assignment Updated the tool registration process to properly chain the mcp.tool decorator and telemetry wrapper, ensuring the wrapped function is correctly assigned to tool_info['func'] for proper tool execution and telemetry tracking. This change improves the reliability of tool registration and monitoring. * Remove AI generated code that was never used... * feat: Rebuild MCP server installation with embedded source Refactored the server repair logic to implement a full rebuild of the MCP server installation using the embedded source. The new RebuildMcpServer method now: - Uses embedded server source instead of attempting repair of existing installation - Deletes the entire existing server directory before re-copying - Handles UV process cleanup for the target path - Simplifies the installation flow by removing the complex Python environment repair logic - Maintains the same installation behavior but with a cleaner, more reliable rebuild approach This change improves reliability of server installations by ensuring a clean slate rebuild rather than attempting to repair potentially corrupted environments. * Add the rebuild server step * docs: clarify tool description field requirements and client compatibility * fix: move initialization flag after tool discovery to prevent race conditions * refactor: remove redundant TryParseVersion overrides in platform detectors * refactor: remove duplicate UV validation code from platform detectors * Update UnityMcpBridge/Editor/Tools/CommandRegistry.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * refactor: replace WriteToConfig reflection with direct McpConfigurationHelper call --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 7c23f24 commit 5488af2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2894
-2739
lines changed

README-zh.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,15 +270,19 @@ claude mcp add UnityMCP -- "C:/Users/USERNAME/AppData/Local/Microsoft/WinGet/Lin
270270

271271
## 开发和贡献 🛠️
272272

273-
### 开发者
273+
### 添加自定义工具
274+
275+
MCP for Unity 使用与 Unity 的 C# 脚本绑定的 Python MCP 服务器来实现工具功能。如果您想使用自己的工具扩展功能,请参阅 **[CUSTOM_TOOLS.md](docs/CUSTOM_TOOLS.md)** 了解如何操作。
276+
277+
### 贡献项目
274278

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

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

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

283287
### 贡献 🤝
284288

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

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

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

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,15 +273,19 @@ On Windows, set `command` to the absolute shim, e.g. `C:\\Users\\YOU\\AppData\\L
273273

274274
## Development & Contributing 🛠️
275275

276-
### For Developers
276+
### Adding Custom Tools
277+
278+
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)**.
279+
280+
### Contributing to the Project
277281

278282
If you're contributing to MCP for Unity or want to test core changes, we have development tools to streamline your workflow:
279283
280284
- **Development Deployment Scripts**: Quickly deploy and test your changes to MCP for Unity Bridge and Python Server
281285
- **Automatic Backup System**: Safe testing with easy rollback capabilities
282286
- **Hot Reload Workflow**: Fast iteration cycle for core development
283287
284-
📖 **See [README-DEV.md](README-DEV.md)** for complete development setup and workflow documentation.
288+
📖 **See [README-DEV.md](docs/README-DEV.md)** for complete development setup and workflow documentation.
285289
286290
### Contributing 🤝
287291
@@ -302,7 +306,7 @@ Unity MCP includes **privacy-focused, anonymous telemetry** to help us improve t
302306
303307
- **🔒 Anonymous**: Random UUIDs only, no personal data
304308
- **🚫 Easy opt-out**: Set `DISABLE_TELEMETRY=true` environment variable
305-
- **📖 Transparent**: See [TELEMETRY.md](TELEMETRY.md) for full details
309+
- **📖 Transparent**: See [TELEMETRY.md](docs/TELEMETRY.md) for full details
306310
307311
Your privacy matters to us. All telemetry is optional and designed to respect your workflow.
308312

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/WriteToConfigTests.cs renamed to TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/WriteToConfigTests.cs

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
using System;
22
using System.Diagnostics;
33
using System.IO;
4-
using System.Reflection;
54
using System.Runtime.InteropServices;
65
using Newtonsoft.Json.Linq;
76
using NUnit.Framework;
87
using UnityEditor;
9-
using UnityEngine;
10-
using MCPForUnity.Editor.Data;
8+
using MCPForUnity.Editor.Helpers;
119
using MCPForUnity.Editor.Models;
12-
using MCPForUnity.Editor.Windows;
1310

14-
namespace MCPForUnityTests.Editor.Windows
11+
namespace MCPForUnityTests.Editor.Helpers
1512
{
1613
public class WriteToConfigTests
1714
{
@@ -68,7 +65,7 @@ public void TearDown()
6865
public void AddsEnvAndDisabledFalse_ForWindsurf()
6966
{
7067
var configPath = Path.Combine(_tempRoot, "windsurf.json");
71-
WriteInitialConfig(configPath, isVSCode:false, command:_fakeUvPath, directory:"/old/path");
68+
WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
7269

7370
var client = new McpClient { name = "Windsurf", mcpType = McpTypes.Windsurf };
7471
InvokeWriteToConfig(configPath, client);
@@ -85,7 +82,7 @@ public void AddsEnvAndDisabledFalse_ForWindsurf()
8582
public void AddsEnvAndDisabledFalse_ForKiro()
8683
{
8784
var configPath = Path.Combine(_tempRoot, "kiro.json");
88-
WriteInitialConfig(configPath, isVSCode:false, command:_fakeUvPath, directory:"/old/path");
85+
WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
8986

9087
var client = new McpClient { name = "Kiro", mcpType = McpTypes.Kiro };
9188
InvokeWriteToConfig(configPath, client);
@@ -102,7 +99,7 @@ public void AddsEnvAndDisabledFalse_ForKiro()
10299
public void DoesNotAddEnvOrDisabled_ForCursor()
103100
{
104101
var configPath = Path.Combine(_tempRoot, "cursor.json");
105-
WriteInitialConfig(configPath, isVSCode:false, command:_fakeUvPath, directory:"/old/path");
102+
WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
106103

107104
var client = new McpClient { name = "Cursor", mcpType = McpTypes.Cursor };
108105
InvokeWriteToConfig(configPath, client);
@@ -118,7 +115,7 @@ public void DoesNotAddEnvOrDisabled_ForCursor()
118115
public void DoesNotAddEnvOrDisabled_ForVSCode()
119116
{
120117
var configPath = Path.Combine(_tempRoot, "vscode.json");
121-
WriteInitialConfig(configPath, isVSCode:true, command:_fakeUvPath, directory:"/old/path");
118+
WriteInitialConfig(configPath, isVSCode: true, command: _fakeUvPath, directory: "/old/path");
122119

123120
var client = new McpClient { name = "VSCode", mcpType = McpTypes.VSCode };
124121
InvokeWriteToConfig(configPath, client);
@@ -219,25 +216,15 @@ private static void WriteInitialConfig(string configPath, bool isVSCode, string
219216
File.WriteAllText(configPath, root.ToString());
220217
}
221218

222-
private static MCPForUnityEditorWindow CreateWindow()
223-
{
224-
return ScriptableObject.CreateInstance<MCPForUnityEditorWindow>();
225-
}
226-
227219
private static void InvokeWriteToConfig(string configPath, McpClient client)
228220
{
229-
var window = CreateWindow();
230-
var mi = typeof(MCPForUnityEditorWindow).GetMethod("WriteToConfig", BindingFlags.Instance | BindingFlags.NonPublic);
231-
Assert.NotNull(mi, "Could not find WriteToConfig via reflection");
232-
233-
// pythonDir is unused by WriteToConfig, but pass server src to keep it consistent
234-
var result = (string)mi!.Invoke(window, new object[] {
235-
/* pythonDir */ string.Empty,
236-
/* configPath */ configPath,
237-
/* mcpClient */ client
238-
});
239-
240-
Assert.AreEqual("Configured successfully", result, "WriteToConfig should return success");
221+
var result = McpConfigurationHelper.WriteMcpConfiguration(
222+
pythonDir: string.Empty,
223+
configPath: configPath,
224+
mcpClient: client
225+
);
226+
227+
Assert.AreEqual("Configured successfully", result, "WriteMcpConfiguration should return success");
241228
}
242229
}
243230
}
Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
24
using Newtonsoft.Json;
5+
using Newtonsoft.Json.Linq;
36
using NUnit.Framework;
47
using MCPForUnity.Editor.Tools;
58

@@ -8,34 +11,41 @@ namespace MCPForUnityTests.Editor.Tools
811
public class CommandRegistryTests
912
{
1013
[Test]
11-
public void GetHandler_ThrowException_ForUnknownCommand()
14+
public void GetHandler_ThrowsException_ForUnknownCommand()
1215
{
13-
var unknown = "HandleDoesNotExist";
14-
try
15-
{
16-
var handler = CommandRegistry.GetHandler(unknown);
17-
Assert.Fail("Should throw InvalidOperation for unknown handler.");
18-
}
19-
catch (InvalidOperationException)
20-
{
16+
var unknown = "nonexistent_command_that_should_not_exist";
2117

22-
}
23-
catch
18+
Assert.Throws<InvalidOperationException>(() =>
2419
{
25-
Assert.Fail("Should throw InvalidOperation for unknown handler.");
26-
}
20+
CommandRegistry.GetHandler(unknown);
21+
}, "Should throw InvalidOperationException for unknown handler");
2722
}
2823

2924
[Test]
30-
public void GetHandler_ReturnsManageGameObjectHandler()
25+
public void AutoDiscovery_RegistersAllBuiltInTools()
3126
{
32-
var handler = CommandRegistry.GetHandler("manage_gameobject");
33-
Assert.IsNotNull(handler, "Expected a handler for manage_gameobject.");
27+
// Verify that all expected built-in tools are registered by trying to get their handlers
28+
var expectedTools = new[]
29+
{
30+
"manage_asset",
31+
"manage_editor",
32+
"manage_gameobject",
33+
"manage_scene",
34+
"manage_script",
35+
"manage_shader",
36+
"read_console",
37+
"manage_menu_item",
38+
"manage_prefabs"
39+
};
3440

35-
var methodInfo = handler.Method;
36-
Assert.AreEqual("HandleCommand", methodInfo.Name, "Handler method name should be HandleCommand.");
37-
Assert.AreEqual(typeof(ManageGameObject), methodInfo.DeclaringType, "Handler should be declared on ManageGameObject.");
38-
Assert.IsNull(handler.Target, "Handler should be a static method (no target instance).");
41+
foreach (var toolName in expectedTools)
42+
{
43+
Assert.DoesNotThrow(() =>
44+
{
45+
var handler = CommandRegistry.GetHandler(toolName);
46+
Assert.IsNotNull(handler, $"Handler for '{toolName}' should not be null");
47+
}, $"Expected tool '{toolName}' to be auto-registered");
48+
}
3949
}
4050
}
4151
}

UnityMcpBridge/Editor/Dependencies/DependencyManager.cs

Lines changed: 0 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -80,69 +80,6 @@ public static DependencyCheckResult CheckAllDependencies()
8080
return result;
8181
}
8282

83-
/// <summary>
84-
/// Quick check if system is ready for MCP operations
85-
/// </summary>
86-
public static bool IsSystemReady()
87-
{
88-
try
89-
{
90-
var result = CheckAllDependencies();
91-
return result.IsSystemReady;
92-
}
93-
catch
94-
{
95-
return false;
96-
}
97-
}
98-
99-
/// <summary>
100-
/// Get a summary of missing dependencies
101-
/// </summary>
102-
public static string GetMissingDependenciesSummary()
103-
{
104-
try
105-
{
106-
var result = CheckAllDependencies();
107-
var missing = result.GetMissingRequired();
108-
109-
if (missing.Count == 0)
110-
{
111-
return "All required dependencies are available.";
112-
}
113-
114-
var names = missing.Select(d => d.Name).ToArray();
115-
return $"Missing required dependencies: {string.Join(", ", names)}";
116-
}
117-
catch (Exception ex)
118-
{
119-
return $"Error checking dependencies: {ex.Message}";
120-
}
121-
}
122-
123-
/// <summary>
124-
/// Check if a specific dependency is available
125-
/// </summary>
126-
public static bool IsDependencyAvailable(string dependencyName)
127-
{
128-
try
129-
{
130-
var detector = GetCurrentPlatformDetector();
131-
132-
return dependencyName.ToLowerInvariant() switch
133-
{
134-
"python" => detector.DetectPython().IsAvailable,
135-
"uv" => detector.DetectUV().IsAvailable,
136-
"mcpserver" or "mcp-server" => detector.DetectMCPServer().IsAvailable,
137-
_ => false
138-
};
139-
}
140-
catch
141-
{
142-
return false;
143-
}
144-
}
145-
14683
/// <summary>
14784
/// Get installation recommendations for the current platform
14885
/// </summary>
@@ -175,104 +112,6 @@ public static (string pythonUrl, string uvUrl) GetInstallationUrls()
175112
}
176113
}
177114

178-
/// <summary>
179-
/// Validate that the MCP server can be started
180-
/// </summary>
181-
public static bool ValidateMCPServerStartup()
182-
{
183-
try
184-
{
185-
// Check if Python and UV are available
186-
if (!IsDependencyAvailable("python") || !IsDependencyAvailable("uv"))
187-
{
188-
return false;
189-
}
190-
191-
// Try to ensure server is installed
192-
ServerInstaller.EnsureServerInstalled();
193-
194-
// Check if server files exist
195-
var serverStatus = GetCurrentPlatformDetector().DetectMCPServer();
196-
return serverStatus.IsAvailable;
197-
}
198-
catch (Exception ex)
199-
{
200-
McpLog.Error($"Error validating MCP server startup: {ex.Message}");
201-
return false;
202-
}
203-
}
204-
205-
/// <summary>
206-
/// Attempt to repair the Python environment
207-
/// </summary>
208-
public static bool RepairPythonEnvironment()
209-
{
210-
try
211-
{
212-
McpLog.Info("Attempting to repair Python environment...");
213-
return ServerInstaller.RepairPythonEnvironment();
214-
}
215-
catch (Exception ex)
216-
{
217-
McpLog.Error($"Error repairing Python environment: {ex.Message}");
218-
return false;
219-
}
220-
}
221-
222-
/// <summary>
223-
/// Get detailed dependency information for diagnostics
224-
/// </summary>
225-
public static string GetDependencyDiagnostics()
226-
{
227-
try
228-
{
229-
var result = CheckAllDependencies();
230-
var detector = GetCurrentPlatformDetector();
231-
232-
var diagnostics = new System.Text.StringBuilder();
233-
diagnostics.AppendLine($"Platform: {detector.PlatformName}");
234-
diagnostics.AppendLine($"Check Time: {result.CheckedAt:yyyy-MM-dd HH:mm:ss} UTC");
235-
diagnostics.AppendLine($"System Ready: {result.IsSystemReady}");
236-
diagnostics.AppendLine();
237-
238-
foreach (var dep in result.Dependencies)
239-
{
240-
diagnostics.AppendLine($"=== {dep.Name} ===");
241-
diagnostics.AppendLine($"Available: {dep.IsAvailable}");
242-
diagnostics.AppendLine($"Required: {dep.IsRequired}");
243-
244-
if (!string.IsNullOrEmpty(dep.Version))
245-
diagnostics.AppendLine($"Version: {dep.Version}");
246-
247-
if (!string.IsNullOrEmpty(dep.Path))
248-
diagnostics.AppendLine($"Path: {dep.Path}");
249-
250-
if (!string.IsNullOrEmpty(dep.Details))
251-
diagnostics.AppendLine($"Details: {dep.Details}");
252-
253-
if (!string.IsNullOrEmpty(dep.ErrorMessage))
254-
diagnostics.AppendLine($"Error: {dep.ErrorMessage}");
255-
256-
diagnostics.AppendLine();
257-
}
258-
259-
if (result.RecommendedActions.Count > 0)
260-
{
261-
diagnostics.AppendLine("=== Recommended Actions ===");
262-
foreach (var action in result.RecommendedActions)
263-
{
264-
diagnostics.AppendLine($"- {action}");
265-
}
266-
}
267-
268-
return diagnostics.ToString();
269-
}
270-
catch (Exception ex)
271-
{
272-
return $"Error generating diagnostics: {ex.Message}";
273-
}
274-
}
275-
276115
private static void GenerateRecommendations(DependencyCheckResult result, IPlatformDetector detector)
277116
{
278117
var missing = result.GetMissingDependencies();

0 commit comments

Comments
 (0)