Plugin API Reference¶
API_VERSION: 1
Python Side (unity_mcp.plugin_api)¶
Import ONLY from plugin_api — never from unity_mcp.tools._annotations or unity_mcp.tools.gating directly.
Entry Point: register(mcp, send_fn, args_fn)¶
Every plugin module must export this function. Called once at server startup.
| Param | Type | Description |
|---|---|---|
mcp | FastMCP | Call @mcp.tool() to register tools |
send_fn | async (cmd, args, timeout?) -> str | Send command to Unity via TCP bridge |
args_fn | (**kwargs) -> dict | Build args dict, drops None values |
Tool Annotations¶
| Constant | Meaning | Use for |
|---|---|---|
RO | Read-only | Queries: get_component, inspect |
RW | Read-write | Mutations: create_object, batch |
RW_IDEM | Idempotent write | Set operations: set_property |
DEL | Destructive | Deletions: delete_object |
register_tools(category, tools)¶
Register tools into capability gating.
category: str— gating category nametools: set[str]— tool names
Auto-gating: If a plugin uses @mcp.tool() without calling register_tools(), those tools are automatically enrolled into the hidden "plugins" category (visible only via discover_tools(category="plugins")). This prevents undeclared plugin tools from cluttering the default Tier1 budget.
register_read_cmds(*names)¶
Mark C# commands as read-only for middleware classification.
register_write_cmds(*names)¶
Mark C# commands as mutating for middleware.
register_dsl_tools(*names)¶
Mark tools that need Python-side DSL expansion. Blocks these from batch().
register_features(features)¶
Register feature metadata for token budget system.
register_features({"my_feature": {"priority": "low", "difficulty": 0.3, "est_in": 200, "est_out": 100, "image": False}})
API_VERSION¶
Integer. Current: 1. Set REQUIRED_API_VERSION in your module to enforce minimum.
C# Side¶
IMCPPlugin Interface¶
public interface IMCPPlugin
{
string Name { get; } // Unique plugin identifier
string CommandPrefix { get; } // Prefix for filtering (e.g. "myext_")
void RegisterCommands(); // Called on registration + domain reload
void OnDomainReload(); // Cleanup hook on script recompile
// Optional: Register additional commands beyond CommandPrefix matching
IReadOnlyList<string> AdditionalCommands => Array.Empty<string>();
// Optional: Organize tools into subcategories (v0.56.0+)
string GetToolSubcategory(string command) => null; // Return subcategory string or null
// Optional: Settings UI for the Plugins settings page
VisualElement BuildSettingsUI() => null; // Return null to skip
bool HasSettingsUI => false; // Override alongside BuildSettingsUI
// Optional: Short description for the plugin card
string Description => "";
}
Subcategories: Tools can be organized in the UI by returning a category string like "Animation" or "Physics". Return null or empty string for top-level placement.
Settings UI: Implement BuildSettingsUI() and set HasSettingsUI => true to show a per-plugin settings panel on the Plugins settings page.
Tool Grouping: The PluginToolGrouping table allows fine-grained organization of tool visibility per subcategory.
PluginRegistry¶
Idempotent by Name — duplicate registrations silently ignored.
CommandRegistry¶
// Read-only command
CommandRegistry.Register("my_query", handler);
// Mutating command
CommandRegistry.Register("my_mutate", handler, mutating: true);
// Runtime-only command (Play Mode)
CommandRegistry.Register("my_runtime", handler, runtime: true);
Handler signature: Func<string, string> — receives JSON args string, returns response string.
JsonHelper¶
Public methods available to plugins:
// Parse incoming args (public)
var value = JsonHelper.ExtractString(args, "param_name");
var unescaped = JsonHelper.UnescapeJsonString(raw);
FormatResponse, EscapeJson, ExtractObject, ExtractArray, and the Format* family are internal — not callable from external assemblies. Return plain text from your handler; the framework wraps it in the wire protocol automatically:
CommandRegistry.Register("my_query", args =>
{
var filter = JsonHelper.ExtractString(args, "name_filter");
return $"count: {SomeLogic(filter)}"; // plain string, no manual JSON wrapping needed
});
Plugin Discovery (3 sources, in order)¶
- Built-in —
server/src/unity_mcp/plugins/(pkgutil scan, internal only) - Entry points —
[project.entry-points."unity_mcp.plugins"]in pyproject.toml - UNITY_MCP_PLUGIN_DIRS — env var, OS pathsep-separated directories
Environment Variables¶
| Variable | Effect |
|---|---|
UNITY_MCP_SKIP_PLUGINS | Comma-separated prefixes to skip |
UNITY_MCP_PLUGIN_DIRS | Additional plugin directories |
Plugin Lifecycle¶
Server startup
└─ load_plugins(mcp, send_fn, args_fn)
├─ 1. Built-in (pkgutil)
├─ 2. Entry points (pip packages)
└─ 3. UNITY_MCP_PLUGIN_DIRS
└─ For each: import → check API_VERSION → register()
Unity domain reload
└─ PluginRegistry.OnDomainReload()
└─ For each: OnDomainReload() → RegisterCommands()
Error Handling¶
- Python: exceptions in
register()caught, logged, plugin skipped — server continues - C#: duplicate
Namesilently ignored (idempotent by design) - Version mismatch:
REQUIRED_API_VERSION > API_VERSION→ plugin skipped with warning
Assembly Setup (C#)¶
Extension code MUST NOT have its own .asmdef if it needs to reference types from Assembly-CSharp (e.g. custom gameplay types). Place files in Assets/ (not Assets/Plugins/) to compile into Assembly-CSharp-Editor.
UnityMCP.Editor asmdef has autoReferenced: true — your code sees it automatically.