Authoring a plugin
Authoring a plugin documentation.
Everything you need to build and publish a plugin. If you only want to use a plugin somebody else wrote, see Using plugins instead.
Two capabilities have their own pages: Native C++ modules (shipping engine APIs the bridge doesn't expose) and Guards (gating every bridge call).
Quick scaffolder
ue-mcp plugin create my-thing
cd my-thing
npm install
npm run build
npm run check # validate the manifest + task wiringThe scaffold is a superset: it ships every way a plugin can extend ue-mcp, wired and working, so you never have to discover a capability before using it. Keep the shapes you want and delete the rest.
| Shape | What it scaffolds |
|---|---|
inject | An action added onto a built-in category, e.g. project(action="[prefix]_hello"). |
provides | A brand-new top-level category the plugin owns (actions unprefixed), e.g. [prefix](action="greet"). |
flows | A chained, one-call orchestration ([prefix]_demo). |
nativeModule | A compile-ready C++ handler skeleton under ue/Plugins/[UePlugin]/, dormant by default. |
The native C++ module is the one shape that is scaffolded but not active: declaring nativeModule: makes ue-mcp plugin install deploy the module and force a UE rebuild, so the manifest block ships commented out. The C++ source still lands on disk - uncomment the block in ue-mcp.plugin.yml and reinstall to activate it. No separate flag, no compile cost until you opt in. See Native C++ modules.
The scaffold also stamps package.json, tsconfig.json, a src/index.ts entry, knowledge/, a scripts/check.mjs validator, LICENSE, and .gitignore.
Package layout
my-plugin/
package.json
tsconfig.json
ue-mcp.plugin.yml # author declaration: actionPrefix, inject, provides, tasks, flows, nativeModule
src/ # author writes TypeScript here
index.ts # package entry (tasks load by class_path, but `main` points here)
tasks/
MyAction.ts # one UeMcpTask subclass per file, default export
shared/ # optional cross-task helpers (never referenced from the declaration)
dist/ # tsc output - what actually ships and loads
tasks/
MyAction.js
ue/ # dormant native C++ module (only deployed when nativeModule: is uncommented)
Plugins/
MyPlugin/
scripts/
check.mjs # pre-publish validator: manifest parses, task refs resolve
knowledge/
gameplay.md # one markdown file per target category
LICENSE
README.mdConventions:
- One task class per file, default export, extending
UeMcpTaskfromue-mcp/task. class_pathin the declaration is resolved against the plugin'sdist/(the loader triesdist/[path].jsthendist/tasks/[path].js).src/shared/holds helpers; never reference it from the declaration.- Compile to
dist/withtscso users need no TypeScript toolchain.
package.json
{
"name": "my-plugin",
"version": "0.1.0",
"description": "My custom actions for ue-mcp",
"type": "module",
"main": "dist/index.js",
"files": ["dist", "ue-mcp.plugin.yml", "knowledge", "README.md"],
"keywords": ["unreal-engine"],
"peerDependencies": {
"ue-mcp": ">=1.0.65"
},
"devDependencies": {
"ue-mcp": "^1.0.65",
"typescript": "^5.7.0"
},
"scripts": {
"build": "tsc"
}
}UeMcpTask (and the types you import alongside it) come from ue-mcp/task - a thin, server-free entry point that the consumer's installed ue-mcp already provides. That's why ue-mcp is a peer dependency: the running server supplies the copy at load time, so your task extends the same base class the server uses. The matching dev dependency is only there to type-check your build. You never depend on the underlying flow runtime directly - it stays an implementation detail behind ue-mcp/task.
ue-mcp.plugin.yml
This is the only file ue-mcp reads from your package. Authored once; never edited by users.
actionPrefix: mypfx # mandatory, lowercase, must match /^[a-z][a-z0-9_]*$/
minServerVersion: 1.0.15 # optional - the server enforces this at install and load
uePluginDependency: SomePlugin # optional - .uplugin filename to check in .uproject
inject:
gameplay:
inspect_something: # -> gameplay(action="mypfx_inspect_something")
task: mypfx.inspect_something
description: "Inspect some game state during a PIE session."
schema:
actorLabel: { type: string, required: true }
includeComponents: { type: boolean }
tasks:
mypfx.inspect_something:
class_path: tasks/InspectSomething
description: "Inspect game state for a given actor"The key under each category is the bare action name. The loader prepends your actionPrefix to compute the injected name: mypfx + inspect_something -> mypfx_inspect_something. The user always sees the prefixed form.
knowledge: and flows: are optional - omit them when you have nothing to attach. A plugin can ship a single action and nothing else.
Param schemas under schema: accept these types: string, number, boolean, object, array. Non-required params become optional at the top level of the host category tool's schema.
Providing new categories (provides:)
When the plugin's actions don't belong inside any built-in category, declare a provides: block. Each entry registers a brand-new top-level MCP category that the plugin owns. Action names are NOT prefixed inside provided categories - the category itself is the namespace.
actionPrefix: terrain # still required (used for any inject: entries)
provides:
terrain_sculpt: # -> terrain_sculpt(action="sample_density", ...)
description: "Terrain sculpting operations"
actions:
sample_density:
task: terrain_sculpt.sample_density
description: "Sample density values along a curve through the terrain"
schema:
start: { type: array, required: true }
end: { type: array, required: true }
steps: { type: number }
tasks:
terrain_sculpt.sample_density:
class_path: tasks/SampleDensityRules:
- Provided category names must match
/^[a-z][a-z0-9_]*$/. - A provided name may not collide with a built-in category. The CLI fails install with the offending name; the runtime loader skips the plugin with a clear status reason.
- Inter-plugin collisions resolve first-writer-wins. If two installed plugins both
provides: terrain_sculpt, the one earlier in yourplugins:array claims the name; the other is skipped with a warning visible inplugins(list). - Knowledge files keyed by a provided category name (
knowledge/terrain_sculpt.md) attach to that category's AI-facing docs the same way they do for injected categories.
A plugin can mix inject: and provides: freely - whatever fits each action best. To back actions with C++ instead of TypeScript tasks, see Native C++ modules.
Writing tasks
// src/tasks/InspectSomething.ts
import { UeMcpTask, type TaskResult } from "ue-mcp/task";
interface Options {
actorLabel: string;
includeComponents?: boolean;
}
export default class InspectSomething extends UeMcpTask<Options> {
get taskName() { return "mypfx.inspect_something"; }
async execute(): Promise<TaskResult> {
const { actorLabel, includeComponents = false } = this.options;
const details = await this.call("level.get_actor_details", {
actorLabel,
includeComponents,
});
if (!details.success) return details;
// ... process the result ...
return { success: true, data: details.data };
}
}Notes:
- Compose existing actions through
this.call('[category].[action]', params). Don't reach into the bridge directly unless you have to - composition gives you free observability and rollback hooks. When you do need a raw bridge method that no task wraps,UeMcpTaskgives you a typedthis.bridge.call(method, params)and a typedthis.ctx(bridge,project) with no casting. - Use the real parameter names of the host task you're calling. Param name drift between TS and C++ is how silent failures start.
- If your task makes multi-step mutations, return a
rollbackrecord so users can opt intorollback_on_failure: trueon the wrapping flow. - Throw, don't return success-with-error-data. The runtime catches throws and turns them into structured failures.
A task can also be a guard that gates every bridge call, not just an action's implementation.
Knowledge files
For each category your plugin injects into, ship a short markdown file under knowledge/. The server attaches it to that category's AI-facing docs at boot, so the agent sees plugin-specific guidance the moment it looks at that category.
Keep it terse - one screenful per category. Concrete examples beat prose. The agent already knows how the category works; the knowledge file is just the delta the plugin introduces.
# PIE Studio - gameplay actions
33 actions for PIE input recording, replay, observation, and injection.
Quick start:
1. `gameplay(action="pie_record_arm", sample_hz=60)` - arm the recorder
2. Press Play, do your thing, stop PIE
3. `gameplay(action="pie_replay_arm", recording_id="[id]", eject=true, time_scale=0.1)` - replay at 10%Publishing
npm run build # tsc -> dist/
npm publish # public registryUsers install with:
ue-mcp plugin install <your-package-name>Before publishing, confirm your plugin passes every validation rule.