Skip to content

Skills API

dcc_mcp_core.SkillCatalog, dcc_mcp_core.SkillScanner, dcc_mcp_core.SkillWatcher, dcc_mcp_core.SkillMetadata, dcc_mcp_core.SkillSummary, dcc_mcp_core.ToolDeclaration, dcc_mcp_core.parse_skill_md, dcc_mcp_core.scan_and_load

dcc_mcp_core.skill (pure-Python): skill_entry, skill_success, skill_error, skill_warning, skill_exception, run_main

SkillCatalog

Progressive skill discovery and loading. Thread-safe (all state stored in DashMap/DashSet).

The Python binding is registry-backed: construct SkillCatalog with an ToolRegistry. Loading a skill registers its tool metadata into that registry on demand.

python
from dcc_mcp_core import SkillCatalog, ToolRegistry

registry = ToolRegistry()
catalog = SkillCatalog(registry)

Constructor

python
SkillCatalog(registry: ToolRegistry) -> SkillCatalog
ParameterTypeDescription
registryToolRegistryAction registry for registering skill tools

Methods

MethodReturnsDescription
discover(extra_paths=None, dcc_name=None)intScan for skills and populate the catalog; returns number of newly discovered skills
load_skill(skill_name)List[str]Load a skill; returns list of registered action names. Raises ValueError if not found
unload_skill(skill_name)intUnload a skill; returns number of actions removed. Raises ValueError if not loaded
search_skills(query=None, tags=None, dcc=None, scope=None, limit=None)List[SkillSummary]Unified discovery with scope ("repo" | "user" | "system" | "admin") and limit. Empty call returns top skills by scope precedence (Admin > System > User > Repo).
list_skills(status=None)List[SkillSummary]List skills. status: "loaded" or "unloaded", or None for all
get_skill_info(skill_name)dict | NoneFull metadata for a skill, or None if not found
is_loaded(skill_name)boolWhether a skill is currently loaded
loaded_count()intNumber of loaded skills
__repr__()strString representation

Example

python
import os
from dcc_mcp_core import SkillCatalog, ToolRegistry

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

registry = ToolRegistry()
catalog = SkillCatalog(registry)

# Discover skills
catalog.discover(extra_paths=["/extra/skills"], dcc_name="maya")

# List all discovered skills
for skill in catalog.list_skills():
    status = "loaded" if skill.loaded else "unloaded"
    print(f"  [{status}] {skill.name} v{skill.version}: {skill.description}")

# Search
results = catalog.search_skills(query="geometry", tags=["create"])
for s in results:
    print(f"  {s.name}: {s.tool_count} tools → {s.tool_names}")

# Load a skill
actions = catalog.load_skill("maya-geometry")
print(f"Loaded actions: {actions}")

# Get full metadata
meta = catalog.get_skill_info("maya-geometry")
if meta:
    print(meta["name"], len(meta["tools"]))

# Inspect loaded skills
print(catalog.loaded_count())

# Unload
removed = catalog.unload_skill("maya-geometry")
print(f"Unloaded {removed} actions")

SkillSummary

Lightweight summary returned by SkillCatalog.search_skills() and list_skills().

Properties (read-only)

PropertyTypeDescription
namestrSkill name
descriptionstrShort description
search_hintstrKeyword hint for search (from search-hint: in SKILL.md; falls back to description)
tagsList[str]Skill tags
dccstrTarget DCC (e.g. "maya")
versionstrSkill version
tool_countintNumber of declared tools
tool_namesList[str]Names of declared tools
loadedboolWhether the skill is currently loaded

Dunder Methods

MethodDescription
__repr__SkillSummary(name='...', loaded=True)

ToolDeclaration

A single tool declaration within a skill, parsed from SKILL.md frontmatter tools: list.

python
from dcc_mcp_core import ToolDeclaration

decl = ToolDeclaration(
    name="create_sphere",
    description="Create a polygon sphere",
    input_schema='{"type":"object","properties":{"radius":{"type":"number"}}}',
    read_only=False,
    destructive=False,
    idempotent=False,
    defer_loading=True,
    source_file="scripts/create_sphere.py",
)

Constructor

python
ToolDeclaration(
    name: str,
    description: str = "",
    input_schema: str | None = None,    # JSON Schema string
    output_schema: str | None = None,   # JSON Schema string
    read_only: bool = False,
    destructive: bool = False,
    idempotent: bool = False,
    defer_loading: bool = False,
    source_file: str = "",
) -> ToolDeclaration

Fields (read-write)

FieldTypeDefaultDescription
namestrrequiredTool name (unique within the skill)
descriptionstr""Human-readable description
read_onlyboolFalseTrue if this tool only reads data (no side effects)
destructiveboolFalseTrue if this tool may cause destructive changes
idempotentboolFalseTrue if same args always produce the same result
defer_loadingboolFalseParse defer-loading: / defer_loading: from SKILL.md for discovery-oriented UIs
source_filestr""Explicit path to the script file

input_schema and output_schema

These are stored internally as JSON values, not strings. When constructing from Python, pass a JSON string and it will be parsed automatically.

Progressive loading signal

Unloaded skill stubs surfaced via tools/list now include annotations.deferredHint = true. After load_skill(...), the real tools appear with deferredHint = false.


SkillMetadata

Parsed from a skill's SKILL.md frontmatter. Supports Anthropic Skills, ClawHub/OpenClaw, and dcc-mcp-core extensions simultaneously.

python
from dcc_mcp_core import SkillMetadata

meta = SkillMetadata(
    name="maya-geometry",
    description="Maya geometry tools",
    tools=[],        # List[str] — tool names
    dcc="maya",
    tags=["geometry"],
    search_hint="polygon modeling, sphere, bevel, mesh",
    scripts=[],      # List[str] — discovered script paths
    skill_path="/path/to/maya-geometry",
    version="1.0.0",
    depends=[],
    metadata_files=[],
)

Constructor

python
SkillMetadata(
    name: str,
    description: str = "",
    tools: List[str] | None = None,
    dcc: str = "python",
    tags: List[str] | None = None,
    search_hint: str = "",
    scripts: List[str] | None = None,
    skill_path: str = "",
    version: str = "1.0.0",
    depends: List[str] | None = None,
    metadata_files: List[str] | None = None,
) -> SkillMetadata

Fields (read-write)

FieldTypeDescription
namestrUnique skill name
descriptionstrShort description
search_hintstrKeyword hint for search_skills (SKILL.md search-hint: field; falls back to description)
toolsList[str]Tool names from frontmatter
dccstrTarget DCC application
tagsList[str]Classification tags
scriptsList[str]Discovered script file paths
skill_pathstrAbsolute path to skill directory
versionstrSkill version
dependsList[str]Dependency skill names
metadata_filesList[str]Paths to .md files in metadata/

SkillScanner

Scanner for discovering skill packages in directories. Caches file modification times for efficient repeated scans.

python
from dcc_mcp_core import SkillScanner

scanner = SkillScanner()

Methods

MethodReturnsDescription
scan(extra_paths=None, dcc_name=None, force_refresh=False)List[str]Scan paths for skill directories
clear_cache()Clear the mtime cache and discovered list

Properties

PropertyTypeDescription
discovered_skillsList[str]Previously discovered skill directory paths

Dunder Methods

MethodDescription
__repr__SkillScanner(cached=N, discovered=N)

SkillWatcher

Hot-reload watcher for skill directories. Monitors filesystem events and reloads skill metadata when SKILL.md files change.

python
from dcc_mcp_core import SkillWatcher

watcher = SkillWatcher(debounce_ms=300)
watcher.watch("/path/to/skills")
skills = watcher.skills()

Constructor

python
SkillWatcher(debounce_ms: int = 300) -> SkillWatcher

debounce_ms: Milliseconds to wait before reloading after a change (multiple rapid events coalesced).

Methods

MethodReturnsDescription
watch(path)Start watching path recursively. Raises RuntimeError if path does not exist
unwatch(path)boolStop watching path. Returns True if was being watched
skills()List[SkillMetadata]Snapshot of all currently loaded skills
skill_count()intNumber of skills currently loaded
watched_paths()List[str]List of currently watched directory paths
reload()Manually trigger a full reload
__repr__strString representation

Functions

parse_skill_md

python
parse_skill_md(skill_dir: str) -> SkillMetadata | None

Parse a SKILL.md from a skill directory. Returns None if missing or invalid.

  • Extracts YAML frontmatter between --- delimiters
  • Enumerates scripts in scripts/ subdirectory
  • Discovers .md files in metadata/ subdirectory

scan_skill_paths

python
scan_skill_paths(
    extra_paths: List[str] | None = None,
    dcc_name: str | None = None,
) -> List[str]

Convenience wrapper: creates a SkillScanner and returns discovered skill directory paths.

scan_and_load

python
scan_and_load(
    extra_paths: List[str] | None = None,
    dcc_name: str | None = None,
) -> tuple[List[SkillMetadata], List[str]]

Full pipeline: scan directories, load all skills, and topologically sort by dependencies.

Returns (ordered_skills, skipped_dirs). Raises ValueError on missing dependencies or cycles.

scan_and_load_lenient

python
scan_and_load_lenient(
    extra_paths: List[str] | None = None,
    dcc_name: str | None = None,
) -> tuple[List[SkillMetadata], List[str]]

Same as scan_and_load but silently skips skills with missing dependencies (warns via logging). Only cyclic dependencies raise ValueError.

Returns (ordered_skills, skipped_dirs).

resolve_dependencies

python
resolve_dependencies(skills: List[SkillMetadata]) -> List[SkillMetadata]

Topologically sort skills so each skill appears after its dependencies. Raises ValueError on missing deps or cycles.

validate_dependencies

python
validate_dependencies(skills: List[SkillMetadata]) -> List[str]

Validate all declared dependencies exist. Returns a list of error messages (empty = no issues).

expand_transitive_dependencies

python
expand_transitive_dependencies(
    skills: List[SkillMetadata],
    skill_name: str,
) -> List[str]

Return names of all skills that skill_name transitively depends on. Raises ValueError on missing deps or cycles.


Search Path Priority

  1. extra_paths parameter (highest priority)
  2. DCC_MCP_{APP}_SKILL_PATHS environment variable (per-app, e.g. DCC_MCP_MAYA_SKILL_PATHS)
  3. DCC_MCP_SKILL_PATHS environment variable (global fallback)
  4. Platform-specific skills directory (DCC-specific, via get_skills_dir(dcc_name))
  5. Platform-specific skills directory (global, via get_skills_dir())

Environment Variables

VariableDescription
DCC_MCP_{APP}_SKILL_PATHSPer-app skill paths, e.g. DCC_MCP_MAYA_SKILL_PATHS (; on Windows, : on Unix)
DCC_MCP_SKILL_PATHSGlobal fallback skill paths

create_skill_server

python
create_skill_server(
    app_name: str,
    config: McpHttpConfig | None = None,
    extra_paths: list[str] | None = None,
    dcc_name: str | None = None,
) -> McpHttpServer

Recommended entry-point for the Skills-First workflow (v0.12.12+).

Creates a fully wired McpHttpServer for a specific DCC application in one call. Automatically:

  1. Creates ToolRegistry + ToolDispatcher
  2. Creates SkillCatalog wired to the dispatcher
  3. Discovers skills from DCC_MCP_{APP}_SKILL_PATHS and DCC_MCP_SKILL_PATHS
  4. Returns a ready-to-start McpHttpServer

Parameters:

ParameterTypeDescription
app_namestrDCC name (e.g. "maya", "blender") — derives env var and MCP server name
configMcpHttpConfig | NoneHTTP server config; defaults to port 8765
extra_pathslist[str] | NoneExtra skill dirs in addition to env vars
dcc_namestr | NoneOverride DCC filter for scanning (defaults to app_name)

Returns: McpHttpServer — call .start() to begin serving.

Example:

python
import os
from dcc_mcp_core import create_skill_server, McpHttpConfig

os.environ["DCC_MCP_MAYA_SKILL_PATHS"] = "/studio/maya-skills"

server = create_skill_server("maya", McpHttpConfig(port=8765))
handle = server.start()
print(f"Serving at {handle.mcp_url()}")

get_app_skill_paths_from_env

python
get_app_skill_paths_from_env(app_name: str) -> list[str]

Return skill paths from the DCC_MCP_{APP_NAME}_SKILL_PATHS environment variable.

The lookup is case-insensitive; the actual env var key is upper-cased automatically (e.g. DCC_MCP_MAYA_SKILL_PATHS for app_name="maya").

Returns [] if the env var is not set.

Action Naming Convention

When SkillCatalog.load_skill() registers tools from a skill, action names follow the pattern:

{skill_name_underscored}__{tool_name}

Examples:

  • skill maya-geometry, tool create_spheremaya_geometry__create_sphere
  • skill blender-utils, tool render-sceneblender_utils__render_scene

Skill Script Helpers (pure-Python)

dcc_mcp_core.skill is a pure-Python sub-module — no compiled extension required. Skill script authors can import helpers directly inside DCC environments that may not have the full wheel installed.

python
from dcc_mcp_core.skill import skill_entry, skill_success, skill_error

All helpers return a plain dict that is fully compatible with ToolResult. When dcc_mcp_core._core is available, you can pass the dict to validate_action_result() to obtain a typed ToolResult object.


skill_success

python
skill_success(
    message: str,
    *,
    prompt: str | None = None,
    **context,
) -> dict

Return a success result dict.

ParameterTypeDescription
messagestrHuman-readable summary of what was accomplished
promptstr | NoneOptional hint for the agent's next action
**contextAnyArbitrary key/value pairs attached to context
python
return skill_success(
    "Timeline set to frames 1–120",
    prompt="Check the timeline slider to verify.",
    start_frame=1,
    end_frame=120,
)

skill_error

python
skill_error(
    message: str,
    error: str,
    *,
    prompt: str | None = None,
    possible_solutions: list[str] | None = None,
    **context,
) -> dict

Return a failure result dict.

ParameterTypeDescription
messagestrUser-facing description of what went wrong
errorstrTechnical error string (exception repr, code …)
promptstr | NoneRecovery hint; defaults to a generic message
possible_solutionslist[str] | NoneActionable suggestions in context["possible_solutions"]
python
return skill_error(
    "Maya is not available",
    "ImportError: No module named 'maya'",
    prompt="Ensure Maya is running before calling this skill.",
    possible_solutions=["Start Maya", "Check DCC_MCP_MAYA_SKILL_PATHS"],
)

skill_warning

python
skill_warning(
    message: str,
    *,
    warning: str = "",
    prompt: str | None = None,
    **context,
) -> dict

Return a success-but-with-warning result (success=True, context["warning"] set).

python
return skill_warning(
    "Timeline set, end_frame clamped to scene length",
    warning="end_frame 9999 > scene length 240; clamped to 240",
    prompt="Verify the timeline slider.",
    actual_end=240,
)

skill_exception

python
skill_exception(
    exc: BaseException,
    *,
    message: str | None = None,
    prompt: str | None = None,
    include_traceback: bool = True,
    possible_solutions: list[str] | None = None,
    **context,
) -> dict

Return a failure result built from an exception. Captures error_type and optionally the formatted traceback in context.

python
try:
    do_work()
except Exception as exc:
    return skill_exception(
        exc,
        possible_solutions=["Check that the scene is open"],
    )

@skill_entry

python
@skill_entry
def my_tool(param: str = "default", **kwargs) -> dict:
    ...

Decorator that wraps a skill function with standard error handling.

  • Catches ImportError (DCC module missing), Exception, and BaseException
  • Converts each to a proper error dict automatically
  • When run directly (__name__ == "__main__"), the JSON result is printed to stdout

Full example (replaces the manual try/except/main() boilerplate):

python
from dcc_mcp_core.skill import skill_entry, skill_success

@skill_entry
def set_timeline(start_frame: float = 1.0, end_frame: float = 120.0, **kwargs):
    """Set the Maya playback timeline range."""
    import maya.cmds as cmds  # ImportError caught automatically if Maya not present

    min_frame = kwargs.get("min_frame", start_frame)
    max_frame = kwargs.get("max_frame", end_frame)

    cmds.playbackOptions(
        min=min_frame, max=max_frame,
        animationStartTime=start_frame, animationEndTime=end_frame,
    )
    return skill_success(
        f"Timeline set to {start_frame}{end_frame}",
        prompt="Inspect the timeline slider to verify.",
        start_frame=start_frame,
        end_frame=end_frame,
    )

def main(**kwargs):
    """Entry point; delegates to set_timeline."""
    return set_timeline(**kwargs)

if __name__ == "__main__":
    from dcc_mcp_core.skill import run_main
    run_main(main)

run_main

python
run_main(main_fn: Callable[..., dict], argv: list[str] | None = None) -> None

Execute main_fn and print the JSON result to stdout. Calls sys.exit(0) on success, sys.exit(1) on failure.

Intended for if __name__ == "__main__" blocks:

python
if __name__ == "__main__":
    from dcc_mcp_core.skill import run_main
    run_main(main)

Migration from DCC-specific helpers

If you previously used dcc_mcp_maya's maya_success / maya_error / maya_from_exception, the generic equivalents map directly:

Old (DCC-specific)New (generic)
maya_success(msg, prompt=..., **ctx)skill_success(msg, prompt=..., **ctx)
maya_error(msg, error, prompt=..., **ctx)skill_error(msg, error, prompt=..., **ctx)
maya_from_exception(exc_msg, ...)skill_exception(exc, ...)

The dict structure is identical — both are compatible with ToolResult.


Result Serialization — serialize_result / deserialize_result

Rust-backed serialization for ToolResult. The format is switchable via SerializeFormat: JSON today, MessagePack tomorrow — without changing calling code.

python
from dcc_mcp_core import (
    serialize_result, deserialize_result, SerializeFormat, success_result
)

SerializeFormat

python
class SerializeFormat:
    Json: SerializeFormat     # UTF-8 JSON text (default)
    MsgPack: SerializeFormat  # binary MessagePack via rmp-serde

serialize_result

python
serialize_result(
    result: ToolResult,
    format: SerializeFormat = SerializeFormat.Json,
) -> str | bytes

Serialize an ToolResult.

formatReturn typeDescription
SerializeFormat.JsonstrUTF-8 JSON string
SerializeFormat.MsgPackbytesBinary MessagePack
python
arm = success_result("Timeline updated", start_frame=1, end_frame=120)

# JSON (default)
json_str = serialize_result(arm)
assert isinstance(json_str, str)

# MessagePack
msgpack_bytes = serialize_result(arm, SerializeFormat.MsgPack)
assert isinstance(msgpack_bytes, bytes)

deserialize_result

python
deserialize_result(
    data: str | bytes,
    format: SerializeFormat = SerializeFormat.Json,
) -> ToolResult

Deserialize a str (JSON) or bytes (MsgPack) back into an ToolResult. The format must match what was used during serialization.

python
original = success_result("done", frame_count=240)
roundtrip = deserialize_result(serialize_result(original))
assert roundtrip.success
assert roundtrip.message == "done"
assert roundtrip.context["frame_count"] == 240

How run_main uses serialization

run_main() automatically uses serialize_result when _core is available, falling back to json.dumps in pure-Python environments:

result dict
    ↓ validate_action_result()  (type-safe validation)
ToolResult
    ↓ serialize_result(arm, SerializeFormat.Json)   (Rust JSON writer)
JSON string → stdout

To switch to MessagePack in a future release, only _serialize_result() in skill.py needs updating — the serialize_result / deserialize_result API remains stable.

Released under the MIT License.