# dcc-mcp-core

> Foundational library for the DCC Model Context Protocol (MCP) ecosystem. Rust-powered core (via PyO3) providing tool registry, structured results, event bus, skills/script registration, MCP protocol types, IPC transport, MCP Streamable HTTP server, process management, sandbox security, shared memory, screen capture, USD bridge, and telemetry for Digital Content Creation applications (Maya, Blender, Houdini, 3ds Max, etc.).

## Quick Decision Guide

| Task | Use this API |
|------|--------------|
| Return DCC tool result | `success_result()` / `error_result()` |
| Register scripts as MCP tools | `ToolRegistry.register()` |
| Discover skills from directories | `scan_and_load()` → returns `(skills, skipped)` tuple |
| Validate tool params | `ToolValidator.from_schema_json()` |
| Connect to running DCC | `connect_ipc(TransportAddress.default_local(...))` |
| Define MCP tool for LLM | `ToolDefinition` + `ToolAnnotations` |
| Monitor DCC process | `PyProcessWatcher` + `PyCrashRecoveryPolicy` |
| Sandbox AI actions | `SandboxPolicy` + `SandboxContext` |
| Share large data zero-copy | `PySharedSceneBuffer.write()` with LZ4 compression |
| Expose DCC tools over HTTP/MCP | `create_skill_server("maya", McpHttpConfig(port=8765))` — Skills-First setup; falls back to `McpHttpServer(registry, config)` for manual registry wiring |
| Build a DCC adapter | `DccServerBase(dcc_name, builtin_skills_dir)` — skill/lifecycle/gateway/hot-reload inherited |
| Write skill scripts | `skill_entry` + `skill_success` / `skill_error` — zero-boilerplate skill authoring |
| Control skill trust level | `SkillScope` (Repo < User < System < Admin) — higher scope shadows lower |
| Progressive tool exposure | `SkillGroup` with `default_active` + `activate_tool_group()` |

## Quick Start

```python
import dcc_mcp_core

# Tool registry
reg = dcc_mcp_core.ToolRegistry()
reg.register(name="create_sphere", description="Create a sphere", dcc="maya")
meta = reg.get_action("create_sphere")

# Structured results (always use factories, never raw dicts)
result = dcc_mcp_core.success_result("Created sphere", prompt="Add materials next", count=1)
error = dcc_mcp_core.error_result("Failed", "File not found")

# Event bus
bus = dcc_mcp_core.EventBus()
bus.subscribe("evt", lambda **kw: print(kw))
bus.publish("evt", x=1)

# Skill scanning + loading
# IMPORTANT: scan_and_load returns (List[SkillMetadata], List[str] skipped_dirs)
skills, skipped = dcc_mcp_core.scan_and_load(dcc_name="maya")
skills, skipped = dcc_mcp_core.scan_and_load_lenient(dcc_name="maya")
for s in skills:
    print(f"{s.name}: {len(s.scripts)} scripts")

# Low-level scan
scanner = dcc_mcp_core.SkillScanner()
dirs = scanner.scan(extra_paths=["/path/to/skills"], dcc_name="maya")
meta = dcc_mcp_core.parse_skill_md(dirs[0])  # -> SkillMetadata or None
```



## Installation

- PyPI: `pip install dcc-mcp-core`
- Build from source: `maturin develop --features python-bindings,ext-module`
- Python: >=3.7 (CI tests 3.7–3.13; abi3-py38 wheel for 3.8+)
- Build: maturin (Rust 1.85+ + PyO3)
- Version: current
- License: MIT

## Architecture

Rust workspace (14 crates) with PyO3 bindings. All logic in Rust sub-crates; Python gets a single `dcc_mcp_core._core` extension module. The `dcc_mcp_core` package re-exports all ~154 public symbols from `_core` plus pure-Python helpers (DccServerBase, DccGatewayElection, DccSkillHotReloader, factory, skill helpers).

```
crates/
├── dcc-mcp-models/      # ToolResult, SkillMetadata
├── dcc-mcp-actions/     # ToolRegistry, ToolDispatcher, ToolValidator, EventBus, ToolPipeline
├── dcc-mcp-skills/      # SkillScanner, SkillWatcher, parse_skill_md, dependency resolver
├── dcc-mcp-protocols/   # MCP type definitions (Tool, Resource, Prompt, DccAdapter)
├── dcc-mcp-transport/   # IPC transport, ConnectionPool, CircuitBreaker, FramedChannel
├── dcc-mcp-process/     # PyDccLauncher, ProcessMonitor, ProcessWatcher, CrashRecovery
├── dcc-mcp-telemetry/   # TelemetryConfig, RecordingGuard, ToolMetrics, ToolRecorder
├── dcc-mcp-sandbox/     # SandboxPolicy, InputValidator, AuditLog
├── dcc-mcp-shm/         # PyBufferPool, PySharedBuffer, LZ4 compression
├── dcc-mcp-capture/     # Capturer, CaptureFrame, CaptureResult
├── dcc-mcp-usd/         # UsdStage, UsdPrim, SdfPath, scene info bridge
├── dcc-mcp-http/        # McpHttpServer, McpHttpConfig, McpServerHandle (MCP Streamable HTTP 2025-03-26)
├── dcc-mcp-server/      # Binary entry point, gateway runner
└── dcc-mcp-utils/       # Filesystem, constants, type wrappers, JSON conversion
```

## Core Concepts

- **ToolRegistry**: Thread-safe registry for action metadata (name, description, dcc, tags, schemas, version)
- **ToolResult**: Structured result (success, message, prompt, error, context dict) — AI-friendly with follow-up hints
- **EventBus**: Thread-safe publish/subscribe system with per-event subscriber lists
- **SkillScanner**: Discovers SKILL.md files in directories with mtime-based caching
- **SkillMetadata**: Parsed from SKILL.md YAML frontmatter (name, dcc, tags, scripts, depends, version)
- **MCP Protocol Types**: ToolDefinition, ResourceDefinition, PromptDefinition, ToolAnnotations
- **ToolDispatcher**: Typed dispatch with validation via ToolValidator
- **VersionedRegistry**: SemVer-aware tool registry with constraint-based lookup

## Key APIs

### Top-level exports (`dcc_mcp_core`)

**Actions:**
- `ToolRegistry` — Thread-safe registry; `.register(name, ...)`, `.register_batch([{...}, ...])`, `.unregister(name, dcc_name=None)`, `.get_action(name)`, `.list_actions()`, `.list_actions_for_dcc(dcc)`, `.get_all_dccs()`, `.search_actions(category, tags, dcc_name)`, `.get_categories()`, `.get_tags()`, `.reset()`
- `ToolDispatcher` — Validated dispatch; `ToolDispatcher(registry)` (ONE arg); `.dispatch(name, json_str) -> dict`; dict keys: `"action"`, `"output"`, `"validation_skipped"`
- `ToolValidator` — Input validation before action execution
- `ToolPipeline(dispatcher)` — Middleware-style processing pipeline; `.add_logging(log_params=False)`, `.add_timing() -> TimingMiddleware`, `.add_audit(record_params=True) -> AuditMiddleware`, `.add_rate_limit(max_calls, window_ms) -> RateLimitMiddleware`, `.add_callable(before_fn=None, after_fn=None)`, `.dispatch(action_name, params_json="null") -> dict`, `.register_handler(name, fn)`, `.middleware_count()`, `.middleware_names()`, `.handler_count()`
- `LoggingMiddleware(log_params=False)` — emits tracing log lines before/after each action
- `TimingMiddleware()` — measures per-tool latency; `.last_elapsed_ms(action) -> int|None`
- `AuditMiddleware(record_params=True)` — in-memory audit log; `.records()`, `.records_for_action(action)`, `.record_count()`, `.clear()`
- `RateLimitMiddleware(max_calls, window_ms)` — fixed-window rate limiter; `.call_count(action)`, `.max_calls`, `.window_ms`
- `ToolMetrics` — Performance and execution metrics collection
- `ToolRecorder` — Record/replay tool executions
- `EventBus` — `.subscribe(event, callback) -> id`, `.unsubscribe(event, id)`, `.publish(event, **kwargs)`
- `VersionedRegistry` — SemVer-aware registry; `.register_versioned(name, dcc, version)`, `.resolve(name, dcc, constraint) -> Optional[dict]`, `.resolve_all(name, dcc, constraint) -> List[dict]`, `.latest_version(name, dcc) -> Optional[str]`, `.versions(name, dcc) -> List[str]`, `.remove(name, dcc, constraint) -> int`, `.keys() -> List[Tuple[str, str]]`, `.total_entries() -> int`, `.router() -> CompatibilityRouter`
- `CompatibilityRouter` — returned by `VersionedRegistry.router()`; resolves the best-matching tool version given a client-side version constraint. **Not yet exported as a standalone Python class** — access via `registry.router()`
- `SemVer(major, minor, patch)` — Semantic version value object
- `VersionConstraint.parse(">=1.0.0")` — Version constraint expression

**Result Factories:**
- `success_result(message, prompt=None, **context) -> ToolResult`
- `error_result(message, error, prompt=None, possible_solutions=None, **context) -> ToolResult`
- `from_exception(error_message, message=None, include_traceback=False, ...) -> ToolResult`
- `validate_action_result(result) -> ToolResult` — normalize dict/str/None → ToolResult

**ToolResult fields:** `.success`, `.message`, `.prompt`, `.error`, `.context`, `.to_dict()`, `.with_error(err)`, `.with_context(**kw)`

**Skills:**
- `SkillScanner` — `.scan(extra_paths=None, dcc_name=None, force_refresh=False) -> List[str]`
- `SkillWatcher` — File-watching auto-reload; `.watch(path)`, `.skills() -> List[SkillMetadata]`
- `parse_skill_md(skill_dir) -> Optional[SkillMetadata]`
- `scan_skill_paths(extra_paths=None, dcc_name=None) -> List[str]`
- `scan_and_load(dcc_name=None, extra_paths=None) -> tuple[List[SkillMetadata], List[str]]` — **(skills, skipped_dirs)**
- `scan_and_load_lenient(dcc_name=None, extra_paths=None) -> tuple[List[SkillMetadata], List[str]]` — same, skips errors
- `resolve_dependencies(skills) -> List[SkillMetadata]`
- `expand_transitive_dependencies(skills, skill_name) -> List[str]`
- `validate_dependencies(skills) -> List[str]` — returns error messages
- `get_skill_paths_from_env() -> List[str]` — reads `DCC_MCP_SKILL_PATHS` env var
- `get_app_skill_paths_from_env(app_name: str) -> List[str]` — reads `DCC_MCP_{APP}_SKILL_PATHS` env var

**SkillCatalog** — progressive skill loading with thread-safe state:
- `SkillCatalog(scanner)` — construct with a `SkillScanner`
- `.discover(extra_paths=None, dcc_name=None)` — scan and populate catalog
- `.list_skills(status=None) -> List[SkillSummary]` — filter by `"loaded"` / `"unloaded"` / `None`
- `.find_skills(query=None, tags=None, dcc=None) -> List[SkillSummary]` — matches name, description, search_hint, tool names
- `.load_skill(skill_name) -> bool` / `.unload_skill(skill_name) -> bool`
- `.get_skill_info(skill_name) -> Optional[SkillMetadata]`
- `.is_loaded(skill_name) -> bool` / `.loaded_count() -> int`
- `.active_groups(skill_name) -> List[str]`, `.activate_group(skill, group) -> bool`, `.deactivate_group(skill, group) -> bool`
- `.list_groups(skill_name) -> List[SkillGroup]`, `.list_tools_catalog(skill_name) -> dict[str, list[str]]`

**SkillSummary fields:** `.name`, `.description`, `.search_hint`, `.version`, `.dcc`, `.tags`, `.tool_count`, `.tool_names`, `.loaded`

**SkillMetadata fields:** `.name`, `.description`, `.search_hint`, `.dcc`, `.version`, `.tags`, `.tools`, `.scripts` (List[str] absolute paths), `.skill_path`, `.depends`, `.metadata_files`, `.groups` (List[SkillGroup]), `.license` (str), `.compatibility` (str), `.allowed_tools` (List[str]), `.external_deps` (str|None — JSON string of external dependency declarations; set via `md.external_deps = json.dumps(deps)`, read via `json.loads(md.external_deps)`)

**SkillGroup fields:** `.name`, `.description`, `.default_active` (bool), `.tools` (List[str]) — declared in SKILL.md `groups:` frontmatter for progressive tool exposure

**Tool groups (progressive exposure):**
- `ToolRegistry.activate_tool_group(skill, group) -> int`, `.deactivate_tool_group(skill, group) -> int` — enable/disable tools in a group; emits `notifications/tools/list_changed`
- `ToolRegistry.list_tools_in_group(skill, group) -> List[dict]`, `.list_actions_enabled() -> List[dict]`, `.set_tool_enabled(name, enabled) -> bool`
- **Note**: `ActionMeta` is a Rust-internal type not accessible from Python. Use the `ToolRegistry` methods above to control tool visibility.
- MCP core tools: `activate_tool_group`, `deactivate_tool_group`, `search_tools` (registered alongside the six skill-discovery tools; `tools/list` also emits `__group__<skill>.<group>` stubs for inactive groups)

**Action naming**: `{skill_name}__{script_stem}` — hyphens in skill names replaced by underscores

**`next-tools` (dcc-mcp-core extension):** Declared per-tool in SKILL.md frontmatter to guide AI agents to follow-up tools:
```yaml
tools:
  - name: create_sphere
    next-tools:
      on-success: [maya_geometry__bevel_edges]
      on-failure: [dcc_diagnostics__screenshot]
```
- `on-success`: suggested tools after successful execution
- `on-failure`: debugging/recovery tools on failure
- Both accept lists of fully-qualified tool names (`skill_name__tool_name` format)
- This is a dcc-mcp-core extension, not part of the agentskills.io specification

### MCP Protocol Types (`dcc_mcp_core`)

- `ToolDefinition(name, description, input_schema, output_schema=None, annotations=None)`
- `ToolAnnotations(title=None, read_only_hint=None, destructive_hint=None, idempotent_hint=None, open_world_hint=None)`
- `ResourceDefinition(uri, name, description, mime_type="text/plain")`
- `ResourceTemplateDefinition(uri_template, name, description, mime_type="text/plain")`
- `PromptDefinition(name, description, arguments=None)`
- `PromptArgument(name, description, required=False)`
- `DccInfo`, `DccCapabilities`, `DccError`, `DccErrorCode`

### Transport (`dcc_mcp_core`)

- `IpcListener.bind(address) -> IpcListener` → `.local_address()`, `.accept(timeout_ms=None) -> FramedChannel` (server-side), `.into_handle() -> ListenerHandle`
- `connect_ipc(address, timeout_ms=10000) -> FramedChannel` (client-side)
- `FramedChannel` methods:
  - `.call(method, params=None, timeout_ms=30000) -> dict` — **primary RPC helper** (v0.12.7+); atomically sends Request + waits for correlated Response; result: `{"id", "success", "payload", "error"}`
  - `.send_request(method, params=None) -> req_id` — async variant; use with `.recv()` for multiplexed patterns
  - `.recv(timeout_ms=None) -> dict|None` — returns next Request/Response/Notify envelope
  - `.send_response(request_id, success, payload=None, error=None)` — server-side reply
  - `.send_notify(topic, data=None)` — one-way event push
  - `.ping(timeout_ms=5000) -> int` — heartbeat RTT in ms
  - `.shutdown()` — graceful close
- `TransportManager(registry_dir, ...)` — connection pool + circuit breaker + service discovery
  - `.bind_and_register(dcc_type, version=None) -> (instance_id, IpcListener)` — one-call server setup
  - `.find_best_service(dcc_type) -> ServiceEntry` — smart routing (IPC > local TCP > remote)
  - `.rank_services(dcc_type) -> List[ServiceEntry]` — all live instances sorted by preference
- `TransportAddress`, `TransportScheme`, `RoutingStrategy`
- Frame encode/decode: `encode_request(method, params)`, `encode_response(id, success, payload, error)`, `encode_notify(topic, data)`, `decode_envelope(data)` — low-level MessagePack framing

### MCP HTTP Server (`dcc_mcp_core`)

- `McpHttpConfig(port=8765, server_name=None, server_version=None, enable_cors=False, request_timeout_ms=30000)` — server configuration
- `McpHttpServer(registry, config=None)` — MCP Streamable HTTP server (**2025-03-26 spec**), axum/Tokio backend
  - `.start() -> McpServerHandle` — starts background thread, returns immediately
  - **Note**: MCP 2025-03-26 implements Streamable HTTP, Tool Annotations, OAuth 2.1. MCP 2025-06-18 adds Structured Tool Output, Elicitation, Resource Links, and removes JSON-RPC batching. MCP 2025-11-25 adds icon metadata, Tasks, Sampling with tools. Do NOT implement manually — wait for library support.
- `McpServerHandle` (alias for `McpServerHandle`) — handle to running server
  - `.mcp_url() -> str` — full MCP endpoint URL (e.g. `"http://127.0.0.1:8765/mcp"`)
  - `.port -> int`, `.bind_addr -> str`
  - `.shutdown()` — blocks until stopped; `.signal_shutdown()` — non-blocking

### Process Management (`dcc_mcp_core`)

- `PyDccLauncher()` → `.launch(name, executable, args=None, launch_timeout_ms=30000) -> dict` (dict: `pid`, `name`, `status`), `.terminate(name, timeout_ms=5000)`, `.kill(name)`, `.pid_of(name) -> int|None`, `.running_count() -> int`
- `PyProcessMonitor()` → `.track(pid, name)`, `.refresh()`, `.query(pid) -> dict|None`, `.list_all() -> list`, `.is_alive(pid) -> bool`
- `PyProcessWatcher(poll_interval_ms=500)` → `.track(pid, name)`, `.start()`, `.stop()`, `.poll_events() -> list[dict]`
- `PyCrashRecoveryPolicy(max_restarts=3)` → `.use_exponential_backoff(initial_ms, max_delay_ms)`, `.use_fixed_backoff(delay_ms)`, `.should_restart(status) -> bool`, `.next_delay_ms(name, attempt) -> int`
- `ScriptResult`, `ScriptLanguage`

### Sandbox (`dcc_mcp_core`)

- `SandboxPolicy()` → `.allow_actions(list)`, `.deny_actions(list)`, `.allow_paths(list)`, `.set_timeout_ms(ms)`, `.set_max_actions(count)`, `.set_read_only(bool)`, `.is_read_only -> bool`
- `SandboxContext(policy)` → `.set_actor(actor)`, `.execute_json(action, params_json) -> str`, `.action_count -> int`, `.audit_log -> AuditLog`, `.is_allowed(action) -> bool`, `.is_path_allowed(path) -> bool`
- `InputValidator()` → `.require_string(field, max_length=None, min_length=None)`, `.require_number(field, min_value=None, max_value=None)`, `.forbid_substrings(field, substrings)`, `.validate(params_json) -> (bool, str|None)`
- `AuditLog` → `.entries() -> list[AuditEntry]`, `.successes()`, `.denials()`, `.entries_for_action(action)`, `.to_json() -> str`
- `AuditEntry` → `.timestamp_ms`, `.actor`, `.action`, `.params_json`, `.duration_ms`, `.outcome`, `.outcome_detail`

### Telemetry (`dcc_mcp_core`)

- `TelemetryConfig`, `RecordingGuard`, `ToolMetrics`, `ToolRecorder`
- `is_telemetry_initialized() -> bool`, `shutdown_telemetry()`

### Shared Memory (`dcc_mcp_core`)

- `PyBufferPool`, `PySharedBuffer`, `PySharedSceneBuffer`, `PySceneDataKind`

### Capture (`dcc_mcp_core`)

- `Capturer` — `.new_auto()` (display), `.new_window_auto()` (single window), `.new_mock(width, height)`
- `Capturer.capture(format, jpeg_quality, scale, timeout_ms, process_id, window_title) -> CaptureFrame`
- `Capturer.capture_window(*, process_id=None, window_handle=None, window_title=None, format="png", jpeg_quality=85, scale=1.0, timeout_ms=5000, include_decorations=True) -> CaptureFrame`
- `Capturer.backend_name() -> str`, `.backend_kind() -> CaptureBackendKind`, `.stats() -> tuple[int, int, int]`
- `CaptureFrame` — `.width`, `.height`, `.data`, `.format`, `.mime_type`, `.timestamp_ms`, `.dpi_scale`, `.window_rect` (tuple or None), `.window_title` (str or None), `.byte_len()`
- `CaptureTarget` — factories: `.primary_display()`, `.monitor_index(i)`, `.process_id(pid)`, `.window_title(title)`, `.window_handle(handle)`
- `CaptureBackendKind` — enum: `DxgiDesktopDuplication`, `ScreenCaptureKit`, `X11Xshm`, `PipeWire`, `HwndPrintWindow`, `Mock`
- `WindowFinder()` — `.find(target) -> Optional[WindowInfo]`, `.enumerate() -> List[WindowInfo]`
- `WindowInfo` — `.handle`, `.pid`, `.title`, `.rect`

### Instance-Bound Diagnostics (`dcc_mcp_core`)

- `DccServerBase(dcc_name, builtin_skills_dir, *, dcc_pid=None, dcc_window_title=None, dcc_window_handle=None, resolver=None)` — bind an adapter to a specific DCC instance; the four `diagnostics__*` tools target this DCC only
- `register_diagnostic_mcp_tools(server, *, dcc_name, dcc_pid=None, dcc_window_handle=None, dcc_window_title=None, resolver=None)` — register `diagnostics__screenshot`, `diagnostics__audit_log`, `diagnostics__tool_metrics`, `diagnostics__process_status` on an `McpHttpServer` before `.start()`
- IPC handlers (renamed in 0.14.0, no compat aliases): `get_audit_log`, `get_tool_metrics` (was `get_action_metrics`), `dispatch_tool` (was `dispatch_action`), `take_screenshot`
- `register_diagnostic_handlers(...)` — the IPC-path equivalent (used by `DccServerBase.start()`)
- Bundled `dcc-diagnostics` skill's `screenshot.py` tries `DCC_MCP_OWNER_IPC` → `channel.call("take_screenshot")` first and falls back to `Capturer.new_auto()` when no owner IPC is set

### USD (`dcc_mcp_core`)

- `UsdStage`, `UsdPrim`, `SdfPath`, `VtValue`, `SceneInfo`, `SceneStatistics`
- `scene_info_json_to_stage(json_str) -> UsdStage`
- `stage_to_scene_info_json(stage) -> str`
- `mpu_to_units(value)`, `units_to_mpu(value)`

### Type Wrappers (`dcc_mcp_core`)

- `wrap_value(v)` / `unwrap_value(v)` — RPyC-safe type wrappers
- `unwrap_parameters(params_dict)` — unwrap all wrapper values in a dict
- `BooleanWrapper`, `FloatWrapper`, `IntWrapper`, `StringWrapper`

### Bridge System (`dcc_mcp_core`)

- `BridgeRegistry` — manages named protocol bridges (RPyC ↔ MCP, HTTP ↔ IPC)
- `BridgeContext` — bridge context with name, description, metadata
- `register_bridge(name, ctx)` — register a named bridge
- `get_bridge_context(name) -> Optional[BridgeContext]` — retrieve bridge by name

### WebView System (`dcc_mcp_core`)

- `WebViewAdapter` — Python-only: embed browser panels in DCC applications
- `WebViewContext` — Python-only: webview context with capabilities and configuration
- `CAPABILITY_KEYS` — Python-only: available capability key constants
- `WEBVIEW_DEFAULT_CAPABILITIES` — Python-only: default capability set
- Note: These are pure-Python helpers, not in `_core.pyi`

### Skill Scopes & Policies (`dcc_mcp_core`)

- `SkillScope` — enum: `Repo`, `User`, `System`, `Admin` (ascending trust; higher shadows lower). **Rust-only type — not importable from Python.** Configure via SKILL.md frontmatter and access via `SkillMetadata` methods.
  - `Repo` — project-local (e.g. `./<project>/.dcc_skills/`)
  - `User` — user-level (e.g. `~/.dcc_mcp/skills/`)
  - `System` — system-wide (e.g. `/opt/dcc_mcp/skills/`)
  - `Admin` — enterprise-managed (elevated privilege)
- `SkillPolicy` — declared in SKILL.md frontmatter. **Rust-only type — not importable from Python.** Access via `SkillMetadata`:
  - `allow_implicit_invocation: bool` (default `True`) — when `False`, skill must be explicitly `load_skill()`'d
  - `products: list[str]` — DCC type whitelist (case-insensitive)
  - `SkillMetadata.is_implicit_invocation_allowed()` — check policy
  - `SkillMetadata.matches_product(dcc_name)` — check product filter

### Scene Data Model (`dcc_mcp_core`)

- `BoundingBox` — axis-aligned bounding box (min/max corner vectors)
- `FrameRange` — animation frame range (start, end, step)
- `ObjectTransform` — 4x4 transform + decomposition (translate, rotate, scale)
- `SceneNode` — hierarchical scene node (path, transform, children)
- `SceneObject` — full scene object (mesh, material, transform references)
- `RenderOutput` — render result metadata (path, format, resolution)

### Serialization (`dcc_mcp_core`)

- `SerializeFormat` — enum: JSON, MESSAGEPACK
- `serialize_result(result, fmt) -> bytes` — ToolResult → transport-safe bytes
- `deserialize_result(data, fmt) -> ToolResult` — bytes → ToolResult

### Utility Functions (`dcc_mcp_core`)

- `get_config_dir()`, `get_data_dir()`, `get_log_dir()`, `get_platform_dir(dir_type)`
- `get_tools_dir(dcc_name)`, `get_skills_dir(dcc_name=None)`
- `get_skill_paths_from_env() -> List[str]` — reads `DCC_MCP_SKILL_PATHS`

### Constants (`dcc_mcp_core`)

- `APP_NAME = "dcc-mcp"`, `APP_AUTHOR = "dcc-mcp"`
- `DEFAULT_DCC = "python"`, `DEFAULT_LOG_LEVEL = "DEBUG"`, `DEFAULT_MIME_TYPE = "text/plain"`, `DEFAULT_VERSION = "1.0.0"`
- `SKILL_METADATA_FILE = "SKILL.md"`, `SKILL_SCRIPTS_DIR = "scripts"`, `SKILL_METADATA_DIR = "metadata"`
- `ENV_SKILL_PATHS = "DCC_MCP_SKILL_PATHS"`, `ENV_LOG_LEVEL = "MCP_LOG_LEVEL"`

## Skills System

Register scripts as MCP tools with zero Python code via SKILL.md. Follows [agentskills.io](https://agentskills.io/specification) specification for SKILL.md format.

### Directory Structure

```
maya-geometry/
├── SKILL.md          # YAML frontmatter + description
├── scripts/
│   ├── create_sphere.py
│   └── batch_rename.mel
├── references/       # Optional: supplementary docs (agentskills.io standard)
│   └── REFERENCE.md
├── assets/           # Optional: templates, images, data files
└── metadata/         # Optional: dependency declarations
    └── depends.md    # YAML list of dependency skill names
```

### SKILL.md Format

Follows [agentskills.io](https://agentskills.io/specification) specification.

```yaml
---
name: maya-geometry
description: "Maya geometry creation tools"
allowed-tools: Bash Read  # space-separated (agentskills.io spec), NOT a YAML list
tags: ["maya", "geometry"]
dcc: maya
version: "1.0.0"
depends: ["other-skill"]  # optional
license: MIT              # optional (agentskills.io spec)
compatibility: "Maya 2024+" # optional (agentskills.io spec) — environment requirements
metadata:                 # optional (agentskills.io spec)
  author: example-org
  version: "1.0"
search-hint: "polygon modeling, bevel, extrude"  # dcc-mcp-core extension
---
# Markdown description body (keep < 500 lines, < 5000 tokens recommended)
```

**agentskills.io standard fields** (V1.0 spec, 2025-12-18): `name` (required), `description` (required), `license`, `compatibility`, `metadata`, `allowed-tools` (experimental — space-separated pre-approved tool strings like `Bash(git:*) Read`).

**dcc-mcp-core extensions**: `dcc`, `tags`, `search-hint`, `tools`, `groups`, `depends`, `external_deps`, `next-tools`.

### Supported Script Types

| Extension | Type |
|-----------|------|
| `.py` | Python |
| `.mel` | MEL (Maya) |
| `.ms` | MaxScript |
| `.bat`, `.cmd` | Batch |
| `.sh`, `.bash` | Shell |
| `.ps1` | PowerShell |
| `.js`, `.jsx` | JavaScript |

### Environment Variable

```bash
export DCC_MCP_SKILL_PATHS="/path/to/skills1:/path/to/skills2"
# Windows: set DCC_MCP_SKILL_PATHS=C:\skills1;C:\skills2
```

## Documentation

- [README (English)](https://github.com/loonghao/dcc-mcp-core/blob/main/README.md)
- [README (中文)](https://github.com/loonghao/dcc-mcp-core/blob/main/README_zh.md)
- [AI Agent Guide (AGENTS.md)](https://github.com/loonghao/dcc-mcp-core/blob/main/AGENTS.md)
- [Claude Guide (CLAUDE.md)](https://github.com/loonghao/dcc-mcp-core/blob/main/CLAUDE.md)
- [Gemini Guide (GEMINI.md)](https://github.com/loonghao/dcc-mcp-core/blob/main/GEMINI.md)
- [Contributing Guide](https://github.com/loonghao/dcc-mcp-core/blob/main/CONTRIBUTING.md)
- [Changelog](https://github.com/loonghao/dcc-mcp-core/blob/main/CHANGELOG.md)
- [Full API Reference](https://github.com/loonghao/dcc-mcp-core/blob/main/llms-full.txt)
- [VitePress Docs](https://loonghao.github.io/dcc-mcp-core/)

## Development

- Tool manager: [vx](https://github.com/loonghao/vx)
- Build: maturin (Rust + PyO3), `vx just dev` or `vx just install`
- Test: `vx just test-rust` (Rust), `vx just test` (Python)
- Lint: `vx just lint` (clippy + ruff + isort), fix: `vx just lint-fix`
- Pre-flight: `vx just preflight`
- Release: [Release Please](https://github.com/googleapis/release-please) with Conventional Commits

## Related Projects

- [dcc-mcp-rpyc](https://github.com/loonghao/dcc-mcp-rpyc) — RPyC bridge for remote DCC operations
- [dcc-mcp-maya](https://github.com/loonghao/dcc-mcp-maya) — Maya MCP server implementation
