AGENTS.md · diff

git:20261022.e582e58 to git:20261024.1aa2598

1 added, 1 removed. Audit B to B.

# AGENTS.md — dcc-mcp-maya Agent Navigation Map
> Progressive disclosure: this file is a **map**, not an encyclopedia.
> Follow the links for depth. Stay here for breadth.
## Agent Control Path
AI agent runtimes default to the shared gateway through the
`dcc-mcp` skill and `dcc-mcp-cli` REST commands:
```bash
dcc-mcp-cli search --query "<task>" --dcc-type maya
dcc-mcp-cli describe <tool-slug>
dcc-mcp-cli call <tool-slug> --json '{"key":"value"}'
```
Use `dcc-mcp-cli list` for live instances and `dcc-mcp-cli dcc-types` for
release-catalog support. IDE users may continue to configure the gateway MCP
endpoint; adapter-local Python start APIs are for host bootstrap and tests.
### CLI availability and updates
If `dcc-mcp-cli` is missing, obtain user consent before using the official
install commands in the README Agent workflow. Keep an official build current
with:
```bash
dcc-mcp-cli update check
dcc-mcp-cli update apply
```
`update apply` stages the latest CLI for the next launch; it does not replace
a running server.
---
## 30-Second Summary
`dcc-mcp-maya` embeds a standards-compliant MCP Streamable HTTP server directly inside Autodesk Maya. It exposes 212 Maya operations as MCP tools that any AI agent (Claude, Cursor, Gemini, etc.) can call over HTTP — no external gateway, no subprocess bridge.
- **Current version:** 0.9.42 <!-- x-release-please-version -->
+ **Current version:** 0.9.43 <!-- x-release-please-version -->
**Core dependency:** `dcc-mcp-core>=0.20.0,<0.21.0`
**Python:** 3.7+
**Maya:** 2020+
---
## Quick Start (3 Lines)
```python
import dcc_mcp_maya
handle = dcc_mcp_maya.start_server()
# MCP client connects through http://127.0.0.1:9765/mcp
```
Or load the Maya plugin (`dcc_mcp_maya_plugin.py`) and the server starts automatically.
---
## Information Layers — Pick Your Depth
### Layer 1 — You Are a User / Operator
*Goal: Install, configure, and connect an MCP host.*
- **README.md** — Installation, quick start, environment variables, bundled skills list.
- **docs/guide/getting-started.md** — Step-by-step for first-time users.
- **docs/guide/local-mcp-debug.md** — Cursor / Claude MCP HTTP URL, **debugpy** attach, gateway vs direct port.
- **examples/mcp/** — Copy-paste MCP JSON (`cursor-maya-streamable-http.json`).
- **install.md** + **skills/dcc-mcp-maya-setup/** — Agent-facing setup entry: install `mayapy` pip dependencies, generate MCP config snippets, guide Maya plugin loading, and run a first smoke prompt.
- **docs/guide/installation.md** — Plugin mode, `userSetup.py`, multi-Maya setup.
- **docs/guide/multi-instance.md** — Run multiple Maya sessions behind one gateway.
- **docs/guide/mcp-tools.md** — Representative tool inventory (scene, geometry, material, animation, render).
### Layer 2 — You Are a Skill Author
*Goal: Write new Maya automation skills and register them as MCP tools.*
- **docs/guide/contributing.md** — Skill package layout, `SKILL.md` format, action script rules.
- **docs/guide/advanced.md** — Custom skills, main-thread scheduling, hot-reload.
- **src/dcc_mcp_maya/api.py** — 18 high-level helpers (`maya_success`, `with_maya`, `validate_node_exists`, `require_param`, …).
- **Key pattern:** Lazy-import `maya.cmds` inside the function so skills can be discovered without a running Maya.
Minimal skill template:
```python
from dcc_mcp_maya.api import maya_success, maya_error, with_maya
@with_maya
def create_sphere(radius: float = 1.0) -> dict:
import maya.cmds as cmds
result = cmds.polySphere(radius=radius)
return maya_success("Created sphere", object_name=result[0])
```
### Layer 3 — You Are a Core Developer
*Goal: Modify the server, dispatcher, or plugin behavior.*
- **src/dcc_mcp_maya/server.py** — `MayaMcpServer` composition root (constructor, `register_builtin_actions`, `start`, `stop`, metrics, job persistence, resources). Heavy lifting lives in private siblings: `_env`, `_executor`, `_skill_loader`, `_version_probe`, `_transport`, `_pyexec`, `_stale_cleanup`, `_readiness`, `_resources`.
- **src/dcc_mcp_maya/dispatcher/** — `MayaUiDispatcher`, `MayaStandaloneDispatcher`, `MayaUiPump`, `check_maya_cancelled` (split into `job` / `cancel` / `ui` / `standalone` / `pump` submodules — public symbols re-exported from the package).
- **maya/plugin/dcc_mcp_maya_plugin.py** — Maya plugin entry point (`initializePlugin`, `uninitializePlugin`, menu, gateway auto-config).
- **tests/** — 50+ unit tests, E2E tests (tahv/mayapy 2022–2025), multi-instance gateway tests.
- **Upstream `dcc-mcp-core` API reference** — https://github.com/dcc-mcp/dcc-mcp-core/blob/main/llms.txt — authoritative one-page index of every public symbol re-used by this repo (`DccServerBase`, `MinimalModeConfig`, `HostExecutionBridge`, `BaseDccCallableDispatcher`, `is_gui_executable` / `correct_python_executable`, `FileRegistry`, `check_dcc_cancelled`, `JobHandle`, result-envelope factories, etc.). Always consult this first before adding a new helper locally — most "missing" utilities already exist upstream and are simply waiting to be wired in.
### Layer 4 — You Are an AI Agent Reading This
*Goal: Discover and use tools effectively inside a live Maya session.*
- **llms.txt** — Core API surface, environment variables, key files (fits in a small context window).
- **llms-full.txt** — Complete public API signatures, all environment variables, bundled skill categories.
- **`src/dcc_mcp_maya/skills/SKILLS_INDEX.md`** — **Cross-skill navigation map**: 5-stage taxonomy and ready-made task → skill chains. Read first before deciding which skill to load.
- **Upstream core reference** — https://github.com/dcc-mcp/dcc-mcp-core/blob/main/llms.txt (and the deeper [`llms-full.txt`](https://github.com/dcc-mcp/dcc-mcp-core/blob/main/llms-full.txt)) — exhaustive `dcc_mcp_core` API surface; use it whenever a tool/skill needs to leverage core primitives that are not surfaced in this repo's own `llms.txt`.
- **Skill discovery workflow:**
1. Prefer MCP `dcc_capability_manifest` with `{loaded_only: false}` for a **compact** index of actions (avoids paying full `inputSchema` cost for every skill up front).
2. Alternatively: `search_skills` / `search_tools` → **`load_skill`** → optional `activate_group("extended")` → invoke the **typed** tool (validated `inputSchema`, e.g. `maya_mesh_ops__bevel_edge`). Treat `maya_scripting__execute_python` / `execute_mel` as **last resort** when no skill covers the task (bulk in-Maya loops, OpenMaya gaps, one-off experiments). Studios can hard-block arbitrary execution with `DCC_MCP_MAYA_DISABLE_EXECUTE_PYTHON`, `DCC_MCP_MAYA_DISABLE_EXECUTE_MEL`, or `DCC_MCP_MAYA_DISABLE_ARBITRARY_SCRIPT` (see `README.md` / `llms.txt`).
3. When using the **gateway**, read **`resources/read` `uri=gateway://docs/agent-workflows`** for MCP + resources + efficiency guidance; for Maya-heavy examples see `examples/workflows/maya_bulk_rbd_fbx.md`.
4. **cmds documentation:** `resources/read` on `maya-cmds://help/<command>` or `maya-cmds://flags/<command>` (use the exact URI from `resources/list`).
- **Token hygiene:** Set `DCC_MCP_MAYA_EXCLUDE_STUBS_FROM_TOOLS_LIST=1` when your MCP host repeatedly syncs a large `tools/list`; discovery remains available via `dcc_capability_manifest` and gateway `/v1/search`.
- **Always check cancellation in long-running loops:**
```python
from dcc_mcp_maya import check_maya_cancelled
for frame in frames:
check_maya_cancelled() # raises CancelledError when cancelled
cmds.currentTime(frame)
cmds.render()
```
---
## Skill Stage Taxonomy (5-stage map)
Every bundled skill is tagged with `metadata.dcc-mcp.stage` in its
`SKILL.md`. `_skill_loader.skills_for_stage()` derives the mapping from
frontmatter at runtime; the cross-skill index
[`src/dcc_mcp_maya/skills/SKILLS_INDEX.md`](src/dcc_mcp_maya/skills/SKILLS_INDEX.md)
is checked against disk by tests so the human-readable inventory cannot drift.
| Stage | Purpose | Default loaded? | Skills |
|---------------|----------------------------------------------------------------------|---------------------------|--------|
| `bootstrap` | Escape hatch; arbitrary code **only** when no typed skill fits. | yes | `maya-scripting` |
| `scene` | Scene file lifecycle, DAG, attributes, node graph, viewport display. | partial (`maya-scene` only) | `maya-scene`, `maya-scene-assembly`, `maya-display`, `maya-attributes`, `maya-node-graph` |
| `authoring` | Create / edit content (mesh, UV, mat, rig, anim, light). | no | `maya-primitives`, `maya-mesh-ops`, `maya-uv-ops`, `maya-materials`, `maya-material-library`, `maya-texture-bake`, `maya-rigging`, `maya-animation`, `maya-pose-library`, `maya-expressions`, `maya-light-rig` |
| `interchange` | Geometry / scene I/O across DCCs (FBX, OBJ, presets, save). | no | `maya-geometry`, `maya-export-preset` |
| `pipeline` | Production: project, publish, shot export, render, render farm, development diagnostics. | no | `maya-dev`, `maya-pipeline`, `maya-shot-export`, `maya-render`, `maya-render-farm` |
Helpers in `_skill_loader.py`:
```python
from dcc_mcp_maya._skill_loader import (
STAGES, # canonical 5-stage tuple
skills_for_stage, # tuple of skills in a given stage
build_minimal_mode_config, # default minimal-mode config
build_minimal_mode_for_stages, # eager-load whole stages at once
)
```
A custom minimal mode that pre-loads `bootstrap + scene + interchange` so the
"create geometry → export FBX" path does not require a `load_skill` call:
```python
cfg = build_minimal_mode_for_stages(["scene", "interchange"]) # bootstrap auto-included
server.register_builtin_actions(minimal_mode=cfg)
```
## MCP-dispatched Safe Session (modal-dialog firewall)
Every in-process skill job runs inside
[`dcc_mcp_maya._safe_session.mcp_safe_session`](src/dcc_mcp_maya/_safe_session.py),
which:
* snoozes Maya AutoSave for the duration of the job;
* replaces `cmds.confirmDialog` / `promptDialog` / `fileDialog` /
`fileDialog2` / `layoutDialog` with non-blocking stubs that emit a
`stderr` warning and return a defaulted value (so the calling code keeps
running);
* restores everything on exit, even on exception. The context is
reentrant via thread-local refcount so nested invocations do not
undo each other's state.
This is the single most important line of defence against the
"adapter looks alive but every MCP request hangs" failure mode. Set
`DCC_MCP_MAYA_SAFE_SESSION=0` to disable for an interactive
authoring session that *actually* needs to spawn dialogs.
---
## Project-State Persistence (issue #576 / core 0.14.21)
The Maya adapter wires `dcc_mcp_core.register_project_tools` into `MayaMcpServer.register_builtin_actions()`, exposing four MCP tools that persist a Maya scene's working set under `<scene_dir>/.dcc-mcp/project.json`:
| Tool | Purpose |
|------------------|-------------------------------------------------------------------------|
| `project_save` | Persist current state (loaded assets, active skills/tool-groups, checkpoint IDs, free-form metadata) for a given `scene_path`. |
| `project_load` | Read an existing `project.json` (returns failure when absent — never auto-creates). |
| `project_resume` | Return the rehydration payload (scene path, assets, skills, tool groups, checkpoints, session id, timestamps, project dir) an agent needs to restore a session across Maya restarts. |
| `project_status` | Pure read: current state + project_dir + state_path. |
Key Python symbols:
```python
from dcc_mcp_maya import (
ENV_PROJECT_TOOLS, # "DCC_MCP_MAYA_PROJECT_TOOLS" — set "0" to disable
MayaSceneResolver, # strategy: returns current scene path or None
ProjectToolsIntegration, # SOLID binder used by the server
attach_project_tools, # one-shot helper invoked from register_builtin_actions
)
```
Operator opt-out: `DCC_MCP_MAYA_PROJECT_TOOLS=0`. Each `project.*` entry adds <800 B to `tools/list` and is guaranteed safe to register before any dispatcher is attached (pure filesystem operations, never touches `maya.cmds`).
---
## Shutdown Hardening (issue #186)
The stock plugin path (`uninitializePlugin` → `_stop_blocking`) only fires when Maya politely tears the plugin down. Non-cooperative exits (Maya crash, `kill -9`, Task Manager End Task, `mayapy` script that `os._exit(...)`s) previously leaked the `FileRegistry` row for up to 30 s.
The Maya adapter now installs **four independent safety nets** composed by `ShutdownCoordinator`:
| Net | Default | Env opt-out | Covers |
|-------------------------------------|---------|---------------------------------------|--------------------------------------------------------------------------|
| `MSceneMessage.kMayaExiting` hook | on | `DCC_MCP_MAYA_KMAYA_EXITING_HOOK=0` | `File → Exit Maya`, `⌘Q` / Alt+F4 — fires before `uninitializePlugin`. |
| `atexit` fallback | on | `DCC_MCP_MAYA_ATEXIT_HOOK=0` | Plain interpreter teardown, `mayapy` scripts. |
| Crash-resilient process sentinel | on | `DCC_MCP_MAYA_PROCESS_SENTINEL=0` | `kill -9` / Task Manager / crash — OS drops the marker when process dies. |
| Defensive `__del__` guard (opt-in) | off | `DCC_MCP_MAYA_DEFENSIVE_DEL=1` enable | `mayapy` / test-fixture paths that never call `stop_server()`. |
The coordinator's guarded-stop wrapper ensures the callback runs **at most once** even when two nets race. All four are wired in `initializePlugin` and torn down in `uninitializePlugin`; each one is a silent no-op when its preconditions are missing (e.g. no `maya.api.OpenMaya` → no hook).
Python symbols (exported from the top-level package):
```python
from dcc_mcp_maya import (
ShutdownCoordinator, # composes all four nets
ProcessSentinel, # low-level OS marker wrapper
DefensiveShutdownGuard, # opt-in __del__ belt
register_kmaya_exiting_hook, # helper — registers just the kMayaExiting net
register_atexit_hook, # helper — registers just the atexit net
write_process_sentinel, # helper — creates just the sentinel
orphan_sentinels, # sweeper helper — list dead-PID sentinels
ENV_KMAYA_EXITING_HOOK,
ENV_ATEXIT_HOOK,
ENV_PROCESS_SENTINEL,
ENV_DEFENSIVE_DEL,
)
```
Support matrix + detailed breakdown in `docs/guide/shutdown-matrix.md` (EN) / `docs/zh/guide/shutdown-matrix.md` (ZH).
---
## MCP Resources (issue #187 / core 0.15.0)
`MayaMcpServer.register_builtin_actions()` wires the inner Rust [`ResourceHandle`][resource-handle] (`server._server.resources()`) so MCP clients see Maya state under stable URIs:
| URI scheme | Purpose |
|-----------------------------------------|--------------------------------------------------------------------------|
| `scene://current` | JSON snapshot of the live Maya scene; refreshed by `scriptJob` events with 500 ms throttling. |
| `maya-cmds://help/<command>` | `cmds.help(command, language="python")` text. |
| `maya-cmds://flags/<command>` | Structured per-flag info from `cmds.help(command, flags=True)`. |
| `maya-api://signatures/<class>` | Public-method index for OpenMaya / OpenMayaAnim / OpenMayaUI classes. |
| `maya-project://current` | Active workspace root + `fileRule` table. |
[resource-handle]: https://github.com/dcc-mcp/dcc-mcp-core/blob/main/llms.txt
Key Python symbols:
```python
from dcc_mcp_maya import (
ENV_RESOURCES, # "DCC_MCP_MAYA_RESOURCES" — set "0" to disable
MayaResourceBinder, # SOLID composition root
install_resources, # one-shot helper from register_builtin_actions
SCHEME_MAYA_CMDS, # "maya-cmds://"
SCHEME_MAYA_API, # "maya-api://"
SCHEME_MAYA_PROJECT, # "maya-project://"
DEFAULT_SCENE_EVENTS, # tuple of scriptJob events we hook
DEFAULT_SCENE_THROTTLE_SECS, # 0.5 s throttle window
)
```
Memory rule (`feedback_resources_api.md`): every Maya call into `server._server.resources()` lives in `_resources.py::MayaResourceBinder`. Skill scripts and plugin code go through the binder, never the raw handle — that lets future schema migrations be a single-file edit.
Throttling: a 1000-node bulk import (which fires 1000 `DagObjectCreated` scriptJob events) collapses to ~5 `notifications/resources/updated` SSE frames thanks to the lead-edge + trail-edge timer in `_on_scene_event`.
Prompts: core advertises `prompts: {listChanged: true}` and PR #373 implements derivation from SKILL.md `examples` / `workflows`, but the 0.15.0 wheel returns `[]`. Once the consumption path lights up upstream (0.15.1+), Maya's bundled skills surface their `examples` automatically — no Maya-side code change required.
---
## Gateway Capability Surface (issues #163 / #164 / #165)
The Maya adapter publishes a **compact capability manifest** so the gateway — and agents that query Maya directly — can enumerate every action (loaded *and* unloaded) without paying the cost of full per-tool JSON Schemas.
Entry points:
| Surface | Where |
|-------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
| Programmatic | `MayaMcpServer.build_capability_manifest(loaded_only=False)` |
| MCP tool (agents) | `dcc_capability_manifest({"loaded_only": bool})` — registered before `start()` |
| Record shape | `{tool_slug, backend_tool, skill_name, summary (<=200 ch), tags, execution, affinity, timeout_hint_secs, has_schema, group}` |
| Live sync | `publish_capability_snapshot(reason=...)` invoked automatically on `start()` / `load_skill` / `unload_skill` |
| Scene context (→ REST `/v1/context`)| `MayaContextSnapshotProvider` wired via `set_context_snapshot_provider` in `__init__` |
Each record is **<= 640 B** serialised JSON and omits `inputSchema` — roughly 4× cheaper than a full `tools/list` entry. The manifest deliberately exposes skill actions that MCP `tools/list` intentionally skips (core only emits `__skill__*` stubs there), so an agent can decide which skill to load without polling.
Key Python symbols exported from the top-level package:
```python
from dcc_mcp_maya import (
MayaCapabilityManifestBuilder, # catalog → list[CapabilityRecord]
CapabilityRecord,
build_manifest_payload,
register_capability_mcp_tool,
MayaContextSnapshotProvider,
collect_gateway_metadata,
make_snapshot_provider,
)
```
---
## Runtime Readiness (issue #184)
Maya's embedded MCP HTTP server publishes itself to the `FileRegistry` long before Maya's main thread has finished booting. Without a readiness signal the gateway happily routes traffic to a Maya whose UI dispatcher has not yet drained its first job — `tools/call` with `affinity: main` accepts the request, queues it, and blocks until the scene finishes loading. Operators see "the service is up, but Maya is frozen".
The three-state probe itself (`process` / `dispatcher` / `dcc`) lives in `dcc-mcp-core` as `dcc_mcp_core.ReadinessProbe` (core 0.14.28+). `GET /v1/readyz` returns `200` only when all three bits are green; otherwise `503` with a `not-ready` envelope. The Maya adapter owns just the **wiring**:
| Bit | Flips to `true` when… |
|---------------|-------------------------------------------------------------------------------------------------------------------------------------|
| `process` | Python interpreter is alive (always `true` while the server object exists — core's default). |
| `dispatcher` | `HostExecutionBridge` has wired the in-process executor (unconditional — the binder flips it the moment `__init__` returns). |
| `dcc` | A host dispatcher is attached **and** Maya's main thread has pumped one deferred no-op, **or** — in inline executor mode (no host dispatcher, `mayapy` / tests) — immediately, since the HTTP worker thread *is* the pump. |
Entry points:
| Surface | Where |
|----------------------------|----------------------------------------------------------------------------------------------------|
| Programmatic snapshot | `MayaMcpServer.readiness_report()` → `{"process": bool, "dispatcher": bool, "dcc": bool}` |
| Binder object | `MayaMcpServer.readiness` → `ReadinessBinder` (tests, diagnostic endpoints) |
| Raw core probe | `MayaMcpServer.readiness.probe` → `dcc_mcp_core.ReadinessProbe` |
| Wiring point | `MayaMcpServer.__init__` / `attach_dispatcher` via `._readiness.install_readiness(server)` |
| Core publish | `server._server.set_readiness_probe(probe)` — called automatically by `ReadinessBinder.bind` |
Key Python symbols:
```python
from dcc_mcp_maya import (
ENV_READINESS_TIMEOUT_SECS, # "DCC_MCP_MAYA_READINESS_TIMEOUT_SECS"
ReadinessBinder, # Maya-side lifecycle binder (wraps core ReadinessProbe)
install_readiness, # one-shot helper (mirrors attach_project_tools)
resolve_readiness_timeout_secs, # env-var → Optional[int]
)
# The three-state probe itself comes from core:
from dcc_mcp_core import ReadinessProbe
```
With `MayaStandaloneDispatcher` attached, the caller must pump `dispatcher.drain_queue()` on Maya's initialization thread; `dcc` flips after that first pump. Only inline executor mode (no host dispatcher) lands all-green synchronously. In interactive Maya with a UI dispatcher, `dcc` flips after the first idle pump tick, so `dcc=true` is an honest "main thread is alive and pumping" signal.
Operator opt-in for a hard timeout: `DCC_MCP_MAYA_READINESS_TIMEOUT_SECS=60` — advisory only; the adapter does not auto-fail the probe when the timeout elapses (a hung Maya is better reported as "still booting" than as a synthetic error).
---
## Version provenance (single source + drift warning)
One host can answer "which adapter version is this?" in several ways, and a
stale install makes them disagree — `importlib.metadata.version('dcc-mcp-maya')`
reports what `pip` recorded, while `dcc_mcp_maya.__version__` reports the module
that is actually executing (also what the startup log, the MCP `initialize`
response, and `dcc-mcp-cli` report).
| Question | Answer |
|-----------|--------|
| Where does the version come from? | `pyproject.toml` → `project.version`; release-please bumps it together with `src/dcc_mcp_maya/__version__.py` and every other file carrying the release-please version marker. `tests/test_version_single_source.py` fails CI when they drift. |
| Which one wins at runtime? | `dcc_mcp_maya.__version__` — it is the code that executes, so it is what bug reports should quote. |
| What happens on drift? | `MayaMcpServer.start()` runs `dcc_mcp_maya._version_check.run_version_self_check()` once: it logs both answers with their paths and emits a **warning** naming the stale side. It never raises and never blocks startup. |
| Does it work on Maya 2022 (Python 3.7)? | Yes. `importlib.metadata` is 3.8+, so when neither it nor the `importlib_metadata` backport is importable the check falls back to a pure-stdlib scan of `sys.path` for `dcc_mcp_maya-*.dist-info/METADATA`. No new runtime dependency. |
```python
from dcc_mcp_maya import version_report, run_version_self_check
version_report()
# {'adapter': {'status': 'mismatch', 'runtime_version': '0.9.16',
# 'distribution_version': '0.9.14', 'module_path': '...', ...},
# 'core': {...}, 'consistent': False, 'drift': True}
run_version_self_check() # same payload, logged (info when healthy, warning on drift)
```
`consistent` is strict — a source checkout (`not-installed`) or a host where
the metadata cannot be read (`unknown`) makes it `False`. Read `drift` when you
only care about "two answers exist and disagree".
`dcc-mcp-core` gets the same treatment, so its startup log line and its
distribution metadata are cross-checked too — "read the version off the log" is
no longer a guess. Opt out with `DCC_MCP_MAYA_VERSION_CHECK=0`.
## Key Conventions
### Adapter Runtime Dependency Boundary
- Maya owns the Python interpreter. Adapter runtime code uses the Python standard
library, host-owned modules, and first-party `dcc-mcp-core` / `dcc-mcp-server`
APIs. Do not add third-party runtime requirements, vendor them into the adapter,
or create an adapter-specific interpreter / virtual environment.
- Host-owned integrations include Maya APIs, Maya's PySide / shiboken bindings,
Arnold's `mtoa`, and Maya USD's `pxr`. Use the host's versions; never pip-install
replacements into a release bundle. Feature-specific host modules may be absent.
- The existing lazy `debugpy` developer integration, optional PyYAML affinity
loader, and `importlib_metadata` compatibility probe are narrowly grandfathered
at their audited call sites. They must handle missing imports, remain outside
required runtime dependencies and release bundles, and must not become startup
prerequisites. These exceptions do not authorize new third-party imports.
- Build / development dependencies are separate: `packaging` is permitted for
wheel validation and test oracles, not adapter runtime version handling. Keep
build tools in build requirements and developer tools in the `dev` extra.
- An adapter whose host has no Python interpreter must use the official shared
DCC-MCP Runtime rather than ship or create its own interpreter / virtual
environment. Changes to other adapters or Runtime belong in their own review.
- Run `python -m pytest tests/test_adapter_runtime_dependencies.py` before changing
dependencies or import paths. The normal CI pytest suite runs this gate. It
audits runtime declarations and all adapter / Maya bootstrap Python sources,
including known dynamic imports. Extend an allowlist only after reviewing
ownership, absent-module behavior, and distribution-size impact; do not bypass
a failure by relabeling a runtime dependency as a development dependency.
Version semantics, compatibility differences, sealed-ZIP gates and measured
size impact are documented in [Runtime dependencies](docs/guide/runtime-dependencies.md).
### Tool Naming
- Action name = `{skill_name.replace("-","_")}__{script_stem}`
- Example: skill `maya-scene` + script `new_scene.py` → tool `maya_scene__new_scene`
### Result Format (Return from Skill Scripts)
Use helpers from `dcc_mcp_maya.api`:
- `maya_success(message, **context)` → `{"success": True, "message": ..., "context": {...}}`
- `maya_error(message, error, possible_solutions=[...], **context)` → `{"success": False, ...}`
- `maya_from_exception(exc, message, **context)` → includes full traceback (preferred over `str(exc)`)
- `maya_typed_success(message, data, return_type=None, **context)` *(core 0.14.22+)* → `maya_success` envelope augmented with an auto-derived JSON Schema under `context.output_schema` and the serialised dataclass under `context.typed_result`. Use when your handler returns a `@dataclass` / `TypedDict` so downstream agents can validate the payload without waiting on upstream tools.yaml `outputSchema` propagation.
### Execution & Affinity (tools.yaml)
Every tool declaration **must** include:
```yaml
tools:
- name: render_frames
execution: async # sync | async
affinity: main # main | any
timeout_hint_secs: 600 # required when execution: async
```
| Value | When to Use |
|-------|-------------|
| `execution: async` | Typical wall-clock > 2s (render, bake, cache, simulation). Must set `timeout_hint_secs`. |
| `execution: sync` | Fast queries, attribute setters, small creations. |
| `affinity: main` | Anything importing `maya.*` or touching scene state. Safe default. |
| `affinity: any` | Pure filesystem / pure Python that never touches Maya. |
The adapter now **honors `affinity: any` at runtime**: such actions execute
inline on the HTTP worker thread instead of being queued behind the Maya
UI dispatcher, freeing the main thread for viewport work. Resolution is
done once per script by `dcc_mcp_maya._affinity.resolve_affinity`
(reads the co-located `tools.yaml` — safe-defaults to `main` on any
lookup failure).
### Minimal Mode (Default)
At startup only 2 skills are fully loaded: `maya-scripting` and `maya-scene` (core groups only).
All other skills appear as `__skill__<name>` stubs (default behavior). Call `load_skill(name)` to activate on demand.
**Note:** Set ``DCC_MCP_MAYA_EXCLUDE_STUBS_FROM_TOOLS_LIST=1`` to exclude ``__skill__*`` / ``__group__*`` stubs from ``tools/list`` (issue #174). Discovery is still possible via ``build_capability_manifest()`` and ``/v1/search``.
### Environment Variables
| Variable | Default | Purpose |
|----------|---------|---------|
| `DCC_MCP_MAYA_PORT` | `0` | MCP instance port (`0` lets the OS choose). |
| `DCC_MCP_MAYA_SERVER_NAME` | `maya-mcp` | Name in MCP `initialize` response. |
| `DCC_MCP_MAYA_SKILL_PATHS` | — | Maya skill search roots (`;` on Windows, `:` on Unix); Rez packages usually append `{root}/skills` whose children are skill packages. |
| `DCC_MCP_MINIMAL` | `1` | `0` = full mode; `1` = minimal mode. |
| `DCC_MCP_DEFAULT_TOOLS` | — | Comma-separated skill names to load at startup (overrides minimal). |
| `DCC_MCP_MAYA_METRICS` | `0` | `1` = enable Prometheus `/metrics` endpoint. |
| `DCC_MCP_MAYA_JOB_STORAGE` | `<data_dir>/jobs.db` | SQLite job persistence path. |
| `DCC_MCP_MAYA_JOB_RECOVERY` | `drop` | `requeue` = resume idempotent jobs on startup. |
| `DCC_MCP_MAYA_READINESS_TIMEOUT_SECS` | — | Advisory Maya-side timeout (positive integer seconds) for the runtime readiness probe (issue #184). Consumed by orchestrators that want to bound how long a cold Maya can stall before `/v1/readyz` is considered permanently red. |
| `DCC_MCP_MAYA_KMAYA_EXITING_HOOK` | `1` | `0` = disable the `MSceneMessage.kMayaExiting` hook that catches clean `File → Exit Maya` / `⌘Q` exits (issue #186). |
| `DCC_MCP_MAYA_ATEXIT_HOOK` | `1` | `0` = disable the `atexit` fallback that catches interpreter teardown (issue #186). |
| `DCC_MCP_MAYA_PROCESS_SENTINEL` | `1` | `0` = disable the crash-resilient sentinel file that lets sweepers detect `kill -9` / Task Manager exits (issue #186). |
| `DCC_MCP_MAYA_DEFENSIVE_DEL` | `0` | `1` = enable the defensive `__del__` guard. Recommended only for `mayapy` / test fixtures — interactive Maya disables by default to avoid Tokio deadlocks (issue #186). |
| `DCC_MCP_MAYA_RESOURCES` | `1` | `0` = disable Maya MCP resource publishing entirely (issue #187 / core 0.15.0). |
| `DCC_MCP_MAYA_FAULTHANDLER` | `1` | `0` = disable plugin-installed Python fatal-signal traceback logging. Logs go under `DCC_MCP_LOG_DIR` or the OS temp directory. |
| `DCC_MCP_MAYA_AUTO_DISMISS_CRASH_DIALOG` | `0` | `1` = auto-dismiss detected Maya Qt recovery dialogs after main-thread tool calls and surface `maya_recovered` in results / `scene://current`. |
| `DCC_MCP_GATEWAY_PORT` | `9765` | Multi-instance gateway election port. `0` = disable. |
| `DCC_MCP_REGISTRY_DIR` | OS temp dir | Shared service-discovery registry directory. |
| `DCC_MCP_MAYA_EXCLUDE_STUBS_FROM_TOOLS_LIST` | `0` | `1` = exclude ``__skill__*`` / ``__group__*`` stubs from ``tools/list`` (issue #174). Discovery still possible via capability manifest / ``/v1/search``. |
| `DCC_MCP_MAYA_DISABLE_EXECUTE_PYTHON` | `0` | `1` / `true` / `yes` / `on` — refuse ``execute_python`` (skills-first policy). |
| `DCC_MCP_MAYA_DISABLE_EXECUTE_MEL` | `0` | Same truthy tokens — refuse ``execute_mel`` only. |
| `DCC_MCP_MAYA_DISABLE_ARBITRARY_SCRIPT` | `0` | Same truthy tokens — refuse **both** ``execute_python`` and ``execute_mel``. |
| `DCC_MCP_MAYA_VERSION_CHECK` | `1` | `0` = skip the startup version self-check that compares the running module version with the installed distribution metadata (see [Version provenance](#version-provenance-single-source--drift-warning)). |
---
## FAQ / Common Pitfalls
**Q: The MCP host says "tool not found" even though the skill exists.**
A: In minimal mode, skills are stubs until `load_skill("maya-primitives")` is called. Load the skill first.
**Q: A tool that uses `maya.cmds` crashes when run from a worker thread.**
A: The tool's `tools.yaml` must declare `affinity: main`. Anything touching Maya state must run on the UI thread.
**Q: How do I make a long-running render cancellable?**
A: Poll `check_maya_cancelled()` inside the loop. It raises `CancelledError` when the client or dispatcher signals cancellation.
**Q: How do I start the server in a Maya batch / `mayapy` script?**
A: Prefer `mayapy -m dcc_mcp_maya`, which uses `serve_headless()` to own the main-thread pump. Custom hosts can pass `MayaStandaloneDispatcher` to `start_server()` and call `dispatcher.drain_queue()` from Maya's initialization thread while clients run on other threads or processes.
**Q: Where are the built-in skills?**
A: `src/dcc_mcp_maya/skills/` (28 packages, 211 scripts, 212 MCP tools). Each package contains `SKILL.md`, `tools.yaml`, optional `groups.yaml`, and `scripts/*.py`.
**Q: How do I force agents to stop using ``execute_python``?**
A: Set ``DCC_MCP_MAYA_DISABLE_EXECUTE_PYTHON=1`` (Python only) or ``DCC_MCP_MAYA_DISABLE_ARBITRARY_SCRIPT=1`` (Python + MEL). Callers get a structured error that points to ``load_skill`` + typed tools.
## Gateway HTTP regression traces (VRS)
Bugs that only reproduce through the **gateway REST** surface (`/v1/search`, `/v1/call`, …) — for example `execute_python` crashing the host after a handled Python error — should get a **JSONL replay trace** in **dcc-mcp-core** (`tests/vrs/traces/`), not only a pytest in this repo. Workflow, `just vrs-replay`, and naming rules live in upstream [`AGENTS.md`](https://github.com/dcc-mcp/dcc-mcp-core/blob/main/AGENTS.md) (section **Verified Regression Suite (VRS)**) and [`tests/vrs/README.md`](https://github.com/dcc-mcp/dcc-mcp-core/blob/main/tests/vrs/README.md). Reference trace: [`maya-215-execute-python-regression.jsonl`](https://github.com/dcc-mcp/dcc-mcp-core/blob/main/tests/vrs/traces/maya-215-execute-python-regression.jsonl).
---
## File Index (Agent Quick-Look)
| File | Role |
|------|------|
| `README.md` | Human-facing overview, installation, config tables |
| `llms.txt` | Condensed AI reference (this project's "man page") |
| `llms-full.txt` | Exhaustive API reference |
| `src/dcc_mcp_maya/__init__.py` | Public API exports |
| `src/dcc_mcp_maya/server.py` | `MayaMcpServer` composition root — lifecycle, discovery, metrics, jobs |
| `src/dcc_mcp_maya/_env.py` | `DCC_MCP_MAYA_*` env-var resolution helpers |
| `src/dcc_mcp_maya/_executor.py` | In-process skill execution + handler registration (respects `_affinity`) |
| `src/dcc_mcp_maya/_affinity.py` | Per-action thread-affinity lookup from sibling `tools.yaml` |
| `src/dcc_mcp_maya/_skill_loader.py` | Minimal-mode skill loading (constants + loaders) |
| `src/dcc_mcp_maya/_version_check.py` | Runtime version provenance — compares ``dcc_mcp_maya.__version__`` with the installed distribution metadata and warns on drift |
| `src/dcc_mcp_maya/_version_probe.py` | Maya availability + version string detection |
| `src/dcc_mcp_maya/_transport.py` | `TransportManager` wrappers (bind / find / rank) |
| `src/dcc_mcp_maya/_pyexec.py` | Auto-correct `DCC_MCP_PYTHON_EXECUTABLE` (issue #125) |
| `src/dcc_mcp_maya/_stale_cleanup.py` | Stale FileRegistry detection + warning (issue #126) |
| `src/dcc_mcp_maya/_readiness.py` | Three-state readiness probe (`process` / `dispatcher` / `dcc`) — honest `/v1/readyz` signal during Maya boot (issue #184) |
| `src/dcc_mcp_maya/_recovery_dialog.py` | Qt-level Maya recovery dialog detector — optional auto-dismiss + `maya_recovered` status in results/context (issue #241) |
| `src/dcc_mcp_maya/_resources.py` | `MayaResourceBinder` — `scene://current` snapshot + `maya-cmds://` / `maya-api://` / `maya-project://` producers (issue #187 / core 0.15.0) |
| `src/dcc_mcp_maya/_shutdown_safety.py` | Non-cooperative shutdown safety nets — `kMayaExiting` hook, `atexit` fallback, crash-resilient process sentinel, defensive `__del__` (issue #186) |
| `src/dcc_mcp_maya/dispatcher/` | Thread-affinity dispatchers + cancellation (directory module) |
| `src/dcc_mcp_maya/api.py` | Skill authoring helpers |
| `src/dcc_mcp_maya/plugin.py` | Maya plugin (`initializePlugin` / menu) |
| `src/dcc_mcp_maya/skills/` | 28 built-in skill packages, 211 scripts |
| `docs/guide/local-mcp-debug.md` | Cursor / Claude MCP URL + **debugpy** remote attach for Maya |
| `examples/mcp/` | MCP host JSON snippets (e.g. `cursor-maya-streamable-http.json`) |
| `tools/maya-dev-build-link-core-win.ps1` | Windows: `maturin develop` core with mayapy (`abi3-py38` for mayapy 3.8+ to match PyPI wheels; non-abi3 for Maya 2022 / 3.7) + symlink into `Documents/maya/modules` |
| `docs/` | VitePress documentation site (EN + ZH) |
| `tests/` | pytest suite (unit + E2E + integration) |
## Vendor Integration Guides
`AGENTS.md` is the **only** agent contract file at the repo root. Vendor-specific
integration notes live under `docs/integrations/` and are linked from here. They are
companions, not alternative entry points: nothing in them may contradict `AGENTS.md`,
and anything not specific to one vendor belongs here instead.
| Vendor | Doc | What is specific to it |
|--------|-----|------------------------|
| Anthropic / Claude Code | [`docs/integrations/anthropic.md`](docs/integrations/anthropic.md) | Extended thinking, computer-use synergy, structured `ToolResult` handling, prompting recommendations |
| OpenAI / GPT | [`docs/integrations/openai.md`](docs/integrations/openai.md) | Function-calling ↔ MCP tool mapping, async `job_id` polling via `jobs_get_status` |
| Claude Desktop | [`docs/integrations/claude.md`](docs/integrations/claude.md) | `claude_desktop_config.json` snippet, progressive loading, quick test prompts |
| Cursor | [`docs/integrations/cursor.md`](docs/integrations/cursor.md) | Cursor MCP configuration, skill-script dev loop, inline review, cross-skill refactor |
| Google Gemini / Vertex AI | [`docs/integrations/gemini.md`](docs/integrations/gemini.md) | Skill script generation, structured results, skill search & discovery |
**Don't** re-add `CLAUDE.md` / `GEMINI.md` / `CURSOR.md` / `ANTHROPIC.md` / `OPENAI.md` /
`COPILOT.md` / `CODEBUDDY.md` / `.cursorrules` at the repo root —
`tests/test_agent_instruction_files.py` fails the build if they reappear.
---
**New in 0.2.20:** Rust-backed dispatchers (`PyPumpedDispatcher`, `PyStandaloneDispatcher`, `_CorePump`, `create_pumped_dispatcher`) provide higher performance via APIs introduced in Core 0.14.17. See `llms-full.txt` for details. |