Advanced Usage
Custom Skills
Create your own skill packages and expose them as MCP tools.
Skill Directory Structure
my-custom-skill/
├── SKILL.md ← required manifest
└── scripts/
├── my_action.py ← becomes tool: my_custom_skill__my_action
└── another.pySKILL.md Format
---
name: my-custom-skill
description: "My custom Maya automation skill"
dcc: maya
version: "1.0.0"
tags: [maya, custom]
license: "MIT"
allowed-tools: ["Bash", "Read"]
depends: []
---
# my-custom-skill
Describe what this skill does.
## Scripts
- `my_action` — Description of this actionAction Script Structure
"""Module docstring — becomes the MCP tool description."""
# Import built-in modules
from typing import Any, Dict
# Import third-party modules
import maya.cmds as cmds
def main(
object_name: str,
value: float = 1.0,
) -> Dict[str, Any]:
"""Action docstring — shown in MCP tools/list.
Args:
object_name: The Maya object to operate on.
value: A numeric parameter with a default.
Returns:
dict with 'success' key and optional result data.
"""
# ... implementation ...
result = cmds.getAttr(f"{object_name}.translateY")
return {
"success": True,
"object": object_name,
"value": result,
}Register Your Skills
import dcc_mcp_maya
handle = dcc_mcp_maya.start_server(
extra_skill_paths=[
"/path/to/my-skills-folder",
"/another/skills/directory",
]
)Or use the environment variable:
# Semicolon-separated on Windows
set DCC_MCP_MAYA_SKILL_PATHS=C:\studio\maya-skills;C:\personal\skills
# Colon-separated on Linux/macOS
export DCC_MCP_MAYA_SKILL_PATHS=/studio/maya-skills:/personal/skillsRez-Shipped Studio Skills
A skill path is a search root. The root may be a single skill package with SKILL.md directly inside it, or a directory whose immediate children are skill packages. For Rez bundles, ship skills under a stable skills/ directory:
studio_maya_dev_skills/
└── skills/
└── lightbox-maya-dev/
├── SKILL.md
├── tools.yaml
└── scripts/Then append that directory in package.py:
def commands():
env.DCC_MCP_MAYA_SKILL_PATHS.append("{root}/skills")The adapter scans DCC_MCP_MAYA_SKILL_PATHS during register_builtin_actions() or start_server(). Gateway search, dcc_capability_manifest, search_skills, and load_skill all use that discovered catalog. After changing a Rez context or appending a new environment path, restart/reload the Maya plugin or start a new server so discovery runs again; load_skill only activates a skill already found by the startup scan.
Main-Thread Scheduling
Maya's UI and cmds operations must run on the main thread. The plugin entry points and startup helpers are written with that constraint in mind, and any custom code that touches Maya UI state should still be scheduled carefully.
If you write custom code that needs main-thread execution:
import maya.utils
def _my_operation():
import maya.cmds as cmds
cmds.polySphere()
# Schedule for next idle on main thread
maya.utils.executeDeferred(_my_operation)
# Or wait for result (use with caution — can deadlock if called from main thread)
result = maya.utils.executeInMainThreadWithResult(_my_operation)Server Configuration
Custom Port and Name
import dcc_mcp_maya
handle = dcc_mcp_maya.start_server(
port=9000,
server_name="maya-studio-2024",
)
print(handle.mcp_url()) # http://127.0.0.1:9000/mcpUsing MayaMcpServer Directly
For more control, use the class directly:
from dcc_mcp_maya.server import MayaMcpServer
server = MayaMcpServer(
server_name="maya-mcp",
)
server.register_builtin_actions(
extra_skill_paths=["/my/custom/skills"]
)
handle = server.start()
# Later:
server.stop()Check Server Status
from dcc_mcp_maya.server import _server_instance
if _server_instance and _server_instance.is_running:
print(f"Running at: {_server_instance.mcp_url}")Inspect Available Tools
Once the server is running, query the MCP endpoint:
# Set this from `dcc-mcp-cli list`, then list all available tools
curl "$MCP_INSTANCE_URL" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'Hot-Reload Skills
Skills can be reloaded without restarting the server (requires dcc-mcp-core v0.13+):
from dcc_mcp_maya.server import _server_instance
# Reload a specific skill
_server_instance._server.load_skill("my-custom-skill")Logging
Enable debug logging for troubleshooting:
import logging
logging.getLogger("dcc_mcp_maya").setLevel(logging.DEBUG)
logging.getLogger("dcc_mcp_core").setLevel(logging.DEBUG)Or set the environment variable:
set DCC_MCP_LOG_LEVEL=DEBUGLong-Running Scripts: defer=True
execute_python and any custom skill that returns a DeferredToolResult can keep the MCP request thread responsive while a long-running script (bake, render, simulation, IO) runs on Maya's idle queue.
When to use it
| Scenario | Recommendation |
|---|---|
| Quick query / attribute write (< 1s) | defer=False (default) — synchronous reply |
| Wall-clock 1–60s, no UI needed | defer=True — client polls until done |
| Multi-minute work (renders, caches) | defer=True + raise timeout_secs |
| Anything that must block the request | defer=False (default) |
From an MCP client (Claude / Cursor / Gemini)
// tools/call request
{
"jsonrpc": "2.0", "id": 7, "method": "tools/call",
"params": {
"name": "maya_scripting__execute_python",
"arguments": {
"code": "import maya.cmds as cmds\nfor f in range(1, 240):\n cmds.currentTime(f)\n cmds.refresh()\n",
"defer": true,
"timeout_secs": 600
}
}
}The server returns immediately with a deferred handle; dcc-mcp-core polls the handle every 100 ms and streams the final ToolResult back to the client when the script completes (or timeout_secs elapses).
Cancellation
Long-running scripts that opt into defer=True should also cooperate with cancellation:
from dcc_mcp_maya import check_maya_cancelled
for frame in frames:
check_maya_cancelled() # raises CancelledError when cancelled
cmds.currentTime(frame)
cmds.render()When the MCP client sends notifications/cancelled (or the dispatcher signals cancellation), check_maya_cancelled() raises and the deferred handle resolves with a structured error envelope.
Returning DeferredToolResult from a custom skill
Any skill can adopt the same pattern by importing the helper from dcc-mcp-core:
"""my_long_action.py"""
from typing import Any, Dict
def _runner(state: Dict[str, Any], target: str) -> None:
import maya.cmds as cmds
cmds.bakeResults(target, simulation=True, time=(1, 240))
state["result"] = {"success": True, "message": "Bake complete"}
state["done"] = True
def main(target: str, defer: bool = True, timeout_secs: float = 600.0):
if not defer:
# Synchronous fallback (blocks the request thread).
state: Dict[str, Any] = {"done": False, "result": None}
_runner(state, target)
return state["result"]
from dcc_mcp_core._server import DeferredToolResult # lazy import
state = {"done": False, "result": None}
def _kick() -> None:
_runner(state, target)
try:
import maya.utils
maya.utils.executeDeferred(_kick)
except ImportError:
# mayapy / standalone — no idle queue; run inline.
_kick()
return DeferredToolResult(
check_is_finished=lambda: state["result"] if state["done"] else None,
timeout_secs=float(timeout_secs),
poll_interval_secs=0.1,
)Declare it as execution: async in tools.yaml so the dispatcher allocates a worker slot:
- name: my_long_action
execution: async
affinity: main
timeout_hint_secs: 600
inputSchema:
type: object
properties:
target: { type: string }
defer: { type: boolean, default: true }
timeout_secs: { type: integer, default: 600, minimum: 1 }How the dispatcher routes deferred results
MayaMcpServer._executor duck-types the return value: if it exposes check_is_finished, the result is passed straight through to dcc-mcp-core's poll loop. No wrapping, no copy, no main-thread reflection. This means dispatcher errors raised before the deferred kick-off are still caught and returned as structured {"success": False, ...} envelopes, while the polled completion uses your skill's own envelope.
Configuration knobs
| Env / arg | Default | Effect |
|---|---|---|
defer=True (per-call argument) | false | Opt the call into the deferred path. |
timeout_secs (per-call argument) | 3600 | Hard timeout enforced by the core poll loop. |
poll_interval_secs (constructor) | 0.1 | How often the core re-checks check_is_finished. |
Tool's timeout_hint_secs (tools.yaml) | — | Advisory hint surfaced to the MCP host UI. |
Production Considerations
Security
The MCP server listens on 127.0.0.1 (localhost) by default — it is not accessible from other machines. If you need network access, use a reverse proxy with authentication.
Resource Management
The server runs in a background thread and consumes minimal resources when idle. For long-running Maya sessions, monitor the server health via:
import dcc_mcp_maya
# If handle is alive, server is running
handle = dcc_mcp_maya.start_server()
print(handle.port)Studio Integration
For studio deployments, use userSetup.py with conditional startup:
# userSetup.py
import os
import maya.cmds as cmds
def _start_mcp_if_enabled():
if os.environ.get("DCC_MCP_MAYA_AUTOSTART", "1") == "0":
return
import dcc_mcp_maya
handle = dcc_mcp_maya.start_server()
print(f"[studio] Maya MCP: {handle.mcp_url()}")
cmds.evalDeferred(_start_mcp_if_enabled, lowestPriority=True)Use cmds.evalDeferred rather than maya.utils.executeDeferred here. The former drains as part of Maya's start-up sequence; the latter waits for an idle main thread, which during a GUI start-up may never arrive — the plug-in then silently never starts.