# dcc-mcp-core — Full API Reference

> This document provides comprehensive API documentation for AI agents and downstream projects
> that depend on dcc-mcp-core. For a concise overview, see [llms.txt](llms.txt).
>
> **For AI agents**: Start with the [Quick Decision Guide](#quick-decision-guide) to pick the right API for your task.

---

## Quick Decision Guide

| I want to… | Use this API |
|-------------|--------------|
| Register a script/tool for AI to call | `ToolRegistry.register()` |
| Derive `inputSchema` / `outputSchema` from a typed Python handler (issue #242) | `tool_spec_from_callable(handler)` — zero-dep, reads `@dataclass` / `TypedDict` / `typing` annotations, refuses untyped handlers |
| Return a result from a DCC action | `success_result()` / `error_result()` |
| Auto-discover skill packages from directories | `scan_and_load()` / `SkillScanner` |
| Watch a skills directory for live updates | `SkillWatcher` |
| Validate action input parameters | `ToolValidator` / `InputValidator` |
| Route action calls to Python handlers | `ToolDispatcher` |
| Define an MCP Tool for the LLM | `ToolDefinition` + `ToolAnnotations` |
| Connect to a running DCC process | `IpcChannelAdapter.connect(name)` / `GracefulIpcChannelAdapter.connect(name)` |
| Launch a new DCC process | `PyDccLauncher` |
| Monitor DCC process health | `PyProcessWatcher` / `PyProcessMonitor` |
| Enforce security policy on actions | `SandboxPolicy` + `SandboxContext` |
| Track performance metrics | `ToolRecorder` + `ToolMetrics` |
| Share large data between processes | `PySharedSceneBuffer` (LZ4 compressed) |
| Capture a DCC viewport screenshot | `Capturer.new_auto().capture()` (full-screen) / `Capturer.new_window_auto().capture_window(process_id=pid)` (single window) |
| Bind diagnostics MCP tools to a specific DCC instance | `DccServerOptions.from_env(..., dcc_pid=pid, dcc_window_title=title)` → `DccServerBase(opts)` |
| Expose a group of tools only when asked | Declare `metadata.dcc-mcp.groups` pointing to sibling `groups.yaml` / `tools.yaml`, then use `activate_tool_group` / `deactivate_tool_group` |
| Assert a producer→file→verifier round-trip between two DCCs (#688) | `dcc_mcp_core.SceneStats(object_count, vertex_count, has_mesh, extra={})` + `SceneStats.matches(other, vertex_tolerance=0.05)`. DCC-specific verifier skills live in downstream repos. |
| Exchange scene data via USD format | `UsdStage` + `scene_info_json_to_stage()` |
| Bridge DCC protocols | `BridgeRegistry` + `register_bridge()` / `get_bridge_context()` |
| Describe scene hierarchy | `SceneNode` + `SceneObject` + `ObjectTransform` + `BoundingBox` |
| Serialize result for transport | `serialize_result()` / `deserialize_result()` with `SerializeFormat` |
| Wrap values for safe RPyC transport | `wrap_value()` / `unwrap_value()` |
| Handle multiple versions of an action | `VersionedRegistry` + `VersionConstraint` |
| Expose DCC tools over HTTP/MCP | `create_skill_server("maya", McpHttpConfig())` — Skills-First one-call setup with an OS-assigned instance port discoverable through the CLI/gateway; gateway/slim agents use `search_tools` → `describe_tool` → `call_tool` / `call_tools`; wrapper payloads are `{tool_slug, arguments?, meta?}` and backend-specific fields live inside `arguments`; falls back to `McpHttpServer(registry, config)` for manual registry wiring |
| Bind adapter readiness probe | `AdapterReadinessBinder.bind_inline(server)` / `.bind_headless(server)` / `.bind_queue_dispatcher(server, dispatcher)` — publishes `ReadinessProbe` gating MCP `tools/call`, REST `/v1/readyz`, and REST `/v1/call` (#1331, #1206) |
| Check DCC adapter readiness | `GET /v1/readyz` → 200 `ready` / 503 `booting` / 503 `not-ready` with per-bit details (process, dcc, catalog, dispatcher, host_bridge, main_thread_executor) (#1331) |
| Discover gateway DCC instances / direct MCP URLs (#813 phase 1) | `resources/read uri="gateway://instances"` or `gateway://instances/{id}` — entries include `mcp_url`; `resources/list` shows the root pointer only; legacy `list_dcc_instances` / `get_dcc_instance` / `connect_to_dcc` and non-standard `instances/list` are removed |
| Register MCP prompts from Python (issue #792) | `handle = server.prompts(); handle.register_prompt(name, template, description=..., arguments=[{"name": str, "description": str, "required": bool}])` — `server.prompts()` returns `PromptHandle`; registration upserts in-place and appears in the next `prompts/list` / `prompts/get` |
| Persist and resume DCC project state | `DccProject.open/load(...)` + `register_project_tools(server, ...)` exposing `project_save`, `project_load`, `project_resume`, and `project_status` |

| Share resources/prompts through a gateway | `resources/list/read/subscribe` with exact gateway-returned URIs; `prompts/list/get` for namespaced backend prompts |
| 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()` |
| 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()` |
| Drive a DCC main-thread dispatcher (interactive/headless) | `HostAdapter` (subclass for your DCC) + `StandaloneHost` (pure-Python driver) + `QueueDispatcher` / `BlockingDispatcher` (Rust-backed primitives). Use public `TickableDispatcher` only for structural typing (`tick`, `shutdown`, `is_shutdown`); do not import private host protocols. Wire via `McpHttpServer.attach_dispatcher()`. See [`docs/guide/host-adapter.md`](docs/guide/host-adapter.md) |
| 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 |
| Register lifecycle hooks | `LifecycleHooks()` + `.on(HookEvent.BEFORE_TOOL_CALL, handler)` + `server.register_lifecycle_hooks(hooks)` (#1337) |
| Veto a skill load / tool call via policy | Raise `HookDeny(reason, hint=...)` from a `BEFORE_*` handler; only works for policy events (#1337) |
| Observe lifecycle events | `HookEvent.AFTER_SEARCH` / `AFTER_SKILL_LOAD` / `AFTER_TOOL_CALL` / `SESSION_START` / `SESSION_END` (#1337) |
| Enable three-tier agent memory | `MemoryRecorder(InMemoryMemoryStore()).install(hooks)` — auto-records skill/tool events, injects summaries (#1334) |
| Query agent memory | `store.query(MemoryQuery(layer=MemoryLayer.WORKING, key_prefix="tool_call:"))` → sorted `tuple[MemoryEntry, ...]` (#1334) |
| Register all built-in MCP tools | `register_all_builtin_skills(server, dcc_name=..., skills=...)` — idempotent one-call registration (#1332) |
| Normalize MCP/REST call payloads | Rust: `dcc-mcp-wire::{normalize_arguments, normalize_meta, decode_call_tool, decode_rest_call}`; Python host wrappers: `dcc_mcp_core.host.normalize_tool_arguments()` / `normalize_tool_meta()` |

---

## Architecture

dcc-mcp-core is a **Rust workspace** with Python bindings via PyO3. All logic lives in focused Rust
sub-crates; the root crate re-exports everything into a single `dcc_mcp_core._core` Python
extension module. The Python package `dcc_mcp_core` re-exports the supported public APIs from `_core`
and pure-Python helpers.

**Rust workspace with package membership defined by the root `Cargo.toml`** — **Zero runtime Python dependencies** — install with just `pip install dcc-mcp-core`.

```
crates/
├── dcc-mcp-naming/       # client-safe tool-name / action-id validators
├── dcc-mcp-models/       # ToolResult, SkillMetadata, DccName, shared errors
├── dcc-mcp-actions/      # ToolRegistry, ToolDispatcher, ToolValidator, EventBus, ToolPipeline
├── dcc-mcp-skills/       # SkillScanner, SkillCatalog, SkillWatcher, parse_skill_md, dependency resolver
├── dcc-mcp-protocols/    # MCP-facing Tool/Resource/Prompt/DccAdapter models
├── dcc-mcp-jsonrpc/      # MCP 2025-03-26 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-ensure/ # Shared gateway health check, launch lock, spawn, process utilities
├── dcc-mcp-gateway/      # Multi-DCC gateway app + dynamic wrappers
├── dcc-mcp-http-types/   # Pure HTTP wire/config/value types
├── 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-transport/    # DccLink IPC adapters
├── dcc-mcp-process/      # PyDccLauncher, monitors, crash recovery
├── dcc-mcp-telemetry/    # TelemetryConfig, RecordingGuard, ToolMetrics, ToolRecorder
├── dcc-mcp-updater/      # Gateway-controlled binary update (check, download, staged apply)
├── dcc-mcp-sandbox/      # SandboxPolicy, SandboxContext, InputValidator, AuditLog
├── dcc-mcp-shm/          # Shared buffers and LZ4 compression
├── dcc-mcp-capture/      # Screen/window capture
├── dcc-mcp-usd/          # USD scene bridge
├── dcc-mcp-server/       # Binary entry point, gateway runner
├── dcc-mcp-logging/      # Rolling file logging
├── dcc-mcp-paths/        # Platform path helpers
├── dcc-mcp-pybridge*/    # PyO3 helper crates
├── dcc-mcp-workflow/     # WorkflowCatalog and YAML workflows
├── dcc-mcp-scheduler/    # ScheduleSpec, TriggerSpec, scheduler service
├── dcc-mcp-artefact/     # FileRef and content-addressed handoff
├── dcc-mcp-host/         # Host execution bridge contracts
├── dcc-mcp-tunnel-*/     # Remote MCP tunnel protocol, relay, and agent
├── dcc-mcp-catalog/      # Public adapter catalog search/describe
└── workspace-hack/       # Cargo-hakari feature unification
```

**Key import pattern** (always use the top-level package):
```python
from dcc_mcp_core import ToolRegistry, success_result, SkillScanner
# Never: from dcc_mcp_core._core import ...
```

---

## Models (`dcc_mcp_core`)

### ToolResult

Structured result for all action executions. AI-friendly: the `prompt` field carries next-step
hints for the LLM. Immutable snapshot — use `with_error()`/`with_context()` to derive.

**Constructor:**
```python
ToolResult(success=True, message="", prompt=None, error=None, context=None)
```

**Properties (read-only except `message`):**
- `success: bool` — Whether the execution was successful
- `message: str` — Human-readable result description (settable)
- `prompt: Optional[str]` — Suggestion for AI about next steps ← **use this for AI guidance**
- `error: Optional[str]` — Error message when success is False
- `context: dict` — Additional context data (returns a new dict each access)

**Methods:**
- `with_error(error: str) -> ToolResult` — Copy with error info (sets success=False)
- `with_context(**kwargs) -> ToolResult` — Copy with updated context
- `to_dict() -> dict` — Full dict representation
- `to_json() -> str` — JSON string representation
- `keys() -> list[str]` — Dict keys (mapping protocol)
- `__iter__()` — Iterate over keys (mapping protocol)

**Factory functions (preferred for creation):**
```python
from dcc_mcp_core import success_result, error_result, from_exception, validate_action_result

# success_result: positional message, optional prompt, remaining kwargs → context
result = success_result("Created 5 spheres", prompt="Use modify_spheres next", count=5)
# result.context == {"count": 5}

# error_result: message, error string, optional prompt+solutions
error = error_result("Failed", "File not found", prompt="Check path", possible_solutions=["fix A", "fix B"])

# from_exception: wrap an exception string (not the exception object itself)
try:
    risky_op()
except Exception as e:
    exc_result = from_exception(str(e), message="Import failed", include_traceback=True)

# validate_action_result: normalize any type to ToolResult
validated = validate_action_result({"success": True, "message": "ok"})  # dict → ToolResult
validated = validate_action_result("hello")  # wraps non-dict as success with context["value"]
validated = validate_action_result(None)     # returns a success result
```

### SkillMetadata

Metadata parsed from agentskills.io `SKILL.md` frontmatter plus sibling files referenced by `metadata.dcc-mcp.*`. All fields are get/set.

**Fields:**
- `name: str` — Unique identifier (used in action naming)
- `description: str` — Human-readable description (default: "")
- `tools: List[ToolDeclaration]` — MCP tool declarations (name, source_file, schemas); default: []
- `allowed_tools: List[str]` — Agent permission list, e.g. ["Bash", "Read"] (agentskills.io spec); default: []
- `license: str` — License identifier, e.g. "MIT" (agentskills.io spec); default: ""
- `compatibility: str` — Runtime compatibility string, e.g. "Python>=3.9" (agentskills.io spec); default: ""
- `dcc: str` — Target DCC application (default: "python")
- `tags: List[str]` — Classification tags (default: [])
- `scripts: List[str]` — Discovered script file paths (populated by loader; absolute paths)
- `skill_path: str` — Absolute path to skill directory (populated by loader)
- `version: str` — Skill version (default: "1.0.0")
- `depends: List[str]` — Names of other skills this skill requires (default: [])
- `metadata_files: List[str]` — Files discovered under metadata/ directory (default: [])
- `external_deps: Optional[str]` — External dependency declaration as JSON string (MCP servers, env vars, binaries). Set via `md.external_deps = json.dumps(deps)`, read via `json.loads(md.external_deps)`. `None` when not set. See `docs/guide/skill-scopes-policies.md` for the full schema.

**Action naming from SkillMetadata:**
```python
# Given: skill.name = "maya-geometry", script = "create_sphere.py"
# Action name = "maya_geometry__create_sphere"  (hyphens→underscores, double __ separator)
```

---

## Actions (`dcc_mcp_core`)

### ToolRegistry

Thread-safe tool registry using DashMap (Rust). Each instance is independent (no singleton).

```python
reg = dcc_mcp_core.ToolRegistry()

# Register — all params except name are optional
reg.register(
    name="create_sphere",
    description="Create a sphere",
    category="geometry",
    tags=["geo", "create"],
    dcc="maya",
    version="1.0.0",
    input_schema='{"type": "object", "required": ["radius"], "properties": {"radius": {"type": "number"}}}',
    output_schema='{"type": "object"}',
    source_file="/path/to/action.py",
)

# Query
meta = reg.get_action("create_sphere")                    # -> dict or None
meta = reg.get_action("create_sphere", dcc_name="maya")   # DCC-scoped lookup (preferred)
names = reg.list_actions_for_dcc("maya")                   # -> List[str]
all_actions = reg.list_actions()                           # -> List[dict]
all_actions = reg.list_actions(dcc_name="maya")            # -> List[dict] scoped
dccs = reg.get_all_dccs()                                  # -> List[str]

# Batch registration (v0.12.6+)
# More efficient than calling register() in a loop; silently skips entries without "name".
reg.register_batch([
    {"name": "create_sphere", "category": "geometry", "tags": ["create"], "dcc": "maya"},
    {"name": "delete_mesh",   "category": "edit",     "tags": ["delete"], "dcc": "maya"},
    {"name": "render_scene",  "category": "render",                       "dcc": "maya"},
])

# Unregister (v0.12.6+)
# Global: removes from the global registry AND all per-DCC maps.
removed = reg.unregister("create_sphere")                  # -> bool (True if found)
# Scoped: removes only from the specified DCC's map.
# The global entry is cleared only when no other DCC still references the action.
removed = reg.unregister("create_sphere", dcc_name="maya") # -> bool

# Search & discovery (v0.12.5+)
# search_actions: all filters are AND-ed; None / [] = no filter
geo_actions = reg.search_actions(category="geometry")               # by category
mesh_actions = reg.search_actions(tags=["mesh"])                    # by tag
create_geo  = reg.search_actions(category="geometry", tags=["create"], dcc_name="maya")
categories = reg.get_categories()                                   # sorted unique categories
tags       = reg.get_tags(dcc_name="maya")                          # sorted unique tags in maya

# Manage
reg.reset()    # clear all entries
len(reg)       # count of registered actions
repr(reg)      # "ToolRegistry(actions=N)"
```

**Returned dict structure from `get_action()` / `list_actions()`:**
```python
{
    "name": "create_sphere",
    "description": "Create a sphere",
    "category": "geometry",
    "tags": ["geo", "create"],
    "dcc": "maya",
    "version": "1.0.0",
    "input_schema": "{...}",   # JSON string
    "output_schema": "{...}",  # JSON string (empty if not set)
    "source_file": "/path/to/action.py" | None,
}
```

### ToolValidator

Validates JSON-encoded action parameters against a JSON Schema.

```python
import json
from dcc_mcp_core import ToolRegistry, ToolValidator

schema = json.dumps({
    "type": "object",
    "required": ["radius"],
    "properties": {"radius": {"type": "number", "minimum": 0.0}}
})
v = ToolValidator.from_schema_json(schema)
ok, errors = v.validate('{"radius": 1.0}')   # -> (True, [])
ok, errors = v.validate("{}")                  # -> (False, ["...missing required: radius"])

# Or build from registry:
v2 = ToolValidator.from_action_registry(reg, "create_sphere", dcc_name="maya")
```

### ToolDispatcher

Routes action calls to registered Python callables with automatic schema validation.

```python
import json
from dcc_mcp_core import ToolRegistry, ToolDispatcher

reg = ToolRegistry()
reg.register("create_sphere",
    input_schema=json.dumps({"type": "object", "required": ["radius"],
                              "properties": {"radius": {"type": "number", "minimum": 0.0}}}))

dispatcher = ToolDispatcher(reg)

def create_sphere_handler(params: dict):
    return {"created": True, "radius": params["radius"]}

dispatcher.register_handler("create_sphere", create_sphere_handler)
# Dispatch: validates params, calls handler, returns result dict
result = dispatcher.dispatch("create_sphere", json.dumps({"radius": 2.0}))
# result == {"action": "create_sphere", "output": {"created": True, "radius": 2.0},
#            "validation_skipped": False}

# Control validation behaviour
dispatcher.skip_empty_schema_validation = True  # skip validation if schema is "{}"

# Other methods
dispatcher.has_handler("create_sphere")  # -> bool
dispatcher.handler_count()               # -> int
dispatcher.handler_names()               # -> List[str] sorted
dispatcher.remove_handler("create_sphere")  # -> bool
```

#### Async `tools/call` dispatch (#318)

When a `tools/call` opts into async dispatch, the MCP HTTP server skips the
blocking dispatch path and returns a pending envelope immediately. Opt-in
triggers (ANY of):

1. Client sends `_meta.dcc.async = true` on the request.
2. Client sends `_meta.progressToken` (MCP 2025-03-26 long-running hint).
3. The tool's `ToolMeta` declares `execution: async` (#317).
4. The tool's `ToolMeta` declares `timeout_hint_secs > 0` (#317).

Response shape (standard `CallToolResult`, spec-compliant):

```json
{
  "content": [{"type": "text", "text": "Job <uuid> queued"}],
  "structuredContent": {
    "job_id": "<uuid>",
    "status": "pending",
    "parent_job_id": "<uuid>|null",
    "_meta": {"status": "pending", "dcc": {"jobId": "<uuid>", "parentJobId": "..."}}
  },
  "isError": false
}
```

Poll `jobs_get_status` (#319) for progress / completion. Cancellation: send
`notifications/cancelled` with the request id, OR use `_meta.dcc.jobId` once
the job-scoped cancel endpoint ships (#319).

Parent-job cascade: pass `_meta.dcc.parentJobId` on a child `tools/call` to
link it to a parent job. The child's `CancellationToken` is derived via
`CancellationToken::child_token`, so cancelling the parent cancels every
descendant within one cooperative checkpoint.

### ToolPipeline

Middleware-style processing pipeline for actions. Wraps `ToolDispatcher` with composable
cross-cutting concerns (logging, timing, auditing, rate limiting, custom Python hooks).

Middleware runs in registration order for `before_dispatch`, in **reverse** for `after_dispatch`
(standard onion model).

```python
from dcc_mcp_core import (
    ToolRegistry, ToolDispatcher, ToolPipeline,
    LoggingMiddleware, TimingMiddleware, AuditMiddleware, RateLimitMiddleware,
)

# Build registry + dispatcher with handlers
reg = ToolRegistry()
reg.register("create_sphere", description="Create sphere", category="geometry")
dispatcher = ToolDispatcher(reg)
dispatcher.register_handler("create_sphere", lambda params: {"name": "sphere1"})

# Wrap in pipeline
pipeline = ToolPipeline(dispatcher)

# Add built-in middleware
pipeline.add_logging(log_params=True)             # tracing before/after
timing  = pipeline.add_timing()                   # returns TimingMiddleware handle
audit   = pipeline.add_audit(record_params=True)  # returns AuditMiddleware handle
rl      = pipeline.add_rate_limit(max_calls=10, window_ms=1000)  # RateLimitMiddleware handle

# Add Python callable hooks
pipeline.add_callable(
    before_fn=lambda action: print(f"before: {action}"),
    after_fn=lambda action, success: print(f"after: {action} ok={success}"),
)

# Register additional handlers directly on pipeline
pipeline.register_handler("delete_sphere", lambda params: True)

# Dispatch — returns dict with action/output/validation_skipped
result = pipeline.dispatch("create_sphere", '{"radius": 1.0}')
# result == {"action": "create_sphere", "output": {"name": "sphere1"}, "validation_skipped": bool}

# Query middleware state
timing.last_elapsed_ms("create_sphere")  # int | None (ms since last dispatch)
audit.records()                           # list[dict] — action/success/error/timestamp_ms
audit.records_for_action("create_sphere")
audit.record_count()
audit.clear()
rl.call_count("create_sphere")  # calls in current window
rl.max_calls                    # int
rl.window_ms                    # int

# Introspect pipeline
pipeline.middleware_count()   # int
pipeline.middleware_names()   # ["logging","timing","audit","rate_limit","python_callable"]
pipeline.handler_count()      # int

# Standalone middleware classes (can also be constructed independently)
lm = LoggingMiddleware(log_params=False)   # lm.log_params
tm = TimingMiddleware()                    # tm.last_elapsed_ms(action)
am = AuditMiddleware(record_params=True)   # am.records(), .clear()
rm = RateLimitMiddleware(max_calls=5, window_ms=500)  # rm.call_count(action)
```



### EventBus

Thread-safe publish/subscribe event system.

```python
bus = dcc_mcp_core.EventBus()

# Subscribe (returns subscriber ID for unsubscription)
sub_id = bus.subscribe("action_completed", lambda **kw: print(f"Done: {kw}"))
sub_id2 = bus.subscribe("action_completed", on_action_done)

# Publish with keyword arguments
bus.publish("action_completed", action="create_sphere", success=True, duration_ms=42)

# Unsubscribe by event + subscriber ID
removed = bus.unsubscribe("action_completed", sub_id)  # -> bool

repr(bus)  # "EventBus(subscriptions=N)"
```

### VersionedRegistry

Multi-version tool registry allowing multiple versions of the same `(name, dcc)` pair.

```python
from dcc_mcp_core import VersionedRegistry, VersionConstraint, SemVer

vr = VersionedRegistry()
vr.register_versioned("create_sphere", "maya", "1.0.0", description="v1 basic")
vr.register_versioned("create_sphere", "maya", "1.5.0", description="v1.5 with subdivisions")
vr.register_versioned("create_sphere", "maya", "2.0.0", description="v2 GPU accelerated")

# Resolve best match (highest version satisfying constraint)
result = vr.resolve("create_sphere", "maya", "^1.0.0")
# result == {"name": "create_sphere", "dcc": "maya", "version": "1.5.0", ...}

# Resolve all matching
all_v1 = vr.resolve_all("create_sphere", "maya", "^1.0.0")
# -> [{"version": "1.0.0",...}, {"version": "1.5.0",...}]

# Query
vr.versions("create_sphere", "maya")         # -> ["1.0.0", "1.5.0", "2.0.0"]
vr.latest_version("create_sphere", "maya")   # -> "2.0.0"
vr.total_entries()                            # -> 3
vr.keys()                                     # -> [("create_sphere", "maya")]

# Remove (returns count of versions removed)
removed = vr.remove("create_sphere", "maya", "^1.0.0")   # removes 1.0.0 and 1.5.0 -> 2
removed = vr.remove("create_sphere", "maya", "*")         # removes all -> remaining count
```

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

```python
from dcc_mcp_core import VersionedRegistry, VersionConstraint

vr = VersionedRegistry()
vr.register_versioned("create_sphere", "maya", "1.0.0")
vr.register_versioned("create_sphere", "maya", "2.0.0")

# Obtain a router for version-aware resolution
router = vr.router()
# The router borrows the registry and provides constraint-based lookups
# Use VersionedRegistry.resolve() as the primary API — the router is
# for advanced multi-step resolution scenarios
```

### SemVer

```python
from dcc_mcp_core import SemVer

v = SemVer(1, 2, 3)
str(v)              # "1.2.3"
v.major, v.minor, v.patch  # 1, 2, 3
SemVer.parse("2.0.0") > v  # True
SemVer.parse("v1.5.0-alpha")  # SemVer(1, 5, 0)
```

### VersionConstraint

```python
from dcc_mcp_core import VersionConstraint, SemVer

# Supported operators: * = >= > <= < ^ ~
c = VersionConstraint.parse("^1.0.0")   # same major, >= minor.patch
c.matches(SemVer(1, 5, 0))   # True
c.matches(SemVer(2, 0, 0))   # False

c2 = VersionConstraint.parse(">=1.2.0")
c3 = VersionConstraint.parse("~1.2.3")   # same major.minor, >= patch
c4 = VersionConstraint.parse("*")        # any version
```

---

## Skills System (`dcc_mcp_core`)

### SkillScanner

Discovers SKILL.md files in directories with mtime-based caching.

```python
scanner = dcc_mcp_core.SkillScanner()
dirs = scanner.scan(
    extra_paths=["/my/skills", "examples/skills"],
    dcc_name="maya",       # also checks platform-specific skills dir
    force_refresh=False,   # True to ignore mtime cache
)
# dirs: List[str] — absolute paths to skill directories containing SKILL.md

scanner.discovered_skills  # -> List[str] (last scan result, same as return value)
scanner.clear_cache()      # reset mtime cache — next scan will re-read all dirs
```

**Search path priority** (highest to lowest):
1. `extra_paths` argument
2. `DCC_MCP_SKILL_PATHS` environment variable (colon/semicolon separated)
3. Platform skills dir: `get_skills_dir(dcc_name)`

### SkillWatcher

Hot-reload watcher for skill directories. Monitors filesystem events using platform-native APIs
(inotify on Linux, FSEvents on macOS, ReadDirectoryChangesW on Windows).

```python
watcher = dcc_mcp_core.SkillWatcher(debounce_ms=300)  # coalesce rapid changes

watcher.watch("/path/to/skills")   # starts watching + does immediate reload
# watcher.watch() can be called multiple times for multiple directories

skills = watcher.skills()          # -> List[SkillMetadata] (immutable snapshot)
watcher.skill_count()              # -> int
watcher.watched_paths()            # -> List[str]
watcher.reload()                   # manual force reload

watcher.unwatch("/path/to/skills") # -> bool (True if was being watched)
```

### parse_skill_md

```python
meta = dcc_mcp_core.parse_skill_md("/path/to/skill-dir")  # -> SkillMetadata or None
```

Parses YAML frontmatter from SKILL.md, enumerates scripts/ directory (all supported extensions),
discovers metadata/ directory files, and merges `depends` from `metadata/depends.md`.

Returns `None` if the directory has no SKILL.md.

### scan_skill_paths

```python
dirs = dcc_mcp_core.scan_skill_paths(extra_paths=["/my/skills"], dcc_name="maya")
# Convenience: creates SkillScanner, scans, returns List[str] paths
```

### scan_and_load / scan_and_load_lenient

```python
# Full pipeline: scan → parse → resolve dependencies (topological sort)
skills, skipped = dcc_mcp_core.scan_and_load(
    extra_paths=["/my/skills"],
    dcc_name="maya",
)
# Returns (List[SkillMetadata], List[str] skipped_dirs)
# Raises ValueError on missing dependencies or cycles

# Lenient variant: skips unresolvable skills instead of raising
skills, skipped = dcc_mcp_core.scan_and_load_lenient(dcc_name="maya")
# Only raises on cyclic dependencies

for s in skills:
    print(f"{s.name} v{s.version}: {len(s.scripts)} scripts → {s.skill_path}")
    # Action names: f"{s.name.replace('-', '_')}__{stem}" for stem in script stems
```

### skills_helper

```python
from dcc_mcp_core.skills_helper import (
    SkillCodecError,
    SkillFileError,
    atomic_write_text,
    check_cancelled,
    compress_bytes,
    decompress_bytes,
    dump_json_file,
    ensure_within_root,
    file_digest,
    json_dumps,
    json_loads,
    load_json_file,
    load_yaml_file,
    normalize_tool_arguments,
    skill_success,
    yaml_dumps,
    yaml_loads,
)
```

Canonical skill-authoring namespace for Rust-backed, dependency-light helpers.
Use it for JSON/YAML codecs, bounded file/path helpers, standard result helpers,
schema validation, MCP/REST argument normalization, and cancellation checks.
Legacy imports such as `from dcc_mcp_core import json_dumps` remain
compatibility re-exports of the same functions. Use `load_json_file` /
`load_yaml_file` when a skill needs explicit UTF-8 handling, byte limits,
source-aware errors, or mapping-root validation. Use `ensure_within_root` for
trusted workspace/session roots, `atomic_write_text` / `atomic_write_bytes` for
same-directory temp-file writes, `file_digest` / `bytes_digest` for SHA-256,
and `compress_bytes` / `decompress_bytes` for bounded LZ4 frames. Use `FileRef`
and artefact helpers when a file crosses tool or MCP resource boundaries. TOML
runtime helpers are intentionally deferred until a real skill script use case
needs dependency-free TOML read/write support on Python 3.7.

### register_metadata_driven_tools

```python
report = dcc_mcp_core.register_metadata_driven_tools(
    server,
    skills=skills,           # omit to scan via scan_and_load_lenient(...)
    dcc_name="maya",
)
```

Registers optional metadata-driven tools such as recipes and skill reference
docs. Pass `registrations=[metadata_extension(...), imported_metadata_extension(...)]`
for adapter-specific extension callbacks. The returned
`MetadataRegistrationReport` exposes `registered_count`, `failed_count`,
`skipped_count`, `ok`, and `to_dict()`.

### scan_and_load_team / scan_and_load_team_lenient

```python
# Discover team-level skills (shared / accumulated / evolved skills)
skills, skipped = dcc_mcp_core.scan_and_load_team(dcc_name="maya")
skills, skipped = dcc_mcp_core.scan_and_load_team_lenient(dcc_name="maya")
# Same return type as scan_and_load: (List[SkillMetadata], List[str])
# Reads DCC_MCP_TEAM_SKILL_PATHS env var + platform team skills dir
```

### scan_and_load_user / scan_and_load_user_lenient

```python
# Discover user-level skills (personal skill directories)
skills, skipped = dcc_mcp_core.scan_and_load_user(dcc_name="maya")
skills, skipped = dcc_mcp_core.scan_and_load_user_lenient(dcc_name="maya")
# Same return type as scan_and_load: (List[SkillMetadata], List[str])
# Reads DCC_MCP_USER_SKILL_PATHS env var + platform user skills dir
```

### Skill Feedback

```python
from dcc_mcp_core import SkillFeedback, get_skill_feedback, record_skill_feedback

# Record feedback for a skill (e.g., from agent or user)
record_skill_feedback("maya-geometry", "success: created sphere quickly")

# Retrieve feedback history
entries: list[SkillFeedback] = get_skill_feedback("maya-geometry")
for entry in entries:
    print(entry.text)   # str feedback text
    print(entry.timestamp_ms)
```

### Skill Version Manifest

```python
from dcc_mcp_core import SkillVersionEntry, SkillVersionManifest, get_skill_version_manifest

manifest = get_skill_version_manifest("/path/to/skill-dir")
# manifest.entries: list[SkillVersionEntry]
# Each entry: version (str), created_at (int ms), description (str), author (str)
```

### Skill Script Helpers (Rust-level)

```python
from dcc_mcp_core import skill_error_with_trace, skill_warning, skill_exception

# Error with automatic traceback capture
result = skill_error_with_trace("Export failed", error="File not found", prompt="Check path")

# Warning-level result (success=True with warning note)
result = skill_warning("Export completed", prompt="Review normals", warning="Some UVs overlap")

# Exception wrapper (captures current exception info)
try:
    risky_op()
except Exception:
    result = skill_exception("Operation failed", prompt="Retry with different params")
```

### run_main

```python
from dcc_mcp_core import run_main

# Convenience entry point for standalone skill script execution
# Executes main_fn, serializes result, prints JSON to stdout, exits 0/1
run_main(skill_dir, params=None) -> dict
```

### Dependency Resolution

```python
from dcc_mcp_core import resolve_dependencies, validate_dependencies, expand_transitive_dependencies

# Validate declared deps exist in a skill list
errors = validate_dependencies(skills)  # -> List[str] error messages; empty if OK

# Topological sort (raises if missing dep or cycle)
ordered = resolve_dependencies(skills)  # -> List[SkillMetadata] sorted

# Full transitive closure for a specific skill
all_deps = expand_transitive_dependencies(skills, "my-skill")  # -> List[str] names
```

### SKILL.md Format

Authoring helpers:

- `skills/dcc-mcp-skills-creator/` scaffolds and validates DCC-MCP skill packages and bundles the agent-facing authoring guidance for building or modernizing DCC adapter skills. It distills dcc-mcp-maya, dcc-mcp-blender, and dcc-mcp-3dsmax patterns for server wiring, `SKILL.md` / `tools.yaml`, execution and affinity metadata, host differences, and testing/VRS coverage.

```yaml
---
name: maya-geometry          # Required agentskills.io identifier (kebab-case, max 64 chars)
description: >-
  Maya geometry creation and modification tools. Use when creating or
  modifying polygon geometry.
allowed-tools: Bash(git:*) Read  # Space-separated pre-approved tools (agentskills.io spec, experimental)
license: "MIT"               # License identifier (agentskills.io spec)
compatibility: "Maya 2024+"  # Environment requirements, max 500 chars (agentskills.io spec)
metadata:
  dcc-mcp:
    dcc: maya
    version: "1.0.0"
    tags: ["maya", "geometry"]
    search-hint: "polygon modeling, sphere, bevel, extrude"
    tools: tools.yaml
    groups: groups.yaml
    depends: ["other-skill"]
    resources: "resources/*.resource.yaml"
    external-deps: external_deps.yaml
---
# Human-readable description (markdown body)

This markdown body describes the skill to humans and AI agents.
Include: purpose, usage examples, expected environment, prerequisites.
```

**agentskills.io standard fields** (V1.0 spec, 2025-12-18): `name` (required), `description` (required), `license`, `compatibility`, `metadata`, `allowed-tools` (experimental — space-separated tool strings like `Bash(git:*) Read`).
**dcc-mcp-core extensions**: live under `metadata.dcc-mcp.*` and point to sibling files when payloads are non-trivial: `.dcc`, `.version`, `.tags`, `.search-hint`, `.tools`, `.groups`, `.depends`, `.resources`, `.workflows`, `.prompts`, `.external-deps`. Legacy top-level extension keys are rejected by the strict loader.

### Supported Script Extensions

`.py`, `.mel`, `.ms`, `.bat`, `.cmd`, `.sh`, `.bash`, `.ps1`, `.jsx`, `.js`

### Environment Variable

```bash
# Unix/macOS
export DCC_MCP_SKILL_PATHS="/path/to/skills1:/path/to/skills2"

# Windows
set DCC_MCP_SKILL_PATHS=C:\skills1;C:\skills2

# DCC-specific paths (set by DCC integration plugins or manually)
export DCC_MCP_MAYA_SKILL_PATHS="/maya/skills1:/maya/skills2"
export DCC_MCP_BLENDER_SKILL_PATHS="/blender/skills"
```

```python
# Read DCC-specific paths programmatically
from dcc_mcp_core import get_app_skill_paths_from_env
maya_paths = get_app_skill_paths_from_env("maya")    # reads DCC_MCP_MAYA_SKILL_PATHS
blender_paths = get_app_skill_paths_from_env("blender")
```

### SkillCatalog (Progressive Loading)

`SkillCatalog` wraps a `SkillScanner` with progressive skill loading and thread-safe state.

```python
from dcc_mcp_core import SkillCatalog, SkillScanner, SkillSummary

# Create catalog (preferred: use create_skill_server for Skills-First)
scanner = SkillScanner()
catalog = SkillCatalog(scanner)

# Discover available skills
catalog.discover(extra_paths=["/my/skills"], dcc_name="maya")

# List skills
all_skills: list[SkillSummary] = catalog.list_skills()
loaded_skills = catalog.list_skills(status="loaded")

# Search — matches name, description, search_hint, and tool names
results = catalog.search_skills(query="geometry", tags=["maya"], dcc="maya")

# Load / unload individual skills
catalog.load_skill("maya-geometry")      # -> bool
catalog.unload_skill("maya-geometry")    # -> bool
catalog.is_loaded("maya-geometry")       # -> bool
catalog.loaded_count()                   # -> int

# Get full metadata
meta = catalog.get_skill_info("maya-geometry")  # -> Optional[SkillMetadata]

# Tool groups (progressive exposure) — see next section
catalog.active_groups("maya-geometry")            # -> List[str]
catalog.activate_group("maya-geometry", "rigging")   # -> bool
catalog.deactivate_group("maya-geometry", "rigging") # -> bool
catalog.list_groups("maya-geometry")               # -> List[SkillGroup]
catalog.list_tools_catalog("maya-geometry")        # -> {group_name: [tool_names]}
```

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

### Tool Groups (Progressive Exposure)

Large skills can declare **tool groups** in the sibling file referenced by
`metadata.dcc-mcp.groups` (often the same `tools.yaml` referenced by
`metadata.dcc-mcp.tools`) so the AI client activates only the toolsets it needs,
keeping `tools/list` compact while all tools stay discoverable via `search_tools`.

```yaml
# SKILL.md frontmatter
---
name: maya-geometry
description: "Maya geometry tools. Use when creating or editing polygon meshes."
metadata:
  dcc-mcp:
    dcc: maya
    tools: tools.yaml
    groups: tools.yaml
---
```

```yaml
# tools.yaml
groups:
  - name: modeling
    description: "Polygon modeling and UV tools"
    default_active: true          # active at load_skill time
    tools: [create_sphere, extrude]
  - name: rigging
    description: "Skeleton, joints, skinning"
    default_active: false         # dormant until activate_tool_group
    tools: [create_joint]
tools:
  - name: create_sphere
    group: modeling
    source_file: scripts/create_sphere.py
  - name: create_joint
    group: rigging
    source_file: scripts/create_joint.py
```

**`SkillGroup` fields:** `.name`, `.description`, `.default_active` (bool), `.tools` (`List[str]`).

**Runtime behaviour after `load_skill`:**

1. Every tool is registered in `ToolRegistry` with its group metadata set.
2. Tools whose group has `default_active=false` are hidden from
   MCP `tools/list` — they remain in the registry and become visible once activated.
3. `tools/list` also emits `__group__<group>` stubs for inactive
   groups so clients can discover and activate them on demand.

**Activate / deactivate from Python:**

```python
from dcc_mcp_core import ToolRegistry

# Via the registry (emits notifications/tools/list_changed)
registry.activate_tool_group("maya-geometry", "rigging")   # -> int enabled count
registry.deactivate_tool_group("maya-geometry", "rigging") # -> int disabled count
registry.list_tools_in_group("maya-geometry", "modeling")  # -> List[dict]
registry.list_actions_enabled()                            # -> List[dict]
registry.set_tool_enabled("maya_geometry__create_joint", True)
```

**Via MCP tools** — `create_skill_server` / `McpHttpServer` register group-control tools alongside the Skills-First discovery/lifecycle tools (`search_skills`, `list_skills`, `get_skill_info`, `load_skill`, `unload_skill`):

| MCP tool | Description |
|----------|-------------|
| `activate_tool_group` | Enable all tools in a group; sends `notifications/tools/list_changed` |
| `deactivate_tool_group` | Disable all tools in a group |
| `search_tools` | Keyword search across enabled tools plus unloaded-skill candidates; default results suppress `__skill__*` / `__group__*` stubs, use `include_unloaded_skills=false` or `include_stubs=true` only for those explicit behaviours |

### `next-tools` — Follow-Up Tool Hints (dcc-mcp-core extension)

The `next-tools` field guides AI agents to appropriate follow-up actions after a tool
executes. This is a dcc-mcp-core extension, not part of the agentskills.io specification.
Declare it per tool inside the sibling `tools.yaml` file referenced by `metadata.dcc-mcp.tools`.

```yaml
# tools.yaml
tools:
  - name: create_sphere
    description: "Create a polygon sphere"
    source_file: scripts/create_sphere.py
    next-tools:
      on-success: [maya_geometry__bevel_edges]      # suggest after success
      on-failure: [dcc_diagnostics__screenshot]      # debug on failure
```

| Key | Type | Description |
|-----|------|-------------|
| `on-success` | `List[str]` | Suggested tools after successful execution |
| `on-failure` | `List[str]` | Debugging/recovery tools on failure |

Both accept fully-qualified tool names in `{skill_name}__{tool_name}` format. Runtime results expose successful hints at `CallToolResult._meta["dcc.next_tools"].on_success` and failure hints at `.on_failure` when declarations exist.

### Action Naming Convention

Each script in `scripts/` becomes an action:
```
{skill_name}__{script_stem}
```
- Hyphens in skill names → underscores
- Double underscore (`__`) separator
- Examples:
  - `maya-geometry/scripts/create_sphere.py` → `maya_geometry__create_sphere`
  - `git-tools/scripts/commit.sh` → `git_tools__commit`

---

## Skill Script Helpers — `dcc_mcp_core.skill` (pure Python)

A **pure-Python** sub-module for skill script authors. No compiled `_core` extension required — works inside any DCC Python environment.

```python
from dcc_mcp_core.skill import skill_entry, skill_success, skill_error, skill_warning, skill_exception, run_main
```

All helpers return a plain `dict` compatible with `ToolResult`.

### Result builders

```python
# Success
skill_success(message, *, prompt=None, **context) -> dict

# Failure — explicit error string
skill_error(message, error, *, prompt=None, possible_solutions=None, **context) -> dict

# Success with a warning note in context["warning"]
skill_warning(message, *, warning="", prompt=None, **context) -> dict

# Failure built from a caught exception (captures traceback in context)
skill_exception(exc, *, message=None, prompt=None, include_traceback=True,
                possible_solutions=None, **context) -> dict
```

### @skill_entry decorator

Wraps a skill function with automatic error handling. Catches `ImportError` (DCC not
available), `Exception`, and `BaseException`. When run directly, prints JSON to stdout.

```python
from dcc_mcp_core.skill import skill_entry, skill_success

@skill_entry
def set_timeline(start_frame: float = 1.0, end_frame: float = 120.0, **kwargs):
    import maya.cmds as cmds          # ImportError caught automatically
    cmds.playbackOptions(min=start_frame, max=end_frame,
                         animationStartTime=start_frame, animationEndTime=end_frame)
    return skill_success(
        f"Timeline set to {start_frame}–{end_frame}",
        prompt="Inspect the timeline slider to verify.",
        start_frame=start_frame,
        end_frame=end_frame,
    )

def main(**kwargs):
    return set_timeline(**kwargs)

if __name__ == "__main__":
    from dcc_mcp_core.skill import run_main
    run_main(main)
```

### run_main

Executes `main_fn()`, prints JSON result to stdout, exits 0/1.

```python
if __name__ == "__main__":
    from dcc_mcp_core.skill import run_main
    run_main(main)
```

### Migration from DCC-specific helpers

| DCC-specific (dcc_mcp_maya) | Generic (dcc_mcp_core.skill) |
|-----------------------------|------------------------------|
| `maya_success(msg, **ctx)` | `skill_success(msg, **ctx)` |
| `maya_error(msg, error, **ctx)` | `skill_error(msg, error, **ctx)` |
| `maya_from_exception(exc_str, ...)` | `skill_exception(exc, ...)` |

Dict structure is identical — both compatible with `ToolResult`.

---

## Result Serialization — `serialize_result` / `deserialize_result`

Rust-backed, format-agnostic serialization for `ToolResult`.
Format is controlled by `SerializeFormat` enum — JSON now, MsgPack later, no API change.

```python
from dcc_mcp_core import serialize_result, deserialize_result, SerializeFormat, success_result
```

### SerializeFormat

```python
SerializeFormat.Json     # str  — UTF-8 JSON text (default)
SerializeFormat.MsgPack  # bytes — binary MessagePack (rmp-serde)
```

### serialize_result

```python
serialize_result(result: ToolResult, format: SerializeFormat = SerializeFormat.Json) -> str | bytes
```

- Returns `str` for `Json`, `bytes` for `MsgPack`.

```python
arm = success_result("done", count=3)
json_str = serialize_result(arm)                              # str
pack_bytes = serialize_result(arm, SerializeFormat.MsgPack)  # bytes
```

### deserialize_result

```python
deserialize_result(data: str | bytes, format: SerializeFormat = SerializeFormat.Json) -> ToolResult
```

- Accepts `str` (JSON) or `bytes` (MsgPack). Format must match serialization.

```python
arm2 = deserialize_result(json_str)
assert arm2.message == "done"
arm3 = deserialize_result(pack_bytes, SerializeFormat.MsgPack)
assert arm3.context["count"] == 3
```

### How run_main serializes

`run_main()` in `dcc_mcp_core.skill` uses this Rust path when `_core` is available:
```
dict → validate_action_result() → ToolResult → serialize_result() → JSON str → stdout
```
Falls back to `json.dumps` in pure-Python DCC environments (no `_core` installed).

### Rust API (for crate authors)

```rust
use dcc_mcp_models::{ActionResultModelData, SerializeFormat};

let data = ActionResultModelData::success("done".into(), None, Default::default());
let json_bytes = data.to_bytes(SerializeFormat::Json)?;
let msgpack_bytes = data.to_bytes(SerializeFormat::MsgPack)?;
let restored = ActionResultModelData::from_bytes(&json_bytes, SerializeFormat::Json)?;
```

---

## MCP Protocol Types (`dcc_mcp_core`)

### ToolDefinition

```python
td = dcc_mcp_core.ToolDefinition(
    name="create_sphere",
    description="Creates a polygon sphere in the active Maya scene",
    input_schema='{"type": "object", "required": ["radius"], "properties": {"radius": {"type": "number", "minimum": 0.01}}}',
    output_schema=None,         # Optional JSON Schema string
    annotations=dcc_mcp_core.ToolAnnotations(
        title="Create Sphere",
        read_only_hint=False,
        destructive_hint=False,
        idempotent_hint=False,
        open_world_hint=True,   # may affect external state
    ),
)
# All fields get/set: td.name, td.description, td.input_schema, td.output_schema, td.annotations
```

### ToolAnnotations

```python
ann = dcc_mcp_core.ToolAnnotations(
    title="Create Sphere",    # Display name for UI
    read_only_hint=True,      # True = doesn't modify state; helps LLM plan safely
    destructive_hint=False,   # True = deletes/overwrites data
    idempotent_hint=True,     # True = safe to call multiple times
    open_world_hint=False,    # True = may affect external/open-ended state
    deferred_hint=None,        # True = full schema deferred until load_skill (set by server)
)
```

### ToolDeclaration

Declaration of a single MCP tool provided by a Skill, parsed from the sibling file referenced by `metadata.dcc-mcp.tools` (usually `tools.yaml`).
Lightweight discovery-time object — no script execution needed. In YAML/JSON, schema keys accept both `input_schema` / `output_schema` and MCP-style `inputSchema` / `outputSchema` aliases.

```python
from dcc_mcp_core import ToolDeclaration

td = ToolDeclaration(
    name="create_sphere",
    description="Create a polygon sphere",
    input_schema='{"type": "object", "required": ["radius"], "properties": {"radius": {"type": "number"}}}',
    output_schema=None,       # Optional JSON Schema string
    read_only=False,
    destructive=False,
    idempotent=False,
    source_file="scripts/create_sphere.py",
    requires_in_process=False,  # True for process-local stateful tools
)

# All fields are get/set:
td.name          # "create_sphere"
td.description   # str
td.input_schema  # JSON Schema string
td.output_schema # str|None
td.read_only     # bool  — True = doesn't modify DCC state
td.destructive   # bool  — True = deletes / overwrites data
td.idempotent    # bool  — True = safe to call multiple times
td.source_file   # str   — relative path within skill's scripts/ dir
td.requires_in_process  # bool — fail skill load instead of per-call subprocess fallback
```

**Relation to SkillMetadata**: `skill.tools` is a `List[ToolDeclaration]` populated from the
sibling file referenced by `metadata.dcc-mcp.tools` (usually `tools.yaml`). Each entry corresponds
to one MCP tool that the skill exposes. The `source_file` field links the declaration back to the
script that implements it.

### ToolSpec

Full tool specification for registry use, including optional output schema and annotations.

```python
from dcc_mcp_core import ToolSpec

ts = ToolSpec(
    name="create_sphere",
    description="Create a polygon sphere",
    input_schema='{"type": "object", "required": ["radius"], "properties": {"radius": {"type": "number"}}}',
    output_schema='{"type": "object", "properties": {"name": {"type": "string"}}}',
    annotations=dcc_mcp_core.ToolAnnotations(
        title="Create Sphere",
        read_only_hint=False,
        destructive_hint=False,
    ),
)
```

**Fields:** `.name`, `.description`, `.input_schema`, `.output_schema` (optional), `.annotations` (optional).

### ResourceDefinition / ResourceTemplateDefinition

```python
rd = dcc_mcp_core.ResourceDefinition(
    uri="file:///project/scene.mb",
    name="current-scene",
    description="The currently open Maya scene file",
    mime_type="application/octet-stream",  # default: "text/plain"
    annotations=dcc_mcp_core.ResourceAnnotations(
        audience=["user", "assistant"],
        priority=0.9,  # 0.0-1.0
    ),
)

# Template (URI with {placeholders})
rtd = dcc_mcp_core.ResourceTemplateDefinition(
    uri_template="maya://scene/{scene_name}/objects",
    name="scene-objects",
    description="Objects in a named Maya scene",
    mime_type="application/json",
)
```

### ResourceHandle and Gateway Resources

Python adapters can mutate the MCP resource registry without reaching into private server internals:

```python
resources = server.resources()
resources.set_scene({"path": "/show/shot/scene.ma", "dcc": "maya"})
resources.notify_updated("scene://current")
resources.register_output_buffer(output_capture)
resources.register_producer("maya-cmds://", lambda uri: {"contents": [{"uri": uri, "text": "..."}]})
```

Static resources declared by skills use `metadata.dcc-mcp.resources` pointing to sibling `*.resource.yaml` files. Current supported static source is `source.type: file`.

Gateway resources preserve backend ownership in the URI. Agents must pass the exact URI returned by `resources/list` to `resources/read` / `resources/subscribe`:

- `dcc://<type>/<id>` — gateway management pointer for a DCC instance.
- `<scheme>://<id8>/<rest>` — forwarded backend resource URI with the instance prefix.

Gateway prompt aggregation follows the same progressive discovery principle: use `prompts/list` to see namespaced prompt templates from every live backend and `prompts/get` with the returned name to fetch one template.

### PromptHandle — Registering Prompts from Python (issue #792)

Python adapters can register MCP prompts directly without touching Rust code, mirroring the `ResourceHandle` pattern:

```python
from dcc_mcp_core import McpHttpServer, McpHttpConfig, ToolRegistry

registry = ToolRegistry()
cfg = McpHttpConfig()
# Prompts are enabled by default; set cfg.enable_prompts = False only to opt out.

server = McpHttpServer(registry, cfg)

# Obtain handle; register startup prompts before start() so clients see them during initial discovery.
handle = server.prompts()

handle.register_prompt(
    name="bake_animation",
    description="Bake animation across a frame range",
    template="Please bake animation from {{start}} to {{end}} with sample rate {{sample_rate}}",
    arguments=[
        {"name": "start",       "description": "Start frame", "required": True},
        {"name": "end",         "description": "End frame", "required": True},
        {"name": "sample_rate", "description": "Frame step", "required": False},
    ],
)

server_handle = server.start()
# prompts/list now includes "bake_animation"
# prompts/get {"name":"bake_animation","arguments":{"start":"1","end":"100"}}
#   → messages[0].content.text = "Please bake animation from 1 to 100 with sample rate "

# Dynamic updates are reflected by subsequent prompts/list and prompts/get calls.
handle.unregister_prompt("bake_animation")
handle.clear()                     # remove every manually registered prompt on this server
```

`PromptHandle` API:
- `.register_prompt(name, template, *, description=None, arguments=None)` — upserts a prompt; `arguments` is a list of `{"name": str, "description": str, "required": bool}` dicts; empty/duplicate argument names raise `ValueError`
- `.unregister_prompt(name)` — removes one prompt (no-op if not found)
- `.clear()` — removes all manually registered prompts on this server

Template placeholder syntax: `{{arg_name}}` — replaced verbatim by `prompts/get` arguments. Missing required arguments return a JSON-RPC error.

### Project Persistence MCP Tools

```python
from dcc_mcp_core import register_project_tools
from dcc_mcp_core.project import DccProject

project = DccProject.open("/show/shot/scene.ma")
project.activate_skill("maya-lookdev")
project.activate_tool_group("maya-lookdev-tools")
project.update_metadata(units="cm", up_axis="y")
register_project_tools(server, dcc_name="maya", project=project)
```

`register_project_tools` exposes `project_save`, `project_load`, `project_resume`, and `project_status` so agents can recover scene paths, loaded assets, active skills/groups, checkpoints, and adapter metadata across DCC restarts.

### PromptDefinition / PromptArgument

```python
arg1 = dcc_mcp_core.PromptArgument("object_name", "Name of the 3D object", required=True)
arg2 = dcc_mcp_core.PromptArgument("format", "Export format", required=False)

pd = dcc_mcp_core.PromptDefinition(
    name="review_model",
    description="Review a 3D model and suggest improvements",
    arguments=[arg1, arg2],
)
```

### DCC Adapter Types

```python
# DccInfo — information about a running DCC instance
info = dcc_mcp_core.DccInfo(
    dcc_type="maya",
    version="2025",
    platform="windows",
    pid=12345,
    python_version="3.11.0",
    metadata={"scene": "/project/scene.mb"},
)
info.to_dict()  # -> dict

# DccCapabilities — what features the DCC adapter supports
caps = dcc_mcp_core.DccCapabilities(
    script_languages=[dcc_mcp_core.ScriptLanguage.PYTHON, dcc_mcp_core.ScriptLanguage.MEL],
    scene_info=True,
    snapshot=True,
    undo_redo=True,
    progress_reporting=False,
    file_operations=True,
    selection=True,
    # Cross-DCC protocol trait flags (v0.12+):
    scene_manager=True,    # implements DccSceneManager (scene/file management)
    transform=True,        # implements DccTransform (object TRS transforms)
    render_capture=False,  # implements DccRenderCapture (viewport capture + render)
    hierarchy=True,        # implements DccHierarchy (parent/child hierarchy)
)

# DccError — structured DCC error
err = dcc_mcp_core.DccError(
    code=dcc_mcp_core.DccErrorCode.SCRIPT_ERROR,
    message="Python script raised an exception",
    details="AttributeError: 'NoneType' ...",
    recoverable=True,
)

# ScriptLanguage enum values
dcc_mcp_core.ScriptLanguage.PYTHON
dcc_mcp_core.ScriptLanguage.MEL
dcc_mcp_core.ScriptLanguage.MAXSCRIPT
dcc_mcp_core.ScriptLanguage.HSCRIPT    # Houdini
dcc_mcp_core.ScriptLanguage.VEX        # Houdini VEX
dcc_mcp_core.ScriptLanguage.LUA
dcc_mcp_core.ScriptLanguage.CSHARP
dcc_mcp_core.ScriptLanguage.BLUEPRINT  # Unreal Engine

# DccErrorCode enum values
dcc_mcp_core.DccErrorCode.CONNECTION_FAILED
dcc_mcp_core.DccErrorCode.TIMEOUT
dcc_mcp_core.DccErrorCode.SCRIPT_ERROR
dcc_mcp_core.DccErrorCode.NOT_RESPONDING
dcc_mcp_core.DccErrorCode.UNSUPPORTED
dcc_mcp_core.DccErrorCode.PERMISSION_DENIED
dcc_mcp_core.DccErrorCode.INVALID_INPUT
dcc_mcp_core.DccErrorCode.SCENE_ERROR
dcc_mcp_core.DccErrorCode.INTERNAL
```

---

## Transport Layer (`dcc_mcp_core`)

### DccLinkFrame

Binary frame for DccLink protocol. Each frame carries a message type, sequence number, and body.

```python
from dcc_mcp_core import DccLinkFrame

# Create a frame
frame = DccLinkFrame(msg_type=1, seq=0, body=b'{"method": "execute_python", "params": {}}')
frame.msg_type   # int — message type
frame.seq        # int — sequence number
frame.body       # bytes — payload

# Encode / decode for wire transfer
encoded: bytes = frame.encode()           # serialize to binary
restored = DccLinkFrame.decode(encoded)   # deserialize from binary

# Message types
# 1 = Call      — request that expects a reply
# 2 = Reply     — response to a Call
# 3 = Err       — error response to a Call
# 4 = Progress  — intermediate progress update
# 5 = Cancel    — cancel a pending Call
# 6 = Push      — server-initiated notification
# 7 = Ping      — keep-alive / latency probe
# 8 = Pong      — response to Ping
```

### IpcChannelAdapter

Named IPC channel adapter using the DccLink framing protocol. Supports both server (create)
and client (connect) modes over OS-native named pipes / Unix domain sockets.

```python
from dcc_mcp_core import IpcChannelAdapter, DccLinkFrame

# Server mode: create a named channel
server = IpcChannelAdapter.create("maya-ipc")
server.wait_for_client()   # blocks until a client connects

# Client mode: connect to a named channel
client = IpcChannelAdapter.connect("maya-ipc")

# Send a frame
call_frame = DccLinkFrame(msg_type=1, seq=0, body=b'{"method": "execute_python", "params": {}}')
client.send_frame(call_frame)

# Receive a frame (blocking)
reply_frame = server.recv_frame()   # -> DccLinkFrame
print(reply_frame.msg_type)         # 2 (Reply) or 3 (Err)
print(reply_frame.body)             # response payload bytes

# Round-trip example
server.send_frame(DccLinkFrame(msg_type=2, seq=reply_frame.seq, body=b'result'))
client_reply = client.recv_frame()
```

### GracefulIpcChannelAdapter

Extended `IpcChannelAdapter` with graceful shutdown and thread-affinity support.
Use this in DCC main-thread scenarios where cleanup must be orderly.

```python
from dcc_mcp_core import GracefulIpcChannelAdapter, DccLinkFrame

# Server mode
server = GracefulIpcChannelAdapter.create("maya-graceful")
server.wait_for_client()

# Client mode
client = GracefulIpcChannelAdapter.connect("maya-graceful")

# Same send_frame / recv_frame as IpcChannelAdapter
client.send_frame(DccLinkFrame(msg_type=1, seq=0, body=b'request'))
reply = server.recv_frame()

# Graceful shutdown (waits for in-flight frames)
server.shutdown()
client.shutdown()

# Thread affinity: bind IPC processing to a specific thread
# Required for DCCs that restrict API calls to the main thread
server.bind_affinity_thread()   # binds to current thread
```

### SocketServerAdapter

TCP socket server adapter for DccLink protocol. Accepts multiple client connections.

```python
from dcc_mcp_core import SocketServerAdapter

# Create a TCP server on a Unix domain socket or named pipe path
server = SocketServerAdapter(
    path="/tmp/dcc-mcp.sock",       # socket path
    max_connections=10,              # max concurrent clients (default: 10)
    connection_timeout_ms=30000,     # per-connection idle timeout (default: 30000)
)

server.socket_path        # str — the socket path
server.connection_count   # int — current active connections
```

### TransportAddress

Protocol-agnostic endpoint. Supports TCP, Windows Named Pipes, Unix Domain Sockets.

```python
from dcc_mcp_core import TransportAddress

# Factory methods
addr = TransportAddress.tcp("127.0.0.1", 18812)
addr = TransportAddress.named_pipe("dcc-mcp-maya")        # Windows
addr = TransportAddress.unix_socket("/tmp/dcc-mcp.sock")  # Unix
addr = TransportAddress.default_local("maya", pid=12345)  # Best for current platform
addr = TransportAddress.parse("tcp://127.0.0.1:18812")    # from URI string

# Properties
addr.scheme         # "tcp" | "pipe" | "unix"
addr.is_local       # bool
addr.is_tcp, addr.is_named_pipe, addr.is_unix_socket  # bool
addr.to_connection_string()  # "tcp://127.0.0.1:18812"
```

### TransportScheme

Transport selection strategy enum. Chooses the optimal communication channel based on platform
and availability.

```python
from dcc_mcp_core import TransportScheme, TransportAddress

# Enum values
TransportScheme.AUTO                # Automatically pick best available transport
TransportScheme.TCP_ONLY            # Force TCP (cross-machine or when IPC unavailable)
TransportScheme.PREFER_NAMED_PIPE   # Prefer Windows Named Pipes (falls back to TCP)
TransportScheme.PREFER_UNIX_SOCKET  # Prefer Unix Domain Sockets (falls back to TCP)
TransportScheme.PREFER_IPC          # Prefer any IPC (Named Pipe on Windows, Unix Socket on Linux/macOS)

# Select an address using the strategy
scheme = TransportScheme.PREFER_IPC
addr = scheme.select_address(dcc_type="maya", host="127.0.0.1", port=18812, pid=12345)
# -> TransportAddress (Named Pipe on Windows, Unix Socket on Linux/macOS, TCP fallback)
```

### ServiceEntry

```python
entry.dcc_type           # str
entry.instance_id        # str
entry.host, entry.port   # str, int
entry.version            # str|None
entry.scene              # str|None (current open scene path)
entry.metadata           # dict[str,str]
entry.status             # ServiceStatus
entry.transport_address  # TransportAddress|None
entry.last_heartbeat_ms  # int (Unix ms)
entry.is_ipc             # bool
entry.effective_address()  # -> TransportAddress (transport_address or TCP fallback)
entry.to_dict()
```

### ServiceStatus

```python
ServiceStatus.AVAILABLE
ServiceStatus.BUSY
ServiceStatus.UNREACHABLE
ServiceStatus.SHUTTING_DOWN
```

---

## Cross-DCC Protocol Data Models (`dcc_mcp_core`)

Coordinate-system–normalized data models for DCC-agnostic communication.
All adapters (Maya, Blender, Houdini, 3dsMax, Unreal, Unity, Photoshop, Figma, …)
convert from their native representation to these shared types.

### ObjectTransform

3D object TRS in right-hand Y-up world space.

```python
from dcc_mcp_core import ObjectTransform

t = ObjectTransform(
    translate=[0.0, 10.0, 0.0],  # [x, y, z] in centimeters
    rotate=[0.0, 45.0, 0.0],     # Euler XYZ in degrees
    scale=[1.0, 1.0, 1.0],       # [sx, sy, sz]
)
t.translate   # [float, float, float]
t.rotate      # [float, float, float]
t.scale       # [float, float, float]
ObjectTransform.identity()  # -> ObjectTransform (all zeros/ones)
t.to_dict()   # -> {"translate": [...], "rotate": [...], "scale": [...]}
```

### BoundingBox

Axis-aligned bounding box in world space (centimeters).

```python
from dcc_mcp_core import BoundingBox

bb = BoundingBox(min=[-1.0, 0.0, -1.0], max=[1.0, 2.0, 1.0])
bb.min, bb.max  # list[float], list[float]
bb.center()     # -> [0.0, 1.0, 0.0]
bb.size()       # -> [2.0, 2.0, 2.0]
bb.to_dict()    # -> {"min": [...], "max": [...]}
```

### SceneObject

Lightweight descriptor of any scene entity (mesh, light, camera, transform, layer, actor, …).

```python
from dcc_mcp_core import SceneObject

obj = SceneObject(
    name="pCube1",
    long_name="|group1|pCube1",  # full DAG / hierarchy path
    object_type="mesh",          # "mesh", "light", "camera", "transform", etc.
    parent="group1",             # parent name (optional)
    visible=True,
    metadata={"material": "lambert1"},
)
obj.name, obj.long_name, obj.object_type
obj.parent   # str|None
obj.visible  # bool
obj.to_dict()
```

### SceneNode

Scene hierarchy node with recursive children (DAG node / actor / layer group).

```python
from dcc_mcp_core import SceneNode, SceneObject

leaf = SceneNode(object=SceneObject(name="pSphere1", object_type="mesh"))
root = SceneNode(
    object=SceneObject(name="group1", object_type="transform"),
    children=[leaf],
)
root.object    # SceneObject
root.children  # list[SceneNode]
root.to_dict()
```

### FrameRange

Animation frame range and timing.

```python
from dcc_mcp_core import FrameRange

fr = FrameRange(start=1.0, end=240.0, fps=24.0, current=1.0)
fr.start, fr.end, fr.fps, fr.current  # float
duration_seconds = (fr.end - fr.start + 1) / fr.fps
fr.to_dict()
```

### RenderOutput

Metadata for a completed render operation.

```python
from dcc_mcp_core import RenderOutput

out = RenderOutput(
    file_path="/renders/frame001.png",
    width=1920,
    height=1080,
    format="png",
    render_time_ms=5000,
)
out.file_path, out.width, out.height, out.format, out.render_time_ms
out.to_dict()
```

---

## Process Management (`dcc_mcp_core`)

### PyDccLauncher

Async DCC process launcher (spawn / terminate / kill).

```python
from dcc_mcp_core import PyDccLauncher

launcher = PyDccLauncher()

# Launch a DCC process
info = launcher.launch(
    name="nuke-mcp",                         # logical name for tracking
    executable="/opt/Nuke15.2/Nuke15.2",
    args=["--disable-nuke-frameserver", "project.nk"],
    launch_timeout_ms=30000,
    environment={
        "NUKE_DISABLE_FRAMESERVER": "1",
        "DCC_MCP_NUKE_PORT": "0",
    },
    working_directory="/projects/solar-system",
)
# info: {"pid": 12345, "name": "nuke-mcp", "status": "starting"}
# environment/cwd overrides apply only to the child process.

# Manage
launcher.terminate("nuke-mcp", timeout_ms=5000)  # graceful SIGTERM
launcher.kill("nuke-mcp")                         # force SIGKILL
pid = launcher.pid_of("nuke-mcp")                 # -> int|None
count = launcher.running_count()                   # int
restarts = launcher.restart_count("nuke-mcp")     # int
```

### PyProcessMonitor

Cross-platform DCC process monitor (CPU/memory sampling via sysinfo).

```python
import os
from dcc_mcp_core import PyProcessMonitor

mon = PyProcessMonitor()
mon.track(os.getpid(), "self")  # register a PID to monitor
mon.refresh()                   # refresh OS data (must call before query)
info = mon.query(os.getpid())
# info: {"pid": N, "name": "self", "status": "running",
#         "cpu_usage_percent": 1.2, "memory_bytes": 123456, "restart_count": 0}

mon.list_all()    # -> List[dict] for all tracked PIDs
mon.is_alive(N)   # -> bool (PID in OS process table)
mon.tracked_count()  # -> int
mon.untrack(N)    # stop monitoring
```

### PyProcessWatcher + PyCrashRecoveryPolicy

Background process watcher with event polling (async under the hood).

```python
import os, time
from dcc_mcp_core import PyProcessWatcher, PyCrashRecoveryPolicy

# Configure crash recovery
policy = PyCrashRecoveryPolicy(max_restarts=3)
policy.use_exponential_backoff(initial_ms=1000, max_delay_ms=30000)
# Or: policy.use_fixed_backoff(delay_ms=2000)
policy.should_restart("crashed")      # -> bool
policy.should_restart("unresponsive") # -> bool
delay = policy.next_delay_ms("maya", attempt=0)  # -> int ms

# Watcher
watcher = PyProcessWatcher(poll_interval_ms=500)
watcher.track(os.getpid(), "self")
watcher.start()

time.sleep(1.0)
events = watcher.poll_events()  # drain all pending events -> List[dict]
# Event dict keys: "type", "pid", "name"
# "heartbeat":      + "new_status", "cpu_usage_percent", "memory_bytes"
# "status_changed": + "old_status", "new_status"
# "exited":         (just type/pid/name)

watcher.stop()
watcher.is_running()       # -> bool
watcher.tracked_count()    # -> int
watcher.untrack(N)
```

### ScriptResult

Result of executing a DCC script.

```python
from dcc_mcp_core import ScriptResult

r = ScriptResult(
    success=True,
    execution_time_ms=42,
    output="sphere1",
    error=None,
    context={"dcc": "maya"},
)
r.to_dict()
```

---

## Sandbox (`dcc_mcp_core`)

### SandboxPolicy

API whitelist, path allowlist, execution constraints.

```python
from dcc_mcp_core import SandboxPolicy

policy = SandboxPolicy()
policy.allow_actions(["get_scene_info", "list_objects", "create_sphere"])
policy.deny_actions(["delete_scene", "shutdown"])  # override allowlist
policy.allow_paths(["/project/assets", "/project/cache"])
policy.set_timeout_ms(5000)
policy.set_max_actions(100)
policy.set_read_only(False)

policy.is_read_only  # bool
```

### SandboxContext

Bundles policy + audit log + action counter.

```python
from dcc_mcp_core import SandboxPolicy, SandboxContext

policy = SandboxPolicy()
policy.allow_actions(["echo"])
ctx = SandboxContext(policy)
ctx.set_actor("ai-agent-v1")   # identifies caller in audit log

result_json = ctx.execute_json("echo", '{"x": 1}')  # -> str (JSON result)
# Raises RuntimeError if denied/timeout/error

ctx.action_count   # int (successful executions)
ctx.audit_log      # AuditLog instance

ctx.is_allowed("echo")              # -> bool
ctx.is_path_allowed("/project/assets")  # -> bool
```

### InputValidator

Schema-based input validator with injection-guard rules.

```python
from dcc_mcp_core import InputValidator

v = InputValidator()
v.require_string("name", max_length=50, min_length=1)
v.require_number("count", min_value=0, max_value=1000)
v.forbid_substrings("script", ["__import__", "exec(", "eval(", "os.system"])

ok, error = v.validate('{"name": "sphere", "count": 5}')
# ok=True, error=None

ok, error = v.validate('{"script": "__import__(os)"}')
# ok=False, error="..."
```

### AuditLog + AuditEntry

```python
log = ctx.audit_log

len(log)            # total entries
entries = log.entries()     # -> List[AuditEntry]
successes = log.successes() # only successful entries
denials = log.denials()     # only denied entries
action_entries = log.entries_for_action("create_sphere")
log_json = log.to_json()    # -> str (JSON array)

# AuditEntry fields (all read-only properties)
entry.timestamp_ms    # Unix ms
entry.actor           # str|None (set via ctx.set_actor())
entry.action          # str (action name)
entry.params_json     # str (parameters as JSON)
entry.duration_ms     # int
entry.outcome         # "success"|"denied"|"error"|"timeout"
entry.outcome_detail  # str|None (error message or denial reason)
```

---

## Telemetry (`dcc_mcp_core`)

### ToolRecorder + RecordingGuard

Per-tool timing and success/failure counters.

```python
from dcc_mcp_core import ToolRecorder

recorder = ToolRecorder("maya-mcp-server")  # scope name

# Method 1: manual guard
guard = recorder.start("create_sphere", "maya")
try:
    result = do_create_sphere()
    guard.finish(success=True)
except Exception:
    guard.finish(success=False)
    raise

# Method 2: context manager (exception → success=False)
with recorder.start("batch_rename", "maya") as guard:
    do_batch_rename()
# guard.finish() called automatically with success=(no exception)

# Query metrics
metrics = recorder.metrics("create_sphere")   # -> ToolMetrics|None
all_m = recorder.all_metrics()               # -> List[ToolMetrics]
recorder.reset()                             # clear in-memory stats
```

### ToolMetrics

Read-only snapshot of per-tool performance.

```python
m = recorder.metrics("create_sphere")
m.action_name         # str
m.invocation_count    # int (total calls)
m.success_count       # int
m.failure_count       # int
m.avg_duration_ms     # float
m.p95_duration_ms     # float (95th percentile)
m.p99_duration_ms     # float (99th percentile)
m.success_rate()      # -> float in [0.0, 1.0]
```

### TelemetryConfig

Global telemetry provider (OpenTelemetry-backed).

```python
from dcc_mcp_core import TelemetryConfig, is_telemetry_initialized, shutdown_telemetry

cfg = (TelemetryConfig("maya-mcp-server")
       .with_stdout_exporter()          # print spans to stdout
       .with_attribute("dcc.type", "maya")
       .with_attribute("dcc.version", "2025")
       .with_service_version("1.0.0")
       .set_enable_metrics(True)
       .set_enable_tracing(True))
cfg.init()  # install as global provider (raises if already initialized)

is_telemetry_initialized()  # -> bool
shutdown_telemetry()        # flush and shut down

# For tests (no output):
cfg = TelemetryConfig("test").with_noop_exporter()
# For structured logs:
cfg = TelemetryConfig("prod").with_json_logs()
```

---

## Shared Memory (`dcc_mcp_core`)

Zero-copy data exchange for large DCC scene data (mesh geometry, animation caches, screenshots).

### PySharedBuffer

Named, fixed-capacity shared memory buffer backed by a memory-mapped file.

```python
from dcc_mcp_core import PySharedBuffer

# Create (producer side)
buf = PySharedBuffer.create(capacity=1024 * 1024)  # 1 MiB
n_written = buf.write(b"vertex data bytes")
data = buf.read()

buf.id           # str (UUID)
buf.path()       # str (file path of backing mmap)
buf.data_len()   # int (bytes currently stored)
buf.capacity()   # int (max bytes)
buf.clear()      # reset to 0 bytes
desc = buf.descriptor_json()  # JSON for cross-process handoff

# Open on consumer side (using producer's descriptor info)
buf2 = PySharedBuffer.open(path=buf.path(), id=buf.id)
assert buf2.read() == b"vertex data bytes"
```

### PyBufferPool

Fixed-capacity pool of reusable shared memory buffers. Amortises mmap allocation cost.

```python
from dcc_mcp_core import PyBufferPool

pool = PyBufferPool(capacity=4, buffer_size=1024 * 1024)  # 4 × 1MiB
buf = pool.acquire()   # -> PySharedBuffer (raises if all slots in use)
buf.write(b"scene snapshot")
# Buffer returned to pool automatically when garbage-collected

pool.available()      # -> int (free slots)
pool.capacity()       # -> int (total)
pool.buffer_size()    # -> int (per-buffer bytes)
```

### PySharedSceneBuffer (High-level)

High-level wrapper. Automatically selects inline (< 256 MiB) vs chunked (≥ 256 MiB) storage.
Supports optional LZ4 compression.

```python
from dcc_mcp_core import PySharedSceneBuffer, PySceneDataKind

# Write (producer DCC side)
ssb = PySharedSceneBuffer.write(
    data=vertex_bytes,
    kind=PySceneDataKind.Geometry,
    source_dcc="Maya",
    use_compression=True,   # LZ4
)
desc_json = ssb.descriptor_json()  # send this to consumer via IPC channel

# Read (consumer agent side) — reconstruct from descriptor is not exposed;
# pass ssb itself if in-process, or use IpcChannelAdapter to send desc_json
recovered = ssb.read()  # -> bytes (decompresses automatically)

ssb.id            # str
ssb.total_bytes   # int (original uncompressed size)
ssb.is_inline     # bool
ssb.is_chunked    # bool

# PySceneDataKind values
PySceneDataKind.Geometry
PySceneDataKind.AnimationCache
PySceneDataKind.Screenshot
PySceneDataKind.Arbitrary
```

---

## Screen Capture (`dcc_mcp_core`)

### Capturer + CaptureFrame

High-level DCC viewport / desktop capture. Platform-specific backends:
- Windows (full-screen): DXGI Desktop Duplication (GPU framebuffer, < 16 ms/frame)
- Windows (single window): Windows.Graphics.Capture (including occluded windows), with HWND `PrintWindow` + `BitBlt` as a runtime fallback
- Linux: X11 XShmGetImage (full-screen) — PipeWire reserved for Wayland
- macOS: ScreenCaptureKit (reserved)
- Fallback: Mock synthetic checkerboard (CI / headless)

```python
from dcc_mcp_core import Capturer, CaptureBackendKind

# Auto-select best full-screen / display backend
capturer = Capturer.new_auto()

# Auto-select best single-window backend (HWND on Windows, Mock elsewhere)
window_cap = Capturer.new_window_auto()

# Mock backend (headless CI, no GPU needed)
capturer = Capturer.new_mock(width=1920, height=1080)

# Capture a frame
frame = capturer.capture(
    format="png",           # "png" | "jpeg" | "raw_bgra"
    jpeg_quality=85,        # 0-100, only for jpeg
    scale=0.5,              # 0.0-1.0 scale factor
    timeout_ms=5000,
    process_id=12345,       # capture specific window by PID
    window_title="Maya",    # capture window by title substring
)

# Or capture a single window explicitly
frame = window_cap.capture_window(
    window_title="Autodesk Maya 2024",
    include_decorations=True,
    format="png",
    timeout_ms=5000,
)
# window_handle=<HWND int> and process_id=<pid> are also accepted

# CaptureFrame fields
frame.data           # bytes (PNG/JPEG/raw)
frame.width          # int
frame.height         # int
frame.format         # "png" | "jpeg" | "raw_bgra"
frame.mime_type      # "image/png" | "image/jpeg"
frame.timestamp_ms   # int (Unix ms)
frame.dpi_scale      # float (1.0=normal, 2.0=HiDPI)
frame.window_rect    # (x, y, w, h) tuple or None for full-screen captures
frame.window_title   # str or None
frame.byte_len()     # int

# Save to disk
with open("screenshot.png", "wb") as f:
    f.write(frame.data)

# Stats / backend introspection
capturer.backend_name()                                 # "Windows.Graphics.Capture" | "GDI PrintWindow" | "DXGI Desktop Duplication" | "X11 XShmGetImage" | "Mock"
capturer.backend_kind() == CaptureBackendKind.WindowsGraphicsCapture  # actual backend used for the latest window frame
count, total_bytes, errors = capturer.stats()
```

### CaptureTarget + WindowFinder

Resolve windows / displays to a capture-ready descriptor without throwing when
the target is missing.

```python
from dcc_mcp_core import CaptureTarget, WindowFinder

# Static factories — opaque descriptor, no raise on invalid
CaptureTarget.primary_display()
CaptureTarget.monitor_index(0)
CaptureTarget.process_id(12345)
CaptureTarget.window_title("Maya 2024")
CaptureTarget.window_handle(0x00A1B2)

finder = WindowFinder()
info = finder.find(CaptureTarget.process_id(12345))
if info is not None:
    info.handle   # int (HWND / X11 window id)
    info.pid      # int
    info.title    # str
    info.rect     # (x, y, w, h)

# Enumerate every visible top-level window on Windows
for info in finder.enumerate():
    print(info.handle, info.title)
```

### ComputerUseSession + UiPoint (Windows)

`ComputerUseSession` owns one exact target window, a visible click-through
border/banner, transient pointer effects, bounded `SendInput`, screenshot
generation ids, and a Windows-logon-session-wide Esc interruption
latch shared across DCC adapter processes. Ordinary Esc remains available to
the target application. Prefer the bundled
`ui-control` skill instead of calling this low-level API directly.

```python
from dcc_mcp_core import ComputerUseSession, UiPoint

session = ComputerUseSession(
    process_id=12345,
    window_handle=0x00A1B2,
    app_name="Godot",
)
session.start()
metadata_json, png_bytes = session.screenshot()
session.act('{"action":"click","observation_id":"...","x":320,"y":240}')
session.stop()  # remove effects; retry if cleanup_pending keeps input ownership fenced

ComputerUseSession.process_user_interrupted()  # shared Windows logon-session Esc latch
# Only after an explicit user instruction:
session.resume_after_user_approval()

point = UiPoint(x=320.0, y=240.0)  # latest screenshot-relative coordinates
```

Screenshots may be scaled to bound MCP payload size; observation coordinates
still map to the exact source HWND. Each action consumes its observation.
`ui-control` tools declare `requires_in_process: true` with `affinity: any`: an
adapter must register `HostExecutionBridge` before loading the skill, and must
call `ui_control__stop_computer_use` on every completion/failure path.

Windows lock, disconnect, and secure-desktop transitions return
`desktop_unavailable` before UIA or raw input runs. This pauses desktop control
without ending the logical `ui_control` session. Keep the same `session_id`;
structured DCC skills and MCP calls may continue only while the host adapter is
ready. Do not automate LockApp or treat the lock screen as an input target.

An ordinary user process cannot render the Computer Use banner on the Windows
lock screen or secure desktop. After unlock or reconnect, discard all prior
snapshot, observation, and control ids and take a fresh exact-target snapshot;
the successful snapshot re-establishes the banner. `resume_computer_use` remains
reserved for explicit post-interruption user approval. Use a dedicated, always-unlocked
VM when Windows GUI Computer Use must continue without interruption.

Computer Use executes on the adapter host in the exact interactive Windows
logon session that owns the DCC process. The central gateway routes the tool
call but does not own the screenshot coordinate space. Never apply coordinates
captured on the gateway, another host, or another logon session to the remote
DCC. RDP disconnect or session switch returns `desktop_unavailable` while
retaining the logical session; reconnect to the DCC's session and take a fresh
snapshot before another UI action.

Observations remain bounded to the scoped target window. Its virtual-desktop
origin may be negative, monitors may use different DPI scales, and the target
may span monitors. Coordinates remain relative to the returned PNG and are
mapped through that observation's source rectangle and DPI metadata. Monitor
add/remove, display-layout, resolution, or DPI/scaling changes invalidate the
observation and require a fresh snapshot. The banner follows the scoped target
window across monitors in the same logon session.

This behavior aligns with public Codex guidance for foreground Computer Use on
an active, unlocked Windows desktop. Public `openai/codex` `codex-rs` contains
feature, policy, and plugin-discovery wiring, not the desktop Computer Use
helper implementation; DCC MCP's Rust backend is independent.

### Instance-Bound Diagnostics

When multiple DCC instances run side-by-side, each `DccServerBase` subclass
should be bound to **its own** DCC process so `dcc_diagnostics__*` MCP tools and
the bundled `dcc-diagnostics` skill target the right window / PID.

```python
from dcc_mcp_core import DccServerBase

class MayaServer(DccServerBase):
    def __init__(self, pid: int, window_title: str):
        super().__init__(
            dcc_name="maya",
            builtin_skills_dir=None,
            dcc_pid=pid,                     # owner DCC PID
            dcc_window_title=window_title,   # title fallback for window lookups
            # dcc_window_handle=0x00A1B2,    # or pass an HWND directly
            # resolver=lambda: current_maya_pid(),  # late-bound PID
        )

server = MayaServer(pid=12345, window_title="Autodesk Maya 2024")
handle = server.start()
# Registers the four diagnostics tools bound to this instance:
#   dcc_diagnostics__screenshot
#   dcc_diagnostics__audit_log
#   dcc_diagnostics__tool_metrics   (renamed from dcc_diagnostics__action_metrics in 0.14.0)
#   dcc_diagnostics__process_status
```

For low-level servers that build on `McpHttpServer` directly, call
`register_diagnostic_mcp_tools(server, *, dcc_name=..., dcc_pid=..., ...)` or
`register_diagnostic_handlers(...)` (IPC path) **before** `server.start()` —
MCP tool registration must complete before the server is running.

The bundled `dcc-diagnostics` skill's `screenshot.py` handler tries
`DCC_MCP_OWNER_IPC` → IPC frame with take_screenshot request first and falls back
to `Capturer.new_auto()` when no owner IPC is configured.

---

## Output Capture (`dcc_mcp_core`)

Capture DCC stdout, stderr, and script-editor output as an `output://` MCP resource.
Useful for streaming logs from long-running DCC operations to the agent.

```python
from dcc_mcp_core import OutputCapture

# OutputCapture is available when the compiled extension supports it.
# Register on an McpHttpServer before .start() to enable output:// resource serving.
# The resource URI format: output://<dcc_name>/<channel>  (e.g., output://maya/stdout)
```

---

## USD Scene Description (`dcc_mcp_core`)

Lightweight USD-like scene exchange format for DCC interoperability.

### SdfPath

```python
from dcc_mcp_core import SdfPath

path = SdfPath("/World")
child = path.child("Cube")  # -> SdfPath("/World/Cube")
parent = child.parent()     # -> SdfPath("/World")
child.name                  # "Cube"
child.is_absolute           # True
hash(child)                 # hashable for use as dict key
```

### VtValue

Variant value container for USD attributes.

```python
from dcc_mcp_core import VtValue

# Create from Python types
v_bool   = VtValue.from_bool(True)
v_int    = VtValue.from_int(42)
v_float  = VtValue.from_float(1.0)
v_str    = VtValue.from_string("Cube")
v_token  = VtValue.from_token("Mesh")    # USD token (interned string)
v_asset  = VtValue.from_asset("/path/to/file.usd")
v_vec3   = VtValue.from_vec3f(1.0, 2.0, 3.0)

v_float.type_name          # "float"
v_vec3.type_name           # "float3"
v_float.to_python()        # 1.0
v_vec3.to_python()         # (1.0, 2.0, 3.0) tuple
```

### UsdPrim

```python
from dcc_mcp_core import UsdStage, UsdPrim, VtValue

stage = UsdStage("my_scene")
prim = stage.define_prim("/World/Cube", "Mesh")  # -> UsdPrim

prim.path         # SdfPath("/World/Cube")
prim.type_name    # "Mesh"
prim.active       # bool
prim.name         # "Cube"

prim.set_attribute("extent", VtValue.from_vec3f(1, 1, 1))
val = prim.get_attribute("extent")   # -> VtValue|None
names = prim.attribute_names()       # -> List[str]
summary = prim.attributes_summary()  # -> dict[str, str]  (attr_name → type_name)
prim.has_api("UsdGeomModelAPI")      # -> bool
```

### UsdStage

Primary unit of cross-DCC scene exchange.

```python
from dcc_mcp_core import UsdStage, VtValue

stage = UsdStage("my_scene")

# Stage properties (get/set)
stage.name              # str
stage.id                # str (UUID)
stage.up_axis           # "Y" (default) | "Z"
stage.up_axis = "Z"
stage.meters_per_unit   # float (default 0.01 = centimeters)
stage.fps               # float|None
stage.start_time_code   # float|None
stage.end_time_code     # float|None
stage.default_prim      # str|None

# Prim operations
stage.define_prim("/World/Cube", "Mesh")  # -> UsdPrim
stage.get_prim("/World/Cube")             # -> UsdPrim|None
stage.has_prim("/World/Cube")             # -> bool
stage.remove_prim("/World/Cube")          # -> bool
stage.traverse()                          # -> List[UsdPrim] (all prims)
stage.prims_of_type("Mesh")              # -> List[UsdPrim]

# Attribute shortcuts
stage.set_attribute("/World/Cube", "extent", VtValue.from_vec3f(1, 1, 1))
stage.get_attribute("/World/Cube", "extent")  # -> VtValue|None

# Stats
stage.metrics()  # -> {"prim_count": N, "attribute_count": M, ...}

# Serialization
json_str = stage.to_json()
stage2 = UsdStage.from_json(json_str)
usda = stage.export_usda()   # USD ASCII format string
```

### USD Bridge Functions

Convert between DCC `SceneInfo` and `UsdStage`.

```python
from dcc_mcp_core import scene_info_json_to_stage, stage_to_scene_info_json, units_to_mpu, mpu_to_units

# Convert SceneInfo JSON (from DCC adapter) to UsdStage
stage = scene_info_json_to_stage(scene_info_json_str, dcc_type="maya")

# Convert UsdStage back to SceneInfo JSON (best-effort)
scene_info_json_str = stage_to_scene_info_json(stage)

# Unit conversion
mpu = units_to_mpu("cm")   # -> 0.01 (meters per unit for centimeters)
mpu = units_to_mpu("m")    # -> 1.0
units = mpu_to_units(0.01) # -> "cm"
units = mpu_to_units(1.0)  # -> "m"
```

---

## Bridge System (`dcc_mcp_core`)

DCC inter-protocol bridging — register named bridges between different transport protocols.

```python
from dcc_mcp_core import BridgeRegistry, BridgeContext, register_bridge, get_bridge_context

# Register a named bridge
register_bridge("rpyc", BridgeContext(name="rpyc", description="RPyC ↔ MCP bridge"))

# Retrieve bridge context by name
ctx = get_bridge_context("rpyc")  # -> BridgeContext or None
```

---

## Artefacts (`dcc_mcp_core`)

Cross-tool file hand-off via the artefact store. Files are referenced by `FileRef` objects
and served as `artefact://` MCP resources when `McpHttpConfig.enable_artefact_resources=True`.

```python
from dcc_mcp_core import FileRef, artefact_put_file, artefact_put_bytes, artefact_get_bytes, artefact_list

# Store a file from disk
ref = artefact_put_file("/tmp/render.png", name="frame_001", metadata={"shot": "sh010"})
# ref.path — original path
# ref.mime_type — auto-detected or "application/octet-stream"
# ref.name — "frame_001"
# ref.metadata — {"shot": "sh010"}

# Store raw bytes
ref2 = artefact_put_bytes(
    b"...",
    name="report.json",
    mime_type="application/json",
    metadata={"job_id": "abc"},
)

# Retrieve bytes by FileRef
data = artefact_get_bytes(ref2)  # -> bytes

# List all stored artefacts
refs = artefact_list()  # -> list[FileRef]
```

**`FileRef` constructor:**
```python
FileRef(path, mime_type=None, name=None, metadata=None)
```

---

## Scene Data Model (`dcc_mcp_core`)

Structured scene data types for representing DCC scene hierarchy and geometry.

```python
from dcc_mcp_core import BoundingBox, FrameRange, ObjectTransform, SceneNode, SceneObject, RenderOutput

# Bounding box (axis-aligned)
bbox = BoundingBox(min_x=0, min_y=0, min_z=0, max_x=1, max_y=1, max_z=1)

# Animation frame range
frames = FrameRange(start=1, end=24, step=1)

# Object transform (translate, rotate, scale decomposition)
xform = ObjectTransform(translate=[0, 5, 0], rotate=[0, 0, 0, 1], scale=[1, 1, 1])

# Scene nodes (hierarchical)
node = SceneNode(path="/world/geo/sphere1", transform=xform)

# Scene objects (full representation)
obj = SceneObject(name="sphere1", node_type="mesh", transform=xform)

# Render output metadata
render = RenderOutput(path="/tmp/render.exr", format="exr", width=1920, height=1080)
```

> **Note**: `BoundingBox` is always available from the compiled extension since v0.13.0.

---

## Serialization (`dcc_mcp_core`)

Transport-safe ToolResult serialization for IPC, storage, or network transfer.

```python
from dcc_mcp_core import SerializeFormat, serialize_result, deserialize_result, success_result

result = success_result("Created sphere", prompt="Add materials next", count=1)

# Serialize to bytes
data = serialize_result(result, fmt=SerializeFormat.JSON)

# Deserialize back
restored = deserialize_result(data, fmt=SerializeFormat.JSON)
assert restored.success
assert restored.message == "Created sphere"
```

---

## Type Wrappers (`dcc_mcp_core`)

For safe RPyC transport (prevents OB proxy wrapping of basic Python types).

```python
from dcc_mcp_core import wrap_value, unwrap_value, unwrap_parameters
from dcc_mcp_core import BooleanWrapper, IntWrapper, FloatWrapper, StringWrapper

# Wrap — dispatches to correct type
w = wrap_value(True)      # -> BooleanWrapper(True)
w = wrap_value(42)        # -> IntWrapper(42)
w = wrap_value(3.14)      # -> FloatWrapper(3.14)
w = wrap_value("hello")   # -> StringWrapper("hello")
w = wrap_value([1, 2])    # -> [1, 2] (passthrough for unsupported types)

# Unwrap
v = unwrap_value(BooleanWrapper(True))  # -> True
v = unwrap_value(42)                    # -> 42 (passthrough)

# Bulk unwrap
result = unwrap_parameters({"enabled": BooleanWrapper(True), "count": IntWrapper(5)})
# -> {"enabled": True, "count": 5}

# Direct construction
BooleanWrapper(True).value    # True
IntWrapper(42).value          # 42; also supports __int__, __index__
FloatWrapper(3.14).value      # 3.14; also supports __float__
StringWrapper("hi").value     # "hi"; also supports __str__

# All wrappers: __repr__, __eq__, __hash__ (StringWrapper and BooleanWrapper and IntWrapper)
```

---

## Utilities (`dcc_mcp_core`)

### Filesystem Functions

```python
from dcc_mcp_core import (
    get_config_dir, get_data_dir, get_log_dir, get_platform_dir,
    get_tools_dir, get_skills_dir, get_skill_paths_from_env,
    get_app_skill_paths_from_env,
)

get_config_dir()              # Platform-specific config dir (e.g. %APPDATA%/dcc-mcp on Windows)
get_data_dir()                # Platform-specific data dir
get_log_dir()                 # Data dir + /log subdirectory
get_platform_dir("config")    # Generic: accepts "config"|"data"|"cache"|"log"|"state"|"documents"
get_tools_dir("maya")       # -> .../data/actions/maya/
get_skills_dir("maya")        # -> .../data/skills/maya/
get_skills_dir()              # -> .../data/skills/
get_skill_paths_from_env()    # -> List[str] from DCC_MCP_SKILL_PATHS env var
get_app_skill_paths_from_env("maya")   # -> List[str] from DCC_MCP_MAYA_SKILL_PATHS env var
get_app_skill_paths_from_env("blender") # -> List[str] from DCC_MCP_BLENDER_SKILL_PATHS env var
```

### Team / User / Bundled Skill Paths

```python
from dcc_mcp_core import (
    get_team_skill_paths_from_env, get_user_skill_paths_from_env,
    get_app_team_skill_paths_from_env, get_app_user_skill_paths_from_env,
    get_team_skills_dir, get_user_skills_dir,
    get_bundled_skills_dir, get_bundled_skill_paths,
    copy_skill_to_team_dir, copy_skill_to_user_dir,
)

# Env-var readers for team / user skill paths
get_team_skill_paths_from_env()          # DCC_MCP_TEAM_SKILL_PATHS
get_user_skill_paths_from_env()          # DCC_MCP_USER_SKILL_PATHS
get_app_team_skill_paths_from_env("maya")   # DCC_MCP_MAYA_TEAM_SKILL_PATHS
get_app_user_skill_paths_from_env("maya")   # DCC_MCP_MAYA_USER_SKILL_PATHS

# Platform directories
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 package)
get_bundled_skill_paths() # -> List[str]  (list containing bundled dir when it exists)

# Copy a skill directory into team / user scope
team_path = copy_skill_to_team_dir("/path/to/my-skill")   # -> str (destination path)
user_path = copy_skill_to_user_dir("/path/to/my-skill")   # -> str (destination path)
```

---

## Scheduler (`dcc_mcp_core`)

Declarative scheduling for recurring or event-driven skill execution.

```python
from dcc_mcp_core import ScheduleSpec, TriggerSpec, parse_schedules_yaml

# Load schedule definitions from a YAML file
schedules: list[ScheduleSpec] = parse_schedules_yaml("/path/to/schedules.yaml")
for spec in schedules:
    print(spec.name, spec.cron, spec.trigger)
```

**`ScheduleSpec` fields:** `.name`, `.cron` (optional), `.trigger` (`TriggerSpec|None`), `.tool` (str), `.inputs` (dict), `.enabled` (bool).

**`TriggerSpec` fields:** `.event` (str), `.condition` (str|None).

---

## File Logging (`dcc_mcp_core`)

Rolling file logging for durable, multi-process debugging. Console output (stderr) is unaffected.

```python
from dcc_mcp_core import FileLoggingConfig, init_file_logging, shutdown_file_logging, flush_logs

# Quick start — reads DCC_MCP_LOG_* env vars
log_dir = init_file_logging()   # returns resolved directory

# Explicit configuration
init_file_logging(FileLoggingConfig(
    directory="./logs",
    file_name_prefix="maya-mcp",
    max_size_bytes=5 * 1024 * 1024,  # 5 MiB
    max_files=10,
    rotation="both",  # "size", "daily", or "both"
))

# Swap at runtime (idempotent)
init_file_logging(FileLoggingConfig.from_env())

# Flush buffered events to disk immediately (issue #402)
flush_logs()

# Uninstall; console stays active
shutdown_file_logging()
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `directory` | `str \| None` | platform log dir | Target directory for log files |
| `file_name_prefix` | `str` | `"dcc-mcp"` | File-name stem (`<prefix>.<YYYYMMDD>.log`) |
| `max_size_bytes` | `int` | `10485760` (10 MiB) | Max bytes before size-triggered rotation |
| `max_files` | `int` | `7` | Retention cap for rolled files (current excluded) |
| `rotation` | `str` | `"both"` | `"size"`, `"daily"`, or `"both"` |
| `include_console` | `bool` | `True` | Reserved; console always active today |

**Functions:**
- `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. `tracing_appender` batches writes on a background thread; this bypasses the async channel and forces an OS-level flush. No-op when no file layer is installed. (issue #402)

**Multi-instance isolation**: `DccServerBase` appends the PID to the log file prefix:
`dcc-mcp-<dcc_name>.<pid>.<YYYYMMDD>.log` — so two Maya processes never share a file.

**Environment variables** (read by `FileLoggingConfig.from_env()`):
- `DCC_MCP_LOG_DIR` — directory override
- `DCC_MCP_LOG_FILE_PREFIX` — prefix override
- `DCC_MCP_LOG_MAX_SIZE` — size override (bytes)
- `DCC_MCP_LOG_MAX_FILES` — retention cap override
- `DCC_MCP_LOG_ROTATION` — policy override (`size`/`daily`/`both`)

---

## Constants (`dcc_mcp_core`)

| Constant | Value | Purpose |
|----------|-------|---------|
| `APP_NAME` | `"dcc-mcp"` | Application name |
| `APP_AUTHOR` | `"dcc-mcp"` | Application author |
| `DEFAULT_DCC` | `"python"` | Default DCC type when none specified |
| `DEFAULT_LOG_LEVEL` | `"DEBUG"` | Default log level |
| `DEFAULT_MIME_TYPE` | `"text/plain"` | Default MIME type for resources |
| `DEFAULT_VERSION` | `"1.0.0"` | Default action version |
| `SKILL_METADATA_FILE` | `"SKILL.md"` | Skill package manifest filename |
| `SKILL_SCRIPTS_DIR` | `"scripts"` | Skill scripts subdirectory name |
| `SKILL_METADATA_DIR` | `"metadata"` | Skill metadata subdirectory name |
| `ENV_SKILL_PATHS` | `"DCC_MCP_SKILL_PATHS"` | Environment variable for skill search paths |
| `ENV_LOG_LEVEL` | `"DCC_MCP_LOG_LEVEL"` | Environment variable for log level override |
| `ENV_DISABLE_ACCUMULATED_SKILLS` | `"DCC_MCP_DISABLE_ACCUMULATED_SKILLS"` | Set to `"1"` to disable accumulated/evolved skills discovery |
| `ENV_DISABLE_DEFAULT_SKILL_PATHS` | `"DCC_MCP_DISABLE_DEFAULT_SKILL_PATHS"` | Set to `"1"` to omit implicit operator-owned skill roots while preserving explicit, bundled, and env paths |
| `ENV_TEAM_SKILL_PATHS` | `"DCC_MCP_TEAM_SKILL_PATHS"` | Environment variable for team-level skill paths |
| `ENV_USER_SKILL_PATHS` | `"DCC_MCP_USER_SKILL_PATHS"` | Environment variable for user-level skill paths |

---

## Environment Variables

| Variable | Description | Example |
|----------|-------------|---------|
| `DCC_MCP_SKILL_PATHS` | Skill search paths (`:` on Unix, `;` on Windows) | `/skills1:/skills2` |
| `DCC_MCP_DISABLE_DEFAULT_SKILL_PATHS` | Disable implicit operator-owned skill roots for hermetic CI/tests | `1` |
| `DCC_MCP_LOG_LEVEL` | Log level override | `INFO` / `DEBUG` / `WARN` |

---

## Lifecycle Hooks (`dcc_mcp_core.lifecycle_hooks`, #1337)

Typed, fail-safe observer/pub-sub framework for DCC adapter lifecycle events.
Handlers fire in registration order; exceptions are caught and logged (never abort host execution).
The only exception is `HookDeny` from **policy events**, which propagates to veto the operation.

### HookEvent

`str, Enum` of eight typed lifecycle hook points:

| Event | Type | Fires when |
|-------|------|------------|
| `SESSION_START` | Observation | Agent session begins |
| `BEFORE_SEARCH` | **Policy** | Before skill search executes |
| `AFTER_SEARCH` | Observation | After skill search completes |
| `BEFORE_SKILL_LOAD` | **Policy** | Before a skill is loaded |
| `AFTER_SKILL_LOAD` | Observation | After a skill is loaded |
| `BEFORE_TOOL_CALL` | **Policy** | Before a tool is invoked |
| `AFTER_TOOL_CALL` | Observation | After a tool call completes |
| `SESSION_END` | Observation | Agent session ends |

`HookEvent.policy_events()` returns `frozenset({BEFORE_SKILL_LOAD, BEFORE_TOOL_CALL, BEFORE_SEARCH})`.

### HookContext

Immutable payload passed to every handler:

```python
@dataclass(frozen=True)
class HookContext:
    event: HookEvent          # which event is firing
    dcc_name: str             # adapter identifier ("maya", "blender", "any")
    payload: dict[str, Any]   # event-specific data (mutable — before-handlers enrich in-place)
    session_id: str | None    # optional stable session identifier
```

### HookDeny

Exception raised by policy handlers to veto an operation:

```python
class HookDeny(Exception):
    def __init__(self, reason: str, *, hint: str | None = None): ...
```

Only meaningful for `HookEvent.policy_events()`. Non-policy events log and swallow `HookDeny`.

### LifecycleHooks

Bounded, fail-safe registry:

```python
class LifecycleHooks:
    def on(self, event: HookEvent, handler: Callable[[HookContext], Any]) -> Callable: ...
    def off(self, event: HookEvent, handler: Callable[[HookContext], Any]) -> bool: ...
    def handlers(self, event: HookEvent) -> tuple[Callable[[HookContext], Any], ...]: ...
    def dispatch(self, context: HookContext) -> None: ...
```

`on()` returns the handler for decorator usage. `off()` removes by identity (`is`). `handlers()` returns an immutable tuple snapshot.

### Usage Patterns

**Policy veto (block raw script when typed skill exists):**

```python
from dcc_mcp_core import LifecycleHooks, HookEvent, HookContext, HookDeny

hooks = LifecycleHooks()

@hooks.on(HookEvent.BEFORE_TOOL_CALL)
def enforce_typed_policy(ctx: HookContext) -> None:
    if "python_execute" in ctx.payload.get("tool_name", ""):
        raise HookDeny("typed skill available", hint="search and load the typed skill first")

server.register_lifecycle_hooks(hooks)
```

**Enrich search context:**

```python
@hooks.on(HookEvent.BEFORE_SEARCH)
def add_context(ctx: HookContext) -> None:
    ctx.payload.setdefault("tags", []).extend(["vetted", "production"])
    ctx.payload["dcc"] = "maya"
```

**Observe skill loading for analytics:**

```python
@hooks.on(HookEvent.AFTER_SKILL_LOAD)
def track_skill(ctx: HookContext) -> None:
    logger.info("skill_loaded", name=ctx.payload["skill_name"],
                actions=ctx.payload.get("registered_actions", 0))
```

**Fluent API:**

```python
server.register_lifecycle_hooks(
    LifecycleHooks()
    .on(HookEvent.SESSION_START, lambda ctx: init(ctx))
    .on(HookEvent.SESSION_END, lambda ctx: cleanup(ctx))
)
```

**DccServerBase integration:**
`DccServerBase` auto-dispatches `BEFORE_SEARCH` / `AFTER_SEARCH` in `search_skills()` and
`BEFORE_SKILL_LOAD` / `AFTER_SKILL_LOAD` in `load_skill()`. Public dispatch helpers:
`dispatch_session_start/end`, `dispatch_before/after_tool_call`.

---

## Agent Memory (`dcc_mcp_core.agent_memory`, #1334)

Three-tier memory model for DCC agents: **Ephemeral** → **Working** → **Longterm**.
The `MemoryStore` Protocol allows pluggable backends (in-memory, SQLite, file).

### MemoryLayer

```python
class MemoryLayer(str, Enum):
    EPHEMERAL = "ephemeral"   # session-scoped ring buffer, never persisted
    WORKING = "working"       # task-scoped, TTL-based expiry (default 6h)
    LONGTERM = "longterm"     # durable, cross-session, summarised facts only
```

### MemoryEntry

```python
@dataclass(frozen=True)
class MemoryEntry:
    layer: MemoryLayer
    key: str                        # e.g. "skill_loaded:modeling", "tool_call:export:ok"
    session_id: str
    dcc_name: str
    payload: Mapping[str, Any]      # JSON-safe; raw prompts NEVER stored
    created_unix_secs: float
    score: float = 1.0              # >0 success, <0 failure
```

### MemoryQuery

```python
@dataclass(frozen=True)
class MemoryQuery:
    layer: MemoryLayer | None = None     # None = all layers
    session_id: str | None = None
    dcc_name: str | None = None
    key_prefix: str | None = None        # startswith match
    limit: int = 16
```

### MemoryStore (Protocol)

```python
class MemoryStore(Protocol):
    def put(self, entry: MemoryEntry) -> None: ...
    def query(self, q: MemoryQuery) -> tuple[MemoryEntry, ...]: ...
    def forget(self, *, session_id: str | None = None, layer: MemoryLayer | None = None) -> int: ...
```

### InMemoryMemoryStore

Threadsafe in-memory implementation with bounded retention:

```python
store = InMemoryMemoryStore(
    ephemeral_cap_per_session=256,    # ring-buffer per session
    working_cap_per_session=1024,
    working_ttl_secs=6 * 60 * 60,     # 6h TTL
    longterm_cap_total=4096,          # global cap
)
```

### MemoryRecorder

Bridge between `LifecycleHooks` events and a `MemoryStore`:

```python
recorder = MemoryRecorder(
    store,
    enabled=True,
    summary_limit=8,
    promote_on_session_end=True,  # compact WORKING → LONGTERM on SESSION_END
)
recorder.install(hooks)  # registers handlers for 6 lifecycle events
```

**Automatic event handling:**

| Hook Event | Action |
|------------|--------|
| `SESSION_START` | Injects longterm `memory_summary` into context payload |
| `BEFORE_SEARCH` | Injects `memory_summary`, `memory_prefer_tools`, `memory_avoid_tools` |
| `AFTER_SKILL_LOAD` | Records EPHEMERAL entry (`skill_loaded:<name>`) |
| `BEFORE_TOOL_CALL` | Injects `memory_summary` into context |
| `AFTER_TOOL_CALL` | Records WORKING entry (`tool_call:<name>:ok`/`:fail`, score ±1.0) |
| `SESSION_END` | Compacts WORKING→LONGTERM patterns, purges session-scoped entries |

**Summarization:** `recorder.summarize(session_id=..., dcc_name=..., limit=8)` returns:
`recent_successes`, `prefer_tools`, `recent_failures`, `avoid_tools`, `escape_hatches`,
`missing_capabilities`, `skip_reasons`.

**Safety:** All payloads are redacted through `_safe_payload` (filters `api_key`, `authorization`,
`password`, `prompt`, `secret`, `token`; truncates strings to 512 chars). Raw prompts are never stored.

**Wiring example:**

```python
from dcc_mcp_core import LifecycleHooks, MemoryRecorder, InMemoryMemoryStore

hooks = LifecycleHooks()
store = InMemoryMemoryStore()
MemoryRecorder(store).install(hooks)
server.register_lifecycle_hooks(hooks)
```

---

## Built-in Skills (`dcc_mcp_core.skills.builtin`, #1332)

`register_all_builtin_skills()` registers all standard host-level MCP tools in one call.
Every DCC adapter should call this after `start()` for a complete baseline tool surface.

```python
from dcc_mcp_core.skills.builtin import register_all_builtin_skills

register_all_builtin_skills(
    server,
    dcc_name="maya",
    dcc_pid=os.getpid(),
    dcc_window_handle=hwd,
    dcc_window_title="Autodesk Maya 2025",
    gateway_failover_resolver=lambda: {"gateway_url": "http://127.0.0.1:9765"},
    skills=scanned_skills,  # optional — enables recipe and metadata-driven tools
)
```

**Six categories registered:**

| Category | Tool prefix | Description |
|----------|-------------|-------------|
| Diagnostics | `dcc_diagnostics__*` | Screenshot, audit log, metrics, gateway failover |
| Introspection | `dcc_introspect__*` | Signature, search, evaluation |
| Feedback | `dcc_feedback__*` | Agent report / rationale |
| Recipes | `recipes__*` | List, search, get, validate, apply pre-configured parameter templates |
| Qt UI Inspector | `qt_ui__*` | Lazy-loaded widget tree inspection |
| Script Materialization | `materialize__*` | Ad-hoc script file creation with TTL/session scoping |

Registration is **idempotent** — calling it multiple times is safe. Skills parameter
allows re-registering with an updated scanned skill set (registration always appends).

**Bundled skill packages** ship inside the wheel under `dcc_mcp_core/skills/`:

```
dcc_mcp_core/skills/
├── ui-control/            # UI automation (activate, find, screenshot, snapshot)
├── dcc-diagnostics/   # Diagnostics (audit, error report, metrics, process)
├── media/             # Media processing (probe, transcode, thumbnail)
├── workflow/          # Workflow orchestration (run chains)
├── builtin.py         # register_all_builtin_skills
└── qt_ui_inspector.py # Qt widget tree inspector
```

Retrieve bundled paths:
```python
from dcc_mcp_core import get_bundled_skills_dir, get_bundled_skill_paths
paths = get_bundled_skill_paths()       # include bundled skills
paths = get_bundled_skill_paths(False)  # opt-out
```

---

## Typical Integration Patterns

### Pattern 1: Register skill-based actions with an MCP server

```python
import os
from dcc_mcp_core import scan_and_load, ToolRegistry, ToolDefinition, ToolAnnotations

os.environ["DCC_MCP_SKILL_PATHS"] = "/opt/skills"
skills, _ = scan_and_load(dcc_name="maya")
reg = ToolRegistry()
tools = []
for skill in skills:
    for script in skill.scripts:
        from pathlib import Path
        stem = Path(script).stem
        action_name = f"{skill.name.replace('-', '_')}__{stem}"
        reg.register(name=action_name, description=skill.description, dcc=skill.dcc)
        tools.append(ToolDefinition(
            name=action_name,
            description=skill.description,
            input_schema='{"type": "object"}',
        ))
```

### Pattern 2: IPC call to a running DCC via DccLink

```python
import json
from dcc_mcp_core import IpcChannelAdapter, DccLinkFrame, success_result, error_result

# Connect to a DCC process via named IPC channel
client = IpcChannelAdapter.connect("maya-ipc")
try:
    # Send a Call frame and wait for a Reply
    req = DccLinkFrame(msg_type=1, seq=0, body=json.dumps({"method": "execute_python", "params": "import maya.cmds; print(maya.cmds.ls())"}).encode())
    client.send_frame(req)
    reply = client.recv_frame()
    if reply.msg_type == 2:  # Reply
        output = reply.body.decode()
        r = success_result(output, prompt="Objects listed. You can now select one to modify.")
    else:  # Err (msg_type=3)
        r = error_result("DCC script failed", reply.body.decode())
finally:
    client.shutdown()
```

### Pattern 3: Sandbox AI-generated code execution

```python
from dcc_mcp_core import SandboxPolicy, SandboxContext, InputValidator

policy = SandboxPolicy()
policy.allow_actions(["create_sphere", "list_objects", "get_scene_info"])
policy.allow_paths(["/project/assets"])
policy.set_timeout_ms(5000)
policy.set_max_actions(50)

ctx = SandboxContext(policy)
ctx.set_actor("claude-agent")

result_json = ctx.execute_json("create_sphere", '{"radius": 1.0, "name": "mySphere"}')
print(f"Executed {ctx.action_count} actions")
for entry in ctx.audit_log.entries():
    print(f"{entry.action}: {entry.outcome} ({entry.duration_ms}ms)")
```

### Pattern 4: Zero-copy scene data transfer

```python
import json
from dcc_mcp_core import PySharedSceneBuffer, PySceneDataKind, IpcChannelAdapter, DccLinkFrame

# In DCC (producer)
mesh_bytes = get_mesh_data_as_bytes()
ssb = PySharedSceneBuffer.write(mesh_bytes, kind=PySceneDataKind.Geometry, use_compression=True)
desc = ssb.descriptor_json()
# Send desc via IPC channel to agent
client = IpcChannelAdapter.connect("maya-ipc")
client.send_frame(DccLinkFrame(msg_type=6, seq=0, body=json.dumps({"topic": "mesh_ready", "desc": desc}).encode()))
```

---

## MCP HTTP Server (`dcc_mcp_core`)

The `dcc-mcp-http` crate provides a **MCP Streamable HTTP server** compliant with the **2025-03-26 MCP
specification**. It uses axum + Tokio and runs in a background thread, making it safe to call from any
DCC main thread.

> **MCP Spec Roadmap**: The MCP specification has evolved significantly since the 2025-03-26 version this library implements:
>
> | Version | Key Changes | Status |
> |---------|-------------|--------|
> | 2025-03-26 | Streamable HTTP, Tool Annotations, OAuth 2.1 | **Implemented** |
> | 2025-06-18 | Structured Tool Output, Elicitation (server asks user for info), Resource Links in tool results, JSON-RPC batching **removed**, `MCP-Protocol-Version` header mandatory | Planned |
> | 2025-11-25 | Icon metadata, Tasks (experimental), Sampling with tool calls, JSON Schema 2020-12, enhanced OAuth | Planned |
>
> **2026 MCP Roadmap** (announced March 2026) — four priority areas:
> 1. **Transport scalability**: `.well-known` server capability discovery, stateless session model for horizontal scaling
> 2. **Agent communication**: Tasks primitive lifecycle (experimental in 2025-11-25), retry/expiration semantics pending
> 3. **Governance**: contributor ladder, delegated workgroup model for faster SEP review
> 4. **Enterprise readiness**: audit trails, SSO integration, gateway behavior (mostly as extensions, not core spec changes)
>
> No new official transport types will be added in the 2026 cycle — only evolution of Streamable HTTP.
>
> When these features land in `dcc-mcp-core`, they will be exposed via `McpHttpServer` —
> do NOT implement these manually.

### McpHttpConfig

```python
from dcc_mcp_core import McpHttpConfig

config = McpHttpConfig(
    port=0,                 # default: OS-assigned instance port
    server_name="maya-mcp", # reported in MCP initialize response
    server_version="1.0.0",
    enable_cors=False,      # set True for browser-based MCP clients
    request_timeout_ms=30000,
)
print(config.port)          # 0 before start; read handle.port after binding
print(config.server_name)   # "maya-mcp"
print(config.server_version) # "1.0.0"
```

```python

# Opt-in: lazy-actions fast-path — tools/list surfaces only 3 meta-tools
# (list_actions, describe_action, call_action) instead of all tools
config.lazy_actions = True  # default: False

# Gateway/admin participation (Python defaults: gateway_port=9765, admin on)
config.gateway_port = 9765      # first process to bind wins; 0 disables gateway/admin
config.admin_enabled = True     # default; set False to keep gateway but hide /admin
config.admin_path = "/admin"    # default
# Durable admin audit/trace storage is env-only: set DCC_MCP_GATEWAY_AUDIT_DIR
# to persist audit.jsonl + traces.jsonl; DCC_MCP_GATEWAY_AUDIT_MAX_ROWS caps rows.
config.dcc_type = "maya"
config.dcc_version = "2025"
config.scene = "/proj/shot01.ma"  # optional: helps routing by scene

# Session management
config.session_ttl_secs = 3600  # idle session eviction (0 = disable)

# Bare tool names (#307) — default True; emits "execute_python" instead of
# "maya-scripting.execute_python" when the bare name is unique. Collisions
# fall back to the full <skill>__<action> form.
config.bare_tool_names = True   # set False to force skill-qualified names
```

### McpHttpServer

```python
from dcc_mcp_core import ToolRegistry, McpHttpServer, McpHttpConfig, McpServerHandle

registry = ToolRegistry()
registry.register(
    "get_scene_info",
    description="Get info about the current scene",
    category="scene", tags=["query"], dcc="maya", version="1.0.0",
)
registry.register(
    "create_sphere",
    description="Create a polygon sphere",
    category="geometry", tags=["create"], dcc="maya", version="1.0.0",
    input_schema='{"type":"object","required":["radius"],"properties":{"radius":{"type":"number"}}}',
)

# Start server — returns immediately, server runs in background
server = McpHttpServer(registry, McpHttpConfig(server_name="maya-mcp"))
handle = server.start()   # -> McpServerHandle (alias for McpServerHandle)
```

### McpServerHandle

```python
# McpServerHandle is the alias for McpServerHandle in __init__.py
from dcc_mcp_core import McpServerHandle

print(handle.mcp_url())      # "http://127.0.0.1:8765/mcp"  (MCP endpoint URL)
print(handle.port)           # 8765 (actual port; useful when McpHttpConfig(port=0))
print(handle.bind_addr)      # "127.0.0.1:8765"

handle.shutdown()            # blocks until server stops
handle.signal_shutdown()     # non-blocking shutdown signal
```

### create_skill_server (Skills-First, recommended)

```python
import os
os.environ["DCC_MCP_MAYA_SKILL_PATHS"] = "/studio/maya-skills"

from dcc_mcp_core import create_skill_server, McpHttpConfig

# One call: creates registry, dispatcher, SkillCatalog, discovers skills from env vars
server = create_skill_server("maya", McpHttpConfig())
handle = server.start()
print(f"Maya MCP server at {handle.mcp_url()}")
# Claude Desktop config: {"mcpServers": {"maya": {"url": "<handle.mcp_url()>"}}}

# On-demand skill discovery workflow (agent-driven):
# 1. tools/list → 5 core tools + __skill__<name> stubs for unloaded skills
#    Core tools: list_skills, get_skill_info, load_skill, unload_skill, search_skills
# 2. search_skills(query="bevel") → compact one-line-per-skill results matching search_hint/tool names
# 3. load_skill("maya-bevel") → registers tools + handlers, sends tools/list_changed
# 4. tools/list → new skill tools visible with full schemas
# 5. tools/call maya_bevel__bevel {offset: 0.1} → runs scripts/bevel.py

count = server.discover()               # scan again (optional)
skills = server.list_skills()           # list all with status
server.load_skill("hello-world")        # registers skill tools into ToolRegistry
handle.shutdown()
```

### Full Example: Manual Registry Wiring (low-level)

```python
import os
from pathlib import Path
from dcc_mcp_core import (
    scan_and_load, ToolRegistry, ToolDispatcher,
    McpHttpServer, McpHttpConfig,
    success_result, error_result,
)

os.environ["DCC_MCP_SKILL_PATHS"] = "/opt/maya-skills"
skills, skipped = scan_and_load(dcc_name="maya")

registry = ToolRegistry()
dispatcher = ToolDispatcher(registry)

for skill in skills:
    for script_path in skill.scripts:
        stem = Path(script_path).stem
        action_name = f"{skill.name.replace('-', '_')}__{stem}"
        registry.register(
            name=action_name,
            description=f"[{skill.name}] {skill.description}",
            dcc=skill.dcc, tags=skill.tags, version=skill.version,
        )
        def make_handler(sp):
            def handler(params):
                import subprocess
                proc = subprocess.run(["python", sp], capture_output=True, text=True, timeout=30)
                if proc.returncode == 0:
                    return success_result(proc.stdout.strip(), prompt="Script finished successfully.")
                return error_result("Script failed", proc.stderr.strip())
            return handler
        dispatcher.register_handler(action_name, make_handler(script_path))

server = McpHttpServer(registry, McpHttpConfig(port=0, server_name="maya-mcp"))
handle = server.start()
print(f"MCP server ready: {handle.mcp_url()}")
# Claude Desktop config: {"mcpServers": {"maya": {"url": "<handle.mcp_url()>"}}}
```

---

## Host Execution Readiness (#1331, #1206)

`AdapterReadinessBinder` publishes a unified `ReadinessProbe` that gates MCP `tools/call`,
REST `/v1/readyz`, and REST `/v1/call` on a shared set of readiness conditions.
This ensures agents and the admin dashboard can see whether a DCC adapter is truly
ready to execute tools.

### Readiness States

`/v1/readyz` returns a 3-state readiness payload:

| State | HTTP | Meaning |
|-------|------|---------|
| `ready` | 200 | All conditions satisfied — tools can be called |
| `booting` | 503 | Adapter is starting up — retry after a few seconds |
| `not-ready` | 503 | Critical condition not met — check the probe details |

### Readiness Bits

Each condition is a named boolean bit in the probe:

| Bit | Description |
|-----|-------------|
| `process` | DCC process is alive and responding |
| `dcc` | DCC application is fully initialized |
| `catalog` | Skill catalog has finished discovery |
| `dispatcher` | Main-thread dispatcher is installed and pumping |
| `host_bridge` | `HostExecutionBridge` is wired |
| `main_thread_executor` | In-process executor is ready (embedded DCC only) |

### Binding Patterns

```python
from dcc_mcp_core import AdapterReadinessBinder

# Pattern 1: Inline (embedded DCC — Maya, Blender, Houdini)
AdapterReadinessBinder.bind_inline(server)
# Sets process=true, dcc=true immediately; catalog/dispatcher set later

# Pattern 2: Headless (mayapy, hython, standalone)
AdapterReadinessBinder.bind_headless(server)
# Process, dcc, catalog all true; no dispatcher needed

# Pattern 3: Queue dispatcher (GUI hosts with main-thread queue)
AdapterReadinessBinder.bind_queue_dispatcher(server, dispatcher, require_first_pump=True)
# Dispatcher bit set after first successful pump tick
```

Call `.bind_*()` **before** `server.start()` — the probe is read at startup.
Use `readiness_report_subset(...)` in tests to compare stable bit subsets without
timing sensitivity.

### Admin UI Integration

The gateway admin dashboard (`GET /admin`) shows per-instance readiness status.
The elected gateway probes each backend's `/v1/readyz` on startup and periodically,
surfacing `booting` / `not-ready` instances with the specific failing bits so
operators can diagnose why a DCC adapter isn't accepting tool calls.

---

## Workflow Primitive (`dcc_mcp_workflow`, issue #348)

**Status**: fully implemented. Spec-driven pipeline engine with six step
kinds, step-level policies (retry / timeout / idempotency), artefact
hand-off, cancellation cascade, and SQLite persistence.

Opt-in via the top-level `workflow` Cargo feature (off by default).
Python surface: `WorkflowSpec`, `WorkflowStep`, `StepPolicy`,
`RetryPolicy`, `BackoffKind`, `WorkflowStatus` — or `None` when the wheel
was built without the feature.

### Rust types

```rust
use dcc_mcp_workflow::{
    WorkflowSpec, Step, StepKind, StepId, WorkflowId,
    WorkflowStatus, WorkflowJob, WorkflowProgress,
    WorkflowCatalog, WorkflowSummary,
    register_builtin_workflow_tools, register_workflow_handlers,
    WorkflowExecutor, WorkflowHost, WorkflowError,
};
```

All structural types are `Serialize + Deserialize + Clone`. IDs are
newtypes with `#[serde(transparent)]`.

#### `WorkflowSpec`

```rust
let spec = WorkflowSpec::from_yaml(yaml_source)?;
spec.validate()?;   // unique step ids, client-safe tool names, JSONPath parse
```

Validation uses `dcc_mcp_naming::validate_tool_name` for tool refs and
`jsonpath_rust::parser::parse_json_path` for `branch.on` / `foreach.items`.

#### `WorkflowExecutor`

```rust
let handle = WorkflowExecutor::run(spec, inputs, parent_job_id)?;
// handle: WorkflowRunHandle { workflow_id, root_job_id, cancel_token, join }
```

Execution pipeline: `spec → validate → spawn driver → drive(steps) →
per-step policy (retry+timeout+idempotency) → dispatch by kind →
artefact handoff → SSE $/dcc.workflowUpdated → sqlite upsert → next
step`.

#### Step kinds

| Kind | Driver | Policy knobs |
|------|--------|--------------|
| `tool` | `ToolCaller::call(name, args)` | timeout, retry, idempotency_key |
| `tool_remote` | `RemoteCaller::call(dcc, name, args)` | same |
| `foreach` | JSONPath → body per item | per-body policy inherited |
| `parallel` | `tokio::join!` over branches | `on_any_fail: abort \| continue` |
| `approve` | `ApprovalGate::wait_handle` + timeout | timeout_secs |
| `branch` | JSONPath condition → `then` or `else` | n/a |

#### Cancellation cascade

Root `CancellationToken` handed to every step driver. On `cancel`:
1. No new steps start.
2. In-flight callers receive the token.
3. Sleeps (retry backoff, `Approve` timeout) aborted via `tokio::select!`.
4. Final `$/dcc.workflowUpdated` with `status: cancelled`.

#### `WorkflowCatalog`

Reads `SkillMetadata.metadata["dcc-mcp.workflows"]` (glob or
comma-separated globs) relative to `skill_root`:

```rust
let cat = WorkflowCatalog::from_skill(&skill_meta, &skill_root)?;
let hits: Vec<_> = cat.search("vendor intake").into_iter().collect();
```

### Step policies (issue #353)

Every step may declare an optional `policy` block:

| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `timeout_secs` | `u64 > 0` | none | Per-attempt wall-clock deadline. |
| `retry.max_attempts` | `u32 >= 1` | required if retry present | `1` = no retry. |
| `retry.backoff` | enum | `exponential` | `fixed` / `linear` / `exponential`. |
| `retry.initial_delay_ms` | `u64` | `500` | `<= max_delay_ms`. |
| `retry.max_delay_ms` | `u64` | `10_000` | Upper clamp after shape + jitter. |
| `retry.jitter` | `f32` | `0.0` | Clamped to `[0.0, 1.0]`. |
| `retry.retry_on` | `[String]` | all errors | Error-kind allowlist. |
| `idempotency_key` | string | none | Mustache template rendered before execution. |
| `idempotency_scope` | enum | `workflow` | `workflow` or `global`. |

Backoff formula: `min(base(n), max_delay) * (1 + rand(-jitter, +jitter))`
where `base` is `initial_delay` (fixed), `initial_delay * (n-1)` (linear),
or `initial_delay * 2^(n-2)` (exponential).

### Built-in MCP tools

Registered by `register_builtin_workflow_tools(&registry)` + bound by
`register_workflow_handlers(&dispatcher, &host)`:

| Tool | Description | ToolAnnotations |
|------|-------------|-----------------|
| `workflows_run` | Start a run (YAML or JSON spec + inputs). | `destructive_hint=true, open_world_hint=true` |
| `workflows_get_status` | Poll terminal status + progress. | `read_only_hint=true, idempotent_hint=true` |
| `workflows_cancel` | Cancel a run by `workflow_id` (cascade). | `destructive_hint=true, idempotent_hint=true` |
| `workflows_lookup` | Catalog search (read-only). | `read_only_hint=true` |

### Artefact hand-off (issue #349)

A tool whose output contains `file_refs` array is captured to
`ArtefactStore`. Downstream steps reference via
`{{steps.<id>.file_refs[<i>].uri}}`.

### Persistence DDL (`job-persist-sqlite` feature)

```rust
dcc_mcp_workflow::sqlite::apply_migrations(&conn)?;
```

Creates `workflows` + `workflow_steps`. On startup,
`WorkflowExecutor::recover_persisted()` flips non-terminal rows to
`Interrupted`. Runs are **not** auto-resumed.

### Python

```python
from dcc_mcp_core import (
    WorkflowSpec, WorkflowStep, StepPolicy,
    RetryPolicy, BackoffKind, WorkflowStatus,
)

spec = WorkflowSpec.from_yaml_str(yaml_source)
spec.validate()        # raises ValueError on failure

step: WorkflowStep = spec.steps[0]
policy: StepPolicy = step.policy
retry: RetryPolicy = policy.retry
assert retry.next_delay_ms(2) == 500   # first retry delay (unjittered)
```

All policy classes are **frozen**. To run workflows, call the MCP tools
(`workflows_run` / `workflows_get_status` / `workflows_cancel`) from the
MCP client side.

### HTTP server gate

```python
from dcc_mcp_core import McpHttpConfig
cfg = McpHttpConfig()
cfg.enable_workflows = True     # default False
```

### Discovery

Point SKILL.md at sibling workflow YAML files via a single metadata key:

```yaml
---
name: vendor-intake
description: "Nightly vendor intake; import, QC, export, handoff."
metadata:
  dcc-mcp:
    workflows: "workflows/*.workflow.yaml"
    workflows.search-hint: "vendor intake, nightly cleanup"
---
```

No new top-level SKILL.md field → `skills-ref validate` stays green.

---

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

Pure-Python modules that extend the MCP HTTP server towards cloud-hosted agents (Claude.ai, Cursor, ChatGPT, VS Code). All symbols re-exported from `dcc_mcp_core`. Per-module API reference lives in `docs/api/{auth,batch,elicitation,rich-content,plugin-manifest,dcc-api-executor}.md`; end-to-end deployment guide in `docs/guide/remote-server.md`.

### `dcc_mcp_core.auth` (issue #408)

Declarative auth configuration + Bearer-token validation.

- `ApiKeyConfig(api_key=None, env_var="DCC_MCP_API_KEY", header_name="Authorization")` — dataclass; `.resolve()` returns field → env var → None.
- `OAuthConfig(provider_url, client_id=None, scopes=[], client_name="dcc-mcp-server", redirect_uri=None)` — derived `.authorization_endpoint`, `.token_endpoint`, `.well_known_url`; `.to_cimd_document(redirect_uri=...)` builds `CimdDocument`.
- `CimdDocument(client_name, redirect_uris, grant_types=["authorization_code"], response_types=["code"], token_endpoint_auth_method="none", scope=None, logo_uri=None, client_uri=None, contacts=[])` — `.to_dict()` produces the JSON served from `GET /.well-known/oauth-client-metadata`.
- `validate_bearer_token(headers, *, expected_token, header_name="Authorization") -> bool` — constant-time compare via `secrets.compare_digest`; case-insensitive header lookup; `expected_token=None` disables auth with a warning.
- `generate_api_key(length=32) -> str` — URL-safe base64 from `secrets.token_urlsafe`.
- `hmac_sha256_hex(secret, payload) -> str` — HMAC-SHA256 digest as lowercase hex string.
- `verify_hub_signature_256(secret, payload, signature) -> bool` — constant-time verification of HMAC-SHA256 signatures (e.g., webhook / hub callbacks).
- `TokenValidationError` — exception type (reserved; `validate_bearer_token` returns bool).

Status: API key path ships today via `McpHttpConfig.api_key`. OAuth Rust-side enforcement (serving CIMD well-known + enforcing `/mcp` Bearer) tracked in #408.

### `dcc_mcp_core.batch` (issue #406)

Server-side batch + sandboxed eval — intermediate results never enter model context.

- `batch_dispatch(dispatcher, calls, *, aggregate="list", stop_on_error=False) -> dict`
  - `calls`: `list[tuple[str, dict]]`
  - `aggregate`: `"list"` / `"merge"` / `"last"` — added result key matches the mode
  - Returns always-present `total`, `succeeded`, `errors` plus the mode-specific payload
  - Failed calls produce `{"index": i, "tool": name, "error": str}` records; when `stop_on_error=True` the batch halts on the first failure
- `EvalContext(dispatcher, *, sandbox=True, timeout_secs=30)`
  - `.run(script: str)` wraps in a function body so top-level `return` works; last-expression is NOT implicitly returned
  - Sandbox strips `open`, `exec`, `eval`, `__import__`, `compile`, `getattr`, `setattr`, `delattr`, `vars`, `dir`, `globals`, `locals` from `__builtins__`
  - POSIX-only `SIGALRM` timeout; silently skipped on Windows
  - Inside script: `dispatch(tool_name, args_dict) -> dict` is available; `json` module pre-imported

Rust-level `tools/batch` + `dcc_mcp_core__eval` built-in MCP tools tracked in #406 — they will call through these same helpers.

### `dcc_mcp_core.elicitation` (issue #407)

Pause a tool mid-call to ask the user for input (MCP 2025-11-25 Elicitation spec).

- `ElicitationMode` enum: `FORM`, `URL`
- `FormElicitation(message, schema, title=None)` / `UrlElicitation(message, url, description=None)` — parameter dataclasses
- `ElicitationRequest(mode, params)` — wraps a mode + params pair
- `ElicitationResponse(accepted, data=None, message=None)` — returned by the helpers
- `await elicit_form(message, schema, *, title=None) -> ElicitationResponse` — async form
- `await elicit_url(message, url, *, description=None) -> ElicitationResponse` — async URL flow
- `elicit_form_sync(message, schema, *, title=None, fallback_values=None) -> ElicitationResponse` — blocking wrapper for DCC main-thread handlers; `fallback_values` (if provided) returns `accepted=True, message="fallback_values_used"`

Status: Rust-side `notifications/elicitation/request` + `notifications/elicitation/response` wiring tracked in #407. Until then, helpers log a warning and return `accepted=False, message="elicitation_not_supported"` — safe fallback lets handlers written today upgrade automatically.

### `dcc_mcp_core.rich_content` (issue #409, MCP Apps)

Inline chart / form / dashboard / image / table results rendered by MCP-Apps-aware clients.

- `RichContentKind` enum: `CHART`, `FORM`, `DASHBOARD`, `IMAGE`, `TABLE`
- `RichContent(kind, payload)` dataclass + class-method constructors:
  - `.chart(spec)` — Vega-Lite v5 / Chart.js spec dict
  - `.form(schema, *, title=None)` — JSON-Schema form (one-shot display; distinct from `elicit_form`)
  - `.image(data, mime="image/png", *, alt=None)` — base64-encodes raw bytes
  - `.image_from_file(path, mime=None, *, alt=None)` — auto-detects MIME from extension
  - `.table(headers, rows, *, title=None)` — grid
  - `.dashboard(components)` — composite layout of other `RichContent`
  - `.to_dict() -> dict` — flattens to `{"kind": value, **payload}`
- `attach_rich_content(result, content) -> dict` — sets `result["context"]["__rich__"] = content.to_dict()`; backward-compatible with plain clients
- Skill-script helpers (return ready-to-use skill dicts):
  - `skill_success_with_chart(message, chart_spec, **context) -> dict`
  - `skill_success_with_table(message, headers, rows, *, title=None, **context) -> dict`
  - `skill_success_with_image(message, image_data=None, image_path=None, mime="image/png", *, alt=None, **context) -> dict` — raises `ValueError` if both `image_data` and `image_path` are `None`

Rust-side MCP Apps envelope wiring tracked in #409. Today's `context.__rich__` placement is ignored by plain clients.

### `dcc_mcp_core.plugin_manifest` (issue #410)

Claude Code one-click plugin bundles.

- `PluginManifest(name, version, description, mcp_servers, skills, sub_agents=[])` dataclass
  - `.to_dict()` / `.to_json(indent=2)`
- `build_plugin_manifest(dcc_name, mcp_url, skill_paths=None, *, version="0.1.0", description=None, api_key=None, extra_mcp_servers=None, sub_agents=None) -> dict`
  - Plugin name auto-derives as `<dcc_name>-mcp`; description auto-generates when `None`
  - `api_key` → injected into `mcp_servers[0].headers.Authorization` as `Bearer <key>`
  - Non-existent `skill_paths` entries are dropped with a debug log
- `export_plugin_manifest(manifest, path, *, indent=2) -> Path` — writes JSON, creates parent dirs, returns resolved path
- Convenience: `DccServerBase.plugin_manifest(version="...")` auto-fills `mcp_url` + `skill_paths` from the running server

### `dcc_mcp_core.dcc_api_executor` (issue #411)

Two-tool Cloudflare-pattern wrapper — covers 1500+ DCC commands in ~500 tokens.

- `DccApiCatalog(dcc_name, commands=None, catalog_text=None)`
  - `commands`: `list[{name, signature, description}]`
  - `catalog_text`: plain-text `name - description` per line; `#` comments + blank lines ignored
  - `.add_command(name, *, signature="", description="")` — runtime append
  - `.search(query, *, limit=10)` — tokenised BM25-lite over `name + description + signature`; stopwords dropped (`the`, `a`, `an`, `in`, `for`, `of`); score-sorted then alphabetical
  - `len(catalog)` returns command count
- `DccApiExecutor(dcc_name, catalog=None, dispatcher=None)`
  - `.search(query, *, limit=10) -> dict` — `dcc_search` handler; zero-hit path returns `"results": []` + `"hint"`
  - `.execute(code, *, timeout_secs=30) -> dict` — `dcc_execute` handler; runs code via `EvalContext(sandbox=True)`, `dispatch()` available when dispatcher passed; handles `TimeoutError` + `RuntimeError` into structured error dicts
  - `.catalog` property exposes the underlying `DccApiCatalog`
- `register_dcc_api_executor(server, executor, *, search_tool_name="dcc_search", execute_tool_name="dcc_execute") -> None` — register both tools on `McpHttpServer` BEFORE `server.start()`

Typical agent flow: `dcc_search({"query": ...})` → narrow down commands → `dcc_execute({"code": "for i in range(5): dispatch('polySphere', {'position': [i,0,0]}); return {'created': 5}"})`. Only the final return value reaches the model.

Rust-level built-in `dcc_search` / `dcc_execute` tools tracked in #411 — they will call through these same handlers.

### Minimum `McpHttpConfig` for remote access

```python
cfg = McpHttpConfig(
    port=8765,
    host="0.0.0.0",              # bind to all interfaces (default "127.0.0.1")
    enable_cors=True,            # required for browser / Claude.ai / Cursor
    allowed_origins=["https://claude.ai", "https://cursor.sh"],
    spawn_mode="dedicated",      # always for PyO3-embedded DCC hosts
)
cfg.api_key = os.environ["DCC_MCP_API_KEY"]   # Bearer token
server = create_skill_server("maya", cfg)
handle = server.start()
```

See `docs/guide/remote-server.md` for the Docker / systemd / k8s / reverse-proxy TLS recipes and the `examples/remote-server/` deployable reference.

---

## Development

- Tool manager: [vx](https://github.com/loonghao/vx)
- Build: `maturin develop --features python-bindings,ext-module`
- Rust tests: `cargo test --workspace` (or `vx just test-rust`)
- Python tests: `vx just test` (requires build first: `vx just dev`)
- Lint: `vx just lint` (clippy + fmt-check + ruff + isort)
- Pre-flight: `vx just preflight` (check + clippy + fmt-check + test-rust)
- Full CI: `vx just ci`
- Release: [Release Please](https://github.com/googleapis/release-please) with Conventional Commits

## AI Agent Tool Priority Guide

When an AI agent needs to interact with DCC software, follow this priority order:

### 1. CLI + Skill Discovery (always start here)

Use `dcc-mcp-cli` for every shell-capable agent. Native MCP is the fallback for
MCP-only clients or an explicit user choice.

```bash
dcc-mcp-cli dcc-types
dcc-mcp-cli list
dcc-mcp-cli search --query "create sphere" --dcc-type maya
dcc-mcp-cli describe <tool-slug>
dcc-mcp-cli load-skill maya-geometry --dcc-type maya
dcc-mcp-cli call <tool-slug> --json '{"radius":2.0}'
```

If the 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`. The apply step stages the next CLI launch and
does not replace a running server.

### 2. Skill-Based Tools (preferred over raw API calls)
- Use skill tools (e.g. `maya_geometry__create_sphere`) — they have validated schemas, error handling, and `next-tools` guidance
- Check `ToolAnnotations` for safety hints before calling destructive tools
- Use `next-tools` from tool results to chain follow-up actions

### 3. Diagnostics Tools (for debugging/verification)
```python
dcc_diagnostics__screenshot    # verify visual state
dcc_diagnostics__audit_log     # check execution history
dcc_diagnostics__tool_metrics  # measure performance
dcc_diagnostics__process_status # check DCC process health
```

### 4. Direct Registry Access (last resort)
- Only when no skill tool covers the needed operation
- Must validate inputs with `ToolValidator` before execution
- Must use `SandboxPolicy` for AI-initiated calls

### Why Skills First?
1. **Safety**: Skills declare `ToolAnnotations` — agents can check `destructive_hint`, `read_only_hint`
2. **Discoverability**: `search_skills` + `search-hint` keywords find the right tool without trial-and-error
3. **Chainability**: `next-tools` guides follow-up actions, reducing hallucination
4. **Progressive exposure**: Tool groups keep `tools/list` small — agents activate only what they need
5. **Validation**: Skill tools have `input_schema` — parameters are validated before execution

### SKILL.md Description Quality
The `description` field (1-1024 chars) should describe **what the skill does AND when to use it**:
- 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).

---

## Related Projects

- [dcc-mcp-maya](https://github.com/dcc-mcp/dcc-mcp-maya) — Maya MCP server implementation

---

## New in this milestone (issues #764–#774)

### Rust Gateway: Middleware Chain (#770)

```rust
use dcc_mcp_gateway::gateway::middleware::{
    AuditMiddleware, MiddlewareChain, QuotaMiddleware, RedactionMiddleware,
};
use std::sync::Arc;

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

Custom: implement `BeforeCallMiddleware` / `AfterCallMiddleware` traits.
`CallContext`: `method`, `tool_slug`, `dcc_type`, `session_id`, `request_id`, `args`, `metadata`.

### Rust Gateway: 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"),
)
```

Auth types: `bearer()`, `api_key()`, `basic()`. `$ENV_VAR` references resolved at call time.

### Rust Server: 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
# Add --expose-sse true only for legacy /sse clients.
# Flags: --no-register, --restart-on-exit false, --max-restarts N (default 10)
```

### Gateway Admin Dashboard (#772)

Default contract: the elected gateway serves one read-only Admin UI at `/admin`; non-winning instances never serve admin. `dcc-mcp-server` and Python `McpHttpConfig` both default to `gateway_port=9765` and admin enabled.

```bash
# server.exe / dcc-mcp-server
# default: first process wins gateway/admin at http://127.0.0.1:9765/admin
dcc-mcp-server --app maya

# disable gateway and admin
dcc-mcp-server --gateway-port 0

# keep gateway, hide admin
dcc-mcp-server --no-admin
```

```python
cfg = McpHttpConfig(port=0)
cfg.gateway_port = 0        # disable gateway/admin
cfg.admin_enabled = False   # keep gateway, hide admin
```

```rust
GatewayConfig { admin_enabled: true, admin_path: "/admin".into(), .. } // defaults
// direct dcc-mcp-gateway users need feature "admin"; dcc-mcp-http/server enable it
// routes: GET /admin (HTML), GET /admin/api/{instances,tools,calls,traces,traces/{request_id},stats,workers,logs,health}
```

### OTLP Distributed Tracing (#768)

```bash
# Environment variable (auto-activates OTLP)
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 dcc-mcp-server ...
```

```rust
// Programmatic (requires otlp-exporter Cargo feature)
TelemetryConfig::default().with_otlp_exporter("http://localhost:4317").init()?;
```

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)

```python
# MCP tools auto-registered on the gateway
tools.call("dcc_catalog__search", {"query": "maya"})
tools.call("dcc_catalog__describe", {"name": "dcc-mcp-maya-skills"})
```

```bash
dcc-mcp-server catalog search --query maya
dcc-mcp-server catalog describe --name dcc-mcp-maya-skills
# Override: DCC_MCP_CATALOG_PATH=/path/to/dcc-mcp-catalog.yml
```

### Gateway Observability (#766)

```
# Prometheus (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 (always available)
resources://gateway/events  →  JSONL ring buffer, last 1000 contention events
```

### Payload Limits (#771)

```rust
// Rust McpHttpConfig sub-config (queue)
McpHttpConfig.queue.max_request_body_bytes     // default 4 MiB → 413 on overflow
McpHttpConfig.queue.max_response_content_bytes // default 1 MiB → TruncationEnvelope
McpHttpConfig.queue.sse_chunk_size_bytes        // default 64 KiB → auto SSE chunking
```

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

Rust `McpHttpConfig` is a thin aggregate in `dcc-mcp-http-types::config` with public sub-config fields (`server`, `instance`, `session`, `gateway`, `queue`, `telemetry`, `features`, `workflow`, `job`) plus compatibility accessors/builders. Python `PyMcpHttpConfig` field-level getters/setters are **unchanged**. Pure wire/config/value types that do not need axum/PyO3 live in `dcc-mcp-http-types` and remain re-exported from `dcc-mcp-http`; reusable runtime support (core tool builders, sessions, executors, in-flight request state, notifications, workspace roots) lives in `dcc-mcp-http-server`; the PyO3 HTTP binding boundary lives in `dcc-mcp-http-py`.
