mcpField
C
create-toolset

create-toolset

github.com/epicgames開発1.7k68 回インストール5 日前に更新
リポジトリ

create-toolset について

create-toolset は、ToolsetRegistry に登録する AI 呼び出し可能な関数群を作る Unreal Engine 開発者向けガイドです。設計、ドキュメント、C++ と Python による実装、エラー処理、テストを扱います。

主な機能

  1. 01ツールセットの設計と再利用原則
  2. 02C++ と Python の実装ガイド
  3. 03ツールと型のドキュメント規約
  4. 04登録と非同期呼び出しの説明
  5. 05エラー処理とテスト要件

活用シーン

  1. 01既存の Unreal ツールセットへの機能追加
  2. 02機能に適したツールセットの選定
  3. 03新しい AI 呼び出し可能関数の実装とテスト

MCP 対応

ToolsetRegistry へのツールセット登録と、MCP サーバーを通じたクライアントへの AI 呼び出し可能関数の公開方法を説明します。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/epicgames/unreal-engine-skills-for-claude-code-plugin --skill create-toolset

インストール後、Claude Code で/create-toolsetを入力して読み込まれたか確認します。

詳細

ツールセットの作成

Unreal Engine のツールセットを作成または拡張します。ツールセットは、ToolsetRegistry に登録され、MCP サーバーを通じて公開される、静的な AI 呼び出し可能関数の集まりです。目標は、エディター内で Claude が実行できることを増やすことです。

原則

優れたツールセットは、次のようなものです。

シンプル:対象領域で役立つ処理を実行できる、最もシンプルな API を設計します。Unreal の既存 API をそのまま模倣しないでください。多くの場合、既存 API は必要以上に複雑です。技術アーティストが実装を読まずに使い方を理解できるかどうかを、判断の目安にしてください。

完全:CRUD の対称性をサポートします。ある値を設定できるなら取得できるようにし、作成できるなら削除できるようにします。変更が不可能または有用でない場合は、取得関数のみでも問題ありません。

組み合わせ可能:ツールセット全体で、同種の処理には一貫した型を使用します。get_graph() が Graph を返すなら、get_graph_nodes() は Graph を受け取るようにします。関数を自然に組み合わせて、より複雑な結果を得られるようにします。

DRY:ツールセット内でも、ツールセット間でも重複を避けます。ObjectTools は汎用的な UObject プロパティの取得と設定をすでに提供しています。再実装しないでください。機能が別の場所にある場合は、それを呼び出すか、ユーザーに案内してください。

作成を始める前に

コードに手を加える前に、以下の質問を順番に検討してください。

1. その機能はすでに存在しますか? エディターが起動している場合は、MCP 経由で list_toolsets を呼び出し、関連するものに対して describe_toolset を実行します。MCP が利用できない場合は、コードベース内で Toolsets という名前のフォルダーを検索し、その中にある C++ ヘッダーまたは Python のツールセットファイルを読みます。その機能がすでに公開されている場合、作業は不要です。ユーザーにその旨を伝え、適切なツールを案内してください。

2. 依頼は、見た目よりも汎用的ですか? ユーザーは、実際にはより広いパターンの一例にすぎない機能を、特定の領域向けに依頼することがあります。領域別のツールセットを調べる前に、その機能が本当にその領域に属するのか、より広く適用されるものなのかを確認してください。たとえば、「ブループリントアセットのメタデータを読み取る」という依頼は BlueprintTools に属するように見えますが、メタデータはすべてのアセットに適用されるため、適切な場所は AssetTools です。汎用的に解決するほうが、特定の用途に限定して解決するより、ほとんどの場合に適しています。

3. この領域向けのツールセットはすでにありますか? 機能がまだなくても、同じ領域を対象とするツールセットがある場合(たとえば、StaticMeshTools にメッシュのクエリを追加する場合)は、新しいツールセットを作るのではなく、既存のツールセットに追加します。

4. 新しいツールセット専用のプラグインが必要ですか? 新しいツールセットを作成する場合、既存のプラグインと密接に関連していれば、そのプラグインに追加します。領域が十分に異なる場合は、独自のプラグインが適切かもしれません。判断が明確でない場合は、ユーザーに確認してください。

5. 実装言語を選びます。 選択するのはユーザーですが、情報に基づいて判断できるよう支援します。通常は、反復開発が速く、変更もしやすい Python が推奨されます。次の両方の選択肢を評価してください。

  • Python:Python スタブ(<project_root>/Intermediate/PythonStub/unreal.py)で利用可能な API を確認します。ファイル全体を読むのではなく、検索してください。ファイルが存在しない場合は、ユーザーに Developer Mode を Edit → Project Settings → Plugins → Python で有効にしてから、エディターを再起動してもらう必要があります。これはどのツールセット作業にも役立つため、積極的に推奨してください。必要な API がすべてそろっている場合は、Python が適切な選択です。機能の大部分は利用可能でも、一部が不足している場合は、その不足をユーザーに伝えてください。小規模なエンジンの調整を加えることで、全体として Python がより適した選択になる場合もあります。
  • C++:Python のカバー範囲が不十分、またはまったくない場合に適しています。必要な機能の大部分は、スタブに含まれていません。

各言語で利用できるものをまとめ、コードを書く前にユーザーが選択できるようにしてください。必要な API がどちらの言語でも利用できない場合は、回避策を講じないでください。作業を中止し、エンジンを拡張できるようユーザーに伝えてください。回避策で作成したツールは脆弱で、気づかないうちに壊れる可能性があります。

共通の規約

以下は C++ と Python の両方に適用されます。

  • 既存のツールを重複させないでください。 たとえば、ObjectTools は汎用的な UObject プロパティの取得と設定をすでに処理します。
  • ドキュメントは必須です。 すべてのツールセットとツール呼び出しにドキュメントが必要です。以下の「ドキュメント」セクションを参照してください。
  • 実際の型を使用してください。 パラメーターと戻り値には実際の型(int32、FVector、TArray<FString>、構造体など)を指定してください。ToolsetRegistry がシリアライズを自動的に処理します。ツール呼び出しの中で JSON 形式の文字列に変換したり、そこから変換したりするのは、コードの不適切な兆候です。文字列型は、構造化データを偽装したものではなく、本当に文字列を意味する必要があります。
  • 戻り値にはデータを含め、ステータスを含めないでください。 正常に戻ることは成功を意味し、例外を発生させることは失敗を意味します。エラーを伝えるために、ブール値、エラー文字列、結果ラッパーを返してはいけません。代わりに例外を発生させてください。
  • すべてのツールメソッドは静的にしてください。 インスタンスの状態を持たせないでください。
  • テストは必須です。 すべてのツールにテストカバレッジが必要です。以下の「テスト」セクションを参照してください。

ドキュメント

すべてのレベルで適切なドキュメントが必要です。任意の仕上げではありません。詳細にしすぎないでください。長い説明より、短く的確な説明が適しています。コードにすでに書かれている内容を超える意味を加えない文は削除してください。

ツールセットクラス:ツールセットが何を行い、どの領域を扱うかを説明する文を1~3文で記述してください。LLM はツールを読み込まずに、ツールセット名とこの説明だけを見ることがよくあるため、説明だけで内容が伝わる必要があります。対象領域と、ツールセットが対応する操作の種類を説明してください。ツール名を列挙しないでください。ツール自体がその内容を示します。

各ツール:ツールの機能を明確に説明し、すべてのパラメーターと戻り値を記述してください。意味と単位に重点を置いてください。

各行について、次の基準で判断してください。有能な読者が、シグネチャ、名前、型だけからこの内容を推測できますか? 推測できるなら削除してください。 LLM はデフォルトで過剰にドキュメントを記述するため、抑制してください。コードだけでは分からない内容を述べている場合に限り、その行を残す価値があります。したがって、シグネチャの言い換え(型、デフォルト値、省略可能かどうか)、使用方法やツールの連携方法の説明(連携方法はシグネチャから分かります)、明白な内容の具体例、フィールドに含まれる可能性があるものの推測(実際の意味ではなく)を削除してください。

なぜそのように書いたかではなく、コードが何をするかを記述してください。 「アセットレジストリが未ロードの型を見落とすため、サブクラスを直接列挙する」は実装上の理由であり、仕様ではありません。ツールの機能を説明し、理由の説明は削除してください。

推測できない情報は残してください。単位、範囲、分かりにくいエンコード形式(例:「スペース区切りの単一文字列」)、空または null の結果が意味すること、名前からは分からない領域固有の意味などです。

// Over-documented: every line restates the signature or narrates usage.
/**
 * Runs a cheat. @param CheatName The cheat name (case-insensitive), pass it to run the cheat.
 * @param Args Optional arguments; fill in the slots from the Args hint (e.g. "<float F>" -> "2.0",
 *             "<float A> <float B>" -> "1 2"). Empty string for no arguments.
 */
// Trimmed: only the non-inferable facts remain.
/**
 * Runs a cheat on the local player, as if typed into the console.
 * @param CheatName The cheat command name.
 * @param Args Arguments as a single space-separated string. Empty for none.
 */

構造体:構造体自体(何を表し、どのような場合に使用するか)と、すべてのフィールド(意味と、該当する場合は単位)を記述してください。ClampMin や ClampMax などの UPROPERTY メタデータは自動的に抽出され、スキーマに含まれます。ドキュメントコメントで制約を繰り返す代わりに、これらを使用してください。

C++ 固有の事項

構成
  • UToolsetDefinition(ToolsetRegistry/ToolsetDefinition.h から)を継承してください。
  • .h / .cpp ファイルごとにツールセットクラスを1つだけ配置してください。
  • AI から呼び出し可能なツールはすべて、UFUNCTION(meta = (AICallable)) を付けた静的メソッドにしてください。関数のドキュメントコメントが、AI に表示されるツールの説明になります。分かりやすく記述してください。
  • プライベートヘルパーは、UFUNCTION(meta = (AICallable)) を付けない静的メソッドにしてください。マクロを省略するだけで、公開されません。
/**
 * Snapshot of a MyThing's current state, returned by GetThingInfo.
 */
USTRUCT(BlueprintType)
struct FMyThingInfo
{
    GENERATED_BODY()

    /** How thing-like this thing is. */
    UPROPERTY(meta=(ClampMin="0.0", ClampMax="1.0")) float Thinginess;

    /** Things that belong to this thing. */
    UPROPERTY() TArray<UMyThing*> SubThings;
};

/**
 * Manages MyThings in the current level. Covers the full lifecycle of these objects:
 * querying by name, reading their state, and performing operations on them.
 */
UCLASS(BlueprintType, MinimalAPI)
class UMyToolset : public UToolsetDefinition
{
    GENERATED_BODY()

public:
    /**
     * Returns all things whose name matches the given pattern.
     * @param NamePattern Substring to match against thing names.
     * @return Matching things, or an empty array if none are found.
     */
    UFUNCTION(meta = (AICallable), Category = "MyToolset")
    static TArray<UMyThing*> FindThings(const FString& NamePattern);

    /**
     * Returns detailed info about the given thing.
     * @param Thing The thing to read state from.
     * @return The thing's current state.
     */
    UFUNCTION(meta = (AICallable), Category = "MyToolset")
    static FMyThingInfo GetThingInfo(UMyThing* Thing);

    /**
     * Performs the primary operation on the given thing.
     * @param Thing The thing to perform the operation on.
     */
    UFUNCTION(meta = (AICallable), Category = "MyToolset")
    static void DoTheThing(UMyThing* Thing);
};
非同期ツール呼び出し

ほとんどのツールは同期処理です。ゲームスレッドで実行され、値を直接返します。スクリーンショットの撮影やエディターの状態変化を待つなど、時間のかかる処理には非同期処理を使用してください。

非同期ツールは値を直接返す代わりに、UToolCallAsyncResult のサブクラスを返します。すでに多くの結果型が用意されており、任意の値型に対応する新しい型は UToolCallAsyncResult をサブクラス化して作成できます。新しい型を作成する前に、既存の型を確認してください。

宣言方法は他のツールと同じですが、戻り値の型に非同期結果を指定します。

/**
 * Renders an image of the given thing.
 * @param Thing The thing to capture.
 * @return An image of the thing.
 */
UFUNCTION(meta = (AICallable), Category = "MyToolset")
static UToolCallAsyncResultImage* CaptureThingImage(UMyThing* Thing);

同期ツールとは異なり、成功と失敗はいずれも結果オブジェクトを通じて通知します。成功時は SetValue() を、失敗時は SetError() を呼び出してください。

UToolCallAsyncResultImage* UMyToolset::CaptureThingImage(UMyThing* Thing)
{
    UToolCallAsyncResultImage* Result = NewObject<UToolCallAsyncResultImage>();

    if (!IsRenderingEnabled())
    {
        Result->SetError(TEXT("Rendering is not enabled."));
        return Result;
    }

    // Initiate the capture; call Result->SetValue(image) on completion.
    return Result;
}

適切な実装方法は、対象とするシステムによって異なります。コードを書く前に、既存のツールセットを確認して実際の例を参考にしてください。

登録

ツールセットはいつでも動的に登録および登録解除できます。よくある方法は、モジュールの起動時と終了時に登録することです。

class FMyToolsetModule : public IModuleInterface
{
    void StartupModule()
    {
        UToolsetRegistry::RegisterToolsetClass(UMyToolset::StaticClass());
    }

    void ShutdownModule()
    {
        UToolsetRegistry::UnregisterToolsetClass(UMyToolset::StaticClass());
    }
};

同じプラグイン内の近くにあるツールセットを確認し、そこで使われている方法を参考にしてください。

カスタム型と JSON の変換

ToolsetRegistry は、すべての Unreal 型を JSON との間で自動的に変換します。シリアル化について考慮する必要はほとんどありません。JSON コンバーターを使うと、特定の型の表現方法をより細かく制御できます。通常は、既定のスキーマよりも簡潔で AI に扱いやすいスキーマにするために使用します。

これは高度な機能です。役に立つかもしれないというだけで使用するのではなく、明確な必要性がある場合に限って使用してください。

すでにいくつかの型には、カスタムコンバーターが組み込まれています。たとえば、以下のものがあります。

  • FToolsetColorConverter:FColor と FLinearColor を単一の色表現に統一し、AI がバイト値と浮動小数点値の違いを把握する必要がないようにします。
  • FToolsetReferenceConverter:すべての UObject* および UClass* プロパティを、通常なら返される生の文字列ではなく、型付きのソフトパスオブジェクトに変換します。
  • FToolsetTransformConverter:FTransform の位置、回転、スケールをオプションのフィールドとして公開します。毎回 3 つすべての要素を必須にするよりも、はるかに扱いやすくなります。

既存のコンバーターで対応していない型にカスタムシリアル化が必要な場合は、FToolsetJsonConverter(ToolsetRegistry/ToolsetJsonConverter.h にあります)をサブクラス化し、ツールセットと一緒に登録してください。独自のコンバーターを書く前に、ToolsetRegistry プラグインのソースにある既存のコンバーターを 1 つ読んでください。インターフェースには複数の要素が関わるため、既存の実装が最適な参考資料になります。

エラー処理

ツールが処理を完了できない場合(入力が無効、アセットが見つからない、前提条件を満たしていないなど)は、スクリプトエラーを発生させ、null または既定値を直ちに返してください。

TArray<UMyThing*> UMyToolset::FindThings(const FString& NamePattern)
{
    if (NamePattern.IsEmpty())
    {
        UKismetSystemLibrary::RaiseScriptError(
            EScriptExceptionType::Error,
            TEXT("NamePattern must not be empty."));
        return {};
    }
}
テスト

テストを実行する前に、LiveCodingToolset.CompileLiveCoding を使って変更内容をコンパイルしてください。完了するまで処理がブロックされ、MSVC の診断情報が表示されます。続行する前に、コンパイルエラーをすべて修正してください。

すべてのツールについて、成功時の処理とすべてのエラー処理の両方をテストで網羅する必要があります。ツールの説明どおりに動作することを確認するテストを少なくとも 1 つ作成し、エラーを発生させる条件ごとに個別のテストも作成してください。BEGIN_DEFINE_SPEC / END_DEFINE_SPEC のパターンを使用してください。参考として、Plugins/Experimental/Toolsets にある既存のテストを確認してください。テストはツールセットの近くに配置し、同じプラグイン内の規約に従ってください。

BEGIN_DEFINE_SPEC(
    FMyToolsetSpec,
    "AI.MyToolset",
    EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter)
END_DEFINE_SPEC(FMyToolsetSpec)

void FMyToolsetSpec::Define()
{
    Describe("FindThings", [this]()
    {
        It("returns matching things", [this]()
        {
            TArray<UMyThing*> Results = UMyToolset::FindThings(TEXT("expected_name"));
            TestFalse(TEXT("Result is not empty"), Results.IsEmpty());
        });

        It("returns an empty array when no things match", [this]()
        {
            TArray<UMyThing*> Results = UMyToolset::FindThings(TEXT("nonexistent"));
            TestTrue(TEXT("Result is empty"), Results.IsEmpty());
        });

        It("raises when the name pattern is empty", [this]()
        {
            AddExpectedError(TEXT("NamePattern must not be empty"));
            UMyToolset::FindThings(TEXT(""));
        });
    });
}

Python 固有の事項

構成
  • クラスに @unreal.uclass() を付け、unreal.ToolsetDefinition を継承します。
  • 1 つの .py ファイルにつき、ツールセットクラスは 1 つにします。
  • AI から呼び出し可能なすべてのツールには、@toolset_registry.tool_call の後に @staticmethod を付けます。docstring は AI に表示されるツールの説明になります。
  • プライベートヘルパーには、名前の先頭に _ を付けた @staticmethod を使用し、クラスの末尾に配置します。
  • すべてのパラメーターと戻り値に型注釈が必須です。 スキーマの生成は型注釈のみに依存します。
  • unreal.Array[str] などの Unreal 固有の型ではなく、標準的な Python の型注釈(list[str]、dict[str, str] など)を使用します。@toolset_registry.tool_call デコレーターが自動的に変換します。
@unreal.uclass()
class MyToolset(unreal.ToolsetDefinition):
    """Manages MyThings in the current level. Covers the full lifecycle of these objects:
    querying by name, reading their state, and performing operations on them."""

    @toolset_registry.tool_call
    @staticmethod
    def find_things(name_pattern: str) -> list[MyThing]:
        """Returns all things whose name matches the given pattern.

        Args:
            name_pattern: Substring to match against thing names.

        Returns:
            Matching things, or an empty list if none are found.
        """
        ...

    @toolset_registry.tool_call
    @staticmethod
    def get_thing_info(thing: MyThing) -> MyThingInfo:
        """Returns detailed info about the given thing.

        Args:
            thing: The thing to read state from.

        Returns:
            The thing's current state.
        """
        ...

    @toolset_registry.tool_call
    @staticmethod
    def do_the_thing(thing: MyThing) -> None:
        """Performs the primary operation on the given thing.

        Args:
            thing: The thing to perform the operation on.
        """
        ...
登録

登録は自動では行われません。プラグインの登録リストにツールセットクラスを追加し、初期化時に unreal.ToolsetRegistry.register_toolset_class を呼び出します。通常は、同じプラグイン内の他のツールセットとともに、__init__.py または init_unreal.py に記述します。

def register_toolsets():
    unreal.ToolsetRegistry.register_toolset_class(MyToolset)

def unregister_toolsets():
    unreal.ToolsetRegistry.unregister_toolset_class(MyToolset)

作業中のプラグインで同等の関数を見つけ、そこにツールセットを追加してください。

エラー処理

ツールが処理を完了できない場合は、例外を直接送出します。

@toolset_registry.tool_call
@staticmethod
def find_things(name_pattern: str) -> list[MyThing]:
    """Returns things whose name matches the given pattern."""
    if not name_pattern:
        raise ValueError("name_pattern must not be empty.")
テスト

すべてのツールについて、成功パスとすべてのエラーパスをテストで網羅する必要があります。ツールが説明どおりに動作することを確認するテストを少なくとも 1 つ作成し、例外が発生する条件ごとに個別のテストを作成してください。参考として Plugins/Experimental/Toolsets にある既存のテストを確認してください。ToolCallTestCase を継承し、エラーパスのテストには assertToolRaisesRuntimeError を使用します。

class MyToolsetTestCase(ToolCallTestCase):
    """Test MyToolset toolset."""

    def test_find_things_returns_matches(self):
        """Returns matching things when they exist."""
        results = MyToolset.find_things("expected_name")
        self.assertGreater(len(results), 0)

    def test_find_things_returns_empty_for_no_match(self):
        """Returns an empty list when no things match the pattern."""
        results = MyToolset.find_things("nonexistent")
        self.assertEqual(results, [])

    def test_find_things_raises_on_empty_pattern(self):
        """Raises when name_pattern is empty."""
        with self.assertToolRaisesRuntimeError():
            MyToolset.find_things("")

編集後にテストを再実行する前に、プラグインのパッケージをリロードしてください。そうしないと、エディターに変更が反映されません。Edit → Project Settings → Plugins → Python → Enable Remote Execution で Remote Execution を有効にしてから、次を実行します。

python Engine/Plugins/Experimental/ToolsetRegistry/Content/Python/toolset_registry/tests/reload_remote.py your_plugin

作業のテスト

テストを実行することで、ツールセットが実際に動作することを確認し、変更によるリグレッションを検出できます。コードを書き、必要に応じてコンパイルし、テストを実行して失敗内容を確認し、修正するというサイクルを、すべてのテストに合格するまで繰り返してください。

起動中のエディター(推奨)

unreal-mcp を使用して起動中のエディターインスタンスに対してテストを実行する方法が、最も迅速に反復作業を進められます。再起動せずに変更をコンパイルまたはホットリロードでき、結果もすぐに返されます。この方法は C++ テストと Python テストの両方で使用できます。

MCP 経由でテストを実行します。

  1. AutomationTestToolset を読み込み、DiscoverTests を呼び出します。これは他のテストツールを使う前に必ず行ってください。省略すると、結果が空になります。
  2. ツールセット名でフィルターした ListTests を呼び出し、テストが検出されたことを確認します。
  3. 完全なテストパスを指定して RunTests を呼び出します。
  4. GetTestStatus をポーリングしてから、GetTestResults を呼び出して、テストごとのエラーや警告の詳細を確認します。

Python テストの反復作業では、リロード後に force_rediscover=true を指定して DiscoverTests を呼び出し、オートメーションシステムに追加または削除されたテストを認識させます。

コマンドライン(エディターが起動していない場合)

エディターが起動していない場合、コマンドラインはヘッドレスエディターのインスタンスを起動し、指定されたテストを実行して終了します。起動に約 30 秒かかるため時間はかかりますが、エディターを起動しておく必要がなく、CI やエディターを利用できない場合に便利です。

UnrealEditor-Cmd と -ExecCmds を使用して、オートメーションテストシステムを直接呼び出します。

UnrealEditor-Cmd.exe <Project>.uproject -ExecCmds="Automation RunTests AI.MyToolset;quit" -Unattended -NullRHI

AI.MyToolset を、ツールセットの spec 名に一致するテストフィルターに置き換えます。プロジェクトに適したフラグとフィルタープレフィックスを確認するには、同じプラグイン内の既存のテストがどのように実行されているかを確認してください。

作業内容のレビュー

コードを書き終えてテストが通ったら、完了とする前に、いったん手を止めて全体を読み返してください。最初に書いたコードには、ツールごとに見ているだけでは気づかない問題が含まれていることがよくあります。

  • ボイラープレートの重複。 複数のツールで繰り返されている、ヘルパーを共有できる類似パターン。
  • テストカバレッジの不足。 後から追加したもののテストされていない例外発生時の処理。成功ケースのテストが1つのシナリオしか確認していない。
  • 機能の重複。 このツールセット内の別のツールや、まったく別のツールセットにあるツールがすでに実現していることを行うツール。
  • APIの一貫性の欠如。 パラメーター名や型が、ツールセット内の他の箇所で使われている規約と一致していない。
  • ドキュメントの不足。 説明が曖昧なままのパラメーターや、単独で役立つほど十分な説明がないクラスのdocstring。
  • コードを言い換えただけのドキュメント。 よくある問題です。型、デフォルト値、呼び出し順序を繰り返す行、明らかな内容の動作例、コードの構成理由の説明などが該当します。コメントを一つずつ読み返し、シグネチャから読み取れる行は削除してください。

見つけた問題は、引き継ぐ前にすべて修正してください。2回目のチェックを丁寧に行うほうが、後からバグ報告に対応するより早く済みます。

コメント