From unreal-engine-skills-for-claude-code
Author and extend Unreal Engine toolsets: static AI-callable functions via ToolsetRegistry and unreal-mcp. For adding, exposing, or registering tool methods, creating new toolsets, or extending existing ones like BlueprintTools.
How this skill is triggered — by the user, by Claude, or both
Slash command
/unreal-engine-skills-for-claude-code:create-toolsetThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are authoring or extending an Unreal Engine toolset: a collection of static, AI-callable functions registered with the `ToolsetRegistry` and exposed through the MCP server. The goal is to expand the surface of things Claude can do inside the editor.
You are authoring or extending an Unreal Engine toolset: a collection of static, AI-callable functions registered with the ToolsetRegistry and exposed through the MCP server. The goal is to expand the surface of things Claude can do inside the editor.
A good toolset is:
Clean: Design the simplest API that can do useful work in the domain. Don't mirror Unreal's existing APIs directly; they're often unnecessarily complex. A good heuristic: would a technical artist understand this without reading the implementation?
Complete: Support CRUD symmetry. If you can set a thing, you should be able to get it. If there's a create, there should be a delete. Getters without setters are fine when mutation isn't possible or useful.
Composable: Use consistent types for the same kinds of operation across the toolset. If get_graph() returns a Graph, then get_graph_nodes() should accept a Graph. Functions should combine naturally to produce more complex results.
DRY: No duplication within a toolset, and no duplication across toolsets. ObjectTools already provides generic UObject property get/set. Don't reimplement it. If functionality lives elsewhere, call it or point to it.
Work through these questions in order before touching any code.
1. Does the functionality already exist? If the editor is running, call list_toolsets via MCP, then describe_toolset on anything relevant. If MCP isn't available, search the codebase for folders named Toolsets and read the C++ headers or Python toolset files there. If the capability is already exposed, there's nothing to do. Tell the user and point them to the right tool.
2. Is the request more general than it sounds? Users often ask for something domain-specific that is actually an instance of a broader pattern. Before looking for a domain toolset, ask whether the capability truly belongs to that domain or whether it applies more widely. For example, a request for "read blueprint asset metadata" sounds like it belongs in BlueprintTools, but metadata applies to all assets, so the right place is AssetTools. Solving it generically is almost always better than solving it narrowly.
3. Does a toolset for this domain already exist? If the functionality is missing but a toolset already covers the same domain (e.g. you're adding a mesh query to StaticMeshTools), add to that toolset rather than creating a new one.
4. Does a new toolset need its own plugin? If you're creating a new toolset and it's closely related to an existing plugin, add it there. If it's a distinct enough domain, it may warrant its own plugin. Ask the user if it isn't obvious.
5. Choose the implementation language. This is the user's call, but help them make an informed one. Python is generally preferred: it's faster to iterate and easier to change. Assess both options:
<project_root>/Intermediate/PythonStub/unreal.py; search it, don't read it wholesale). If the file doesn't exist, the user needs to enable Developer Mode in Edit → Project Settings → Plugins → Python and restart the editor. This is worth doing for any toolset work, so recommend it proactively. If the necessary APIs are all there, Python is the right choice. If most of the functionality is available but something small is missing, flag the gap to the user; a minor engine tweak may still make Python the better option overall.Summarize what's available in each and let the user decide before writing any code. If the APIs needed aren't available in either language, don't work around it. Stop and tell the user so they can extend the engine. Workarounds create fragile tools that break silently.
These apply in both C++ and Python:
ObjectTools already handles generic UObject property get/set.int32, FVector, TArray<FString>, a struct, etc.). The ToolsetRegistry handles serialization automatically. Converting to or from a JSON-formatted string inside a tool call is a code smell. A string type should mean it's genuinely a string, not structured data in disguise.Good documentation is required at every level. It's not optional polish. Don't go into too much detail; a short, precise description is better than a long one. If a sentence doesn't add meaning beyond what the code already says, cut it.
Toolset class: Write 1-3 sentences describing what the toolset does and the domain it covers. An LLM will often see only the toolset name and this description without ever loading its tools, so it needs to stand on its own. Describe the domain and the kinds of operations the toolset supports. Don't enumerate tool names; the tools speak for themselves.
Each tool: Write a clear description of what the tool does, document every parameter, and document the return value. Focus on meaning and units.
The test for every line: could a competent reader infer this from the signature, names, and types alone? If yes, cut it. LLMs over-document by default; resist it. A doc line earns its place only by stating what the code cannot. So cut signature restatement (types, defaults, optionality), usage and chaining narration (how tools combine is visible in the signatures), worked examples of the obvious, and speculation about what a field might contain instead of what it is.
Document what the code does, not why you wrote it that way. "Iterates the subclasses directly because the asset registry misses unloaded types" is implementation rationale, not contract. Say what the tool does; cut the reasoning.
Keep what can't be inferred: units, ranges, non-obvious encodings (e.g. "a single space-separated string"), what an empty or null result means, and domain meaning the name doesn't carry.
// 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.
*/
Structs: Document the struct itself (what it represents and when it's used) and every field (meaning and units where applicable). UPROPERTY metadata such as ClampMin and ClampMax is extracted automatically and included in the schema. Use it rather than restating constraints in the doc comment.
UToolsetDefinition (from ToolsetRegistry/ToolsetDefinition.h)..h / .cpp file.UFUNCTION(meta = (AICallable)). The function's doc comment becomes the AI-visible tool description. Write it clearly.UFUNCTION(meta = (AICallable)). Simply omit the macro and they won't be exposed./**
* 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);
};
Most tools are synchronous: they run on the game thread and return a value directly. Use async for long-running operations such as capturing a screenshot or waiting for an editor state change.
Async tools return a subclass of UToolCallAsyncResult instead of the value directly. Many result types already exist, and new ones can be created by subclassing UToolCallAsyncResult for any value type. Check the existing types before creating a new one.
The declaration looks like any other tool, just with an async result return type:
/**
* 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);
Unlike synchronous tools, success and failure are both communicated through the result object. Call SetValue() on success and SetError() on failure.
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;
}
The right implementation approach depends on the system being exercised. Look at existing toolsets for real examples before writing any code.
Toolsets can be registered and unregistered dynamically at any time. A common pattern is to do it in module startup and shutdown:
class FMyToolsetModule : public IModuleInterface
{
void StartupModule()
{
UToolsetRegistry::RegisterToolsetClass(UMyToolset::StaticClass());
}
void ShutdownModule()
{
UToolsetRegistry::UnregisterToolsetClass(UMyToolset::StaticClass());
}
};
Check nearby toolsets in the same plugin to see what pattern is used there.
The ToolsetRegistry automatically converts all Unreal types to and from JSON. You rarely need to think about serialization. JSON converters let you take extra control over how a specific type is represented, typically to produce a cleaner or more AI-friendly schema than the default.
This is an advanced feature and should only be used reactively, when there is a clear need for it, not just because it might be helpful.
Several types already have built-in custom converters. For example:
FToolsetColorConverter: unifies FColor and FLinearColor into a single color representation so the AI doesn't need to know about the byte vs. float distinction.FToolsetReferenceConverter: converts all UObject* and UClass* properties to typed soft path objects rather than the raw strings you'd otherwise get.FToolsetTransformConverter: exposes FTransform location, rotation, and scale as optional fields, which is much more ergonomic than requiring all three components every time.When you need custom serialization for a type not already covered, subclass FToolsetJsonConverter (from ToolsetRegistry/ToolsetJsonConverter.h) and register it alongside your toolset. Read one of the existing converters in the ToolsetRegistry plugin source before writing your own. The interface has several moving parts and the existing implementations are the best guide.
When a tool cannot complete its work (invalid input, missing asset, precondition not met), raise a script error and return immediately with a null or default value:
TArray<UMyThing*> UMyToolset::FindThings(const FString& NamePattern)
{
if (NamePattern.IsEmpty())
{
UKismetSystemLibrary::RaiseScriptError(
EScriptExceptionType::Error,
TEXT("NamePattern must not be empty."));
return {};
}
}
Before running tests, compile your changes with LiveCodingToolset.CompileLiveCoding. It blocks until done and surfaces MSVC diagnostics. Fix any compile errors before proceeding.
Every tool needs test coverage for both the success path and every error path. Write at least one test that confirms the tool does what it says, and a separate test for each condition that raises. Use the BEGIN_DEFINE_SPEC / END_DEFINE_SPEC pattern. Read existing tests in Plugins/Experimental/Toolsets for reference. Place tests near the toolset and follow the convention in the same plugin:
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(""));
});
});
}
@unreal.uclass() and inherit from unreal.ToolsetDefinition..py file.@toolset_registry.tool_call followed by @staticmethod. The docstring becomes the AI-visible tool description.@staticmethod with a _-prefixed name, placed at the end of the class.list[str], dict[str, str], etc.) rather than Unreal equivalents like unreal.Array[str]. The @toolset_registry.tool_call decorator handles the conversion automatically.@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.
"""
...
Registration is never automatic. Add the toolset class to the plugin's registration list and call unreal.ToolsetRegistry.register_toolset_class during initialization, typically in an __init__.py or init_unreal.py alongside other toolsets in the same plugin:
def register_toolsets():
unreal.ToolsetRegistry.register_toolset_class(MyToolset)
def unregister_toolsets():
unreal.ToolsetRegistry.unregister_toolset_class(MyToolset)
Find the equivalent functions in the plugin you're working in and add your toolset there.
When a tool cannot complete its work, raise an exception directly:
@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.")
Every tool needs test coverage for both the success path and every error path. Write at least one test that confirms the tool does what it says, and a separate test for each condition that raises. Read existing tests in Plugins/Experimental/Toolsets for reference. Extend ToolCallTestCase and use assertToolRaisesRuntimeError to test error paths:
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("")
Before re-running tests after editing, reload the plugin's package. The editor won't pick up changes otherwise. Enable Remote Execution in Edit → Project Settings → Plugins → Python → Enable Remote Execution, then run:
python Engine/Plugins/Experimental/ToolsetRegistry/Content/Python/toolset_registry/tests/reload_remote.py your_plugin
Tests are how you verify the toolset actually works and catch regressions when things change. Work in a tight loop: write code, compile if needed, run the tests, read the failures, fix them, and repeat until everything passes.
Running tests against a live editor instance using unreal-mcp is the fastest way to iterate. Changes can be compiled or hot-reloaded without restarting, and results come back immediately. This flow works for both C++ and Python tests.
Run tests via MCP:
AutomationTestToolset and call DiscoverTests. This is required before any other test tool; skipping it causes empty results.ListTests filtered to your toolset name to confirm the tests are discovered.RunTests with the full test paths.GetTestStatus, then call GetTestResults for detailed per-test errors and warnings.When iterating on Python tests, call DiscoverTests with force_rediscover=true after reloading so the automation system picks up any added or removed tests.
When no editor is running, the command line launches a headless editor instance, runs the specified tests, and exits. It's slower due to startup time (~30 seconds) but requires no running editor and is useful in CI or when the editor isn't available.
Use UnrealEditor-Cmd with -ExecCmds to invoke the automation test system directly:
UnrealEditor-Cmd.exe <Project>.uproject -ExecCmds="Automation RunTests AI.MyToolset;quit" -Unattended -NullRHI
Replace AI.MyToolset with the test filter matching your toolset's spec name. Check how existing tests in the same plugin are run to confirm the right flags and filter prefix for the project.
Once the code is written and tests pass, step back and read what you've built as a whole before declaring it done. First-pass code often has issues that aren't visible tool-by-tool:
Fix anything you find before handing off. A clean second pass is faster than a bug report later.
npx claudepluginhub full-stack-assets/unreal-engine-skills-for-claude-code-plugin2plugins reuse this skill
First indexed Jun 18, 2026
Author and extend Unreal Engine toolsets: static AI-callable functions via ToolsetRegistry and unreal-mcp. For adding, exposing, or registering tool methods, creating new toolsets, or extending existing ones like BlueprintTools.
Builds Unreal Editor tools, validators, utility widgets, and commandlets to reduce manual work and pipeline risk. Focuses on safe bulk-edit and validation behavior.
Provides expert-level C++ development guidelines for Unreal Engine 5.x, covering UObject hygiene, reflection system, performance patterns, and Epic's coding standards.