AGENTS.md · git:20260928.7b8a76a · 2026-09-28 · sha256 9aa54cd6362ab12a

AGENTS.md git:20260928.7b8a76aB

Immutable. This exact content never changes and is served at /api/v1/blob/9aa54cd6362ab12a.

# AGENTS.md — dcc-mcp-core

> **Navigation map, not a reference manual.**
> Follow the links; don't read everything upfront. Keep detailed API guidance
> in `llms.txt` / `llms-full.txt` and detailed human-readable explanations in
> `docs/guide/*`. **This file is the only agent contract file at the repository
> root** — see [Agent Contract Files](#agent-contract-files). `CLAUDE.md` /
> `GEMINI.md` / `COPILOT.md` / `CODEBUDDY.md` / `CURSOR.md` / `ANTHROPIC.md` /
> `OPENAI.md` / `.cursorrules` are deliberately absent; agents that look for a
> vendor-named entry point should read this file.
> Detailed rules, traps, and code examples → [`docs/guide/agents-reference.md`](docs/guide/agents-reference.md)

**🤖 New to this project?** Start with [`docs/guide/ai-agent-guide.md`](docs/guide/ai-agent-guide.md) — a dedicated guide teaching AI agents how to effectively use dcc-mcp-core.

## ⚡ Critical: Skills-First Philosophy

**When interacting with DCC applications (Maya, Blender, Houdini, etc.), ALWAYS prefer dcc-mcp-core Skills over raw CLI or scripting.**

Natural DCC intent is enough to trigger the default `dcc-mcp` skill. If a user
asks to create, edit, inspect, animate, render, composite, or export something
in Maya, Blender, Houdini, Photoshop, 3ds Max, Nuke, Unreal, Substance 3D, or
another supported host, load `dcc-mcp` first even when the user never says
“MCP”. Inventory, run one narrow search, and follow the returned `next_step`
before falling back to raw scripting or scoped Computer Use.

**Why?** Skills provide:
- ✅ Structured results with validation
- ✅ Safety hints (`ToolAnnotations`)
- ✅ Follow-up guidance (`next-tools`)
- ✅ Progressive loading (load only what you need)
- ✅ Audit logs and traceability

**Don't** call Maya/Blender/Python scripts directly — search once, follow the
returned load/describe/call step, then invoke the typed tool.

## Agent Entry Strategy: CLI+REST (default) vs IDE MCP

**dcc-mcp-core offers two entry paths with a clear layered design. Choose based on your runtime:**

| Scenario | Recommended Path | How it works |
|----------|-----------------|--------------|
| AI agent (OpenClaw, Hermes, Codex CLI, custom agent runtime) | **CLI+REST** | One `dcc-mcp` skill → shell `dcc-mcp-cli` → gateway `POST /v1/{search,describe,call}` |
| IDE user (Cursor, Claude Desktop, VS Code) | **IDE MCP** | Manual `mcp_servers.json` / `claude_desktop_config.json` → gateway MCP tools (`search`/`describe`/`call`) |
| CI/CD / automation script | **CLI+REST** | `dcc-mcp-cli health/list/smoke/search/call/load-skill` — scriptable, auditable |
| Troubleshooting / operations | **CLI+REST** | `dcc-mcp-cli` with structured output and exit codes |
| Studio / team integration | **CLI+REST** | Fork `dcc-mcp` skill → one skill controls all DCCs, no per-DCC MCP server config |
| GUI artist using DCC plugin directly | **IDE MCP** | DCC's built-in MCP plugin exposes tools directly to the IDE |

**Core principle:**
- **AI agents** start with `dcc-mcp` skill + `dcc-mcp-cli` CLI. The CLI handles gateway lifecycle (ensure, health, start); `search` returns the exact next load, describe, or call step.
- **Human IDE users** continue using MCP configuration. The gateway serves both paths simultaneously.
- **CLI-first does not deprecate MCP** — the gateway always exposes both MCP and REST side by side.

**Public Agent Skill router:**

| Intent | Install from ClawHub |
|--------|----------------------|
| Operate a live DCC, discover tools, or search the Marketplace | [`@loonghao/dcc-mcp`](https://clawhub.ai/loonghao/skills/dcc-mcp) |
| Create or modernize a complete DCC-MCP adapter/runtime | [`@loonghao/dcc-mcp-creator`](https://clawhub.ai/loonghao/skills/dcc-mcp-creator) |
| Create, validate, or improve a DCC-specific Skill package | [`@loonghao/dcc-mcp-skills-creator`](https://clawhub.ai/loonghao/skills/dcc-mcp-skills-creator) |

Install only the Skill matching the task. OpenClaw uses
`openclaw skills install @loonghao/<slug>`; a ClawHub-compatible agent can use
`npx --yes clawhub@0.23.1 install @loonghao/<slug>`.
Start a new agent turn after installation.

**CLI availability and updates:**
- If `dcc-mcp-cli` is missing, obtain user consent before running an official
  installer. Prefer the verified helper bundled with the installed `dcc-mcp`
  Skill; otherwise inspect `scripts/install-cli.sh` or `scripts/install-cli.ps1`
  before running the local file.
- Official release builds refresh their CLI update cache in the background at
  most once per 24 hours; failures retry after one hour and never block the
  current command. When output contains `cli_update.version_status` set to
  `update_available`, report the current/latest versions and ask the user
  before changing anything. After explicit approval, run
  `dcc-mcp-cli update apply --yes`. The apply step stages the latest CLI for
  the next launch and re-verifies its mandatory SHA-256 before replacement.
  Legacy unsigned staging is quarantined. It does not replace or restart a
  running `dcc-mcp-server` or DCC host. Use `dcc-mcp-cli update check` for an
  immediate structured status refresh.

## Response Language

- Reply to the user in **Simplified Chinese** (中文简体) by default.
- Keep all code, identifiers, commit messages, branch names, docstrings,
  comments, and file contents in **English** — this rule governs only the
  conversational/assistant-facing output, not anything written to disk or
  pushed to git.
- If the user explicitly requests another language for a specific reply,
  follow that request for that turn.

## Command Efficiency

- Prefer `vx rg`, `vx git`, and `vx gh` for routine repository search, git,
  and GitHub CLI operations in this repo. These wrappers keep tool resolution
  stable and produce compact, predictable output for agents.
- Use direct `rg`, `git`, or `gh` only when the `vx` wrapper would hide behavior
  you need to inspect or is unavailable; mention that deviation in validation
  notes when it matters.

## PR / Commit Authorship

- **Do not append AI-attribution footers** to PR bodies, commit messages,
  issue comments, or any other artefact pushed to GitHub. This includes
  (but is not limited to) phrases like
  `"Pull Request opened by Augment Code with guidance from the PR author"`,
  `"Co-authored-by: Augment <…>"`, `"Generated by …"`, etc.
- The author of the change is the human contributor whose git identity
  signs the commit; the assistant is a tool, not a co-author.
- If a PR-creation tool injects such a footer automatically, strip it
  before submitting (or edit the PR body afterwards to remove it).

## PR Merge Workflow

- **Always rebase onto the latest `main` before merging.** Never use a
  merge commit to bring `main` into a feature branch, and never let
  GitHub's "Update branch" button fast-forward via a merge commit.
- For stacked PRs (a branch based on another open PR), rebase the
  downstream branch onto `main` as soon as the upstream PR merges, then
  force-push so the diff collapses to just the new work.
- Final history on `main` must be linear: every PR lands as one (or a
  few) clean commits on top of `main`, no "Merge branch …" noise.

## Agent Contract Files

`AGENTS.md` is the **only** agent contract file at the repository root. It is the
native instruction file for Codex, OpenCode, Cursor, GitHub Copilot, Windsurf,
Cline, Roo Code, Kiro, Trae, and Augment, and Claude Code falls back to it when
no `CLAUDE.md` exists. `CLAUDE.md`, `GEMINI.md`, `COPILOT.md`, and `CODEBUDDY.md`
have been removed — they were byte-identical shims whose only unique line was
their own filename, and every statement in them already lives below.

**Gemini CLI exception:** Gemini CLI defaults its context file to `GEMINI.md`. To
make it read `AGENTS.md`, set `context.fileName` once in `~/.gemini/settings.json`:

```json
{
  "context": {
    "fileName": ["AGENTS.md", "GEMINI.md"]
  }
}
```

---

## Document Hierarchy

| Layer | File | When to read it |
|-------|------|-----------------|
| Navigation | `AGENTS.md` (this file) | First contact |
| AI agent tutorial | [`docs/guide/ai-agent-guide.md`](docs/guide/ai-agent-guide.md) | How to *use* dcc-mcp-core effectively — workflow, key concepts, common tasks, skill authoring |
| AI-friendly index | `llms.txt` | When you need to *use* APIs |
| Full index | `llms-full.txt` | When `llms.txt` lacks detail |
| Detailed rules | [`docs/guide/agents-reference.md`](docs/guide/agents-reference.md) | Before writing code — traps, do/don't, code style |
| Iteration loop | [`docs/guide/agents-reference.md#iteration-playbook`](docs/guide/agents-reference.md#iteration-playbook) | Script reuse, params-only reruns, workflow resume |
| Conceptual docs | [`docs/guide/INDEX.md`](docs/guide/INDEX.md) + `docs/api/` | Building a new adapter or skill — see INDEX.md for topic list |
| Skill authoring | [`dcc-mcp-skills-creator` on ClawHub](https://clawhub.ai/loonghao/skills/dcc-mcp-skills-creator) | Creating or modifying DCC-specific Skills |
| Adapter developer guidance | [`dcc-mcp-creator` on ClawHub](https://clawhub.ai/loonghao/skills/dcc-mcp-creator) | Before creating or changing DCC adapter server/runtime wiring, dispatcher bridges, readiness, resources, gateway behavior, or core-escalation plans |
| CLI + marketplace operations | [`dcc-mcp` on ClawHub](https://clawhub.ai/loonghao/skills/dcc-mcp) | Agents doing DCC control via `dcc-mcp-cli` — gateway ensure, inventory, search, describe, call, marketplace install/update |
| Marketplace extension publishing | [`skills/marketplace-publish-extension/SKILL.md`](skills/marketplace-publish-extension/SKILL.md) | Before publishing a new skill package to the DCC-MCP marketplace |
| Marketplace extension creation | [`skills/marketplace-create-extension/SKILL.md`](skills/marketplace-create-extension/SKILL.md) | Before creating a new marketplace extension package |
| Gateway REST regressions (VRS) | [`tests/vrs/README.md`](tests/vrs/README.md) + `scripts/vrs_replay.py` | After gateway `/v1/*` or live-adapter bugs — add a JSONL trace per regression |

---

## Iteration Playbook

Repeating work in a DCC host has one normative entry point:
[`docs/guide/agents-reference.md`](docs/guide/agents-reference.md#iteration-playbook).

1. **Re-run a script → reuse it.** Call `materialize_script(..., reuse=True, reuse_key="...")`
   and pass the returned `file_path` back. A byte-identical repeat returns the same stable
   path with `reused=True`; never re-send the source.
2. **Change a value → change `params` only.** Re-send the same `file_path` with a new
   `params` object. There is no CLI flag and no params file for this.
3. **Recover a dead multi-step run → `workflows_resume`.** Pass `workflow_id`, plus optional
   `force_steps` / `expected_spec_hash` / `strict`. Requires the executor to be built with
   `WorkflowStorage` and the `job-persist-sqlite` feature.

---

## Public Skill Coordination

- The public `dcc-mcp`, `dcc-mcp-creator`, and `dcc-mcp-skills-creator`
  packages are authored and released from
  [`dcc-mcp-agent-plugins`](https://github.com/dcc-mcp/dcc-mcp-agent-plugins).
- If a Core change affects public APIs, adapter/server wiring, dispatcher
  contracts, readiness, resources, gateway behavior, skill authoring rules,
  tool/resource/prompt schemas, testing expectations, or agent workflow,
  coordinate a matching agent-plugins pull request. Do not recreate a public
  Skill mirror in Core.
- If no public Skill update is needed, mention that explicitly in the Core PR
  validation notes.

## Quick Orientation

**What**: Rust-powered MCP library for DCC software (Maya, Blender, Houdini, Photoshop…). PyO3/maturin. Zero Python runtime deps. MCP 2025-03-26 Streamable HTTP.

**Why this project matters for AI agents**: dcc-mcp-core provides the **bridge between AI agents and DCC applications**. When you need to interact with Maya, Blender, Houdini, or any other DCC tool, **ALWAYS use dcc-mcp-core Skills** — they provide structured, validated, and safe MCP tools with built-in follow-up guidance.

### Skills-First Workflow (MEMORIZE THIS)

**The default path for AI agents is CLI+REST through the gateway.** The per-DCC MCP server path is for IDE users and legacy setups.

```
Gateway CLI+REST (agent default — use dcc-mcp skill + dcc-mcp-cli):
Support check (only when unclear): `dcc-mcp-cli dcc-types` lists canonical adapter-backed identifiers from the release catalog without starting a gateway. For one target, use `dcc-mcp-cli --output json dcc-types --dcc-type <dcc>` to keep catalog support, local registration, readiness, and later capability evidence separate; `live_instances: 0` means an observed empty local registry while `null` means the observation is unavailable, and neither is proof that the adapter, installation, or project-local host is absent. Use `list` for full live-session inventory.
1. Discover: `dcc-mcp-cli search --query "keyword" --dcc-type maya` or `POST /v1/search` → get `tool_slug`; names/summaries are tokenized, so underscores are optional (`create_sphere` and `sphere` both work).
2. Follow `next_step`: a no-schema hit calls directly only when search also carries safety hints; otherwise perform the returned targeted `load_skill` or `describe`. A correlated load may inline `compact_schema` with safety/execution hints and point straight to `call`.
3. Execute: `dcc-mcp-cli call <slug> --json '{"radius": 2.0}'` or `POST /v1/call`.
4. Batch execute: `dcc-mcp-cli call --batch --steps '<json-array>'` or `POST /v1/call_batch` for ordered batches (max 25).
5. Package as a skill: fork or extend `dcc-mcp` to reuse `dcc-mcp-cli` calls as structured MCP tools.
6. Review reusable friction only after acceptance: query narrowly scoped `dcc-mcp-cli stats`, then use the `review_skill_improvement` prompt from `dcc-mcp-skills-creator`; zero calls means no evidence, and review never expands edit/publish authority.
7. On failure, preserve `request_id`; use `doctor`, failure-filtered `stats`, and the CLI-discovered `dcc_feedback__report`. Use public-safe `/v1/debug/issue-reports/{request_id}` for a reviewed bug report; never publish raw evidence or create an external issue without user authorization.
8. The gateway never fans out per-tool backend tools into `tools/list`; the advertised gateway MCP surface is read-only discovery (`search`, `describe`) only.
9. Use `ui-control` Computer Use only when a structured DCC skill/script/API is unsupported, missing, or cannot reach the required semantic UI. Never switch to another UI/input path after a policy, authorization, authentication, security, confirmation, desktop-availability, or user-interruption failure. The adapter/operator must bind native input to its DCC with `DCC_MCP_UI_CONTROL_PROCESS_ID` or `DCC_MCP_UI_CONTROL_WINDOW_HANDLE`; requests may only narrow that trusted scope. Use `snapshot` → one `act` → `snapshot`, then `stop_computer_use` on every exit path.
10. Ctrl+Alt+Esc stops UI Control across DCC adapter processes in the Windows logon session. After the hard stop, do not retry, change `session_id`, or restart. Resume only with `ui_control__snapshot(resume_computer_use=true)` after an explicit user request and only while no Computer Use owner is active.
11. Treat `desktop_unavailable` as a pause when Windows is locked, disconnected, or on a secure desktop. No CUA observation or raw input runs, but the logical `session_id` remains and structured DCC/MCP calls may continue subject to host readiness. Never automate LockApp. After unlock, discard old ids and take a fresh exact-target snapshot so the banner returns.
12. An ordinary process cannot show the banner on the Windows lock screen. Use a dedicated, always-unlocked VM for uninterrupted Windows GUI control. Public `openai/codex` `codex-rs` contains policy/plugin wiring, not the desktop Computer Use helper implementation; this repository's backend is independent.
13. Computer Use executes on the adapter host in the interactive Windows logon session that owns the DCC. A central gateway routes calls but does not own coordinates. Never reuse gateway, other-host, or other-session coordinates. RDP disconnect/session switch pauses with `desktop_unavailable`; reconnect to the DCC session and take a fresh snapshot.
14. Multi-monitor observations remain bounded to the scoped target window, which may sit at a negative virtual-desktop origin or span monitors with different DPI values. Any topology, resolution, or DPI/scaling change invalidates the observation and requires a fresh snapshot. The banner follows the scoped target within its owning session.
15. Windows plug-in setup may use `ui_control__system_operation` only for an exact operator-owned HKCU String/DWORD or file/directory symlink grant. It is windowless and always confirmed, but never permits shell commands, arbitrary file operations, alternate registry hives, overwrite/delete, credentials, elevation, UAC, or security surfaces. Stop on `elevation_required`, `approval_required`, or `system_operation_not_granted`.

IDE MCP (for human IDE users — Cursor, Claude Desktop, VS Code):
1. Discover: MCP `search(query="keyword", dcc_type="maya")` → get `tool_slug`.
2. Follow the hit's `next_step`; describe only when requested, and pass correlated load arguments unchanged.
3. Execute through MCP: `call(tool_slug, arguments)` or REST `POST /v1/call` / `POST /v1/call_batch` (max 25).
4. Hidden MCP compatibility routes still accept older `search_tools` / `describe_tool` / `call_tool` / `call_tools` names.

Per-DCC MCP server (legacy / direct DCC connection):
1. Discover: search_skills(query="keyword") → find the right skill
2. Activate: load_skill("skill-name") → expose the tools and cascade-activate declared tool groups by default
3. Execute: call the specific tool with validated parameters
4. Follow up: check next-tools.on-success for suggested next steps
5. Debug on failure: use dcc_diagnostics__screenshot or audit_log

Gateway instance discovery:
1. Usually skip instance discovery and go straight to `search` / `POST /v1/search`, then follow its returned `next_step` to targeted load, optional describe, or call.
2. When you need a concrete DCC session or direct MCP URL, call resources/read with uri="gateway://instances"; rows always include `mcp_url`, `instance_short`, `source`, `source_meta`, and a normalized `dispatch` object (`reported`, `status`, `ready`, failure metadata), and the list payload includes `by_source` counts.
3. For one instance, read gateway://instances/{instance_id}; full UUID, `instance_short`, or any unique ≥4-char UUID prefix works across gateway tools and tool slugs.
4. Instance rows may come from the local FileRegistry (`source: "file"`), HTTP registration (`source: "http"`), relay-backed tunnels (`source: "relay"`), or optional mDNS/DNS-SD LAN discovery (`source: "mdns"`); HTTP rows win conflicts, then relay, then mDNS, then file rows.
5. Do not call legacy list_dcc_instances / get_dcc_instance / connect_to_dcc; #813 removed them from tools/list.

Gateway resources/prompts:
1. List hand-off artefacts with resources/list; gateway-prefixed URIs identify the owning DCC instance.
2. resources/list includes the gateway://instances root pointer only; do not expect every gateway://instances/{id} URI to be enumerated.
3. Read/subscribe with the exact URI returned by resources/list; do not strip `dcc://<type>/<id>` or backend-resource prefixes.
4. Agent workflow guide (MCP + resources + efficiency) lives at **`gateway://docs/agent-workflows`** (gateway-native `resources/read`).
5. Use prompts/list and prompts/get through the gateway when prompt templates are aggregated from multiple DCC instances.
6. Subscribe only to URIs you plan to react to, and unsubscribe when done.

```

### Multi-DCC Guardrails

- This repository must support multiple DCC hosts (Maya, Blender, Houdini,
  Photoshop, ZBrush, Unreal, Unity, Figma, and custom studio hosts). Do not
  introduce Maya-only assumptions in core code, gateway routing, skill
  discovery, auth examples, error messages, or tests.
- Keep DCC identity parameterized (`dcc_name`, `dcc_type`, `app_name`,
  `DccName::parse`) and preserve unknown/custom DCC names instead of rejecting
  them unless a boundary explicitly requires a known host.
- When fixing an issue, add realistic Rust **and** Python regression tests when
  the behavior crosses Rust/Python or downstream APIs. Use real-looking DCC
  examples from at least two host families where practical (for example Maya +
  Photoshop/ZBrush/custom), not only synthetic single-DCC fixtures.
- Skill actions may opt into `enforce_thread_affinity: true`; then dispatch
  rejects calls whose observed thread context does not match `thread_affinity`
  with `THREAD_AFFINITY_VIOLATION`. Use this for audited main-thread-only DCC
  API actions; host-side guards such as `@require_main_thread` still belong
  inside action bodies for defense in depth.
- For MCP/gateway changes, include an E2E-style validation path that exercises
  actual `tools/list` / `tools/call` / REST or gateway routing. If a real DCC is
  unavailable, use the closest executable/server path and state the gap in the
  PR.

**API surface** — read in this order:
1. 🆕 **[`docs/guide/ai-agent-guide.md`](docs/guide/ai-agent-guide.md)** — **START HERE** for using dcc-mcp-core effectively
2. `llms.txt` — compact AI-friendly API index
3. `llms-full.txt` — complete AI-friendly API index
4. `python/dcc_mcp_core/__init__.py` — every top-level Python re-export
5. Generated stub `python/dcc_mcp_core/_core.pyi` — parameter names/types after a `stub-gen` or development build; do not treat it as checked-in source of truth

Python ownership namespaces: adapter-facing server contracts live in
`dcc_mcp_core.server`, runtime/fallback contracts in `dcc_mcp_core.runtime`,
Rez deployment helpers in `dcc_mcp_core.deployment`, host transports in
`dcc_mcp_core.host`, and non-stable APIs in `dcc_mcp_core.experimental`.
Historical flat modules remain compatibility imports; do not add new top-level
Python modules or source stable exports from private packages.

---

## Decision Tables — Find the Right API

### What do you need?

| Need | Use this |
|------|----------|
| Expose DCC tools over MCP | `DccServerBase` → subclass → `start()` |
| Apply adapter policy before any skill load | `DccServerBase.set_skill_load_transform(fn)` — runs for Python, MCP `load_skill`, REST `/v1/load_skill`, batch/group loads; use `set_after_load_skill_hook(fn)` only for observation |
| Zero-code tool registration | agentskills.io `SKILL.md` + `metadata.dcc-mcp.tools` → sibling `tools.yaml` + `scripts/` |
| Zero-code static MCP resources | `metadata.dcc-mcp.resources` → `resources/*.resource.yaml` with `source.type: file` |
| Advertise optional adapter runtimes | `metadata.dcc-mcp.runtimes` descriptors (`python_package`, `python_extra`, `binary`, `env_var`, `feature`) → search/detail runtime state (`available` / `degraded` / `missing`) without executing tool scripts |
| Structured results | `success_result()` / `error_result()` |
| Rich error with traceback | `skill_error_with_trace()` |
| Bridge non-Python DCC | `DccBridge` (WebSocket JSON-RPC 2.0) |
| IPC | `IpcChannelAdapter` / `SocketServerAdapter` + `DccLinkFrame` |
| Hand off files between tools | `FileRef` + `artefact_put_file()` / `artefact_get_bytes()` |
| Multi-DCC gateway (three-layer default) | Runtime Supervisor (per-backend guardian) → Central Gateway (machine-wide daemon) → Per-DCC FileRegistry registration; auto-launch with single-flight lock; guard patrol |
| Choose `dcc-mcp-server` run mode | No subcommand or `auto` = per-DCC server + Runtime Supervisor (auto-launches and patrols the machine-wide `gateway` daemon); `serve --no-auto-gateway` = per-DCC server only; `gateway` = machine-wide gateway daemon with no inline DCC execution; `auto --legacy-gateway-election` = per-DCC first-wins election (fallback when `gateway-daemon` feature is off) |
| Discover gateway DCC instances / direct MCP URLs | `resources/read uri="gateway://instances"` or `gateway://instances/{id}`; entries carry `mcp_url` and replace the removed `list_dcc_instances` / `get_dcc_instance` / `connect_to_dcc` tools |
| Register a remote DCC with a gateway that cannot share `FileRegistry` | `POST /v1/instances/register` with `{instance_id, dcc_type, mcp_url, ttl_secs?}`; refresh with `/heartbeat`, remove with `/deregister`; `gateway://instances` marks these rows with `source: "http"` |
| Discover NAT-hidden DCCs through a relay | `GatewayConfig.relay_sources` / `dcc-mcp-server gateway --relay-source ADMIN_URL=PUBLIC_BASE_URL` polls relay `/tunnels`, probes `<PUBLIC_BASE_URL>/tunnel/<id>/mcp`, and exposes healthy rows with `source: "relay"` (#1363) |
| Discover LAN-local DCC MCP endpoints without a shared registry | Build with `mdns`; run DCC servers with `--advertise-mdns` and gateways with `--discover-mdns`; mDNS rows are probed before surfacing and appear with `source: "mdns"` (#1362) |
| Gateway dynamic capabilities | MCP `search` → `describe` for read-only discovery, then REST `/v1/call` / `POST /v1/call_batch` for execution |
| Gateway resources/prompts | `resources/list` / `resources/read` with exact gateway-returned URIs; `prompts/list` / `prompts/get` for aggregated backend prompt templates |

| Persist project state | `DccProject.open/load(...)` + `register_project_tools(server, ...)` exposing `project.save/load/resume/status` |
| Remote MCP relay (zero-config tunnel) | `RelayServer::start(RelayConfig, agent_bind, frontend_bind).await` — accepts agent registrations on `agent_bind`, multiplexes remote-client TCP from `frontend_bind` to the agent's local MCP server (issue #504) |
| Enable WS/HTTP frontend / `/tunnels` admin | `RelayServer::start_with(cfg, agent_bind, frontend_bind, OptionalBinds { ws_frontend, admin })` — opt-in WS upgrade endpoint at `/tunnel/<id>`, HTTP proxy at `/tunnel/<id>/...`, and read-only `GET /tunnels` + `/healthz` |
| Spawn the local tunnel agent | `dcc_mcp_tunnel_agent::run_once(AgentConfig::new(relay_url, jwt, dcc, local_target)).await` — registers, holds the connection open, bridges per-session bytes to the local DCC HTTP server, and may advertise `instance_id`, `capabilities_fingerprint`, `adapter_version`, and `scene` |
| Long-lived agent with back-off | `dcc_mcp_tunnel_agent::run_with_reconnect(cfg, shutdown_rx).await` — wraps `run_once` in a reconnect loop honouring `AgentConfig::reconnect` (Constant or Exponential); fails fast on `Rejected` |
| Mint a tunnel JWT | `dcc_mcp_tunnel_protocol::auth::issue(&TunnelClaims { sub, iat, exp, iss, allowed_dcc }, secret)` — relay uses `auth::validate` to enforce DCC scope on every registration |
| Gateway lifecycle (idle shutdown) | `DCC_MCP_GATEWAY_PERSIST=1` keeps daemon alive with no backends; `DCC_MCP_GATEWAY_IDLE_TIMEOUT_SECS` controls grace period before shutdown (standalone `gateway` CLI default `30`, daemon auto-ensure default `300`) |
| Gateway failover | `DccGatewayElection(dcc_name, server)` — auto-promote on gateway failure (legacy election mode) |
| Hide unknown DCC types from gateway | `McpHttpConfig.allow_unknown_tools = false` (default) — drops tools whose `dcc_type` is not registered with the gateway (#553, #555) |
| Auto-evict dead gateway instances | Gateway runs a TCP probe loop; deregisters after 3 consecutive failures, also runs a startup probe to evict instances whose listener died while the registry entry survived (#551, #552, #556) |
| Crash-safe heartbeat | `FileRegistry::heartbeat` writes via `tempfile::persist` + Windows `LockFileEx` so concurrent processes can't stomp each other's entry (#554) |
| Remote HTTP registration heartbeat | `POST /v1/instances/heartbeat` refreshes TTL-scoped HTTP rows; the shared `live_instances` view merges them with FileRegistry rows, and HTTP rows win conflicts on identical `instance_id` (#1361) |
| Optional mDNS/DNS-SD discovery | `GatewayConfig.discover_mdns` / `dcc-mcp-server gateway --discover-mdns` browses `_dcc-mcp._tcp.local`; `dcc-mcp-server serve --advertise-mdns` publishes the DCC endpoint; HTTP registration wins over relay, relay wins over mDNS, mDNS wins over file rows (#1362, #1363) |
| Crash-resilient liveness check | `FileRegistry` sentinel lock files — `read_alive()` / `prune_dead_entries()` evict rows whose owner process released its OS-held lock (#748) |
| Default rolling file logging | `default_file_logging_config()` from `dcc_mcp_logging::file_logging` — rolling daily files under the platform log dir (#557) |
| Trim old log files | `prune_old_logs(retention_days, max_total_size_mb)` — call on a schedule or at startup to enforce retention (#558) |
| Prometheus `/metrics` (gateway) | Build `dcc-mcp-http` with `--features prometheus`; `attach_gateway_metrics_route` + `dcc_mcp_telemetry::PrometheusExporter` expose `dcc_mcp_instances_total{status}`, `dcc_mcp_tools_total{dcc_type}`, request duration / failure counters at `GET /metrics` (#559) |
| Skill scoping | `SkillScope` (Repo → User → Team → System → Admin) |
| Optional offline semantic indexes (Python) | Import `RrfFusionIndex`, `LexicalSkillIndex`, and `VectorSkillIndex` from the canonical `dcc_mcp_core.skill_index` namespace; top-level and legacy module aliases remain compatible. The fused index remains an opt-in application utility. Production Rust skill catalog, REST, and gateway discovery all use the single `dcc-mcp-gateway-search::Scorer` contract; the Python utility is not a second server ranking path (#2184). |
| Local-only dense embeddings, zero-dep default | `VectorSkillIndex()` ships `HashedEmbedder` + `InMemoryVectorStore` by default — no ONNX / FAISS in the base wheel. Install `pip install 'dcc-mcp-core[semantic]'` to enable `OnnxEmbedder`: the three-tier backend prefers the Rust-native `dcc-mcp-core-semantic` companion wheel (fastembed-rs + ONNX Runtime, releases the GIL during inference, #1395), falls back to the pure-Python `fastembed` package, and finally raises `EmbedderError`. Override model / cache via `DCC_MCP_EMBED_MODEL` and `DCC_MCP_EMBED_MODEL_DIR` env vars so studios can pre-place the ONNX model on a shared mount (#1393) |
| Progressive tool exposure | `SkillGroup` + `activate_tool_group()` |
| Declarative progressive loading on startup | `MinimalModeConfig(skills=…, deactivate_groups=…)` → pass to `register_builtin_actions(minimal_mode=…)` (#525) |
| Connection-scoped cache | `McpHttpConfig(enable_tool_cache=True)` — per-session `tools/list` snapshot (#438) |
| Instance-bound diagnostics | `DccServerOptions.from_env(..., dcc_pid=pid)` → `DccServerBase(opts)` |
| Run code on DCC exit | `DccServerBase.register_quit_hook(callback)` — LIFO best-effort hooks run on `stop()`, context-manager exit, and atexit fallback (#747) |
| Defensive Python handle cleanup | `McpHttpConfig(shutdown_on_drop=True)` or `with server.start() as handle:` — safety net for forgotten `handle.shutdown()` (#749) |
| Remote auth | `ApiKeyConfig` / `OAuthConfig` / `validate_bearer_token` |
| Batch / orchestration | `batch_dispatch()`, `EvalContext`, `DccApiExecutor` |
| Mid-call user input | `elicit_form()` / `elicit_url()` |
| Rich content results | `skill_success_with_chart/table/image` |
| Plugin bundle | `build_plugin_manifest()` / `server.plugin_manifest()` |
| In-process skill execution (embedded DCC) | `SkillCatalog.set_in_process_executor(callable)` |
| In-process skill execution wired through DccServerBase | `DccServerBase.register_inprocess_executor(dispatcher=...)` (#521); pass a `BaseDccCallableDispatcher` to route onto the host UI thread |
| Full callable-payload dispatch protocol for DCC plugins | `BaseDccCallableDispatcherFull` + `BaseDccPump` + `JobOutcome` / `PendingEnvelope` (#520); use `InProcessCallableDispatcher` as reference impl for `mayapy` / headless / pytest |
| Bind adapter readiness to MCP/REST | `AdapterReadinessBinder.bind_inline(server)`, `.bind_headless(server)`, or `.bind_queue_dispatcher(server, dispatcher, require_first_pump=True)` publish one `ReadinessProbe` through `DccServerBase.set_readiness_probe()` / `McpHttpServer.set_readiness_probe()` so MCP `tools/call`, REST `/v1/readyz`, and REST `/v1/call` share state (#1206) |
| Skill scanning | `scan_and_load(dcc_name=...)` → always unpack `(skills, skipped)` tuple |
| Tolerate broken SKILL.md | `scan_and_load_lenient(...)` instead of `scan_and_load` |
| Fail-fast on broken SKILL.md | `scan_and_load_strict(...)` — raises `ValueError` listing every skipped directory (issue maya#138) |
| Stamp gateway sentinel for election | `McpHttpConfig(..., adapter_version=..., adapter_dcc=...)` or `.with_adapter_version().with_adapter_dcc()` (issue maya#137) |
| 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()` |
| Pick up admin-UI-added skill paths in a running adapter | `DccServerBase.reload_skill_paths()` re-runs discovery with the gateway admin SQLite `skill_paths_custom` table merged in (#1400). The merge happens automatically inside `collect_skill_search_paths(include_admin_custom=True)` on every startup; `reload_skill_paths()` is the runtime trigger for re-scanning after a path is added through the dashboard. Read the raw rows via `dcc_mcp_core.read_custom_skill_paths()` / `resolve_admin_db_path()` |
| Re-scan a running adapter's skill dirs from the gateway, no restart | Every `DccServerBase` auto-registers the `dcc_admin__reload_skills` tool in `register_builtin_actions`. The elected gateway pushes a best-effort `tools/call` for it to all live backends whenever an operator adds/removes a path **or just opens** the admin skill-paths panel (`GET /admin/api/skill-paths` now triggers a reload), so freshly dropped `~/.dcc-mcp/<dcc>/skills` packages appear without a restart. Event-driven only — zero idle cost (no file watcher / polling). |
| Surface broken / non-compliant skills | `DccServerBase.reload_skill_paths_report()` returns `{"count", "skipped"}`; the inner core exposes `McpHttpServer.discover_report()`. Skipped directories (missing/invalid `SKILL.md`) are logged at WARNING in the adapter and folded into the gateway admin operator note so an operator can see *why* an expected skill stayed missing (maya#138). |
| Persist `SkillCatalog.loaded` + active groups across restarts | `DccServerBase.enable_skill_load_persistence(policy="skip_on_drift")` (#1405) wires the catalog's after-load / after-unload / after-group-change hooks to a [`LoadedStateStore`](python/dcc_mcp_core/loaded_state_store.py) backed by `~/.dcc-mcp/<dcc>/loaded.json` (source of truth) and the gateway admin SQLite `skill_loaded_state` / `skill_active_groups` tables (best-effort mirror for admin UI visibility). Call after `start()` so the catalog has finished discovery before the persisted snapshot is replayed via `SkillCatalog.replay_loaded(state_json, policy)`. Policies: `"skip_on_drift"` (default), `"require_exact_version"`, `"ignore_version"` — see [`LoadReplayPolicy`](crates/dcc-mcp-skills/src/catalog/persistence.rs) |
| Bridge stdio MCP server to HTTP/SSE | `dcc-mcp-server translate --stdio "cmd" --app-type foo --port N` |
| Add audit/quota/redact to all gateway calls | `GatewayConfig::middleware_chain` + `AuditMiddleware` / `QuotaMiddleware` / `RedactionMiddleware` |
| Mount OpenAPI REST API as MCP tools | `GatewayBuilder::mount_openapi(OpenApiMount::from_url(...).auth(...))` |
| Enable built-in gateway admin dashboard | Default ON for the elected gateway: open `GET /admin`; JSON APIs include `/admin/api/{instances,tools,calls,traces,stats,workers,logs,health}` plus `/admin/api/traces/{request_id}` detail; `/logs` merges contention events, `DCC_MCP_LOG_DIR` `*.log` rows, and audit call summaries; set `DCC_MCP_GATEWAY_AUDIT_DIR` for durable `audit.jsonl` + `traces.jsonl`; disable with `--no-admin`, `DCC_MCP_NO_ADMIN=true`, or `cfg.admin_enabled = False` |
| Wire OTLP distributed tracing to Jaeger/Tempo/Grafana | Set `OTEL_EXPORTER_OTLP_ENDPOINT` env var at server startup |
| Configure admin integrations (Sentry, webhooks, OTLP) | Set env vars (`DCC_MCP_SENTRY_DSN`, `DCC_MCP_WEBHOOKS_CONFIG`, `OTEL_EXPORTER_OTLP_ENDPOINT`) at server startup; view or change pending restart through the Admin UI → Integrations panel when enabled |
| Read gateway contention event history | MCP `resources/read` on `resources://gateway/events` |
| Search public DCC-MCP adapter catalog | MCP `resources/read` on `gateway://catalog?query=...` (or `gateway://catalog/{name}` for a single entry); CLI: `dcc-mcp-server catalog search --query ...` |
| Disable evolved skills | `ENV_DISABLE_ACCUMULATED_SKILLS` |
| Make skill discovery hermetic in CI/tests | `DCC_MCP_DISABLE_DEFAULT_SKILL_PATHS=1` excludes implicit operator-owned roots (local/platform defaults, marketplace installs, and Admin custom paths) while preserving explicit, bundled, and `DCC_MCP_*_SKILL_PATHS` paths |
| MCP HTTP (recommended) | `create_skill_server("<dcc>", McpHttpConfig())` — OS-assigned instance port; CLI/gateway discovery |
| MCP HTTP (manual) | `McpHttpServer(registry, McpHttpConfig())` — pass `port=N` only for an explicit fixed listener |
| Full-screen capture | `Capturer.new_auto().capture()` |
| Single-window capture | `Capturer.new_window_auto().capture_window(...)` |
| Capture DCC output streams | `OutputCapture` — stdout/stderr/script-editor as `output://` resource |
| Register a custom DCC resource | Prefer a module-level helper first: `register_docs_resource(server, ...)` for `docs://` payloads, `register_adapter_instruction_resources(server, ...)` for adapter instructions. For `DccServerBase`, use `server.register_resource_producer(...)`, `server.set_scene_resource(...)`, `server.notify_resource_updated(...)`, or the shared `server.resources()` handle. Only drop to low-level `server.resources().register_producer("maya-cmds://", cb)` (also `set_scene({...})`, `notify_updated(uri)`, `register_output_buffer(capture)`) when no helper fits the scheme (#730, #1205). Never reach into `server._server.*` — that is private. |
| Cooperative cancellation (MCP request) | `check_cancelled()` in long-running skill scripts |
| Cooperative cancellation (DCC dispatcher + MCP) | `check_dcc_cancelled()` — combines MCP token + per-job `JobHandle` (#522) |
| Detect a misconfigured GUI binary as `DCC_MCP_PYTHON_EXECUTABLE` | `is_gui_executable(path)` → `GuiExecutableHint(dcc_kind, recommended_replacement)` (#524) |
| Auto-correct GUI binary to its headless sibling | `correct_python_executable(path)` — falls back to original path if no sibling found (#524) |
| Checkpoint/resume | `save_checkpoint(job_id, state)` / `get_checkpoint(job_id)` |
| Job recovery policy on restart | `McpHttpConfig.with_job_recovery(JobRecoveryPolicy::Drop\|Requeue)` / Python `cfg.job_recovery = "drop"\|"requeue"` — `Requeue` is reserved (degrades to `Drop` + `WARN` until tool-arg persistence lands) (#567) |
| Persistent workflow idempotency cache | `WorkflowExecutor::builder().idempotency_store(SqliteIdempotencyStore::new(workflow_storage)).build()` — survives restarts; honours per-step `idempotency_ttl_secs`; cascade-deletes workflow-scoped rows when their parent workflow row is removed (#566, gated on `dcc-mcp-workflow/job-persist-sqlite`) |
| Resume a persisted workflow run | `executor.resume(workflow_id, ResumeOptions { force_steps, expected_spec_hash, strict })` / MCP tool `workflows.resume`. Skips steps already recorded `completed`; re-runs `force_steps`; refuses on hash drift when `strict=true` (#565, gated on `dcc-mcp-workflow/job-persist-sqlite`) |
| Agent-facing docs resources | `register_docs_server(server)` → `docs://` MCP resources |
| Headless USD project resources | `register_usd_project_resources(server, project_root=..., stage=..., layers=...)` → canonical `openusd://stage`, `openusd://layers`, `openusd://assets`, `openusd://materials`, `openusd://validation`, `openusd://snapshots`, and `openusd://packages` resources with stable MIME metadata (#1209) |
| Agent feedback | `register_feedback_tool(server)` → `dcc_feedback__report` tool |
| Runtime introspection | `register_introspect_tools(server)` → `dcc_introspect__*` tools |
| Skill recipe lookup | `register_recipes_tools(server, skills=...)` |
| YAML workflow definitions | `load_workflow_yaml(path)` / `register_workflow_yaml_tools(server)` |
| Skill hot-reload | `DccSkillHotReloader(dcc_name, server).enable(paths)` |
| Singleton server factory | `make_start_stop(ServerClass)` → `(start_fn, stop_fn)` |
| Skill validation | `validate_skill(skill_dir)` → `SkillValidationReport` |
| Zero-dep JSON/YAML | `json_dumps/loads` (native Rust preferred, stdlib fallback in py37-lite) / `yaml_dumps/loads` (Rust-backed); the JSON helpers are a narrow package contract, not a stdlib drop-in |
| Canonical MCP/REST call envelopes | Rust `dcc-mcp-wire::{decode_call_tool, decode_rest_call, normalize_arguments, normalize_meta}`; Python `dcc_mcp_core.wire.normalize_tool_arguments()` / `normalize_tool_meta()` (`host` aliases are compatibility-only) |
| Typed handler return envelope | `ToolResultEnvelope.ok("msg", **ctx).to_dict()` / `ToolResultEnvelope.fail("msg", error="code").to_dict()` from `dcc_mcp_core.result_envelope`; `dcc_mcp_core.ToolResult` is the distinct Rust runtime model. Tool errors use a string code and structured details under `_meta["dcc.error"]` (#2183) |
| Centralised metadata keys | `from dcc_mcp_core import METADATA_*, LAYER_*, CATEGORY_*` — re-exported at top level; also available from `dcc_mcp_core.constants`. Never inline `"dcc-mcp.recipes"` etc. (#487) |
| Custom JSON-RPC method (Rust) | `MethodRouter::register(method, Arc::new(handler))` — implement `MethodHandler` trait (#492) |
| Custom action validation (Rust) | implement `ValidationStrategy` and return it from `select_strategy(...)` (#493) |
| Custom version constraint shape (Rust) | implement `VersionMatcher` + wrap in `VersionConstraint::Custom` (#493) |
| Build a registry-like container (Rust) | implement `Registry<V>` over your storage, or use `DefaultRegistry<V>` (#489) |
| Build a JSON-RPC notification (Rust) | `NotificationBuilder::new("notifications/...").with_params(json).as_sse_event()` (#484) |
| Typed DCC name (Rust) | `DccName::parse("Maya")` → `DccName::Maya`; canonicalises + hashes safely (#491) |
| Register lifecycle hooks | `LifecycleHooks()` + `.on(HookEvent.BEFORE_TOOL_CALL, handler)` + `server.register_lifecycle_hooks(hooks)` — typed, fail-safe observer/pub-sub for skill/tool/session events (#1337) |
| Veto a skill load or tool call by policy | Raise `HookDeny(reason, hint=...)` from a `BEFORE_SKILL_LOAD` / `BEFORE_TOOL_CALL` / `BEFORE_SEARCH` handler — only policy events propagate denial; non-policy events swallow it (#1337) |
| Observe skill/tool lifecycle (analytics, audit) | `HookEvent.AFTER_SEARCH` / `AFTER_SKILL_LOAD` / `AFTER_TOOL_CALL` / `SESSION_START` / `SESSION_END` — non-policy events; handlers fire in registration order, exceptions logged & swallowed (#1337) |
| Enrich search context before discovery | `HookEvent.BEFORE_SEARCH` handler mutates `ctx.payload` in-place (add tags, dcc filters); the mutated dict is visible to the search caller (#1337) |
| Three-tier agent memory (ephemeral → working → longterm) | `InMemoryMemoryStore()` + `MemoryRecorder(store).install(hooks)` — session-scoped ring buffers for EPHEMERAL/WORKING, global cap for LONGTERM; auto-compacts working→longterm on SESSION_END (#1334) |
| Inject memory summaries into search/tool-call context | `MemoryRecorder` auto-injects `memory_summary` / `memory_prefer_tools` / `memory_avoid_tools` on `BEFORE_SEARCH` and `BEFORE_TOOL_CALL` (#1334) |
| Query agent memory | `store.query(MemoryQuery(layer=..., session_id=..., key_prefix=...))` → `tuple[MemoryEntry, ...]` sorted by recency then score (#1334) |
| Pluggable memory backend | Implement `MemoryStore` Protocol (`put`, `query`, `forget`) — `InMemoryMemoryStore` is the default; swap in SQLite/file/Redis backends without changing recorder logic (#1334) |
| Register all standard built-in MCP tools at once | `register_all_builtin_skills(server, dcc_name=..., skills=...)` — registers diagnostics, introspection, feedback, recipes, Qt UI inspector, and script materialization tools; idempotent (#1332) |
| Structured skill recall metadata | `RecallContext`, `Precondition`, `SideEffects`, `ToolRole`, `RiskLevel` on `SkillMetadata` / `ToolDeclaration` — optional fields for ranking, capability graph, and policy decisions (#1335) |
| Associate a tool with its safety role | `ToolRole.ReadOnly` / `Action` / `Destructive` / `EscapeHatch` / `DebugOnly` — ranking demotes `EscapeHatch`, safety surfaces warn on `Destructive` (#1335) |

### Skill layer taxonomy (`metadata.dcc-mcp.layer`)

| Layer | Purpose | `search_skills` rank |
|-------|---------|----------------------|
| `domain` | Pipeline-level intent (shot export, render farm) | × 1.00 |
| `infrastructure` | Safety, diagnostics, introspection — fallback tier | × 0.35 |
| `thin-harness` | One Python script + minimal SKILL.md — raw `python` / `bash` / CLI wrappers | × 0.20 |
| `example` | Authoring reference only | **excluded from results** |

The ranking rules (#1398) keep low-level skills out of the agent's primary
flow for neutral queries:

- `domain` skills win by default.
- `infrastructure` skills (e.g. `ui-control`, `dcc-adapter`, `dcc-diagnostics`)
  stay around as fallbacks but rank below domain.
- `thin-harness` skills are the lowest tier that still appears — useful as a
  last-resort wrapper around a raw script.
- `example` skills are dropped from results entirely; they only surface when
  the caller explicitly asks for them (see below) or types the exact name.

The penalty and the `example` exclusion are bypassed when the caller filters
by a known layer name through `tags=` (case-insensitive), e.g.
`search_skills(tags=["example"])` or `search_skills(tags=["infrastructure"])`,
so the raw shared-scorer order is honoured inside the filtered slice.

### Skill path-source rank (#1403)

A second multiplier is layered on top of the layer multiplier based on
where the skill was discovered from. User-curated locations rank at
parity (× 1.00); bundled / platform-installed starter material is
slightly damped so a local-dev skill always wins a tie.

| Source         | Where it comes from                              | Rank   |
|----------------|--------------------------------------------------|-------:|
| `ExplicitArg`  | `extra_paths` passed to `discover()` / `scan_*`  | × 1.00 |
| `AdminCustom`  | Added through the admin UI (gateway SQLite lane) | × 1.00 |
| `EnvVar`       | `DCC_MCP_SKILL_PATHS` / `DCC_MCP_<APP>_SKILL_PATHS` | × 1.00 |
| `LocalDev`     | `~/.dcc-mcp/<dcc>/skills` (local iteration root) | × 1.00 |
| `Platform`     | Platform-wide install dir (`get_skills_dir`)     | × 0.85 |
| `Bundled`      | Shipped with the dcc-mcp package itself          | × 0.70 |

The path-source multiplier compounds multiplicatively with the layer
multiplier (both are `≤ 1.00`). The exact-name fast-path bypasses it —
`search_skills("dcc-diagnostics")` still surfaces a bundled diagnostics
skill at the top regardless of source.

Source tagging happens at scan time in
`crates/dcc-mcp-skills/src/scanner.rs::scan_with_sources`. Adapters can
read the assigned source via `SkillEntry.path_source` for diagnostic
surfaces (e.g. the admin UI's skill panel) — it is stable across
restarts because the field is `#[serde(default)]`.

---

## AI Agent Tool Priority

**CRITICAL: Always prefer dcc-mcp-core tools over direct CLI or scripting.** These tools provide structured results, input validation, safety annotations, and follow-up guidance that raw scripting cannot.

1. **Skill Discovery**: `search_skills(query)` → `load_skill(name)` → use tools
2. **Skill-Based Tools**: Validated schemas + `next-tools` + `ToolAnnotations` safety hints
3. **Diagnostics**: per-DCC `dcc_diagnostics__screenshot` / `dcc_diagnostics__audit_log` MCP tools; gateway-side `gateway://diagnostics/{process,audit,metrics}` MCP resources (`resources/read`).
4. **Direct Registry** (last resort): Validate with `ToolValidator` + sandbox with `SandboxPolicy`

### Why Skills-First?

| Aspect | dcc-mcp-core Skills | Raw CLI / Scripting |
|--------|---------------------|---------------------|
| Input validation | JSON Schema validated | None — garbage in, garbage out |
| Safety | `ToolAnnotations` (read-only, destructive, idempotent) | Unknown |
| Follow-up guidance | `next-tools` chains | Manual discovery |
| Progressive loading | Load only what you need | All or nothing |
| Error recovery | Structured `error_result` with `prompt` suggestions | Unstructured stderr |
| Traceability | Audit log + telemetry | None |

---

## Top Traps — Memorize These

1. **`scan_and_load` returns a 2-tuple** → `skills, skipped = scan_and_load(...)` — never iterate directly
2. **`success_result` kwargs → context** → `success_result("msg", count=5)` — do NOT use `context=`
3. **`ToolDispatcher` uses `.dispatch()`** → never `.call()`
4. **Register ALL handlers BEFORE `server.start()`** — server reads registry at startup
5. **SKILL.md extensions use `metadata.dcc-mcp.<feature>`** → sibling files, never top-level keys (v0.15+ / #356)
6. **Use `dcc_mcp_core.METADATA_*` / `LAYER_*` / `CATEGORY_*`** → re-exported at top level (also in `constants` sub-module); no inline `"dcc-mcp.recipes"` / `"thin-harness"` literals (#487)
7. **Return `ToolResultEnvelope` from Python tool handlers** → `ToolResultEnvelope.ok("...", **ctx).to_dict()` (or `success_(...)`); `error` is a string code and structured diagnostics belong under namespaced `_meta` entries (#2183)
8. **Gateway REST `/v1/call` / `/v1/call_batch` payloads accept only `tool_slug`, `arguments`, and `meta`** → put backend fields (`code`, `file_path`, `radius`, …) inside `arguments`; use `dcc-mcp-wire` / `dcc_mcp_core.wire.normalize_tool_arguments()` for shared normalization
9. **Lifecycle hooks — policy events propagate `HookDeny`, observation events swallow it** → `BEFORE_SKILL_LOAD`, `BEFORE_TOOL_CALL`, `BEFORE_SEARCH` are policy; all others are observation-only. Raising `HookDeny` from an observation event is silently logged (#1337)
10. **Lifecycle hooks — `off()` removes by identity (`is`), not equality** → store the handler reference; `hooks.off(event, lambda ctx: ...)` never matches (#1337)
11. **Agent memory — `MemoryRecorder.install()` must be called** → the recorder does nothing until wired to a `LifecycleHooks` instance; forgetting `install()` means zero memory is recorded (#1334)
12. **Agent memory — raw prompts are redacted** → never pass LLM prompts or `api_key`/`password`/`secret`/`token` keys in `MemoryEntry.payload`; the `_safe_payload` filter strips them (#1334)
13. **Phase hook signature mismatch** → `_registration.py` calls 3 hooks with `(context)` and 7 with no args. Overriding a hook with the wrong parameter count silently fails because `run_registration_phases` catches non-fatal exceptions. Always run an integration test that calls `get_standard_phases()` against your real server class (not `MockServer`) and assert no `TypeError` (PIP-2479). See [`tests/test_phase_hook_signature_consistency.py`](tests/test_phase_hook_signature_consistency.py) for the static check.

Full trap list + code examples → [`docs/guide/agents-reference.md`](docs/guide/agents-reference.md)

---

## Verified Regression Suite (VRS) — gateway `/v1/*` replay

Use this when a bug is only visible **through HTTP** (gateway election, routing, error envelopes, DCC adapter behaviour under load) and unit tests are not enough.

### When to add a trace

Whenever you close a regression that involves **gateway REST** or **per-DCC `/v1/*`** behaviour:

1. Open or reference the GitHub issue in the trace header `trace_id` / PR description.
2. Add `tests/vrs/traces/<issue-or-topic>-<short-slug>.jsonl` (one concern per file; keep steps ordered).
3. Prefer `expect_any` when legitimate outcomes differ by transport (e.g. HTTP 200 with `output.success: false` vs 502 `backend-error`).
4. For traces that need a live Maya (or any optional host), put a header `skip_preflight` so CI and agent runs **exit 0 skipped** when `POST /v1/search` shows no matching instance (see `maya-215-execute-python-regression.jsonl`).
5. Extend [`tests/vrs/README.md`](tests/vrs/README.md) index table with: file name, required live DCC?, one-line purpose.

### How to run

```bash
# Live gateway (default port 9765 in examples)
just vrs-replay BASE=http://127.0.0.1:9765 TRACE=tests/vrs/traces/gateway-smoke.jsonl

# Same, explicit script
python scripts/vrs_replay.py --base-url http://127.0.0.1:9765 --trace tests/vrs/traces/<your-trace>.jsonl
```

Validate JSON step order and substitutions **without** a server:

```bash
python scripts/vrs_replay.py --base-url http://127.0.0.1:1 --dry-run --trace tests/vrs/traces/<your-trace>.jsonl
```

Optional: `VRS_HTTP_TIMEOUT_SECS` overrides per-request timeout (default `120`).

Full step schema, `expect` / `capture` / `skip_preflight` → [`tests/vrs/README.md`](tests/vrs/README.md).

---

## Build & Test

`vx just dev` (build wheel) → `vx just test` → `vx just preflight` (pre-commit check + docs dead-link check)

---

## File Size Policy

CI enforces hard line-count limits (`.github/workflows/check-file-size.yml`):

| Language | Limit | Test files |
|----------|-------|------------|
| Rust (`.rs`) | **1 500 lines** | **2 000 lines** |
| Python (`.py`) | **1 000 lines** | excluded |
| Admin UI source/test (`.ts`, `.tsx`, `.css`, `.json`) | **3 000 lines** | included |

### When a file exceeds the limit

Do **not** add it to `.github/file-size-exemptions.txt` unless it is pre-existing
technical debt tracked in an open issue. **Split it instead.**

#### Rust — Clean Architecture split

A file that exceeds 1 500 lines almost always mixes multiple responsibilities.
Split it along these boundaries:

```
crates/<name>/src/
├── domain/        # pure business logic, no I/O, no framework deps
│   ├── model.rs   # types, enums, value objects
│   └── service.rs # domain services (no async unless domain-driven)
├── application/   # orchestration, use-case handlers, coordinator structs
│   └── handler.rs
├── infra/         # I/O: file system, network, OS, external crates
│   ├── store.rs
│   └── transport.rs
└── lib.rs         # re-exports only; no logic here
```

Rules:
- One public type / one cohesive set of related functions per file.
- `domain/` must not import from `infra/` or `application/`.
- `application/` orchestrates `domain/` + `infra/`; no business rules.
- Keep `impl` blocks with their type — do not scatter impls across files.
- Keep Rust test files below 2 000 lines. Extract shared fixtures into focused
  helper modules and split broad integration suites by user-facing workflow.
- Remove code smells during the split: eliminate `unwrap`/`expect` outside
  tests, replace god-structs with focused structs, break cyclic deps.

#### Python — Clean Architecture split

```
python/dcc_mcp_core/<feature>/
├── __init__.py    # public re-exports only
├── _model.py      # dataclasses / TypedDicts / Pydantic models
├── _service.py    # pure business logic
└── _adapter.py    # I/O adapters (files, subprocesses, DCC bridges)
```

Rules:
- Max one public class or one cohesive group of related pure functions per file.
- No circular imports; domain layer must not import adapters.
- Prefer composition over inheritance for adapter variations.

#### Admin UI — feature split

The Admin UI under `admin-ui/src/` should keep source, test, style, and locale
files below 3 000 lines.

Rules:
- Split API/data model types and normalization helpers into focused `.ts`
  modules.
- Split shared view helpers and panel-level components into focused `.tsx`
  modules instead of growing `App.tsx`.
- Split large stylesheets by feature area and import them explicitly from
  `main.tsx` so cascade order stays visible.
- Store translation dictionaries under `admin-ui/src/locales/<locale>/` by
  namespace, for example `locales/en/action.json`, `locales/en/traffic.json`, or
  `locales/zh-CN/skill-paths.json`.

#### Exemption process

If a split is genuinely out-of-scope for the current PR:
1. Open a tracking issue titled `[Refactor] Split <file> into modules`.
2. Add one line to `.github/file-size-exemptions.txt`:
   ```
   # issue #NNN — <reason>
   path/to/the/file.rs
   ```
3. The PR author is accountable for closing that issue within two sprints.

---

## Repo Layout (What Lives Where)

```
crates/          # Rust workspace; package membership comes from Cargo.toml
python/dcc_mcp_core/__init__.py  # ← top-level Python public re-exports
python/dcc_mcp_core/result_envelope.py  # ← typed ToolResultEnvelope wire builder (#2183)
python/dcc_mcp_core/constants.py        # ← metadata key / layer / category constants (#487)
python/dcc_mcp_core/_server/            # ← DccServerBase collaborators (observability, skill_query, window_resolver) (#486)
tests/           # integration/regression tests
examples/skills/ # 15 complete SKILL.md packages
docs/            # human-readable guides + API reference
```

---

## Essential Do / Don't

### Do ✅
- Use `create_skill_server()` — Skills-First entry point
- Use `success_result("msg", count=5)` — kwargs become context
- Use `ToolResultEnvelope.ok("...", **ctx).to_dict()` (or `.success_(...)`) from `result_envelope`; `.fail(msg, error="...")` for string error codes; structured details belong under `_meta` (#2183)
- Import metadata strings from `dcc_mcp_core` (`METADATA_*`, `LAYER_*`, `CATEGORY_*` re-exported at top level; `dcc_mcp_core.constants.*` also works) (#487)
- Use `ToolAnnotations` — safety hints for AI clients
- Use `search_skills(query)` — don't guess tool names
- Use `metadata.dcc-mcp.<feature>` keys + sibling files for all SKILL.md extensions
- Tag every skill with `metadata.dcc-mcp.layer`
- Unpack `scan_and_load()`: `skills, skipped = scan_and_load(...)`
- Use `DccName::parse(s)` at Rust API boundaries instead of `&str` (#491)
- Keep core code DCC-agnostic; parameterize `dcc_name` / `dcc_type` and cover multi-DCC behavior in Rust + Python tests
- Use `MethodRouter::with_builtins()` then `.register(...)` to add custom JSON-RPC methods (#492)
- Use Conventional Commits: `feat:`, `fix:`, `docs:`, `refactor:`
- Use `vx just dev` before `vx just test`

### Don't ❌ (and what to do instead)
- Don't iterate `scan_and_load()` → **unpack the 2-tuple**
- Don't use `context=` kwarg in `success_result()` → **pass kwargs directly**
- Don't call `ToolDispatcher.call()` → **use `.dispatch(name, json_str)`**
- Don't put SKILL.md extensions at top level → **use `metadata.dcc-mcp.<feature>` + sibling file**
- Don't hand-roll `{"success": ..., "context": ...}` dicts in handlers → **return `ToolResultEnvelope.ok(...).to_dict()`** (the factory is `ok`/`success_`, not `success`) (#2183)
- Don't write inline `"dcc-mcp.recipes"` / `"thin-harness"` literals → **import from `dcc_mcp_core`** (constants re-exported at top level) (#487)
- Don't pass raw `&str` DCC names through Rust APIs → **`DccName::parse(s)` at the boundary** (#491)
- Don't hardcode Maya as the default/example for generic core behavior → **use generic DCC examples or at least two DCCs in tests**
- Don't fix issues with Rust-only or Python-only coverage when both surfaces are affected → **add realistic tests on both sides**
- Don't extend the JSON-RPC `match` arm in `dispatch.rs` → **register a `MethodHandler` on `MethodRouter`** (#492)
- Don't hand-roll JSON-RPC envelopes → **`NotificationBuilder` / `JsonRpcRequestBuilder`** (#484)
- Don't add per-crate `*Error` enums → **return `DccMcpError` via `From` impls** (#488)
- Don't break Python 3.7 support → **it is an LTS profile; native Linux + Windows cp37 gates are required, and py37-lite is not a substitute. See ADR 011 and `compatibility/python.json`**
- Don't manually bump versions → **Release Please handles this**
- Don't add `CLAUDE.md` / `GEMINI.md` / `COPILOT.md` / `CODEBUDDY.md` / `CURSOR.md` / `ANTHROPIC.md` / `OPENAI.md` / `.cursorrules` at the repo root → **`AGENTS.md` is the single source; `tests/test_agent_instruction_files.py` fails the build if extras reappear**
- Don't hardcode scope strings → **use `SkillScope` when introspecting from Python and `SkillMetadata` methods for policy checks**
- Don't add a generic `utils` / `common` / `helpers` crate → **route helpers to their owner: domain crate, `dcc-mcp-paths`, `dcc-mcp-logging`, or `dcc-mcp-pybridge`** ([rationale](docs/guide/agents-reference.md#workspace-boundary-rationale))

Full list with code examples → [`docs/guide/agents-reference.md`](docs/guide/agents-reference.md)

---

## External Standards

| What | Where |
|------|-------|
| MCP spec (2025-03-26) | <https://modelcontextprotocol.io/specification/2025-03-26> |
| SKILL.md format | <https://agentskills.io/specification> |
| AGENTS.md standard | <https://agents.md/> |
| llms.txt format | <https://llmstxt.org/> |