# 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/*`. Agent-specific files (`CLAUDE.md`, `GEMINI.md`, `COPILOT.md`,
> `CODEBUDDY.md`) intentionally point back here.
> Detailed rules, traps, and code examples → [`docs/guide/agents-reference.md`](docs/guide/agents-reference.md)

**🤖 New to this project?** Start with [`AI_AGENT_GUIDE.md`](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.**

**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 — use `search_skills()` → `load_skill()` → call the tool.

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

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

## Document Hierarchy

| Layer | File | When to read it |
|-------|------|-----------------|
| Navigation | `AGENTS.md` (this file) | First contact |
| AI agent entry-point stubs | `CLAUDE.md`, `GEMINI.md`, `COPILOT.md`, `CODEBUDDY.md` | Thin shims — each file just redirects its respective agent to **this** `AGENTS.md`. Edit them **only** to record agent-specific divergences; keep all shared guidance here so it stays single-sourced. |
| 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 |
| 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 | `skills/README.md` + `examples/skills/` | Creating or modifying skills |
| Skill developer guidance | [`skills/dcc-mcp-skill-developer/SKILL.md`](skills/dcc-mcp-skill-developer/SKILL.md) | After code changes that affect adapter skill authoring, tool schemas, server wiring, testing, or agent-facing workflows |
| 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 |

---

## Agent-Facing Skill Sync

- After any code change, check whether the installed skill developer guidance
  at `skills/dcc-mcp-skill-developer/SKILL.md` needs to change.
- If the change affects public APIs, adapter/server wiring, skill authoring
  rules, tool/resource/prompt schemas, testing expectations, or agent workflow,
  update that `SKILL.md` in the same PR.
- If no update is needed, mention that explicitly in the 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)

```
Per-DCC MCP server:
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 dynamic-capability / REST exposure:
1. Discover: search_tools(query="keyword", dcc_type="maya") → get tool_slug; names/summaries are tokenized, so underscores are optional (`create_sphere` and `sphere` both work)
2. Inspect: describe_tool(tool_slug) → read schema + annotations
3. Execute: call_tool(tool_slug, arguments={...}), or call_tools({calls:[...], stop_on_error?}) for ordered batches (max 25) to save MCP round-trips
4. The gateway never fans out per-tool backend tools into tools/list.
5. For non-MCP clients, use the matching /v1/search, /v1/describe, /v1/call REST endpoints (or POST /v1/call_batch for batches).

Gateway instance discovery:
1. Usually skip instance discovery and go straight to search_tools → describe_tool → call_tool (or call_tools when invoking several slugs in order).
2. When you need a concrete DCC session or direct MCP URL, call resources/read with uri="gateway://instances".
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. 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. 🆕 **[`AI_AGENT_GUIDE.md`](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

---

## Decision Tables — Find the Right API

### What do you need?

| Need | Use this |
|------|----------|
| Expose DCC tools over MCP | `DccServerBase` → subclass → `start()` |
| 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` |
| 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 | `McpHttpConfig(gateway_port=9765)` |
| 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 |
| Gateway dynamic capabilities | `search_tools` → `describe_tool` → `call_tool` / `call_tools` or REST `/v1/search` → `/v1/describe` → `/v1/call` / `POST /v1/call_batch` |
| 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 frontend / `/tunnels` admin | `RelayServer::start_with(cfg, agent_bind, frontend_bind, OptionalBinds { ws_frontend, admin })` — opt-in WS upgrade endpoint 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 |
| 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 failover | `DccGatewayElection(dcc_name, server)` — auto-promote on gateway failure |
| 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) |
| 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) |
| 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 |
| 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()` |
| 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 |
| 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` |
| MCP HTTP (recommended) | `create_skill_server("<dcc>", McpHttpConfig(port=8765))` |
| MCP HTTP (manual) | `McpHttpServer(registry, McpHttpConfig(port=8765))` |
| 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. Only drop to the low-level surface `server.resources().register_producer("maya-cmds://", cb)` (also `set_scene({...})`, `notify_updated(uri)`, `register_output_buffer(capture)`) when no helper fits the scheme (#730). 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 |
| 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` / `yaml_dumps/loads` (Rust-powered) |
| Canonical MCP/REST call envelopes | Rust `dcc-mcp-wire::{decode_call_tool, decode_rest_call, normalize_arguments, normalize_meta}`; Python wrappers `dcc_mcp_core.host.normalize_tool_arguments()` / `normalize_tool_meta()` |
| Typed handler return envelope | `ToolResult.ok("msg", **ctx).to_dict()` / `ToolResult.fail("msg", error="code").to_dict()` from `dcc_mcp_core.result_envelope` — note the underscored aliases `success_`/`error_`; `success`/`error` are dataclass fields, **not** factories (#487) |
| 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) |

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

| Layer | Purpose |
|-------|---------|
| `thin-harness` | One Python script + minimal SKILL.md — agents wrap a single CLI/API |
| `infrastructure` | Safety, diagnostics, introspection |
| `domain` | Pipeline-level intent (shot export, render farm) |
| `example` | Authoring reference only |

---

## 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 `ToolResult` from Python tool handlers** → `ToolResult.ok("...", **ctx).to_dict()` (or `success_(...)`); `success`/`error` are dataclass *fields*, the factories are `success_`/`error_` / `ok`/`fail` (#487)
8. **Gateway `call_tool` / `call_tools` wrappers accept only `tool_slug`, `arguments`, and `meta`** → put backend fields (`code`, `file_path`, `radius`, …) inside `arguments`; use `dcc-mcp-wire` / `dcc_mcp_core.host.normalize_tool_arguments()` for shared normalization

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** | excluded |
| Python (`.py`) | **1 000 lines** | excluded |

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

#### 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 — 41 packages (40 functional packages + workspace-hack); `Cargo.toml` is source of truth
python/dcc_mcp_core/__init__.py  # ← top-level Python public re-exports
python/dcc_mcp_core/result_envelope.py  # ← typed ToolResult dataclass (#487)
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 `ToolResult.ok("...", **ctx).to_dict()` (or `.success_(...)`) from `result_envelope`; `.fail(msg, error="...")` for errors; `.not_found("Skill", name)` / `.invalid_input(msg)` for the common error codes (#487)
- 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 `ToolResult.ok(...).to_dict()`** (the factory is `ok`/`success_`, NOT `success`) (#487)
- 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 add Python runtime deps → **zero-dep by design**
- Don't manually bump versions → **Release Please handles this**
- 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/> |
