# 自定义工具 — Unity · AI Game Developer Wiki

使用 AiTool 特性以 C# 编写自己的 Unity MCP 工具、提示词、资源和技能，在主线程上运行，并在构建后的游戏中加以使用。

本页由英文机器翻译而成。[阅读英文原文](https://ai-game.dev/docs/wiki/unity/custom-tools)。

Unity MCP 可以把项目里任意一个 C# 方法变成 AI 智能体可以调用的工具。这样你就能赋予智能体针对具体项目的能力：“生成一波敌人”“用这份 JSON 重建关卡”“校验所有物品预制体”。本页介绍用于工具、提示词、资源和技能的特性 API，如何安全地执行 Unity API 调用，以及如何从运行中的游戏里暴露工具。

## 编写工具之前

RQL 已经能读写你自己组件中的成员并调用其公共方法，所以很多任务根本不需要自定义工具。当你想要一个描述清晰、带校验的单一操作，或者某段逻辑不该每次都让模型即兴发挥时，才需要编写工具。若想让 RQL 更好地理解你自己的类型，请参阅[自定义 RQL 适配器](https://ai-game.dev/docs/rql/custom-adapters)。

## 你的第一个工具

工具就是在标记了 `[AiToolType]` 的类中，用 `[AiTool]` 标记的方法。这两个特性都位于 `com.IvanMurzak.McpPlugin` 命名空间中。

```csharp
using System.ComponentModel;
using com.IvanMurzak.McpPlugin;
using com.IvanMurzak.ReflectorNet.Utils;
using UnityEngine;

[AiToolType]
public static class Tool_Enemies
{
    [AiTool("enemies-spawn-wave", Title = "Enemies / Spawn wave")]
    [Description("Spawns a wave of enemy prefabs in a circle around the origin. Returns how many were spawned.")]
    public static string SpawnWave
    (
        [Description("Asset path of the enemy prefab, e.g. 'Assets/Prefabs/Enemy.prefab'.")]
        string prefabPath,
        [Description("How many enemies to spawn.")]
        int count = 5,
        [Description("Circle radius in meters.")]
        float radius = 10f
    )
    {
        return MainThread.Instance.Run(() =>
        {
            var prefab = UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
            if (prefab == null)
                return $"[Error] No prefab at '{prefabPath}'.";

            for (var i = 0; i < count; i++)
            {
                var angle = i * Mathf.PI * 2f / count;
                var position = new Vector3(Mathf.Cos(angle), 0f, Mathf.Sin(angle)) * radius;
                Object.Instantiate(prefab, position, Quaternion.identity);
            }
            return $"[Success] Spawned {count} enemies.";
        });
    }
}
```

把这类仅限编辑器使用的代码（它用到了 `UnityEditor`）放在 `Editor` 文件夹或 Editor 程序集中。插件在启动时以及每次重新编译后都会扫描项目的程序集，因此工具无需任何注册就会出现在智能体的工具列表中。如果智能体没有自行刷新工具列表，请重新连接它。

### 命名与描述

- `[AiTool]` 的第一个参数是智能体调用时使用的工具 id。请使用 kebab-case，与内置工具（`rql-query`、`tests-run`）保持一致。
- `Title` 是显示在工具列表中的可读名称。
- 方法和每个参数上的 `[Description]`（来自 `System.ComponentModel`）才是模型读到的东西。请说明工具做什么、何时调用它、每个值代表什么，并给出一个示例。
- 带默认值的参数，或可为空的参数（如 `string? tag = null`），对模型来说是可选的。

### 返回值

工具可以返回 `string`、任意可序列化对象（插件会把它转换为 JSON，并向模型描述其 schema）、用于异步工作的 `Task<T>`，或者在需要显式控制时返回 `com.IvanMurzak.McpPlugin.Common.Model` 中的 `ResponseCallTool`，例如 `ResponseCallTool.Success("…")`、`ResponseCallTool.Error("…")` 或 `ResponseCallTool.Image(bytes, "image/png")`。请返回清晰的成败与错误消息：模型会据此决定下一步做什么。

## 主线程

工具可能会在 Unity 主线程之外被调用，而大多数 Unity API 在其他线程调用时会抛出异常。请把 Unity 调用包在 `MainThread.Instance.Run(...)`（命名空间 `com.IvanMurzak.ReflectorNet.Utils`）中，或在异步工具中使用 `MainThread.Instance.RunAsync(...)`。不涉及 Unity 的重活（例如解析大文件）应放在包装之外，以保持编辑器响应流畅。

## 工具选项

`[AiTool]` 接受一些可选提示，用来告诉 MCP 客户端该工具的行为特征：

| 属性 | 含义 |
| --- | --- |
| `ReadOnlyHint` | 该工具只读取数据。 |
| `DestructiveHint` | 该工具可能删除或覆盖数据。 |
| `IdempotentHint` | 用相同参数再次调用不会产生更多变化。 |
| `OpenWorldHint` | 该工具会与外部系统交互，例如网络。 |
| `Visible` | 设为 `false` 时，首次发现该工具时它不会出现在智能体的列表中；但按名称调用时它仍会执行。 |
| `Target` | `ToolTarget.Editor`、`ToolTarget.Runtime` 或 `ToolTarget.Both`（默认）。也可以在 `[AiToolType]` 上设置。 |

示例：`[AiTool("level-validate", Title = "Level / Validate", ReadOnlyHint = true, Visible = false)]`。

## 自定义提示词

提示词是一段可复用的指令，用户可以从 AI 客户端调用它，通常以斜杠命令的形式。给类标记 `[AiPromptType]`，给返回字符串的方法标记 `[AiPrompt]`：

```csharp
using System.ComponentModel;
using com.IvanMurzak.McpPlugin;
using com.IvanMurzak.McpPlugin.Common.Model;

[AiPromptType]
public static class Prompt_ProjectRules
{
    [AiPrompt(Name = "project-conventions", Role = Role.User)]
    [Description("Coding and scene conventions for this project.")]
    public static string Conventions()
        => "Use PascalCase for public members. Put new UI under the MainCanvas object. Prefer object pooling for bullets.";
}
```

`Role` 可以是 `Role.User` 或 `Role.Assistant`。设置 `Enabled = false` 可让提示词默认隐藏；内置提示词就是这么做的。

## 自定义资源

资源是客户端可以通过 URI 获取的只读数据。给类标记 `[AiResourceType]`，给方法标记 `[AiResource]`，并提供一个列举方法：

```csharp
using System.ComponentModel;
using com.IvanMurzak.McpPlugin;
using com.IvanMurzak.McpPlugin.Common.Model;

[AiResourceType]
public static class Resource_Balance
{
    const string Uri = "game://balance";

    [AiResource(Route = Uri, Name = "balance", MimeType = "application/json", ListResources = nameof(List))]
    [Description("Current game balance values as JSON.")]
    public static ResponseResourceContent[] Get(string uri)
        => new[] { ResponseResourceContent.CreateText(uri, "{\"enemyHp\":100}", "application/json") };

    public static ResponseListResource[] List()
        => new[] { new ResponseListResource(uri: Uri, name: "balance", mimeType: "application/json", description: "Game balance") };
}
```

## 自定义技能

技能是生成到智能体技能文件夹（例如 `.claude/skills`）中的 Markdown 文件。在静态类上用 `[AiSkillType]`、在存放 Markdown 的静态字符串属性或字段上用 `[AiSkill]`，即可添加自己的技能：

```csharp
using com.IvanMurzak.McpPlugin;

[AiSkillType]
public static class Skill_MyGame
{
    [AiSkill("my-game-levels", "How levels are structured in this project and which tools build them.")]
    public static string Markdown => "# Levels\n\nEvery level is a scene under Assets/Levels ...";
}
```

在工具方法上，`[AiSkillDescription]` 和 `[AiSkillBody]` 会为该工具生成的技能补充简短摘要和长篇幅 Markdown 内容。可通过窗口中的 **Skills > Generate** 或 `unity-mcp-cli setup-skills <agent>` 重新生成技能。

## 在运行中的游戏里使用工具

相同的特性在播放器构建中同样适用。在游戏中默认不会建立任何连接：你需要自行初始化运行时插件并注册所需的工具。用 `Target = ToolTarget.Runtime`（命名空间 `com.IvanMurzak.McpPlugin.Common`）标记仅限游戏使用的工具。这样 Editor 会跳过它们，即使 Editor 已连接，服务器也会把这些工具的调用路由到正在运行的游戏。

```csharp
using System.ComponentModel;
using System.Reflection;
using System.Threading.Tasks;
using com.IvanMurzak.McpPlugin;
using com.IvanMurzak.McpPlugin.Common;
using com.IvanMurzak.ReflectorNet.Utils;
using com.IvanMurzak.Unity.MCP;
using UnityEngine;

[AiToolType(Target = ToolTarget.Runtime)]
public static class ChessGameAI
{
    [AiTool("chess-do-turn", Title = "Do the turn")]
    [Description("Moves a figure. Returns true if the move was accepted.")]
    public static Task<bool> DoTurn(int figureId, Vector2Int position)
        => MainThread.Instance.RunAsync(() => ChessGameController.Instance.DoTurn(figureId, position));

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    static void SetupMcp()
    {
        var mcpPlugin = UnityMcpPluginRuntime.Initialize(builder =>
            {
                builder.WithConfig(c => c.Host = "http://localhost:8080");
                builder.WithToolsFromAssembly(Assembly.GetExecutingAssembly());
            })
            .Build();
        _ = mcpPlugin.Connect();
    }
}
```

`ChessGameController` 代表你自己的游戏代码。要针对服务器进行身份验证，请把 `c.CredentialProvider` 设置为一个返回令牌的函数。调用 `UnityMcpPluginRuntime.DisposeInstance()` 可关闭运行时连接。玩家需要一台可访问的 MCP 服务器，因此要规划好它的运行位置（参见[自托管](https://ai-game.dev/zh-hans/docs/wiki/unity/configuration#self-hosting)）。

## 自定义工具疑难解答

- **工具没有出现。** 检查 Console 是否有编译错误，确认两个特性都已存在，并在窗口中打开 **Tools > Open**：该工具可能被隐藏了。名称以 `Unity.`、`UnityEngine`、`UnityEditor`、`System` 或 `Microsoft` 开头的程序集不会被扫描，因此请为你的程序集换一个名称。
- **“Can only be called from the main thread”错误。** 请用 `MainThread.Instance.Run` 包裹 Unity 调用。
- **模型错误地调用了工具。** 改进 `[Description]` 文本，并添加有效值的示例。

旧的 `[McpPluginToolType]` / `[McpPluginTool]` 名称仍然可以编译，但已弃用；请使用 `Ai*` 系列特性。

## 相关页面

- [工具与 RQL](https://ai-game.dev/zh-hans/docs/wiki/unity/tools-and-rql)：了解内置工具。
- [自定义 RQL 适配器](https://ai-game.dev/docs/rql/custom-adapters)。
- [配置](https://ai-game.dev/zh-hans/docs/wiki/unity/configuration#tools-prompts-resources)：显示或隐藏工具。

---

Source (HTML): https://ai-game.dev/zh-hans/docs/wiki/unity/custom-tools

Machine-translated (zh-Hans) from the English page: https://ai-game.dev/docs/wiki/unity/custom-tools
