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.
from dcc_mcp_core import SkillCatalog, ToolRegistry
registry = ToolRegistry()
catalog = SkillCatalog(registry)Constructor
SkillCatalog(registry: ToolRegistry) -> SkillCatalog| Parameter | Type | Description |
|---|---|---|
registry | ToolRegistry | Action registry for registering skill tools |
Methods
| Method | Returns | Description |
|---|---|---|
discover(extra_paths=None, dcc_name=None) | int | Scan 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) | int | Unload 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 | None | Full metadata for a skill, or None if not found |
is_loaded(skill_name) | bool | Whether a skill is currently loaded |
loaded_count() | int | Number of loaded skills |
__repr__() | str | String representation |
Example
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)
| Property | Type | Description |
|---|---|---|
name | str | Skill name |
description | str | Short description |
search_hint | str | Keyword hint for search (from search-hint: in SKILL.md; falls back to description) |
tags | List[str] | Skill tags |
dcc | str | Target DCC (e.g. "maya") |
version | str | Skill version |
tool_count | int | Number of declared tools |
tool_names | List[str] | Names of declared tools |
loaded | bool | Whether the skill is currently loaded |
Dunder Methods
| Method | Description |
|---|---|
__repr__ | SkillSummary(name='...', loaded=True) |
ToolDeclaration
A single tool declaration within a skill, parsed from SKILL.md frontmatter tools: list.
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
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 = "",
) -> ToolDeclarationFields (read-write)
| Field | Type | Default | Description |
|---|---|---|---|
name | str | required | Tool name (unique within the skill) |
description | str | "" | Human-readable description |
read_only | bool | False | True if this tool only reads data (no side effects) |
destructive | bool | False | True if this tool may cause destructive changes |
idempotent | bool | False | True if same args always produce the same result |
defer_loading | bool | False | Parse defer-loading: / defer_loading: from SKILL.md for discovery-oriented UIs |
source_file | str | "" | 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.
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
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,
) -> SkillMetadataFields (read-write)
| Field | Type | Description |
|---|---|---|
name | str | Unique skill name |
description | str | Short description |
search_hint | str | Keyword hint for search_skills (SKILL.md search-hint: field; falls back to description) |
tools | List[str] | Tool names from frontmatter |
dcc | str | Target DCC application |
tags | List[str] | Classification tags |
scripts | List[str] | Discovered script file paths |
skill_path | str | Absolute path to skill directory |
version | str | Skill version |
depends | List[str] | Dependency skill names |
metadata_files | List[str] | Paths to .md files in metadata/ |
SkillScanner
Scanner for discovering skill packages in directories. Caches file modification times for efficient repeated scans.
from dcc_mcp_core import SkillScanner
scanner = SkillScanner()Methods
| Method | Returns | Description |
|---|---|---|
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
| Property | Type | Description |
|---|---|---|
discovered_skills | List[str] | Previously discovered skill directory paths |
Dunder Methods
| Method | Description |
|---|---|
__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.
from dcc_mcp_core import SkillWatcher
watcher = SkillWatcher(debounce_ms=300)
watcher.watch("/path/to/skills")
skills = watcher.skills()Constructor
SkillWatcher(debounce_ms: int = 300) -> SkillWatcherdebounce_ms: Milliseconds to wait before reloading after a change (multiple rapid events coalesced).
Methods
| Method | Returns | Description |
|---|---|---|
watch(path) | — | Start watching path recursively. Raises RuntimeError if path does not exist |
unwatch(path) | bool | Stop watching path. Returns True if was being watched |
skills() | List[SkillMetadata] | Snapshot of all currently loaded skills |
skill_count() | int | Number of skills currently loaded |
watched_paths() | List[str] | List of currently watched directory paths |
reload() | — | Manually trigger a full reload |
__repr__ | str | String representation |
Functions
parse_skill_md
parse_skill_md(skill_dir: str) -> SkillMetadata | NoneParse a SKILL.md from a skill directory. Returns None if missing or invalid.
- Extracts YAML frontmatter between
---delimiters - Enumerates scripts in
scripts/subdirectory - Discovers
.mdfiles inmetadata/subdirectory
scan_skill_paths
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
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
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
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
validate_dependencies(skills: List[SkillMetadata]) -> List[str]Validate all declared dependencies exist. Returns a list of error messages (empty = no issues).
expand_transitive_dependencies
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
extra_pathsparameter (highest priority)DCC_MCP_{APP}_SKILL_PATHSenvironment variable (per-app, e.g.DCC_MCP_MAYA_SKILL_PATHS)DCC_MCP_SKILL_PATHSenvironment variable (global fallback)- Platform-specific skills directory (DCC-specific, via
get_skills_dir(dcc_name)) - Platform-specific skills directory (global, via
get_skills_dir())
Environment Variables
| Variable | Description |
|---|---|
DCC_MCP_{APP}_SKILL_PATHS | Per-app skill paths, e.g. DCC_MCP_MAYA_SKILL_PATHS (; on Windows, : on Unix) |
DCC_MCP_SKILL_PATHS | Global fallback skill paths |
create_skill_server
create_skill_server(
app_name: str,
config: McpHttpConfig | None = None,
extra_paths: list[str] | None = None,
dcc_name: str | None = None,
) -> McpHttpServerRecommended entry-point for the Skills-First workflow (v0.12.12+).
Creates a fully wired McpHttpServer for a specific DCC application in one call. Automatically:
- Creates
ToolRegistry+ToolDispatcher - Creates
SkillCatalogwired to the dispatcher - Discovers skills from
DCC_MCP_{APP}_SKILL_PATHSandDCC_MCP_SKILL_PATHS - Returns a ready-to-start
McpHttpServer
Parameters:
| Parameter | Type | Description |
|---|---|---|
app_name | str | DCC name (e.g. "maya", "blender") — derives env var and MCP server name |
config | McpHttpConfig | None | HTTP server config; defaults to port 8765 |
extra_paths | list[str] | None | Extra skill dirs in addition to env vars |
dcc_name | str | None | Override DCC filter for scanning (defaults to app_name) |
Returns: McpHttpServer — call .start() to begin serving.
Example:
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
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, toolcreate_sphere→maya_geometry__create_sphere - skill
blender-utils, toolrender-scene→blender_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.
from dcc_mcp_core.skill import skill_entry, skill_success, skill_errorAll 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
skill_success(
message: str,
*,
prompt: str | None = None,
**context,
) -> dictReturn a success result dict.
| Parameter | Type | Description |
|---|---|---|
message | str | Human-readable summary of what was accomplished |
prompt | str | None | Optional hint for the agent's next action |
**context | Any | Arbitrary key/value pairs attached to context |
return skill_success(
"Timeline set to frames 1–120",
prompt="Check the timeline slider to verify.",
start_frame=1,
end_frame=120,
)skill_error
skill_error(
message: str,
error: str,
*,
prompt: str | None = None,
possible_solutions: list[str] | None = None,
**context,
) -> dictReturn a failure result dict.
| Parameter | Type | Description |
|---|---|---|
message | str | User-facing description of what went wrong |
error | str | Technical error string (exception repr, code …) |
prompt | str | None | Recovery hint; defaults to a generic message |
possible_solutions | list[str] | None | Actionable suggestions in context["possible_solutions"] |
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
skill_warning(
message: str,
*,
warning: str = "",
prompt: str | None = None,
**context,
) -> dictReturn a success-but-with-warning result (success=True, context["warning"] set).
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
skill_exception(
exc: BaseException,
*,
message: str | None = None,
prompt: str | None = None,
include_traceback: bool = True,
possible_solutions: list[str] | None = None,
**context,
) -> dictReturn a failure result built from an exception. Captures error_type and optionally the formatted traceback in context.
try:
do_work()
except Exception as exc:
return skill_exception(
exc,
possible_solutions=["Check that the scene is open"],
)@skill_entry
@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, andBaseException - 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):
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
run_main(main_fn: Callable[..., dict], argv: list[str] | None = None) -> NoneExecute 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:
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.
from dcc_mcp_core import (
serialize_result, deserialize_result, SerializeFormat, success_result
)SerializeFormat
class SerializeFormat:
Json: SerializeFormat # UTF-8 JSON text (default)
MsgPack: SerializeFormat # binary MessagePack via rmp-serdeserialize_result
serialize_result(
result: ToolResult,
format: SerializeFormat = SerializeFormat.Json,
) -> str | bytesSerialize an ToolResult.
format | Return type | Description |
|---|---|---|
SerializeFormat.Json | str | UTF-8 JSON string |
SerializeFormat.MsgPack | bytes | Binary MessagePack |
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
deserialize_result(
data: str | bytes,
format: SerializeFormat = SerializeFormat.Json,
) -> ToolResultDeserialize a str (JSON) or bytes (MsgPack) back into an ToolResult. The format must match what was used during serialization.
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"] == 240How 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 → stdoutTo switch to MessagePack in a future release, only _serialize_result() in skill.py needs updating — the serialize_result / deserialize_result API remains stable.