# Skills System The Skills system registers scripts (Python, MEL, MaxScript, BAT, Shell, etc.) as MCP-discoverable tools with **zero Python glue code**. `SKILL.md` follows the agentskills.io V1.0 frontmatter shape; dcc-mcp-core extensions live under `metadata.dcc-mcp.*` and point to sibling files such as `tools.yaml` and `groups.yaml`. Keep extension data out of top-level `SKILL.md` keys so generic agentskills.io readers can still parse the package. For **adapter maintenance** (I/O tool copy, `recipes` / `skill-reference-docs`, gateway-friendly descriptions), use [skill-maintenance.md](skill-maintenance.md). For agent-facing adapter skill development guidance, load `skills/dcc-mcp-skills-creator/`. In-tree reference skills: `python/dcc_mcp_core/skills/dcc-diagnostics`, `python/dcc_mcp_core/skills/workflow`. ## Evidence-Informed Skill Improvement Review reusable friction only after the task and its validation are complete. Route tool calls through the gateway with one stable task identifier, query the narrowest useful gateway window, then invoke the first-class `review_skill_improvement` prompt: ```bash dcc-mcp-cli call --require-gateway --agent-session-id task-42 --json '{}' dcc-mcp-cli stats --range 24h --dcc-type maya --session-id task-42 ``` The prompt is defined in `skills/dcc-mcp-skills-creator/prompts.yaml`. Supply only bounded task, stats, validation, and existing-skill summaries; never send hidden reasoning, raw prompts, secrets, private paths, or full tool payloads. Stats are supporting evidence, not root-cause proof. Inspect `stats_coverage`: direct local MCP is excluded, and `configured_route_recorded=false` means the configured route was not measurable. Zero calls means missing evidence, not that no calls occurred. Prefer `no_change`, then `update_existing`; create a new skill only for a repeated, stable workflow with one clear owner, and validate it before adoption. ## Quick Start ### 1. Create a Skill Directory ``` maya-geometry/ ├── SKILL.md ├── tools.yaml └── scripts/ ├── create_sphere.py └── export_fbx.bat ``` ### 2. Write `SKILL.md` and `tools.yaml` `SKILL.md`: ```yaml --- name: maya-geometry description: >- Domain skill — Maya geometry creation and modification tools. Use when the user asks to create, inspect, or export polygon meshes in Maya. Not for render-farm submission — use maya-render-farm for that. license: MIT compatibility: maya>=2022 metadata: dcc-mcp: dcc: maya version: "1.0.0" layer: domain tags: [geometry, create] search-hint: "polygon modeling, sphere, bevel, extrude, mesh editing" search-aliases: [primitive ball, mesh globe] tools: tools.yaml --- # Maya Geometry Skill Use these tools to create and modify geometry in Maya. ``` ```yaml # tools.yaml tools: - name: create_sphere description: Create a polygon sphere with the given radius. search_aliases: [primitive ball, mesh globe] source_file: scripts/create_sphere.py annotations: read_only_hint: false destructive_hint: false idempotent_hint: false next-tools: on-failure: [dcc_diagnostics__screenshot, dcc_diagnostics__audit_log] - name: export_fbx description: Export selected objects to FBX. source_file: scripts/export_fbx.bat ``` `metadata.dcc-mcp.version` is the canonical skill package version field and is projected into `SkillMetadata.version`. Do not put `version` at the frontmatter root: top-level `version` is outside the agentskills.io 1.0 key set and the strict loader rejects it with a diagnostic that points to `metadata.dcc-mcp.version: "1.0.0"`. The `metadata.dcc-mcp.search-hint` field provides comma-separated keywords for efficient skill discovery via `search_skills` without loading full tool schemas. Use bounded `metadata.dcc-mcp.search-aliases` and per-tool `search_aliases` in `tools.yaml` for domain synonyms, localized terms, or common user phrases that should improve gateway/per-DCC search recall without changing tool names, summaries, tags, or dispatch inputs. Optional adapter runtimes can be declared without turning skill discovery into an installer or shell executor. Use inline `metadata.dcc-mcp.runtimes`, or point that field at a sibling `runtimes.yaml`, for safe probes that run during search/detail: environment variables are checked for non-empty values, binaries are resolved on `PATH`, and Python packages use `importlib.util.find_spec()` without importing the package or executing tool scripts. ```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 guidance: "Install OpenUSD command-line tools to enable USD CLI checks." - name: houdini-solaris type: env_var env: HFS optional: true feature_level: solaris guidance: "Start from a Houdini environment or set HFS." ``` Runtime states are `available`, `degraded`, and `missing`. Optional missing runtimes report `degraded`, while required missing runtimes report `missing`. `search_skills()`, `list_skills()`, `get_skill_info()`, gateway search, and REST describe surfaces expose the resolved state so agents can decide whether to load/call a skill before invoking any adapter code. ### Preferred Runtime Helpers New skill scripts should import dependency-light helper APIs from `dcc_mcp_core.skills_helper` before adding small Python dependencies or local utility modules: ```python from dcc_mcp_core.skills_helper import ( SkillFileError, SkillHttpError, ToolValidator, atomic_write_text, http_get_json, json_dumps, json_loads, load_yaml_file, normalize_tool_arguments, skill_entry, skill_error_from_exception, skill_success, yaml_dumps, yaml_loads, ) ``` Use `json_dumps` / `json_loads` and `yaml_dumps` / `yaml_loads` instead of adding PyYAML or local JSON/YAML wrappers for ordinary skill payloads. Use `http_request`, `http_get_json`, and `http_post_json` for bounded synchronous JSON REST calls; always set `timeout_ms` and `max_bytes`, and redact headers before surfacing them in errors or audit metadata. Use file/path helpers such as `ensure_within_root`, `atomic_write_text`, `load_json_file`, `load_yaml_file`, and `file_digest` for local session files. Use `ToolValidator`, `normalize_tool_arguments`, result helpers, and cancellation checks from the same namespace so scripts do not mix helper import paths. Keep a domain-specific dependency only when it owns behavior that `skills_helper` intentionally does not cover: sessions, streaming, multipart upload, custom retry/auth flows, SDK-specific API models, non-JSON protocols, or rich domain file formats. Existing top-level imports such as `from dcc_mcp_core import json_dumps` remain supported for compatibility, but new docs, generators, and templates should show `dcc_mcp_core.skills_helper` as the canonical path. When migrating old skills, replace one concern at a time and keep the old test fixtures running: 1. Replace direct `requests.get(...).json()` calls with `http_get_json(...)` where the call is a simple bounded JSON request. 2. Replace PyYAML or local YAML helpers with `load_yaml_file(...)` / `yaml_loads(...)` for bounded configs. 3. Replace ad-hoc path containment and temp-file writes with `ensure_within_root(...)` and `atomic_write_text(...)`. 4. Re-run `dcc_mcp_skills_creator__validate_skill_dir`; it now reports `skill-helper-adoption` warnings for generated/reference skills importing avoidable dependencies such as `requests`, `httpx`, PyYAML, or local helper modules covered by `skills_helper`. ### 3. Set Environment Variable ```bash # Linux/macOS export DCC_MCP_SKILL_PATHS="/path/to/my-skills" # Windows set DCC_MCP_SKILL_PATHS=C:\path\to\my-skills # Multiple paths (use platform path separator) export DCC_MCP_SKILL_PATHS="/path/skills1:/path/skills2" ``` ### 4. Discover and Load Use `SkillCatalog` (recommended) for full progressive loading, or low-level scan functions for one-shot use: ```python from dcc_mcp_core import SkillCatalog, ToolRegistry registry = ToolRegistry() catalog = SkillCatalog(registry) # Discover all skills from DCC_MCP_SKILL_PATHS discovered = catalog.discover(dcc_name="maya") print(f"Discovered {discovered} skills") # List available skills for skill in catalog.list_skills(): print(f" {skill.name} v{skill.version}: {skill.description} (loaded={skill.loaded})") # Load a skill — returns the registered tool names tool_names = catalog.load_skill("maya-geometry") print(tool_names) ``` ## Skill Catalog (Recommended API) `SkillCatalog` manages the full lifecycle: discovery → progressive loading → unloading. ```python from dcc_mcp_core import SkillCatalog, ToolRegistry registry = ToolRegistry() catalog = SkillCatalog(registry) # Discovery catalog.discover(extra_paths=["/my/skills"], dcc_name="maya") # Search results = catalog.search_skills(query="geometry", tags=["create"], dcc="maya") for s in results: print(f"{s.name}: {s.tool_count} tools {s.tool_names}") # Load/unload actions = catalog.load_skill("maya-geometry") # returns List[str] — tool names catalog.is_loaded("maya-geometry") # True removed = catalog.unload_skill("maya-geometry") # Status inspection catalog.loaded_count() # int len(catalog) # total skills in catalog catalog.list_skills() # all skills (SkillSummary list) catalog.list_skills("loaded") # only loaded catalog.list_skills("unloaded") # only unloaded # Detail info = catalog.get_skill_info("maya-geometry") # dict with full details or None # Adapter-side runtime policy changes skill = catalog.get_skill("maya-geometry") # detached SkillMetadata copy for tool in skill.tools: if tool.thread_affinity == "main": tool.enforce_thread_affinity = False catalog.load_skill_object(skill) # registers through the normal core path ``` Use `get_skill()` / `load_skill_object()` when an adapter must adjust skill metadata at runtime before registration, for example standalone/headless compatibility for main-thread tools. Do not parse or rewrite `SKILL.md` or `tools.yaml` from adapter code for those runtime overrides. `get_skill_info()` remains the serialized inspection view; mutating the returned dict does not change catalog state. ### SkillSummary Fields `search_skills()` and `list_skills()` return `SkillSummary` objects: | Field | Type | Description | |-------|------|-------------| | `name` | `str` | Skill name | | `description` | `str` | Short description | | `search_hint` | `str` | Keyword hint for discovery (from `metadata.dcc-mcp.search-hint`; 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 | | `scope` | `str` | Trust scope such as `repo`, `user`, `system`, or `admin` | | `implicit_invocation` | `bool` | Whether tools may be invoked without an explicit `load_skill` step | | `runtime` | `SkillRuntimeSummary \| None` | Aggregate optional runtime state from `metadata.dcc-mcp.runtimes` | ## ToolDeclaration A `ToolDeclaration` describes a single tool within a skill. Declare tools in the sibling `tools.yaml` file referenced by `metadata.dcc-mcp.tools`. Legacy top-level `tools:` frontmatter is rejected by the strict loader; migrate old skills to the sibling-file pattern. Schema keys accept both dcc-mcp-core snake_case (`input_schema`, `output_schema`) and MCP-style camelCase (`inputSchema`, `outputSchema`) so authors can copy schemas from MCP tooling without renaming them: ```yaml tools: - name: create_sphere description: "Create a polygon sphere" inputSchema: type: object properties: radius: type: number description: Sphere radius in scene units search_aliases: [primitive ball, mesh globe] outputSchema: type: object properties: name: type: string read_only: false destructive: false idempotent: false defer-loading: true source_file: scripts/create_sphere.py ``` ```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", ) ``` | Field | Type | Default | Description | |-------|------|---------|-------------| | `name` | `str` | required | Tool name (unique within the skill) | | `description` | `str` | `""` | Human-readable description | | `input_schema` | `str` (JSON) | `None` | JSON Schema for input parameters; YAML also accepts `inputSchema` | | `output_schema` | `str` (JSON) | `None` | JSON Schema for output; YAML also accepts `outputSchema` | | `search_aliases` | `List[str]` | `[]` | Bounded search-only synonyms. These are indexed for discovery with schema field names, but are not tool arguments and are not a replacement for a concise description. | | `read_only` | `bool` | `False` | Whether this tool only reads data | | `destructive` | `bool` | `False` | Whether this tool may cause destructive changes | | `idempotent` | `bool` | `False` | Whether calling with same args always produces same result | | `defer_loading` | `bool` | `False` | Accepts `defer-loading` / `defer_loading` in SKILL.md and marks the declaration as discovery-oriented | | `source_file` | `str` | `""` | Explicit path to the script (relative to skill dir) | Runtime discovery is manifest-first: if `input_schema` is omitted, loaded and unloaded tool metadata falls back to `{"type": "object"}`. Core does not import or execute tool scripts to infer schemas during normal DCC startup, search, or describe calls. Set `DCC_MCP_ENABLE_SCHEMA_INTROSPECTION=1` only as a development/migration aid, then copy the generated schema into `tools.yaml` or register it through `ToolSpec`. ### Deriving `input_schema` / `output_schema` from Python types (#242) Hand-writing JSON Schema is error-prone — agent-authored actions drift, cached schemas diverge from runtime code, and the schema text is noisy to review. `dcc_mcp_core.schema` derives both schemas from Python type annotations using only the standard library (no `pydantic`, no `jsonschema`, no `attrs`). Write a typed handler: ```python from dataclasses import dataclass, field from typing import Tuple from dcc_mcp_core import tool_spec_from_callable from dcc_mcp_core._tool_registration import register_tools @dataclass class ExportInput: scene_path: str = field(metadata={"description": "Scene file to export."}) format: str = "fbx" frame_range: Tuple[int, int] = (1, 100) @dataclass class ExportResult: path: str size_bytes: int took_ms: int def export_scene(args: ExportInput) -> ExportResult: """Export a scene to an interchange format.""" ... spec = tool_spec_from_callable(export_scene) register_tools(server, [spec], dcc_name="maya") ``` `tool_spec_from_callable` understands two signature styles: - **Single dataclass / TypedDict parameter** — the whole parameter becomes the `inputSchema` (used above). - **Multiple primitive-typed parameters** — each parameter becomes a property of an `object` `inputSchema`. The return annotation, when present, becomes the `outputSchema` so MCP 2025-06-18 clients can validate `structuredContent` payloads. Untyped handlers raise `TypeError` rather than silently falling back to a permissive `{"type": "object"}` — this closes the #588-era footgun. Supported types (stdlib only): `bool`, `int`, `float`, `str`, `bytes`, `None`, `list[X]`, `tuple[X, ...]`, `tuple[A, B, ...]` (fixed), `dict[str, V]`, `Optional[X]` / `X | None`, `Union[A, B]`, `Literal[...]`, `Enum`, `datetime.datetime`, `datetime.date`, `pathlib.Path`, `uuid.UUID`, `@dataclass`, `TypedDict`. On Python 3.7 (Maya 2022), spell containers and unions with `typing.List`, `typing.Dict`, `typing.Tuple`, `typing.Optional`, and `typing.Union`; `Literal` and `TypedDict` require `typing_extensions` in the skill author's environment. The core package still imports without third-party Python library dependencies. Unsupported types raise `TypeError` with a clear escape hatch: pass an explicit `input_schema=...` dict or use pydantic's `MyModel.model_json_schema()`. This helper is for Python registration-time authoring (`ToolSpec`) and offline schema generation. It is not part of the default file-based skill discovery path, which intentionally avoids importing arbitrary DCC scripts. ::: tip Why not pydantic? We intentionally stay free of third-party Python library dependencies. Adding `pydantic` for this one feature would drag in a 3MB wheel plus `pydantic-core` and is too heavy for authors who only want a few dataclass handlers. For callers who already use pydantic, the emitted shape matches pydantic's conventions (`title`, `$defs`, `$ref`, `anyOf`, `required`), so swapping in `MyModel.model_json_schema()` is a drop-in replacement. `pydantic-core` (Rust) was evaluated for reuse and doesn't help here: pydantic's own architecture doc confirms JSON Schema generation lives entirely in the Python package — the Rust crate only runs validation and serialization, never introspecting Python type annotations. ::: See `examples/skills/typed-schema-demo/` for a runnable example, and `tests/test_schema.py` for the full type-mapping table in executable form. ### `next-tools` — Follow-Up Tool Hints (dcc-mcp-core extension) The `next-tools` field guides AI agents to appropriate follow-up actions after a tool executes. This is a dcc-mcp-core extension not present in the agentskills.io specification. ```yaml tools: - name: create_sphere description: "Create a polygon sphere" source_file: scripts/create_sphere.py next-tools: on-success: [maya_geometry__bevel_edges] # suggest after success on-failure: [dcc_diagnostics__screenshot] # debug on failure ``` | Key | Type | Description | |-----|------|-------------| | `on-success` | `List[str]` | Suggested tools after successful execution | | `on-failure` | `List[str]` | Debugging/recovery tools on failure | Both accept lists of fully-qualified tool names in `{skill_name}__{tool_name}` format. Dotted MCP tool names are not valid; see [Naming Rules](/guide/naming) for validation. Unloaded skill stubs returned by `tools/list` also expose `annotations.deferredHint = true` as an explicit progressive-loading signal. Once you call `load_skill(...)`, the real tools replace the stub and return `deferredHint = false`. ## Script Lookup Priority When loading a skill, the catalog resolves which script backs each tool declaration: 1. `ToolDeclaration.source_file` — explicit path wins 2. A script in `scripts/` whose stem matches the tool name 3. If the skill has only one script, it backs all tools 4. No handler registered (tool visible in registry but not executable) ## Low-Level Skill Functions For simple one-shot scanning without progressive loading: ```python import os from dcc_mcp_core import ( SkillScanner, SkillWatcher, SkillMetadata, parse_skill_md, scan_skill_paths, scan_and_load, scan_and_load_lenient, scan_and_load_strict, ) os.environ["DCC_MCP_SKILL_PATHS"] = "/path/to/skills" # One-shot scan + load + dependency sort → returns (skills, skipped_dirs) skills, skipped = scan_and_load(extra_paths=["/my/skills"], dcc_name="maya") skills_lenient, skipped = scan_and_load_lenient(dcc_name="maya") # keep soft-dep skills discoverable # Strict variant (issue maya#138): raises ValueError when any directory # was silently skipped, so embedders can fail start-up loudly instead of # discovering the missing tools at run-time. The exception message lists # every offending directory and points at scan_and_load_lenient as the # opt-out for installations that genuinely want the silent-skip default. try: skills, _ = scan_and_load_strict(dcc_name="maya") except ValueError as exc: # Wire this into your DCC plugin's start-up error reporting. raise SystemExit(f"Refusing to start: {exc}") from exc ``` ::: tip Choosing the right entry point - `scan_and_load` — fail-fast on missing dependencies or cycles after logging skipped directories and parse failures. Good for CI and packaged adapter releases where dependency completeness should block the build. - `scan_and_load_lenient` — same return shape but keeps skills with missing soft dependencies discoverable; present dependencies still sort before their dependents, and dependency cycles still fail. - `scan_and_load_strict` — fails fast when the scanner skipped any directory. Use in CI / packaged adapter releases where a malformed `SKILL.md` should be a blocking error rather than a silent omission. ::: ```python # Scan directories for SKILL.md files scanner = SkillScanner() skill_dirs = scanner.scan(extra_paths=["/my/skills"], dcc_name="maya") # Parse a single skill directory metadata = parse_skill_md("/path/to/maya-geometry") # Get raw list of skill directory paths paths = scan_skill_paths(extra_paths=["/my/skills"], dcc_name="maya") ``` ## Live Reload with SkillWatcher `SkillWatcher` monitors the filesystem and automatically reloads skills when `SKILL.md` files change: ```python from dcc_mcp_core import SkillWatcher watcher = SkillWatcher(debounce_ms=300) watcher.watch("/path/to/skills") # Get current skills (snapshot) skills = watcher.skills() # List[SkillMetadata] count = watcher.skill_count() # int # Manual reload watcher.reload() # Stop watching watcher.unwatch("/path/to/skills") # Inspect watched paths paths = watcher.watched_paths() # List[str] ``` ## Layered Skill Architecture ### Why layers matter As a DCC adapter's skill set grows, AI agents can encounter routing ambiguity: two skills share overlapping keywords and the agent picks the wrong one. The layered architecture solves this by making each skill's scope and counter-examples explicit in its `description`, and by tagging every skill with a `dcc-mcp.layer` metadata key that partitions discovery. ### The four layers | Layer | Role | `dcc-mcp.layer` value | Search rank | |-------|------|----------------------|-------------| | **domain** | Business workflows for a specific DCC or task. Depends on infrastructure. | `domain` | × 1.00 | | **infrastructure** | Low-level, DCC-agnostic primitives. Stable API. Auto-loaded or shared. | `infrastructure` | × 0.35 | | **thin-harness** | One Python script + minimal SKILL.md — raw `python` / `bash` / CLI wrappers. | `thin-harness` | × 0.20 | | **example** | Authoring references and demos. Never loaded in production. | `example` | **excluded** | **Domain skills** (e.g. `maya-geometry`, `maya-pipeline`) implement a specific DCC workflow. They declare `metadata.dcc-mcp.depends` for the infrastructure skills they chain to, and their tool descriptions guide agents toward other domain skills when the requested operation is out of scope. **Infrastructure skills** (e.g. `dcc-diagnostics`, `workflow`, `usd-tools`) are the fallback and recovery layer. Every domain skill chains to them via `next-tools.on-failure`. **Thin-harness skills** are the lowest tier that still appears in search results. They wrap a raw script with minimal SKILL.md boilerplate — useful as a last-resort escape hatch when no domain or infrastructure skill covers the needed operation. **Example skills** are dropped from search results entirely. They only surface when the caller explicitly asks for them via `search_skills(tags=["example"])` or types the exact skill name. ::: tip Bypassing penalties The layer penalty and the `example` exclusion are bypassed when the caller filters by a known layer name through `tags=` (case-insensitive), e.g. `search_skills(tags=["thin-harness"])` or `search_skills(tags=["infrastructure"])`. The raw BM25 order is honoured inside the filtered slice. ::: ### Description pattern: explicit negative routing The `description` field must follow a 3-part structure that tells agents both when to use and when **not** to use the skill: ``` skill — . Use when . Not for — use for that. ``` ```yaml # Infrastructure skill description: >- Infrastructure skill — low-level OpenUSD scene inspection and validation: read layer stacks, traverse prims, validate schemas. Use when working directly with raw USD files. Not for Maya-specific USD export — use maya-pipeline__export_usd for that. # Domain skill description: >- Domain skill — Maya geometry primitives: create spheres, cubes, cylinders; bevel and extrude polygon components. Use for individual geometry operations in Maya. Not for full asset export pipelines — use maya-pipeline for that. Not for raw USD file inspection — use usd-tools for that. ``` ### Skill path-source rank A second multiplier is layered on top of the layer multiplier based on where the skill was discovered from. User-curated locations rank at parity (× 1.00); bundled / platform-installed starter material is slightly damped so a local-dev skill always wins a tie. | Source | Where it comes from | Rank | |--------|---------------------|------| | `ExplicitArg` | `extra_paths` passed to `discover()` / `scan_*` | × 1.00 | | `AdminCustom` | Added through the admin UI (gateway SQLite lane) | × 1.00 | | `EnvVar` | `DCC_MCP_SKILL_PATHS` / `DCC_MCP__SKILL_PATHS` | × 1.00 | | `LocalDev` | `~/.dcc-mcp//skills` (local iteration root) | × 1.00 | | `Platform` | Platform-wide install dir (`get_skills_dir`) | × 0.85 | | `Bundled` | Shipped with the dcc-mcp package itself | × 0.70 | The path-source multiplier compounds multiplicatively with the layer multiplier (both are ≤ 1.00). The exact-name fast-path bypasses it — `search_skills("dcc-diagnostics")` still surfaces a bundled diagnostics skill at the top regardless of source. Source tagging happens at scan time in `crates/dcc-mcp-skills/src/scanner.rs::scan_with_sources`. Adapters can read the assigned source via `SkillEntry.path_source` for diagnostic surfaces (e.g. the admin UI's skill panel) — it is stable across restarts because the field is `#[serde(default)]`. ### Metadata layer tag Tag every skill with its layer so `search_skills` can filter by layer: ```yaml metadata: dcc-mcp: layer: domain # or: infrastructure | thin-harness | example ``` ```python # Browse only infrastructure skills infra = catalog.search_skills(tags=["infrastructure"]) # Browse only domain skills for maya domain = catalog.search_skills(tags=["domain"], dcc="maya") ``` ### search-hint partitioning Keep `search-hint` keywords non-overlapping across layers so `search_skills()` returns the most relevant skill: ```yaml # Infrastructure — mechanism-oriented (describes the tool/API itself) metadata: dcc-mcp: search-hint: "usd stage, prim, schema validation, usdcat, usdchecker" # Domain — intent-oriented (describes the user's goal) metadata: dcc-mcp: search-hint: "export Maya scene to USD, asset pipeline, project setup" # Example — append "authoring reference" to avoid production matches metadata: dcc-mcp: search-hint: "async tool, deferred hint, authoring reference" ``` ### Failure chain wiring Every **domain skill tool** must wire `on-failure` to infrastructure diagnostics. This gives the agent a consistent recovery path regardless of which domain skill failed: ```yaml tools: - name: export_usd source_file: scripts/export_usd.py next-tools: on-success: [usd_tools__validate] on-failure: [dcc_diagnostics__screenshot, dcc_diagnostics__audit_log] ``` Infrastructure skills do not need `on-failure` chains — they are the fallback. ### Checklist for a new domain skill Before opening a PR for a new domain skill, verify: - [ ] `metadata.dcc-mcp.layer: domain` is set - [ ] `description` starts with `Domain skill —` and ends with at least one `Not for … — use … for that.` sentence - [ ] `search-hint` uses intent-oriented keywords that do not overlap with infrastructure skills - [ ] `metadata.dcc-mcp.depends` lists every infrastructure skill referenced in `next-tools.on-failure` - [ ] Every tool has `on-failure: [dcc_diagnostics__screenshot, dcc_diagnostics__audit_log]` ## Complex Skill Architecture The "Layered Skill Architecture" section above is about **discovery layers** (`infrastructure` / `domain` / `example`) — how a skill is positioned for agent routing. This section is about the **internal** organisation of the files inside a single skill once it grows past one script. ### When to use a layered internal structure The default single-file pattern (`scripts/execute.py`) is the right starting point. Reach for an internal Tools / Services / Utils split when: - multiple tools share orchestration logic, - DCC commands need to be sequenced rather than called one-shot, - helper functions deserve their own unit tests, - one `execute.py` file has grown past ~200 lines. Reference implementation: [`examples/skills/example-layered-skill/`](https://github.com/dcc-mcp/dcc-mcp-core/tree/main/examples/skills/example-layered-skill). ### Recommended layout ```text my-complex-skill/ ├── SKILL.md ← agentskills.io frontmatter + prose ├── tools.yaml ← MCP tool declarations (sibling, per #356) ├── scripts/ │ ├── __init__.py │ ├── create_asset.py ← thin adapter entry point │ ├── publish_asset.py ← thin adapter entry point │ ├── services/ ← business-logic layer (orchestration) │ │ ├── __init__.py │ │ └── asset_service.py │ └── utils/ ← pure helpers (no I/O, no DCC calls) │ ├── __init__.py │ └── path_utils.py └── prompts/ └── system.md ← optional system-prompt sidecar ``` ### Layer responsibilities | Layer | Responsibility | Imports allowed | Size guidance | |-------|----------------|-----------------|---------------| | top-level `scripts/*.py` entry points | Read JSON from stdin, validate, delegate, return `success/error` envelope. | `services/`, stdlib. | < 30 lines per file | | `services/` | Orchestrate DCC commands. Raise typed exceptions on failure. No MCP knowledge. | `utils/`, DCC SDK. | grows with feature | | `utils/` | Pure helpers — path/name normalisation, primitive math. No side effects. | stdlib only. | grows with feature | ### Wiring `source_file` to entry points Keep executable tool entry points at the **top level** of `scripts/` and declare their explicit `source_file:` in `tools.yaml`. Service and utility modules may remain in subpackages because they are imported by an entry point rather than executed as tools: ```yaml # tools.yaml tools: - name: create_asset description: Create a new asset record on disk. source_file: scripts/create_asset.py input_schema: type: object required: [name] properties: name: { type: string } kind: { type: string, default: model } ``` Relative `source_file` paths are resolved against the skill root. ### Cross-layer imports Tool adapters need to import from sibling `services/` and `utils/` packages. Import them directly. The dcc-mcp-core runner exposes the executing script directory only for the duration of the call, so production scripts must not mutate process-global `sys.path`: ```python from services.asset_service import AssetService ``` This specifically excludes the historical [`sys.path.insert()` pattern shown in dcc-mcp-houdini PR #157](https://github.com/dcc-mcp/dcc-mcp-houdini/pull/157/changes#diff-20f6c4a5b206da54475e771ac54351c25975cbcb533595f074c7f26d07ad09a2R11-R13). If a custom runner cannot resolve same-skill helpers, fix the runner's scoped import context. For a direct smoke test, invoke the shared skill runner or let the test harness scope the scripts directory; do not patch `sys.path` inside the shipped entry point. ### Tool adapter template ```python """Tool entry point — create_asset (thin adapter).""" from __future__ import annotations import json import sys from services.asset_service import AssetError, AssetService def main() -> dict: params = json.loads(sys.stdin.read() or "{}") if not params.get("name"): return {"success": False, "message": "`name` is required"} try: asset = AssetService().create(name=params["name"], kind=params.get("kind", "model")) except AssetError as exc: return {"success": False, "message": str(exc)} return { "success": True, "message": f"Created asset {asset.id}", "context": {"asset_id": asset.id, "state": asset.state}, } if __name__ == "__main__": print(json.dumps(main())) ``` ### Anti-patterns to avoid - **Business logic in entry points** — adapters must stay under ~30 lines and only translate between MCP envelopes and service calls. - **`utils/` doing I/O** — anything in `utils/` must be pure so it can be unit-tested without a DCC, a filesystem, or network access. - **Cross-skill imports via relative paths** — share code by promoting it to its own infrastructure skill instead of `from ../other_skill ...`. - **Returning envelopes from `services/`** — services raise typed exceptions; only the adapter wraps the outcome with `success_result()` / `error_result()`. - **Nested executable entry points** — keep declared tool scripts at the top-level `scripts/*.py` boundary so every runner gets the same scoped import context. - **Production `sys.path` mutation** — same-skill helpers are a runner concern; never add `sys.path.insert()` or `sys.path.append()` to a shipped tool script. ### Checklist for a new layered skill - [ ] Entry points live at `scripts/*.py` and stay under ~30 lines - [ ] Shared logic lives in `scripts/services/` and raises typed exceptions - [ ] Pure helpers live in `scripts/utils/` and have no side effects - [ ] Every tool in `tools.yaml` has an explicit `source_file:` - [ ] Entry points import same-skill helpers directly and never mutate `sys.path` - [ ] `metadata.dcc-mcp.tools` is set to `tools.yaml` in `SKILL.md` frontmatter ## Dependency Resolution Skills can declare dependencies on other skills using `metadata.dcc-mcp.depends` in SKILL.md or the sibling `metadata/depends.md` file: ```yaml --- name: maya-animation metadata: dcc-mcp: depends: ["maya-geometry"] --- ``` ```python from dcc_mcp_core import ( resolve_dependencies, validate_dependencies, expand_transitive_dependencies, ) skills, _ = scan_and_load() # Topologically sorted (each skill after its dependencies) ordered = resolve_dependencies(skills) # Validate — returns list of error messages errors = validate_dependencies(skills) # All transitive dependencies for a specific skill deps = expand_transitive_dependencies(skills, "maya-animation") # ["maya-geometry"] ``` Dependency declarations are soft during catalog discovery. If a composition skill depends on `maya-dev` before that skill has been discovered, the composition skill still appears in `search_skills()` / `list_skills()` with `status: "pending_deps"` and a `missing_dependencies` list. Calling `load_skill("maya-animation")` auto-loads any discovered dependencies first; when a dependency is still missing, `load_skill` returns an actionable error that names the missing skill and asks the caller to discover or install it. Use `scan_and_load()` / `resolve_dependencies()` when you explicitly want a fail-fast dependency check for packaging or CI. ## Skill Directory Structure Beyond the required `SKILL.md` and `scripts/`, the [agentskills.io](https://agentskills.io/specification) specification defines optional directories: ``` my-skill/ ├── SKILL.md # Required: metadata + instructions ├── scripts/ # Required: executable code ├── references/ # Optional: supplementary docs loaded on demand │ ├── REFERENCE.md # Detailed technical reference │ └── FORMS.md # Form templates or structured data formats ├── assets/ # Optional: templates, images, data files └── metadata/ # Optional: dcc-mcp-core dependency declarations └── depends.md # YAML list of dependency skill names ``` **`references/`** — Additional documentation that agents load on demand. Keep each file focused and small (< 2000 tokens recommended) to minimize context consumption. Reference files from SKILL.md body using relative paths: `See [reference guide](references/REFERENCE.md) for details.` **`assets/`** — Static resources like document templates, configuration templates, images, or lookup tables. Not automatically loaded; agents access them when needed. > **Tip**: Keep the main `SKILL.md` body under **500 lines** and **5000 tokens**. Move detailed reference material to `references/` files — agents load them only when needed (progressive disclosure). ## SkillMetadata Fields Parsed from agentskills.io `SKILL.md` frontmatter plus sibling files referenced by `metadata.dcc-mcp.*`. Top-level extension keys are rejected by the strict loader; keep dcc-mcp-core payloads in the namespace and sibling files. | Field | Type | Description | |-------|------|-------------| | `name` | `str` | Unique skill name | | `description` | `str` | Short description (should describe what the skill does and when to use it) | | `search_hint` | `str` | Keyword hint for `search_skills` (`metadata.dcc-mcp.search-hint`; falls back to `description`) | | `tools` | `List[ToolDeclaration]` | Tool declarations from `metadata.dcc-mcp.tools` sibling file (use `.name` to get tool names) | | `dcc` | `str` | Target DCC application (default: `"python"`) | | `tags` | `List[str]` | Classification tags | | `scripts` | `List[str]` | Discovered script file paths | | `skill_path` | `str` | Absolute path to the skill directory | | `version` | `str` | Skill version (default: `"1.0.0"`) | | `depends` | `List[str]` | Skill dependency names | | `metadata_files` | `List[str]` | Paths to `.md` files in `metadata/` | | `groups` | `List[SkillGroup]` | Tool groups for progressive exposure (see below) | | `runtimes` | `List[SkillRuntimeDescriptor]` | Optional runtime descriptors from inline `metadata.dcc-mcp.runtimes` or a sibling `runtimes.yaml`; resolved into discovery/detail runtime state without executing tool scripts | | `license` | `str` | License identifier (agentskills.io spec, e.g. `"MIT"`, `"Apache-2.0"`) | | `compatibility` | `str` | Environment requirements, max 500 chars (agentskills.io spec) | | `allowed_tools` | `List[str]` | Pre-approved tools (agentskills.io spec, experimental) | | `external_deps` | `str \| None` | External dependency declaration as JSON string (MCP servers, env vars, binaries). Set via `md.external_deps = json.dumps(deps)`, read via `json.loads(md.external_deps)`. See [Skill Scopes & Policies](skill-scopes-policies.md) for the full schema. | ## Tool Groups (Progressive Exposure) Large skills often expose far more tools than an AI client needs at any given moment. Tool groups let a skill ship several related toolsets and let the client activate only the ones it needs — keeping `tools/list` small while all tools remain discoverable. ### Declaring Groups with Sibling Files Declare groups in `groups.yaml`, referenced from `SKILL.md` via `metadata.dcc-mcp.groups`. Tools then reference a group name through their `group:` field in `tools.yaml`: ```yaml # SKILL.md frontmatter --- name: maya-geometry description: "Domain skill — Maya geometry, modeling, and rigging tools. Use when ..." metadata: dcc-mcp: dcc: maya tools: tools.yaml groups: groups.yaml --- ``` ```yaml # groups.yaml groups: - name: modeling description: Polygon modeling and UV tools. default_active: true tools: [create_sphere, create_cube, extrude] - name: rigging description: Skeleton, joints, and skinning. default_active: false tools: [create_joint] ``` ```yaml # tools.yaml tools: - name: create_sphere description: Create a polygon sphere. group: modeling source_file: scripts/create_sphere.py - name: create_joint description: Create a joint chain. group: rigging source_file: scripts/create_joint.py ``` ### `SkillGroup` Fields | Field | Type | Default | Description | |-------|------|---------|-------------| | `name` | `str` | required | Group identifier (kebab-case recommended) | | `description` | `str` | `""` | Human-readable description | | `default_active` | `bool` | `False` | Active immediately after `load_skill`; `False` means dormant until activated | | `tools` | `List[str]` | `[]` | Tool names in this group — must match `ToolDeclaration.name` entries | ### How Groups Behave After `load_skill` When `SkillCatalog.load_skill("maya-geometry")` runs: 1. All tool declarations are registered in `ToolRegistry` with their group metadata set to the declared group name. 2. Tools in groups where `default_active=false` are hidden from `tools/list`. They remain in the registry (visible via `list_actions()`) and become active once the group is activated. 3. `SkillCatalog.active_groups()` returns the active group names. `SkillCatalog.list_groups()` returns `(skill_name, group_name, active)` tuples for all declared groups. ### Controlling Groups at Runtime ```python from dcc_mcp_core import SkillCatalog, ToolRegistry registry = ToolRegistry() catalog = SkillCatalog(registry) # Via the high-level catalog: group names are global registry group keys. changed = catalog.activate_group("rigging") # enables rigging tools changed = catalog.deactivate_group("rigging") # disables them again groups = catalog.list_groups() # -> [(skill_name, group_name, active)] active = catalog.active_groups() # -> ["modeling", ...] # Or via the registry directly (the HTTP server emits tools/list_changed notifications). changed = registry.set_group_enabled("rigging", True) group_tools = registry.list_actions_in_group("modeling") enabled_only = registry.list_actions_enabled() ``` ### MCP Tools for Group Management `create_skill_server` / `McpHttpServer` register three core MCP tools for group control in addition to the six skill-discovery tools: | Tool | Description | |------|-------------| | `activate_tool_group` | Activate a group by `{ "group": "rigging" }`; emits `notifications/tools/list_changed` so clients refresh their view | | `deactivate_tool_group` | Deactivate a group by `{ "group": "rigging" }` | | `search_tools` | Keyword search across currently-enabled tools (name, description, tags) | `tools/list` also returns `__group__` stubs for any group that is inactive, making the full tool surface discoverable without exposing schemas or handlers. ### Gateway Progressive Group Search When skills are exposed through the gateway, the capability index tracks group activation state. A search hit for a tool in an inactive group includes: - `callable: false` — the agent cannot call it yet - `load_state: "loaded"` — the skill is loaded but the group is inactive - `disabled_by_group: "modeling"` — which group blocks it - `next_step` with `action: "activate_tool_group"` — prescriptive agent guidance The gateway `load_skill` meta-tool supports progressive group activation: ```json { "tool": "load_skill", "arguments": { "skill_name": "maya-modeling", "tool_group": "modeling", "group_action": "activate" } } ``` This implements "progressive tool exposure" — tools are discoverable but not callable until their group is activated, keeping `tools/list` compact while maintaining full discoverability. ## Skill Persistence Across Restarts `DccServerBase.enable_skill_load_persistence(policy)` wires the catalog's after-load / after-unload / after-group-change hooks to a `LoadedStateStore` backed by `~/.dcc-mcp//loaded.json` (source of truth) and the gateway admin SQLite `skill_loaded_state` / `skill_active_groups` tables (best-effort mirror for admin UI visibility). ```python server = DccServerBase(opts) server.start() server.enable_skill_load_persistence(policy="skip_on_drift") ``` On startup the store replays the persisted snapshot so previously loaded skills and active groups are restored without requiring the AI client to re-discover and re-activate them. ### Replay policies | Policy | Behaviour | |--------|-----------| | `"skip_on_drift"` (default) | Skip a skill if its on-disk version differs from the persisted version | | `"require_exact_version"` | Fail the replay if any version mismatch is detected | | `"ignore_version"` | Always reload regardless of version drift | ### Persistence contract - Source of truth: per-DCC JSON file at `~/.dcc-mcp//loaded.json`. - Gateway admin SQLite is a best-effort mirror for dashboard visibility. - All writes use atomic-replace (`write → fsync → rename`). - Hooks must never raise; missing files or schema mismatches behave like empty state. ### Key types ```python from dcc_mcp_core.loaded_state_store import ( LoadedStateStore, LoadedSkillRecord, PersistedCatalogState, ) store = LoadedStateStore("maya") # path defaults to ~/.dcc-mcp/maya/loaded.json store.record_loaded("maya-geometry", version="1.0.0", skill_path="/skills/maya-geometry") store.record_unloaded("maya-geometry") store.record_group_change("rigging", activated=True) snapshot = store.snapshot() # -> PersistedCatalogState ``` ## Semantic Skill Search dcc-mcp-core ships a lexical + vector fusion index so morphology variants and inflected queries (`rendering` vs `render`) still recall the right skill. ### Standard fusion setup ```python from dcc_mcp_core import LexicalSkillIndex, RrfFusionIndex, VectorSkillIndex fused = ( RrfFusionIndex() .register("lex", LexicalSkillIndex()) .register("vec", VectorSkillIndex()) ) fused.index(documents) hits = fused.search("how do i create a polygon sphere", k=8) ``` ### Components | Class | Purpose | Dependencies | |-------|---------|-------------| | `LexicalSkillIndex` | BM25-style keyword matching | Zero deps (pure Python) | | `VectorSkillIndex` | Dense embedding similarity search | Zero deps by default (`HashedEmbedder` + `InMemoryVectorStore`) | | `RrfFusionIndex` | Reciprocal Rank Fusion combiner (Cormack et al. 2009) | Zero deps | | `HashedEmbedder` | Deterministic feature-hashing embedder (~5 µs/doc at dim=256) | Zero deps | | `OnnxEmbedder` | High-quality neural embeddings (three-tier backend) | Optional: `dcc-mcp-core-semantic` or `fastembed` | ### Embedder backends `OnnxEmbedder` uses a three-tier fallback: 1. `dcc_mcp_core_semantic.native.NativeEmbedder` — Rust-native via fastembed-rs (fastest) 2. `fastembed.TextEmbedding` — pure-Python fastembed 3. Neither installed → `EmbedderError` ```python from dcc_mcp_core import OnnxEmbedder, VectorSkillIndex emb = OnnxEmbedder(model_name="BAAI/bge-base-en-v1.5") idx = VectorSkillIndex(embedder=emb) ``` Environment variables for model overrides: | Variable | Purpose | |----------|---------| | `DCC_MCP_EMBED_MODEL` | Override the default ONNX model name | | `DCC_MCP_EMBED_MODEL_DIR` | Override the local model cache directory | ### `SkillDocument` and `SkillSearchHit` ```python from dcc_mcp_core.semantic_skill_index import SkillDocument, SkillSearchHit doc = SkillDocument( skill_id="maya-geometry", name="maya-geometry", summary="Maya geometry creation and modification tools", intent="Create spheres, cubes, and extrude polygon components", tags=("geometry", "create"), dcc_name="maya", ) # doc.corpus() returns the concatenated text for embedding hit = SkillSearchHit(skill_id="maya-geometry", score=0.85, rank=1) ``` ### Gateway Hybrid Search Mode (opt-in) The Rust gateway daemon supports three search modes via the `mode` parameter in the unified `search` tool: | `mode` | Strategy | Default | |--------|----------|---------| | `fuzzy` | BM25 + nucleo fuzzy matching with prefix/subsequence bonuses | Yes | | `exact` | Substring-only matching (legacy deterministic table) | No | | `hybrid` | Fuzzy primary + semantic boost when the gateway is configured | No | `mode=hybrid` currently maps to `mode=fuzzy` internally (Phase 1 wire-up). When the gateway is configured with `semantic_search_enabled: true`, a future release will apply semantic embedding similarity to boost conceptual recall. **Enable semantic search**: ```bash pip install 'dcc-mcp-core[semantic]' export DCC_MCP_SEMANTIC_SEARCH_ENABLED=1 ``` The search response includes a `semantic` object indicating whether the semantic provider is active and whether a hybrid→fuzzy downgrade occurred: ```json { "search_id": "...", "ranker_version": "gateway-hybrid-v2", "semantic": { "active": false, "note": "mode=hybrid requested but semantic search is not enabled; fell back to mode=fuzzy" }, "total": 5, "hits": [...] } ``` ## Lifecycle Hooks The typed lifecycle-hook framework lets adapter and policy code subscribe to discovery, tool-call, and session events without patching `DccServerBase` internals. ### Hook events ```python from dcc_mcp_core.lifecycle_hooks import HookEvent # Available events: HookEvent.SESSION_START # on_session_start HookEvent.BEFORE_SEARCH # before_search (policy — can veto) HookEvent.AFTER_SEARCH # after_search HookEvent.BEFORE_SKILL_LOAD # before_skill_load (policy — can veto) HookEvent.AFTER_SKILL_LOAD # after_skill_load HookEvent.BEFORE_TOOL_CALL # before_tool_call (policy — can veto) HookEvent.AFTER_TOOL_CALL # after_tool_call HookEvent.SESSION_END # on_session_end ``` Policy events (`BEFORE_SKILL_LOAD`, `BEFORE_TOOL_CALL`, `BEFORE_SEARCH`) can veto an operation by raising `HookDeny`: ```python from dcc_mcp_core.lifecycle_hooks import HookDeny def my_policy(ctx): if is_blocked(ctx.payload.get("tool_name")): raise HookDeny("Tool blocked by policy", hint="Use dcc_diagnostics__screenshot instead") hooks.on(HookEvent.BEFORE_TOOL_CALL, my_policy) ``` ### Registration ```python from dcc_mcp_core.lifecycle_hooks import LifecycleHooks, HookEvent hooks = LifecycleHooks() server.register_lifecycle_hooks(hooks) @hooks.on(HookEvent.AFTER_TOOL_CALL) def log_call(ctx): print(f"Tool {ctx.payload.get('tool_name')} called on {ctx.dcc_name}") ``` ### Dispatch helpers `DccServerBase` provides convenience methods for adapter code to fire events: ```python server.dispatch_session_start(session_id="abc-123") server.dispatch_before_tool_call("create_sphere", session_id="abc-123") server.dispatch_after_tool_call("create_sphere", payload={"ok": True}) server.dispatch_session_end(session_id="abc-123") ``` ### Fail-safe fan-out Handlers are dispatched in registration order. For non-policy events, handler exceptions are logged at WARNING and swallowed. For policy events, a `HookDeny` raised by any handler propagates to the caller; other exceptions are logged and treated as "no decision". ## Agent Memory Three-tier memory model for DCC adapters: bounded, session-aware, and privacy-conscious. The contract is "summarised facts only, never raw prompts". ### Memory layers | Layer | Scope | Persistence | Default cap | |-------|-------|-------------|-------------| | `EPHEMERAL` | Session-scoped ring-buffer | Never persisted | 256 per session | | `WORKING` | Task-scoped, TTL-based | Never persisted by default | 1024 per session, 6h TTL | | `LONGTERM` | Durable patterns | Requires explicit storage backend | 4096 total | ### Quick start ```python from dcc_mcp_core.agent_memory import InMemoryMemoryStore, MemoryRecorder from dcc_mcp_core.lifecycle_hooks import LifecycleHooks hooks = LifecycleHooks() store = InMemoryMemoryStore() recorder = MemoryRecorder(store).install(hooks) server.register_lifecycle_hooks(hooks) ``` `MemoryRecorder.install(hooks)` registers handlers for `SESSION_START`, `BEFORE_SEARCH`, `AFTER_SKILL_LOAD`, `BEFORE_TOOL_CALL`, `AFTER_TOOL_CALL`, and `SESSION_END`. Injection is conservative: search payloads receive compact ranking hints (`memory_summary`, `memory_prefer_tools`, `memory_avoid_tools`, `memory_skip_reasons`), tool-call payloads receive memory only when it matches the current `tool_name`, and `SESSION_START` injection is opt-in via `inject_on_session_start=True`. Use `SqliteMemoryStore()` when operators need durable, Admin-visible longterm patterns: ```python from dcc_mcp_core.agent_memory import SqliteMemoryStore, MemoryRecorder store = SqliteMemoryStore() # resolves the gateway admin SQLite path recorder = MemoryRecorder(store).install(hooks) ``` By default this persists only `LONGTERM` entries. `EPHEMERAL` and `WORKING` entries remain process-local so short-lived scene/task context is not made durable by accident. ### Session compaction On `SESSION_END`, working entries are promoted to `LONGTERM` as `"pattern:*"` summaries, then ephemeral and working entries are forgotten. This keeps the memory footprint bounded while preserving learned patterns across sessions. ### Privacy toggle ```python recorder.set_enabled(False) # disables capture/injection without unregistering hooks ``` `recorder.stats()` returns low-cardinality observability counters such as `summary_queries`, `summary_hits`, `summary_hit_rate`, `search_injections`, `tool_call_injections`, and `promotions`. The Admin Memory tab reads the same SQLite table for durable records and computes aggregate hit-rate from `ok_count` / `fail_count` payload fields. ### Querying memory ```python from dcc_mcp_core.agent_memory import MemoryQuery, MemoryLayer # Query longterm patterns for a specific DCC results = store.query(MemoryQuery(layer=MemoryLayer.LONGTERM, dcc_name="maya", limit=10)) # Query all layers for a session results = store.query(MemoryQuery(session_id="abc-123")) # Forget all entries for a session forgotten = store.forget(session_id="abc-123") ``` ## Capability Graph (#1336) The `CapabilityGraph` models directed relationships between skills, tools, and named capabilities so the search ranker, agent planner, and admin UI can answer: - "What else do I need before this skill is useful?" (`REQUIRES`) - "What does this skill produce that other skills need?" (`PRODUCES`) - "If this skill is missing, what is the fallback?" (`FALLBACK_FOR`) ### Edge kinds | Kind | Direction | Meaning | |------|-----------|---------| | `DEPENDS_ON` | source → target | Source depends on target (weaker than `REQUIRES`) | | `REQUIRES` | source → target | Source cannot function without target | | `PRODUCES` | source → target | Source generates capability that target consumes | | `USED_IN` | source → target | Source capability is used in target context | | `COMPATIBLE_WITH` | source ↔ target | Bidirectional compatibility (soft) | | `REPLACES` | source → target | Source supersedes target | | `FALLBACK_FOR` | source → target | Source is a fallback when target is unavailable | ### Quick start ```python from dcc_mcp_core import CapabilityGraph, CapabilityEdge, EdgeKind graph = CapabilityGraph() # Register a skill with its declared metadata graph.register_skill( "maya-geometry", requires=["dcc-diagnostics"], produces=["geometry-output"], depends_on=["maya-core"], compatible_with=["maya-render"], ) # Add individual edges graph.add_edge(CapabilityEdge( source="maya-geometry", target="maya-modeling", kind=EdgeKind.COMPATIBLE_WITH, weight=0.8, )) ``` ### Expansion and query ```python # Bounded BFS expansion (depth clamped to [1, 16]) reachable = graph.expand(["maya-geometry"], max_depth=2, direction="out") # Inspect neighbors deps = graph.neighbors("maya-geometry", kinds=[EdgeKind.REQUIRES], direction="out") # Serialize payload = graph.to_json() # {"nodes": [...], "edges": [...]} restored = CapabilityGraph.from_json(payload) # Stats print(len(graph)) # node count print(graph.edge_count()) # edge count ``` The graph is **threadsafe** (RLock) and designed as pure data with bounded expansion — no I/O, no async, no embedding inference. Idempotent inserts prevent duplicate edges on re-registration. ### Integration points The capability graph feeds into: - **Semantic skill index** (#1333) — enrichment of search hits with graph context - **Agent memory layers** (#1334) — pattern-level recall of successful skill chains - **Gateway search reranker** — boosting results that have verified `REQUIRES` edges satisfied by loaded skills ## 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 (used when per-app var is not set) | ::: tip Per-app paths take priority `DCC_MCP_MAYA_SKILL_PATHS` is checked first for `app_name="maya"`. `DCC_MCP_SKILL_PATHS` is the global fallback. ::: ## one-call Skills-First Setup: `create_skill_server` For the fastest possible setup, use `create_skill_server` (v0.12.12+). It wires together `ToolRegistry`, `ToolDispatcher`, `SkillCatalog`, and `McpHttpServer` in one call: ```python import os from dcc_mcp_core import create_skill_server, McpHttpConfig # Set per-app skill paths os.environ["DCC_MCP_MAYA_SKILL_PATHS"] = "/studio/maya-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 connect to http://127.0.0.1:8765/mcp ``` `create_skill_server` automatically: 1. Creates an `ToolRegistry` and `ToolDispatcher` 2. Creates a `SkillCatalog` wired to the dispatcher 3. Discovers skills from `DCC_MCP_MAYA_SKILL_PATHS` and `DCC_MCP_SKILL_PATHS` 4. Returns a ready-to-start `McpHttpServer` ```python def create_skill_server( app_name: str, config: McpHttpConfig | None = None, extra_paths: list[str] | None = None, dcc_name: str | None = None, ) -> McpHttpServer: ... ``` | Parameter | Type | Description | |-----------|------|-------------| | `app_name` | `str` | DCC application name (`"maya"`, `"blender"`, etc.) — derives env var and server name | | `config` | `McpHttpConfig \| None` | HTTP config; defaults to port 0 (OS-assigned) | | `extra_paths` | `list[str] \| None` | Extra skill dirs to scan in addition to env vars | | `dcc_name` | `str \| None` | Override DCC filter for skill scanning (defaults to `app_name`) | ## Built-in Skills Unified Registration (#1332) `register_all_builtin_skills()` registers the full set of standard, host-local tools in one idempotent call. Every DCC adapter gets diagnostics, introspection, feedback, recipes, Qt UI inspection, and script materialization without wiring each family by hand. ```python from dcc_mcp_core import DccServerBase, DccServerOptions from dcc_mcp_core.skills.builtin import register_all_builtin_skills opts = DccServerOptions.from_env("maya") server = DccServerBase(opts) register_all_builtin_skills( server, dcc_name="maya", dcc_pid=os.getpid(), dcc_window_title="Autodesk Maya", ) server.start() ``` ### Registered tool families | Family | Prefix | Tools | Description | |--------|--------|-------|-------------| | Diagnostics | `dcc_diagnostics__*` | `screenshot`, `audit_log`, `tool_metrics`, `process_status`, `gateway_failover` | Host-local diagnostics and observability | | Introspection | `dcc_introspect__*` | `list_module`, `signature`, `search`, `eval` | Runtime DCC namespace introspection | | Feedback | `dcc_feedback__*` | `report` | Agent feedback / rationale capture | | Recipes | `dcc_recipes__*` | `list`, `search`, `get`, `validate`, `apply` | Skill/domain recipe operations | | Qt Inspector | `qt_ui_inspector__*` | `list_windows`, `find_widgets`, `describe_widget`, `snapshot_tree`, `wait_for_widget` | DCC-agnostic Qt widget introspection | | Script Materialization | `materialize__*` | `materialize_script` | Host-local script file materialization | The call is **idempotent** — adapters can call it multiple times (e.g. once at base-server init with an empty skill set, then again after scanning skills to populate recipe data). The `skills` parameter forwards `SkillMetadata` objects to `register_recipes_tools`; pass `None` (the default) when skills haven't been scanned yet. `DccServerBase.__init__` calls `register_all_builtin_skills` automatically, so most adapters don't need to call it directly. Use the standalone function only when building a custom server that does not inherit from `DccServerBase`. ## Qt UI Inspector (#1332) The Qt UI inspector is a **DCC-agnostic, read-only** capability that lets AI agents inspect the Qt widget tree of any Qt-based DCC host (Maya, Houdini, Nuke, Substance, Katana, etc.). It imports the Qt binding lazily so the module never pulls Qt into the host on import. ### Supported Qt bindings (priority order) 1. `qtpy` — abstraction layer (recommended) 2. `PySide6` — Qt 6 official bindings 3. `PySide2` — Qt 5 official bindings 4. `PyQt6` — Qt 6 Riverbank bindings 5. `PyQt5` — Qt 5 Riverbank bindings When no binding is importable, every tool returns a structured `qt-binding-unavailable` envelope rather than crashing the host. ### Tools | Tool | Description | Key Parameters | |------|-------------|----------------| | `qt_ui_inspector__list_windows` | List every top-level Qt window with object name, class, visibility, geometry, and child count | `include_hidden`, `max_results` (max 256) | | `qt_ui_inspector__find_widgets` | Locate Qt widgets by object name (exact/substring/regex), class name, and visibility | `object_name`, `class_name`, `visible_only`, `max_results` | | `qt_ui_inspector__describe_widget` | Return a single widget's structured state: class, geometry, flags, accessible name/description, bounded property snapshot (≤32 properties) | `widget_id` (required) | | `qt_ui_inspector__snapshot_tree` | Walk the Qt widget tree from a root and return a JSON-safe tree with depth (≤16) and node-count (≤4096) budgets | `root_widget_id`, `max_depth`, `max_nodes` | | `qt_ui_inspector__wait_for_widget` | Poll for a widget by name/class with visible/enabled gates and bounded timeout (≤60 s) | `object_name`, `class_name`, `visible`, `enabled`, `timeout_ms` | ### One-line registration ```python from dcc_mcp_core import register_qt_ui_inspector register_qt_ui_inspector(server, dcc_name="maya") ``` Call this **before** `server.start()`. `register_all_builtin_skills` calls it automatically, so adapters using that path don't need a separate call. ## Supported Script Types | Extension | Type | Execution | |-----------|------|-----------| | `.py` | Python | `python` interpreter | | `.sh`, `.bash` | Shell | `bash` | | `.bat`, `.cmd` | Batch | `cmd /C` | | `.mel` | MEL (Maya) | `python` wrapper | | `.ms` | MaxScript (3ds Max) | `python` wrapper | | `.lua`, `.hscript` | Lua / Houdini | `python` wrapper | ::: tip Skills-First Architecture `create_skill_server` is the recommended entry-point since v0.12.12. It combines `SkillCatalog` automatic script execution with MCP HTTP serving — agents can call tools via `tools/call` with zero manual handler registration. ::: ::: warning script execution All scripts run as subprocesses. Input parameters are passed via stdin as JSON. The script should write a JSON result to stdout and exit with code 0 on success. ::: ## On-Demand Skill Discovery (MCP HTTP) When using the MCP HTTP server (`McpHttpServer` or `create_skill_server`), `tools/list` returns a **three-tier** response: ### Three-Tier `tools/list` Response 1. **5 core discovery tools** (always present): - `list_skills` — List all skills with optional status filter - `get_skill_info` — Get full metadata for a specific skill - `load_skill` — Load a skill, registering its tools in ToolRegistry - `unload_skill` — Unload a skill, removing its tools - `search_skills` — Keyword search across name, description, search_hint, and tool_names 2. **Loaded skill tools** — Full `input_schema` from the `ToolRegistry` for all currently loaded skills 3. **Unloaded skill stubs** — `__skill__` entries with a one-line description only (no full schema) ### Workflow ``` 1. AI calls tools/list → sees core tools + loaded tools + __skill__ stubs 2. AI calls search_skills(query="geometry") → finds matching skills 3. AI calls load_skill(skill_name="maya-geometry") → tools registered 4. AI calls tools/list again → maya-geometry tools now have full schemas 5. AI calls maya_geometry__create_sphere → skill script executes ``` ### Skill Stub Behavior Calling an unloaded skill stub (`__skill__`) returns an error with a hint: ```json { "error": "Skill 'maya-geometry' is not loaded. Call load_skill(skill_name=\"maya-geometry\") to register its tools." } ``` ### `search_skills` MCP Tool ```json { "name": "search_skills", "description": "Search for skills matching a keyword query", "inputSchema": { "type": "object", "properties": { "query": {"type": "string", "description": "Search keyword"}, "dcc": {"type": "string", "description": "Filter by DCC type"} }, "required": ["query"] } } ``` Searches across: `name`, `description`, `search_hint`, `search_aliases`, `tags`, `dcc`, and the sibling `tools.yaml` entries (`name`, `description`, and per-tool `search_aliases`). The `search_hint` field (from `metadata.dcc-mcp.search-hint`) improves keyword matching without loading full schemas. #### How `search_skills` ranks results Since dcc-mcp-core 0.15 (issue [#343](https://github.com/dcc-mcp/dcc-mcp-core/issues/343)) the ranker is a tokenised BM25-lite scorer. It is deterministic — the same skill set + query always yields the same order. **Tokeniser** — lowercase, split on `[\s_\-.,;:/]+`, drop a small stopword list (`a, an, the, of, and, or, to, for, with, from`). No stemming, no fuzzy match. **Field weights** | Field | Weight | |-------|-------:| | `name` | 5.0 | | `dcc` (exact token match only) | 4.0 | | `tags` | 3.0 | | `search_hint` | 3.0 | | `search_aliases` | 3.0 | | `description` | 2.0 | | sibling tool names (from `tools.yaml`) | 2.0 | | sibling tool search aliases | 2.0 | | sibling tool descriptions | 1.0 | **Scoring** — standard BM25 per query token with `k1=1.2`, `b=0.75`, document length = total tokens across all weighted fields. The per-field contributions are multiplied by the weights above and summed across query tokens. **Tie-breaks** (in order) 1. **Exact-name fast-path** — if the query equals a skill's name (case-insensitive, after trimming), that skill sorts first unconditionally. 2. **Name-substring hit** — skills whose `name` contains the query as a raw substring rank above equal-scoring skills that don't. 3. **Scope precedence** — `Admin > System > User > Repo`. 4. **Alphabetical name**. Skills with a total score of `0.0` (and no exact-name match) are dropped. The `tags` and `dcc` filter arguments are applied *before* scoring. `create_skill_server()` only calls `discover()` at startup — skills are **not** automatically loaded. This keeps the initial tool list small and lets agents load only what they need. ## Migrating pre-0.15 SKILL.md Starting with dcc-mcp-core 0.15 (issue [#356](https://github.com/dcc-mcp/dcc-mcp-core/issues/356)), dcc-mcp-core-specific extension keys (`dcc`, `version`, `tags`, `tools`, ...) MUST live under the agentskills.io-compliant nested `metadata.dcc-mcp` namespace rather than at the top level of SKILL.md frontmatter. The strict v0.15+ loader also no longer promotes the pre-0.15 flat dotted form (`metadata: { "dcc-mcp.dcc": ... }`) into typed fields. A SKILL.md with legacy top-level keys fails to load and emits a `tracing::error!`; MCP `list_skills(status="skipped")`, `search_skills`, and `load_skill` now surface the skipped reason and suggested replacement so operators do not need startup logs to diagnose a missing skill. ### Before (pre-0.15 legacy form — no longer accepted) ```yaml --- name: maya-geometry description: "Maya geometry creation and modification tools" dcc: maya version: "1.0.0" tags: [geometry, create] search-hint: "polygon modeling, sphere, bevel, extrude" tools: - name: create_sphere description: "Create a polygon sphere" source_file: scripts/create_sphere.py --- ``` ### After (0.15+, agentskills.io 1.0 compliant) ```yaml --- name: maya-geometry description: "Maya geometry creation and modification tools" license: MIT metadata: dcc-mcp: dcc: maya version: "1.0.0" tags: [geometry, create] search-hint: "polygon modeling, sphere, bevel, extrude" tools: tools.yaml # sibling file (relative to SKILL.md) --- ``` Then put the MCP tool declarations in a sibling `tools.yaml`: ```yaml # tools.yaml — same schema as the inline tools: block tools: - name: create_sphere description: "Create a polygon sphere" source_file: scripts/create_sphere.py groups: - name: advanced default-active: false tools: [create_sphere] ``` #### Declaring tool annotations (issue #344) Each tool entry in `tools.yaml` can carry MCP [`ToolAnnotations`](https://modelcontextprotocol.io/specification/2025-03-26/server/tools#tool-annotations) that describe how the tool behaves. dcc-mcp-core surfaces them on `tools/list` with spec-compliant camelCase keys (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`). Tools without any declared annotations **omit** the `annotations` field entirely — no empty object. Two declaration forms are accepted: **1. Canonical — nested `annotations:` map (preferred).** ```yaml # tools.yaml tools: - name: delete_keyframes description: Delete keyframes in a frame range annotations: read_only_hint: false destructive_hint: true idempotent_hint: true open_world_hint: false ``` **2. Shorthand — flat hint keys on the tool entry (backward compatibility).** ```yaml tools: - name: get_keyframes read_only_hint: true idempotent_hint: true ``` **Precedence: the nested `annotations:` map wins whole-map when both forms are present for the same tool** — not a per-field merge. This avoids confusing precedence when authors migrate from shorthand to canonical: ```yaml tools: - name: risky read_only_hint: true # ignored (shorthand) idempotent_hint: true # ignored (shorthand) annotations: destructive_hint: true # wins — only this hint is surfaced ``` **`deferred_hint` is a dcc-mcp-core extension, not MCP 2025-03-26.** When you declare `deferred_hint: true`, it rides in `_meta["dcc.deferred_hint"]` on the tool declaration — never inside the spec-standard `annotations` map (which would make the payload non-compliant). The same `_meta` slot also carries the `execution: async` implication from issue #317. #### Declaring `next-tools` (issue #342) `next-tools` belongs **inside each tool entry in `tools.yaml`** — never as a top-level SKILL.md frontmatter key. The server surfaces the declared list on `CallToolResult._meta["dcc.next_tools"]`: - `on-success` list → attached after a successful tool call - `on-failure` list → attached after an error (`isError == true`) - No `next-tools` declared → `_meta["dcc.next_tools"]` is omitted entirely ```yaml # tools.yaml tools: - name: create_sphere description: "Create a polygon sphere" source_file: scripts/create_sphere.py next-tools: on-success: - maya_geometry__bevel_edges - maya_geometry__assign_material on-failure: - diagnostics__screenshot - diagnostics__audit_log ``` Tool names are validated with `dcc_mcp_naming::validate_tool_name` at skill-load time. Invalid entries are dropped with a `tracing::warn!` and the rest of the skill loads normally — a single malformed name will not fail the whole skill. A top-level `next-tools:` block on SKILL.md is rejected by the loader (issue #356 strict mode) — move per-tool `next-tools` entries into `tools.yaml` alongside the tool they belong to. ### Metadata key reference | Legacy top-level (no longer accepted) | Spec-compliant `metadata` key | Value type | | ------------------------------------- | -------------------------------------------- | --------------------- | | `dcc: maya` | `metadata["dcc-mcp.dcc"]` | string | | `version: 1.0.0` | `metadata["dcc-mcp.version"]` | string | | `tags: [a, b]` | `metadata["dcc-mcp.tags"]` | comma-separated string | | `search-hint: "…"` | `metadata["dcc-mcp.search-hint"]` | string | | `depends: [x, y]` | `metadata["dcc-mcp.depends"]` | list or comma-separated string | | `products: [maya]` | `metadata["dcc-mcp.products"]` | comma-separated string | | `allow_implicit_invocation` | `metadata["dcc-mcp.allow-implicit-invocation"]` | `"true"` / `"false"` | | `external_deps: {...}` | `metadata["dcc-mcp.external-deps"]` | JSON string | | `tools: [...]` inline block | `metadata["dcc-mcp.tools"]` | sibling `.yaml` file | | `groups: [...]` inline block | `metadata["dcc-mcp.groups"]` | sibling `.yaml` file | | *(MCP prompts primitive)* | `metadata["dcc-mcp.prompts"]` | sibling `.yaml` file or `prompts/*.prompt.yaml` glob | | *(MCP resources primitive)* | `metadata["dcc-mcp.resources"]` | sibling `.yaml` file, directory, or `resources/*.resource.yaml` glob | ### Validating your skill `validate_skill()` reports any non-spec top-level key as a frontmatter error. Use it from a pre-commit hook or CI to catch migrations that were missed: ```python report = dcc_mcp_core.validate_skill("/path/to/my-skill") if report.has_errors: for issue in report.issues: print(f"[{issue.severity}] {issue.category}: {issue.message}") ``` A one-shot CLI migrator (`dcc-mcp-migrate-skill`) is planned as a follow-up; see the tracking issue for status. ### The sibling-file pattern is the rule for every new extension The migration table above is not an exhaustive list — it is an example of **the single design rule** that governs every SKILL.md extension dcc-mcp-core adds from v0.15 onward: > **New extensions live under `metadata.dcc-mcp.` and point at sibling files; they never add new top-level frontmatter keys and they never inline large payloads into SKILL.md.** Concrete applications (shipped or in flight): | Feature | Metadata key | Sibling file(s) | Issue | |---|---|---|---| | Tool declarations + groups | `metadata["dcc-mcp.tools"]`, `metadata["dcc-mcp.groups"]` | `tools.yaml` | #356 | | Workflow specs | `metadata["dcc-mcp.workflows"]` | `workflows/*.workflow.yaml` | #348 | | Prompts / templates | `metadata["dcc-mcp.prompts"]` | `prompts/*.prompt.yaml` | #351, #355 | | Static MCP resources | `metadata["dcc-mcp.resources"]` | `resources/*.resource.yaml` | #733 | | Example dialogues | `metadata["dcc-mcp.examples"]` | `references/EXAMPLES.md` or `examples/*.md` | #1236 | | Tool annotation packs | `metadata["dcc-mcp.annotations"]` | `annotations.yaml` or carried in `tools.yaml` | #344 | | next-tools behaviour chains | (carried inline inside `tools.yaml`) | n/a — never a top-level SKILL.md field | #342 | Layout convention (pick the shape that matches the feature's cardinality): ``` my-skill/ ├── SKILL.md ├── tools.yaml # one file — tools + groups + next-tools ├── workflows/ # many files — one per workflow │ ├── vendor_intake.workflow.yaml │ └── nightly_cleanup.workflow.yaml ├── prompts/ # many files — one per prompt template │ └── review_scene.prompt.yaml ├── resources/ # many files — one per MCP resource bundle │ ├── cmds_help.resource.yaml │ └── help/ │ └── polySphere.txt └── references/ # Markdown reference material (agentskills.io standard) ├── EXAMPLES.md └── REFERENCE.md ``` Why the rule holds: - **Spec-conformance**: `skills-ref validate` (agentskills.io's reference validator) passes without a custom ruleset. - **Progressive disclosure**: `search_skills` reads only SKILL.md; a sibling file loads on demand when the agent activates that feature. - **Bounded SKILL.md**: the body stays ≤500 lines / ≤5000 tokens regardless of how many workflows / prompts / examples ship. - **Diff-friendly**: a new workflow is one new YAML file in review, not a 300-line diff inside SKILL.md. If a feature you are designing can't fit this pattern, that is a signal to write a proposal under `docs/proposals/` and discuss the frontmatter impact before implementing. ### Declaring static MCP resources A skill can expose static reference material through MCP `resources/list` and `resources/read` without Python glue. Point `metadata.dcc-mcp.resources` at a resources directory, glob, or single YAML file: ```yaml metadata: dcc-mcp: resources: resources/ ``` Each `.resource.yaml` file may contain one resource, a top-level list, or a `resources:` list. Static file resources use paths relative to the YAML file: ```yaml resources: - uri: maya-cmds://help/polySphere name: cmds.polySphere help mimeType: text/plain description: Output of cmds.help('polySphere') captured at skill build time. source: type: file path: help/polySphere.txt ``` Loaded skills contribute these entries to `resources/list`; `resources/read` returns text for `text/*`, JSON, YAML, XML, and JavaScript MIME types, and a base64 `blob` for other MIME types. ## Validating Skills Use `validate_skill` to check a skill directory against the specification before loading it at runtime. This catches structural errors, missing files, and format violations early. ```python from dcc_mcp_core import validate_skill report = validate_skill("/path/to/my-skill") if report.is_clean: print("Skill is valid!") else: for issue in report.issues: print(f"[{issue.severity}] {issue.category}: {issue.message}") ``` ### Validation Rules The validator checks the following categories: | Category | Checks | |----------|--------| | **SkillMd** | `SKILL.md` exists and is readable | | **Frontmatter** | YAML is well-formed; required fields (`name`, `description`) present; `name` is kebab-case and ≤64 chars; `description` ≤1024 chars; `compatibility` ≤500 chars | | **Tools** | Tool names are non-empty, unique, and snake_case; descriptions are present; `group` references exist in declared groups; `next-tools` references point to existing tools | | **Scripts** | `source_file` references exist in `scripts/`; file extensions are supported | | **Sidecars** | `metadata.dcc-mcp.tools/groups/prompts` sibling files exist | | **Dependencies** | `depends` entries are non-empty; `metadata/depends.md` exists when `depends` is declared | ### Severity Levels - **Error** — the skill cannot be loaded or will malfunction - **Warning** — the skill loads but violates a best-practice or spec recommendation - **Info** — purely informational (e.g. legacy extension field deprecation notice) ### Using the `dcc-mcp-skills-creator` Skill The `skills/dcc-mcp-skills-creator/` skill provides scaffolding and validation helpers as MCP tools, plus the agent-facing authoring guidance for building or modernizing DCC-MCP adapter skill packages: ```python # Scaffold a new skill directory via the loaded MCP tool: # dcc_mcp_skills_creator__create_skill(name="my-new-skill", parent_dir="/path/to/skills", dcc="maya") # Creates: my-new-skill/SKILL.md, tools.yaml, scripts/, metadata/ # Get a current SKILL.md template via: # dcc_mcp_skills_creator__skill_template() ``` Load it before changing `SKILL.md`, `tools.yaml`, adapter skill taxonomies, or host-specific scripts in repositories such as `dcc-mcp-maya`, `dcc-mcp-blender`, `dcc-mcp-3dsmax`, or future DCC adapters. Its `references/` cover: - `SKILL.md`, `tools.yaml`, and script authoring checklists. - DCC tool contracts and host differences across Maya, Blender, 3ds Max, Houdini, bridge-based hosts, and custom studio tools. - Unit, lint, gateway, E2E, and VRS validation choices.