# Custom tools

Write your own Unity MCP tools, prompts, resources and skills in C# with the AiTool attributes, run them on the main thread and use them in a built game.

_Last updated: 2026-11-04_

Unity MCP can turn any C# method in your project into a tool the AI agent can call. This is how you give the agent project-specific abilities: "spawn a wave of enemies", "rebuild the level from this JSON", "validate all item prefabs". This page shows the attribute API for tools, prompts, resources and skills, how to run Unity API calls safely, and how to expose tools from a running game.

## Before you write a tool

RQL already reads and sets members of your own components and calls their public methods, so many tasks need no custom tool at all. Write a tool when you want a single, well-described operation with validation, or logic that should not be improvised by the model each time. To make RQL understand your own types better instead, see [custom RQL adapters](https://ai-game.dev/docs/rql/custom-adapters).

## Your first tool

A tool is a method marked `[AiTool]` inside a class marked `[AiToolType]`. Both attributes are in the `com.IvanMurzak.McpPlugin` namespace.

```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.";
        });
    }
}
```

Put Editor-only code like this (it uses `UnityEditor`) in an `Editor` folder or an Editor assembly. The plugin scans the project's assemblies when it starts and after every recompile, so the tool appears in the agent's list without any registration. Reconnect the agent if it does not refresh its tool list by itself.

### Naming and descriptions

- The first argument of `[AiTool]` is the tool id the agent calls. Use kebab-case, like the built-in tools (`rql-query`, `tests-run`).
- `Title` is a human-readable name shown in tool lists.
- `[Description]` (from `System.ComponentModel`) on the method and on each parameter is what the model reads. Say what the tool does, when to call it, and what each value means, with an example.
- Parameters with a default value, or nullable ones such as `string? tag = null`, are optional for the model.

### Return values

A tool can return a `string`, any serializable object (the plugin converts it to JSON and describes its schema to the model), `Task<T>` for async work, or a `ResponseCallTool` from `com.IvanMurzak.McpPlugin.Common.Model` when you want explicit control, for example `ResponseCallTool.Success("…")`, `ResponseCallTool.Error("…")` or `ResponseCallTool.Image(bytes, "image/png")`. Return clear success and error messages: the model uses them to decide what to do next.

## The main thread

Tools may be invoked off Unity's main thread, and most Unity APIs throw when called from another thread. Wrap Unity calls in `MainThread.Instance.Run(...)` (namespace `com.IvanMurzak.ReflectorNet.Utils`), or `MainThread.Instance.RunAsync(...)` from an async tool. Keep heavy work that does not touch Unity, such as parsing a large file, outside the wrapper so the Editor stays responsive.

## Tool options

`[AiTool]` accepts optional hints that tell MCP clients how the tool behaves:

| Property | Meaning |
| --- | --- |
| `ReadOnlyHint` | The tool only reads data. |
| `DestructiveHint` | The tool may delete or overwrite data. |
| `IdempotentHint` | Calling it again with the same arguments changes nothing more. |
| `OpenWorldHint` | The tool talks to external systems, such as the web. |
| `Visible` | `false` hides the tool from the agent's list when first discovered; it still runs when called by name. |
| `Target` | `ToolTarget.Editor`, `ToolTarget.Runtime` or `ToolTarget.Both` (default). Can also be set on `[AiToolType]`. |

Example: `[AiTool("level-validate", Title = "Level / Validate", ReadOnlyHint = true, Visible = false)]`.

## Custom prompts

A prompt is a reusable instruction the user can invoke from the AI client, often as a slash command. Mark a class `[AiPromptType]` and a method returning a string `[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` is `Role.User` or `Role.Assistant`. Set `Enabled = false` to ship a prompt hidden by default; the built-in prompts do this.

## Custom resources

A resource is read-only data the client can fetch by URI. Mark a class `[AiResourceType]` and a method `[AiResource]`, and provide a listing method:

```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") };
}
```

## Custom skills

Skills are Markdown files generated into the agent's skills folder (for example `.claude/skills`). Add your own with `[AiSkillType]` on a static class and `[AiSkill]` on a static string property or field that holds the Markdown:

```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 ...";
}
```

On a tool method, `[AiSkillDescription]` and `[AiSkillBody]` add a short summary and long-form Markdown to the generated skill for that tool. Regenerate skills with **Skills > Generate** in the window or `unity-mcp-cli setup-skills <agent>`.

## Tools in a running game

The same attributes work in a player build. In a game nothing connects by default: you initialize the runtime plugin yourself and register the tools you want. Mark game-only tools with `Target = ToolTarget.Runtime` (namespace `com.IvanMurzak.McpPlugin.Common`). The Editor then skips them, and the server routes their calls to the running game even while the Editor is connected.

```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` stands for your own game code. To authenticate against a server, set `c.CredentialProvider` to a function that returns the token. Call `UnityMcpPluginRuntime.DisposeInstance()` to shut the runtime connection down. Players need a reachable MCP server, so plan where it runs (see [self-hosting](https://ai-game.dev/docs/wiki/unity/configuration#self-hosting)).

## Troubleshooting custom tools

- **The tool does not appear.** Check the Console for compile errors, confirm both attributes are present, and open **Tools > Open** in the window: the tool may be hidden. Assemblies whose names start with `Unity.`, `UnityEngine`, `UnityEditor`, `System` or `Microsoft` are not scanned, so give your assembly another name.
- **"Can only be called from the main thread" errors.** Wrap the Unity calls in `MainThread.Instance.Run`.
- **The model calls the tool wrongly.** Improve the `[Description]` texts and add examples of valid values.

The older `[McpPluginToolType]` / `[McpPluginTool]` names still compile but are deprecated; use the `Ai*` attributes.

## Related pages

- [Tools and RQL](https://ai-game.dev/docs/wiki/unity/tools-and-rql) for the built-in tools.
- [Custom RQL adapters](https://ai-game.dev/docs/rql/custom-adapters).
- [Configuration](https://ai-game.dev/docs/wiki/unity/configuration#tools-prompts-resources) to show or hide tools.

---

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