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

## 🚨 CRITICAL: Skills-First Philosophy

**When interacting with DCC applications (Maya, Blender, Houdini, etc.), use
`dcc-mcp-cli` as the control path for every shell-capable agent and invoke
dcc-mcp-core Skills through it. Do not confuse the typed CLI with raw DCC
scripting.**

### Why Skills-First?

| Aspect | dcc-mcp-core Skills | Raw CLI / Scripting |
|--------|---------------------|---------------------|
| Input Validation | ✅ JSON Schema validated | ❌ None — garbage in, garbage out |
| Safety | ✅ `ToolAnnotations` (read-only, destructive, idempotent) | ❌ Unknown |
| Discoverability | ✅ `search_skills()` + semantic search | ❌ Manual exploration |
| Follow-up Guidance | ✅ `next-tools` chains | ❌ Manual discovery |
| Progressive Loading | ✅ Load only what you need | ❌ All or nothing |
| Error Recovery | ✅ Structured `error_result` with `prompt` suggestions | ❌ Unstructured stderr |
| Traceability | ✅ Audit log + telemetry | ❌ None |

### Skills-First Workflow (MEMORIZE!)

```
1. DISCOVER: `dcc-mcp-cli search --query "keyword" --dcc-type <dcc>`
2. CHECK: `dcc-mcp-cli describe <tool-slug>`
3. ACTIVATE: `dcc-mcp-cli load-skill <skill-name> --dcc-type <dcc>` when required
4. EXECUTE: `dcc-mcp-cli call <tool-slug> --json '{...}'`

`search_tools` tokenizes backend tool names and summaries; literal underscores are honored but not required (`create_sphere` and `sphere` both match). Multi-word natural-language phrases ("create sphere", "rig framework") match word-split tokens across both names and descriptions.
5. FOLLOW UP: Check next-tools.on-success for suggested next steps
6. DEBUG: On failure, use dcc_diagnostics__screenshot or audit_log
```

**❌ DON'T**: Run Maya/Blender/Python scripts directly via subprocess
**❌ DON'T**: Guess tool names without searching
**✅ DO**: Always use `dcc-mcp-cli search` before assuming a tool exists
**✅ DO**: Always check `next-tools` in results for workflow guidance

If `dcc-mcp-cli` is missing, obtain user consent before using the official
release installers in `README.md`. Keep it current with
`dcc-mcp-cli update check`, then `dcc-mcp-cli update apply`; apply stages the
next CLI launch and does not replace a running server.

**New to this project?** Read [`AI_AGENT_GUIDE.md`](AI_AGENT_GUIDE.md) FIRST.

**Building a DCC-MCP adapter?** Follow the release train docs in order:
1. [`new-adapter-onboarding.md`](docs/guide/new-adapter-onboarding.md) — scaffolding template
2. [`adapter-release-checklist.md`](docs/guide/adapter-release-checklist.md) — release gate checklist
3. [`adapter-compatibility-matrix.md`](docs/guide/adapter-compatibility-matrix.md) — per-DCC version table

---

## Quick Decision Guide

| Task | Use this API |
|------|--------------|
| Return DCC tool result | `success_result()` / `error_result()` |
| Register scripts as MCP tools | `ToolRegistry.register()` |
| Derive inputSchema / outputSchema from a typed Python handler (issue #242) | `tool_spec_from_callable(handler)` — zero-dep, reads `@dataclass` / `TypedDict` / `typing` annotations |
| 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())` — Skills-First setup with an OS-assigned instance port discoverable through the CLI/gateway; falls back to `McpHttpServer(registry, config)` for manual registry wiring |
| Register MCP prompts from Python (issue #792) | `handle = server.prompts(); handle.register_prompt(name, template, description=..., arguments=[...])` — `server.prompts()` returns `PromptHandle`; registration upserts in-place and appears in the next `prompts/list` / `prompts/get` |
| Build a DCC adapter | `opts = DccServerOptions.from_env("maya", Path(...)); DccServerBase(opts)` — skill/lifecycle/gateway/hot-reload inherited |
| Apply adapter metadata policy before skills load (#1204) | `DccServerBase.set_skill_load_transform(fn)` / `McpHttpServer.set_skill_load_transform(fn)` — callable receives a mutable `SkillMetadata`, may return `None` or a replacement, and applies consistently to direct Python `load_skill`, MCP `load_skill`, REST `/v1/load_skill`, batch, and group load paths. Use `set_after_load_skill_hook(fn)` only to observe `(skill, registered_actions)` after registration. |
| Drive a DCC main-thread dispatcher | `from dcc_mcp_core.host import HostAdapter, StandaloneHost, TickableDispatcher, QueueDispatcher, BlockingDispatcher`; `TickableDispatcher` is the public minimal protocol (`tick`, `shutdown`, `is_shutdown`) for adapter typing. Do not import private host protocols. |
| Host a universal Qt sidecar endpoint | `from dcc_mcp_core.qt_dispatcher import start_qt_server`; pass `dispatch_handler(payload)` and optional `session_info_provider`; the returned dict-compatible `ServerHandle` exposes `.port`, `.url`, and `.close()`. This is the canonical Python source mirrored byte-for-byte for Rust `qtserver://`; adapters should not vendor their own copy. |
| Inspect Qt widget trees in any DCC host (#1332) | `from dcc_mcp_core.skills.qt_ui_inspector import register_qt_ui_inspector` → five read-only tools: `qt_ui_inspector__list_windows`, `qt_ui_inspector__find_widgets`, `qt_ui_inspector__describe_widget`, `qt_ui_inspector__snapshot_tree`, `qt_ui_inspector__wait_for_widget`. DCC-agnostic; lazy-imports Qt binding (qtpy > PySide6 > PySide2 > PyQt6 > PyQt5); returns structured `qt-binding-unavailable` envelope when no binding is present. |
| Model cross-skill relationships for agent reasoning (#1336) | `from dcc_mcp_core import CapabilityGraph, CapabilityEdge, EdgeKind` — threadsafe in-memory graph for REQUIRES / PRODUCES / DEPENDS_ON / REPLACES / FALLBACK_FOR / COMPATIBLE_WITH edges. `graph.register_skill(skill_id, requires=..., produces=...)` registers a skill's outbound edges from declared metadata; `graph.expand(seeds, max_depth=2)` does bounded BFS; `graph.to_json()`/`from_json()` for serialisation. Feeds into semantic search (#1333), agent memory (#1334), and gateway reranker. |
| Dispatch script-backed sidecar actions (#1274) | `from dcc_mcp_core.sidecar import SidecarActionDispatcher`; use it as the `start_qt_server(..., dispatch_handler=dispatcher.dispatch_payload)` hook for payloads with `action`, `args`, `request_id`, and optional `script_path` / `source_file`. Core owns validation, source resolution, JSON-safe envelopes, and standard errors (`server-not-running`, `payload-malformed`, `unknown-action`, `no-source-file`, `dispatch-failed`); adapters supply `server_provider`, `action_resolver`, and either `maya_executor(execute_in_process)` or `script_executor(run_skill_script)`. Keep using direct `HostRpcClient` paths for native host RPC that is not script-backed. |
| Write skill scripts | `skill_entry` + `skill_success` / `skill_error` — zero-boilerplate skill authoring |
| Materialize ad-hoc scripts on the DCC host (#1220) | Python: `materialize_script(content, dcc_type=..., instance_id=..., session_id=..., ttl_secs=None, reuse=False)` returns `MaterializedScript` with `file_ref`, absolute `file_path`, `sha256`, `bytes`, language/suffix, session/tool/correlation metadata, and reuse/expiry fields. Default root: `~/.dcc-mcp/<dcc_type>/temp/<instance_id>/<session_id>/...`, override with `DCC_MCP_SCRIPT_MATERIALIZATION_ROOT`. Rust: `dcc_mcp_artefact::ScriptMaterializationStore`. Legacy `write_temp_script()` remains a thin compatibility wrapper. |
| Enforce file-backed ad-hoc script execution (#1221) | Use `normalize_file_backed_script_execution_params(...)` with policy set to `auto`, `require`, or `off`, or call `HostExecutionBridge.prepare_script_execution_params(...)` before host execution. `auto` materializes inline `code`; `require` rejects inline code unless a trusted `file_path` / `script_path` is supplied; `off` preserves legacy inline execution. `DccApiExecutor.execute_params(...)` and registered `dcc_execute` tools accept `code`, `file_path`, `script_path`, and `script_materialization_policy`; successful results should expose `context.materialized_script.{path,file_ref,sha256,bytes,reused}`. `DccServerBase` adds the materialization root to `McpHttpConfig.sandbox_policy` path allowlists when wiring a `HostExecutionBridge`. |
| Agent-facing materialized script API (#1222) | `DccServerBase` registers a `materialize_script` tool exposed through MCP `tools/call` and REST `/v1/call`; discover it with `search_tools("materialize script")`. Inputs: `content`/`code`, `language`, `suffix`, `display_name`, `reuse`, `reuse_key`, `ttl_secs`, `session_id`, `tool_call_id`, `correlation_id`. Output: descriptor only (`file_ref`, `file_path`, `sha256`, `bytes`, `ttl_secs`, `reused`, session/tool/correlation metadata); raw source is never echoed, and gateway trace input capture redacts script-source fields by default. Skill validation warns when script execution tools expose unbounded inline `code` without `file_path`/`script_path`. |
| Control skill trust level | `SkillScope` (Repo < User < Team < System < Admin) — higher scope shadows lower |
| Progressive tool exposure | `SkillGroup`; `load_skill()` activates groups by default, or use `load_skill(activate_groups=false)` + `activate_tool_group(group_name=...)` for lazy activation |
| 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 in-process skill scripts (MCP + REST) | `check_dcc_cancelled()` — raises `CancelledError` when the server-owned async job is cancelled; `current_job_id()` returns that job id; `current_cancel_token()` exposes a read-only `CancellationProbe` (`cancelled`, `job_id`) |
| Cooperative cancellation in DCC dispatcher skills (#522) | `check_dcc_cancelled()` — also honours per-job `JobHandle` published via `set_current_job(handle)`; required for skills launched outside an MCP request context (queued batch render, `scriptJob` callback, simulation runner) |
| Detect a misconfigured GUI binary on `DCC_MCP_PYTHON_EXECUTABLE` (#524) | `is_gui_executable(path)` → `GuiExecutableHint(gui_path, dcc_kind, recommended_replacement)` or `None` for python/unknown binaries; covers maya/houdini/unreal/blender/3dsmax/nuke/modo/motionbuilder/c4d/katana |
| Auto-correct a GUI binary to its headless sibling (#524) | `correct_python_executable(path)` — returns `mayapy.exe` next to `maya.exe`, `hython` next to `houdini`, `UnrealEditor-Cmd.exe` next to `UnrealEditor.exe`; falls back to original path when no sibling is found on disk |
| Adapter install/uninstall and sidecar launch without loading `_core` (#1101) | `from dcc_mcp_core.install_lifecycle import query_runtime_state, sidecar_readiness_status, wait_for_sidecar_ready, probe_sidecar_tool, stop_runtime_entries, build_sidecar_command, launch_sidecar, sidecar_host_rpc_dispatch_contract, inspect_install_root, safe_remove_tree, safe_replace_tree`; helpers emit the stable `dcc-mcp-server sidecar` argv while the Rust runtime lives in `dcc-mcp-sidecar`; `build_sidecar_command()` returns matching readiness selectors/argv/CLI plus `dispatch_contract` (`uri_valid`, `validation_error`, `dispatch_ready_capable`) and `readiness_contract` (`ready_on_launch=false`, `direct_use_status`); pass `require_dispatch_capable=True` or CLI `--require-dispatch-capable` when startup code must reject malformed `commandport://` / `qtserver://` URIs, `stub://`, or unsupported schemes before claiming tools are callable; `launch_sidecar(...)` always returns `ready=false` / `readiness_checked=false` until `wait_ready_timeout_secs` is provided and the bounded readiness verdict succeeds, so process spawn success is never a direct-use proof; CLI: `python -m dcc_mcp_core.install_lifecycle sidecar-ready --dcc maya --timeout-secs 5 --probe-tool maya_diagnostics__ping` |
| Declarative progressive skill loading on startup (#525) | `MinimalModeConfig(skills=("skill_a","skill_b"), deactivate_groups={"skill_a": ("preview",)})` → `register_builtin_actions(minimal_mode=cfg)`. Env vars: `DCC_MCP_DEFAULT_TOOLS` (comma-separated explicit list, takes precedence), `DCC_MCP_MINIMAL=0` (load every discovered skill). |
| Re-scan a running adapter's skill dirs without restart (#1400 follow-up) | `register_builtin_actions` auto-registers the `dcc_admin__reload_skills` MCP tool; the elected gateway pushes a best-effort `tools/call` for it to every live backend on any admin skill-path add/remove **and** on `GET /admin/api/skill-paths` (opening the panel), so new `~/.dcc-mcp/<dcc>/skills` packages appear without a restart. Event-driven, zero idle cost. Adapter-side trigger: `DccServerBase.reload_skill_paths()`. |
| Report broken / skipped skills (maya#138) | `DccServerBase.reload_skill_paths_report()` → `{"count", "skipped"}`; inner core `McpHttpServer.discover_report(extra_paths=None, dcc_name=None)` → `{"count", "skipped"}` (Rust `SkillCatalog::discover_report` → `DiscoverReport`). Skipped dirs (missing/invalid `SKILL.md`) are logged at WARNING and surfaced in the gateway admin operator note. |
| Unified host execution bridge for embedded adapters (#599) | `HostExecutionBridge(dispatcher=..., runner=...)` + `DccServerOptions.from_env(..., execution_bridge=bridge)` → `DccServerBase(opts)` — one adapter-facing object for in-process skill scripts and direct host callables; use `bridge.dispatch_callable(...)` for dynamic work so affinity metadata and error normalization match skill execution. Audited skills can set `enforce_thread_affinity: true` to reject mismatched runtime contexts with `THREAD_AFFINITY_VIOLATION`. |
| Deferred host-operation completion (#604) | Return `DeferredToolResult(check_is_finished=..., timeout_secs=..., poll_interval_secs=..., stdout=..., stderr=...)` from an in-process skill or `HostExecutionBridge.dispatch_callable()`; `check_is_finished()` returns `None` while running and the final JSON-serialisable result when complete. Async `tools/call` keeps the existing `JobManager` row running until the bridge returns or times out. |
| Wire in-process Python skill execution from a DCC adapter (#521) | `DccServerBase.register_inprocess_executor(dispatcher=None)` — compatibility shortcut for existing adapters; pass a `BaseDccCallableDispatcher` (Protocol with `dispatch_callable(func, *args, **kwargs)`) to route onto the host UI thread; `None` runs scripts inline (`mayapy`, headless Houdini, pytest). |
| Full callable-payload dispatch contract for DCC adapters (#520) | `BaseDccCallableDispatcherFull` Protocol — `submit_callable(request_id, task, affinity, timeout_ms) -> JobOutcome`, `submit_async_callable(...) -> PendingEnvelope`, `cancel(request_id) -> bool`, `shutdown(reason) -> int`. Companion `BaseDccPump` for cooperative idle-tick draining. Reference impl `InProcessCallableDispatcher` runs jobs inline for `mayapy` / headless / pytest; production hosts (Maya UI thread, Houdini …) compose or subclass it. Per-job `current_callable_job` ContextVar enables cooperative-cancel probes from inside the task. |
| Reuse host pump/timer lifecycle (#1276) | `HostPumpController(pump, timer_adapter, policy=AdaptivePumpPolicy(...), budget_ms=8)` composes with `HostUiDispatcherBase` or any `drain_queue(budget_ms)` pump. Use `ManualHostTimerAdapter` for conformance tests, `ThreadedHostTimerAdapter` for standalone/headless tests, and `QtHostTimerAdapter` for Qt-bearing DCCs. Stats expose queue size, active jobs, last interval, overrun count, last tick time, shutdown, ticks, drained jobs, and elapsed ms; adapters map Maya script jobs, 3ds Max timers, Blender timers, or Qt `QTimer` onto the `HostPumpTimerAdapter` methods. |
| Reuse UI-thread dispatcher lifecycle (#1275) | Subclass `HostUiDispatcherBase` for interactive DCC UI-thread dispatchers; implement only `poke_host_pump()` and optional hooks `format_exception_error`, `format_timeout_error`, `on_job_queued`, `on_job_started`, `on_job_finished`, plus constructor `label=...`. Core owns queueing, sync/async submission, cancellation, timeout waits, shutdown cleanup, active-job tracking, `queue_size()`, `active_count()`, and standard outcome envelopes. |
| Migrate adapter dispatchers (#1277) | Use `docs/guide/adapter-dispatcher-migration.md` for the decision table: Qt sidecars use `start_qt_server` + `SidecarActionDispatcher`, interactive UI hosts subclass `HostUiDispatcherBase` + `HostPumpController`, headless hosts use `InProcessCallableDispatcher`, and native non-script RPC keeps `HostRpcClient`. `tests/test_dispatcher_migration_conformance.py` is the fake-adapter fixture covering successful dispatch, malformed payloads, missing server/source, executor error, cancellation, timeout, and shutdown cleanup. |
| Cross-DCC asset round-trip contract (#688) | `dcc_mcp_core.SceneStats(object_count, vertex_count, has_mesh, extra={})` — minimal shape every DCC verifier skill returns from `import_and_inspect(file_path)`; `SceneStats.matches(other, vertex_tolerance=0.05)` adjudicates producer→file→verifier drift. DCC-specific implementations live in downstream repos (`dcc-mcp-blender`, `dcc-mcp-maya`, ...). See `docs/guide/cross-dcc-verification.md`. |
| 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 |
| Project-level state persistence (#576) | `DccProject.open(scene_path)` creates/loads `.dcc-mcp/project.json`; mutate via `add_asset`, `activate_skill`, `activate_tool_group`, `add_checkpoint_id`, `update_metadata`; call `register_project_tools(server, ...)` to expose `project_save`, `project_load`, `project_resume`, and `project_status`; use `resume_session()` to recover scene/assets/skills/checkpoints across DCC restarts |
| 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` |
| Optional skill metadata extension tools (issue #1229) | `register_metadata_driven_tools(server, skills=..., dcc_name="maya")` — registers optional metadata-driven tools such as recipes and skill reference docs, returning a report with `registered` / `failed` / `skipped` extension outcomes |
| Skill/domain recipes (issues #428, #616) | `register_recipes_tools(server, skills=...)` — `recipes__list/search/get/validate/apply` tools for Markdown anchors and structured YAML recipe packs |
| 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 |
| Daemon-backed gateway auto-launch | Python `DccServerBase`, Rust sidecars (`dcc-mcp-sidecar`, surfaced through `dcc-mcp-server sidecar`), `dcc-mcp-server` implicit/`auto`/`serve` backends, and `dcc-mcp-server translate` bridges ensure the machine-wide `dcc-mcp-server gateway` when `gateway_port > 0`, keep a post-startup guardian, stamp `gateway_runtime_mode` / `gateway_guardian_enabled` / `gateway_recovery_driver` / `registration_refresh_mode` into daemon-backed service rows, and reclaim a leftover `gateway-launch.lock` after `DCC_MCP_GATEWAY_LAUNCH_LOCK_STALE_SECS` (default `30`) so any surviving DCC can restart the shared daemon. Explicit `dcc-mcp-server gateway --daemon` respawns the current executable as a detached child instead of forking inside the async runtime; `--pidfile PATH` implies daemon mode, records the detached child PID, and fails fast if respawn or pidfile writing fails. `gateway_recovery_driver="daemon_guardian"` means recovery is driven by guardian `/health` probes; `registration_refresh_mode="file_registry_heartbeat"` means the service row is refreshed by registry heartbeat rather than an explicit re-register after gateway restart. Gateway `GET /v1/readyz` exposes `gateway_recovery_driver_counts`, `registration_refresh_mode_counts`, `gateway_daemon_guardian_instance_count`, and `gateway_daemon_guardian_ready` so launchers and admin panels can answer whether a live DCC service can restart the daemon without scanning each instance row. |
| Optional gateway instance pooling (#615, #810) | Registry entries expose `pool.capacity`, `lease_owner`, `current_job_id`, `available`; gateway tools `acquire_dcc_instance(dcc_type, instance_id?, lease_owner?, current_job_id?, ttl_secs?)` and `release_dcc_instance(instance_id, lease_owner?)` accept full UUID, `instance_short`, or any unique ≥4-char UUID prefix; both advertise `annotations: {destructiveHint:false, openWorldHint:true}` |
| Hide unknown DCC types from gateway tools list (issues #553, #555) | `McpHttpConfig.allow_unknown_tools = false` (default) — gateway capability exposure skips entries whose `dcc_type == "unknown"`; flip to `true` only for trusted bootstrapping |
| Gateway MCP surface is minimal by design (PR A, supersedes #652 / #674; tightened in #813 phase 1) | `tools/list` on the gateway only ever returns the discover+dispatch primitives (`search_skills`, `load_skill`, `unload_skill`, `get_skill_info`, `list_skills`, `search_tools`, `describe_tool`, `call_tool`, `call_tools`, plus `acquire_dcc_instance` / `release_dcc_instance` / diagnostics). The legacy instance discovery triple (`list_dcc_instances`, `get_dcc_instance`, `connect_to_dcc`) was removed in #813 phase 1 — agents now `resources/read uri=gateway://instances` (or `gateway://instances/{id}`) instead, and each entry already carries `mcp_url` so no separate connect verb is needed. The non-standard `instances/list` JSON-RPC method was removed at the same time. Per-tool backend tools are NEVER fanned out — agents discover them via `search_tools` / `describe_tool` and invoke them via `call_tool` or batched `call_tools` (which routes into the per-DCC REST `POST /v1/call` / gateway `POST /v1/call_batch` below). The `GatewayToolExposure` enum and its `tool_exposure` / `publishes_backend_tools` config knobs have been removed; there is a single, unconditional surface now. Diagnostics JSON reports `metrics.mcp_surface = "discover+dispatch"` + `metrics.publishes_backend_tools = false`. |
| Cursor-safe gateway prompt names (#656) | Aggregating gateway `prompts/list` always emits `i_<id8>__<escaped>` names so they match `decode_tool_name` and survive Cursor's `^[A-Za-z0-9_]+$` filter. |
| Dynamic DCC capability index + REST API + MCP wrappers (issues #653 / #654 / #655, tracking #657; #810 annotations) | `dcc_mcp_gateway::capability::CapabilityIndex` stores compact (~200 B) per-tool records (`tool_slug`, `backend_tool`, `skill_name`, `summary`, `tags`, `dcc_type`, `instance_id`, `has_schema`) keyed by slug `<dcc>.<id8>.<tool>`; `dcc-mcp-gateway-search` owns reusable tokenization/fuzzy/exact ranking and `dcc_mcp_gateway::capability_service` routes REST and MCP surfaces through the same code path. REST endpoints on the gateway: `POST /v1/search`, `POST /v1/describe`, `POST /v1/call`, `POST /v1/call_batch`, `GET /v1/instances` (mirrors `/instances`). MCP wrappers exposed through `tools/list` as fixed, cursor-safe names: `search_tools {query, dcc_type, instance_id, tags, scene_hint, limit, offset, loaded_only, include_unloaded_skills, include_stubs, mode}` (default `mode: "fuzzy"` uses `nucleo-matcher` for typo/prefix tolerance per #659; set `mode: "exact"` for pre-#659 substring table; default search omits `__skill__*` / `__group__*` stubs but returns unloaded skills as `skill_candidate` hits with `requires_load_skill: true` unless `include_unloaded_skills=false`; set `include_stubs=true` only when debugging progressive-loading placeholders; `search_page(...)` returns `{hits, total, offset, limit}` for pagination), `describe_tool {tool_slug}`, `call_tool {tool_slug, arguments, meta}`, `call_tools {calls:[{tool_slug, arguments?, meta?}], stop_on_error?}` (max 25 calls; backend-specific fields such as `code` or `file_path` must live inside each `arguments` object, never at the wrapper top level). `search_tools` and `describe_tool` advertise `annotations: {readOnlyHint:true, openWorldHint:true}`; `call_tool` and `call_tools` advertise `{destructiveHint:true, openWorldHint:true, idempotentHint:false}`. The index is refreshed on demand before each search/describe call (short-circuited via per-instance fingerprint) so the first agent query after startup or `load_skill` sees fresh data without waiting for a periodic tick; instances that leave the registry are evicted, and terminal `host-died` backend errors immediately drop that instance's records plus its FileRegistry or HTTP registration row instead of waiting for heartbeat/stale cleanup. Skill stubs (`__skill__*`), gateway-local, and skill-management tools are filtered out of the index so they never appear twice. Slim / REST `tools/list` stays bounded (~17 entries) regardless of how many backends are live — the whole point of the #657 redesign. Ambiguous / offline routing produces structured `{error: {kind, message, candidates}}` envelopes (`kind` ∈ `unknown-slug`, `ambiguous`, `instance-offline`, `backend-error`, `bad-request`). |
| Per-DCC REST skill API surface (issues #658 / #660) | `dcc_mcp_skill_rest::{SkillRestService, SkillRestConfig, build_skill_rest_router}` mounts a tiny `/v1/*` router on the *per-DCC* `McpHttpServer` so non-MCP agents and remote enterprise platforms can call DCC skills without going through the gateway. Routes: `GET /v1/healthz` (liveness), `GET /v1/readyz` (3-state readiness: process/dispatcher/dcc), `GET /v1/openapi.json` (utoipa-generated OpenAPI 3.x), `GET /v1/skills` (loaded tools), `POST /v1/search` (compact hits, schema-omitted; <512 B/hit token budget), `POST /v1/describe` & `GET /v1/tools/{slug}` (schema + annotations), `POST /v1/call` (invoke; respects skill thread-affinity & main-thread executor), `GET /v1/context` (scene/document snapshot). SOLID: every collaborator is a trait — `SkillCatalogSource`, `ToolInvoker`, `AuthGate`, `AuditSink`, `ReadinessProbe` — defaults wire to existing `SkillCatalog` + `ToolDispatcher`; adapters swap impls without touching the router. Auth defaults to `AllowLocalhostGate` (loopback-only); remote callers must install `BearerTokenGate::new(vec![token])`. Errors share one envelope `ServiceError {kind, message, hint?, request_id?, candidates?}` with kebab-case `kind` ∈ `unknown-slug` `ambiguous` `skill-not-loaded` `invalid-params` `unauthorized` `bad-request` `affinity-violation` `not-ready` `backend-error` `internal`. Every call emits one `AuditEvent` via the configured `AuditSink`. The exact same `SkillRestService` powers gateway MCP wrappers, guaranteeing REST/MCP envelope parity (regression-tested). Gateway probes prefer `/v1/readyz` and fall back to `/health` only when the readiness endpoint is absent. |
| Headless USD project resources (issue #1209) | `register_usd_project_resources(server, project_root=..., stage=..., layers=...)` registers canonical `openusd://stage`, `openusd://layers`, `openusd://assets`, `openusd://materials`, `openusd://validation`, `openusd://snapshots`, and `openusd://packages` resources. `UsdProjectResourceProvider.list_resources()` feeds rich MCP `resources/list` records with stable names, descriptions, MIME types, project-root labels, and file-ref metadata without executing tool scripts. The convention is DCC-agnostic for OpenUSD, Houdini Solaris, Maya USD, Blender USD, Unreal, and Omniverse-style adapters. |
| Adapter readiness binding (issue #1206) | Use `AdapterReadinessBinder.bind_inline(server)`, `.bind_headless(server)`, or `.bind_queue_dispatcher(server, dispatcher, require_first_pump=True)` before `server.start()` instead of hand-toggling readiness in every adapter. The binder publishes one `ReadinessProbe` through `DccServerBase.set_readiness_probe()` / `McpHttpServer.set_readiness_probe()` so MCP `tools/call`, REST `/v1/readyz`, and REST `/v1/call` share process, DCC, catalog, dispatcher, host bridge, and main-thread executor bits. Use `readiness_report_subset(...)` in downstream tests to compare stable bit subsets. |
| Auto-evict dead gateway instances (issues #551, #552, #556) | Built-in TCP probe task in `dcc_mcp_gateway::tasks::start_gateway_runtime` — connects to each backend's listener, deregisters after `health_check_max_failures` consecutive misses (default 3) and immediately on startup probe miss; expose tuning via `McpHttpConfig.health_check_*` |
| Crash-safe FileRegistry heartbeat (issue #554) | `FileRegistry::heartbeat` writes via `tempfile::NamedTempFile::persist` (atomic rename) plus Windows `LockFileEx`/`UnlockFileEx` so concurrent processes never produce a half-written or stomped registry entry |
| Default rolling file logging (issue #557) | `dcc_mcp_logging::file_logging::default_file_logging_config()` returns a `FileLoggingConfig` with daily rotation under the platform-standard log directory; pair with `init_file_logging(cfg)` |
| Trim old log files (issue #558) | `dcc_mcp_logging::file_logging::prune_old_logs(retention_days, max_total_size_mb)` — call on a schedule or at startup to enforce both age- and size-based retention |
| Prometheus `/metrics` endpoint for gateway (issue #559) | Build `dcc-mcp-http` with `--features prometheus`; `attach_gateway_metrics_route(router)` mounts `GET /metrics`, while a 5 s background task in the gateway runtime refreshes `dcc_mcp_instances_total` with `status` labels `active` / `stale` from the live `FileRegistry`; `dcc_mcp_telemetry::PrometheusExporter` also exposes `dcc_mcp_tools_total{dcc_type}`, `dcc_mcp_request_duration_seconds`, `dcc_mcp_requests_failed_total{method,error}` |
| Zero-config remote MCP relay (issue #504) | `RelayServer::start(RelayConfig, agent_bind, frontend_bind).await` (server) + `dcc_mcp_tunnel_agent::run_once(AgentConfig::new(relay_url, jwt, dcc, local_target)).await` (sidecar) — agent registers, frontend TCP clients select the tunnel via 2-byte length-prefixed id and full-duplex byte-stream over multiplexed sessions |
| Relay WS frontend + admin endpoint (issue #504) | `RelayServer::start_with(cfg, agent_bind, frontend_bind, OptionalBinds { ws_frontend: Some(addr), admin: Some(addr) }).await` — opt-in `ws://host/tunnel/<id>` upgrade transport (one binary WS message per MCP payload) and read-only `GET /tunnels` JSON listing + `GET /healthz` |
| Tunnel agent reconnect with back-off | `dcc_mcp_tunnel_agent::run_with_reconnect(cfg, watch_rx).await` → `ReconnectExit::{Shutdown, Fatal}` — wraps `run_once` in an outer loop applying `AgentConfig::reconnect` (Constant or Exponential, doubling capped at `max`); successful registration resets the delay; `Rejected(_)` short-circuits to fail-fast on bad JWT |
| Mint a tunnel JWT | `dcc_mcp_tunnel_protocol::auth::issue(&TunnelClaims { sub, iat, exp, iss, allowed_dcc }, secret)` — relay validates `allowed_dcc` scope on every registration |
| 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-backed skill helpers (issues #1255, #1256, #1257, #1258) | `from dcc_mcp_core.skills_helper import json_dumps, json_loads, yaml_dumps, yaml_loads, http_request, http_get_json, http_post_json, load_json_file, dump_json_file, atomic_write_text, file_digest, ensure_within_root, compress_bytes, decompress_bytes, SkillCodecError, SkillFileError, SkillHttpError, skill_success, check_cancelled` — canonical skill-authoring namespace for dependency-light helper APIs; legacy top-level codec imports still work |
| Rust-powered JSON/YAML | `json_dumps(obj)` / `json_loads(s)` / `yaml_dumps(obj)` / `yaml_loads(s)` plus `load_json_file(...)` / `load_yaml_file(...)` — zero-dependency serialization, canonically exported from `dcc_mcp_core.skills_helper`; TOML runtime helpers are intentionally deferred until a real skill runtime use case appears |
| Rust-backed skill file helpers | `ensure_within_root(root, path, must_exist=False)` rejects traversal outside trusted roots; `atomic_write_text/bytes(path, data, root=None, max_bytes=...)` write via same-directory temp files; `file_digest(...)` / `bytes_digest(...)` expose SHA-256; `compress_bytes(...)` / `decompress_bytes(...)` use bounded LZ4 frames. Use `FileRef` / `artefact_put_file` when handing files between tools. |
| Rust-backed skill HTTP helpers (#1256) | `http_request(method, url, headers=None, query=None, json=None, body=None, timeout_ms=5000, max_bytes=1048576, raise_for_status=False)` returns `HttpResponse(status, headers, bytes, text, json(), url, elapsed_ms, truncated)`; `http_get_json` and `http_post_json` require 2xx and parse with the Rust JSON codec. Catch `SkillHttpError` / `HttpStatusError`; use `redact_http_headers` before echoing headers into errors or audit metadata. |
| Canonical MCP/REST wire normalization | Rust: `dcc-mcp-wire::{normalize_arguments, normalize_meta, decode_call_tool, decode_rest_call}`; Python host wrappers: `from dcc_mcp_core.host import normalize_tool_arguments, normalize_tool_meta`. Missing / `null` / empty-string arguments become `{}`; objects pass through; object-shaped JSON strings are accepted; arrays/numbers/booleans/non-object strings are rejected. |
| Discover team-level skills | `scan_and_load_team()` / `scan_and_load_team_lenient()` |
| Discover user-level skills | `scan_and_load_user()` / `scan_and_load_user_lenient()` |
| Reference files across tool calls (artefacts) | `FileRef` + `artefact_put_file()` / `artefact_get_bytes()` / `artefact_list()` |
| Host-local executable script files | `materialize_script(...)` for DCC/session-scoped script files; the returned `file_ref` is compatible with FileRef metadata but the executable path stays on the host filesystem for `execute_python(file_path=...)` style tools. |
| Capture DCC stdout/stderr/script-editor output | `OutputCapture` |
| Rich skill error with traceback | `skill_error_with_trace()` |
| Skill warning / exception helpers | `skill_warning()` / `skill_exception()` |
| Disable accumulated/evolved skills | `ENV_DISABLE_ACCUMULATED_SKILLS` |
| Make skill discovery hermetic | `DCC_MCP_DISABLE_DEFAULT_SKILL_PATHS=1` omits implicit operator-owned roots (local/platform defaults, marketplace installs, and Admin custom paths) while preserving explicit, bundled, and `DCC_MCP_*_SKILL_PATHS` paths |
| Typed Python tool handler return | `from dcc_mcp_core.result_envelope import ToolResult` → `ToolResult.ok("msg", **ctx).to_dict()` / `ToolResult.fail("msg", error="code").to_dict()` (also `success_`/`error_`, plus `not_found(entity_type, name)` / `invalid_input(msg)` shortcuts). **Note**: `success`/`error` are dataclass fields, not factories; calling `ToolResult.success(...)` raises `AttributeError` (#487) |
| Centralised metadata key constants | `from dcc_mcp_core import METADATA_RECIPES_KEY, METADATA_LAYER_KEY, LAYER_THIN_HARNESS, CATEGORY_DIAGNOSTICS, ...` — re-exported at top level; also `from dcc_mcp_core.constants import ...`. Every `"dcc-mcp.<feature>"` string lives here. Available constants: `METADATA_DCC_MCP`, `METADATA_RECIPES_KEY`, `METADATA_WORKFLOWS_KEY`, `METADATA_LAYER_KEY`, `METADATA_DCC_KEY`, `METADATA_VERSION_KEY`, `METADATA_TOOLS_KEY`, `METADATA_GROUPS_KEY`, `METADATA_SEARCH_HINT_KEY`, `METADATA_TAGS_KEY`, `METADATA_EXTERNAL_DEPS_KEY`, `LAYER_THIN_HARNESS`, `LAYER_DOMAIN`, `LAYER_INFRASTRUCTURE`, `LAYER_EXAMPLE`, `CATEGORY_DIAGNOSTICS`, `CATEGORY_FEEDBACK`, `CATEGORY_INTROSPECT`, `CATEGORY_RECIPES`, `CATEGORY_WORKFLOWS`, `CATEGORY_DOCS`, `CATEGORY_GENERAL` (#487) |
| Custom JSON-RPC method (Rust) | `dcc_mcp_http::handler::{MethodRouter, MethodHandler, HandlerFuture}` — `router.register("ping", Arc::new(handler))`; capability gating lives in the handler, not the router (#492) |
| Custom action validation (Rust) | `dcc_mcp_actions::validation_strategy::{ValidationStrategy, ValidationOutcome, NoOpValidator, SchemaValidator, select_strategy}` — adding a flavour does not touch `dispatch()` (#493) |
| Custom version constraint shape (Rust) | `dcc_mcp_actions::versioned::matcher::{VersionMatcher, AnyMatcher, ExactMatcher, AtLeastMatcher, GreaterThanMatcher, AtMostMatcher, LessThanMatcher, CaretMatcher, TildeMatcher}` — adding a shape = enum variant + matcher impl + `with_matcher` arm; `matches()` / `Display` untouched (#493) |
| Build a registry-like container (Rust) | `dcc_mcp_models::registry::{Registry, RegistryEntry, DefaultRegistry, SearchQuery}` — `ToolRegistry`, `SkillCatalog`, `WorkflowCatalog` all `impl Registry<V>`; shared contract test in `registry::testing` (feature `testing`) (#489) |
| Build a JSON-RPC notification (Rust) | `dcc_mcp_jsonrpc::{NotificationBuilder, JsonRpcRequestBuilder}` — single source for the `{"jsonrpc":"2.0","method":..,"params":..}` shape; `.as_sse_event()` for SSE frames (#484) |
| Typed DCC name (Rust) | `dcc_mcp_models::DccName` — canonical enum + `parse(s)` (case-insensitive aliases) + `as_str()` round-trip; `DccName::Other(String)` preserves unknown values (#491) |
| Unified workspace error (Rust) | `dcc_mcp_models::DccMcpError` — single error enum with `From<HttpError>` / `From<ProcessError>` impls; per-crate enums are `From`-converted at the boundary (#488) |
| Register lifecycle hooks (typed observer / policy) | `from dcc_mcp_core import LifecycleHooks, HookEvent, HookContext, HookDeny` — `hooks = LifecycleHooks()` + `@hooks.on(HookEvent.BEFORE_TOOL_CALL)` + `server.register_lifecycle_hooks(hooks)` (#1337) |
| Veto a skill load or tool call | Raise `HookDeny(reason, hint="use typed skill X first")` from a `BEFORE_SKILL_LOAD` / `BEFORE_TOOL_CALL` / `BEFORE_SEARCH` handler (#1337) |
| Observe lifecycle events (analytics/audit) | `HookEvent.AFTER_SEARCH` / `AFTER_SKILL_LOAD` / `AFTER_TOOL_CALL` / `SESSION_START` / `SESSION_END` — non-policy; exceptions swallowed (#1337) |
| Enrich search context via hooks | `HookEvent.BEFORE_SEARCH` handler mutates `ctx.payload` in-place; mutated dict visible to search caller (#1337) |
| Enable three-tier agent memory | `MemoryRecorder(InMemoryMemoryStore()).install(lifecycle_hooks)` — EPHEMERAL → WORKING → LONGTERM with auto-compaction on SESSION_END (#1334) |
| Inject memory into search/tool-call context | `MemoryRecorder` auto-injects `memory_summary` / `memory_prefer_tools` / `memory_avoid_tools` in `BEFORE_SEARCH` / `BEFORE_TOOL_CALL` (#1334) |
| Query agent memory | `store.query(MemoryQuery(layer=MemoryLayer.WORKING, session_id=..., key_prefix="tool_call:"))` → sorted `tuple[MemoryEntry, ...]` (#1334) |
| Swap memory backend | Implement `MemoryStore` Protocol; `InMemoryMemoryStore` is default; plug SQLite/file for persisted LONGTERM (#1334) |
| Register all standard built-in MCP tools | `register_all_builtin_skills(server, dcc_name=..., skills=...)` — idempotent one-call registration (#1332) |
| Understand skill layer ranking | `metadata.dcc-mcp.layer` ∈ `domain` (×1.00), `infrastructure` (×0.35), `thin-harness` (×0.20), `example` (excluded) — layer penalty bypassed when `tags=` filters by layer name (#1398) |
| Understand skill path-source ranking | `SkillPathSource` × multiplier: `ExplicitArg`/`AdminCustom`/`EnvVar`/`LocalDev` ×1.00, `Platform` ×0.85, `Bundled` ×0.70 — compounds with layer multiplier; exact-name bypasses both (#1403) |
| Structured skill recall metadata | `RecallContext`, `Precondition`, `SideEffects`, `ToolRole`, `RiskLevel` on `SkillMetadata` / `ToolDeclaration` — optional ranking/policy hints; `ToolRole.EscapeHatch` is demoted (#1335) |
| Check sidecar dispatch readiness | `dispatch.reported`, `dispatch.status` (`ready`/`unavailable`/`not_reported`), `dispatch.ready` on `gateway://instances` rows; gateway `GET /v1/readyz` includes `dispatch_reported_instance_count`, `dispatch_ready_instance_count`, `dispatch_not_ready_instance_count`. Use dispatch counters to distinguish "DCC process registered" from "sidecar dispatcher actually callable" (#1398 follow-up). |
| Inspect gateway daemon guardian state | `gateway.runtime_mode` (`daemon-backed`/`embedded-fallback`/`null`), `gateway.guardian_enabled`, `gateway.recovery_driver` (`daemon_guardian`/`embedded_election`/`none`), `gateway.registration_refresh_mode` on `gateway://instances` rows; `GET /v1/readyz` exposes aggregate counts (`gateway_recovery_driver_counts`, `gateway_daemon_guardian_instance_count`, `gateway_daemon_guardian_ready`) so launchers know whether a live service can restart the shared daemon without scanning each row. |
| Discover instances via multiple sources | `gateway://instances` rows carry `source` ∈ `file` (FileRegistry), `http` (HTTP registration #1361), `mdns` (mDNS/DNS-SD #1362), `relay` (tunnel relay #1363); conflict resolution: http > relay > mdns > file. mDNS requires `--features mdns` build flag; relay requires `--relay-source ADMIN_URL=PUBLIC_BASE_URL`. |

## 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.95+ + PyO3 0.28+)
- Version: 0.19.64 <!-- x-release-please-version -->
- License: MIT

## Architecture

Rust workspace with package membership defined by the root `Cargo.toml`, using PyO3 bindings. All logic lives in Rust sub-crates; Python gets a single `dcc_mcp_core._core` extension module. The `dcc_mcp_core` package re-exports the public API from `_core` plus pure-Python helpers (DccServerBase, DccServerOptions, DccGatewayElection, DccSkillHotReloader, factory, skill helpers, ToolResult dataclass, constants).

### dcc-mcp-http SOLID extraction (PR #667)

The historical god-crate `dcc-mcp-http` was decomposed into cohesive crates. New code should import the extracted crates directly:

- **`dcc-mcp-jsonrpc`** — MCP 2025-03-26 JSON-RPC builders. Zero axum/tokio dependency, so standalone clients/CLIs can use it.
- **`dcc-mcp-wire`** — canonical MCP/REST call envelopes, validation, and argument/meta normalization shared by JSON-RPC, gateway, REST, and Python host wrappers.
- **`dcc-mcp-job`** — async job tracker + pluggable persistence. Owns the optional `job-persist-sqlite` feature; `dcc-mcp-http` forwards the flag.
- **`dcc-mcp-skill-rest`** — per-DCC `/v1/*` REST skill API. Owns the `utoipa` OpenAPI generator.
- **`dcc-mcp-gateway-core`** — pure gateway domain types and algorithms (`CapabilityRecord`, search query/page/hit types, slug helpers, ranking scorers) with no HTTP or async runtime dependency.
- **`dcc-mcp-gateway-search`** — reusable capability tokenization, fuzzy/exact matching, pagination, and ranking engine.
- **`dcc-mcp-gateway`** — multi-DCC gateway app/infrastructure: registry probing, REST facade, and dynamic-capability MCP wrappers. It depends inward on `dcc-mcp-gateway-core`, `dcc-mcp-gateway-search`, and `dcc-mcp-wire`.
- **`dcc-mcp-http-types`** — pure HTTP wire/config/value types (`HttpError`, `JobConfig`, `InstanceConfig`, `TelemetryConfig`, `FeatureFlags`, prompt/resource/output/session values), re-exported by `dcc-mcp-http` for compatibility.
- **`dcc-mcp-http-server`** — reusable runtime support for the embedded server (core tool builders, executor bridge, session state, in-flight cancellation/progress, notifications, workspace roots) without axum/PyO3.

`dcc-mcp-http` remains the embedded MCP HTTP server facade (axum routes, server startup, resource/prompt registries, gateway bootstrap, and compatibility re-exports) and depends on the extracted crates. New Rust code should import pure config/value types, including `McpHttpConfig`, from `dcc-mcp-http-types`; the PyO3 HTTP binding boundary lives in `dcc-mcp-http-py`.

```
crates/
├── dcc-mcp-naming/         # client-safe tool-name / action-id validators
├── dcc-mcp-models/         # SkillMetadata, ToolResult, DccName, DccMcpError
├── dcc-mcp-actions/        # ToolRegistry, ToolDispatcher, EventBus, validation
├── dcc-mcp-skills/         # SkillScanner, SkillCatalog, SkillWatcher, resolver
├── dcc-mcp-protocols/      # MCP Tool/Resource/Prompt/DccAdapter models
├── dcc-mcp-jsonrpc/        # JSON-RPC builders
├── dcc-mcp-wire/           # Canonical MCP/REST call envelopes + normalization
├── dcc-mcp-job/            # Async job tracker + optional persistence
├── dcc-mcp-skill-rest/     # Per-DCC /v1/* REST skill API
├── dcc-mcp-gateway-core/   # Pure gateway domain/search/ranking types
├── dcc-mcp-gateway-search/ # Reusable capability search/ranking engine
├── dcc-mcp-gateway/        # Multi-DCC gateway app + dynamic wrappers
├── dcc-mcp-gateway-ensure/ # Shared gateway health check, launch lock, spawn, process utilities
├── dcc-mcp-http-types/     # Pure HTTP wire/config/value types, McpHttpConfig
├── dcc-mcp-http-server/    # Reusable HTTP runtime support
├── dcc-mcp-http-py/        # PyO3 binding boundary for HTTP APIs
├── dcc-mcp-http/           # Embedded MCP HTTP facade + compatibility re-exports
├── dcc-mcp-server/         # Binary entry point and CLI wrapper
├── dcc-mcp-sidecar/        # Per-DCC sidecar + gateway-daemon runtime
├── dcc-mcp-logging/        # File logging + retention helpers
├── dcc-mcp-paths/          # Platform cache/config/data path helpers
├── dcc-mcp-pybridge*/      # PyO3 helper macros and derive crate
├── dcc-mcp-transport/      # IPC transport, frames, channel adapters, FileRegistry
├── dcc-mcp-process/        # Launch, monitor, watcher, crash recovery
├── dcc-mcp-telemetry/      # Metrics, recorders, optional Prometheus exporter
├── dcc-mcp-updater/        # Gateway-controlled binary update (check, download, staged apply)
├── dcc-mcp-sandbox/        # SandboxPolicy, validation, audit log
├── dcc-mcp-shm/            # Shared-memory buffers
├── dcc-mcp-capture/        # Screen/window capture
├── dcc-mcp-usd/            # USD stage bridge
├── dcc-mcp-workflow/       # WorkflowCatalog and YAML workflows
├── dcc-mcp-scheduler/      # Cron/webhook scheduler service
├── dcc-mcp-artefact/       # FileRef and content-addressed handoff
├── dcc-mcp-host/           # Host execution bridge / adapter contracts
├── dcc-mcp-tunnel-*/       # Tunnel protocol, relay, and local agent
├── dcc-mcp-catalog/        # Public adapter catalog search/describe
└── workspace-hack/         # Cargo-hakari feature unification
```

Pure-Python modules: `cancellation`, `checkpoint`, `constants` (#487), `result_envelope` (#487), `_server/{observability,skill_query,window_resolver}` (#486), `_tool_registration` (#481), `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`, `install_lifecycle` (#1101), `skill`

## 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 `ToolMeta` 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
- `scan_and_load_strict(dcc_name=None, extra_paths=None)` — fail-fast variant; raises `ValueError` listing every skipped directory (use for CI/validation; issue maya#138)
- `scan_and_load_team(dcc_name=None, extra_paths=None) -> tuple[List[SkillMetadata], List[str]]`
- `scan_and_load_team_lenient(dcc_name=None, extra_paths=None) -> tuple[List[SkillMetadata], List[str]]`
- `scan_and_load_user(dcc_name=None, extra_paths=None) -> tuple[List[SkillMetadata], List[str]]`
- `scan_and_load_user_lenient(dcc_name=None, extra_paths=None) -> tuple[List[SkillMetadata], List[str]]`
- `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
- `skill_error(message, error, prompt=None, possible_solutions=None, **context) -> dict` — zero-boilerplate error result for skill scripts
- `skill_error_with_trace(message, error=None, prompt=None, **context) -> dict` — same as `skill_error` but includes traceback
- `skill_warning(message, prompt=None, **context) -> dict` — warning-level result
- `skill_exception(message, exc_info=None, prompt=None, **context) -> dict` — exception-style result
- `run_main(skill_dir, params=None) -> dict` — convenience entry point for standalone skill script execution
- `SkillFeedback` — feedback data model
- `get_skill_feedback(skill_name) -> List[SkillFeedback]`
- `record_skill_feedback(skill_name, feedback)`
- `SkillVersionEntry`, `SkillVersionManifest`
- `get_skill_version_manifest(skill_dir) -> SkillVersionManifest`

**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`, `.runtime` (optional aggregate `SkillRuntimeSummary`)

**SkillMetadata fields:** `.name`, `.description`, `.search_hint`, `.dcc`, `.version`, `.tags`, `.tools`, `.scripts` (List[str] absolute paths), `.skill_path`, `.depends`, `.metadata_files`, `.groups` (List[SkillGroup]), `.runtimes` (List[SkillRuntimeDescriptor]), `.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 the sibling file referenced by `metadata.dcc-mcp.groups` (usually `groups.yaml` or `tools.yaml`) 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**: `ToolMeta` 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__<group>` stubs for inactive groups)

**Action naming**: `{skill_name}__{script_stem}` — hyphens in skill names replaced by underscores. Published MCP tool names must match `^[A-Za-z0-9_-]{1,64}$`; see `docs/guide/naming.md` for validation rules (`validate_tool_name`, `validate_action_id`).

**`next-tools` (dcc-mcp-core extension):** Declared per-tool in the sibling `tools.yaml` file referenced by `metadata.dcc-mcp.tools` to guide AI agents to follow-up tools:
```yaml
# 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; successful calls expose `CallToolResult._meta["dcc.next_tools"].on_success`, failed calls expose `.on_failure`

**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) read gateway diagnostics via `gateway://diagnostics/*` resources or per-DCC diagnostics tools (`dcc_diagnostics__screenshot` etc.), (4) Direct registry access (last resort). Skills-first provides safety, discoverability, chainability, progressive exposure, and input validation.

**Adapter skill authoring**: Use `skills/dcc-mcp-skills-creator/` to scaffold and validate skill packages and for agent-facing authoring guidance before building or modernizing DCC adapter skills; it summarizes patterns from dcc-mcp-maya, dcc-mcp-blender, and dcc-mcp-3dsmax, including stage taxonomy, execution/affinity metadata, host matrix, and testing/VRS choices.

### MCP Protocol Types (`dcc_mcp_core`)

- `ToolDefinition(name, description, input_schema, output_schema=None, annotations=None)`
- `ToolDeclaration(name, description, input_schema, annotations=None, requires_in_process=False)` — lightweight tool declaration; stateful tools set `requires_in_process=True` so loading fails instead of falling back to per-call subprocesses
- `ToolSpec(name, description, input_schema, output_schema=None, annotations=None)` — full tool specification for registry use
- `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=0, server_name=None, server_version=None, enable_cors=False, request_timeout_ms=30000)` — server configuration; `0` delegates instance-port allocation to the OS
  - `.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` — default `9765` in Python; first process to bind wins the single gateway/admin, `0` disables gateway/admin
  - `.admin_enabled` / `.admin_path` — default `True` / `"/admin"`; disable manually with `cfg.admin_enabled = False`
  - `.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 the client-safe full form (`<skill>__<action>`).
  - `.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, #730, #732, #1205):
- Python: `McpHttpServer.resources()` and `DccServerBase.resources()` return `ResourceHandle`; use `.set_scene(dict|None)`, `.notify_updated(uri)`, `.register_output_buffer(OutputCapture)`, `.register_producer(scheme_or_uri, callable)`. `DccServerBase` also exposes `register_resource_producer(...)`, `set_scene_resource(...)`, and `notify_resource_updated(...)` helpers for adapters.
- Rust: `server.resources()` returns `ResourceRegistry`; use `set_scene`, `wire_audit_log`, and `add_producer(Arc<dyn ResourceProducer>)`.
- Static skill resources: declare `metadata.dcc-mcp.resources` pointing to `resources/*.resource.yaml` with `source.type: file`.
- Gateway: `resources/list` returns gateway-owned management URIs (`dcc://<type>/<id>`) and forwarded backend resource URIs (`<scheme>://<id8>/<rest>`); always pass the exact returned URI to `resources/read` / `resources/subscribe`.
- 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, environment=None, working_directory=None) -> dict` (dict: `pid`, `name`, `status`); environment/cwd overrides apply only to the child. Also exposes `.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`, `WindowsGraphicsCapture`, `ScreenCaptureKit`, `X11Xshm`, `PipeWire`, `HwndPrintWindow`, `Mock`; window capture reports the backend that actually produced the latest frame when WGC falls back to GDI
- `WindowFinder()` — `.find(target) -> Optional[WindowInfo]`, `.enumerate() -> List[WindowInfo]`
- `WindowInfo` — `.handle`, `.pid`, `.title`, `.rect`

### Scoped Computer Use (`dcc_mcp_core`, Windows)

- `ComputerUseSession(process_id=None, window_handle=None, window_title=None, app_name=None)` — persistent exact-window native input session with visible border/banner, pointer effects, and a Esc stop latch shared across DCC adapter processes in the Windows logon session
- `.start() -> str`, `.screenshot() -> tuple[str, bytes|None]`, `.act(request_json) -> str`, `.status() -> str`, `.stop() -> str`
- `.resume_after_user_approval() -> str` clears the Esc latch only after an explicit user instruction and only when the named input owner is idle; the historically named `ComputerUseSession.process_user_interrupted() -> bool` inspects the shared Windows logon-session latch
- `UiPoint(x, y)` and `UiActionRequest(..., path=[UiPoint(...)])` use coordinates relative to the latest (possibly scaled) screenshot
- Prefer bundled `ui-control`: structured DCC tool first, then semantic UIA, then one screenshot-coordinate action. `ui-control` requires a persistent in-process executor and must end with `ui_control__stop_computer_use`.
- Windows lock, disconnect, or secure desktop returns `desktop_unavailable` before UIA/raw input. Keep the logical session; structured DCC/MCP work may continue subject to host readiness. Never automate LockApp. After unlock, discard old ids and take a fresh snapshot so the banner returns.
- A normal process cannot show the banner on the Windows lock screen. Use a dedicated always-unlocked VM for uninterrupted GUI control. Public `openai/codex` `codex-rs` exposes policy/plugin wiring, not its desktop helper implementation.
- Computer Use runs on the adapter host in the interactive Windows logon session that owns the DCC; the gateway only routes calls. Never reuse coordinates across hosts or sessions. RDP disconnect/session switch retains the logical session but requires reconnect plus a fresh snapshot.
- Multi-monitor observations remain bounded to the scoped target window; that window may use a negative virtual-desktop origin or span monitors with different DPI values. Topology, resolution, or DPI/scaling changes invalidate the observation. The banner follows the scoped target in its owning session.

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

- `DccServerOptions.from_env(..., dcc_pid=..., dcc_window_title=..., dcc_window_handle=...)` + `DccServerBase(opts)` — bind an adapter to a specific DCC instance; the `dcc_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, gateway_failover_resolver=None)` — register `dcc_diagnostics__screenshot`, `dcc_diagnostics__audit_log`, `dcc_diagnostics__tool_metrics`, `dcc_diagnostics__process_status`, `dcc_diagnostics__gateway_failover` on an `McpHttpServer` before `.start()`. `DccServerBase` auto-wires `gateway_failover_resolver=self.get_gateway_election_status` so the failover tool returns `{enabled, running, consecutive_failures, gateway_host, gateway_port, is_gateway, reason}` with `reason` in `{failover_disabled_by_adapter, gateway_port_not_configured, election_thread_not_started, election_active, active_gateway, failover_resolver_not_registered}` (#1355)
- 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

### Output Capture (`dcc_mcp_core`)

- `OutputCapture` — capture DCC stdout/stderr/script-editor output as `output://` MCP resource (issue #461)

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

### Artefacts (`dcc_mcp_core`)

- `FileRef(path, mime_type=None, name=None, metadata=None)` — reference to a file for cross-tool hand-off
- `artefact_put_file(path, name=None, metadata=None) -> FileRef`
- `artefact_put_bytes(data, name, mime_type="application/octet-stream", metadata=None) -> FileRef`
- `artefact_get_bytes(ref) -> bytes`
- `artefact_list() -> list[FileRef]`

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

- `CancellationProbe` (`Protocol`, runtime-checkable) — read-only contract exposed to skill code: `.cancelled -> bool`, `.job_id -> str | None`
- `CancelToken(job_id=None)` — mutable pure-Python implementation for dispatchers; `.cancel()`, `.cancelled -> bool`, `.job_id -> str | None`
- `CancelledError(Exception)` — raised by `check_cancelled()` / `check_dcc_cancelled()` when cancellation was signalled
- `check_cancelled() -> None` — raise `CancelledError` if the active MCP request was cancelled (no-op outside request context)
- `check_dcc_cancelled() -> None` (#522) — combines `check_cancelled()` with a per-job check; raises if either the MCP token OR the per-job `JobHandle` reports cancellation; use this in skills launched from a DCC dispatcher rather than an MCP request
- `JobHandle` (`Protocol`, runtime-checkable) — contract for the per-job handle a host dispatcher publishes; only `cancelled: bool` is contractual
- `current_job: ContextVar[JobHandle | None]` — read-only contextvar (default `None`) holding the active per-job handle
- `set_current_job(job) -> contextvars.Token` — install a `JobHandle` (dispatcher use only)
- `reset_current_job(reset) -> None` — restore previous job handle (pair with `set_current_job`)
- `set_cancel_token(token) -> contextvars.Token` — install a `CancellationProbe` (dispatcher use only)
- `reset_cancel_token(reset) -> None` — restore previous token (pair with set_cancel_token)
- `current_cancel_token() -> CancellationProbe | None` — return the active read-only probe
- `current_job_id() -> str | None` — return the authoritative server-owned async job id; synchronous/out-of-context calls return `None`

### 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
- `get_recipes_paths(metadata) -> list[str]` — extract filename/glob/list recipe sibling files
- `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
- `load_recipe_pack(path) -> list[RecipeDefinition]` — load YAML `recipes:` packs with inputs schema, steps, output contract, and provenance (#616)
- `list_recipe_entries(skill_metadata) -> list[dict]` / `find_recipe_entry(skill_metadata, name)` — merge Markdown anchors and structured recipe pack entries
- `validate_recipe_inputs(recipe, inputs) -> list[str]` — zero-dep required/type validation for recipe input schemas
- `register_recipes_tools(server, *, skills, dcc_name="dcc")` — register `recipes__list/search/get/validate/apply` tools
- `register_metadata_driven_tools(server, *, skills=None, extra_paths=None, registrations=None, dcc_name="dcc")` — one helper for optional metadata-driven tools; defaults to recipes + skill reference docs and returns `MetadataRegistrationReport`

### 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=None, 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; `None` resolves an explicit DCC override or uses an OS-assigned port
- `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`, `Team`, `System`, `Admin` (ascending trust; higher shadows lower). Python exports this enum for introspection; scope still comes from the discovery path and is surfaced via `SkillMetadata` / `SkillSummary` fields.
  - `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 under `metadata.dcc-mcp.allow-implicit-invocation` / `metadata.dcc-mcp.products`. **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`
- `get_team_skill_paths_from_env() -> List[str]` — reads `DCC_MCP_TEAM_SKILL_PATHS`
- `get_user_skill_paths_from_env() -> List[str]` — reads `DCC_MCP_USER_SKILL_PATHS`
- `get_app_team_skill_paths_from_env(app_name) -> List[str]` — reads `DCC_MCP_{APP}_TEAM_SKILL_PATHS`
- `get_app_user_skill_paths_from_env(app_name) -> List[str]` — reads `DCC_MCP_{APP}_USER_SKILL_PATHS`
- `get_team_skills_dir() -> str` — team-level skills directory
- `get_user_skills_dir() -> str` — user-level skills directory
- `get_bundled_skills_dir() -> str` — bundled skills directory shipped with the package
- `get_bundled_skill_paths() -> List[str]` — list containing the bundled skills directory when it exists
- `copy_skill_to_team_dir(skill_dir) -> str` — copy a skill to the team directory
- `copy_skill_to_user_dir(skill_dir) -> str` — copy a skill to the user directory

### Adapter Install Lifecycle (`dcc_mcp_core.install_lifecycle`)

- Import-light helpers for adapter installers/uninstallers that must not load `dcc_mcp_core._core` from an install root they may remove.
- Runtime vocabulary: a DCC startup hook runs inside the host and must only launch/prepare; a per-DCC service is one registered runtime row per concrete DCC instance; a sidecar is the Rust `dcc-mcp-sidecar` child launched via `dcc-mcp-server sidecar`; the gateway daemon is the one machine-wide routing/Admin process; a guardian is the live service loop that probes gateway `/health` and re-ensures the daemon; service heartbeat only keeps registry rows fresh and is not the gateway restart trigger.
- `query_runtime_state(registry_dir=None, dcc_type=None, role=None, install_root=None, include_dead=True)` reads `services.json` with stdlib only; sidecar rows expose top-level `dispatch_status`, `dispatch_ready`, `host_rpc_uri`, `host_rpc_scheme`, `failure_stage`, and `failure_reason` for compatibility plus a nested `dispatch` object (`reported`, `status`, `ready`, `ready_at_unix`, `host_rpc_uri`, `host_rpc_scheme`, `failure_stage`, `failure_reason`) for new callers. Daemon-backed sidecars and Python `DccServerBase` adapters publish `gateway_runtime_mode`, `gateway_guardian_enabled`, `gateway_recovery_driver`, and `registration_refresh_mode`; Gateway Admin mirrors the worker fields on `GET /admin/api/workers`; `gateway://instances`, `GET /v1/instances`, and `/admin/api/instances` expose nested `dispatch` and `gateway` objects with the same readiness, host-RPC, failure, gateway recovery, and registration-refresh metadata. Gateway `GET /v1/readyz` also includes per-instance `dispatch` / `gateway`, `dispatch_reported_instance_count`, `dispatch_ready_instance_count`, `dispatch_not_ready_instance_count`, `gateway_recovery_driver_counts`, `registration_refresh_mode_counts`, `gateway_daemon_guardian_instance_count`, and `gateway_daemon_guardian_ready` so launchers can distinguish listed processes from callable sidecar dispatchers and check whether at least one live DCC service can restart the daemon.
- `sidecar_readiness_status(...)` returns a one-shot sidecar dispatch verdict (`ready`, `missing`, `booting`, `unavailable`, `ambiguous`, or `dead`); `wait_for_sidecar_ready(...)` polls the same verdict for bounded installer/supervisor checks without importing native core. When `instance_id` and/or `host_rpc` selectors match multiple live sidecar rows, the verdict is `ambiguous` rather than choosing an arbitrary instance; direct-use startup checks should pass the full `readiness_selector` from `build_sidecar_command(...)`, while `dcc_type`-only checks are aggregate diagnostics.
- Pass `probe_tool="maya_diagnostics__ping"` (or CLI `--probe-tool`) to require one real `tools/call` through the sidecar before reporting ready; optional `probe_arguments` / `--probe-args-json` must be a JSON object. Probe failures keep polling until timeout in `wait_for_sidecar_ready(...)`.
- `stop_runtime_entries(..., role="per-dcc-sidecar", include_host_processes=False)` requests sidecar stop without terminating the owning DCC host by default.
- `build_sidecar_command(...)` returns a canonical `dcc-mcp-server sidecar` argv + environment updates plus `readiness_selector`, `readiness_argv`, `readiness_command`, `dispatch_contract`, and `readiness_contract` for DCC startup hooks; `readiness_command` honours `DCC_MCP_PYTHON_EXECUTABLE` when set. `dispatch_contract` classifies the host RPC scheme, URI shape (`uri_valid` / `validation_error`), and `dispatch_ready_capable`; `readiness_contract.ready_on_launch` is always `false` because spawning/registering a sidecar is not a proof that DCC tools are callable. Pass `require_dispatch_capable=True` or CLI `--require-dispatch-capable` for installer/startup code that must fail fast on malformed real host RPC URIs, `stub://`, or unsupported schemes before claiming "open DCC, directly usable".
- `launch_sidecar(...)` starts that sidecar with detached stdio by default so the DCC main process is not blocked. Without `wait_ready_timeout_secs`, the result intentionally carries `ready=false`, `readiness_checked=false`, and `readiness.status="not_checked"` (or `"dispatch_not_capable"` for diagnostics-only host RPC schemes). Pass `wait_ready_timeout_secs` only from an installer, supervisor, or background startup task to replace that with a bounded `readiness` verdict, and pass `extra_args` only for deliberate sidecar flags not modeled by the helper. Treat the registry row as dispatch-ready only after `metadata.dispatch_status=ready` and `metadata.mcp_url` appear. Host RPC failures stay visible with `metadata.dispatch_status=unavailable` plus `failure_stage` / `failure_reason`; they may still publish `metadata.mcp_url` for diagnostics, but `/v1/readyz` reports `dispatcher=false` and `tools/call` returns a structured transport error until dispatch becomes ready. For supported real host RPC schemes (`commandport://`, `qtserver://`, `ws://` / `wss://`), the sidecar keeps retrying while the parent DCC process is alive and promotes the same row to `dispatch_status=ready` once the bridge accepts a connection. `stub://` is test-only and remains unavailable by default even when it connects, so adapter startup checks must use a real host RPC scheme.
- `inspect_install_root(path)` reports loaded native artifacts under the install root as `status="requires_restart"` with `locked_path`.
- `safe_remove_tree(path)` / `safe_replace_tree(source, destination)` attempt immediate cleanup and classify Windows native-file lock failures into a deferred operation.
- CLI: `python -m dcc_mcp_core.install_lifecycle {query|stop|layout|sidecar-command|launch-sidecar|sidecar-ready|plan-update|inspect|remove|replace}`.

### Scheduler (`dcc_mcp_core`)

- `ScheduleSpec` — scheduling specification for recurring or delayed tasks
- `TriggerSpec` — trigger definition for event-driven scheduling
- `parse_schedules_yaml(path) -> List[ScheduleSpec]` — load and parse schedule definitions from YAML

### 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 = "DCC_MCP_LOG_LEVEL"`, `ENV_DISABLE_ACCUMULATED_SKILLS` — set to "1" to disable accumulated/evolved skills discovery, `ENV_DISABLE_DEFAULT_SKILL_PATHS` — set to "1" for hermetic discovery without implicit operator-owned roots, `ENV_TEAM_SKILL_PATHS` / `ENV_USER_SKILL_PATHS` — team-level and user-level skill path env vars
- `TOOL_NAME_RE` / `ACTION_ID_RE` — regex patterns for client-safe tool names and internal action IDs
- `MAX_TOOL_NAME_LEN = 64` — maximum tool name length

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

- `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)
- `load_text(path, max_bytes=None) -> str` / `dump_text(path, text) -> Path` — explicit UTF-8 file helpers
- `load_json_file(path, require_mapping=False, max_bytes=None) -> obj` / `dump_json_file(path, obj, ensure_ascii=True, indent=2) -> Path`
- `load_yaml_file(path, require_mapping=False, max_bytes=None) -> obj` / `dump_yaml_file(path, obj) -> Path`
- `SkillCodecError` includes codec and source context for actionable skill errors
- Existing `from dcc_mcp_core import json_dumps` style imports are compatibility re-exports of `dcc_mcp_core.skills_helper`.

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

### Naming Validators (`dcc_mcp_core`)

- `validate_tool_name(name: str) -> None` — raise `ValueError` if name doesn't match `TOOL_NAME_RE` (`^[A-Za-z0-9_-]{1,64}$`)
- `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. Keep top-level frontmatter limited to agentskills.io fields; put every dcc-mcp-core extension under `metadata.dcc-mcp.*` and point large payloads to sibling files.

```yaml
---
name: maya-geometry
description: >-
  Maya geometry creation tools. Use when creating or modifying polygon meshes.
allowed-tools: Bash Read  # space-separated (agentskills.io spec), NOT a YAML list
license: MIT              # optional (agentskills.io spec)
compatibility: "Maya 2024+" # optional (agentskills.io spec) — environment requirements
metadata:
  dcc-mcp:
    dcc: maya
    version: "1.0.0"
    tags: ["maya", "geometry"]
    search-hint: "polygon modeling, bevel, extrude"
    tools: tools.yaml
    groups: groups.yaml
    runtimes:
      - name: usd-core
        type: python_package
        package: usd-core
        module: pxr
        optional: true
        feature_level: full-usd
        install_hint: "pip install dcc-mcp-openusd[usd-core]"
      - name: usdcat
        type: binary
        binary: usdcat
        optional: true
    external-deps: external_deps.yaml
---
# 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**: `metadata.dcc-mcp.dcc`, `.version`, `.tags`, `.search-hint`, `.tools`, `.groups`, `.depends`, `.runtimes`, `.external-deps`, `.resources`, `.workflows`, `.prompts`. Runtime descriptors can be inline or in sibling `runtimes.yaml` and safely surface `available` / `degraded` / `missing` state for optional Python packages/extras, binaries, env vars, and feature levels during search/detail without executing skill scripts. Legacy top-level extension keys such as `dcc:`, `tools:`, `groups:`, `depends:`, `search-hint:`, or `external_deps:` are rejected by the strict loader.

### 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, client-safe 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"])

# HMAC utilities for webhook / hub signature verification
signature = hmac_sha256_hex(secret, payload)
ok = verify_hub_signature_256(secret, payload, signature)
```

### 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: **STUB** — `elicit_form` / `elicit_url` / `elicit_form_sync` currently return `accepted=False, message="elicitation_not_supported"` because the Rust transport does not yet wire `notifications/elicitation/request`. You can write handlers today (they upgrade automatically when transport lands), but do NOT rely on elicitation for critical control flow. Design fallback paths.

### 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/dcc-mcp/dcc-mcp-core/blob/main/README.md)
- [README (中文)](https://github.com/dcc-mcp/dcc-mcp-core/blob/main/README_zh.md)
- [AI Agent Guide (AGENTS.md)](https://github.com/dcc-mcp/dcc-mcp-core/blob/main/AGENTS.md)
- [Claude Guide (CLAUDE.md)](https://github.com/dcc-mcp/dcc-mcp-core/blob/main/CLAUDE.md)
- [Gemini Guide (GEMINI.md)](https://github.com/dcc-mcp/dcc-mcp-core/blob/main/GEMINI.md)
- [CodeBuddy Guide (CODEBUDDY.md)](https://github.com/dcc-mcp/dcc-mcp-core/blob/main/CODEBUDDY.md)
- [Contributing Guide](https://github.com/dcc-mcp/dcc-mcp-core/blob/main/CONTRIBUTING.md)
- [Changelog](https://github.com/dcc-mcp/dcc-mcp-core/blob/main/CHANGELOG.md)
- [Full API Reference](https://github.com/dcc-mcp/dcc-mcp-core/blob/main/llms-full.txt)
- [VitePress Docs](https://dcc-mcp.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-maya](https://github.com/dcc-mcp/dcc-mcp-maya) — Maya MCP server implementation

---

## New Gateway Features (issues #764–#774)

### Quick Decision — New APIs

| I want to… | Use this |
|------------|---------|
| Bridge stdio MCP server to HTTP/SSE | `dcc-mcp-server translate --stdio "cmd" --port N --app-type external` — Streamable HTTP `/mcp` is on by default; registered bridges ensure the standalone gateway daemon and keep a daemon guardian unless `--no-register`; add `--expose-sse true` only for legacy `/sse` clients |
| Search DCC-MCP adapter catalog | MCP `resources/read uri=gateway://catalog?query=...` (or `gateway://catalog/{name}` for a single entry); CLI: `dcc-mcp-server catalog search` |
| Mount OpenAPI REST API as MCP tools | `GatewayBuilder::mount_openapi(OpenApiMount::from_url(...).auth(...))` |
| Add audit/quota/redact to gateway calls | `GatewayConfig::middleware_chain` with built-in middleware |
| Gateway admin dashboard | Default ON for the elected gateway: `GET /admin`; disable with `--no-admin`, `DCC_MCP_NO_ADMIN=true`, or `cfg.admin_enabled = False`; agent panel map: `docs/guide/admin-ui.md#agent-facing-panel-map` |
| Enforce payload size + SSE chunking | `McpHttpConfig.queue.max_request_body_bytes` (default 4 MiB) |
| Wire OTLP distributed tracing | Set `OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317` at startup |
| Watch gateway contention events | MCP resource `resources://gateway/events` (JSONL ring buffer) |

### Middleware Chain (#770)

```rust
let config = GatewayConfig {
    middleware_chain: MiddlewareChain::new()
        .with_before(Arc::new(AuditMiddleware::default()))
        .with_before(Arc::new(QuotaMiddleware::new(100)))  // 100 calls/min
        .with_before(Arc::new(RedactionMiddleware::new(["api_key"]))),
    ..GatewayConfig::default()
};
```

Built-in middleware: `AuditMiddleware` (structured tracing log), `QuotaMiddleware` (rate limit → 429), `RedactionMiddleware` (field scrubbing).
Custom: implement `BeforeCallMiddleware` or `AfterCallMiddleware` traits. `CallContext` fields: `method`, `tool_slug`, `dcc_type`, `session_id`, `request_id`, `args`, `metadata`.

### OpenAPI → MCP Bridge (#773)

```rust
gateway_builder.mount_openapi(
    OpenApiMount::from_url("https://api.example.com/openapi.json")
        .base_url("https://api.example.com")
        .auth(AuthConfig::bearer("$MY_API_TOKEN"))
        .tool_prefix("example"),
)
```

One config block exposes all OpenAPI paths as MCP tools with preserved auth + schemas. Auth types: `bearer()`, `api_key()`, `basic()`. `$ENV_VAR` references resolved at call time.

### stdio → HTTP Bridge (#769)

```bash
dcc-mcp-server translate \
  --stdio "npx @modelcontextprotocol/server-filesystem /tmp" \
  --app-type filesystem --port 3333
```

Default endpoint: `http://127.0.0.1:3333/mcp` (Streamable HTTP). Add `--expose-sse true` to also serve legacy `/sse`.
By default, the bridge ensures the standalone gateway daemon, registers as a backend, and keeps a guardian so a surviving bridge can re-ensure the daemon after `/health` fails. Flags: `--no-register` (standalone), `--restart-on-exit false` (disable supervisor), `--max-restarts N` (default 10).

### Admin Dashboard (#772)

```rust
GatewayConfig { admin_enabled: true, admin_path: "/admin".into(), .. }
// requires: dcc-mcp-gateway = { features = ["admin"] }
```

Routes: `GET /admin` (HTML dashboard), `GET /admin/api/{instances,tools,calls,traces,traces/{request_id},stats,workers,logs,health}` (JSON APIs).
Default: enabled on the elected gateway. Disable with `--no-admin`, `DCC_MCP_NO_ADMIN=true`, or `cfg.admin_enabled = False`; `gateway_port=0` disables the gateway and therefore admin too.

### OTLP Distributed Tracing (#768)

```bash
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 dcc-mcp-server ...
```

Standard env vars: `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_SERVICE_NAME`, `OTEL_RESOURCE_ATTRIBUTES`, `OTEL_EXPORTER_OTLP_HEADERS`.
DCC span attrs: `dcc.type`, `dcc.instance_id`, `dcc.scene`, `mcp.method`, `mcp.tool_slug`, `mcp.session_id`, `mcp.request_id`, `dcc.job_id`.

### DCC-MCP Catalog (#774)

```bash
dcc-mcp-server catalog search --query maya
dcc-mcp-server catalog describe --name dcc-mcp-maya-skills
```

MCP resources (#813 phase 2): `resources/read uri=gateway://catalog?query=...`, `resources/read uri=gateway://catalog/{name}`.
Gateway-native agent guide: `resources/read uri=gateway://docs/agent-workflows` (MCP + resources + prompts + describe_tool hints + efficiency; JSON envelope with markdown `document` field).
Override catalog path: `DCC_MCP_CATALOG_PATH=/path/to/dcc-mcp-catalog.yml`.

### Gateway Observability (#766)

Prometheus counters (under `prometheus` Cargo feature):
- `dcc_mcp_gateway_elections_total{outcome="won|yielded|lost"}`
- `dcc_mcp_gateway_evictions_total{reason="stale|ghost|probe_fail"}`
- `dcc_mcp_gateway_probes_total{outcome="ready|booting|unreachable"}`

MCP resource `resources://gateway/events`: JSONL ring buffer, last 1000 contention events.

### Payload Limits (#771)

| Config field | Default | Effect |
|-------------|---------|--------|
| `McpHttpConfig.queue.max_request_body_bytes` | 4 MiB | 413 Payload Too Large |
| `McpHttpConfig.queue.max_response_content_bytes` | 1 MiB | `TruncationEnvelope` (`truncated`, `original_size`, `truncated_size`) |
| `McpHttpConfig.queue.sse_chunk_size_bytes` | 64 KiB | Automatic SSE chunking |

### McpHttpConfig Sub-configs and HTTP type crates (#764 / #852)

`McpHttpConfig` is a thin aggregate in `dcc-mcp-http-types::config`. Access cohesive sub-configs through public fields: `.server`, `.instance`, `.session`, `.gateway`, `.queue`, `.telemetry`, `.features`, `.workflow`, and `.job`; compatibility accessors/builders remain available. Pure wire/config/value types that no longer need axum/PyO3 live in `dcc-mcp-http-types` and are re-exported from `dcc-mcp-http` for compatibility; reusable runtime support such as core tool builders, sessions, executors, in-flight request state, notifications, and workspace roots lives in `dcc-mcp-http-server`; the PyO3 HTTP binding boundary lives in `dcc-mcp-http-py`.
