AI agents' spatial awareness inside Autodesk Maya: audit, rollback safety, visual loop (Beta)
Runs on your machine from its package; the steps are below.

At most: moves money, runs code or commands, deletes data, or sends messages for you.
Odel charges nothing per call. The publisher may charge for its own service.
Runs locally over stdio
This server isn't hosted — your MCP client launches it from a package registry. Use one of the commands below, or drop the config into your client (e.g. Claude Desktop).
Run
uvx mcp-for-maya
MCP client config
{
"mcpServers": {
"mcp-for-maya": {
"command": "uvx",
"args": [
"mcp-for-maya"
]
}
}
}English | 简体中文
MCP server giving AI agents spatial awareness of Autodesk Maya scenes
mcp-for-maya is a Model Context Protocol (MCP) server that lets LLM agents (Codex, Claude, etc.) directly drive Autodesk Maya for 3D modeling, scene planning, and engineering-grade delivery.
Forked from chadrik/maya-mcp-server, it adds a scene-intelligence layer on top of the upstream connection stack: spatial awareness, deterministic auditing, transactional safety, and a visual loop.
Key capability: the agent stops "writing blind" — it can perceive spatial state, materials, and object relationships, then verify changes against engineering rules.
Warning
This server executes arbitrary Python inside Maya — that is a designed capability, not a bug. The built-in validation / rate-limit / audit pipeline is a safety net for accidents and injected instructions, not a boundary against a malicious client; the connected agent is trusted. See docs/threat-model.md.
Every asset below is a real capture produced by this project's own tool chain — no mockups: scenes were built procedurally through execute_code / write_module (plus one live asset_import), frames rendered through scene_render_preview, the audit pair via scene_review + scene_viewport_snapshot, and the orbit sequence captured frame-by-frame with playblast (Maya 2024 GUI session).
| Prompt | Result |
|---|---|
write_module → t20_movement.build() — "parametric involute gear train caliber: true involute teeth, Geneva-striped plates, blue-steel screws, ruby jewels, spiral hairspring" | ![]() |
write_module → t20_bonsai.build() — "low-poly L-system bonsai: recursive branching, faceted foliage pads, pot + moss + stones" | ![]() |
asset_import("vintage_pocket_watch") + asset_import("metal_tool_chest") → t20_workbench.build() — "horologist's workbench: imported assets with textures wired, procedural involute spares" | ![]() |
write_module → t20_street.build() — "low-poly corner block: 11 parametric buildings, gable roofs, awnings, street furniture, parked vans" | ![]() |
write_module → t20_teapot.build() — "Utah teapot recreation: revolve-profile body, Bezier tapered spout, ear handle, checkered floor" | ![]() |
scene_review() on a bench littered with junk → scene_plan(auto_fix=True) + cleanup → scene_review() again — score 53.7 → 64.2 | ![]() ![]() |

Reproduce every frame with the scripts in .github/assets-src/.
An honest three-tier comparison with ahujasid/mcp-for-blender (measured 2026-09):
| Tier | Contents |
|---|---|
| Unique to this project | ICEV enforced workflow (in server instructions), CoS notation output, checkpoint/rollback transactional safety, scene_plan holistic planning (zone mapping + layout suggestions), multi-session management, 11 deterministic audit checks, playblast render preview with metadata |
| Unique to blender-mcp | Asset ecosystem breadth (Sketchfab/Hyper3D/Hunyuan3D), first-class object CRUD tools, AI-generated-model integrations, community scale |
| Shared | MCP tool surface, Poly Haven asset source, viewport screenshot feedback, arbitrary Python execution, local socket connection |
Poly Haven model search + import shipped in 0.2.0 (issue #2 thin slice: FBX + texture wiring; HDRIs and texture packs remain roadmap). AI generation and first-class object CRUD are explicitly out of scope — the latter is already covered by execute_code.
On the shared asset-download path, this project's controls are enforced in code rather than conventional: downloads happen host-side only — https + host allowlist, per-file md5, size caps, filenames sanitized from the URL's last segment (polyhaven.py) — and Maya itself never touches the network. Enforcement detail: docs/threat-model.md.
| Capability | Tools | Notes |
|---|---|---|
| 🧊 Spatial awareness | scene_snapshot scene_inspect scene_measure | One-call full-scene spatial model; precise distance/overlap/gap measurement |
| 🎨 Aesthetic analysis | scene_aesthetics | 5 dimensions: color theory (60-30-10), spatial composition (golden ratio / rule of thirds), proportion & scale (ergonomics), lighting quality (three-point / fill ratio / temperature / decay), visual flow |
| 🎬 Camera planning | camera_create camera_orbit | 8 industry-standard shot types + orbit animation |
| 🛡️ Disaster recovery | scene_checkpoint scene_rollback scene_checkpoint_list | exportAll in-memory snapshots; rollback explicitly rebinds the original path |
| 🧠 Scene planning | scene_plan | Organization health, zone balance, layout suggestions, conflict prevention, natural-language planning |
| 📋 Engineering audit | scene_review scene_validate scene_assert | 11 deterministic checks (0-100 score) + custom constraint validation + state assertions |
| ⚡ Code execution | execute_code write_module | Run arbitrary Python in Maya / inject reusable modules |
| 👁️ Visual loop | scene_viewport_snapshot scene_render_preview | WYSIWYG viewport capture + single-frame playblast preview (GUI sessions only) |
| 🔌 Session management | list_sessions add_session maya_setup_guide | Multi-session discovery/attach + connection diagnosis/install/fallback guidance |
| 📦 Asset library | asset_search asset_import | Poly Haven CC0 models — host-side HTTPS download (host allowlist, md5 verify, size caps, platformdirs cache), Maya-side FBX import with texture wiring, polycount/dims guards, GRP_asset_<id> dedup |
| 📤 Scene export | scene_export | FBX/OBJ/USD export — whole scene or named objects; format inferred from extension (conflict is an error, never a guess), parent dirs auto-created, overwrite opt-in, selection restored |
| 🔎 Scene-graph introspection | scene_describe scene_nodes | API-level node self-description (exact type, per-attribute metadata: keyable/connectable/enum/ranges, connection wiring — size-bounded, *_truncated disclosure, limit up to 1000) + bounded enumeration incl. non-DAG nodes (materials/tool nodes) with has_more/next_cursor pagination |
25 MCP tools in total.
# Install from PyPI (recommended)
pip install mcp-for-maya
# or run straight away with uvx
uvx mcp-for-maya
# alternative: install from git with uv
uv tool install git+https://github.com/Xxx91n/mcp-for-maya.git
# or clone the source
git clone https://github.com/Xxx91n/mcp-for-maya.git
cd mcp-for-maya
pip install -e .
Note
Three-layer naming: dist name mcp-for-maya (PyPI shelf name) → installs import package maya_mcp_server (kept from upstream); script name mcp-for-maya (old name maya-mcp-server remains as a compat alias). uvx mcp-for-maya resolves precisely because the command name matches the dist name.
With the MCP server running, the agent calls maya_setup_guide to walk the connection:
userSetup.py (idempotent marker-block merge, timestamped backup before writing)In Maya's Script Editor (Python mode, not MEL):
import maya.cmds as cmds
cmds.commandPort(name=":7001", sourceType="python")
Save this as userSetup.py in your Maya scripts directory:
| Platform | Path |
|---|---|
| Windows | %MAYA_APP_DIR%\<version>\scripts\ |
| Linux | ~/maya/<version>/scripts/ |
| macOS | ~/Library/Preferences/Autodesk/maya/<version>/scripts/ |
import maya.cmds as cmds
cmds.evalDeferred('cmds.commandPort(name=":7001", sourceType="python")', lowestPriority=True)
A commandPort is a single listening socket bound to one host:port — a second instance fails to bind the same port, so each Maya instance needs its own port. Typical topology:
| Maya instance | commandPort | Notes |
|---|---|---|
| Instance A | :7001 python | primary |
| Instance B | :7002 python | second instance |
| Instance A | :7011 mel | MEL port (auto-detected and exempted — no error spam) |
Run cmds.commandPort(name=":<port>", sourceType="python") inside each instance (its Script Editor or its own userSetup.py). Auto-scan is the primary path — the server periodically enumerates listening Maya ports and bootstraps them; if a session is missed, add_session(host, port) is the manual fallback. If scanning probes a non-Maya TCP service on the box, bound the probe set with MAYA_MCP_INCLUDE_PORTS=7001,7002 (comma-separated, 7005-7010 ranges allowed) or exclude offenders via MAYA_MCP_EXCLUDE_PORTS=<port>.
Note: a commandPort does not persist across sessions — it dies with Maya; for persistence write it into userSetup.py (Option C).
| Problem | Fix |
|---|---|
list_sessions returns empty | call maya_setup_guide(action="diagnose") |
| Port in use | close other Maya instances or pick another port |
| userSetup.py not loading | check it sits in the right scripts dir, restart Maya |
| Firewall blocks | ensure localhost:7001 is reachable |
| Second Maya instance not listed | each instance needs its own commandPort (same port = bind conflict); if scanning still misses it, call add_session(host, port) |
| Script Editor spams syntax errors | legacy symptom of probing a MEL port — current versions auto-exempt non-Python ports; if it persists, exclude the port via MAYA_MCP_EXCLUDE_PORTS |
Codex ~/.codex/config.toml:
[mcp_servers.maya]
command = "uvx"
args = ["mcp-for-maya"]
tool_timeout_sec = 120
For the git source use args = ["--from", "git+https://github.com/Xxx91n/mcp-for-maya.git", "mcp-for-maya"]; for a source checkout use command = "python", args = ["-m", "maya_mcp_server"], and point PYTHONPATH at <repo>/src under env.
Talk naturally:
"Look at what's in my Maya scene, then create a display shelf at the entrance"
The agent calls scene_snapshot() → understands the scene → models → scene_review() audits the result.
Every scene modification follows the ICEV loop (also shipped as an agent process card, see skills/icev-workflow):
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ INSPECT │ ──→ │ COMPUTE │ ──→ │ EXECUTE │ ──→ │ VERIFY │
│ snapshot │ │ plan │ │ apply │ │ audit │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
scene_snapshot() for full-scene spatial dataexecute_code() applies Maya Pythonscene_assert() + scene_review() confirm the result; on GUI sessions the visual tools give pixel-level confirmationscene_snapshot(detail="compact", format="cos") # full scene in one call
scene_inspect(target="wall_entrance", include_neighbors=True)
scene_measure(obj_a="wall_north", obj_b="counter_A", mode="clearance")
scene_assert(expectations='{"wall": {"exists": true, "position": [0,0,500]}}')
scene_review() # 11 deterministic checks, 0-100 score
scene_validate(rules='[{"type": "min_clearance", "value": 180}]')
camera_create(target="product_display", shot_type="medium", azimuth=30, elevation=15)
# extreme_wide / wide / medium / close / extreme_close / bird_eye / low_angle / over_shoulder
camera_orbit(center=[0, 100, 0], radius=500, frames=120)
scene_checkpoint(name="before_renovation") # exportAll in-memory snapshot; no undo history
scene_checkpoint_list()
scene_rollback(filename="cp_before_renovation.ma")
# auto safety snapshot first, then rebinds the scene name to the original path
# call scene_snapshot() after a rollback — snapshots carry no undo history
# self-contained: references are flattened (no write-back); one scene file per session assumed
asset_search(query="camera", asset_type="models", limit=20) # Poly Haven index
asset_import(asset_id="Camera_01", resolution="1k")
# HTTPS-only download into a platformdirs cache (per-file md5 verify + sha256 audit),
# then Maya imports the local FBX under GRP_asset_<asset_id> (repeat calls dedup;
# force=True re-imports). Textures auto-wire by filename suffix
# (diff/rough/metal/nor_gl/ao/disp...) with sRGB/Raw color spaces and bump2d normals.
# Single-part assets ship bare map names (Diffuse/Rough/Metal...) — those are
# recognised too and wired onto the asset's single material.
# Guards: 100k-face polycount cap (allow_high_polycount override), dims sanity report.
scene_export(path="D:/out/scene.fbx") # whole scene; format inferred from .fbx
scene_export(path="/tmp/kit", format="obj") # missing extension is appended -> kit.obj
scene_export(path="D:/out/kit.usd", objects=["GEO_box"]) # exportSelected; prior selection restored
# format {fbx,obj,usd}: an extension/format conflict is an error, never a guess.
# An existing file is rejected unless overwrite=True. prompt=False is forced
# (no modal can hang the channel); the scene's modified flag is untouched.
# Alembic is not supported — AbcExport is not a cmds.file surface.
scene_viewport_snapshot(
max_size=800, format="jpeg"
) # HUD/selection included — what the artist sees
scene_render_preview(camera="CAM_hero", width=640, height=360) # clean single-frame playblast
# both return [image, JSON metadata]; headless sessions get a gui_session_required error
# prefer format="png" for wireframe/line-art review; trust returned metadata for actual size
Default output uses Chain-of-Symbol notation to compress scene data. The format's paper reports ~65% token savings vs JSON on its demo scenes (arXiv:2305.10276, −65.8%) — this project implements the notation; that figure is the paper's measurement, not a benchmark of this project.
SCENE[164obj, 5zones] UNIT=cm UP=y
shell (23obj) @(-11.8,178.8,145.7)
GRP_floor[mesh]@(0,0,0) 1121.5x20x1530.5
Two Experimental process cards ship in skills/:
| Skill | Purpose |
|---|---|
skills/icev-workflow | ICEV discipline: every scene mutation goes through Inspect→Compute→Execute→Verify |
skills/scene-review-playbook | Audit playbook: what the 11 checks weigh and how to map findings to fixes |
Evaluated on Claude Code only; untested on Codex/Gemini CLI/Cursor. Cross-model evaluation is tracked in issue #3.
scene_review() provides 11 universal checks (score normalized to 0-100):
| Check | Max pts | What it looks at |
|---|---|---|
| spatial | 10 | object/camera/light presence |
| overlaps | 10 | bbox collisions (parent-child excluded) |
| conflicts | 10 | penetration between unrelated objects |
| zones | 5 | naming-rule zone coverage |
| naming | 5 | production naming convention |
| components | 10 | GRP_ grouping + nesting depth ≤ 4 |
| orphans | 5 | empty groups / default names |
| aesthetics | 15 | 5-dimension aesthetics (color/composition/scale/lighting/flow) |
| lighting | 10 | three-point setup, fill ratio, decay |
| organization | 10 | overall hierarchy health |
| constraints | 5 | custom rule violations |
api.polyhaven.com / dl.polyhaven.org|.com (host allowlist + md5 + size caps in polyhaven.py), and only when you call them.pipeline.py + security.py) validates arguments + token-bucket rate limits (~100/60s reads, ~20/60s writes, per session) + pattern scan (warn-only by default) + an independent JSONL audit log across all 25 tools. It catches accidents, not malicious clients — full model in docs/threat-model.md.scene_checkpoint/scene_rollback give in-memory snapshots and explicit rollback (no undo history; references flattened).MAYA_MCP_DISABLE_EXECUTE=1, MAYA_MCP_DISABLE_WRITE_MODULE=1, or MAYA_MCP_DISABLE_ARBITRARY=1 (all three) to remove the escape-hatch tools from tools/list and deny the call with [policy_disabled]. Read-only tools are unaffected. These are misuse guardrails, not a hostile-agent boundary — an agent that still holds execute_code is already inside the trust boundary; what they buy is a smaller blast radius for a demo, a mistake, or a CI sandbox. Rationale: docs/threat-model.md. Per-code reference: docs/guide/error-codes.md.This project follows Semantic Versioning:
4 - Beta).5 - Production/Stable classifier in one commit; gated by the #7 real-machine checklist (Pass-set green + explicit waived items with owner/expiry — three-state gate, D-163).The public API is the MCP tool surface: tool names, their input/output shapes, the two-layer error contract (host isError failures vs {error:{code,message,suggestion}} domain results), and tool-annotation semantics (docs/threat-model.md §5). Additive changes (new tools, new optional response fields) ship as minor releases; breaking changes ship as a major bump. 1.0.0 is a freeze commitment on this surface — not a quality certification: the remaining real-machine verification surface is tracked explicitly in #7 rather than implied away.
Releases are milestone-driven — no fixed cadence promised. Roadmap lives in GitHub issues: #2 Poly Haven integration (model slice shipped in 0.2.0; scene_plan recommendation residual split to #31), #3 Skills program (v1.x), #4 security & permission model (v1.x), #5 scene export + introspection (scene_export shipped in 0.3.0: FBX/OBJ/USD; scene_describe/scene_nodes introspection shipped in the same 0.3.0), #6 more asset sources (exploratory), #7 real-machine checklist + v1.0 feedback (pinned).
| Environment | Requirement |
|---|---|
| Host (runs the MCP server) | Python >= 3.10 (requires-python); Windows / Linux / macOS |
| Injected helper (runs inside Maya) | Maya >= 2023 (bundled Python >= 3.9; relies on ast.unparse — older versions get an explicit refusal at injection) |
| Verified against | Maya 2024 GUI |
The two visual-loop tools need a GUI session (headless/mayapy returns a structured capability error). On the first capture the server probes the VP2 readback direction per-session (a disposable scene probe, net-zero side effects); MAYA_MCP_VP2_BOTTOM_UP=0|1 forces it when a driver misreports.
uv sync --frozen # locked dev env from uv.lock (alt: pip install -e . --group dev, pip>=25.1)
python -m pytest tests/ -q # tests
ruff check src tests # lint
mypy src # typecheck
python -m maya_mcp_server -vv # run with DEBUG logs (-v=INFO, -vv=DEBUG)
See CONTRIBUTING.md for the PR flow and CHANGELOG.md for the change log.
Forked from chadrik/maya-mcp-server — upstream MIT copyright retained (see LICENSE); this project adds the scene-intelligence layer on top of its connection stack.
How each upstream open issue maps to a disposition and release version in this fork: docs/upstream-issue-status.md.