Skip to content

Getting Started

Installation

CLI from GitHub Releases

bash
curl -fsSL https://raw.githubusercontent.com/loonghao/dcc-mcp-core/main/scripts/install-cli.sh | sh

# Windows PowerShell
powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/loonghao/dcc-mcp-core/main/scripts/install-cli.ps1 | iex"

This installs the standalone dcc-mcp-cli control-plane binary from the latest GitHub Release. Pin a release with DCC_MCP_VERSION=v0.17.17.

From PyPI

bash
pip install dcc-mcp-core

From Source (requires Rust toolchain)

bash
git clone https://github.com/loonghao/dcc-mcp-core.git
cd dcc-mcp-core
pip install -e .

TIP

Building from source requires the Rust toolchain. Install it from rustup.rs. The build is handled by maturin which compiles the Rust core and installs the Python package.

Requirements

  • Python: >= 3.7 (CI tests 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13)
  • Rust: >= 1.95 (for building from source)
  • License: MIT
  • Python Dependencies: Zero — everything is in the compiled Rust extension

Quick Start

The fastest way to expose scripts as MCP tools. Create a SKILL.md in your script folder, then use create_skill_server to wire everything in one call:

python
import os
from dcc_mcp_core import create_skill_server, McpHttpConfig

# Point to your skill directories (per-app env var)
os.environ["DCC_MCP_MAYA_SKILL_PATHS"] = "/path/to/my-skills"

# One call: discover skills + start MCP HTTP server
server = create_skill_server("maya", McpHttpConfig(port=8765))
handle = server.start()
print(f"Maya MCP server at {handle.mcp_url()}")
# AI clients (Claude Desktop, etc.) connect to http://127.0.0.1:8765/mcp

Or use SkillCatalog directly for more control:

python
import os
from dcc_mcp_core import SkillCatalog, ToolRegistry

os.environ["DCC_MCP_SKILL_PATHS"] = "/path/to/my-skills"

registry = ToolRegistry()
catalog = SkillCatalog(registry)

discovered = catalog.discover(dcc_name="maya")
print(f"Discovered {discovered} skills")

# Load a skill and inspect the registered tool names
tool_names = catalog.load_skill("maya-geometry")
print(tool_names)

See the Skills System guide for writing SKILL.md files and advanced options.

Writing a Minimal SKILL.md

Create a skill in three steps:

bash
# 1. Create the skill directory structure
mkdir -p my-skill/scripts

# 2. Write SKILL.md (agentskills.io top-level fields + metadata.dcc-mcp pointers)
cat > my-skill/SKILL.md << 'EOF'
---
name: my-skill
description: "Does something useful in Maya. Use when user asks to do X."
metadata:
  dcc-mcp:
    dcc: maya
    version: "1.0.0"
    search-hint: "keyword1, keyword2, related task"
    tools: tools.yaml
---

# My Skill

Instructions for the AI agent on how to use this skill.
EOF

# 3. Declare tools in a sibling file
cat > my-skill/tools.yaml << 'EOF'
tools:
  - name: do_thing
    description: Do a useful Maya task.
    source_file: scripts/do_thing.py
EOF

# 4. Add a script
cat > my-skill/scripts/do_thing.py << 'EOF'
import sys, json

def main():
    params = json.loads(sys.stdin.read())
    # ... do work ...
    print(json.dumps({"success": True, "message": "Done"}))

if __name__ == "__main__":
    main()
EOF

Then set DCC_MCP_SKILL_PATHS to the parent directory and use create_skill_server or SkillCatalog.discover().

Tool Registry

python
from dcc_mcp_core import ToolRegistry

registry = ToolRegistry()
registry.register(
    name="create_sphere",
    description="Creates a sphere in the scene",
    category="geometry",
    tags=["geometry", "creation"],
    dcc="maya",
)

tool = registry.get_action("create_sphere")
print(tool)  # dict with tool metadata

maya_tools = registry.list_actions(dcc_name="maya")

Action → Tool terminology

In v0.13+, the project renamed "action" → "tool" at the conceptual level. However, some Rust API method names (get_action, list_actions, search_actions) still use "action" for backward compatibility. These are not bugs — they are compatibility aliases.

Tool Results

python
from dcc_mcp_core import success_result, error_result

result = success_result("Created 5 spheres", prompt="Use modify next", count=5)
print(result.success)  # True
print(result.message)  # "Created 5 spheres"
print(result.context)  # {"count": 5}

err = error_result("Failed", "File not found", prompt="Check path")
print(err.success)  # False

Event Bus

python
from dcc_mcp_core import EventBus

bus = EventBus()
sid = bus.subscribe("scene.changed", lambda: print("Scene updated!"))
bus.publish("scene.changed")
bus.unsubscribe("scene.changed", sid)

MCP HTTP Server

Expose your registry to AI clients (Claude Desktop, etc.) over HTTP in one call:

python
from dcc_mcp_core import ToolRegistry, McpHttpServer, McpHttpConfig

registry = ToolRegistry()
# ... register tools or load skills ...

config = McpHttpConfig(port=8765)
server = McpHttpServer(registry, config)
handle = server.start()

print(f"MCP server running at {handle.mcp_url()}")
# handle.shutdown() to shut down

Job lifecycle notifications

Every tools/call emits SSE notifications on completion (issue #326):

  • notifications/progress — fires when the call included _meta.progressToken.
  • notifications/$/dcc.jobUpdated — fires on every status transition while McpHttpConfig.enable_job_notifications is True (default).
  • notifications/$/dcc.workflowUpdated — emitted by the workflow executor (#348).

Disable the $/dcc.* channels with cfg.enable_job_notifications = False; the spec-mandated progress channel still fires whenever a token is supplied.

Instance-Bound Diagnostics

When multiple DCC instances run side-by-side (two Maya processes, Maya + Blender, etc.), each adapter server should be bound to its own DCC process so diagnostics (screenshot, audit log, metrics) target the right window and PID.

DccServerBase accepts three optional instance-binding kwargs and exposes four diagnostics__* MCP tools:

python
from dcc_mcp_core import DccServerBase

class MayaServer(DccServerBase):
    def __init__(self, pid: int, window_title: str):
        super().__init__(
            dcc_name="maya",
            builtin_skills_dir=None,
            dcc_pid=pid,                   # owner DCC PID
            dcc_window_title=window_title, # fallback match when PID lookup fails
            # dcc_window_handle=0x00A1B2,  # or pass an HWND directly
        )

server = MayaServer(pid=12345, window_title="Autodesk Maya 2024")
handle = server.start()  # exposes diagnostics__screenshot / audit_log /
                         # tool_metrics / process_status tools bound to
                         # this Maya instance only

If the PID can change at runtime, consult your adapter for how it refreshes diagnostics bindings; dcc_pid on :class:DccServerOptions is resolved at construction time.

python
from pathlib import Path

from dcc_mcp_core import DccServerBase
from dcc_mcp_core._server.options import DccServerOptions

skills_dir = Path(__file__).parent / "skills"
opts = DccServerOptions.from_env("maya", skills_dir, dcc_pid=12345)
server = DccServerBase(opts)

For low-level servers built around McpHttpServer directly, call register_diagnostic_mcp_tools(server, dcc_name=..., dcc_pid=...) beforeserver.start() — per the "register all actions before start" rule.

Development Setup

bash
git clone https://github.com/loonghao/dcc-mcp-core.git
cd dcc-mcp-core

# Install with vx (recommended)
vx just install

# Or manual setup
pip install maturin
maturin develop

Running Tests

bash
vx just test
vx just lint

Next Steps

Troubleshooting

Build/Import Errors

bash
# Symbol in __init__.py but ImportError → rebuild the dev wheel
vx just dev

# Verify import works
python -c "import dcc_mcp_core; print(hasattr(dcc_mcp_core, 'MyNewSymbol'))"

# Verbose cargo build to catch errors
cargo build --workspace --features python-bindings 2>&1 | grep -E "error|warning" | head -30

Common Mistakes

ProblemSolution
scan_and_load returns wrong resultsAlways unpack: skills, skipped = scan_and_load(...) — it returns a 2-tuple
success_result context is emptyPass kwargs directly: success_result("msg", count=5) — NOT context={"count":5}
ToolDispatcher.call() not foundUse .dispatch(name, json_str) — there is no .call() method
McpHttpServer tools not appearingRegister all tools BEFORE server.start() — the server reads the registry at startup
SkillPolicy ImportErrorSkillScope is exported for Python introspection; policy checks still belong on SkillMetadata methods and metadata.dcc-mcp.* policy keys
Main-thread dispatch confusionPrefer HostExecutionBridge / InProcessCallableDispatcher via DccServerOptions; use low-level DeferredExecutor only with /guide/dcc-thread-safety
Skill scripts not discoveredCheck DCC_MCP_SKILL_PATHS env var and dcc: field in SKILL.md matches your filter
ToolMeta AttributeErrorRust-only type. Use ToolRegistry.set_tool_enabled() and list_tools_in_group() instead

AI Agent Best Practices

When building tools for AI agents to consume:

  1. Design around user workflows, not raw API calls. A tool called create_character is better than three separate calls to create_joint, bind_skin, apply_animation.
  2. Use ToolAnnotations to signal safety properties — read_only_hint=True, destructive_hint=False, idempotent_hint=True — so AI clients make informed choices.
  3. Return human-readable errors via error_result("msg", "specific error") with actionable suggestions in prompt.
  4. Use next-tools inside sibling tools.yaml declarations to guide AI agents to follow-up tools (e.g. on-failure: [dcc_diagnostics__screenshot]).
  5. Keep tools/list small by using tool groups with default_active=false for power-user features. Agents activate groups on demand.
  6. Validate all AI-provided inputs with ToolValidator.from_schema_json() before execution — never trust LLM output blindly.
  7. Write action-oriented descriptions — describe what the tool does and when to use it in the first sentence. Include specific keywords so search_skills() can match. Bad: "Helper for geometry." Good: "Create a polygon sphere with configurable radius and subdivisions. Use when the user asks to create a sphere, ball, or round 3D object in Maya."
  8. Always provide on-failure chains for domain skills — point to dcc_diagnostics__screenshot and dcc_diagnostics__audit_log so agents can debug failures automatically.
  9. Declare dependencies under metadata.dcc-mcp.depends in every domain skill — ensures diagnostics are loaded before the skill's tools become available.
  10. Tag every skill with metadata.dcc-mcp.layer — infrastructure, domain, thin-harness, or example. Untagged skills cause routing ambiguity as the catalog grows.

MCP Tool Design Checklist

Before registering a new tool, verify:

  • [ ] Single responsibility: Tool does one clear thing (not a kitchen-sink endpoint)
  • [ ] Descriptive name: Follows {skill}__{action} naming; self-explanatory action
  • [ ] Input schema: JSON Schema with per-parameter descriptions (≤100 chars each)
  • [ ] Output schema: Returns ToolResult via ToolResult.ok() / ToolResult.fail() — never raw dicts
  • [ ] ToolAnnotations: Set read_only_hint, destructive_hint, idempotent_hint, open_world_hint
  • [ ] Error taxonomy: Document error codes in error_result() with actionable prompt suggestions
  • [ ] Follow-up guidance: next-tools.on-success for the logical next step; next-tools.on-failure pointing to diagnostics
  • [ ] Description quality: Includes "what" + "when to use" + keywords for discoverability

Building a DCC Adapter with DccServerBase

DccServerBase is the recommended base class for building DCC adapters. It bundles all the boilerplate that every adapter needs:

python
from pathlib import Path
from dcc_mcp_core import DccServerBase, DccServerOptions

class BlenderMcpServer(DccServerBase):
    def __init__(self, port: int = 8765, **kwargs):
        opts = DccServerOptions.from_env(
            "blender",
            Path(__file__).parent / "skills",
            port=port,
            **kwargs,
        )
        super().__init__(options=opts)

    def _version_string(self) -> str:
        import bpy
        return bpy.app.version_string

# That's it — skill management, hot-reload, gateway election are all inherited.
server = BlenderMcpServer(gateway_port=9765)
server.register_builtin_actions()  # discover and load skills
server.enable_hot_reload()         # optional: auto-reload on file changes
handle = server.start()            # returns McpServerHandle
print(f"Running at {handle.mcp_url()}")

For zero-boilerplate adapters, use make_start_stop:

python
from dcc_mcp_core import make_start_stop

start_server, stop_server = make_start_stop(
    BlenderMcpServer,
    hot_reload_env_var="DCC_MCP_BLENDER_HOT_RELOAD",
)

DeferredExecutor — DCC Main-Thread Safety

Many DCCs (Maya, Blender, Houdini) require that API calls execute on the main thread. DeferredExecutor provides a task queue that the DCC event loop polls:

python
from dcc_mcp_core._core import DeferredExecutor  # low-level bridge; prefer HostExecutionBridge for adapters

# Create a queue (capacity = max pending tasks)
executor = DeferredExecutor(capacity=16)

# Submit a callable from any thread (e.g. from MCP HTTP handler)
executor.execute(lambda: maya.cmds.sphere(radius=1.0))

# In the DCC main-loop callback (e.g. Maya's idleCallback, Blender's app.handlers):
executor.poll_pending()  # runs all queued callables on the main thread

Note: DeferredExecutor is not yet in the public __init__.py — import directly from dcc_mcp_core._core. This will be promoted to the public API in a future release.

Released under the MIT License.