Unity MCP 自定义工具开发指南:从反射发现到长任务轮询的完整实现

📅 发布时间:2026/9/14 6:11:48
Unity MCP 自定义工具开发指南:从反射发现到长任务轮询的完整实现
Unity MCP 自定义工具开发指南从反射发现到长任务轮询的完整实现【免费下载链接】unity-mcpUnity MCP acts as a bridge between AI assistants and your Unity Editor. Give your LLM tools to manage assets, control scenes, edit scripts, and automate tasks within Unity.项目地址: https://gitcode.com/GitHub_Trending/un/unity-mcpMCP for Unity 通过 C# 特性标注与反射扫描让开发者无需任何手动配置即可把自定义能力注册给 AI 助手。本文以 website/docs/guides/custom-tools.md 为主线结合仓库内 McpForUnityToolAttribute.cs、ToolDiscoveryService.cs、custom_tool_service.py 等源码与 CLI 实现完整讲解如何编写同步工具、截图工具与支持 domain reload 的长任务轮询工具并深入剖析背后的发现、注册、路由与轮询协议。一、快速开始写出你的第一个自定义工具MCP for Unity 的自定义工具遵循零配置设计你只需要把 C# 文件放进 Unity 项目的任意Editor/文件夹系统会在运行时通过反射自动扫描并注册。Editor 文件夹是硬性要求——系统只扫描 Editor 程序集放在其他地方的工具永远不会被发现。每个工具由两个核心要素组成[McpForUnityTool]特性——向系统声明我是一个 MCP 工具一个HandleCommand(JObject)静态方法——承担实际工作。同时可以定义一个嵌套的Parameters类并用[ToolParameter]标注每个参数这样 AI 助手就能获得参数描述与必填信息从而正确生成调用参数。using Newtonsoft.Json.Linq; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Tools; namespace MyProject.Editor.CustomTools { [McpForUnityTool(my_custom_tool)] public static class MyCustomTool { public class Parameters { [ToolParameter(Value to process)] public string param1 { get; set; } [ToolParameter(Optional integer payload, Required false)] public int? param2 { get; set; } } public static object HandleCommand(JObject params) { var parameters params.ToObjectParameters(); if (string.IsNullOrEmpty(parameters.param1)) { return new ErrorResponse(param1 is required); } DoSomethingAmazing(parameters.param1, parameters.param2); return new SuccessResponse(Custom tool executed successfully!, new { parameters.param1, parameters.param2 }); } private static void DoSomethingAmazing(string param1, int? param2) { // Your implementation } } }把文件放到Editor/下并等待 Unity 编译完成后工具即可被服务端发现。二、工具属性全解McpForUnityTool 的每一个开关特性定义位于 McpForUnityToolAttribute.cs。除了文档中的基础用法源码揭示了更多可控属性属性默认值作用Name或兼容的CommandName由类名自动生成工具名不填时把类名PascalCase转为snake_case例如ManageAsset→manage_assetDescriptionTool: {toolName}提供给 LLM 的工具描述StructuredOutputtrue是否返回结构化输出AutoRegistertrue是否自动注册到 FastMCP内置工具通常设为false如execute_code声明为AutoRegister false因为它们已在服务端预注册Groupcore工具分组如vfx、animation、ui、scripting_ext、testing、menu用于服务端按会话动态显隐RequiresPollingfalse是否启用轮询中间件用于长任务PollActionstatus轮询时调用的 action 名称MaxPollSeconds0使用服务端默认值最长轮询秒数超时即放弃参数特性ToolParameter同样支持三个配置项Description描述、Required默认true、DefaultValue字符串形式的默认值。服务端在 custom_tool_service.py 的_build_signature与_coerce_default中会把DefaultValue按参数类型integer/number/boolean等转换为 Python 默认值进而生成 LLM 可见的签名。三、工具是如何被发现的反射扫描链路当你在Editor/下新增一个带特性的类后ToolDiscoveryService.cs 会执行双重扫描主扫描TypeCache.GetTypesWithAttributeMcpForUnityToolAttribute()速度快但某些 domain reload 状态下可能漏掉项目程序集兜底扫描遍历AppDomain.CurrentDomain.GetAssemblies()中所有非动态程序集并读取特性速度慢但结果完备。两种结果按类型去重合并后ExtractToolMetadata会提取工具名缺省时ConvertToSnakeCase转换类名、描述、参数列表来自嵌套Parameters类中带[ToolParameter]的公有属性以及RequiresPolling、PollAction、MaxPollSeconds、Group等元数据并写入缓存。工具名重复时后发现的会覆盖先发现的并输出警告日志。发现到的工具会以项目级注册进 Python 服务端/register-toolsHTTP 端点由 custom_tool_service.py 的_register_project_tools管理HTTP 传输模式下unity_instance_middleware.py 的on_list_tools还会按当前 Unity 会话已注册的工具名对工具列表做过滤保证只有当前项目真实可用的工具暴露给 AI。四、响应协议Success / Error / Pending 三件套所有HandleCommand的返回值最终都要能被服务端理解。仓库在 Response.cs 中定义了三种响应类型SuccessResponse(message, data null)序列化为{ success: true, message: ..., data: {...} }ErrorResponse(messageOrCode, data null)序列化为{ success: false, error: ..., code: ... }PendingResponse(message , pollIntervalSeconds 1.0, data null)序列化为{ success: true, _mcp_status: pending, _mcp_poll_interval: 1.0, ... }用于告诉服务端任务还在进行请轮询。服务端 custom_tool_service.py 的_normalize_response会把任意 dict 或对象统一转换为MCPResponse若响应中没有success和_mcp_status字段则整个响应体被当作data返回。五、实战示例完整的截图工具下面是一个可直接落地的截图工具展示了参数解析、相机渲染、RenderTexture读取与文件保存的完整流程。文件路径建议为Assets/Editor/ScreenShots/CaptureScreenshotTool.csusing System.IO; using Newtonsoft.Json.Linq; using UnityEngine; using MCPForUnity.Editor.Tools; using MCPForUnity.Editor.Helpers; namespace MyProject.Editor.CustomTools { [McpForUnityTool( name: capture_screenshot, Description Capture screenshots in Unity, saving them as PNGs )] public static class CaptureScreenshotTool { public class Parameters { [ToolParameter(Screenshot filename without extension, e.g., screenshot_01)] public string filename { get; set; } [ToolParameter(Width of the screenshot in pixels, Required false)] public int? width { get; set; } [ToolParameter(Height of the screenshot in pixels, Required false)] public int? height { get; set; } } public static object HandleCommand(JObject params) { var parameters params.ToObjectParameters(); if (string.IsNullOrEmpty(parameters.filename)) { return new ErrorResponse(filename is required); } try { int width parameters.width ?? Screen.width; int height parameters.height ?? Screen.height; string absolutePath Path.Combine(Application.dataPath, Screenshots, parameters.filename .png); Directory.CreateDirectory(Path.GetDirectoryName(absolutePath)); Camera camera Camera.main ?? Object.FindFirstObjectByTypeCamera(); if (camera null) { return new ErrorResponse(No camera found in the scene); } RenderTexture rt new RenderTexture(width, height, 24); camera.targetTexture rt; camera.Render(); RenderTexture.active rt; Texture2D screenshot new Texture2D(width, height, TextureFormat.RGB24, false); screenshot.ReadPixels(new Rect(0, 0, width, height), 0, 0); screenshot.Apply(); camera.targetTexture null; RenderTexture.active null; Object.DestroyImmediate(rt); byte[] bytes screenshot.EncodeToPNG(); File.WriteAllBytes(absolutePath, bytes); Object.DestroyImmediate(screenshot); return new SuccessResponse($Screenshot saved to {absolutePath}, new { path absolutePath, width width, height height }); } catch (System.Exception ex) { return new ErrorResponse($Failed to capture screenshot: {ex.Message}); } } } }注意width/height声明为可空int?且Required false配合?? Screen.width实现缺省使用当前屏幕分辨率的回退逻辑——这是ToolParameter可选参数的标准写法。六、让 AI 助手看到你的工具虽然 MCP 服务端支持动态注册新工具但并非所有客户端都会自动感知变更。推荐按以下顺序处理最简单在客户端中断开并重连 MCP 服务器强制触发一次全新的工具发现兜底部分客户端如 Windsurf需要彻底移除并重新配置 MCP for Unity 服务器才能保证新工具出现在工具列表中。七、从 CLI 列出来并调用自定义工具CLI 直接支持自定义工具的列举与调用实现见 tool.py 与 editor.py。tool与custom_tool是互为别名的两个命令组列出当前 Unity 项目的自定义工具unity-mcp tool list unity-mcp custom_tool list按名称调用工具--params以 JSON 传入参数解析失败会直接报错退出unity-mcp editor custom-tool my_custom_tool unity-mcp editor custom-tool my_custom_tool --params {param1:value}如果工具名找不到CLI 会自动列出相近名称并给出可复制的示例命令suggest_matchesformat_suggestions。八、长任务轮询工具扛住 domain reload运行测试、烘焙光照贴图、构建 Player 这类操作耗时较长甚至可能触发 Unity 的 domain reload重载后所有静态字段被清空。此时应使用轮询工具你的工具先启动任务并返回pending信号Python 中间件在后台自动轮询 Unity 直到任务完成默认最长 10 分钟超时。8.1 启用轮询在特性上标注RequiresPolling true并指定PollAction通常为status[McpForUnityTool(RequiresPolling true, PollAction status)]仓库内置工具大量使用该模式例如 ManageBuild.cs 声明RequiresPolling true, PollAction status, MaxPollSeconds 1800构建任务最长 30 分钟GenerateImage.cs 与 GenerateModel.cs 分别设置MaxPollSeconds 300。可见MaxPollSeconds允许按任务粒度覆盖服务端默认的 10 分钟上限。8.2 三个关键要素启动任务返回new PendingResponse(message, pollIntervalSeconds)表示任务已启动轮询间隔告诉服务端每次检查之间等待多久。实现 poll action提供一个类似Status的方法检查进度并返回含_mcp_status的响应取值为pending、complete或error。注意中间件会原样调用你写的PollAction字符串不做任何大小写/命名转换务必保证你的HandleCommand或独立方法能识别它。持久化状态用McpJobStateStore把进度保存到项目Library/目录——即使 domain reload 清空了内存任务状态也不会丢失。McpJobStateStore的实现位于 McpJobStateStore.cs状态以McpState_{toolName}.json的形式写入Library/文件夹Path.Combine(Application.dataPath, .., Library)提供SaveStateT、LoadStateT、ClearState三个方法序列化采用 Newtonsoft.Json。因为位于Library/状态天然保持项目级隔离且 Unity 可按需清理。8.3 完整示例模拟异步光照贴图烘焙using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Tools; [McpForUnityTool( bake_lightmaps, Description Simulated async lightmap bake with polling, RequiresPolling true, PollAction status )] public static class BakeLightmaps { private const string ToolName bake_lightmaps; private const float SimulatedDurationSeconds 5f; private static bool s_isRunning; private static double s_lastUpdateTime; private class State { public string lastStatus { get; set; } public float progress { get; set; } } public static object HandleCommand(JObject params) { if (s_isRunning) { var existing McpJobStateStore.LoadStateState(ToolName) ?? new State { lastStatus in_progress, progress 0f }; return new PendingResponse(Bake already running, 0.5, existing); } var state new State { lastStatus in_progress, progress 0f }; McpJobStateStore.SaveState(ToolName, state); s_isRunning true; s_lastUpdateTime EditorApplication.timeSinceStartup; EditorApplication.update UpdateBake; return new PendingResponse(Starting lightmap bake, 0.5, new { state.lastStatus, state.progress }); } public static object Status(JObject _) { var state McpJobStateStore.LoadStateState(ToolName) ?? new State { lastStatus unknown, progress 0f }; if (state.lastStatus completed) { return new { _mcp_status complete, message Bake finished, data state }; } if (state.lastStatus error) { return new { _mcp_status error, error Bake failed, data state }; } return new PendingResponse($Baking... {state.progress:P0}, 0.5, state); } private static void UpdateBake() { if (!s_isRunning) { EditorApplication.update - UpdateBake; return; } var now EditorApplication.timeSinceStartup; var delta now - s_lastUpdateTime; s_lastUpdateTime now; var state McpJobStateStore.LoadStateState(ToolName) ?? new State { lastStatus in_progress, progress 0f }; state.progress Mathf.Clamp01(state.progress (float)(delta / SimulatedDurationSeconds)); if (state.progress 1f) { state.lastStatus completed; s_isRunning false; EditorApplication.update - UpdateBake; } else { state.lastStatus in_progress; } McpJobStateStore.SaveState(ToolName, state); } }该示例通过EditorApplication.update驱动进度推进每帧把进度写入McpJobStateStoreStatus方法读取同一份持久化状态返回complete/error/pending。注意它没有使用Parameters嵌套类因为该工具无需输入参数——HandleCommand直接忽略传入参数。九、轮询协议内部机制服务端轮询逻辑集中在 custom_tool_service.py 的_poll_until_complete与_interpret_status协议行为如下_mcp_status: pending告诉中间件继续轮询_mcp_poll_interval秒控制两次轮询的间隔服务端会钳制在 0.15 秒之间max(0.1, min(interval, 5.0))兼顾响应速度与服务端性能空响应 / 无_mcp_status字段的空 dict视为仍在工作继续下一轮轮询超时保护超过 10 分钟_MAX_POLL_SECONDS 600仍未完成时服务端返回超时错误并附上最后一次收到的响应_safe_responseaction 路由首次调用使用工具期望的 action通常隐式后续每次轮询都在参数中注入action 你的 PollAction 字符串不做 snake_case / camelCase 转换——所以你的HandleCommandswitch 必须精确匹配该字符串容错重试轮询途中网络异常时服务端会构造一个pending响应并退避重试间隔翻倍钳制在 15 秒避免 domain reload 或瞬时断连导致任务被误判失败。十、进阶建议与适用边界内置工具是最佳参考仓库内置工具MCPForUnity/Editor/Tools/下各文件是自定义工具的活教材——execute_code展示了带action分发与安全拦截_blockedPatterns的复杂工具写法run_tests展示了异步任务 job id 轮询模式RunTests.cs。工具分组与显隐Group属性决定工具在 Python 服务端的可见性分组core组默认可见其他组如vfx、asset_gen默认隐藏、按会话动态启用适合插件型工具。命名规范工具名应使用 snake_case如my_custom_tool与 MCP 工具命名惯例一致类名不显式指定Name时会被自动转换。注意事项Editor/位置是发现前提domain reload 会清空静态状态长任务务必依赖McpJobStateStore而非内存字段轮询的 action 字符串必须与PollAction完全一致。参考路径速查原指南文档website/docs/guides/custom-tools.md特性定义MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs响应协议MCPForUnity/Editor/Helpers/Response.cs工具发现MCPForUnity/Editor/Services/ToolDiscoveryService.cs状态持久化MCPForUnity/Editor/Helpers/McpJobStateStore.cs服务端注册与轮询Server/src/services/custom_tool_service.py会话级实例注入与工具过滤Server/src/transport/unity_instance_middleware.pyCLI 实现Server/src/cli/commands/tool.py、Server/src/cli/commands/editor.pyCLI 使用说明Server/src/cli/CLI_USAGE_GUIDE.md【免费下载链接】unity-mcpUnity MCP acts as a bridge between AI assistants and your Unity Editor. Give your LLM tools to manage assets, control scenes, edit scripts, and automate tasks within Unity.项目地址: https://gitcode.com/GitHub_Trending/un/unity-mcp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考