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. For agent-facing adapter skill development guidance, load skills/dcc-mcp-skill-developer/. In-tree reference skills: python/dcc_mcp_core/skills/dcc-diagnostics, python/dcc_mcp_core/skills/workflow.
Quick Start
1. Create a Skill Directory
maya-geometry/
├── SKILL.md
├── tools.yaml
└── scripts/
├── create_sphere.py
└── export_fbx.bat2. Write SKILL.md and tools.yaml
SKILL.md:
---
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"
tools: tools.yaml
---
# Maya Geometry Skill
Use these tools to create and modify geometry in Maya.# tools.yaml
tools:
- name: create_sphere
description: Create a polygon sphere with the given radius.
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.batThe metadata.dcc-mcp.search-hint field provides comma-separated keywords for efficient skill discovery via search_skills without loading full tool schemas.
3. Set Environment Variable
# 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:
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.
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 NoneSkillSummary 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 |
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:
tools:
- name: create_sphere
description: "Create a polygon sphere"
inputSchema:
type: object
properties:
radius:
type: number
outputSchema:
type: object
properties:
name:
type: string
read_only: false
destructive: false
idempotent: false
defer-loading: true
source_file: scripts/create_sphere.pyfrom 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 |
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) |
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:
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
objectinputSchema.
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, 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 with zero runtime dependencies. Unsupported types raise TypeError with a clear escape hatch: pass an explicit input_schema=... dict or use pydantic's MyModel.model_json_schema().
Why not pydantic?
We intentionally stay zero-dependency. 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.
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. SEP-986 dot-namespacing (skill.tool_name) is also supported — see Naming Rules 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:
ToolDeclaration.source_file— explicit path wins- A script in
scripts/whose stem matches the tool name - If the skill has only one script, it backs all tools
- No handler registered (tool visible in registry but not executable)
Low-Level Skill Functions
For simple one-shot scanning without progressive loading:
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") # skip errors
# 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 excChoosing the right entry point
scan_and_load— default; logs awarn-level summary for every skipped directory (issue maya#138) and a per-failurewarnfromparse_skill_md. Good for production where missing skills should be visible in logs but not block start-up.scan_and_load_lenient— same return shape but tolerates resolver errors (missing dependency, dependency cycle).scan_and_load_strict— fails fast when the scanner skipped any directory. Use in CI / packaged adapter releases where a malformedSKILL.mdshould be a blocking error rather than a silent omission.
# 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:
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 three layers
| Layer | Role | dcc-mcp.layer value |
|---|---|---|
| infrastructure | Low-level, DCC-agnostic primitives. Stable API. Auto-loaded or shared. | infrastructure |
| domain | Business workflows for a specific DCC or task. Depends on infrastructure. | domain |
| example | Authoring references and demos. Never loaded in production. | example |
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.
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.
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:
<Layer> skill — <one-sentence what + scope keywords>. Use when <trigger>.
Not for <counter-example> — use <other-skill> for that.# 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.Metadata layer tag
Tag every skill with its layer so search_skills can filter by layer:
metadata:
dcc-mcp:
layer: infrastructure # or: domain | example# 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:
# 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:
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: domainis set - [ ]
descriptionstarts withDomain skill —and ends with at least oneNot for … — use … for that.sentence - [ ]
search-hintuses intent-oriented keywords that do not overlap with infrastructure skills - [ ]
metadata.dcc-mcp.dependslists every infrastructure skill referenced innext-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.pyfile has grown past ~200 lines.
Reference implementation: examples/skills/example-layered-skill/.
Recommended layout
my-complex-skill/
├── SKILL.md ← agentskills.io frontmatter + prose
├── tools.yaml ← MCP tool declarations (sibling, per #356)
├── scripts/
│ ├── __init__.py
│ ├── tools/ ← thin adapter layer (entry points)
│ │ ├── __init__.py
│ │ └── create_asset.py
│ ├── 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 sidecarLayer responsibilities
| Layer | Responsibility | Imports allowed | Size guidance |
|---|---|---|---|
tools/ | 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 nested scripts
Because the SKILL.md scanner only auto-enumerates the top level of scripts/, every tool whose entry point lives under scripts/tools/ must declare an explicit source_file: in tools.yaml:
# tools.yaml
tools:
- name: create_asset
description: Create a new asset record on disk.
source_file: scripts/tools/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. Add a small sys.path shim at the top of each tool entry point so the imports work whether the script is run via the dcc-mcp-core subprocess executor, an in-process executor, or directly with python scripts/tools/create_asset.py:
from pathlib import Path
import sys
_SCRIPTS_DIR = Path(__file__).resolve().parent.parent
if str(_SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPTS_DIR))
from services.asset_service import AssetService # noqa: E402Tool adapter template
"""Tool entry point — create_asset (thin adapter)."""
from __future__ import annotations
import json, sys
from pathlib import Path
_SCRIPTS_DIR = Path(__file__).resolve().parent.parent
if str(_SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPTS_DIR))
from services.asset_service import AssetError, AssetService # noqa: E402
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
tools/— adapters must stay under ~30 lines and only translate between MCP envelopes and service calls. utils/doing I/O — anything inutils/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 withsuccess_result()/error_result(). - Auto-discovering nested scripts — only top-level
scripts/*.pyare enumerated; nested entry points must be declared viasource_file.
Checklist for a new layered skill
- [ ] Entry points live under
scripts/tools/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.yamlhas an explicitsource_file: - [ ] Each adapter installs the
sys.pathshim shown above - [ ]
metadata.dcc-mcp.toolsis set totools.yamlin nested 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:
---
name: maya-animation
metadata:
dcc-mcp:
depends: ["maya-geometry"]
---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"]Skill Directory Structure
Beyond the required SKILL.md and scripts/, the agentskills.io 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 namesreferences/ — 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.mdbody under 500 lines and 5000 tokens. Move detailed reference material toreferences/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) |
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 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:
# 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
---# 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]# 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.pySkillGroup 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:
- All tool declarations are registered in
ToolRegistrywith their group metadata set to the declared group name. - Tools in groups where
default_active=falseare hidden fromtools/list. They remain in the registry (visible vialist_actions()) and become active once the group is activated. 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
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__<skill>.<group> stubs for any group that is inactive, making the full tool surface discoverable without exposing schemas or handlers.
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) |
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:
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/mcpcreate_skill_server automatically:
- Creates an
ToolRegistryandToolDispatcher - Creates a
SkillCatalogwired to the dispatcher - Discovers skills from
DCC_MCP_MAYA_SKILL_PATHSandDCC_MCP_SKILL_PATHS - Returns a ready-to-start
McpHttpServer
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 8765 |
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) |
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 |
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.
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
5 core discovery tools (always present):
list_skills— List all skills with optional status filterget_skill_info— Get full metadata for a specific skillload_skill— Load a skill, registering its tools in ToolRegistryunload_skill— Unload a skill, removing its toolssearch_skills— Keyword search across name, description, search_hint, and tool_names
Loaded skill tools — Full
input_schemafrom theToolRegistryfor all currently loaded skillsUnloaded skill stubs —
__skill__<name>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 executesSkill Stub Behavior
Calling an unloaded skill stub (__skill__<name>) returns an error with a hint:
{
"error": "Skill 'maya-geometry' is not loaded. Call load_skill(skill_name=\"maya-geometry\") to register its tools."
}search_skills MCP Tool
{
"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, tags, dcc, and the sibling tools.yaml entries (name + description). 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) 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 |
description | 2.0 |
sibling tool names (from tools.yaml) | 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)
- Exact-name fast-path — if the query equals a skill's name (case-insensitive, after trimming), that skill sorts first unconditionally.
- Name-substring hit — skills whose
namecontains the query as a raw substring rank above equal-scoring skills that don't. - Scope precedence —
Admin > System > User > Repo. - 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), 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!.
Before (pre-0.15 legacy form — no longer accepted)
---
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)
---
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:
# 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 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).
# 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: false2. Shorthand — flat hint keys on the tool entry (backward compatibility).
tools:
- name: get_keyframes
read_only_hint: true
idempotent_hint: truePrecedence: 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:
tools:
- name: risky
read_only_hint: true # ignored (shorthand)
idempotent_hint: true # ignored (shorthand)
annotations:
destructive_hint: true # wins — only this hint is surfaceddeferred_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-successlist → attached after a successful tool callon-failurelist → attached after an error (isError == true)- No
next-toolsdeclared →_meta["dcc.next_tools"]is omitted entirely
# 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_logTool 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:
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.<feature>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 | (future) |
| 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.mdWhy the rule holds:
- Spec-conformance:
skills-ref validate(agentskills.io's reference validator) passes without a custom ruleset. - Progressive disclosure:
search_skillsreads 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:
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:
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.txtLoaded 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.
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-skills-creator Skill
The skills/dcc-skills-creator/ skill provides scaffolding and validation helpers as MCP tools:
# Scaffold a new skill directory via the loaded MCP tool:
# dcc_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_skills_creator__skill_template()Using the dcc-mcp-skill-developer Skill
The skills/dcc-mcp-skill-developer/ skill is an instruction-only companion for agents that are building or modernizing DCC-MCP adapter skills. 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.
It points agents to focused references for:
- Server and adapter composition patterns.
SKILL.md,tools.yaml, and script authoring checklists.- Host differences across Maya, Blender, 3ds Max, Houdini, bridge-based hosts, and custom studio tools.
- Unit, lint, gateway, E2E, and VRS validation choices.