Advanced

How to create new MCP tools for gengine — FMCPToolBase, parameter validation, annotations, registration, testing, and a complete example.

Tool Development

gengine's tool system is extensible. You can add new operations to existing domain tools or create entirely new domain tools. This page covers the FMCPToolBase interface, validation helpers, annotation factory methods, registration, test patterns, and a complete working example.

FMCPToolBase

Every tool handler inherits from FMCPToolBase and implements two methods: GetInfo() and Execute().

// Source/Gengine/Private/MCP/MCPToolBase.h

struct FMCPToolInfo
{
    FString Name;
    FString Description;
    TArray<FMCPParamDef> Params;
    TArray<EMCPAnnotation> Annotations;
    EMCPSafetyTier SafetyRequirement;
};

class FMCPToolBase
{
public:
    virtual ~FMCPToolBase() = default;

    /** Return static metadata — called once at registration. */
    virtual FMCPToolInfo GetInfo() const = 0;

    /** Execute the operation. Called on the game thread. */
    virtual FMCPResult Execute(const FJsonObject& Params) = 0;

    /** Optional: override to check safety annotations at dispatch time. */
    virtual bool HasAnnotation(EMCPAnnotation Annotation) const;

    /** Optional: override to declare the minimum safety tier required. */
    virtual EMCPSafetyTier GetSafetyRequirement() const;
};

Implementing GetInfo()

GetInfo() declares the tool's name, description, parameter schema, annotations, and safety tier. This data feeds tool discovery — the AI reads it during the MCP initialize handshake.

FMCPToolInfo FMCPTool_TextureAudit::GetInfo() const
{
    FMCPToolInfo Info;
    Info.Name        = TEXT("texture_audit");
    Info.Description = TEXT("Audit texture assets in a content path for oversized dimensions or missing compression settings.");
    Info.SafetyRequirement = EMCPSafetyTier::ReadOnly;
    Info.Annotations = {
        EMCPAnnotation::ReadOnly
    };

    // Required parameter
    Info.Params.Add(FMCPParamDef::Required(
        TEXT("path"),
        TEXT("Content path to audit, e.g. /Game/Textures/"),
        EMCPParamType::String
    ));

    // Optional parameter with default
    Info.Params.Add(FMCPParamDef::Optional(
        TEXT("max_dimension"),
        TEXT("Flag textures larger than this dimension (width or height). Default: 2048."),
        EMCPParamType::Integer
    ));

    Info.Params.Add(FMCPParamDef::Optional(
        TEXT("recursive"),
        TEXT("Include subdirectories. Default: false."),
        EMCPParamType::Bool
    ));

    return Info;
}

Parameter definition helpers

HelperUsage
FMCPParamDef::Required(name, desc, type)Mandatory parameter — validator rejects calls that omit it
FMCPParamDef::Optional(name, desc, type)Optional parameter — validator passes calls that omit it

Param types

EMCPParamTypeJSON type
Stringstring
Integernumber (integer)
Floatnumber (float)
Boolboolean
Objectobject
Arrayarray

Implementing Execute()

Execute() runs on the game thread. Keep it non-blocking — use AsyncTask for heavy work and return a task handle.

FMCPResult FMCPTool_TextureAudit::Execute(const FJsonObject& Params)
{
    // 1. Extract and validate parameters
    FString ContentPath;
    if (!Params.TryGetStringField(TEXT("path"), ContentPath))
    {
        return FMCPResult::Error(TEXT("Missing required parameter: path"));
    }

    // Validate path — no traversal, no absolute paths
    if (!FMCPParamValidator::IsValidContentPath(ContentPath))
    {
        return FMCPResult::Error(TEXT("Invalid content path"));
    }

    int32 MaxDimension = 2048;
    Params.TryGetNumberField(TEXT("max_dimension"), MaxDimension);

    bool bRecursive = false;
    Params.TryGetBoolField(TEXT("recursive"), bRecursive);

    // 2. Do the work using UE APIs (game thread — safe to call asset registry)
    FAssetRegistryModule& AssetReg =
        FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));

    FARFilter Filter;
    Filter.PackagePaths.Add(*ContentPath);
    Filter.ClassPaths.Add(UTexture2D::StaticClass()->GetClassPathName());
    Filter.bRecursivePaths = bRecursive;

    TArray<FAssetData> Assets;
    AssetReg.Get().GetAssets(Filter, Assets);

    // 3. Build result
    TArray<TSharedPtr<FJsonValue>> Violations;

    for (const FAssetData& Asset : Assets)
    {
        UTexture2D* Tex = Cast<UTexture2D>(Asset.GetAsset());
        if (!Tex) continue;

        const int32 Width  = Tex->GetSizeX();
        const int32 Height = Tex->GetSizeY();

        if (Width > MaxDimension || Height > MaxDimension)
        {
            TSharedPtr<FJsonObject> Entry = MakeShared<FJsonObject>();
            Entry->SetStringField(TEXT("path"),   Asset.GetObjectPathString());
            Entry->SetNumberField(TEXT("width"),  Width);
            Entry->SetNumberField(TEXT("height"), Height);
            Entry->SetStringField(TEXT("issue"),  TEXT("Exceeds max dimension"));
            Violations.Add(MakeShared<FJsonValueObject>(Entry));
        }
    }

    TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
    ResultObj->SetNumberField(TEXT("total_checked"),    Assets.Num());
    ResultObj->SetNumberField(TEXT("violations_found"), Violations.Num());
    ResultObj->SetArrayField(TEXT("violations"),        Violations);

    return FMCPResult::Success(ResultObj);
}

Result helpers

MethodUsage
FMCPResult::Success(JsonObject)Operation completed, return structured data
FMCPResult::Success(FString)Operation completed, return plain text
FMCPResult::Error(FString)Operation failed, return error message
FMCPResult::ValidationError(FString)Parameter was invalid — triggers 400-style response

Validator Helpers

Use FMCPParamValidator static methods for common checks rather than rolling your own:

// Path safety
FMCPParamValidator::IsValidContentPath(FString Path)      // /Game/... format, no traversal
FMCPParamValidator::IsValidActorName(FString Name)         // no special chars, no traversal
FMCPParamValidator::IsValidAssetName(FString Name)         // alphanumeric + underscores

// Numeric safety
FMCPParamValidator::IsFiniteFloat(float Value)             // not NaN, not Inf
FMCPParamValidator::IsValidIntRange(int32 Value, int32 Min, int32 Max)

// String safety
FMCPParamValidator::ContainsInjectionPatterns(FString S)  // <script>, javascript:, etc.
FMCPParamValidator::ContainsPathTraversal(FString S)       // ../, ..\, etc.

Annotation Factory Methods

Declare annotations in GetInfo() using the EMCPAnnotation enum:

// Read-only — does not modify editor state
Info.Annotations = { EMCPAnnotation::ReadOnly };

// Destructive — may permanently delete data
Info.Annotations = { EMCPAnnotation::Destructive };

// Idempotent — calling twice produces same result
Info.Annotations = { EMCPAnnotation::Idempotent };

// Open-world — may affect external systems
Info.Annotations = { EMCPAnnotation::OpenWorld };

// Combine as needed
Info.Annotations = { EMCPAnnotation::Idempotent, EMCPAnnotation::OpenWorld };

Registration

Register tools in StartupModule() of GengineModule.cpp:

// Source/Gengine/Private/GengineModule.cpp

void FGengineModule::StartupModule()
{
    // ... existing registrations ...

    // Register your new tool
    FMCPToolRegistry::Get().RegisterTool(
        MakeShared<FMCPTool_TextureAudit>()
    );
}

That's it. The tool appears in tools/list discovery on the next editor session and is immediately callable by AI clients.

Test Patterns

Tests live in Source/Gengine/Private/Tests/. Follow the existing test pattern:

// Source/Gengine/Private/Tests/MCPTool_TextureAudit_Test.cpp

#include "CoreMinimal.h"
#include "Misc/AutomationTest.h"
#include "MCP/Tools/MCPTool_TextureAudit.h"

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
    FMCPTool_TextureAudit_ValidPath,
    "gengine.Tools.TextureAudit.ValidPath",
    EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)

bool FMCPTool_TextureAudit_ValidPath::RunTest(const FString& Parameters)
{
    FMCPTool_TextureAudit Tool;

    // Build params
    TSharedPtr<FJsonObject> Params = MakeShared<FJsonObject>();
    Params->SetStringField(TEXT("path"), TEXT("/Game/"));
    Params->SetNumberField(TEXT("max_dimension"), 1024);

    FMCPResult Result = Tool.Execute(*Params);

    TestTrue("Result is success", Result.bSuccess);
    TestTrue("Has total_checked field", Result.Data->HasField(TEXT("total_checked")));

    return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
    FMCPTool_TextureAudit_MissingPath,
    "gengine.Tools.TextureAudit.MissingPath",
    EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)

bool FMCPTool_TextureAudit_MissingPath::RunTest(const FString& Parameters)
{
    FMCPTool_TextureAudit Tool;

    TSharedPtr<FJsonObject> EmptyParams = MakeShared<FJsonObject>();
    FMCPResult Result = Tool.Execute(*EmptyParams);

    TestFalse("Result is error for missing path", Result.bSuccess);
    TestTrue("Error message mentions path", Result.ErrorMessage.Contains(TEXT("path")));

    return true;
}

Run tests with:

Automation RunTests gengine.Tools.TextureAudit

Or all gengine tests:

Automation RunTests gengine

Complete Example: TextureAudit Tool

Putting it all together — a complete read-only tool that audits textures for oversized dimensions:

Header (MCPTool_TextureAudit.h):

#pragma once
#include "MCP/MCPToolBase.h"

class FMCPTool_TextureAudit : public FMCPToolBase
{
public:
    virtual FMCPToolInfo GetInfo() const override;
    virtual FMCPResult Execute(const FJsonObject& Params) override;
};

Implementation (MCPTool_TextureAudit.cpp): see Execute() example above.

Registration (in GengineModule.cpp):

FMCPToolRegistry::Get().RegisterTool(MakeShared<FMCPTool_TextureAudit>());

After registration, an AI client can call:

{
  "tool": "texture_audit",
  "operation": "query",
  "params": {
    "path": "/Game/Environment/",
    "max_dimension": 2048,
    "recursive": true
  }
}

And receives:

{
  "total_checked": 47,
  "violations_found": 3,
  "violations": [
    { "path": "/Game/Environment/Rocks/T_Boulder_Diffuse", "width": 4096, "height": 4096, "issue": "Exceeds max dimension" },
    { "path": "/Game/Environment/Trees/T_Oak_Normal",      "width": 4096, "height": 2048, "issue": "Exceeds max dimension" },
    { "path": "/Game/Environment/Ground/T_Mud_Diffuse",    "width": 4096, "height": 4096, "issue": "Exceeds max dimension" }
  ]
}