# 自定义工具 — Unreal Engine · AI Game Developer Wiki

通过任意 C++ 插件向 Unreal MCP 添加你自己的 AI 工具、RQL 模块、提示词和资源，并在运行中的游戏里暴露玩法工具。

本页由英文机器翻译而成。[阅读英文原文](https://ai-game.dev/docs/wiki/unreal/custom-tools)。

Unreal MCP 允许任何 Unreal 插件（包括你自己游戏项目中的插件）为 AI 代理添加能力。你不需要 fork Unreal MCP：只需实现一个小的公开接口，将其注册为 Unreal **modular feature**，插件会在你的模块加载时自动发现它。

本页面向 C++ 开发者。如果你只是想扩大引擎覆盖范围，请先查看现成的[扩展](https://ai-game.dev/zh-hans/docs/wiki/unreal/extensions)。

## 选择正确的贡献方式

| 你希望代理能够…… | 贡献类型 | 接口 |
| --- | --- | --- |
| 读取或修改某个引擎功能或你自己类型在编辑器中的数据 | **RQL 模块**（推荐用于编辑器相关工作） | `IUnrealMcpRqlModule` |
| 执行非数据访问的操作，或在运行中的游戏内执行操作 | **工具** | `IUnrealMcpToolProvider` |
| 获取可复用的提示词模板 | **提示词** | `IUnrealMcpPromptProvider` |
| 按 URI 读取固定内容 | **资源** | `IUnrealMcpResourceProvider` |

所有官方扩展都是 RQL 模块，因此代理通过与其他编辑器功能相同的 `rql-query` 和 `rql-mutate` 工具访问它们。

## 方案 1：RQL 模块

在编辑器模块的 `Build.cs` 中依赖 `UnrealMcpRql` 模块，并在 `.uplugin` 的 `Plugins` 数组中声明 `UnrealMCP` 插件。然后实现该模块：

```cpp
#include "IUnrealMcpRqlModule.h"

class FMyRqlModule : public IUnrealMcpRqlModule
{
public:
    virtual FString GetRqlModuleName() const override { return TEXT("Niagara"); }
    virtual FString GetExtensionId() const override { return TEXT("com.example.niagara-ai"); }
    virtual FString GetExtensionVersion() const override { return UnrealMcpRql::PluginVersion(TEXT("MyNiagaraAI")); }
    virtual void ConfigureRql(FUnrealMcpRqlModuleContext& Context) override
    {
        Context.AddAssetSource(TEXT("niagaraSystems"), TEXT("Niagara system assets"), UNiagaraSystem::StaticClass());
        Context.AddApiSource(TEXT("niagara"), TEXT("Niagara operations"), UMyNiagaraOperations::StaticClass(),
            TEXT("NiagaraOperations"));
        Context.AddGuide(TEXT("Niagara"), TEXT("... markdown with example queries ..."));
    }
};

IMPLEMENT_MODULE(TUnrealMcpRqlEditorModule<FMyRqlModule>, MyNiagaraAI)
```

上下文可以添加的内容：

- **源**（查询的起点）：`AddObjectSource`、`AddObjectListSource`、`AddAssetSource`、`AddClassDefaultsSource`。
- **操作**（通过 `call` 调用）：`AddApiClass` 或 `AddApiSource` 会让某个类的所有 `UFUNCTION` 都可调用。请将它们声明为 `UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))`；每个函数上方的文档注释会成为代理读取的描述。
- **成员**：`ExposeProperties` 用于反射隐藏的属性，以及通过 `AddJsonMember`、`AddObjectMember`、`AddObjectListMember` 添加的只读计算成员。
- **指南**：`AddGuide` 会向 `unreal-rql` 技能中添加一节内容。

变更操作在一个编辑器事务中运行，因此请使用 `RF_Transactional` 创建对象，并对你修改的内容调用 `Modify()`。编辑器的 Extensions 部分会列出你的模块，并可以将其禁用。可构建的参考示例是 [Unreal-MCP 仓库](https://github.com/IvanMurzak/Unreal-MCP)中的 `samples/UnrealAITemplate` 插件。

## 方案 2：自定义工具

### 步骤 1：添加依赖

在你的模块的 `Build.cs` 中：

```csharp
PublicDependencyModuleNames.AddRange(new string[] { "Core" });

PrivateDependencyModuleNames.AddRange(new string[]
{
    "CoreUObject", "Engine", "Projects",
    "Json",            // the registry header includes Dom/JsonObject.h
    "UnrealMcpEditor", // the extension contract and tool registry
});
```

在你的 `.uplugin` 中添加插件依赖，以便解析加载顺序：

```json
{
  "Plugins": [ { "Name": "UnrealMCP", "Enabled": true } ]
}
```

### 步骤 2：实现 provider

```cpp
#include "IUnrealMcpToolProvider.h"
#include "UnrealMcpToolRegistry.h"

class FMyExtensionProvider : public IUnrealMcpToolProvider
{
public:
    virtual FString GetExtensionId() const override      { return TEXT("com.foo.my-extension"); }
    virtual FText   GetDisplayName() const override      { return NSLOCTEXT("Foo", "Name", "My Extension"); }
    virtual FString GetExtensionVersion() const override { return TEXT("1.0.0"); }

    virtual void RegisterTools(FUnrealMcpToolRegistry& Registry) override
    {
        Registry.Tool(TEXT("hello-extension"))
            .Title(TEXT("Hello Extension"))
            .Description(TEXT("Returns a friendly greeting."))
            .ParamString(TEXT("name"), TEXT("Who to greet. Defaults to 'world'."))
            .ReadOnlyHint(true).DestructiveHint(false).IdempotentHint(true).OpenWorldHint(false)
            .Handle([](const FUnrealMcpToolCall& Call) -> FUnrealMcpToolResult
            {
                const FString Name = Call.Has(TEXT("name")) ? Call.GetString(TEXT("name")) : TEXT("world");
                return FUnrealMcpToolResult::Success(FString::Printf(TEXT("Hello, %s!"), *Name));
            });
    }
};
```

builder 提供 `.Title`、`.Description`、参数辅助方法 `.ParamString`、`.ParamInt`、`.ParamNumber`、`.ParamBool` 和 `.ParamVector`（传入 `EUnrealMcpParamRequirement::Required` 表示必填）、四个 MCP hints 以及 `.Handle`。处理函数在游戏线程上运行，因此你可以直接调用编辑器 API 和 `UObject` API。

重要规则：

- 工具 id 使用 kebab-case：小写字母和数字，用单个连字符分隔。
- 为每个工具提供标题和全部四个 hints。将任何删除或覆盖内容的工具标记为 destructive。
- 不要自行调用 `.ExtensionId(...)`；你的 `GetExtensionId()` 会自动被写入。

### 步骤 3：将其注册为 modular feature

```cpp
virtual void StartupModule() override
{
    Provider = MakeUnique<FMyExtensionProvider>();
    IModularFeatures::Get().RegisterModularFeature(
        IUnrealMcpToolProvider::GetModularFeatureName(), Provider.Get());
}

virtual void ShutdownModule() override
{
    if (Provider.IsValid())
    {
        IModularFeatures::Get().UnregisterModularFeature(
            IUnrealMcpToolProvider::GetModularFeatureName(), Provider.Get());
        Provider.Reset();
    }
}
```

在注册期间保持 provider 存活，并且始终在关闭时注销它。

## 注册行为说明

- **发现与热重载。** Provider 在编辑器启动时被发现。之后加载或卸载你的模块会重建注册表，AI 客户端会自动看到变化。
- **确定性顺序。** Provider 按扩展 id 升序合并；同一个 provider 内的工具保持其声明顺序。
- **隔离性。** 无效的工具（id 不合法、schema 损坏、缺少处理函数）或重名的工具会被丢弃，并记录到你的扩展条目上。你的其他工具和其他扩展不受影响。处理函数内部崩溃仍然会导致编辑器崩溃，因此请校验输入并返回 `FUnrealMcpToolResult::Error(...)`。
- **可见性与开关。** 你的工具在 MCP Tools 窗口中与内置工具一样拥有 **Visible** 复选框。被禁用的扩展不会贡献任何内容。

## 提示词与资源

Prompt 和 resource 使用相同的模式，即 `IUnrealMcpPromptProvider` 加 `FUnrealMcpPromptRegistry`，以及 `IUnrealMcpResourceProvider` 加 `FUnrealMcpResourceRegistry`。Prompt 用相同的 `Param*` 辅助函数声明参数，并返回带角色标记的消息。Resource 由其 URI 标识，返回文本或带 MIME 类型的 base64 blob。目前只支持固定 URI，不支持模板化 URI。内置的 `level-design-brief` prompt 和 `unreal://project/levels` resource 是可用的参考示例。

## 让 agent 生成工具

在插件以源码方式安装的情况下，`unreal-skill-create` 系统工具会把新的 C++ 工具文件写入插件的 editor 模块。只有在重新构建编辑器之后（Live Coding 或重新构建项目），该工具才会存在。

## 游戏内（运行时）工具

Unreal MCP 还可以连接正在运行的游戏（Play In Editor、Standalone 或打包的 Development 构建），让 AI agent 可以驱动它。运行时连接不提供任何引擎工具：只有你的游戏注册的工具。

1. 让你的游戏模块依赖 `UnrealMcpRuntime`（而不是 `UnrealMcpEditor`），并完全按照上面的方式编写工具。
2. 启用 **Project Settings, Plugins, Unreal MCP (Runtime)**。该开关默认关闭，其值保存在 `DefaultGame.ini` 中。
3. 显式连接。游戏绝不会自行连接。

在 C++ 中：

```cpp
#include "UnrealMcpRuntimeSubsystem.h"

if (UUnrealMcpRuntimeSubsystem* Mcp = UUnrealMcpRuntimeSubsystem::Get(this))
{
    Mcp->RegisterToolProvider(MyToolProvider);
    Mcp->Connect(TEXT("http://localhost:8080"), TEXT("my-token"));
}
```

在 Blueprint 中，使用 **Get Unreal MCP Runtime Subsystem**，然后使用 **Connect**、**Disconnect** 和 **Is Connected**。在控制台中：

```text
UnrealMcp.Connect <host> [token]
UnrealMcp.Disconnect
```

`Connect()` 内置的安全规则：

- 除非编译时加入 `bUnrealMcpAllowShipping` 标志，否则 Shipping 构建会拒绝连接。
- 除非传入 `bAllowRemoteHost = true`，否则只允许 loopback 主机。
- 运行时模式仅适用于桌面平台（Windows、macOS、Linux）。

仓库中的 `samples/UnrealAIRuntimeSample` 插件新增了一个 `game-time-dilation` 工具，可在运行中的游戏里读取并设置世界的时间膨胀。如果你不需要运行时工具，请按照 [Installation](https://ai-game.dev/zh-hans/docs/wiki/unreal/installation#editor-only) 中所述，将插件从打包构建中排除。

---

Source (HTML): https://ai-game.dev/zh-hans/docs/wiki/unreal/custom-tools

Machine-translated (zh-Hans) from the English page: https://ai-game.dev/docs/wiki/unreal/custom-tools
