# 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 | `IpcChannelAdapter.connect(name)` or `SocketServerAdapter(path)` |
| 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()` |
| Remote-accessible MCP server (cloud agents) | `create_skill_server("maya", McpHttpConfig(host="0.0.0.0", port=8765, enable_cors=True))` — bind `0.0.0.0`, CORS on, Bearer auth via `cfg.api_key` |
| Bearer-token / OAuth auth | `ApiKeyConfig`, `OAuthConfig`, `CimdDocument`, `validate_bearer_token(headers, expected_token=...)`, `generate_api_key()` |
| Batch multiple tool calls server-side (issue #406) | `batch_dispatch(dispatcher, calls, aggregate="merge")` — returns `{total, succeeded, errors, merged}` |
| Sandboxed script orchestration (issue #406) | `EvalContext(dispatcher, sandbox=True).run("...")` — script uses `dispatch(name, args)` |
| Thin 2-tool surface for huge DCC APIs (issue #411) | `DccApiExecutor("maya", catalog, dispatcher)` + `register_dcc_api_executor(server, executor)` → `dcc_search`, `dcc_execute` |
| Mid-call user input (issue #407) | `await elicit_form(message, schema)` / `elicit_form_sync(...)` / `await elicit_url(message, url)` — returns `ElicitationResponse` |
| Inline chart / table / image result (MCP Apps, issue #409) | `skill_success_with_chart(msg, spec)` / `skill_success_with_table(msg, headers, rows)` / `skill_success_with_image(msg, image_data=...)` |
| Run skill scripts inside embedded DCC (no subprocess) | `SkillCatalog.set_in_process_executor(callable)` on the server's catalog — callable receives `(script_path, params) -> dict` |
| Claude Code one-click plugin bundle (issue #410) | `build_plugin_manifest(dcc_name, mcp_url, skill_paths, api_key=...)` + `export_plugin_manifest(manifest, path)` or `server.plugin_manifest(version=...)` |
| Cooperative cancellation in skill scripts | `check_cancelled()` — raises `CancelledError` when the active request was cancelled; dispatcher sets `CancelToken` via `set_cancel_token()` |
| Checkpoint/resume long-running tool executions (issue #436) | `save_checkpoint(job_id, state)` / `get_checkpoint(job_id)` / `checkpoint_every(n, job_id, state_fn)` — persist progress at intervals so interrupted jobs resume from last checkpoint |
| Agent-facing docs:// MCP resources (issue #435) | `register_docs_server(server)` — serves `docs://output-format/*` and `docs://skill-authoring/*` resources; agents fetch only specs they need |
| Agent feedback / rationale (issues #433, #434) | `register_feedback_tool(server)` / `extract_rationale(params)` / `make_rationale_meta(text)` — `dcc_feedback__report` tool + `_meta.dcc.rationale` extraction |
| Runtime DCC namespace introspection (issue #426) | `register_introspect_tools(server)` — `dcc_introspect__list_module`, `dcc_introspect__signature`, `dcc_introspect__search`, `dcc_introspect__eval` |
| Skill recipe anchors (issue #428) | `register_recipes_tools(server, skills=...)` — `recipes__list` and `recipes__get` tools for thin-harness skill recipe lookup |
| YAML declarative workflows (issue #439) | `WorkflowYaml` + `load_workflow_yaml(path)` + `register_workflow_yaml_tools(server)` — task vs step semantics for multi-step DCC workflows |
| WebSocket bridge for non-Python DCCs | `DccBridge(host, port)` — WebSocket JSON-RPC 2.0 bridge; `.call(method, **params)` for synchronous RPC to DCC plugin |
| Gateway failover election | `DccGatewayElection(dcc_name, server)` — automatic gateway failover via first-wins socket election |
| Skill hot-reload without server restart | `DccSkillHotReloader(dcc_name, server)` — monitors skill directories and auto-reloads on change |
| Singleton DCC server factory | `create_dcc_server(instance_holder, lock, server_class, ...)` / `make_start_stop(ServerClass)` — zero-boilerplate singleton server pattern |
| Skill validation | `validate_skill(skill_dir)` → `SkillValidationReport` with `SkillValidationIssue` list |
| Rust-powered JSON/YAML | `json_dumps(obj)` / `json_loads(s)` / `yaml_dumps(obj)` / `yaml_loads(s)` — zero-dependency serialization |

## 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 (15 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 ~275 public symbols from `_core` plus pure-Python helpers (DccServerBase, DccGatewayElection, DccSkillHotReloader, factory, skill helpers).

```
crates/
├── dcc-mcp-naming/      # SEP-986 tool-name / action-id validators (TOOL_NAME_RE, validate_tool_name)
├── 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 (ipckit), DccLinkFrame, IpcChannelAdapter, SocketServerAdapter
├── 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
```

Pure-Python modules: cancellation, checkpoint, docs_resources, feedback, introspect, recipes, workflow_yaml, bridge, gateway_election, hotreload, factory, dcc_server, server_base, adapters, auth, batch, elicitation, rich_content, plugin_manifest, dcc_api_executor, skill, workflow_yaml

## Core Concepts

- **ToolRegistry**: Thread-safe registry for tool 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)`, `.count_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"`
- **Async `tools/call` (#318)** — over the wire, a `tools/call` opts into async dispatch when ANY of: `_meta.dcc.async = true`, `_meta.progressToken` is set, or the tool's `ActionMeta` declares `execution: async` / `timeout_hint_secs > 0`. Response returns immediately with `CallToolResult.structuredContent = {"job_id", "status": "pending", "parent_job_id"}`. Poll status via `jobs.get_status` (#319). Parent-job cascade: `_meta.dcc.parentJobId` wires the new job's `CancellationToken` as a child of the parent's — cancelling the parent cancels every descendant
- `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()`, `.to_json()`, `.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(registry)` — construct with a `ToolRegistry`
- `.set_in_process_executor(executor)` — register an in-process callable so skill scripts run inside the host DCC's Python interpreter instead of spawning `DCC_MCP_PYTHON_EXECUTABLE` subprocesses. Callable signature: `def executor(script_path: str, params: dict) -> dict`. Pass `None` to revert to subprocess mode. Use inside embedded DCC adapters (Maya, Blender, Houdini) where DCC APIs are available in-process.
- `.discover(extra_paths=None, dcc_name=None)` — scan and populate catalog
- `.list_skills(status=None) -> List[SkillSummary]` — filter by `"loaded"` / `"unloaded"` / `None`
- `.search_skills(query=None, tags=None, dcc=None, scope=None, limit=None) -> List[SkillSummary]` — unified discovery: matches name, description, search_hint, tool names; scope filters by trust level; empty call browses by scope precedence
- `.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)`)
- **SKILL.md description quality**: The `description` field (1-1024 chars) should describe **what the skill does AND when to use it** — include specific keywords so AI agents can match tasks. Bad: "Helps with geometry." Good: "Creates and modifies polygon geometry in Maya. Use when user asks to create spheres, cubes, bevel edges, or extrude faces."
- **Progressive disclosure**: Keep `SKILL.md` body < 500 lines / < 5000 tokens; move details to `references/` (loaded on demand by agents)

**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. SEP-986 dot-namespacing (`skill.tool_name`) also supported — see `docs/guide/naming.md` for validation rules (`validate_tool_name`, `validate_action_id`)

**`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

**AI Agent Tool Priority**: When interacting with DCCs, prefer: (1) Skill Discovery (`search_skills` → `load_skill`), (2) Skill-based tools (validated schemas + `next-tools` + `ToolAnnotations` safety hints), (3) Diagnostics tools (`diagnostics__screenshot` etc.), (4) Direct registry access (last resort). Skills-first provides safety, discoverability, chainability, progressive exposure, and input validation.

### 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, deferred_hint=None)`
- `ResourceAnnotations(audience=None, priority=None)`
- `ResourceDefinition(uri, name, description, mime_type="text/plain", annotations=None)`
- `ResourceTemplateDefinition(uri_template, name, description, mime_type="text/plain", annotations=None)`
- `PromptDefinition(name, description, arguments=None)`
- `PromptArgument(name, description, required=False)`
- `DccInfo`, `DccCapabilities`, `DccError`, `DccErrorCode`

### Transport (`dcc_mcp_core`)

**DccLinkFrame** — framed message for IPC transport:
- `DccLinkFrame(msg_type, seq, body)` — construct a frame
  - `.msg_type` — message type (int): 1=Call, 2=Reply, 3=Err, 4=Progress, 5=Cancel, 6=Push, 7=Ping, 8=Pong
  - `.seq` — sequence number (u32)
  - `.body` — payload bytes
  - `.encode() -> bytes` — serialize frame to wire format
  - `.decode(data) -> DccLinkFrame` — deserialize from wire format (static)

**IpcChannelAdapter** — named-pipe IPC channel:
- `IpcChannelAdapter.create(name) -> IpcChannelAdapter` — create server-side endpoint
- `IpcChannelAdapter.connect(name) -> IpcChannelAdapter` — connect client-side endpoint
- `.wait_for_client()` — block until a client connects (server-side)
- `.send_frame(frame)` — send a DccLinkFrame
- `.recv_frame() -> DccLinkFrame` — receive next frame

**GracefulIpcChannelAdapter** — IPC channel with graceful shutdown and thread affinity:
- Same methods as `IpcChannelAdapter` plus:
- `.shutdown()` — graceful close
- `.bind_affinity_thread()` — bind to current thread for thread-safe access

**SocketServerAdapter** — Unix domain socket / named pipe server:
- `SocketServerAdapter(path, max_connections=1, connection_timeout_ms=30000)` — create socket server
- `.socket_path` — the socket path (read-only)
- `.connection_count` — current active connections (read-only)

**Service Discovery** (still available):
- `TransportAddress`, `TransportScheme` — address types for service discovery
- `ServiceEntry` — discovered service metadata
- `ServiceStatus` — service health status

### MCP HTTP Server (`dcc_mcp_core`)

- `McpHttpConfig(port=8765, server_name=None, server_version=None, enable_cors=False, request_timeout_ms=30000)` — server configuration
  - `.lazy_actions = True` — opt-in: `tools/list` surfaces only 3 meta-tools (`list_actions`, `describe_action`, `call_action`) instead of all tools
  - `.session_ttl_secs` — idle session eviction (default 3600s)
  - `.gateway_port`, `.registry_dir`, `.stale_timeout_secs`, `.heartbeat_secs`, `.dcc_type`, `.dcc_version`, `.scene` — gateway participation config
  - `.bare_tool_names` — default `True`; publish unique action names without `<skill>.` prefix in `tools/list` (#307). Collisions fall back to full form; `tools/call` accepts both shapes
  - `.enable_resources` — default `True`; advertise `resources: { subscribe, listChanged }` and serve `resources/list|read|subscribe|unsubscribe` (#350). Built-ins: `scene://current`, `capture://current_window`, `audit://recent`
  - `.enable_artefact_resources` — default `False`; when `True`, `artefact://` URIs are served from the artefact store (#349, not yet implemented); otherwise returns JSON-RPC error `-32002`
  - `.enable_tool_cache` — default `True`; per-session `tools/list` snapshot cache (#438). Cache hit avoids registry scan + bare-name resolution + `McpTool` construction. Invalidated on skill load/unload, group activation/deactivation, session TTL eviction, or `_meta.dcc.refresh=true` on the `tools/list` request
- `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. The 2026 roadmap focuses on: (1) transport scalability — `.well-known` capability discovery, stateless sessions; (2) agent communication — Tasks lifecycle, retry/expiration; (3) governance — delegated workgroups; (4) enterprise readiness — audit, SSO (mostly extensions). No new transport types in 2026. 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

**Resources primitive** (#350, Rust-only API — Python exposure is via the HTTP methods):
- `server.resources()` (Rust) — get the `ResourceRegistry`
- `registry.set_scene(Option<Value>)` — update `scene://current`; fires `notifications/resources/updated`
- `registry.wire_audit_log(&AuditLog)` — forward `AuditLog.record()` to `audit://recent` subscribers
- `registry.add_producer(Arc<dyn ResourceProducer>)` — register a custom scheme
- JSON-RPC: `resources/list`, `resources/read`, `resources/subscribe`, `resources/unsubscribe`
- Errors: `-32601` method-not-found (resources disabled), `-32602` invalid params, `-32002` resource-not-enabled (artefact://), `-32603` producer read failure

### 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

### DccBridge — WebSocket Bridge (`dcc_mcp_core.bridge`)

- `DccBridge(host="localhost", port=9001, timeout=30.0)` — WebSocket bridge server for non-Python DCCs
  - `.connect(wait_for_dcc=False)` — start the WebSocket server; optionally block until DCC plugin connects
  - `.call(method, **params) -> Any` — synchronous RPC to the DCC plugin (thread-safe)
  - `.disconnect()` — shut down the WebSocket server
  - `.is_connected() -> bool`, `.endpoint -> str` (e.g. `"ws://localhost:9001"`)
  - Context manager: `with DccBridge(port=9001) as bridge: ...`
- `BridgeError` — base class for all DccBridge errors
- `BridgeConnectionError(BridgeError)` — DCC plugin not connected / connection lost
- `BridgeTimeoutError(BridgeError)` — call timed out
- `BridgeRpcError(BridgeError)` — DCC plugin returned JSON-RPC error; attributes: `.code`, `.message`, `.data`

### Cancellation (`dcc_mcp_core.cancellation`)

- `CancelToken()` — thread-safe cancellation flag; `.cancel()`, `.cancelled -> bool`
- `CancelledError(Exception)` — raised by `check_cancelled()` when the active request was cancelled
- `check_cancelled() -> None` — raise `CancelledError` if the active request was cancelled (no-op outside request context)
- `set_cancel_token(token) -> contextvars.Token` — install a CancelToken (dispatcher use only)
- `reset_cancel_token(reset) -> None` — restore previous token (pair with set_cancel_token)
- `current_cancel_token() -> CancelToken | None` — return the active token

### Checkpoint / Resume (`dcc_mcp_core.checkpoint`)

- `CheckpointStore(path=None)` — thread-safe checkpoint storage; in-memory by default, JSON file when `path` is set
  - `.save(job_id, state, progress_hint="")`, `.get(job_id) -> dict | None`, `.clear(job_id) -> bool`, `.list_ids() -> list[str]`, `.clear_all() -> int`
- `configure_checkpoint_store(path=None) -> CheckpointStore` — replace the module-level default store
- `save_checkpoint(job_id, state, *, progress_hint="", store=None) -> None` — persist a checkpoint
- `get_checkpoint(job_id, *, store=None) -> dict | None` — retrieve last checkpoint (keys: `job_id`, `saved_at`, `progress_hint`, `context`)
- `clear_checkpoint(job_id, *, store=None) -> bool` — delete a job's checkpoint
- `list_checkpoints(*, store=None) -> list[str]` — enumerate stored checkpoint job IDs
- `checkpoint_every(n, job_id, state_fn, *, progress_fn=None, store=None)` — auto-checkpoint every *n* iterations inside a loop
- `register_checkpoint_tools(server, *, dcc_name="dcc", store=None)` — register `jobs.checkpoint_status` and `jobs.resume_context` tools

### docs:// MCP Resources (`dcc_mcp_core.docs_resources`)

- `get_builtin_docs_uris() -> list[str]` — list built-in `docs://` resource URIs
- `get_docs_content(uri) -> dict | None` — return content dict for a `docs://` URI
- `register_docs_resource(server, *, uri, name, description, content, mime="text/markdown")` — register a single `docs://` resource
- `register_docs_resources_from_dir(server, *, directory, uri_prefix="docs://custom", glob="**/*.md") -> list[str]` — register all Markdown files as resources
- `register_docs_server(server)` — register all built-in `docs://` resources; call **before** `server.start()`
- Built-in URIs: `docs://output-format/call-action`, `docs://output-format/list-actions`, `docs://skill-authoring/tools-yaml`, `docs://skill-authoring/annotations`, `docs://skill-authoring/sibling-files`, `docs://skill-authoring/thin-harness`

### Agent Feedback & Rationale (`dcc_mcp_core.feedback`)

- `register_feedback_tool(server, *, dcc_name="dcc")` — register `dcc_feedback__report` MCP tool; call **before** `server.start()`
- `extract_rationale(params) -> str | None` — extract `_meta.dcc.rationale` from a `tools/call` params dict
- `make_rationale_meta(rationale) -> dict` — build the `_meta` fragment for a `tools/call` request with a rationale
- `get_feedback_entries(*, tool_name=None, severity=None, limit=50) -> list[dict]` — return recent feedback entries (newest first)
- `clear_feedback() -> int` — clear all in-memory feedback entries; returns count cleared

### Runtime Introspection (`dcc_mcp_core.introspect`)

- `register_introspect_tools(server, *, dcc_name="dcc")` — register four `dcc_introspect__*` tools; call **before** `server.start()`
- `introspect_list_module(module_name, *, limit=200) -> dict` — list exported names in a module
- `introspect_signature(qualname) -> dict` — get signature + docstring for a callable (e.g. `"maya.cmds.polyCube"`)
- `introspect_search(pattern, module_name, *, limit=50) -> dict` — regex-search names across a module
- `introspect_eval(expression) -> dict` — evaluate a short read-only expression and return its repr

### Recipes (`dcc_mcp_core.recipes`)

- `get_recipes_path(metadata) -> str | None` — extract recipes file path from SkillMetadata
- `parse_recipe_anchors(recipes_path) -> list[str]` — list anchor names from a RECIPES.md file
- `get_recipe_content(recipes_path, anchor) -> str | None` — fetch content of a specific anchor section
- `register_recipes_tools(server, *, skills, dcc_name="dcc")` — register `recipes__list` and `recipes__get` tools

### YAML Workflows (`dcc_mcp_core.workflow_yaml`)

- `WorkflowTask` (dataclass) — `name`, `kind` (`"task"` / `"step"`), `tool`, `inputs`, `outputs`, `on_failure`, `description`; `.interpolate_inputs(variables)` replaces `{{var}}` templates
- `WorkflowYaml` (dataclass) — `name`, `goal`, `config`, `variables`, `tasks` (list[WorkflowTask]), `source_path`; `.validate()`, `.task_names()`, `.get_task(name)`, `.to_summary_dict()`
- `load_workflow_yaml(path) -> WorkflowYaml` — load and validate a workflow YAML file
- `get_workflow_path(metadata) -> str | None` — extract workflow file path from SkillMetadata
- `register_workflow_yaml_tools(server, *, workflows=None, skills=None, dcc_name="dcc")` — register `workflows.list` and `workflows.describe` tools

### Gateway Election (`dcc_mcp_core.gateway_election`)

- `DccGatewayElection(dcc_name, server, gateway_host="127.0.0.1", gateway_port=9765, ..., on_promote=None)` — automatic gateway failover via first-wins socket election
  - `.start()` — start the background election thread
  - `.stop()` — gracefully stop the election thread
  - `.is_running -> bool`, `.consecutive_failures -> int`
  - `.get_status() -> dict` — `{running, consecutive_failures, gateway_host, gateway_port}`
  - Env vars: `DCC_MCP_GATEWAY_PROBE_INTERVAL` (5s), `DCC_MCP_GATEWAY_PROBE_TIMEOUT` (2s), `DCC_MCP_GATEWAY_PROBE_FAILURES` (3)

### Skill Hot-Reload (`dcc_mcp_core.hotreload`)

- `DccSkillHotReloader(dcc_name, server)` — generic skill hot-reload manager
  - `.enable(skill_paths, debounce_ms=300) -> bool` — enable hot-reload for given directories
  - `.disable()` — disable hot-reload
  - `.reload_now() -> int` — manually trigger a reload
  - `.is_enabled -> bool`, `.reload_count -> int`, `.watched_paths -> list[str]`
  - `.get_stats() -> dict` — `{enabled, watched_paths, reload_count}`

### Server Factory (`dcc_mcp_core.factory`)

- `create_dcc_server(*, instance_holder, lock, server_class, port=8765, register_builtins=True, extra_skill_paths=None, include_bundled=True, enable_hot_reload=False, hot_reload_env_var=None, **server_kwargs) -> McpServerHandle` — create-or-return a singleton DCC MCP server
- `make_start_stop(server_class, hot_reload_env_var=None) -> (start_fn, stop_fn)` — generate a `(start_server, stop_server)` function pair
- `get_server_instance(instance_holder) -> server | None` — return the current singleton instance

### 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`

### File Logging (`dcc_mcp_core`)

- `FileLoggingConfig(directory=None, file_name_prefix="dcc-mcp", max_size_bytes=10485760, max_files=7, rotation="both", include_console=True)` — rolling file logger config. `FileLoggingConfig.from_env()` reads `DCC_MCP_LOG_*` env vars.
- `init_file_logging(config=None) -> str` — install (or swap) the rolling-file layer. Returns resolved log directory.
- `shutdown_file_logging()` — uninstall the file-logging layer; console unaffected.
- `flush_logs()` — flush buffered log events to disk immediately; no-op when file logging is not installed. (issue #402)
- Note: `DccServerBase` writes to `dcc-mcp-<dcc_name>.<pid>.<YYYYMMDD>.log` — PID is included to isolate multi-instance log files.

### 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"
- `TOOL_NAME_RE` / `ACTION_ID_RE` — SEP-986 regex patterns for tool names and action IDs
- `MAX_TOOL_NAME_LEN = 48` — maximum tool name length

### Serialization — Rust-Powered (`dcc_mcp_core`)

- `json_dumps(obj) -> str` — serialize to JSON string (zero-dependency, Rust-powered)
- `json_loads(s) -> obj` — deserialize from JSON string (zero-dependency, Rust-powered)
- `yaml_dumps(obj) -> str` — serialize to YAML string (zero-dependency, Rust-powered)
- `yaml_loads(s) -> obj` — deserialize from YAML string (zero-dependency, Rust-powered)

### Skill Validation (`dcc_mcp_core`)

- `validate_skill(skill_dir) -> SkillValidationReport` — validate a skill directory structure and metadata
- `SkillValidationReport` — `.issues: list[SkillValidationIssue]`, `.is_valid -> bool`
- `SkillValidationIssue` — `.severity`, `.message`, `.path`

### Dispatchers (`dcc_mcp_core`)

- `PyPumpedDispatcher` — async-pump dispatcher for DCC main-thread execution
- `PyStandaloneDispatcher` — standalone dispatcher for non-DCC environments

### Workspace (`dcc_mcp_core`)

- `WorkspaceRoots` — workspace root directory management

### Garbage Collection (`dcc_mcp_core`)

- `gc_orphans() -> int` — collect orphaned resources; returns count

### Logging Configuration Constants (`dcc_mcp_core`)

- `DEFAULT_LOG_FILE_PREFIX = "dcc-mcp"`, `DEFAULT_LOG_MAX_FILES = 7`, `DEFAULT_LOG_MAX_SIZE = 10485760`, `DEFAULT_LOG_ROTATION = "both"`
- `ENV_LOG_DIR`, `ENV_LOG_FILE`, `ENV_LOG_FILE_PREFIX`, `ENV_LOG_MAX_FILES`, `ENV_LOG_MAX_SIZE`, `ENV_LOG_ROTATION` — environment variable names for file logging configuration

### SEP-986 Naming Validators (`dcc_mcp_core`)

- `validate_tool_name(name: str) -> None` — raise `ValueError` if name doesn't match `TOOL_NAME_RE` (dot-separated lowercase, max 48 chars)
- `validate_action_id(name: str) -> None` — raise `ValueError` if id doesn't match `ACTION_ID_RE` (dotted lowercase identifier chain)
- See `docs/guide/naming.md` for the full naming rules`

## 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
```

## Workflow Primitive (issue #348)

Opt-in via the `workflow` Cargo feature. Full spec-driven pipeline engine with six step kinds, step-level policies, artefact hand-off, and persistence.

- **Types**: `WorkflowSpec`, `WorkflowStep`, `StepKind` (`Tool`/`ToolRemote`/`Foreach`/`Parallel`/`Approve`/`Branch`), `WorkflowStatus`, `WorkflowJob`, `WorkflowProgress`, `StepPolicy`, `RetryPolicy`, `BackoffKind`
- **Parser / validator**: `WorkflowSpec::from_yaml_str(&str)` + `.validate()` — checks unique step ids, SEP-986 tool names, JSONPath in `branch.on` / `foreach.items`
- **Catalog**: `WorkflowCatalog::from_skill(&SkillMetadata, skill_root)` reads the `metadata["dcc-mcp.workflows"]` glob
- **Execution engine**: `WorkflowExecutor::run(spec, inputs, parent)` — drives steps sequentially, dispatches by kind, applies retry/timeout/idempotency policies, handles cancellation cascade
- **Tools** (registered via `register_builtin_workflow_tools` + `register_workflow_handlers`):
  - `workflows.run` — start a run (spec + inputs)
  - `workflows.get_status` — poll terminal status + progress
  - `workflows.cancel` — cancel by `workflow_id` (cascade)
  - `workflows.lookup` — catalog search
- **Step policies** (per-step): `timeout_secs`, `retry { max_attempts, backoff: fixed|linear|exponential, initial_delay_ms, max_delay_ms, jitter, retry_on }`, `idempotency_key`, `idempotency_scope: workflow|global`
- **Artefact hand-off**: step outputs with `file_refs` are captured to `ArtefactStore`; downstream steps reference via `{{steps.<id>.file_refs[<i>].uri}}`
- **Persistence**: `workflows` + `workflow_steps` tables under `job-persist-sqlite`; non-terminal rows flip to `Interrupted` on restart
- **Python**: `from dcc_mcp_core import WorkflowSpec, WorkflowStep, StepPolicy, RetryPolicy, BackoffKind, WorkflowStatus`

Full docs: `docs/guide/workflows.md`.

## Remote-Server Extensions (issues #404–#411)

Pure-Python helpers that enable cloud-hosted MCP agents (Claude.ai, Cursor, ChatGPT, VS Code) to reach your DCC server. All symbols are re-exported from `dcc_mcp_core`.

### Auth (`#408`, `dcc_mcp_core.auth`)

```python
from dcc_mcp_core import (
    ApiKeyConfig, OAuthConfig, CimdDocument,
    validate_bearer_token, generate_api_key, TokenValidationError,
)

# API key — simplest: read from env, attach to McpHttpConfig
cfg = McpHttpConfig(port=8765, host="0.0.0.0", enable_cors=True)
cfg.api_key = ApiKeyConfig(env_var="DCC_MCP_API_KEY").resolve()

# OAuth 2.1 + CIMD — serve /.well-known/oauth-client-metadata
oauth = OAuthConfig(
    provider_url="https://auth.example.com",
    scopes=["scene:read", "render:write"],
    client_name="Maya MCP Server",
)
cimd = oauth.to_cimd_document(redirect_uri="http://localhost:8765/oauth/callback")

# Manual validation inside a pure-Python handler
ok = validate_bearer_token(request_headers, expected_token=os.environ["DCC_MCP_API_KEY"])
```

### Batch Dispatch (`#406`, `dcc_mcp_core.batch`)

```python
from dcc_mcp_core import batch_dispatch, EvalContext

# Sequential N calls → single aggregated summary; intermediate results never hit the model
summary = batch_dispatch(
    dispatcher,
    [("get_scene_objects", {}), ("get_render_stats", {"layer": "beauty"})],
    aggregate="merge",   # "list" | "merge" | "last"
)
summary["total"], summary["succeeded"], summary["errors"], summary["merged"]

# Sandboxed script with access to dispatch() — the Cloudflare orchestration pattern
ctx = EvalContext(dispatcher, sandbox=True, timeout_secs=30)
keyframes = ctx.run("""
frames = []
for i in range(1, 11):
    r = dispatch("get_frame_data", {"frame": i})
    if r.get("output", {}).get("has_keyframe"):
        frames.append(i)
return frames
""")
```

### Elicitation (`#407`, `dcc_mcp_core.elicitation`)

```python
from dcc_mcp_core import (
    ElicitationMode, ElicitationRequest, ElicitationResponse,
    FormElicitation, UrlElicitation,
    elicit_form, elicit_form_sync, elicit_url,
)

# Async form (async def handlers)
resp = await elicit_form(
    message=f"Delete {n} objects? This cannot be undone.",
    schema={"type": "object", "properties": {"confirm": {"type": "boolean"}}, "required": ["confirm"]},
)
if not resp.accepted or not resp.data.get("confirm"):
    return {"success": False, "message": "Cancelled by user"}

# Sync variant for DCC main-thread handlers; optional fallback_values for unsupported clients
resp = elicit_form_sync(message="...", schema={...}, fallback_values={"confirm": True})

# URL mode (OAuth / payment / credential collection)
resp = await elicit_url(message="Authorize Shotgrid access", url="https://.../oauth/authorize?...")
```

Status: stubs return `accepted=False, message="elicitation_not_supported"` until Rust transport wires `notifications/elicitation/request`. Design handlers today — they upgrade automatically.

### Rich Content / MCP Apps (`#409`, `dcc_mcp_core.rich_content`)

```python
from dcc_mcp_core import (
    RichContent, RichContentKind, attach_rich_content,
    skill_success_with_chart, skill_success_with_table, skill_success_with_image,
)

# Inline Vega-Lite chart from a skill script
return skill_success_with_chart(
    "Render complete",
    chart_spec={
        "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
        "data": {"values": stats},
        "mark": "bar",
        "encoding": {"x": {"field": "layer"}, "y": {"field": "time_secs"}},
    },
    total_frames=250,
)

# Table / image
return skill_success_with_table("Scene objects", headers=["Name", "Type"], rows=[["pCube1", "mesh"]])
return skill_success_with_image("Viewport captured", image_data=png_bytes, alt="Maya viewport")

# Low-level: attach any RichContent to an existing result dict
result = skill_success("Render complete", total_frames=250)
return attach_rich_content(result, RichContent.dashboard([RichContent.chart({...}), RichContent.table(...)]))
```

Today rich content lives under `result.context["__rich__"]`; MCP-Apps-aware clients render it, others ignore gracefully. Full `tools/call` envelope wiring tracked in #409.

### Plugin Manifest (`#410`, `dcc_mcp_core.plugin_manifest`)

```python
from dcc_mcp_core import PluginManifest, build_plugin_manifest, export_plugin_manifest

manifest = build_plugin_manifest(
    dcc_name="maya",
    mcp_url="https://mcp.example.com/mcp",
    skill_paths=["/opt/skills/maya-geometry"],
    version="1.2.0",
    api_key="studio-token",     # → headers.Authorization: Bearer …
)
export_plugin_manifest(manifest, "dist/maya-mcp.plugin.json")

# Recommended on DccServerBase:
manifest = server.plugin_manifest(version="1.2.0")
```

### DCC API Executor (`#411`, `dcc_mcp_core.dcc_api_executor`)

Covers 2000+ DCC commands in a 2-tool surface (~500 tokens) — the "Cloudflare pattern" for DCC APIs.

```python
from dcc_mcp_core import DccApiCatalog, DccApiExecutor, register_dcc_api_executor

catalog = DccApiCatalog("maya", catalog_text="""
polyCube - Create a cube polygon mesh
polySphere - Create a sphere polygon mesh
select - Select nodes in the scene
""")
catalog.add_command("render", signature="render(frame)", description="Render the current frame")

executor = DccApiExecutor("maya", catalog=catalog, dispatcher=dispatcher)
register_dcc_api_executor(server, executor)   # before server.start()
# tools/list now contains only: dcc_search, dcc_execute

# Agent workflow:
# dcc_search({"query": "create sphere"}) → finds polySphere
# dcc_execute({"code": "dispatch('polySphere', {...}); return {'ok': True}"})
```

Full guide: `docs/guide/remote-server.md`. Per-module API: `docs/api/{auth,batch,elicitation,rich-content,plugin-manifest,dcc-api-executor}.md`.

## 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)
- [CodeBuddy Guide (CODEBUDDY.md)](https://github.com/loonghao/dcc-mcp-core/blob/main/CODEBUDDY.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
