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.register_metadata_driven_tools

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
registryToolRegistryTool 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", "unloaded", "pending_deps", "error", 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 package version projected from metadata.dcc-mcp.version; top-level version in SKILL.md is rejected
tool_countintNumber of declared tools
tool_namesList[str]Names of declared tools
loadedboolWhether the skill is currently loaded
statusstrMachine-readable load status: "discovered", "pending_deps", "loaded", or "error"
missing_dependenciesList[str]Dependency skill names that are not currently present in the catalog
runtimeSkillRuntimeSummary | NoneAggregate optional runtime state from metadata.dcc-mcp.runtimes

Dunder Methods ​

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

ToolDeclaration ​

A single tool declaration within a skill, parsed from the sibling file referenced by metadata.dcc-mcp.tools (usually tools.yaml).

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/
runtimesList[SkillRuntimeDescriptor]Optional runtime descriptors from metadata.dcc-mcp.runtimes

Optional Runtime Metadata ​

Skills may declare optional external runtimes inline in metadata.dcc-mcp.runtimes or by pointing that field at a sibling runtimes.yaml. Discovery resolves these descriptors with safe probes only: env vars are checked for non-empty values, binaries are resolved on PATH, and Python packages use importlib.util.find_spec() without importing the package or running a tool script.

yaml
metadata:
  dcc-mcp:
    runtimes:
      - name: usd-core
        type: python_package
        package: usd-core
        module: pxr
        optional: true
        feature_level: full-usd
        install_hint: "pip install dcc-mcp-openusd[usd-core]"
      - name: usdcat
        type: binary
        binary: usdcat
        optional: true
        feature_level: usd-cli
      - name: houdini-solaris
        type: env_var
        env: HFS
        optional: true

SkillRuntimeSummary.state is one of available, degraded, or missing. Optional absent runtimes resolve to degraded; required absent runtimes resolve to missing. get_skill_info() also includes per-runtime reports under the runtimes key.


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 keeps skills with missing soft dependencies in the returned skill list (warns via logging). Missing dependencies are ignored only for ordering; dependencies that are present are still sorted before their dependents. Only cyclic dependencies raise ValueError.

Returns (ordered_skills, skipped_dirs).

register_metadata_driven_tools ​

python
register_metadata_driven_tools(
    server,
    *,
    skills: Sequence[Any] | None = None,
    skipped: Sequence[Any] | None = None,
    dcc_name: str = "dcc",
    extra_paths: Iterable[str] | None = None,
    registrations: Sequence[MetadataExtensionRegistration | Callable | tuple[str, Callable]] | None = None,
    scan: Callable | None = None,
    phase: str = "startup",
) -> MetadataRegistrationReport

Register optional tools derived from loaded skill metadata. When skills is omitted the helper calls scan_and_load_lenient(extra_paths=..., dcc_name=...) once, then invokes each extension callback as callback(server, skills=loaded_skills, dcc_name=dcc_name).

Default registrations cover:

  • recipes → register_recipes_tools
  • skill-reference-docs → register_skill_reference_docs_tools

Adapters can pass custom callbacks or lazy import descriptors:

python
from dcc_mcp_core import (
    imported_metadata_extension,
    register_metadata_driven_tools,
)

report = register_metadata_driven_tools(
    server,
    dcc_name="maya",
    extra_paths=[studio_skill_root],
    registrations=[
        imported_metadata_extension(
            "recipes",
            "dcc_mcp_core.recipes",
            "register_recipes_tools",
        ),
        imported_metadata_extension(
            "refs",
            "dcc_mcp_core.skill_reference_docs",
            "register_skill_reference_docs_tools",
        ),
    ],
)
logger.info("metadata tools: %s", report.to_dict())

The report records registered, failed, and skipped extension outcomes. One optional extension failing to import or register does not prevent later extensions from running.

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 0 (OS-assigned)
extra_pathslist[str] | NoneExtra skill dirs in addition to env vars
dcc_namestr | NoneOverride DCC filter for scanning (defaults to app_name)

Config contract: config is typed McpHttpConfig | None. A plain dict, string, integer, or object missing the config attributes raises TypeError at the call site with a snippet that builds a valid config; these values are never silently coerced. None is a supported value meaning "backend defaults" (OS-assigned port, shared gateway port, no job persistence), not a skipped check. A config object built from either spelling is accepted: the PyO3 type the installed wheel resolves, or the pure-Python dataclass used on the py37-lite profile. They are distinct classes exposing the same attributes. Config-like objects exposing port, server_name, and endpoint_path are also supported.

Both create_skill_server and create_adapter_server convert Python config objects to the native McpHttpConfig before calling the embedded backend. Supported public data attributes are preserved; omitted attributes keep the native defaults. Unsupported settings or non-default values for native read-only properties (such as endpoint_path) raise a factory-specific TypeError rather than being silently dropped. Native configs and None pass through unchanged. The sidecar continues to consume the original object by attributes and does not require a native config.

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_sphere → maya_geometry__create_sphere
  • skill blender-utils, tool render-scene → blender_utils__render_scene

Rust-Backed Skill Helpers ​

dcc_mcp_core.skills_helper is the canonical import path for dependency-light helpers that skill scripts can rely on when the full dcc-mcp-core wheel is available. Prefer it over ad-hoc utils modules or adding small runtime dependencies for JSON, YAML, file/path work, LZ4 payload compression, schema validation, result envelopes, argument normalization, or cancellation checks. Skill authoring docs, templates, and generator output should show this module as the preferred import path even though older top-level imports remain available for compatibility.

python
from dcc_mcp_core.skills_helper import (
    check_cancelled,
    json_dumps,
    json_loads,
    normalize_tool_arguments,
    skill_success,
    yaml_dumps,
    yaml_loads,
)

The JSON helpers prefer the Rust/PyO3 backend and fall back to stdlib in a py37-lite installation; YAML remains Rust-backed. They remain available as backwards-compatible top-level imports:

python
from dcc_mcp_core.skills_helper import json_dumps, json_loads, yaml_dumps, yaml_loads

payload = json_loads('{"name": "cube"}')
text = json_dumps(payload, ensure_ascii=False)
config = yaml_loads("enabled: true\n")

The JSON helpers intentionally expose a narrow dependency-light contract, not the complete stdlib API. Exact Unicode escaping and indentation, integer range, non-finite floats, accepted container/object types, arbitrary keyword arguments, and file-oriented APIs may differ between the native codec and json. Use stdlib JSON directly for canonical bytes, hashes, length budgets, or behavior outside this documented helper surface.

For files, prefer the source-aware helpers so UTF-8 handling, byte limits, and parse errors are consistent across skills:

python
from dcc_mcp_core.skills_helper import (
    SkillCodecError,
    dump_json_file,
    load_json_file,
    load_yaml_file,
)

try:
    manifest = load_json_file("manifest.json", require_mapping=True, max_bytes=1_000_000)
    settings = load_yaml_file("settings.yaml", require_mapping=True)
except SkillCodecError as exc:
    return skill_error_from_exception(exc)

dump_json_file("out/report.json", manifest, ensure_ascii=False)

For generated artefacts and hand-off payloads, prefer the Rust-backed file/path helpers instead of one-off local helpers:

python
from dcc_mcp_core.skills_helper import (
    SkillFileError,
    atomic_write_text,
    compress_bytes,
    decompress_bytes,
    ensure_within_root,
    file_digest,
)

try:
    out_path = atomic_write_text(
        "reports/summary.json",
        json_dumps(summary, ensure_ascii=False),
        root=session_temp_dir,
        max_bytes=2_000_000,
    )
    sha256 = file_digest(out_path, root=session_temp_dir)
    packed = compress_bytes(out_path.read_bytes(), max_bytes=2_000_000)
    restored = decompress_bytes(packed, max_bytes=2_000_000)
except SkillFileError as exc:
    return skill_error_from_exception(exc)

ensure_within_root(root, path) resolves relative paths under a trusted workspace/session root, canonicalizes existing ancestors, and rejects traversal outside that root. atomic_write_text() / atomic_write_bytes() write via a same-directory temporary file. file_digest() / bytes_digest() currently support SHA-256; BLAKE3 is intentionally deferred until the wheel already depends on it for another feature. compress_bytes() / decompress_bytes() use the existing LZ4 frame implementation from the shared-memory layer and enforce explicit byte limits.

For bounded REST calls, use the Rust-backed HTTP helpers instead of adding requests for common JSON APIs:

python
from dcc_mcp_core.skills_helper import (
    SkillHttpError,
    http_get_json,
    http_post_json,
    skill_error_from_exception,
)

try:
    info = http_get_json(
        "https://pipeline.example/api/asset",
        query={"name": asset_name},
        headers={"Authorization": f"Bearer {token}"},
        timeout_ms=5_000,
        max_bytes=1_000_000,
    )
    created = http_post_json(
        "https://pipeline.example/api/report",
        {"asset": asset_name, "info": info},
        timeout_ms=5_000,
    )
except SkillHttpError as exc:
    return skill_error_from_exception(exc)

http_request() returns an HttpResponse object with status, headers, bytes, text, json(), url, elapsed_ms, and truncated. Convenience helpers http_get_json() and http_post_json() require a 2xx status and parse the response with the same selected JSON backend as json_loads(). Response bodies are bounded by max_bytes; truncated responses remain inspectable via response.bytes / response.text, and response.json() raises SkillHttpError(kind="response-truncated").

Use redact_http_headers() before echoing request headers into skill errors or audit metadata. It masks common credential headers such as Authorization, Proxy-Authorization, Cookie, Set-Cookie, X-Api-Key, and X-Auth-Token. Keep domain-specific HTTP client dependencies only when you need sessions, streaming protocols, custom auth flows, multipart upload, or API-specific retry logic that these small helpers intentionally do not provide.

Use FileRef, artefact_put_file(), and artefact_get_bytes() when a file must be handed to another tool or exposed through MCP resources. The skills_helper file helpers are lower-level building blocks for local files inside one skill or session root; they do not replace the higher-level artefact store contract.

Existing imports such as from dcc_mcp_core import json_dumps continue to work and re-export the same canonical functions. New helpers for skill authoring belong under skills_helper, not a vague utils namespace.

Use skills_helper when:

  • a skill needs dependency-free JSON or YAML parsing instead of json, PyYAML, or local wrapper modules;
  • a skill needs bounded atomic writes, safe path containment, SHA-256 digests, or LZ4 compression for local session files;
  • a handler needs standard result helpers such as skill_success, skill_error, success_result, or error_result;
  • a tool wrapper needs normalize_tool_arguments() / normalize_tool_meta() to match the shared MCP/REST call-envelope contract;
  • long-running scripts need check_cancelled() / check_dcc_cancelled().
  • a bounded HTTP or file/path helper is covered by this namespace in your installed version; use requests or domain-specific file libraries only for behavior that skills_helper does not provide.

Use a domain-specific Python dependency only when the dependency owns real domain behavior that skills_helper does not cover.

The bundled dcc-mcp-skills-creator validation tools add skill-helper-adoption warnings when a skill script imports avoidable helpers covered by this namespace, such as requests, httpx, PyYAML, or local json_utils / http_utils / file_utils modules. Warnings are advisory for existing skills but should be treated as blockers for new generated/reference skills.

TOML helpers are intentionally not exposed yet. The current adapter and bundled skill use cases read TOML as metadata handled by core loaders rather than skill script runtime code, and Python 3.7 compatibility (Maya 2022) would require adding another runtime dependency for a stable read/write API. Revisit TOML helpers when a skill script needs dependency-free TOML at runtime.


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,
    postcondition: Mapping[str, Any] | None = None,
    verified: bool | None = None,
    _meta: Mapping[str, Any] | 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
postconditionMapping[str, Any] | NoneOptional structured mutation readback evidence
verifiedbool | NoneAdds postcondition.verified; omission means verification was not reported
_metaMapping[str, Any] | NoneOptional namespaced top-level metadata
**contextAnyArbitrary key/value pairs attached to context
python
return skill_success(
    "Timeline set to frames 1–120",
    prompt="Check the timeline slider to verify.",
    verified=True,
    postcondition={"method": "timeline_readback", "actual": [1, 120]},
    start_frame=1,
    end_frame=120,
)

Post-condition readback ​

verified=True is only trustworthy when something actually read the state back. dcc_mcp_core.runtime.postcondition performs that readback so a mutating tool cannot report success while doing nothing observable:

python
from dcc_mcp_core.runtime.postcondition import PostconditionCheck, changed_from, with_postcondition

@with_postcondition(
    PostconditionCheck(
        "material_slot_readback",
        read=lambda: read_slot(shader, "normalCamera"),
        expected=changed_from(None),
    ),
    on_unverified="fail",
)
def assign_texture(shader, path):
    ...
    return skill_success("Texture assigned", shader=shader)
SymbolDescription
PostconditionCheck(method, read, expected=PRESENT, equals=None)One readback assertion; read runs after the mutation
changed_from(value)Expectation meaning "this value must have moved"
PRESENTDefault expectation: the readback value must be non-empty
verify_postcondition(checks, on_unverified=..., method=None)Run checks and return the postcondition evidence mapping
apply_postcondition(result, checks, on_unverified=...)Attach evidence to one result dict and enforce the policy
with_postcondition(*checks, on_unverified="fail")Decorate a mutating handler with readback enforcement

on_unverified="fail" turns an unconfirmed effect into a postcondition_unverified failure; on_unverified="warn" keeps success=true but sets postcondition.verified = false. Readback evidence always outranks a verified=True the handler declared itself. Values are bounded and sensitive keys are redacted before the evidence crosses the wire.


skill_error ​

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

Return a failure result dict.

ParameterTypeDescription
messagestrUser-facing description of what went wrong
errorstrStable machine-readable error code; put exception details under _meta["dcc.error"]
promptstr | NoneRecovery hint; defaults to a generic message
possible_solutionslist[str] | NoneActionable suggestions in context["possible_solutions"]
_metaMapping[str, Any] | NoneOptional namespaced top-level metadata
python
return skill_error(
    "Maya is not available",
    "import_error",
    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,
    _meta: Mapping[str, Any] | 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,
    _meta: Mapping[str, Any] | None = None,
    **context,
) -> dict

Return a failure result built from an exception. The string error is the exception type; the message and optional formatted traceback are stored in _meta["dcc.error"]. Caller metadata is merged by namespace. The legacy context.error_type and context.traceback mirrors remain for one migration window.

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() always normalizes through the dependency-light ToolResultEnvelope and emits JSON with the standard library. Using one path keeps native-wheel and source-only skill subprocess payloads identical:

result dict
    ↓ ToolResultEnvelope.from_dict()  (schema validation)
canonical mapping
    ↓ json.dumps(..., allow_nan=False)
JSON string → stdout

The Rust-backed serialize_result / deserialize_result functions remain the explicit JSON/MessagePack API for runtime ToolResult objects.

Released under the MIT License.