How-To: Add MCP Tools to a Plugin

Goal: expose your plugin's editor operations to AI agents over Model Context Protocol (MCP), the way CrimsonEditorUtilities does. CrimsonCommonEditor gives you two things: the master switch every Crimson plugin honors, and a low-level tool base for cases the engine path can't express. This is an editor-only, C++ task.

Prerequisites
An editor module in your plugin. It depends on CrimsonCommonEditor, and its .uplugin declares CrimsonCommon, ModelContextProtocol, and ToolsetRegistry in the Plugins array. CrimsonEditorUtilities is a complete working example.
Always gate on the master switch
Every registration path below must be guarded by UCrimsonMCPSettings::IsEnabled() (from MCP/CrimsonMCPSettings.h). That single Project Settings toggle - Crimson -> Crimson MCP - is what lets users turn the whole suite's MCP surface off. Skipping the gate breaks that guarantee.

1. Recommended: a UToolsetDefinition

UE 5.8's Toolset Registry is the modern, discoverable path: tools authored here show up in an agent's list_toolsets. Declare a UToolsetDefinition subclass with static UFUNCTION(meta = (AICallable)) functions - the class name becomes the toolset name and each function is one tool. Report failures with UKismetSystemLibrary::RaiseScriptError. Add ToolsetRegistry and Kismet to the editor module's .Build.cs.

cpp
// MyPluginToolset.h
#include "ToolsetRegistry/ToolsetDefinition.h"
#include "MyPluginToolset.generated.h"
/** Tools for my plugin. */
UCLASS(BlueprintType, Hidden)
class UMyPluginToolset : public UToolsetDefinition
{
GENERATED_BODY()
public:
/**
* One-line description the agent sees.
* @param Name What this parameter means.
*/
UFUNCTION(meta = (AICallable), Category = "MyPlugin")
static FString DoThing(const FString& Name);
};

Register the toolset in the editor module's StartupModule (gated), and unregister on shutdown:

cpp
#include "ToolsetRegistry/UToolsetRegistry.h"
#include "MCP/CrimsonMCPSettings.h"
void FMyPluginEditorModule::StartupModule()
{
if (UCrimsonMCPSettings::IsEnabled())
{
UToolsetRegistry::RegisterToolsetClass(UMyPluginToolset::StaticClass());
}
}
void FMyPluginEditorModule::ShutdownModule()
{
if (UToolsetRegistry::IsAvailable())
{
UToolsetRegistry::UnregisterToolsetClass(UMyPluginToolset::StaticClass());
}
}
Verify
Enable Crimson MCP, restart, start the server, and confirm your toolset appears in the agent's list_toolsets. Run ModelContextProtocol.RefreshTools after live-coding a tool method.

2. Advanced: the low-level tool base

For tools that need richer results than the toolset path expresses - explicit error flags, structured JSON, images, audio, or async operations - CrimsonCommonEditor ships a typed base. Define a USTRUCT of params, subclass TCrimsonMCPTool<T>, and return with the Crimson::MCP:: helpers. Add ModelContextProtocol, Json, and JsonUtilities to the .Build.cs.

cpp
#include "MCP/CrimsonMCPTool.h"
#include "MCP/CrimsonMCPResults.h"
USTRUCT()
struct FMyParams { GENERATED_BODY()
UPROPERTY(meta = (ToolTip = "What to do.")) FString Name;
};
struct FMyTool : TCrimsonMCPTool<FMyParams>
{
virtual FString GetName() const override { return TEXT("myplugin.do_thing"); }
virtual FString GetDescription() const override { return TEXT("Does the thing."); }
virtual FModelContextProtocolToolResult RunWithParams(const FMyParams& P) override
{
if (P.Name.IsEmpty()) return Crimson::MCP::Error(TEXT("Name is required."));
return Crimson::MCP::Text(TEXT("Done."));
}
};

Register these through an FCrimsonMCPToolRegistrar member on your editor module - it re-adds tools when ModelContextProtocol.RefreshTools runs, and consults the master switch for you inside Register():

cpp
// Module member: FCrimsonMCPToolRegistrar MCPTools;
#include "MCP/CrimsonMCPRegistrar.h"
void FMyPluginEditorModule::StartupModule()
{
MCPTools.Emplace<FMyTool>();
MCPTools.Register(); // no-op unless UCrimsonMCPSettings::IsEnabled()
}
void FMyPluginEditorModule::ShutdownModule()
{
MCPTools.Unregister();
}
Low-level tools are not discovered by tool search
Tools added this way are top-level MCP tools. Under the engine default (tool search on), an agent can call them by exact name but will not see them in list_toolsets. Prefer the UToolsetDefinition path for anything an agent should discover; reserve the low-level base for image / audio / struct / async results.

See also

  • CrimsonEditorUtilities -> Quick Start - enable Crimson MCP and connect an agent
  • CrimsonEditorUtilities -> Concept: How Crimson MCP Works - toolsets, tool search, and the master-switch cascade