# 自訂工具 — Unity · AI Game Developer Wiki

用 C# 與 AiTool 屬性撰寫你自己的 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.";
        });
    }
}
```

像這樣的 Editor 專用程式碼（它使用了 `UnityEditor`）請放在 `Editor` 資料夾或 Editor 組件中。外掛會在啟動時以及每次重新編譯後掃描專案的組件，因此工具不需要任何註冊就會出現在代理的工具清單中。如果代理沒有自行重新整理工具清單，請重新連線。

### 命名與描述

- `[AiTool]` 的第一個引數是代理呼叫時使用的工具 id。請使用 kebab-case，就像內建工具（`rql-query`、`tests-run`）一樣。
- `Title` 是顯示在工具清單中、供人閱讀的名稱。
- 方法與每個參數上的 `[Description]`（來自 `System.ComponentModel`）就是模型會讀到的內容。請說明工具做什麼、何時呼叫它，以及每個值的含義，並附上範例。
- 具有預設值的參數，或像 `string? tag = null` 這種可為 null 的參數，對模型而言都是選填的。

### 傳回值

工具可以傳回 `string`、任何可序列化的物件（外掛會將其轉為 JSON 並向模型描述其結構）、用於非同步工作的 `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-hant/docs/wiki/unity/configuration#self-hosting)）。

## 自訂工具疑難排解

- **工具沒有出現。** 請檢查 Console 是否有編譯錯誤，確認兩個屬性都存在，並在視窗中開啟 **Tools > Open**：工具可能被隱藏了。組件名稱開頭為 `Unity.`、`UnityEngine`、`UnityEditor`、`System` 或 `Microsoft` 的組件不會被掃描，因此請為你的組件取其他名稱。
- **出現「Can only be called from the main thread」錯誤。** 請把 Unity 呼叫包在 `MainThread.Instance.Run` 中。
- **模型呼叫工具的方式錯誤。** 請改善 `[Description]` 文字，並加入有效值的範例。

較舊的 `[McpPluginToolType]` / `[McpPluginTool]` 名稱仍可編譯，但已棄用；請改用 `Ai*` 屬性。

## 相關頁面

- [工具與 RQL](https://ai-game.dev/zh-hant/docs/wiki/unity/tools-and-rql)：內建工具的說明。
- [自訂 RQL 適配器](https://ai-game.dev/docs/rql/custom-adapters)。
- [組態設定](https://ai-game.dev/zh-hant/docs/wiki/unity/configuration#tools-prompts-resources)：顯示或隱藏工具。

---

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

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