13 KiB
Adding Custom Tools to MCP for Unity
MCP for Unity supports auto-discovery of custom tools using decorators (Python) and attributes (C#). This allows you to easily extend the MCP server with your own tools.
Be sure to review the developer README first:
| English | 简体中文 |
|---|
Part 1: How to Use (Quick Start Guide)
This section shows you how to add custom tools to your Unity project.
Step 1: Create a PythonToolsAsset
First, create a ScriptableObject to manage your Python tools:
- In Unity, right-click in the Project window
- Select Assets > Create > MCP For Unity > Python Tools
- Name it (e.g.,
MyPythonTools)
Step 2: Create Your Python Tool File
Create a Python file anywhere in your Unity project. For example, Assets/Editor/MyTools/my_custom_tool.py:
from typing import Annotated, Any
from fastmcp import Context
from registry import mcp_for_unity_tool
from unity_connection import send_command_with_retry
@mcp_for_unity_tool(
description="My custom tool that does something amazing"
)
async def my_custom_tool(
ctx: Context,
param1: Annotated[str, "Description of param1"],
param2: Annotated[int, "Description of param2"] | None = None
) -> dict[str, Any]:
await ctx.info(f"Processing my_custom_tool: {param1}")
# Prepare parameters for Unity
params = {
"action": "do_something",
"param1": param1,
"param2": param2,
}
params = {k: v for k, v in params.items() if v is not None}
# Send to Unity handler
response = send_command_with_retry("my_custom_tool", params)
return response if isinstance(response, dict) else {"success": False, "message": str(response)}
Step 3: Add Python File to Asset
- Select your
PythonToolsAssetin the Project window - In the Inspector, expand Python Files
- Drag your
.pyfile into the list (or click + and select it)
Note: If you can't see .py files in the object picker, go to Window > MCP For Unity > Tool Sync > Reimport Python Files to force Unity to recognize them as text assets.
Step 4: Create C# Handler
Create a C# file anywhere in your Unity project (typically in Editor/):
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Helpers;
namespace MyProject.Editor.CustomTools
{
[McpForUnityTool("my_custom_tool")]
public static class MyCustomTool
{
public static object HandleCommand(JObject @params)
{
string action = @params["action"]?.ToString();
string param1 = @params["param1"]?.ToString();
int? param2 = @params["param2"]?.ToObject<int?>();
// Your custom logic here
if (string.IsNullOrEmpty(param1))
{
return Response.Error("param1 is required");
}
// Do something amazing
DoSomethingAmazing(param1, param2);
return Response.Success("Custom tool executed successfully!");
}
private static void DoSomethingAmazing(string param1, int? param2)
{
// Your implementation
}
}
}
Step 5: Rebuild the MCP Server
- Open the MCP for Unity window in the Unity Editor
- Click Rebuild Server to apply your changes
- Your tool is now available to MCP clients!
What happens automatically:
- ✅ Python files are synced to the MCP server on Unity startup
- ✅ Python files are synced when modified (you would need to rebuild the server)
- ✅ C# handlers are discovered via reflection
- ✅ Tools are registered with the MCP server
Complete Example: Screenshot Tool
Here's a complete example showing how to create a screenshot capture tool.
Python File (Assets/Editor/ScreenShots/Python/screenshot_tool.py)
from typing import Annotated, Any
from fastmcp import Context
from registry import mcp_for_unity_tool
from unity_connection import send_command_with_retry
@mcp_for_unity_tool(
description="Capture screenshots in Unity, saving them as PNGs"
)
async def capture_screenshot(
ctx: Context,
filename: Annotated[str, "Screenshot filename without extension, e.g., screenshot_01"],
) -> dict[str, Any]:
await ctx.info(f"Capturing screenshot: {filename}")
params = {
"action": "capture",
"filename": filename,
}
params = {k: v for k, v in params.items() if v is not None}
response = send_command_with_retry("capture_screenshot", params)
return response if isinstance(response, dict) else {"success": False, "message": str(response)}
Add to PythonToolsAsset
- Select your
PythonToolsAsset - Add
screenshot_tool.pyto the Python Files list - The file will automatically sync to the MCP server
C# Handler (Assets/Editor/ScreenShots/CaptureScreenshotTool.cs)
using System.IO;
using Newtonsoft.Json.Linq;
using UnityEngine;
using MCPForUnity.Editor.Tools;
namespace MyProject.Editor.Tools
{
[McpForUnityTool("capture_screenshot")]
public static class CaptureScreenshotTool
{
public static object HandleCommand(JObject @params)
{
string filename = @params["filename"]?.ToString();
if (string.IsNullOrEmpty(filename))
{
return MCPForUnity.Editor.Helpers.Response.Error("filename is required");
}
try
{
string absolutePath = Path.Combine(Application.dataPath, "Screenshots", filename);
Directory.CreateDirectory(Path.GetDirectoryName(absolutePath));
// Find the main camera
Camera camera = Camera.main;
if (camera == null)
{
camera = Object.FindFirstObjectByType<Camera>();
}
if (camera == null)
{
return MCPForUnity.Editor.Helpers.Response.Error("No camera found in the scene");
}
// Create a RenderTexture
RenderTexture rt = new RenderTexture(Screen.width, Screen.height, 24);
camera.targetTexture = rt;
// Render the camera's view
camera.Render();
// Read pixels from the RenderTexture
RenderTexture.active = rt;
Texture2D screenshot = new Texture2D(Screen.width, Screen.height, TextureFormat.RGB24, false);
screenshot.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
screenshot.Apply();
// Clean up
camera.targetTexture = null;
RenderTexture.active = null;
Object.DestroyImmediate(rt);
// Save to file
byte[] bytes = screenshot.EncodeToPNG();
File.WriteAllBytes(absolutePath, bytes);
Object.DestroyImmediate(screenshot);
return MCPForUnity.Editor.Helpers.Response.Success($"Screenshot saved to {absolutePath}", new
{
path = absolutePath,
});
}
catch (System.Exception ex)
{
return MCPForUnity.Editor.Helpers.Response.Error($"Failed to capture screenshot: {ex.Message}");
}
}
}
}
Rebuild and Test
- Open the MCP for Unity window
- Click Rebuild Server
- Test your tool from your MCP client!
Part 2: How It Works (Technical Details)
This section explains the technical implementation of the custom tools system.
Python Side: Decorator System
The @mcp_for_unity_tool Decorator
The decorator automatically registers your function as an MCP tool:
@mcp_for_unity_tool(
name="custom_name", # Optional: function name used by default
description="Tool description", # Required: describe what the tool does
)
How it works:
- Auto-generates the tool name from the function name (e.g.,
my_custom_tool) - Registers the tool with FastMCP during module import
- Supports all FastMCP
mcp.tooldecorator options: https://gofastmcp.com/servers/tools#tools
Note: All tools should have the description field. It's not strictly required, however, that parameter is the best place to define a description so that most MCP clients can read it. See issue #289.
Auto-Discovery
Python tools are automatically discovered when:
- The Python file is added to a
PythonToolsAsset - The file is synced to
MCPForUnity/UnityMcpServer~/src/tools/custom/ - The file is imported during server startup
- The decorator
@mcp_for_unity_toolis used
Sync System
The PythonToolsAsset system automatically syncs your Python files:
When sync happens:
- ✅ Unity starts up
- ✅ Python files are modified
- ✅ Python files are added/removed from the asset
Manual controls:
- Sync Now: Window > MCP For Unity > Tool Sync > Sync Python Tools
- Toggle Auto-Sync: Window > MCP For Unity > Tool Sync > Auto-Sync Python Tools
- Reimport Python Files: Window > MCP For Unity > Tool Sync > Reimport Python Files
How it works:
- Uses content hashing to detect changes (only syncs modified files)
- Files are copied to
MCPForUnity/UnityMcpServer~/src/tools/custom/ - Stale files are automatically cleaned up
C# Side: Attribute System
The [McpForUnityTool] Attribute
The attribute marks your class as a tool handler:
// Explicit command name
[McpForUnityTool("my_custom_tool")]
public static class MyCustomTool { }
// Auto-generated from class name (MyCustomTool → my_custom_tool)
[McpForUnityTool]
public static class MyCustomTool { }
Auto-Discovery
C# handlers are automatically discovered when:
- The class has the
[McpForUnityTool]attribute - The class has a
public static HandleCommand(JObject)method - Unity loads the assembly containing the class
How it works:
- Unity scans all assemblies on startup
- Finds classes with
[McpForUnityTool]attribute - Registers them in the command registry
- Routes MCP commands to the appropriate handler
Best Practices
Python
- ✅ Use type hints with
Annotatedfor parameter documentation - ✅ Return
dict[str, Any]with{"success": bool, "message": str, "data": Any} - ✅ Use
ctx.info()for logging - ✅ Handle errors gracefully and return structured error responses
- ✅ Use
send_command_with_retry()for Unity communication
C#
- ✅ Use the
Response.Success()andResponse.Error()helper methods - ✅ Validate input parameters before processing
- ✅ Use
@params["key"]?.ToObject<Type>()for safe type conversion - ✅ Return structured responses with meaningful data
- ✅ Handle exceptions and return error responses
Debugging
Python
- Check server logs:
~/Library/Application Support/UnityMCP/Logs/unity_mcp_server.log - Look for:
"Registered X MCP tools"message on startup - Use
ctx.info()for debugging messages
C#
- Check Unity Console for:
"MCP-FOR-UNITY: Auto-discovered X tools"message - Look for warnings about missing
HandleCommandmethods - Use
Debug.Log()in your handler for debugging
Troubleshooting
Tool not appearing:
- Python:
- Ensure the
.pyfile is added to aPythonToolsAsset - Check Unity Console for sync messages: "Python tools synced: X copied"
- Verify file was synced to
UnityMcpServer~/src/tools/custom/ - Try manual sync: Window > MCP For Unity > Tool Sync > Sync Python Tools
- Rebuild the server in the MCP for Unity window
- Ensure the
- C#:
- Ensure the class has
[McpForUnityTool]attribute - Ensure the class has a
public static HandleCommand(JObject)method - Check Unity Console for: "MCP-FOR-UNITY: Auto-discovered X tools"
- Ensure the class has
Python files not showing in Inspector:
- Go to Window > MCP For Unity > Tool Sync > Reimport Python Files
- This forces Unity to recognize
.pyfiles as TextAssets - Check that
.py.metafiles showScriptedImporter(notDefaultImporter)
Sync not working:
- Check if auto-sync is enabled: Window > MCP For Unity > Tool Sync > Auto-Sync Python Tools
- Look for errors in Unity Console
- Verify
PythonToolsAssethas the correct files added
Name conflicts:
- Use explicit names in decorators/attributes to avoid conflicts
- Check registered tools:
CommandRegistry.GetAllCommandNames()in C#
Tool not being called:
- Verify the command name matches between Python and C#
- Check that parameters are being passed correctly
- Look for errors in logs

