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.

Updated November 4, 2026 View as Markdown

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.

Your first tool

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

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:

PropertyMeaning
ReadOnlyHintThe tool only reads data.
DestructiveHintThe tool may delete or overwrite data.
IdempotentHintCalling it again with the same arguments changes nothing more.
OpenWorldHintThe tool talks to external systems, such as the web.
Visiblefalse hides the tool from the agent's list when first discovered; it still runs when called by name.
TargetToolTarget.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]:

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:

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:

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.

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).

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.