Skip to content

API Reference

This is the generated Python API reference for Maya MCP.

Use it when you are working directly against the Python package. If you are integrating through MCP, start with Getting Started and Tool Guide first.

Typed Result Models

Tool functions return plain dictionaries at runtime, but high-use tools expose TypedDict result models in their public Python annotations. These models describe the same keys documented in the tool contracts; they do not change MCP JSON response shapes or error conventions.

Nodes

Model Backing tools
maya_mcp.tools.nodes.NodesListOutput nodes.list
maya_mcp.tools.nodes.NodesCreateOutput nodes.create
maya_mcp.tools.nodes.NodesInfoOutput nodes.info
maya_mcp.tools.nodes.NodesDeleteOutput nodes.delete
maya_mcp.tools.nodes.NodesRenameOutput nodes.rename
maya_mcp.tools.nodes.NodesParentOutput nodes.parent
maya_mcp.tools.nodes.NodesDuplicateOutput nodes.duplicate

Attributes

Model Backing tools
maya_mcp.tools.attributes.AttributesGetOutput attributes.get
maya_mcp.tools.attributes.AttributesSetOutput attributes.set

Selection

Model Backing tools
maya_mcp.tools.selection.SelectionOutput selection.get, selection.set, selection.clear
maya_mcp.tools.selection.SelectionWithErrorsOutput selection.set_components
maya_mcp.tools.selection.SelectionComponentsOutput selection.get_components
maya_mcp.tools.selection.SelectionConvertComponentsOutput selection.convert_components

Mesh

Model Backing tools
maya_mcp.tools.mesh.MeshInfoOutput mesh.info
maya_mcp.tools.mesh.MeshVerticesOutput mesh.vertices
maya_mcp.tools.mesh.MeshEvaluateOutput mesh.evaluate

Connections

Model Backing tools
maya_mcp.tools.connections.ConnectionEntry connections.list
maya_mcp.tools.connections.ConnectionsListOutput connections.list
maya_mcp.tools.connections.ConnectionAttributeInfo connections.get
maya_mcp.tools.connections.ConnectionsGetOutput connections.get
maya_mcp.tools.connections.ConnectionsConnectOutput connections.connect
maya_mcp.tools.connections.ConnectionPair connections.disconnect
maya_mcp.tools.connections.ConnectionsDisconnectOutput connections.disconnect
maya_mcp.tools.connections.ConnectionHistoryEntry connections.history
maya_mcp.tools.connections.ConnectionsHistoryOutput connections.history

Viewport

Model Backing tools
maya_mcp.tools.viewport.ViewportCaptureOutput viewport.capture

Modeling

Model Backing tools
maya_mcp.tools.modeling.ModelingCreatePolygonPrimitiveOutput modeling.create_polygon_primitive
maya_mcp.tools.modeling.ModelingExtrudeFacesOutput modeling.extrude_faces
maya_mcp.tools.modeling.ModelingBooleanOutput modeling.boolean
maya_mcp.tools.modeling.ModelingCombineOutput modeling.combine
maya_mcp.tools.modeling.ModelingSeparateOutput modeling.separate
maya_mcp.tools.modeling.ModelingMergeVerticesOutput modeling.merge_vertices
maya_mcp.tools.modeling.ModelingBevelOutput modeling.bevel
maya_mcp.tools.modeling.ModelingBridgeOutput modeling.bridge
maya_mcp.tools.modeling.ModelingInsertEdgeLoopOutput modeling.insert_edge_loop
maya_mcp.tools.modeling.ModelingDeleteFacesOutput modeling.delete_faces
maya_mcp.tools.modeling.ModelingMoveComponentsOutput modeling.move_components
maya_mcp.tools.modeling.ModelingFreezeTransformsOutput modeling.freeze_transforms
maya_mcp.tools.modeling.ModelingDeleteHistoryOutput modeling.delete_history
maya_mcp.tools.modeling.ModelingCenterPivotOutput modeling.center_pivot
maya_mcp.tools.modeling.ModelingSetPivotOutput modeling.set_pivot

Shading

Model Backing tools
maya_mcp.tools.shading.ShadingCreateMaterialOutput shading.create_material
maya_mcp.tools.shading.ShadingAssignMaterialOutput shading.assign_material
maya_mcp.tools.shading.ShadingSetMaterialColorOutput shading.set_material_color

Curves

Model Backing tools
maya_mcp.tools.curve.CurveInfoOutput curve.info
maya_mcp.tools.curve.CurveCvsOutput curve.cvs

Skinning

Model Backing tools
maya_mcp.tools.skin.SkinBindOutput skin.bind
maya_mcp.tools.skin.SkinUnbindOutput skin.unbind
maya_mcp.tools.skin.SkinInfluenceEntry skin.influences
maya_mcp.tools.skin.SkinInfluencesOutput skin.influences
maya_mcp.tools.skin.SkinWeightEntry skin.weights.get
maya_mcp.tools.skin.SkinWeightsGetOutput skin.weights.get
maya_mcp.tools.skin.SkinWeightsSetOutput skin.weights.set
maya_mcp.tools.skin.SkinCopyWeightsOutput skin.copy_weights

Animation

Model Backing tools
maya_mcp.tools.animation.AnimationSetTimeOutput animation.set_time
maya_mcp.tools.animation.AnimationGetTimeRangeOutput animation.get_time_range
maya_mcp.tools.animation.AnimationSetTimeRangeOutput animation.set_time_range
maya_mcp.tools.animation.AnimationSetKeyframeOutput animation.set_keyframe
maya_mcp.tools.animation.KeyframeEntry animation.get_keyframes
maya_mcp.tools.animation.AnimationGetKeyframesOutput animation.get_keyframes
maya_mcp.tools.animation.AnimationDeleteKeyframesOutput animation.delete_keyframes

Scripts

Model Backing tools
maya_mcp.tools.scripts.ScriptListEntry script.list
maya_mcp.tools.scripts.ScriptListOutput script.list
maya_mcp.tools.scripts.ScriptExecuteOutput script.execute
maya_mcp.tools.scripts.ScriptRunOutput script.run

Package

maya_mcp

Maya MCP Server.

A Model Context Protocol (MCP) server for controlling Autodesk Maya via its commandPort socket interface.

This package provides
  • MCP tools for interacting with Maya (scene, nodes, selection, etc.)
  • A transport layer for Maya commandPort communication
  • Typed error handling and resilience features
Example

Run the MCP server::

python -m maya_mcp.server

Or import and use programmatically::

from maya_mcp.server import mcp
mcp.run()
Note

This package does NOT import any Maya modules directly. All communication with Maya happens via TCP socket to Maya's commandPort.

MayaCommandError dataclass

MayaCommandError(message: str, details: dict[str, Any] = dict(), command: str = '', maya_error: str = '')

Bases: MayaMCPError

Raised when a Maya command fails to execute.

This error indicates that the command was sent to Maya but failed during execution. The command syntax may be invalid, or the operation may have failed for other reasons.

ATTRIBUTE DESCRIPTION
message

Human-readable error description.

TYPE: str

command

The command that failed (may be truncated for security).

TYPE: str

maya_error

The error message returned by Maya.

TYPE: str

MayaMCPError dataclass

MayaMCPError(message: str, details: dict[str, Any] = dict())

Bases: Exception

Base exception for all Maya MCP errors.

All errors raised by Maya MCP inherit from this class, making it easy to catch all Maya MCP-related exceptions with a single handler.

ATTRIBUTE DESCRIPTION
message

Human-readable error description.

TYPE: str

details

Additional context as key-value pairs.

TYPE: dict[str, Any]

MayaTimeoutError dataclass

MayaTimeoutError(message: str, details: dict[str, Any] = dict(), timeout_seconds: float = 0.0, operation: str = '')

Bases: MayaMCPError

Raised when a Maya operation times out.

This error indicates that a command was sent to Maya but no response was received within the configured timeout period.

ATTRIBUTE DESCRIPTION
message

Human-readable error description.

TYPE: str

timeout_seconds

The timeout value that was exceeded.

TYPE: float

operation

Description of the operation that timed out.

TYPE: str

MayaUnavailableError dataclass

MayaUnavailableError(message: str, details: dict[str, Any] = dict(), host: str = 'localhost', port: int = 7001, attempts: int = 0, last_error: str | None = None)

Bases: MayaMCPError

Raised when Maya cannot be reached via commandPort.

This error indicates that the MCP server cannot establish a connection to Maya's commandPort. This typically happens when:

  • Maya is not running
  • commandPort is not open in Maya
  • Network/socket issues
ATTRIBUTE DESCRIPTION
message

Human-readable error description.

TYPE: str

host

Target host that was attempted.

TYPE: str

port

Target port that was attempted.

TYPE: int

attempts

Number of connection attempts made.

TYPE: int

last_error

The last underlying error message, if any.

TYPE: str | None

ConnectionStatus

Bases: Enum

Connection status for Maya commandPort.

ATTRIBUTE DESCRIPTION
OK

Connected and responsive.

OFFLINE

Not connected, no active connection attempts.

RECONNECTING

Attempting to establish/re-establish connection.

Server

server

Maya MCP Server entrypoint.

This module creates and configures the FastMCP server instance, registers all tools, and provides the main entry point.

Example

Run the server::

python -m maya_mcp.server

Or use the CLI::

maya-mcp

Or import and use programmatically::

from maya_mcp.server import mcp
mcp.run()

mcp module-attribute

mcp = create_server()

create_server

create_server() -> FastMCP

Create and configure the FastMCP server instance.

Source code in src/maya_mcp/server.py
def create_server() -> FastMCP:
    """Create and configure the FastMCP server instance."""
    mcp = FastMCP(
        name="Maya MCP",
        instructions=SERVER_INSTRUCTIONS,
        version=SERVER_VERSION,
        website_url=SERVER_WEBSITE_URL,
    )
    register_all_tools(mcp)
    return mcp

main

main() -> None

Run the Maya MCP server.

This is the main entry point for the server. It starts the FastMCP server with stdio transport (the default for MCP).

Source code in src/maya_mcp/server.py
def main() -> None:
    """Run the Maya MCP server.

    This is the main entry point for the server. It starts the FastMCP
    server with stdio transport (the default for MCP).
    """
    if os.environ.get("MAYA_MCP_SKIP_RUN") == "1":
        return
    mcp.run()

Errors

errors

Typed error classes for Maya MCP.

This module defines the error hierarchy used throughout Maya MCP. All errors inherit from MayaMCPError, making it easy to catch all Maya MCP-related exceptions.

Example

Handling Maya errors::

from maya_mcp.errors import MayaUnavailableError, MayaCommandError

try:
    result = client.execute("cmds.ls()")
except MayaUnavailableError as e:
    print(f"Maya not available: {e.message}")
except MayaCommandError as e:
    print(f"Command failed: {e.message}")

MayaMCPError dataclass

MayaMCPError(message: str, details: dict[str, Any] = dict())

Bases: Exception

Base exception for all Maya MCP errors.

All errors raised by Maya MCP inherit from this class, making it easy to catch all Maya MCP-related exceptions with a single handler.

ATTRIBUTE DESCRIPTION
message

Human-readable error description.

TYPE: str

details

Additional context as key-value pairs.

TYPE: dict[str, Any]

MayaUnavailableError dataclass

MayaUnavailableError(message: str, details: dict[str, Any] = dict(), host: str = 'localhost', port: int = 7001, attempts: int = 0, last_error: str | None = None)

Bases: MayaMCPError

Raised when Maya cannot be reached via commandPort.

This error indicates that the MCP server cannot establish a connection to Maya's commandPort. This typically happens when:

  • Maya is not running
  • commandPort is not open in Maya
  • Network/socket issues
ATTRIBUTE DESCRIPTION
message

Human-readable error description.

TYPE: str

host

Target host that was attempted.

TYPE: str

port

Target port that was attempted.

TYPE: int

attempts

Number of connection attempts made.

TYPE: int

last_error

The last underlying error message, if any.

TYPE: str | None

MayaCommandError dataclass

MayaCommandError(message: str, details: dict[str, Any] = dict(), command: str = '', maya_error: str = '')

Bases: MayaMCPError

Raised when a Maya command fails to execute.

This error indicates that the command was sent to Maya but failed during execution. The command syntax may be invalid, or the operation may have failed for other reasons.

ATTRIBUTE DESCRIPTION
message

Human-readable error description.

TYPE: str

command

The command that failed (may be truncated for security).

TYPE: str

maya_error

The error message returned by Maya.

TYPE: str

MayaTimeoutError dataclass

MayaTimeoutError(message: str, details: dict[str, Any] = dict(), timeout_seconds: float = 0.0, operation: str = '')

Bases: MayaMCPError

Raised when a Maya operation times out.

This error indicates that a command was sent to Maya but no response was received within the configured timeout period.

ATTRIBUTE DESCRIPTION
message

Human-readable error description.

TYPE: str

timeout_seconds

The timeout value that was exceeded.

TYPE: float

operation

Description of the operation that timed out.

TYPE: str

ValidationError dataclass

ValidationError(message: str, details: dict[str, Any] = dict(), field_name: str = '', value: str = '', constraint: str = '')

Bases: MayaMCPError

Raised when input validation fails.

This error indicates that the input parameters to a tool or function did not pass validation.

ATTRIBUTE DESCRIPTION
message

Human-readable error description.

TYPE: str

field

The name of the field that failed validation.

TYPE: str

value

The invalid value (sanitized for security).

TYPE: str

constraint

Description of the violated constraint.

TYPE: str

Types

types

Type definitions for Maya MCP.

This module contains all shared type definitions used throughout Maya MCP, including enums, dataclasses, and type aliases.

These types are designed to be stable and form the public API contract that MCP clients can rely on.

ConnectionStatus

Bases: Enum

Connection status for Maya commandPort.

ATTRIBUTE DESCRIPTION
OK

Connected and responsive.

OFFLINE

Not connected, no active connection attempts.

RECONNECTING

Attempting to establish/re-establish connection.

ConnectionConfig dataclass

ConnectionConfig(host: str = 'localhost', port: int = 7001, connect_timeout: float = 5.0, command_timeout: float = 30.0, max_retries: int = 3, retry_base_delay: float = 0.5)

Configuration for Maya commandPort connection.

ATTRIBUTE DESCRIPTION
host

Target host (localhost only in v0).

TYPE: str

port

Target port number.

TYPE: int

connect_timeout

Connection timeout in seconds.

TYPE: float

command_timeout

Command execution timeout in seconds.

TYPE: float

max_retries

Maximum number of connection retry attempts.

TYPE: int

retry_base_delay

Base delay for exponential backoff (seconds).

TYPE: float

HealthCheckResult dataclass

HealthCheckResult(status: Literal['ok', 'offline', 'reconnecting'], last_error: str | None, last_contact: str | None, host: str, port: int)

Result of a health check operation.

ATTRIBUTE DESCRIPTION
status

Current connection status.

TYPE: Literal['ok', 'offline', 'reconnecting']

last_error

Last error message, if any.

TYPE: str | None

last_contact

Timestamp of last successful contact with Maya.

TYPE: str | None

host

Current target host.

TYPE: str

port

Current target port.

TYPE: int

ConnectResult dataclass

ConnectResult(connected: bool, host: str, port: int, error: str | None = None)

Result of a connection attempt.

ATTRIBUTE DESCRIPTION
connected

Whether connection was successful.

TYPE: bool

host

Target host.

TYPE: str

port

Target port.

TYPE: int

error

Error message if connection failed.

TYPE: str | None

DisconnectResult dataclass

DisconnectResult(disconnected: bool, was_connected: bool)

Result of a disconnect operation.

ATTRIBUTE DESCRIPTION
disconnected

Whether disconnect was successful.

TYPE: bool

was_connected

Whether was connected before disconnect.

TYPE: bool

SceneInfo dataclass

SceneInfo(file_path: str | None, modified: bool, fps: float, frame_range: tuple[float, float], up_axis: Literal['y', 'z'])

Information about the current Maya scene.

ATTRIBUTE DESCRIPTION
file_path

Path to the current scene file, or None if untitled.

TYPE: str | None

modified

Whether the scene has unsaved changes.

TYPE: bool

fps

Frames per second.

TYPE: float

frame_range

Tuple of (start_frame, end_frame).

TYPE: tuple[float, float]

up_axis

Scene up axis ('y' or 'z').

TYPE: Literal['y', 'z']

NodeListResult dataclass

NodeListResult(nodes: list[str] = list(), count: int = 0)

Result of a node listing operation.

ATTRIBUTE DESCRIPTION
nodes

List of node names.

TYPE: list[str]

count

Number of nodes in the list.

TYPE: int

SelectionResult dataclass

SelectionResult(selection: list[str] = list(), count: int = 0)

Result of a selection query or modification.

ATTRIBUTE DESCRIPTION
selection

List of selected node names.

TYPE: list[str]

count

Number of selected items.

TYPE: int

ClientState dataclass

ClientState(status: ConnectionStatus = OFFLINE, last_error: str | None = None, last_contact: datetime | None = None, config: ConnectionConfig = ConnectionConfig())

Internal state of the CommandPort client.

ATTRIBUTE DESCRIPTION
status

Current connection status.

TYPE: ConnectionStatus

last_error

Last error encountered.

TYPE: str | None

last_contact

Timestamp of last successful Maya contact.

TYPE: datetime | None

config

Current connection configuration.

TYPE: ConnectionConfig

update_contact

update_contact() -> None

Update last_contact to current time.

Source code in src/maya_mcp/types.py
def update_contact(self) -> None:
    """Update last_contact to current time."""
    self.last_contact = datetime.now(timezone.utc)

get_last_contact_iso

get_last_contact_iso() -> str | None

Get last_contact as ISO8601 string.

Source code in src/maya_mcp/types.py
def get_last_contact_iso(self) -> str | None:
    """Get last_contact as ISO8601 string."""
    if self.last_contact is None:
        return None
    return self.last_contact.isoformat() + "Z"

Transport

transport

Transport layer for Maya MCP.

This package provides the communication layer between Maya MCP and Maya's commandPort socket interface.

CommandPortClient

CommandPortClient(host: str = 'localhost', port: int = 7001, connect_timeout: float = 5.0, command_timeout: float = 30.0, max_retries: int = 3, retry_base_delay: float = 0.5)

Client for communicating with Maya via commandPort.

This client manages socket connections to Maya's commandPort, handles timeouts and retries, and translates errors to typed exceptions.

The client is designed for Level 1 resilience
  • Detects when Maya is unavailable
  • Returns typed errors
  • Automatically reconnects on next call when Maya restarts
ATTRIBUTE DESCRIPTION
config

Connection configuration.

state

Current client state.

Example

Basic usage::

client = CommandPortClient(host="localhost", port=7001)
try:
    client.connect()
    result = client.execute("cmds.ls()")
    print(result)
finally:
    client.disconnect()

With custom timeouts::

client = CommandPortClient(
    connect_timeout=10.0,
    command_timeout=60.0,
    max_retries=5,
)

Initialize the CommandPortClient.

PARAMETER DESCRIPTION
host

Target host. Only "localhost" or "127.0.0.1" are supported.

TYPE: str DEFAULT: 'localhost'

port

Target port number (1-65535).

TYPE: int DEFAULT: 7001

connect_timeout

Connection timeout in seconds.

TYPE: float DEFAULT: 5.0

command_timeout

Command execution timeout in seconds.

TYPE: float DEFAULT: 30.0

max_retries

Maximum number of connection retry attempts.

TYPE: int DEFAULT: 3

retry_base_delay

Base delay for exponential backoff (seconds).

TYPE: float DEFAULT: 0.5

RAISES DESCRIPTION
ValueError

If configuration is invalid.

Source code in src/maya_mcp/transport/commandport.py
def __init__(
    self,
    host: str = "localhost",
    port: int = 7001,
    connect_timeout: float = 5.0,
    command_timeout: float = 30.0,
    max_retries: int = 3,
    retry_base_delay: float = 0.5,
) -> None:
    """Initialize the CommandPortClient.

    Args:
        host: Target host. Only "localhost" or "127.0.0.1" are supported.
        port: Target port number (1-65535).
        connect_timeout: Connection timeout in seconds.
        command_timeout: Command execution timeout in seconds.
        max_retries: Maximum number of connection retry attempts.
        retry_base_delay: Base delay for exponential backoff (seconds).

    Raises:
        ValueError: If configuration is invalid.
    """
    self.config = ConnectionConfig(
        host=host,
        port=port,
        connect_timeout=connect_timeout,
        command_timeout=command_timeout,
        max_retries=max_retries,
        retry_base_delay=retry_base_delay,
    )
    self.state = ClientState(config=self.config)
    self._socket: socket.socket | None = None
    self._lock = threading.RLock()

connect

connect() -> bool

Establish connection to Maya commandPort.

Attempts to connect to Maya's commandPort with retry logic. Uses exponential backoff between retry attempts.

RETURNS DESCRIPTION
bool

True if connection was successful.

RAISES DESCRIPTION
MayaUnavailableError

If connection fails after all retries.

Example

client = CommandPortClient() if client.connect(): ... print("Connected to Maya")

Source code in src/maya_mcp/transport/commandport.py
def connect(self) -> bool:
    """Establish connection to Maya commandPort.

    Attempts to connect to Maya's commandPort with retry logic.
    Uses exponential backoff between retry attempts.

    Returns:
        True if connection was successful.

    Raises:
        MayaUnavailableError: If connection fails after all retries.

    Example:
        >>> client = CommandPortClient()
        >>> if client.connect():
        ...     print("Connected to Maya")
    """
    with self._lock:
        if self._socket is not None:
            # Already connected
            return True

        self.state.status = ConnectionStatus.RECONNECTING
        last_error: str | None = None
        logger.info("Connecting to Maya at %s:%d", self.config.host, self.config.port)

        for attempt in range(self.config.max_retries):
            try:
                self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
                self._socket.settimeout(self.config.connect_timeout)
                self._socket.connect((self.config.host, self.config.port))

                # Connection successful
                self.state.status = ConnectionStatus.OK
                self.state.last_error = None
                self.state.update_contact()
                logger.info("Connected to Maya at %s:%d", self.config.host, self.config.port)
                return True

            except TimeoutError:
                last_error = f"Connection timed out after {self.config.connect_timeout}s"
                self._cleanup_socket()
            except ConnectionRefusedError:
                last_error = "Connection refused - is Maya running with commandPort open?"
                self._cleanup_socket()
            except OSError as e:
                last_error = f"Socket error: {e}"
                self._cleanup_socket()

            # Exponential backoff before retry
            if attempt < self.config.max_retries - 1:
                delay = self.config.retry_base_delay * (2**attempt)
                logger.debug(
                    "Connection attempt %d/%d failed: %s. Retrying in %.1fs",
                    attempt + 1,
                    self.config.max_retries,
                    last_error,
                    delay,
                )
                time.sleep(delay)

        # All retries exhausted
        self.state.status = ConnectionStatus.OFFLINE
        self.state.last_error = last_error
        logger.warning(
            "Failed to connect to Maya after %d attempts: %s",
            self.config.max_retries,
            last_error,
        )

        raise MayaUnavailableError(
            message=f"Cannot connect to Maya commandPort at {self.config.host}:{self.config.port}",
            host=self.config.host,
            port=self.config.port,
            attempts=self.config.max_retries,
            last_error=last_error,
        )

disconnect

disconnect() -> bool

Close the connection to Maya.

RETURNS DESCRIPTION
bool

True if disconnection was successful, False if wasn't connected.

Example

client.disconnect() True

Source code in src/maya_mcp/transport/commandport.py
def disconnect(self) -> bool:
    """Close the connection to Maya.

    Returns:
        True if disconnection was successful, False if wasn't connected.

    Example:
        >>> client.disconnect()
        True
    """
    with self._lock:
        was_connected = self._socket is not None
        self._cleanup_socket()
        self.state.status = ConnectionStatus.OFFLINE
        if was_connected:
            logger.info("Disconnected from Maya")
        return was_connected

execute

execute(command: str) -> str

Execute a Python command in Maya and return the result.

Sends a command to Maya via commandPort and waits for the response. Automatically connects if not already connected. If the connection drops during the send phase, reconnects and retries once.

PARAMETER DESCRIPTION
command

Python code to execute in Maya.

TYPE: str

RETURNS DESCRIPTION
str

Command output as string.

RAISES DESCRIPTION
MayaUnavailableError

Cannot connect to Maya.

MayaCommandError

Command execution failed.

MayaTimeoutError

Command timed out.

Example

result = client.execute("cmds.ls(selection=True)") print(result) ['pCube1', 'pSphere1']

Source code in src/maya_mcp/transport/commandport.py
def execute(self, command: str) -> str:
    """Execute a Python command in Maya and return the result.

    Sends a command to Maya via commandPort and waits for the response.
    Automatically connects if not already connected. If the connection
    drops during the send phase, reconnects and retries once.

    Args:
        command: Python code to execute in Maya.

    Returns:
        Command output as string.

    Raises:
        MayaUnavailableError: Cannot connect to Maya.
        MayaCommandError: Command execution failed.
        MayaTimeoutError: Command timed out.

    Example:
        >>> result = client.execute("cmds.ls(selection=True)")
        >>> print(result)
        ['pCube1', 'pSphere1']
    """
    with self._lock:
        return self._execute_with_retry(command, allow_retry=True)

is_connected

is_connected() -> bool

Check if currently connected to Maya.

RETURNS DESCRIPTION
bool

True if socket is connected.

Example

if client.is_connected(): ... print("Connected")

Source code in src/maya_mcp/transport/commandport.py
def is_connected(self) -> bool:
    """Check if currently connected to Maya.

    Returns:
        True if socket is connected.

    Example:
        >>> if client.is_connected():
        ...     print("Connected")
    """
    with self._lock:
        return self._socket is not None and self.state.status == ConnectionStatus.OK

get_status

get_status() -> ConnectionStatus

Get the current connection status.

RETURNS DESCRIPTION
ConnectionStatus

Current ConnectionStatus enum value.

Example

status = client.get_status() if status == ConnectionStatus.OK: ... print("Connected and healthy")

Source code in src/maya_mcp/transport/commandport.py
def get_status(self) -> ConnectionStatus:
    """Get the current connection status.

    Returns:
        Current ConnectionStatus enum value.

    Example:
        >>> status = client.get_status()
        >>> if status == ConnectionStatus.OK:
        ...     print("Connected and healthy")
    """
    with self._lock:
        return self.state.status

get_health

get_health() -> HealthCheckResult

Get detailed health information.

RETURNS DESCRIPTION
HealthCheckResult

HealthCheckResult with current connection health details.

Example

health = client.get_health() print(f"Status: {health.status}")

Source code in src/maya_mcp/transport/commandport.py
def get_health(self) -> HealthCheckResult:
    """Get detailed health information.

    Returns:
        HealthCheckResult with current connection health details.

    Example:
        >>> health = client.get_health()
        >>> print(f"Status: {health.status}")
    """
    with self._lock:
        return HealthCheckResult(
            status=self.state.status.value,
            last_error=self.state.last_error,
            last_contact=self.state.get_last_contact_iso(),
            host=self.config.host,
            port=self.config.port,
        )

reconfigure

reconfigure(host: str | None = None, port: int | None = None) -> None

Update connection configuration.

Disconnects if currently connected and updates the configuration.

PARAMETER DESCRIPTION
host

New target host (optional).

TYPE: str | None DEFAULT: None

port

New target port (optional).

TYPE: int | None DEFAULT: None

RAISES DESCRIPTION
ValueError

If new configuration is invalid.

Example

client.reconfigure(port=7002)

Source code in src/maya_mcp/transport/commandport.py
def reconfigure(
    self,
    host: str | None = None,
    port: int | None = None,
) -> None:
    """Update connection configuration.

    Disconnects if currently connected and updates the configuration.

    Args:
        host: New target host (optional).
        port: New target port (optional).

    Raises:
        ValueError: If new configuration is invalid.

    Example:
        >>> client.reconfigure(port=7002)
    """
    with self._lock:
        # Disconnect first
        self.disconnect()

        # Update config
        new_host = host if host is not None else self.config.host
        new_port = port if port is not None else self.config.port

        self.config = ConnectionConfig(
            host=new_host,
            port=new_port,
            connect_timeout=self.config.connect_timeout,
            command_timeout=self.config.command_timeout,
            max_retries=self.config.max_retries,
            retry_base_delay=self.config.retry_base_delay,
        )
        self.state.config = self.config

get_client

get_client() -> CommandPortClient

Get the global CommandPortClient instance.

Returns a singleton CommandPortClient instance, creating it if necessary. This is the recommended way to get a client for use in MCP tools.

RETURNS DESCRIPTION
CommandPortClient

The global CommandPortClient instance.

Example

client = get_client() client.execute("cmds.ls()")

Source code in src/maya_mcp/transport/commandport.py
def get_client() -> CommandPortClient:
    """Get the global CommandPortClient instance.

    Returns a singleton CommandPortClient instance, creating it if necessary.
    This is the recommended way to get a client for use in MCP tools.

    Returns:
        The global CommandPortClient instance.

    Example:
        >>> client = get_client()
        >>> client.execute("cmds.ls()")
    """
    global _client
    with _client_lock:
        if _client is None:
            _client = CommandPortClient()
        return _client

commandport

Maya commandPort client.

This module provides the CommandPortClient class for communicating with Maya via its commandPort socket interface.

The client handles
  • TCP socket connection management
  • Command encoding (UTF-8)
  • Response parsing
  • Timeout enforcement
  • Retry with exponential backoff
  • Error translation to typed exceptions
Example

Basic usage::

from maya_mcp.transport import CommandPortClient

client = CommandPortClient()
client.connect()
result = client.execute("cmds.ls(selection=True)")
client.disconnect()
Note

This module does NOT import any Maya modules. All communication happens via TCP socket.

CommandPortClient

CommandPortClient(host: str = 'localhost', port: int = 7001, connect_timeout: float = 5.0, command_timeout: float = 30.0, max_retries: int = 3, retry_base_delay: float = 0.5)

Client for communicating with Maya via commandPort.

This client manages socket connections to Maya's commandPort, handles timeouts and retries, and translates errors to typed exceptions.

The client is designed for Level 1 resilience
  • Detects when Maya is unavailable
  • Returns typed errors
  • Automatically reconnects on next call when Maya restarts
ATTRIBUTE DESCRIPTION
config

Connection configuration.

state

Current client state.

Example

Basic usage::

client = CommandPortClient(host="localhost", port=7001)
try:
    client.connect()
    result = client.execute("cmds.ls()")
    print(result)
finally:
    client.disconnect()

With custom timeouts::

client = CommandPortClient(
    connect_timeout=10.0,
    command_timeout=60.0,
    max_retries=5,
)

Initialize the CommandPortClient.

PARAMETER DESCRIPTION
host

Target host. Only "localhost" or "127.0.0.1" are supported.

TYPE: str DEFAULT: 'localhost'

port

Target port number (1-65535).

TYPE: int DEFAULT: 7001

connect_timeout

Connection timeout in seconds.

TYPE: float DEFAULT: 5.0

command_timeout

Command execution timeout in seconds.

TYPE: float DEFAULT: 30.0

max_retries

Maximum number of connection retry attempts.

TYPE: int DEFAULT: 3

retry_base_delay

Base delay for exponential backoff (seconds).

TYPE: float DEFAULT: 0.5

RAISES DESCRIPTION
ValueError

If configuration is invalid.

Source code in src/maya_mcp/transport/commandport.py
def __init__(
    self,
    host: str = "localhost",
    port: int = 7001,
    connect_timeout: float = 5.0,
    command_timeout: float = 30.0,
    max_retries: int = 3,
    retry_base_delay: float = 0.5,
) -> None:
    """Initialize the CommandPortClient.

    Args:
        host: Target host. Only "localhost" or "127.0.0.1" are supported.
        port: Target port number (1-65535).
        connect_timeout: Connection timeout in seconds.
        command_timeout: Command execution timeout in seconds.
        max_retries: Maximum number of connection retry attempts.
        retry_base_delay: Base delay for exponential backoff (seconds).

    Raises:
        ValueError: If configuration is invalid.
    """
    self.config = ConnectionConfig(
        host=host,
        port=port,
        connect_timeout=connect_timeout,
        command_timeout=command_timeout,
        max_retries=max_retries,
        retry_base_delay=retry_base_delay,
    )
    self.state = ClientState(config=self.config)
    self._socket: socket.socket | None = None
    self._lock = threading.RLock()

connect

connect() -> bool

Establish connection to Maya commandPort.

Attempts to connect to Maya's commandPort with retry logic. Uses exponential backoff between retry attempts.

RETURNS DESCRIPTION
bool

True if connection was successful.

RAISES DESCRIPTION
MayaUnavailableError

If connection fails after all retries.

Example

client = CommandPortClient() if client.connect(): ... print("Connected to Maya")

Source code in src/maya_mcp/transport/commandport.py
def connect(self) -> bool:
    """Establish connection to Maya commandPort.

    Attempts to connect to Maya's commandPort with retry logic.
    Uses exponential backoff between retry attempts.

    Returns:
        True if connection was successful.

    Raises:
        MayaUnavailableError: If connection fails after all retries.

    Example:
        >>> client = CommandPortClient()
        >>> if client.connect():
        ...     print("Connected to Maya")
    """
    with self._lock:
        if self._socket is not None:
            # Already connected
            return True

        self.state.status = ConnectionStatus.RECONNECTING
        last_error: str | None = None
        logger.info("Connecting to Maya at %s:%d", self.config.host, self.config.port)

        for attempt in range(self.config.max_retries):
            try:
                self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
                self._socket.settimeout(self.config.connect_timeout)
                self._socket.connect((self.config.host, self.config.port))

                # Connection successful
                self.state.status = ConnectionStatus.OK
                self.state.last_error = None
                self.state.update_contact()
                logger.info("Connected to Maya at %s:%d", self.config.host, self.config.port)
                return True

            except TimeoutError:
                last_error = f"Connection timed out after {self.config.connect_timeout}s"
                self._cleanup_socket()
            except ConnectionRefusedError:
                last_error = "Connection refused - is Maya running with commandPort open?"
                self._cleanup_socket()
            except OSError as e:
                last_error = f"Socket error: {e}"
                self._cleanup_socket()

            # Exponential backoff before retry
            if attempt < self.config.max_retries - 1:
                delay = self.config.retry_base_delay * (2**attempt)
                logger.debug(
                    "Connection attempt %d/%d failed: %s. Retrying in %.1fs",
                    attempt + 1,
                    self.config.max_retries,
                    last_error,
                    delay,
                )
                time.sleep(delay)

        # All retries exhausted
        self.state.status = ConnectionStatus.OFFLINE
        self.state.last_error = last_error
        logger.warning(
            "Failed to connect to Maya after %d attempts: %s",
            self.config.max_retries,
            last_error,
        )

        raise MayaUnavailableError(
            message=f"Cannot connect to Maya commandPort at {self.config.host}:{self.config.port}",
            host=self.config.host,
            port=self.config.port,
            attempts=self.config.max_retries,
            last_error=last_error,
        )

disconnect

disconnect() -> bool

Close the connection to Maya.

RETURNS DESCRIPTION
bool

True if disconnection was successful, False if wasn't connected.

Example

client.disconnect() True

Source code in src/maya_mcp/transport/commandport.py
def disconnect(self) -> bool:
    """Close the connection to Maya.

    Returns:
        True if disconnection was successful, False if wasn't connected.

    Example:
        >>> client.disconnect()
        True
    """
    with self._lock:
        was_connected = self._socket is not None
        self._cleanup_socket()
        self.state.status = ConnectionStatus.OFFLINE
        if was_connected:
            logger.info("Disconnected from Maya")
        return was_connected

execute

execute(command: str) -> str

Execute a Python command in Maya and return the result.

Sends a command to Maya via commandPort and waits for the response. Automatically connects if not already connected. If the connection drops during the send phase, reconnects and retries once.

PARAMETER DESCRIPTION
command

Python code to execute in Maya.

TYPE: str

RETURNS DESCRIPTION
str

Command output as string.

RAISES DESCRIPTION
MayaUnavailableError

Cannot connect to Maya.

MayaCommandError

Command execution failed.

MayaTimeoutError

Command timed out.

Example

result = client.execute("cmds.ls(selection=True)") print(result) ['pCube1', 'pSphere1']

Source code in src/maya_mcp/transport/commandport.py
def execute(self, command: str) -> str:
    """Execute a Python command in Maya and return the result.

    Sends a command to Maya via commandPort and waits for the response.
    Automatically connects if not already connected. If the connection
    drops during the send phase, reconnects and retries once.

    Args:
        command: Python code to execute in Maya.

    Returns:
        Command output as string.

    Raises:
        MayaUnavailableError: Cannot connect to Maya.
        MayaCommandError: Command execution failed.
        MayaTimeoutError: Command timed out.

    Example:
        >>> result = client.execute("cmds.ls(selection=True)")
        >>> print(result)
        ['pCube1', 'pSphere1']
    """
    with self._lock:
        return self._execute_with_retry(command, allow_retry=True)

is_connected

is_connected() -> bool

Check if currently connected to Maya.

RETURNS DESCRIPTION
bool

True if socket is connected.

Example

if client.is_connected(): ... print("Connected")

Source code in src/maya_mcp/transport/commandport.py
def is_connected(self) -> bool:
    """Check if currently connected to Maya.

    Returns:
        True if socket is connected.

    Example:
        >>> if client.is_connected():
        ...     print("Connected")
    """
    with self._lock:
        return self._socket is not None and self.state.status == ConnectionStatus.OK

get_status

get_status() -> ConnectionStatus

Get the current connection status.

RETURNS DESCRIPTION
ConnectionStatus

Current ConnectionStatus enum value.

Example

status = client.get_status() if status == ConnectionStatus.OK: ... print("Connected and healthy")

Source code in src/maya_mcp/transport/commandport.py
def get_status(self) -> ConnectionStatus:
    """Get the current connection status.

    Returns:
        Current ConnectionStatus enum value.

    Example:
        >>> status = client.get_status()
        >>> if status == ConnectionStatus.OK:
        ...     print("Connected and healthy")
    """
    with self._lock:
        return self.state.status

get_health

get_health() -> HealthCheckResult

Get detailed health information.

RETURNS DESCRIPTION
HealthCheckResult

HealthCheckResult with current connection health details.

Example

health = client.get_health() print(f"Status: {health.status}")

Source code in src/maya_mcp/transport/commandport.py
def get_health(self) -> HealthCheckResult:
    """Get detailed health information.

    Returns:
        HealthCheckResult with current connection health details.

    Example:
        >>> health = client.get_health()
        >>> print(f"Status: {health.status}")
    """
    with self._lock:
        return HealthCheckResult(
            status=self.state.status.value,
            last_error=self.state.last_error,
            last_contact=self.state.get_last_contact_iso(),
            host=self.config.host,
            port=self.config.port,
        )

reconfigure

reconfigure(host: str | None = None, port: int | None = None) -> None

Update connection configuration.

Disconnects if currently connected and updates the configuration.

PARAMETER DESCRIPTION
host

New target host (optional).

TYPE: str | None DEFAULT: None

port

New target port (optional).

TYPE: int | None DEFAULT: None

RAISES DESCRIPTION
ValueError

If new configuration is invalid.

Example

client.reconfigure(port=7002)

Source code in src/maya_mcp/transport/commandport.py
def reconfigure(
    self,
    host: str | None = None,
    port: int | None = None,
) -> None:
    """Update connection configuration.

    Disconnects if currently connected and updates the configuration.

    Args:
        host: New target host (optional).
        port: New target port (optional).

    Raises:
        ValueError: If new configuration is invalid.

    Example:
        >>> client.reconfigure(port=7002)
    """
    with self._lock:
        # Disconnect first
        self.disconnect()

        # Update config
        new_host = host if host is not None else self.config.host
        new_port = port if port is not None else self.config.port

        self.config = ConnectionConfig(
            host=new_host,
            port=new_port,
            connect_timeout=self.config.connect_timeout,
            command_timeout=self.config.command_timeout,
            max_retries=self.config.max_retries,
            retry_base_delay=self.config.retry_base_delay,
        )
        self.state.config = self.config

get_client

get_client() -> CommandPortClient

Get the global CommandPortClient instance.

Returns a singleton CommandPortClient instance, creating it if necessary. This is the recommended way to get a client for use in MCP tools.

RETURNS DESCRIPTION
CommandPortClient

The global CommandPortClient instance.

Example

client = get_client() client.execute("cmds.ls()")

Source code in src/maya_mcp/transport/commandport.py
def get_client() -> CommandPortClient:
    """Get the global CommandPortClient instance.

    Returns a singleton CommandPortClient instance, creating it if necessary.
    This is the recommended way to get a client for use in MCP tools.

    Returns:
        The global CommandPortClient instance.

    Example:
        >>> client = get_client()
        >>> client.execute("cmds.ls()")
    """
    global _client
    with _client_lock:
        if _client is None:
            _client = CommandPortClient()
        return _client

Tool Modules

Health

health

Health check tool for Maya MCP.

This module provides the health.check tool for monitoring the connection status between Maya MCP and Maya.

HealthCheckOutput

Bases: TypedDict

Return payload for the health.check tool.

health_check

health_check() -> HealthCheckOutput

Check the health status of the Maya connection.

Returns current connection status, last error (if any), last successful contact timestamp, and connection configuration.

RETURNS DESCRIPTION
HealthCheckOutput

Dictionary with health check results: - status: "ok" | "offline" | "reconnecting" - last_error: Last error message or None - last_contact: ISO8601 timestamp or None - host: Current target host - port: Current target port

Example

result = health_check() print(result["status"]) 'ok'

Source code in src/maya_mcp/tools/health.py
def health_check() -> HealthCheckOutput:
    """Check the health status of the Maya connection.

    Returns current connection status, last error (if any), last successful
    contact timestamp, and connection configuration.

    Returns:
        Dictionary with health check results:
            - status: "ok" | "offline" | "reconnecting"
            - last_error: Last error message or None
            - last_contact: ISO8601 timestamp or None
            - host: Current target host
            - port: Current target port

    Example:
        >>> result = health_check()
        >>> print(result["status"])
        'ok'
    """
    client = get_client()
    health: HealthCheckResult = client.get_health()

    return {
        "status": health.status,
        "last_error": health.last_error,
        "last_contact": health.last_contact,
        "host": health.host,
        "port": health.port,
    }

Connection

connection

Connection management tools for Maya MCP.

This module provides tools for manually controlling the connection to Maya's commandPort. These are optional debugging controls.

MayaConnectOutput

Bases: TypedDict

Return payload for the maya.connect tool.

MayaDisconnectOutput

Bases: TypedDict

Return payload for the maya.disconnect tool.

maya_connect

maya_connect(host: str = 'localhost', port: int = 7001, source_type: Literal['python', 'mel'] = 'python') -> MayaConnectOutput

Establish a connection to Maya's commandPort.

Attempts to connect to Maya at the specified host and port. This is primarily for debugging and explicit connection control.

PARAMETER DESCRIPTION
host

Target host. Only "localhost" or "127.0.0.1" are supported.

TYPE: str DEFAULT: 'localhost'

port

Target port number (1-65535).

TYPE: int DEFAULT: 7001

source_type

Command interpreter type. Currently only "python" is actually used; "mel" is accepted for compatibility.

TYPE: Literal['python', 'mel'] DEFAULT: 'python'

RETURNS DESCRIPTION
MayaConnectOutput

Dictionary with connection result: - connected: Whether connection succeeded - host: Target host - port: Target port - error: Error message if connection failed, else None

Example

result = maya_connect(port=7001) if result["connected"]: ... print("Connected!")

Source code in src/maya_mcp/tools/connection.py
def maya_connect(
    host: str = "localhost",
    port: int = 7001,
    source_type: Literal["python", "mel"] = "python",  # noqa: ARG001
) -> MayaConnectOutput:
    """Establish a connection to Maya's commandPort.

    Attempts to connect to Maya at the specified host and port.
    This is primarily for debugging and explicit connection control.

    Args:
        host: Target host. Only "localhost" or "127.0.0.1" are supported.
        port: Target port number (1-65535).
        source_type: Command interpreter type. Currently only "python" is
            actually used; "mel" is accepted for compatibility.

    Returns:
        Dictionary with connection result:
            - connected: Whether connection succeeded
            - host: Target host
            - port: Target port
            - error: Error message if connection failed, else None

    Example:
        >>> result = maya_connect(port=7001)
        >>> if result["connected"]:
        ...     print("Connected!")
    """
    client = get_client()

    # Reconfigure if host/port differ
    if client.config.host != host or client.config.port != port:
        client.reconfigure(host=host, port=port)

    try:
        client.connect()
        return {
            "connected": True,
            "host": host,
            "port": port,
            "error": None,
        }
    except MayaUnavailableError as e:
        return {
            "connected": False,
            "host": host,
            "port": port,
            "error": e.message,
        }

maya_disconnect

maya_disconnect() -> MayaDisconnectOutput

Close the connection to Maya.

Disconnects from Maya's commandPort and moves the client state to offline.

RETURNS DESCRIPTION
MayaDisconnectOutput

Dictionary with disconnect result: - disconnected: Whether disconnect was performed - was_connected: Whether was connected before

Example

result = maya_disconnect() print(f"Was connected: {result['was_connected']}")

Source code in src/maya_mcp/tools/connection.py
def maya_disconnect() -> MayaDisconnectOutput:
    """Close the connection to Maya.

    Disconnects from Maya's commandPort and moves the client state
    to offline.

    Returns:
        Dictionary with disconnect result:
            - disconnected: Whether disconnect was performed
            - was_connected: Whether was connected before

    Example:
        >>> result = maya_disconnect()
        >>> print(f"Was connected: {result['was_connected']}")
    """
    client = get_client()
    was_connected = client.is_connected()
    client.disconnect()

    return {
        "disconnected": True,
        "was_connected": was_connected,
    }

Scene

scene

Scene tools for Maya MCP.

This module provides tools for querying and manipulating Maya scenes.

SceneInfoOutput

Bases: TypedDict

Return payload for the scene.info tool.

SceneNewOutput

Bases: TypedDict

Return payload for the scene.new tool.

SceneOpenOutput

Bases: TypedDict

Return payload for the scene.open tool.

SceneUndoOutput

Bases: TypedDict

Return payload for the scene.undo tool.

SceneRedoOutput

Bases: TypedDict

Return payload for the scene.redo tool.

SceneSaveOutput

Bases: TypedDict

Return payload for the scene.save tool.

SceneSaveAsOutput

Bases: TypedDict

Return payload for the scene.save_as tool.

SceneImportOutput

Bases: _GuardedOutput

Return payload for the scene.import tool.

SceneExportOutput

Bases: TypedDict

Return payload for the scene.export tool.

scene_info

scene_info() -> SceneInfoOutput

Get information about the current Maya scene.

Returns information about the currently open scene including file path, modification status, frame rate, and frame range.

RETURNS DESCRIPTION
SceneInfoOutput

Dictionary with scene information: - file_path: Current scene file path, or None if untitled - modified: Whether scene has unsaved changes - fps: Frames per second - frame_range: [start_frame, end_frame] - up_axis: "y" or "z"

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

Example

info = scene_info() print(f"Scene: {info['file_path']}")

Source code in src/maya_mcp/tools/scene.py
def scene_info() -> SceneInfoOutput:
    """Get information about the current Maya scene.

    Returns information about the currently open scene including
    file path, modification status, frame rate, and frame range.

    Returns:
        Dictionary with scene information:
            - file_path: Current scene file path, or None if untitled
            - modified: Whether scene has unsaved changes
            - fps: Frames per second
            - frame_range: [start_frame, end_frame]
            - up_axis: "y" or "z"

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.

    Example:
        >>> info = scene_info()
        >>> print(f"Scene: {info['file_path']}")
    """
    client = get_client()

    # Build a compound command that returns all info in one call
    # This is more efficient than multiple round-trips
    command = """
import maya.cmds as cmds
import json

scene_name = cmds.file(query=True, sceneName=True)
modified = cmds.file(query=True, modified=True)
time_unit = cmds.currentUnit(query=True, time=True)
min_time = cmds.playbackOptions(query=True, minTime=True)
max_time = cmds.playbackOptions(query=True, maxTime=True)
up_axis = cmds.upAxis(query=True, axis=True)

result = {
    "scene_name": scene_name if scene_name else None,
    "modified": modified,
    "time_unit": time_unit,
    "min_time": min_time,
    "max_time": max_time,
    "up_axis": up_axis
}
print(json.dumps(result))
"""

    response = client.execute(command)

    # Parse the JSON response
    data = parse_json_response(response)

    # Convert time unit to FPS
    time_unit = data.get("time_unit", "film")
    fps = TIME_UNIT_TO_FPS.get(time_unit, 24.0)

    return {
        "file_path": data.get("scene_name"),
        "modified": data.get("modified", False),
        "fps": fps,
        "frame_range": [data.get("min_time", 1.0), data.get("max_time", 24.0)],
        "up_axis": data.get("up_axis", "y"),
    }

scene_undo

scene_undo() -> SceneUndoOutput

Undo the last operation in Maya.

Critical for LLM error recovery - allows reverting mistakes made during automated operations.

RETURNS DESCRIPTION
SceneUndoOutput

Dictionary with undo result: - success: Whether undo succeeded - undone: Description of the undone action, or None - can_undo: Whether more undo operations are available - can_redo: Whether redo is now available

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

Example

result = scene_undo() if result['success']: ... print(f"Undone: {result['undone']}")

Source code in src/maya_mcp/tools/scene.py
def scene_undo() -> SceneUndoOutput:
    """Undo the last operation in Maya.

    Critical for LLM error recovery - allows reverting mistakes made
    during automated operations.

    Returns:
        Dictionary with undo result:
            - success: Whether undo succeeded
            - undone: Description of the undone action, or None
            - can_undo: Whether more undo operations are available
            - can_redo: Whether redo is now available

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.

    Example:
        >>> result = scene_undo()
        >>> if result['success']:
        ...     print(f"Undone: {result['undone']}")
    """
    client = get_client()

    command = """
import maya.cmds as cmds
import json

result = {"success": False, "undone": None, "can_undo": False, "can_redo": False}

# Check if undo is available
can_undo = cmds.undoInfo(query=True, undoQueueEmpty=True) == False
result["can_undo"] = can_undo

if can_undo:
    # Get the name of the operation that will be undone
    undone_name = cmds.undoInfo(query=True, undoName=True)
    # Perform the undo
    cmds.undo()
    result["success"] = True
    result["undone"] = undone_name if undone_name else None

# Check states after undo
result["can_undo"] = cmds.undoInfo(query=True, undoQueueEmpty=True) == False
result["can_redo"] = cmds.undoInfo(query=True, redoQueueEmpty=True) == False

print(json.dumps(result))
"""

    response = client.execute(command)

    # Parse the JSON response
    data = parse_json_response(response)

    return {
        "success": data.get("success", False),
        "undone": data.get("undone"),
        "can_undo": data.get("can_undo", False),
        "can_redo": data.get("can_redo", False),
    }

scene_new

scene_new(force: bool = False) -> SceneNewOutput

Create a new, empty Maya scene.

Checks whether the current scene has unsaved changes before proceeding. When force is False (default) and the scene has been modified, the operation is rejected with an actionable error message instead of discarding work. When force is True, unsaved changes are discarded and a new scene is created unconditionally.

Important: This tool never triggers Maya's interactive "Save changes?" dialog, which would block the commandPort indefinitely. Instead it pre-checks the modification state and always passes force=True to the underlying cmds.file(new=True, force=True) call.

PARAMETER DESCRIPTION
force

If True, discard unsaved changes and create a new scene. If False (default), refuse when the scene has unsaved changes.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
SceneNewOutput

Dictionary with scene_new result: - success: Whether the new scene was created - previous_file: File path of the previous scene (or None) - was_modified: Whether the previous scene had unsaved changes - error: Error message if the operation was refused, or None

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

Example

result = scene_new() if not result['success']: ... print(result['error']) # "Scene has unsaved changes..." result = scene_new(force=True) assert result['success']

Source code in src/maya_mcp/tools/scene.py
def scene_new(force: bool = False) -> SceneNewOutput:
    """Create a new, empty Maya scene.

    Checks whether the current scene has unsaved changes before proceeding.
    When ``force`` is False (default) and the scene has been modified, the
    operation is rejected with an actionable error message instead of
    discarding work.  When ``force`` is True, unsaved changes are discarded
    and a new scene is created unconditionally.

    **Important:** This tool never triggers Maya's interactive "Save changes?"
    dialog, which would block the commandPort indefinitely.  Instead it
    pre-checks the modification state and always passes ``force=True`` to the
    underlying ``cmds.file(new=True, force=True)`` call.

    Args:
        force: If True, discard unsaved changes and create a new scene.
            If False (default), refuse when the scene has unsaved changes.

    Returns:
        Dictionary with scene_new result:
            - success: Whether the new scene was created
            - previous_file: File path of the previous scene (or None)
            - was_modified: Whether the previous scene had unsaved changes
            - error: Error message if the operation was refused, or None

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.

    Example:
        >>> result = scene_new()
        >>> if not result['success']:
        ...     print(result['error'])  # "Scene has unsaved changes..."
        >>> result = scene_new(force=True)
        >>> assert result['success']
    """
    client = get_client()

    force_py = str(force)
    command = f"""
import maya.cmds as cmds
import json

force = {force_py}
result = {{"success": False, "previous_file": None, "was_modified": False, "error": None}}

scene_name = cmds.file(query=True, sceneName=True)
result["previous_file"] = scene_name if scene_name else None
modified = cmds.file(query=True, modified=True)
result["was_modified"] = modified

if modified and not force:
    result["error"] = {json.dumps(SCENE_UNSAVED_CHANGES_ERROR)}
else:
    _ = cmds.file(new=True, force=True)
    result["success"] = True

print(json.dumps(result))
"""

    response = client.execute(command)

    data = parse_json_response(response)

    return {
        "success": data.get("success", False),
        "previous_file": data.get("previous_file"),
        "was_modified": data.get("was_modified", False),
        "error": data.get("error"),
    }

scene_open

scene_open(file_path: str, force: bool = False) -> SceneOpenOutput

Open a Maya scene file.

Loads the specified scene file into Maya. Checks whether the current scene has unsaved changes before proceeding. When force is False (default) and the scene has been modified, the operation is rejected with an actionable error message. When force is True, unsaved changes are discarded and the file is opened unconditionally.

Important: This tool never triggers Maya's interactive "Save changes?" dialog, which would block the commandPort indefinitely. Instead it pre-checks the modification state and always passes force=True to the underlying cmds.file(path, open=True, force=True) call.

The file path is validated before being sent to Maya:

  • Must not be empty
  • Must not contain shell metacharacters (``;|&$``` etc.)
  • Must end with a supported Maya file extension (.ma, .mb)
  • The file must exist on disk (verified inside Maya)
PARAMETER DESCRIPTION
file_path

Absolute or relative path to the Maya scene file to open. Supported extensions: .ma (Maya ASCII), .mb (Maya Binary).

TYPE: str

force

If True, discard unsaved changes and open the file. If False (default), refuse when the current scene has unsaved changes.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
SceneOpenOutput

Dictionary with scene_open result: - success: Whether the file was opened - file_path: The opened file path (as reported by Maya), or None - previous_file: File path of the previous scene, or None - was_modified: Whether the previous scene had unsaved changes - error: Error message if the operation was refused, or None

RAISES DESCRIPTION
ValidationError

If the file path is invalid or contains dangerous characters.

MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

Example

result = scene_open("/path/to/scene.ma") if not result['success']: ... print(result['error']) # "Scene has unsaved changes..." result = scene_open("/path/to/scene.ma", force=True) assert result['success']

Source code in src/maya_mcp/tools/scene.py
def scene_open(file_path: str, force: bool = False) -> SceneOpenOutput:
    """Open a Maya scene file.

    Loads the specified scene file into Maya.  Checks whether the current scene
    has unsaved changes before proceeding.  When ``force`` is False (default)
    and the scene has been modified, the operation is rejected with an
    actionable error message.  When ``force`` is True, unsaved changes are
    discarded and the file is opened unconditionally.

    **Important:** This tool never triggers Maya's interactive "Save changes?"
    dialog, which would block the commandPort indefinitely.  Instead it
    pre-checks the modification state and always passes ``force=True`` to the
    underlying ``cmds.file(path, open=True, force=True)`` call.

    The file path is validated before being sent to Maya:

    - Must not be empty
    - Must not contain shell metacharacters (``;|&$``` etc.)
    - Must end with a supported Maya file extension (``.ma``, ``.mb``)
    - The file must exist on disk (verified inside Maya)

    Args:
        file_path: Absolute or relative path to the Maya scene file to open.
            Supported extensions: ``.ma`` (Maya ASCII), ``.mb`` (Maya Binary).
        force: If True, discard unsaved changes and open the file.
            If False (default), refuse when the current scene has unsaved
            changes.

    Returns:
        Dictionary with scene_open result:
            - success: Whether the file was opened
            - file_path: The opened file path (as reported by Maya), or None
            - previous_file: File path of the previous scene, or None
            - was_modified: Whether the previous scene had unsaved changes
            - error: Error message if the operation was refused, or None

    Raises:
        ValidationError: If the file path is invalid or contains dangerous
            characters.
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.

    Example:
        >>> result = scene_open("/path/to/scene.ma")
        >>> if not result['success']:
        ...     print(result['error'])  # "Scene has unsaved changes..."
        >>> result = scene_open("/path/to/scene.ma", force=True)
        >>> assert result['success']
    """
    normalized_file_path = _validate_file_path(file_path, ALLOWED_SCENE_EXTENSIONS)

    client = get_client()

    # Normalize slashes for Maya and embed safely via JSON string literal
    safe_path = normalized_file_path.replace("\\", "/")
    path_literal = json.dumps(safe_path)

    force_py = str(force)
    command = f"""
import maya.cmds as cmds
import json
import os

force = {force_py}
file_path = {path_literal}

result = {{"success": False, "file_path": None, "previous_file": None, "was_modified": False, "error": None}}

# Capture previous scene info
scene_name = cmds.file(query=True, sceneName=True)
result["previous_file"] = scene_name if scene_name else None
modified = cmds.file(query=True, modified=True)
result["was_modified"] = modified

# Check unsaved changes
if modified and not force:
    result["error"] = {json.dumps(SCENE_UNSAVED_CHANGES_ERROR)}
elif not os.path.isfile(file_path):
    result["error"] = "File not found: " + file_path
else:
    _ = cmds.file(file_path, open=True, force=True)
    result["success"] = True
    result["file_path"] = cmds.file(query=True, sceneName=True) or None

print(json.dumps(result))
"""

    response = client.execute(command)

    data = parse_json_response(response)

    return {
        "success": data.get("success", False),
        "file_path": data.get("file_path"),
        "previous_file": data.get("previous_file"),
        "was_modified": data.get("was_modified", False),
        "error": data.get("error"),
    }

scene_redo

scene_redo() -> SceneRedoOutput

Redo the last undone operation in Maya.

Allows re-applying an operation that was previously undone.

RETURNS DESCRIPTION
SceneRedoOutput

Dictionary with redo result: - success: Whether redo succeeded - redone: Description of the redone action, or None - can_undo: Whether undo is now available - can_redo: Whether more redo operations are available

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

Example

result = scene_redo() if result['success']: ... print(f"Redone: {result['redone']}")

Source code in src/maya_mcp/tools/scene.py
def scene_redo() -> SceneRedoOutput:
    """Redo the last undone operation in Maya.

    Allows re-applying an operation that was previously undone.

    Returns:
        Dictionary with redo result:
            - success: Whether redo succeeded
            - redone: Description of the redone action, or None
            - can_undo: Whether undo is now available
            - can_redo: Whether more redo operations are available

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.

    Example:
        >>> result = scene_redo()
        >>> if result['success']:
        ...     print(f"Redone: {result['redone']}")
    """
    client = get_client()

    command = """
import maya.cmds as cmds
import json

result = {"success": False, "redone": None, "can_undo": False, "can_redo": False}

# Check if redo is available
can_redo = cmds.undoInfo(query=True, redoQueueEmpty=True) == False
result["can_redo"] = can_redo

if can_redo:
    # Get the name of the operation that will be redone
    redone_name = cmds.undoInfo(query=True, redoName=True)
    # Perform the redo
    cmds.redo()
    result["success"] = True
    result["redone"] = redone_name if redone_name else None

# Check states after redo
result["can_undo"] = cmds.undoInfo(query=True, undoQueueEmpty=True) == False
result["can_redo"] = cmds.undoInfo(query=True, redoQueueEmpty=True) == False

print(json.dumps(result))
"""

    response = client.execute(command)

    # Parse the JSON response
    data = parse_json_response(response)

    return {
        "success": data.get("success", False),
        "redone": data.get("redone"),
        "can_undo": data.get("can_undo", False),
        "can_redo": data.get("can_redo", False),
    }

scene_save

scene_save() -> SceneSaveOutput

Save the current Maya scene.

Saves the currently open scene file. If the scene is untitled (never saved), the operation is rejected with an error instructing to use scene.save_as.

RETURNS DESCRIPTION
SceneSaveOutput

Dictionary with save result: - success: Whether the scene was saved - file_path: The saved file path (or None if failed) - error: Error message if the operation failed, or None

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

Example

result = scene_save() if result['success']: ... print(f"Saved: {result['file_path']}")

Source code in src/maya_mcp/tools/scene.py
def scene_save() -> SceneSaveOutput:
    """Save the current Maya scene.

    Saves the currently open scene file. If the scene is untitled (never saved),
    the operation is rejected with an error instructing to use ``scene.save_as``.

    Returns:
        Dictionary with save result:
            - success: Whether the scene was saved
            - file_path: The saved file path (or None if failed)
            - error: Error message if the operation failed, or None

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.

    Example:
        >>> result = scene_save()
        >>> if result['success']:
        ...     print(f"Saved: {result['file_path']}")
    """
    client = get_client()

    command = """
import maya.cmds as cmds
import json

result = {"success": False, "file_path": None, "error": None}

scene_name = cmds.file(query=True, sceneName=True)

if not scene_name:
    result["error"] = "Scene is untitled. Use scene.save_as to save for the first time."
else:
    try:
        # Save the file
        new_name = cmds.file(save=True)
        result["success"] = True
        result["file_path"] = new_name
    except Exception as e:
        result["error"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)

    data = parse_json_response(response)

    return {
        "success": data.get("success", False),
        "file_path": data.get("file_path"),
        "error": data.get("error"),
    }

scene_save_as

scene_save_as(file_path: str) -> SceneSaveAsOutput

Save the current scene to a new file path.

Renames the current scene and saves it to the specified path. If the file extension is .ma, it saves as Maya ASCII. If the file extension is .mb, it saves as Maya Binary.

The file path is validated before being sent to Maya: - Must not be empty - Must not contain shell metacharacters - Must end with a supported Maya file extension (.ma, .mb)

PARAMETER DESCRIPTION
file_path

Absolute or relative path to save the scene to.

TYPE: str

RETURNS DESCRIPTION
SceneSaveAsOutput

Dictionary with save result: - success: Whether the scene was saved - file_path: The new file path (or None if failed) - error: Error message if the operation failed, or None

RAISES DESCRIPTION
ValidationError

If the file path is invalid.

MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

Example

result = scene_save_as("/path/to/new_scene.ma") if result['success']: ... print(f"Saved as: {result['file_path']}")

Source code in src/maya_mcp/tools/scene.py
def scene_save_as(file_path: str) -> SceneSaveAsOutput:
    """Save the current scene to a new file path.

    Renames the current scene and saves it to the specified path.
    If the file extension is ``.ma``, it saves as Maya ASCII.
    If the file extension is ``.mb``, it saves as Maya Binary.

    The file path is validated before being sent to Maya:
    - Must not be empty
    - Must not contain shell metacharacters
    - Must end with a supported Maya file extension (``.ma``, ``.mb``)

    Args:
        file_path: Absolute or relative path to save the scene to.

    Returns:
        Dictionary with save result:
            - success: Whether the scene was saved
            - file_path: The new file path (or None if failed)
            - error: Error message if the operation failed, or None

    Raises:
        ValidationError: If the file path is invalid.
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.

    Example:
        >>> result = scene_save_as("/path/to/new_scene.ma")
        >>> if result['success']:
        ...     print(f"Saved as: {result['file_path']}")
    """
    normalized_file_path = _validate_file_path(file_path, ALLOWED_SCENE_EXTENSIONS)

    client = get_client()

    # Normalize slashes and embed safely
    safe_path = normalized_file_path.replace("\\", "/")
    path_literal = json.dumps(safe_path)

    command = f"""
import maya.cmds as cmds
import json

file_path = {path_literal}
result = {{"success": False, "file_path": None, "error": None}}

# Determine file type based on extension
file_type = "mayaAscii" if file_path.lower().endswith(".ma") else "mayaBinary"

try:
    # Rename and save
    cmds.file(rename=file_path)
    new_name = cmds.file(save=True, type=file_type)
    result["success"] = True
    result["file_path"] = new_name
except Exception as e:
    result["error"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)

    data = parse_json_response(response)

    return {
        "success": data.get("success", False),
        "file_path": data.get("file_path"),
        "error": data.get("error"),
    }

scene_import

scene_import(file_path: str, namespace: str | None = None, force: bool = False) -> SceneImportOutput

Import a file into the current Maya scene.

Imports geometry, animation, or other scene data from an external file. Supports multiple formats including Maya scenes, OBJ, FBX, Alembic, and USD.

The file path is validated before being sent to Maya: - Must not be empty - Must not contain shell metacharacters (``;|&$``` etc.) - Must end with a supported extension - The file must exist on disk (verified inside Maya)

Token Protection: Returns only top-level parent transforms of imported nodes, not all descendants. This prevents token budget explosion when importing complex assets.

PARAMETER DESCRIPTION
file_path

Absolute or relative path to the file to import. Supported extensions: .ma, .mb, .obj, .fbx, .abc, .usd, .usda, .usdc, .usdz.

TYPE: str

namespace

Namespace behavior: - None (default): Import without namespace - "" (empty string): Auto-generate namespace from filename - "myNs": Use specified namespace

TYPE: str | None DEFAULT: None

force

If True, replace existing namespace contents. If False (default), merge with existing namespace.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
SceneImportOutput

Dictionary with import result: - success: Whether the file was imported - file_path: The imported file path - nodes: List of top-level parent transforms created by import - count: Number of top-level nodes returned - error: Error message if the operation failed, or None

RAISES DESCRIPTION
ValidationError

If the file path is invalid or contains dangerous characters.

MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

Example

result = scene_import("/assets/character.fbx", namespace="char") if result['success']: ... print(f"Imported {result['count']} top-level nodes")

Source code in src/maya_mcp/tools/scene.py
def scene_import(
    file_path: str,
    namespace: str | None = None,
    force: bool = False,
) -> SceneImportOutput:
    """Import a file into the current Maya scene.

    Imports geometry, animation, or other scene data from an external file.
    Supports multiple formats including Maya scenes, OBJ, FBX, Alembic, and USD.

    The file path is validated before being sent to Maya:
    - Must not be empty
    - Must not contain shell metacharacters (``;|&$``` etc.)
    - Must end with a supported extension
    - The file must exist on disk (verified inside Maya)

    **Token Protection**: Returns only top-level parent transforms of imported
    nodes, not all descendants. This prevents token budget explosion when
    importing complex assets.

    Args:
        file_path: Absolute or relative path to the file to import.
            Supported extensions: ``.ma``, ``.mb``, ``.obj``, ``.fbx``, ``.abc``,
            ``.usd``, ``.usda``, ``.usdc``, ``.usdz``.
        namespace: Namespace behavior:
            - ``None`` (default): Import without namespace
            - ``""`` (empty string): Auto-generate namespace from filename
            - ``"myNs"``: Use specified namespace
        force: If True, replace existing namespace contents. If False (default),
            merge with existing namespace.

    Returns:
        Dictionary with import result:
            - success: Whether the file was imported
            - file_path: The imported file path
            - nodes: List of top-level parent transforms created by import
            - count: Number of top-level nodes returned
            - error: Error message if the operation failed, or None

    Raises:
        ValidationError: If the file path is invalid or contains dangerous
            characters.
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.

    Example:
        >>> result = scene_import("/assets/character.fbx", namespace="char")
        >>> if result['success']:
        ...     print(f"Imported {result['count']} top-level nodes")
    """
    normalized_file_path = _validate_file_path(file_path, ALLOWED_IMPORT_EXTENSIONS)

    client = get_client()

    safe_path = normalized_file_path.replace("\\", "/")
    path_literal = json.dumps(safe_path)

    namespace_literal = "None" if namespace is None else json.dumps(namespace)
    force_py = str(force)

    command = f"""
import maya.cmds as cmds
import json
import os

file_path = {path_literal}
namespace = {namespace_literal}
force = {force_py}

result = {{"success": False, "file_path": None, "nodes": [], "count": 0, "error": None}}

if not os.path.isfile(file_path):
    result["error"] = "File not found: " + file_path
    print(json.dumps(result))
else:
    try:
        before_nodes = set(cmds.ls(long=True, transforms=True) or [])

        import_kwargs = {{"i": True, "returnNewNodes": True}}

        if namespace is None:
            import_kwargs["namespace"] = ":"
        elif namespace == "":
            pass
        else:
            import_kwargs["namespace"] = namespace
            if force:
                import_kwargs["ra"] = True

        new_nodes = cmds.file(file_path, **import_kwargs) or []

        after_nodes = set(cmds.ls(long=True, transforms=True) or [])
        created_transforms = after_nodes - before_nodes

        top_level = []
        for node in created_transforms:
            parent = cmds.listRelatives(node, parent=True, fullPath=True)
            if not parent or parent[0] not in created_transforms:
                short_name = node.split("|")[-1]
                top_level.append(short_name)

        result["success"] = True
        result["file_path"] = file_path
        result["nodes"] = top_level
        result["count"] = len(top_level)

    except Exception as e:
        result["error"] = str(e)

    print(json.dumps(result))
"""

    response = client.execute(command)
    data = parse_json_response(response)

    result = {
        "success": data.get("success", False),
        "file_path": data.get("file_path"),
        "nodes": data.get("nodes", []),
        "count": data.get("count", 0),
        "error": data.get("error"),
    }

    result = guard_response_size(result, list_key="nodes")

    return cast("SceneImportOutput", result)

scene_export

scene_export(file_path: str, export_mode: str = 'selected', animation: bool = False) -> SceneExportOutput

Export scene content to a file.

Exports the current selection or the entire scene to an external file format. Supports multiple formats including Maya scenes, OBJ, FBX, Alembic, and USD.

The file path is validated before being sent to Maya: - Must not be empty - Must not contain shell metacharacters (``;|&$``` etc.) - Must end with a supported extension

PARAMETER DESCRIPTION
file_path

Absolute or relative path for the exported file. Supported extensions: .ma, .mb, .obj, .fbx, .abc, .usd, .usda, .usdc.

TYPE: str

export_mode

What to export: - "selected" (default): Export currently selected nodes - "all": Export entire scene

TYPE: str DEFAULT: 'selected'

animation

If True, include animation data in export (FBX only). If False (default), export static geometry only.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
SceneExportOutput

Dictionary with export result: - success: Whether the file was exported - file_path: The exported file path - nodes_exported: Number of nodes exported (approximate) - error: Error message if the operation failed, or None

RAISES DESCRIPTION
ValidationError

If the file path is invalid or contains dangerous characters, or if export_mode is invalid.

MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

Example

result = scene_export("/output/asset.fbx", animation=True) if result['success']: ... print(f"Exported to: {result['file_path']}")

Source code in src/maya_mcp/tools/scene.py
def scene_export(
    file_path: str,
    export_mode: str = "selected",
    animation: bool = False,
) -> SceneExportOutput:
    """Export scene content to a file.

    Exports the current selection or the entire scene to an external file format.
    Supports multiple formats including Maya scenes, OBJ, FBX, Alembic, and USD.

    The file path is validated before being sent to Maya:
    - Must not be empty
    - Must not contain shell metacharacters (``;|&$``` etc.)
    - Must end with a supported extension

    Args:
        file_path: Absolute or relative path for the exported file.
            Supported extensions: ``.ma``, ``.mb``, ``.obj``, ``.fbx``, ``.abc``,
            ``.usd``, ``.usda``, ``.usdc``.
        export_mode: What to export:
            - ``"selected"`` (default): Export currently selected nodes
            - ``"all"``: Export entire scene
        animation: If True, include animation data in export (FBX only).
            If False (default), export static geometry only.

    Returns:
        Dictionary with export result:
            - success: Whether the file was exported
            - file_path: The exported file path
            - nodes_exported: Number of nodes exported (approximate)
            - error: Error message if the operation failed, or None

    Raises:
        ValidationError: If the file path is invalid or contains dangerous
            characters, or if export_mode is invalid.
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.

    Example:
        >>> result = scene_export("/output/asset.fbx", animation=True)
        >>> if result['success']:
        ...     print(f"Exported to: {result['file_path']}")
    """
    if export_mode not in ("selected", "all"):
        raise ValidationError(
            message=f"Invalid export_mode: {export_mode!r}. Must be 'selected' or 'all'.",
            field_name="export_mode",
            value=export_mode,
            constraint="'selected' or 'all'",
        )

    normalized_file_path = _validate_file_path(file_path, ALLOWED_EXPORT_EXTENSIONS)

    client = get_client()

    safe_path = normalized_file_path.replace("\\", "/")
    path_literal = json.dumps(safe_path)

    animation_py = str(animation)

    command = f"""
import maya.cmds as cmds
import json
import os

file_path = {path_literal}
export_mode = {json.dumps(export_mode)}
animation = {animation_py}

result = {{"success": False, "file_path": None, "nodes_exported": 0, "error": None}}

try:
    ext = os.path.splitext(file_path)[1].lower()

    export_all = (export_mode == "all")
    export_selected = (export_mode == "selected")

    if export_selected:
        selection = cmds.ls(selection=True, long=True) or []
        if not selection:
            result["error"] = "Nothing selected. Select nodes to export, or use export_mode='all'."
        else:
            result["nodes_exported"] = len(selection)

    if result["error"] is None:
        export_kwargs = {{}}

        if ext == ".ma":
            export_kwargs["type"] = "mayaAscii"
        elif ext == ".mb":
            export_kwargs["type"] = "mayaBinary"
        elif ext == ".obj":
            export_kwargs["type"] = "OBJexport"
        elif ext == ".fbx":
            export_kwargs["type"] = "FBX export"
            if animation:
                cmds.bakeResults(simulation=True)
        elif ext == ".abc":
            export_kwargs["type"] = "Alembic"
        elif ext in (".usd", ".usda", ".usdc"):
            export_kwargs["type"] = "USD Export"

        if export_all:
            exported = cmds.file(file_path, exportAll=True, force=True, **export_kwargs)
        else:
            exported = cmds.file(file_path, exportSelected=True, force=True, **export_kwargs)

        result["success"] = True
        result["file_path"] = file_path

except Exception as e:
    result["error"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    data = parse_json_response(response)

    return {
        "success": data.get("success", False),
        "file_path": data.get("file_path"),
        "nodes_exported": data.get("nodes_exported", 0),
        "error": data.get("error"),
    }

Nodes

nodes

Node tools for Maya MCP.

This module provides tools for listing, creating, deleting, and querying comprehensive information about Maya nodes.

NodesListOutput

Bases: _GuardedOutput

Return payload for the nodes.list tool.

NodesCreateOutput

Bases: TypedDict

Return payload for the nodes.create tool.

NodesInfoOutput

Bases: TypedDict

Return payload for the nodes.info tool.

Category-specific fields are present only for the requested info category.

NodesDeleteOutput

Bases: TypedDict

Return payload for the nodes.delete tool.

NodesRenameOutput

Bases: TypedDict

Return payload for the nodes.rename tool.

NodesParentOutput

Bases: TypedDict

Return payload for the nodes.parent tool.

NodesDuplicateOutput

Bases: TypedDict

Return payload for the nodes.duplicate tool.

nodes_list

nodes_list(node_type: str | None = None, pattern: str = '*', long_names: bool = False, limit: int | None = DEFAULT_NODE_LIMIT) -> NodesListOutput

List nodes in the Maya scene.

Returns a list of nodes, optionally filtered by type and/or name pattern.

PARAMETER DESCRIPTION
node_type

Filter by node type (e.g., "transform", "mesh", "camera"). If None, returns all nodes.

TYPE: str | None DEFAULT: None

pattern

Name pattern filter. Supports wildcards ( and ?). Default is "" (all names).

TYPE: str DEFAULT: '*'

long_names

If True, return full DAG paths. If False, return short names.

TYPE: bool DEFAULT: False

limit

Maximum number of nodes to return. Default is 500. Set to None or 0 for unlimited (use with caution in large scenes). When truncated, response includes 'truncated' and 'total_count'.

TYPE: int | None DEFAULT: DEFAULT_NODE_LIMIT

RETURNS DESCRIPTION
NodesListOutput

Dictionary with node list: - nodes: List of node names - count: Number of nodes returned - truncated: True if results were truncated (only if limit hit) - total_count: Total nodes matching before limit (only if truncated)

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

ValueError

If pattern contains invalid characters.

Example

result = nodes_list(node_type="mesh") print(f"Found {result['count']} meshes") for node in result['nodes']: ... print(node)

Source code in src/maya_mcp/tools/nodes.py
def nodes_list(
    node_type: str | None = None,
    pattern: str = "*",
    long_names: bool = False,
    limit: int | None = DEFAULT_NODE_LIMIT,
) -> NodesListOutput:
    """List nodes in the Maya scene.

    Returns a list of nodes, optionally filtered by type and/or name pattern.

    Args:
        node_type: Filter by node type (e.g., "transform", "mesh", "camera").
            If None, returns all nodes.
        pattern: Name pattern filter. Supports wildcards (* and ?).
            Default is "*" (all names).
        long_names: If True, return full DAG paths. If False, return
            short names.
        limit: Maximum number of nodes to return. Default is 500.
            Set to None or 0 for unlimited (use with caution in large scenes).
            When truncated, response includes 'truncated' and 'total_count'.

    Returns:
        Dictionary with node list:
            - nodes: List of node names
            - count: Number of nodes returned
            - truncated: True if results were truncated (only if limit hit)
            - total_count: Total nodes matching before limit (only if truncated)

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.
        ValueError: If pattern contains invalid characters.

    Example:
        >>> result = nodes_list(node_type="mesh")
        >>> print(f"Found {result['count']} meshes")
        >>> for node in result['nodes']:
        ...     print(node)
    """
    # Input validation
    _validate_pattern(pattern)
    if node_type is not None:
        _validate_pattern(node_type)

    client = get_client()

    # Build the Maya command
    # We use json.dumps to safely escape the pattern string
    pattern_escaped = json.dumps(pattern)
    long_flag = "True" if long_names else "False"

    if node_type is not None:
        type_escaped = json.dumps(node_type)
        command = f"""
import maya.cmds as cmds
import json

nodes = cmds.ls({pattern_escaped}, type={type_escaped}, long={long_flag}) or []
print(json.dumps(nodes))
"""
    else:
        command = f"""
import maya.cmds as cmds
import json

nodes = cmds.ls({pattern_escaped}, long={long_flag}) or []
print(json.dumps(nodes))
"""

    response = client.execute(command)

    # Parse the JSON response
    nodes = parse_json_response(response)

    if not isinstance(nodes, list):
        nodes = []

    # Apply limit to prevent token budget explosion
    total_count = len(nodes)
    truncated = False
    if limit and limit > 0 and total_count > limit:
        nodes = nodes[:limit]
        truncated = True

    result: dict[str, Any] = {
        "nodes": nodes,
        "count": len(nodes),
    }

    if truncated:
        result["truncated"] = True
        result["total_count"] = total_count

    # Apply response size guard to prevent token budget explosion
    return cast("NodesListOutput", guard_response_size(result, list_key="nodes"))

nodes_create

nodes_create(node_type: str, name: str | None = None, parent: str | None = None, attributes: dict[str, Any] | None = None) -> NodesCreateOutput

Create a new node in Maya.

Creates a node of the specified type with optional name, parent, and initial attribute values.

PARAMETER DESCRIPTION
node_type

Type of node to create (e.g., "transform", "locator", "joint").

TYPE: str

name

Desired node name. Maya may modify for uniqueness.

TYPE: str | None DEFAULT: None

parent

Parent node to parent under.

TYPE: str | None DEFAULT: None

attributes

Initial attribute values to set after creation.

TYPE: dict[str, Any] | None DEFAULT: None

RETURNS DESCRIPTION
NodesCreateOutput

Dictionary with creation result: - node: Name of the created node - node_type: Type of node created - parent: Parent node (if parented), or None - attributes_set: List of attributes successfully set - attribute_errors: Map of attribute to error message, or None

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

ValueError

If node_type, name, or parent contains invalid characters.

Example

result = nodes_create("transform", name="myGroup") print(f"Created: {result['node']}")

Source code in src/maya_mcp/tools/nodes.py
def nodes_create(
    node_type: str,
    name: str | None = None,
    parent: str | None = None,
    attributes: dict[str, Any] | None = None,
) -> NodesCreateOutput:
    """Create a new node in Maya.

    Creates a node of the specified type with optional name, parent, and
    initial attribute values.

    Args:
        node_type: Type of node to create (e.g., "transform", "locator", "joint").
        name: Desired node name. Maya may modify for uniqueness.
        parent: Parent node to parent under.
        attributes: Initial attribute values to set after creation.

    Returns:
        Dictionary with creation result:
            - node: Name of the created node
            - node_type: Type of node created
            - parent: Parent node (if parented), or None
            - attributes_set: List of attributes successfully set
            - attribute_errors: Map of attribute to error message, or None

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.
        ValueError: If node_type, name, or parent contains invalid characters.

    Example:
        >>> result = nodes_create("transform", name="myGroup")
        >>> print(f"Created: {result['node']}")
    """
    # Input validation
    _validate_pattern(node_type)
    if name is not None:
        _validate_node_name(name)
    if parent is not None:
        _validate_node_name(parent)

    client = get_client()

    # Build the Maya command
    type_escaped = json.dumps(node_type)
    name_escaped = json.dumps(name) if name else "None"
    parent_escaped = json.dumps(parent) if parent else "None"
    attrs_json = json.dumps(attributes or {})
    attrs_escaped = json.dumps(attrs_json)

    command = f"""
import maya.cmds as cmds
import json

node_type = {type_escaped}
desired_name = {name_escaped}
parent_node = {parent_escaped}
attrs = json.loads({attrs_escaped})

result = {{"node": None, "node_type": node_type, "parent": None, "attributes_set": [], "attribute_errors": {{}}}}

# Mapping of primitive types to their creation functions
# These return [transform, shape/history] instead of just the node
PRIMITIVE_CREATORS = {{
    "polyCube": lambda n: cmds.polyCube(name=n)[0] if n else cmds.polyCube()[0],
    "polySphere": lambda n: cmds.polySphere(name=n)[0] if n else cmds.polySphere()[0],
    "polyCylinder": lambda n: cmds.polyCylinder(name=n)[0] if n else cmds.polyCylinder()[0],
    "polyCone": lambda n: cmds.polyCone(name=n)[0] if n else cmds.polyCone()[0],
    "polyPlane": lambda n: cmds.polyPlane(name=n)[0] if n else cmds.polyPlane()[0],
    "polyTorus": lambda n: cmds.polyTorus(name=n)[0] if n else cmds.polyTorus()[0],
    "nurbsCircle": lambda n: cmds.circle(name=n)[0] if n else cmds.circle()[0],
    "nurbsCurve": lambda n: cmds.curve(d=1, p=[(0,0,0), (1,0,0)], name=n) if n else cmds.curve(d=1, p=[(0,0,0), (1,0,0)]),
    "locator": lambda n: cmds.spaceLocator(name=n)[0] if n else cmds.spaceLocator()[0],
    "camera": lambda n: cmds.camera(name=n)[0] if n else cmds.camera()[0],
}}

try:
    # Create the node using appropriate method
    if node_type in PRIMITIVE_CREATORS:
        created = PRIMITIVE_CREATORS[node_type](desired_name)
    elif desired_name:
        created = cmds.createNode(node_type, name=desired_name)
    else:
        created = cmds.createNode(node_type)
    result["node"] = created
    # Parent if requested
    if parent_node:
        if cmds.objExists(parent_node):
            cmds.parent(created, parent_node)
            result["parent"] = parent_node
        else:
            result["attribute_errors"]["_parent"] = f"Parent node '{{parent_node}}' does not exist"
    # Set initial attributes
    for attr, value in attrs.items():
        try:
            full_attr = f"{{created}}.{{attr}}"
            if not cmds.attributeQuery(attr, node=created, exists=True):
                result["attribute_errors"][attr] = f"Attribute '{{attr}}' not found"
            elif cmds.getAttr(full_attr, lock=True):
                result["attribute_errors"][attr] = f"Attribute '{{attr}}' is locked"
            else:
                if isinstance(value, (list, tuple)) and len(value) == 3:
                    cmds.setAttr(full_attr, value[0], value[1], value[2], type="double3")
                elif isinstance(value, str):
                    cmds.setAttr(full_attr, value, type="string")
                else:
                    cmds.setAttr(full_attr, value)
                result["attributes_set"].append(attr)
        except Exception as e:
            result["attribute_errors"][attr] = str(e)

except Exception as e:
    result["attribute_errors"]["_create"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)

    # Parse the JSON response
    parsed = parse_json_response(response)

    # Check for creation error
    if "_create" in parsed.get("attribute_errors", {}):
        raise ValueError(parsed["attribute_errors"]["_create"])

    result: dict[str, Any] = {
        "node": parsed.get("node"),
        "node_type": parsed.get("node_type"),
        "parent": parsed.get("parent"),
        "attributes_set": parsed.get("attributes_set", []),
    }

    errors = parsed.get("attribute_errors", {})
    if errors:
        result["attribute_errors"] = errors
    else:
        result["attribute_errors"] = None

    return cast("NodesCreateOutput", result)

nodes_info

nodes_info(node: str, info_category: str = 'summary') -> NodesInfoOutput

Get comprehensive information about a Maya node in a single call.

Consolidates what would otherwise require multiple attributes.get and nodes.list calls into one tool invocation, reducing LLM tool-call chaining.

PARAMETER DESCRIPTION
node

Name of the node to query.

TYPE: str

info_category

Category of information to retrieve: - "summary" (default): node type, parent, children count, exists - "transform": translate, rotate, scale, visibility - "hierarchy": parent (full path), children list, full path - "attributes": all keyable attributes with current values - "shape": shape node(s) under transform, shape type, connections - "all": everything combined (parent = short name from summary, parent_full_path = full DAG path from hierarchy)

TYPE: str DEFAULT: 'summary'

RETURNS DESCRIPTION
NodesInfoOutput

Dictionary with node information. Contents depend on info_category: - node: The queried node name - info_category: The category requested - exists: Whether the node exists - (category-specific fields) - errors: Error details if any queries failed, or None

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

ValueError

If node name or info_category is invalid.

Example

result = nodes_info("pCube1", info_category="transform") print(f"Position: {result['translate']}")

Source code in src/maya_mcp/tools/nodes.py
def nodes_info(
    node: str,
    info_category: str = "summary",
) -> NodesInfoOutput:
    """Get comprehensive information about a Maya node in a single call.

    Consolidates what would otherwise require multiple attributes.get and
    nodes.list calls into one tool invocation, reducing LLM tool-call chaining.

    Args:
        node: Name of the node to query.
        info_category: Category of information to retrieve:
            - "summary" (default): node type, parent, children count, exists
            - "transform": translate, rotate, scale, visibility
            - "hierarchy": parent (full path), children list, full path
            - "attributes": all keyable attributes with current values
            - "shape": shape node(s) under transform, shape type, connections
            - "all": everything combined (parent = short name from summary,
              parent_full_path = full DAG path from hierarchy)

    Returns:
        Dictionary with node information. Contents depend on info_category:
            - node: The queried node name
            - info_category: The category requested
            - exists: Whether the node exists
            - (category-specific fields)
            - errors: Error details if any queries failed, or None

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.
        ValueError: If node name or info_category is invalid.

    Example:
        >>> result = nodes_info("pCube1", info_category="transform")
        >>> print(f"Position: {result['translate']}")
    """
    # Input validation
    _validate_node_name(node)
    if info_category not in _VALID_INFO_CATEGORIES:
        raise ValueError(
            f"Invalid info_category: {info_category!r}. "
            f"Must be one of: {', '.join(sorted(_VALID_INFO_CATEGORIES))}"
        )

    client = get_client()

    node_escaped = json.dumps(node)
    category_escaped = json.dumps(info_category)

    command = _build_info_command(node_escaped, category_escaped)
    response = client.execute(command)

    # Parse the JSON response
    parsed: dict[str, Any] = parse_json_response(response)

    # Clean up errors field
    errors = parsed.get("errors", {})
    if not errors:
        parsed["errors"] = None

    # Apply response size guard for categories that may produce large output.
    # The guard truncates list fields; for keyable_attributes (a dict), truncation
    # is handled in Maya code via _MAX_KEYABLE_ATTRIBUTES. The guard here catches
    # any remaining oversized list fields (e.g. children, shapes).
    if info_category in ("attributes", "all", "hierarchy", "shape"):
        for key in ("children", "shapes"):
            if key in parsed:
                parsed = guard_response_size(parsed, list_key=key)

    return cast("NodesInfoOutput", parsed)

nodes_delete

nodes_delete(nodes: list[str], hierarchy: bool = False) -> NodesDeleteOutput

Delete one or more nodes from the Maya scene.

PARAMETER DESCRIPTION
nodes

List of node names to delete.

TYPE: list[str]

hierarchy

If True, delete entire hierarchy below each node.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
NodesDeleteOutput

Dictionary with deletion result: - deleted: List of nodes successfully deleted - count: Number of nodes deleted - errors: Map of node name to error message, or None

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

ValueError

If node names contain invalid characters.

Example

result = nodes_delete(["pCube1", "pSphere1"]) print(f"Deleted {result['count']} nodes")

Source code in src/maya_mcp/tools/nodes.py
def nodes_delete(
    nodes: list[str],
    hierarchy: bool = False,
) -> NodesDeleteOutput:
    """Delete one or more nodes from the Maya scene.

    Args:
        nodes: List of node names to delete.
        hierarchy: If True, delete entire hierarchy below each node.

    Returns:
        Dictionary with deletion result:
            - deleted: List of nodes successfully deleted
            - count: Number of nodes deleted
            - errors: Map of node name to error message, or None

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.
        ValueError: If node names contain invalid characters.

    Example:
        >>> result = nodes_delete(["pCube1", "pSphere1"])
        >>> print(f"Deleted {result['count']} nodes")
    """
    # Input validation
    if not nodes:
        raise ValueError("nodes list cannot be empty")
    for node in nodes:
        _validate_node_name(node)

    client = get_client()

    # Build the Maya command
    nodes_escaped = json.dumps(nodes)
    hierarchy_flag = "True" if hierarchy else "False"

    command = f"""
import maya.cmds as cmds
import json

nodes_to_delete = {nodes_escaped}
delete_hierarchy = {hierarchy_flag}

result = {{"deleted": [], "errors": {{}}}}

for node in nodes_to_delete:
    try:
        if not cmds.objExists(node):
            result["errors"][node] = f"Node '{{node}}' does not exist"
        else:
            cmds.delete(node)
            result["deleted"].append(node)
    except Exception as e:
        result["errors"][node] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)

    # Parse the JSON response
    parsed = parse_json_response(response)

    deleted = parsed.get("deleted", [])
    errors = parsed.get("errors", {})

    result: dict[str, Any] = {
        "deleted": deleted,
        "count": len(deleted),
    }

    if errors:
        result["errors"] = errors
    else:
        result["errors"] = None

    return cast("NodesDeleteOutput", result)

nodes_rename

nodes_rename(mapping: dict[str, str]) -> NodesRenameOutput

Rename one or more nodes in the Maya scene.

PARAMETER DESCRIPTION
mapping

Dictionary mapping current node names to new names.

TYPE: dict[str, str]

RETURNS DESCRIPTION
NodesRenameOutput

Dictionary with rename result: - renamed: Map of original name to actual new name - errors: Map of original name to error message, or None

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

ValueError

If node names contain invalid characters.

Example

result = nodes_rename({"pCube1": "myCube"}) print(f"Renamed to: {result['renamed']['pCube1']}")

Source code in src/maya_mcp/tools/nodes.py
def nodes_rename(mapping: dict[str, str]) -> NodesRenameOutput:
    """Rename one or more nodes in the Maya scene.

    Args:
        mapping: Dictionary mapping current node names to new names.

    Returns:
        Dictionary with rename result:
            - renamed: Map of original name to actual new name
            - errors: Map of original name to error message, or None

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.
        ValueError: If node names contain invalid characters.

    Example:
        >>> result = nodes_rename({"pCube1": "myCube"})
        >>> print(f"Renamed to: {result['renamed']['pCube1']}")
    """
    if not mapping:
        raise ValueError("mapping cannot be empty")

    # Input validation
    for old_name, new_name in mapping.items():
        _validate_node_name(old_name)
        _validate_node_name(new_name)

    client = get_client()

    # Escape mapping for Maya script
    mapping_json = json.dumps(mapping)

    command = f"""
import maya.cmds as cmds
import json

mapping = {mapping_json}
result = {{"renamed": {{}}, "errors": {{}}}}

for old_name, new_name in mapping.items():
    if not cmds.objExists(old_name):
        result["errors"][old_name] = "Node '" + old_name + "' does not exist"
        continue


    try:
        # cmds.rename returns the new name (which might handle collisions)
        actual_name = cmds.rename(old_name, new_name)
        result["renamed"][old_name] = actual_name
    except Exception as e:
        result["errors"][old_name] = str(e)

# Only return the JSON string, do not print it (which goes to script editor log)
# The last expression is returned by commandPort
print(json.dumps(result))
"""

    response = client.execute(command)
    parsed = parse_json_response(response)

    renamed = parsed.get("renamed", {})
    errors = parsed.get("errors", {})

    result: dict[str, Any] = {
        "renamed": renamed,
        "errors": errors if errors else None,
    }

    return cast("NodesRenameOutput", result)

nodes_parent

nodes_parent(nodes: list[str], parent: str | None = None, relative: bool = False) -> NodesParentOutput

Reparent one or more nodes in the Maya hierarchy.

PARAMETER DESCRIPTION
nodes

List of nodes to reparent.

TYPE: list[str]

parent

New parent node. If None, unparent (parent to world).

TYPE: str | None DEFAULT: None

relative

Preserve existing local transformations.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
NodesParentOutput

Dictionary with reparent result: - parented: List of nodes successfully reparented - count: Number of nodes reparented - errors: Map of node name to error message, or None

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

ValueError

If node names contain invalid characters.

Source code in src/maya_mcp/tools/nodes.py
def nodes_parent(
    nodes: list[str],
    parent: str | None = None,
    relative: bool = False,
) -> NodesParentOutput:
    """Reparent one or more nodes in the Maya hierarchy.

    Args:
        nodes: List of nodes to reparent.
        parent: New parent node. If None, unparent (parent to world).
        relative: Preserve existing local transformations.

    Returns:
        Dictionary with reparent result:
            - parented: List of nodes successfully reparented
            - count: Number of nodes reparented
            - errors: Map of node name to error message, or None

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.
        ValueError: If node names contain invalid characters.
    """
    if not nodes:
        raise ValueError("nodes list cannot be empty")

    for node in nodes:
        _validate_node_name(node)
    if parent is not None:
        _validate_node_name(parent)

    client = get_client()

    nodes_json = json.dumps(nodes)
    parent_json = json.dumps(parent) if parent else "None"
    relative_str = "True" if relative else "False"

    command = f"""
import maya.cmds as cmds
import json

nodes = {nodes_json}
parent_node = {parent_json}
relative_flag = {relative_str}

result = {{"parented": [], "errors": {{}}}}

# Validate parent exists if specified
if parent_node and not cmds.objExists(parent_node):
    result["errors"]["_parent"] = f"Parent node '{{parent_node}}' does not exist"
else:
    for node in nodes:
        try:
            if not cmds.objExists(node):
                result["errors"][node] = f"Node '{{node}}' does not exist"
                continue


            # Perform parenting
            if parent_node:
                res = cmds.parent(node, parent_node, relative=relative_flag)
            else:
                res = cmds.parent(node, world=True, relative=relative_flag)


            # cmds.parent returns list of new names (in case of instance/rename)
            # We usually just want the node name we operated on, but let's record what Maya returned
            if res:
                result["parented"].extend(res)
            else:
                # Should not happen on success, but fallback
                result["parented"].append(node)


        except Exception as e:
            result["errors"][node] = str(e)

# Only return the JSON string, do not print it (which goes to script editor log)
# The last expression is returned by commandPort
print(json.dumps(result))
"""

    response = client.execute(command)
    parsed = parse_json_response(response)

    parented = parsed.get("parented", [])
    errors = parsed.get("errors", {})

    result: dict[str, Any] = {
        "parented": parented,
        "count": len(parented),
        "errors": errors if errors else None,
    }
    return cast("NodesParentOutput", result)

nodes_duplicate

nodes_duplicate(nodes: list[str], name: str | None = None, input_connections: bool = False, upstream_nodes: bool = False, parent_only: bool = False) -> NodesDuplicateOutput

Duplicate one or more nodes.

PARAMETER DESCRIPTION
nodes

List of nodes to duplicate.

TYPE: list[str]

name

Name for the new node (only valid when duplicating single node).

TYPE: str | None DEFAULT: None

input_connections

Duplicate input connections.

TYPE: bool DEFAULT: False

upstream_nodes

Duplicate upstream nodes.

TYPE: bool DEFAULT: False

parent_only

Duplicate only the specified node, not its children.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
NodesDuplicateOutput

Dictionary with duplication result: - duplicated: Map of original name to new name - count: Number of nodes duplicated - errors: Map of original name to error message, or None

Source code in src/maya_mcp/tools/nodes.py
def nodes_duplicate(
    nodes: list[str],
    name: str | None = None,
    input_connections: bool = False,
    upstream_nodes: bool = False,
    parent_only: bool = False,
) -> NodesDuplicateOutput:
    """Duplicate one or more nodes.

    Args:
        nodes: List of nodes to duplicate.
        name: Name for the new node (only valid when duplicating single node).
        input_connections: Duplicate input connections.
        upstream_nodes: Duplicate upstream nodes.
        parent_only: Duplicate only the specified node, not its children.

    Returns:
        Dictionary with duplication result:
            - duplicated: Map of original name to new name
            - count: Number of nodes duplicated
            - errors: Map of original name to error message, or None
    """
    if not nodes:
        raise ValueError("nodes list cannot be empty")
    if name and len(nodes) > 1:
        raise ValueError("Cannot specify name when duplicating multiple nodes")

    for node in nodes:
        _validate_node_name(node)
    if name:
        _validate_node_name(name)

    client = get_client()

    nodes_json = json.dumps(nodes)
    name_json = json.dumps(name) if name else "None"
    ic_str = "True" if input_connections else "False"
    un_str = "True" if upstream_nodes else "False"
    po_str = "True" if parent_only else "False"

    command = f"""
import maya.cmds as cmds
import json

nodes = {nodes_json}
desired_name = {name_json}
ic_flag = {ic_str}
un_flag = {un_str}
po_flag = {po_str}

result = {{"duplicated": {{}}, "errors": {{}}}}

for node in nodes:
    try:
        if not cmds.objExists(node):
            result["errors"][node] = f"Node '{{node}}' does not exist"
            continue


        # Build args
        kwargs = {{
            "inputConnections": ic_flag,
            "upstreamNodes": un_flag,
            "parentOnly": po_flag,
        }}
        if desired_name:
            kwargs["name"] = desired_name


        dup = cmds.duplicate(node, **kwargs)
        if dup:
            result["duplicated"][node] = dup[0]


    except Exception as e:
        result["errors"][node] = str(e)

# Return JSON string as the last expression
print(json.dumps(result))
"""

    response = client.execute(command)
    parsed = parse_json_response(response)

    duplicated = parsed.get("duplicated", {})
    errors = parsed.get("errors", {})

    result: dict[str, Any] = {
        "duplicated": duplicated,
        "count": len(duplicated),
        "errors": errors if errors else None,
    }
    return cast("NodesDuplicateOutput", result)

Attributes

attributes

Attribute tools for Maya MCP.

This module provides tools for getting and setting node attributes in Maya. Supports batch operations to reduce tool call chaining.

AttributesGetOutput

Bases: TypedDict

Return payload for the attributes.get tool.

AttributesSetOutput

Bases: TypedDict

Return payload for the attributes.set tool.

attributes_get

attributes_get(node: str, attributes: list[str]) -> AttributesGetOutput

Get one or more attribute values from a Maya node.

Supports batch attribute queries to reduce tool call chaining. Returns partial results if some attributes fail.

PARAMETER DESCRIPTION
node

The node name to query.

TYPE: str

attributes

List of attribute names to get (e.g., ["translateX", "visibility"]).

TYPE: list[str]

RETURNS DESCRIPTION
AttributesGetOutput

Dictionary with attribute values: - node: Node name queried - attributes: Map of attribute name to value - count: Number of attributes successfully retrieved - errors: Map of attribute name to error message (if any failed), or None

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails completely.

ValueError

If node or attribute names are invalid.

Example

result = attributes_get("pCube1", ["translateX", "translateY", "visibility"]) print(f"translateX = {result['attributes']['translateX']}")

Source code in src/maya_mcp/tools/attributes.py
def attributes_get(
    node: str,
    attributes: list[str],
) -> AttributesGetOutput:
    """Get one or more attribute values from a Maya node.

    Supports batch attribute queries to reduce tool call chaining.
    Returns partial results if some attributes fail.

    Args:
        node: The node name to query.
        attributes: List of attribute names to get (e.g., ["translateX", "visibility"]).

    Returns:
        Dictionary with attribute values:
            - node: Node name queried
            - attributes: Map of attribute name to value
            - count: Number of attributes successfully retrieved
            - errors: Map of attribute name to error message (if any failed), or None

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails completely.
        ValueError: If node or attribute names are invalid.

    Example:
        >>> result = attributes_get("pCube1", ["translateX", "translateY", "visibility"])
        >>> print(f"translateX = {result['attributes']['translateX']}")
    """
    # Input validation
    _validate_node_name(node)
    if not attributes:
        raise ValueError("attributes list cannot be empty")
    for attr in attributes:
        _validate_attribute_name(attr)

    client = get_client()

    # Build the Maya command - get each attribute and collect results
    node_escaped = json.dumps(node)
    attrs_json = json.dumps(attributes)
    attrs_escaped = json.dumps(attrs_json)

    command = f"""
import maya.cmds as cmds
import json

node = {node_escaped}
attrs = json.loads({attrs_escaped})

result = {{"values": {{}}, "errors": {{}}}}

# Check if node exists
if not cmds.objExists(node):
    result["errors"]["_node"] = f"Node '{{node}}' does not exist"
else:
    for attr in attrs:
        try:
            full_attr = f"{{node}}.{{attr}}"
            if not cmds.attributeQuery(attr, node=node, exists=True):
                result["errors"][attr] = f"Attribute '{{attr}}' not found on node '{{node}}'"
            else:
                value = cmds.getAttr(full_attr)
                result["values"][attr] = value
        except Exception as e:
            result["errors"][attr] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)

    # Parse the JSON response
    parsed = parse_json_response(response)

    values = parsed.get("values", {})
    errors = parsed.get("errors", {})

    # Check for node-level error
    if "_node" in errors:
        raise ValueError(errors["_node"])

    result: dict[str, Any] = {
        "node": node,
        "attributes": values,
        "count": len(values),
    }

    if errors:
        result["errors"] = errors
    else:
        result["errors"] = None

    return cast("AttributesGetOutput", result)

attributes_set

attributes_set(node: str, attributes: dict[str, Any]) -> AttributesSetOutput

Set one or more attribute values on a Maya node.

Supports batch attribute setting to reduce tool call chaining. Returns partial results if some attributes fail.

PARAMETER DESCRIPTION
node

The node name to modify.

TYPE: str

attributes

Map of attribute name to value.

TYPE: dict[str, Any]

RETURNS DESCRIPTION
AttributesSetOutput

Dictionary with set results: - node: Node name modified - set: List of attributes successfully set - count: Number of attributes successfully set - errors: Map of attribute name to error message (if any failed), or None

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails completely.

ValueError

If node or attribute names are invalid.

Example

result = attributes_set("pCube1", {"translateX": 10.0, "visibility": False}) print(f"Set {result['count']} attributes")

Source code in src/maya_mcp/tools/attributes.py
def attributes_set(
    node: str,
    attributes: dict[str, Any],
) -> AttributesSetOutput:
    """Set one or more attribute values on a Maya node.

    Supports batch attribute setting to reduce tool call chaining.
    Returns partial results if some attributes fail.

    Args:
        node: The node name to modify.
        attributes: Map of attribute name to value.

    Returns:
        Dictionary with set results:
            - node: Node name modified
            - set: List of attributes successfully set
            - count: Number of attributes successfully set
            - errors: Map of attribute name to error message (if any failed), or None

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails completely.
        ValueError: If node or attribute names are invalid.

    Example:
        >>> result = attributes_set("pCube1", {"translateX": 10.0, "visibility": False})
        >>> print(f"Set {result['count']} attributes")
    """
    # Input validation
    _validate_node_name(node)
    if not attributes:
        raise ValueError("attributes dict cannot be empty")
    for attr in attributes:
        _validate_attribute_name(attr)

    client = get_client()

    # Build the Maya command - set each attribute and collect results
    node_escaped = json.dumps(node)
    attrs_json = json.dumps(attributes)
    attrs_escaped = json.dumps(attrs_json)

    command = f"""
import maya.cmds as cmds
import json

node = {node_escaped}
attrs = json.loads({attrs_escaped})

result = {{"set": [], "errors": {{}}}}

# Check if node exists
if not cmds.objExists(node):
    result["errors"]["_node"] = f"Node '{{node}}' does not exist"
else:
    for attr, value in attrs.items():
        try:
            full_attr = f"{{node}}.{{attr}}"
            if not cmds.attributeQuery(attr, node=node, exists=True):
                result["errors"][attr] = f"Attribute '{{attr}}' not found on node '{{node}}'"
            elif cmds.getAttr(full_attr, lock=True):
                result["errors"][attr] = f"Attribute '{{attr}}' is locked"
            else:
                # Handle different value types
                if isinstance(value, (list, tuple)) and len(value) == 3:
                    # Compound attribute like translate, rotate, scale
                    cmds.setAttr(full_attr, value[0], value[1], value[2], type="double3")
                elif isinstance(value, str):
                    cmds.setAttr(full_attr, value, type="string")
                else:
                    cmds.setAttr(full_attr, value)
                result["set"].append(attr)
        except Exception as e:
            result["errors"][attr] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)

    # Parse the JSON response
    parsed = parse_json_response(response)

    set_attrs = parsed.get("set", [])
    errors = parsed.get("errors", {})

    # Check for node-level error
    if "_node" in errors:
        raise ValueError(errors["_node"])

    result: dict[str, Any] = {
        "node": node,
        "set": set_attrs,
        "count": len(set_attrs),
    }

    if errors:
        result["errors"] = errors
    else:
        result["errors"] = None

    return cast("AttributesSetOutput", result)

Selection

selection

Selection tools for Maya MCP.

This module provides tools for querying and modifying the Maya selection, including component-level selection (vertices, edges, faces).

SelectionOutput

Bases: TypedDict

Return payload for node selection state tools.

SelectionWithErrorsOutput

Bases: SelectionOutput, _GuardedOutput

Return payload for selection operations that may partially fail.

SelectionComponentsOutput

Bases: _GuardedOutput

Return payload for the selection.get_components tool.

SelectionConvertComponentsOutput

Bases: SelectionWithErrorsOutput

Return payload for the selection.convert_components tool.

selection_get

selection_get() -> SelectionOutput

Get the current selection in Maya.

Returns the list of currently selected nodes.

RETURNS DESCRIPTION
SelectionOutput

Dictionary with selection: - selection: List of selected node names - count: Number of selected items

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

Example

result = selection_get() if result['count'] > 0: ... print(f"Selected: {result['selection']}")

Source code in src/maya_mcp/tools/selection.py
def selection_get() -> SelectionOutput:
    """Get the current selection in Maya.

    Returns the list of currently selected nodes.

    Returns:
        Dictionary with selection:
            - selection: List of selected node names
            - count: Number of selected items

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.

    Example:
        >>> result = selection_get()
        >>> if result['count'] > 0:
        ...     print(f"Selected: {result['selection']}")
    """
    client = get_client()

    command = """
import maya.cmds as cmds
import json

selection = cmds.ls(selection=True) or []
print(json.dumps(selection))
"""

    response = client.execute(command)

    # Parse the JSON response
    selection = parse_json_response(response)

    if not isinstance(selection, list):
        selection = []

    return {
        "selection": selection,
        "count": len(selection),
    }

selection_set_components

selection_set_components(components: list[str], add: bool = False, deselect: bool = False) -> SelectionWithErrorsOutput

Select mesh components (vertices, edges, or faces).

Selects components specified by Maya component notation (e.g., "pCube1.vtx[0:10]", "pSphere1.e[5]", "pPlane1.f[0:99]").

PARAMETER DESCRIPTION
components

List of component specifications in Maya notation. Examples: - "pCube1.vtx[0]" - single vertex - "pCube1.vtx[0:10]" - vertex range - "pCube1.e[5]" - single edge - "pCube1.f[0:99]" - face range

TYPE: list[str]

add

If True, add to existing selection instead of replacing.

TYPE: bool DEFAULT: False

deselect

If True, remove from selection instead of adding.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
SelectionWithErrorsOutput

Dictionary with selection result: - selection: List of currently selected components - count: Number of selected components - errors: Map of component to error message, or None

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

ValueError

If components list is empty or contains invalid specifications.

Example

result = selection_set_components(["pCube1.vtx[0:7]"]) print(f"Selected {result['count']} vertices")

Source code in src/maya_mcp/tools/selection.py
def selection_set_components(
    components: list[str],
    add: bool = False,
    deselect: bool = False,
) -> SelectionWithErrorsOutput:
    """Select mesh components (vertices, edges, or faces).

    Selects components specified by Maya component notation
    (e.g., "pCube1.vtx[0:10]", "pSphere1.e[5]", "pPlane1.f[0:99]").

    Args:
        components: List of component specifications in Maya notation.
            Examples:
            - "pCube1.vtx[0]" - single vertex
            - "pCube1.vtx[0:10]" - vertex range
            - "pCube1.e[5]" - single edge
            - "pCube1.f[0:99]" - face range
        add: If True, add to existing selection instead of replacing.
        deselect: If True, remove from selection instead of adding.

    Returns:
        Dictionary with selection result:
            - selection: List of currently selected components
            - count: Number of selected components
            - errors: Map of component to error message, or None

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.
        ValueError: If components list is empty or contains invalid specifications.

    Example:
        >>> result = selection_set_components(["pCube1.vtx[0:7]"])
        >>> print(f"Selected {result['count']} vertices")
    """
    if not components:
        raise ValueError("components list cannot be empty")

    for comp in components:
        _validate_component_name(comp)

    if add and deselect:
        raise ValueError("Cannot specify both add=True and deselect=True")

    client = get_client()
    components_escaped = json.dumps(components)

    if deselect:
        mode = "deselect=True"
    elif add:
        mode = "add=True"
    else:
        mode = "replace=True"

    command = f"""
import maya.cmds as cmds
import json

components = {components_escaped}
result = {{"selection": [], "errors": {{}}}}

valid_components = []
for comp in components:
    try:
        if cmds.objExists(comp):
            valid_components.append(comp)
        else:
            result["errors"][comp] = "Component '" + comp + "' does not exist"
    except Exception as e:
        result["errors"][comp] = str(e)

if valid_components:
    try:
        cmds.select(valid_components, {mode})
        result["selection"] = cmds.ls(selection=True, flatten=True) or []
    except Exception as e:
        result["errors"]["_select"] = str(e)

result["count"] = len(result["selection"])
print(json.dumps(result))
"""

    response = client.execute(command)
    parsed = parse_json_response(response)

    selection = parsed.get("selection", [])
    errors = parsed.get("errors", {})

    result: dict[str, Any] = {
        "selection": selection,
        "count": len(selection),
    }

    if errors:
        result["errors"] = errors
    else:
        result["errors"] = None

    return cast("SelectionWithErrorsOutput", guard_response_size(result, list_key="selection"))

selection_get_components

selection_get_components() -> SelectionComponentsOutput

Get the currently selected mesh components.

Returns the selected components grouped by type (vertex, edge, face) with their indices.

RETURNS DESCRIPTION
SelectionComponentsOutput

Dictionary with component selection: - selection: List of all selected components (flattened) - vertices: List of selected vertex specifications - edges: List of selected edge specifications - faces: List of selected face specifications - vertex_count: Number of selected vertices - edge_count: Number of selected edges - face_count: Number of selected faces - total_count: Total number of selected components - has_components: True if any components are selected

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

Example

result = selection_get_components() print(f"Selected {result['vertex_count']} vertices") for v in result['vertices']: ... print(v)

Source code in src/maya_mcp/tools/selection.py
def selection_get_components() -> SelectionComponentsOutput:
    """Get the currently selected mesh components.

    Returns the selected components grouped by type (vertex, edge, face)
    with their indices.

    Returns:
        Dictionary with component selection:
            - selection: List of all selected components (flattened)
            - vertices: List of selected vertex specifications
            - edges: List of selected edge specifications
            - faces: List of selected face specifications
            - vertex_count: Number of selected vertices
            - edge_count: Number of selected edges
            - face_count: Number of selected faces
            - total_count: Total number of selected components
            - has_components: True if any components are selected

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.

    Example:
        >>> result = selection_get_components()
        >>> print(f"Selected {result['vertex_count']} vertices")
        >>> for v in result['vertices']:
        ...     print(v)
    """
    client = get_client()

    command = """
import maya.cmds as cmds
import json

result = {
    "selection": [],
    "vertices": [],
    "edges": [],
    "faces": [],
    "vertex_count": 0,
    "edge_count": 0,
    "face_count": 0,
    "total_count": 0,
    "has_components": False
}

try:
    all_sel = cmds.ls(selection=True, flatten=True) or []
    result["selection"] = all_sel
    result["total_count"] = len(all_sel)

    for item in all_sel:
        if ".vtx[" in item or ".vtxs[" in item:
            result["vertices"].append(item)
        elif ".e[" in item or ".edge[" in item:
            result["edges"].append(item)
        elif ".f[" in item or ".face[" in item:
            result["faces"].append(item)

    result["vertex_count"] = len(result["vertices"])
    result["edge_count"] = len(result["edges"])
    result["face_count"] = len(result["faces"])
    result["has_components"] = result["total_count"] > 0

except Exception as e:
    result["error"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    return cast("SelectionComponentsOutput", guard_response_size(parsed, list_key="selection"))

selection_convert_components

selection_convert_components(to_type: Literal['vertex', 'edge', 'face'], nodes: list[str] | None = None) -> SelectionConvertComponentsOutput

Convert the current selection to a different component type.

Converts selected components (or specified nodes) to vertices, edges, or faces.

PARAMETER DESCRIPTION
to_type

Target component type: "vertex", "edge", or "face".

TYPE: Literal['vertex', 'edge', 'face']

nodes

Optional list of nodes to convert selection on. If None, uses current selection.

TYPE: list[str] | None DEFAULT: None

RETURNS DESCRIPTION
SelectionConvertComponentsOutput

Dictionary with converted selection: - selection: List of converted components - to_type: The target component type - count: Number of converted components - errors: Error details if any, or None

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

ValueError

If to_type is invalid or nodes contain invalid names.

Example

Convert edge selection to faces

result = selection_convert_components("face") print(f"Now have {result['count']} faces selected")

Source code in src/maya_mcp/tools/selection.py
def selection_convert_components(
    to_type: Literal["vertex", "edge", "face"],
    nodes: list[str] | None = None,
) -> SelectionConvertComponentsOutput:
    """Convert the current selection to a different component type.

    Converts selected components (or specified nodes) to vertices,
    edges, or faces.

    Args:
        to_type: Target component type: "vertex", "edge", or "face".
        nodes: Optional list of nodes to convert selection on.
            If None, uses current selection.

    Returns:
        Dictionary with converted selection:
            - selection: List of converted components
            - to_type: The target component type
            - count: Number of converted components
            - errors: Error details if any, or None

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.
        ValueError: If to_type is invalid or nodes contain invalid names.

    Example:
        >>> # Convert edge selection to faces
        >>> result = selection_convert_components("face")
        >>> print(f"Now have {result['count']} faces selected")
    """
    valid_types = {"vertex", "edge", "face"}
    if to_type not in valid_types:
        raise ValueError(
            f"Invalid to_type: {to_type!r}. Must be one of: {', '.join(sorted(valid_types))}"
        )

    if nodes is not None:
        for node in nodes:
            _validate_node_name(node)

    client = get_client()

    nodes_escaped = json.dumps(nodes) if nodes else "None"

    command = f"""
import maya.cmds as cmds
import json

to_type = {json.dumps(to_type)}
nodes = {nodes_escaped}
result = {{"selection": [], "to_type": to_type, "count": 0, "errors": {{}}}}

try:
    current_sel = cmds.ls(selection=True, flatten=True) or []

    if not current_sel and nodes:
        cmds.select(nodes, replace=True)

    sel = cmds.ls(selection=True) or []
    if sel:
        if to_type == "vertex":
            converted = cmds.polyListComponentConversion(sel, toVertex=True)
        elif to_type == "edge":
            converted = cmds.polyListComponentConversion(sel, toEdge=True)
        elif to_type == "face":
            converted = cmds.polyListComponentConversion(sel, toFace=True)

        if converted:
            cmds.select(converted, replace=True)
            result["selection"] = cmds.ls(selection=True, flatten=True) or []
        else:
            result["selection"] = []
    else:
        result["selection"] = []

    result["count"] = len(result["selection"])

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed = parse_json_response(response)

    selection = parsed.get("selection", [])
    errors = parsed.get("errors", {})

    result: dict[str, Any] = {
        "selection": selection,
        "to_type": to_type,
        "count": len(selection),
    }

    if errors:
        result["errors"] = errors
    else:
        result["errors"] = None

    return cast(
        "SelectionConvertComponentsOutput", guard_response_size(result, list_key="selection")
    )

selection_clear

selection_clear() -> SelectionOutput

Clear the Maya selection.

Deselects all currently selected nodes.

RETURNS DESCRIPTION
SelectionOutput

Dictionary with empty selection state: - selection: Empty list - count: 0

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

Example

result = selection_clear() print(f"Selection cleared: {result['count']} items")

Source code in src/maya_mcp/tools/selection.py
def selection_clear() -> SelectionOutput:
    """Clear the Maya selection.

    Deselects all currently selected nodes.

    Returns:
        Dictionary with empty selection state:
            - selection: Empty list
            - count: 0

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.

    Example:
        >>> result = selection_clear()
        >>> print(f"Selection cleared: {result['count']} items")
    """
    client = get_client()

    command = """
import maya.cmds as cmds
import json

cmds.select(clear=True)
selection = cmds.ls(selection=True) or []
print(json.dumps(selection))
"""

    response = client.execute(command)

    # Parse the JSON response
    selection = parse_json_response(response)

    if not isinstance(selection, list):
        selection = []

    return {
        "selection": selection,
        "count": len(selection),
    }

selection_set

selection_set(nodes: list[str], add: bool = False, deselect: bool = False) -> SelectionOutput

Set the Maya selection.

Modifies the current selection by selecting, adding to, or removing from the selection.

PARAMETER DESCRIPTION
nodes

List of node names to operate on.

TYPE: list[str]

add

If True, add to existing selection instead of replacing.

TYPE: bool DEFAULT: False

deselect

If True, remove from selection instead of adding.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
SelectionOutput

Dictionary with new selection state: - selection: List of selected node names after operation - count: Number of selected items

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

ValueError

If nodes list is empty or contains invalid names.

Example

Replace selection

result = selection_set(["pCube1", "pSphere1"])

Add to selection

result = selection_set(["pCone1"], add=True)

Remove from selection

result = selection_set(["pCube1"], deselect=True)

Source code in src/maya_mcp/tools/selection.py
def selection_set(
    nodes: list[str],
    add: bool = False,
    deselect: bool = False,
) -> SelectionOutput:
    """Set the Maya selection.

    Modifies the current selection by selecting, adding to, or
    removing from the selection.

    Args:
        nodes: List of node names to operate on.
        add: If True, add to existing selection instead of replacing.
        deselect: If True, remove from selection instead of adding.

    Returns:
        Dictionary with new selection state:
            - selection: List of selected node names after operation
            - count: Number of selected items

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.
        ValueError: If nodes list is empty or contains invalid names.

    Example:
        >>> # Replace selection
        >>> result = selection_set(["pCube1", "pSphere1"])
        >>>
        >>> # Add to selection
        >>> result = selection_set(["pCone1"], add=True)
        >>>
        >>> # Remove from selection
        >>> result = selection_set(["pCube1"], deselect=True)
    """
    # Input validation
    if not nodes:
        raise ValueError("nodes list cannot be empty")

    for node in nodes:
        _validate_node_name(node)

    if add and deselect:
        raise ValueError("Cannot specify both add=True and deselect=True")

    client = get_client()

    # Build the Maya command
    # We use json.dumps to safely escape the node names
    nodes_escaped = json.dumps(nodes)

    if deselect:
        command = f"""
import maya.cmds as cmds
import json

nodes = {nodes_escaped}
cmds.select(nodes, deselect=True)
selection = cmds.ls(selection=True) or []
print(json.dumps(selection))
"""
    elif add:
        command = f"""
import maya.cmds as cmds
import json

nodes = {nodes_escaped}
cmds.select(nodes, add=True)
selection = cmds.ls(selection=True) or []
print(json.dumps(selection))
"""
    else:
        command = f"""
import maya.cmds as cmds
import json

nodes = {nodes_escaped}
cmds.select(nodes, replace=True)
selection = cmds.ls(selection=True) or []
print(json.dumps(selection))
"""

    response = client.execute(command)

    # Parse the JSON response
    selection = parse_json_response(response)

    if not isinstance(selection, list):
        selection = []

    return {
        "selection": selection,
        "count": len(selection),
    }

Connections

connections

Connection tools for Maya MCP.

This module provides tools for querying and managing node connections in Maya's dependency graph, including construction/deformation history.

ConnectionEntry

Bases: TypedDict

A single dependency-graph connection.

ConnectionsListOutput

Bases: _GuardedOutput

Return payload for the connections.list tool.

ConnectionAttributeInfo

Bases: TypedDict

Connection details for a single attribute.

ConnectionsGetOutput

Bases: TypedDict

Return payload for the connections.get tool.

ConnectionsConnectOutput

Bases: TypedDict

Return payload for the connections.connect tool.

ConnectionPair

Bases: TypedDict

A disconnected source/destination pair.

ConnectionsDisconnectOutput

Bases: TypedDict

Return payload for the connections.disconnect tool.

ConnectionHistoryEntry

Bases: TypedDict

A single history node discovered during traversal.

ConnectionsHistoryOutput

Bases: _GuardedOutput

Return payload for the connections.history tool.

connections_list

connections_list(node: str, direction: Literal['incoming', 'outgoing', 'both'] = 'both', connections_type: str | None = None, limit: int | None = DEFAULT_CONNECTIONS_LIMIT) -> ConnectionsListOutput

List connections on a Maya node.

PARAMETER DESCRIPTION
node

Node name to query connections for.

TYPE: str

direction

Filter by connection direction: - "incoming": Only connections where data flows INTO this node - "outgoing": Only connections where data flows FROM this node - "both" (default): All connections

TYPE: Literal['incoming', 'outgoing', 'both'] DEFAULT: 'both'

connections_type

Filter by connection type (e.g., "animCurve", "shader"). If None, returns all connection types.

TYPE: str | None DEFAULT: None

limit

Maximum number of connections to return. Default 500. Set to 0 or None for unlimited.

TYPE: int | None DEFAULT: DEFAULT_CONNECTIONS_LIMIT

RETURNS DESCRIPTION
ConnectionsListOutput

Dictionary with: - node: The queried node name - connections: List of connection info dicts - count: Number of connections returned - truncated: True if results were truncated (only if limit hit) - total_count: Total connections before limit (only if truncated)

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

ValueError

If node name is invalid.

Example

result = connections_list("pCube1", direction="incoming") for conn in result["connections"]: ... print(f"{conn['source']} -> {conn['destination']}")

Source code in src/maya_mcp/tools/connections.py
def connections_list(
    node: str,
    direction: Literal["incoming", "outgoing", "both"] = "both",
    connections_type: str | None = None,
    limit: int | None = DEFAULT_CONNECTIONS_LIMIT,
) -> ConnectionsListOutput:
    """List connections on a Maya node.

    Args:
        node: Node name to query connections for.
        direction: Filter by connection direction:
            - "incoming": Only connections where data flows INTO this node
            - "outgoing": Only connections where data flows FROM this node
            - "both" (default): All connections
        connections_type: Filter by connection type (e.g., "animCurve", "shader").
            If None, returns all connection types.
        limit: Maximum number of connections to return. Default 500.
            Set to 0 or None for unlimited.

    Returns:
        Dictionary with:
            - node: The queried node name
            - connections: List of connection info dicts
            - count: Number of connections returned
            - truncated: True if results were truncated (only if limit hit)
            - total_count: Total connections before limit (only if truncated)

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.
        ValueError: If node name is invalid.

    Example:
        >>> result = connections_list("pCube1", direction="incoming")
        >>> for conn in result["connections"]:
        ...     print(f"{conn['source']} -> {conn['destination']}")
    """
    _validate_node_name(node)

    client = get_client()

    node_escaped = json.dumps(node)
    direction_escaped = json.dumps(direction)
    type_escaped = json.dumps(connections_type) if connections_type else "None"
    limit_val = limit if limit and limit > 0 else 0

    command = f"""
import maya.cmds as cmds
import json

node = {node_escaped}
direction = {direction_escaped}
conn_type = {type_escaped}
limit = {limit_val}

result = {{"node": node, "connections": [], "errors": {{}}}}

if not cmds.objExists(node):
    result["errors"]["_node"] = f"Node '{{node}}' does not exist"
else:
    all_conns = []

    # Get connections based on direction
    if direction in ("incoming", "both"):
        incoming = cmds.listConnections(node, source=True, destination=False,
                                        plugs=True, connections=True) or []
        # incoming is [dest_plug, src_plug, dest_plug, src_plug, ...]
        for i in range(0, len(incoming), 2):
            dest_plug = incoming[i]
            src_plug = incoming[i + 1]
            src_node = src_plug.split(".")[0]
            src_node_type = cmds.nodeType(src_node)

            # Type filter
            if conn_type and src_node_type != conn_type:
                continue

            all_conns.append({{
                "source": src_plug,
                "source_node": src_node,
                "source_type": src_node_type,
                "destination": dest_plug,
                "destination_node": node,
                "destination_type": cmds.nodeType(node),
                "direction": "incoming"
            }})

    if direction in ("outgoing", "both"):
        outgoing = cmds.listConnections(node, source=False, destination=True,
                                        plugs=True, connections=True) or []
        # outgoing is [src_plug, dest_plug, src_plug, dest_plug, ...]
        for i in range(0, len(outgoing), 2):
            src_plug = outgoing[i]
            dest_plug = outgoing[i + 1]
            dest_node = dest_plug.split(".")[0]
            dest_node_type = cmds.nodeType(dest_node)

            # Type filter
            if conn_type and dest_node_type != conn_type:
                continue

            all_conns.append({{
                "source": src_plug,
                "source_node": node,
                "source_type": cmds.nodeType(node),
                "destination": dest_plug,
                "destination_node": dest_node,
                "destination_type": dest_node_type,
                "direction": "outgoing"
            }})

    total_count = len(all_conns)
    truncated = False

    if limit > 0 and total_count > limit:
        all_conns = all_conns[:limit]
        truncated = True

    result["connections"] = all_conns
    result["count"] = len(all_conns)

    if truncated:
        result["truncated"] = True
        result["total_count"] = total_count

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    errors = parsed.get("errors") or {}
    if "_node" in errors:
        raise ValueError(errors["_node"])

    parsed["errors"] = errors if errors else None

    parsed = guard_response_size(parsed, list_key="connections")

    return cast("ConnectionsListOutput", parsed)

connections_get

connections_get(node: str, attributes: list[str] | None = None) -> ConnectionsGetOutput

Get detailed connection information for specific attributes.

Returns connection details for each specified attribute, including connected plugs, connection types, and lock status.

PARAMETER DESCRIPTION
node

Node name to query.

TYPE: str

attributes

List of attribute names to check for connections. If None, checks all connectable attributes.

TYPE: list[str] | None DEFAULT: None

RETURNS DESCRIPTION
ConnectionsGetOutput

Dictionary with: - node: The queried node name - attributes: Dict mapping attribute name to connection info - count: Number of attributes with connections - errors: Map of attribute to error message, or None

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

ValueError

If node name is invalid.

Example

result = connections_get("pCube1", ["translateX", "visibility"]) if result["attributes"]["translateX"]["connected"]: ... print(f"translateX is connected to: {{result['attributes']['translateX']['connections']}}")

Source code in src/maya_mcp/tools/connections.py
def connections_get(
    node: str,
    attributes: list[str] | None = None,
) -> ConnectionsGetOutput:
    """Get detailed connection information for specific attributes.

    Returns connection details for each specified attribute, including
    connected plugs, connection types, and lock status.

    Args:
        node: Node name to query.
        attributes: List of attribute names to check for connections.
            If None, checks all connectable attributes.

    Returns:
        Dictionary with:
            - node: The queried node name
            - attributes: Dict mapping attribute name to connection info
            - count: Number of attributes with connections
            - errors: Map of attribute to error message, or None

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.
        ValueError: If node name is invalid.

    Example:
        >>> result = connections_get("pCube1", ["translateX", "visibility"])
        >>> if result["attributes"]["translateX"]["connected"]:
        ...     print(f"translateX is connected to: {{result['attributes']['translateX']['connections']}}")
    """
    _validate_node_name(node)
    if attributes:
        for attr in attributes:
            _validate_attribute_name(attr)

    client = get_client()

    node_escaped = json.dumps(node)
    attrs_escaped = json.dumps(attributes) if attributes else "None"

    command = f"""
import maya.cmds as cmds
import json

node = {node_escaped}
attrs_to_check = {attrs_escaped}

result = {{"node": node, "attributes": {{}}, "count": 0, "errors": {{}}}}

if not cmds.objExists(node):
    result["errors"]["_node"] = f"Node '{{node}}' does not exist"
else:
    # If no specific attributes, get all connectable ones
    if attrs_to_check is None:
        attrs_to_check = cmds.listAttr(node, connectable=True) or []

    connected_count = 0

    for attr in attrs_to_check:
        try:
            full_attr = f"{{node}}.{{attr}}"

            if not cmds.attributeQuery(attr, node=node, exists=True):
                result["errors"][attr] = f"Attribute '{{attr}}' not found"
                continue

            attr_info = {{
                "attribute": attr,
                "connected": False,
                "connections": [],
                "locked": cmds.getAttr(full_attr, lock=True),
                "type": cmds.getAttr(full_attr, type=True)
            }}

            # Get incoming connections
            incoming = cmds.listConnections(full_attr, source=True, destination=False,
                                            plugs=True) or []
            for src_plug in incoming:
                src_node = src_plug.split(".")[0]
                attr_info["connections"].append({{
                    "source": src_plug,
                    "source_node": src_node,
                    "source_type": cmds.nodeType(src_node),
                    "destination": full_attr,
                    "direction": "incoming"
                }})

            # Get outgoing connections
            outgoing = cmds.listConnections(full_attr, source=False, destination=True,
                                            plugs=True) or []
            for dest_plug in outgoing:
                dest_node = dest_plug.split(".")[0]
                attr_info["connections"].append({{
                    "source": full_attr,
                    "destination": dest_plug,
                    "destination_node": dest_node,
                    "destination_type": cmds.nodeType(dest_node),
                    "direction": "outgoing"
                }})

            attr_info["connected"] = len(attr_info["connections"]) > 0
            if attr_info["connected"]:
                connected_count += 1

            result["attributes"][attr] = attr_info

        except Exception as e:
            result["errors"][attr] = str(e)

    result["count"] = connected_count

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    errors = parsed.get("errors") or {}
    if "_node" in errors:
        raise ValueError(errors["_node"])

    parsed["errors"] = errors if errors else None

    return cast("ConnectionsGetOutput", parsed)

connections_connect

connections_connect(source: str, destination: str, force: bool = False) -> ConnectionsConnectOutput

Connect two attributes in Maya.

Creates a connection from the source attribute to the destination attribute. Implements the disconnect-before-reconnect safety pattern when force=True.

PARAMETER DESCRIPTION
source

Source plug in "node.attribute" format.

TYPE: str

destination

Destination plug in "node.attribute" format.

TYPE: str

force

If True, disconnect any existing connection to the destination before creating the new connection. If False (default), the operation fails if destination is already connected.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
ConnectionsConnectOutput

Dictionary with: - connected: True if connection was created - source: The source plug - destination: The destination plug - disconnected: List of plugs that were disconnected (if force=True) - error: Error message if failed, or None

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

ValueError

If plug names are invalid.

Example

result = connections_connect("ramp1.outColor", "lambert1.color", force=True) print(f"Connected: {{result['connected']}}")

Source code in src/maya_mcp/tools/connections.py
def connections_connect(
    source: str,
    destination: str,
    force: bool = False,
) -> ConnectionsConnectOutput:
    """Connect two attributes in Maya.

    Creates a connection from the source attribute to the destination attribute.
    Implements the disconnect-before-reconnect safety pattern when force=True.

    Args:
        source: Source plug in "node.attribute" format.
        destination: Destination plug in "node.attribute" format.
        force: If True, disconnect any existing connection to the destination
            before creating the new connection. If False (default), the
            operation fails if destination is already connected.

    Returns:
        Dictionary with:
            - connected: True if connection was created
            - source: The source plug
            - destination: The destination plug
            - disconnected: List of plugs that were disconnected (if force=True)
            - error: Error message if failed, or None

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.
        ValueError: If plug names are invalid.

    Example:
        >>> result = connections_connect("ramp1.outColor", "lambert1.color", force=True)
        >>> print(f"Connected: {{result['connected']}}")
    """
    _validate_plug_name(source)
    _validate_plug_name(destination)

    client = get_client()

    source_escaped = json.dumps(source)
    dest_escaped = json.dumps(destination)
    force_str = "True" if force else "False"

    command = f"""
import maya.cmds as cmds
import json

source_plug = {source_escaped}
dest_plug = {dest_escaped}
force_disconnect = {force_str}

result = {{
    "connected": False,
    "source": source_plug,
    "destination": dest_plug,
    "disconnected": [],
    "error": None
}}

try:
    def _attribute_query_name(plug):
        attr_name = plug.split(".", 1)[1]
        return attr_name.split("[", 1)[0]

    # Validate source exists
    src_parts = source_plug.split(".")
    if len(src_parts) < 2:
        result["error"] = f"Invalid source plug format: '{{source_plug}}'. Expected 'node.attribute'"
    elif not cmds.objExists(src_parts[0]):
        result["error"] = f"Source node '{{src_parts[0]}}' does not exist"
    elif not cmds.attributeQuery(_attribute_query_name(source_plug), node=src_parts[0], exists=True):
        result["error"] = f"Source attribute '{{src_parts[1]}}' does not exist on '{{src_parts[0]}}'"

    # Validate destination exists
    dest_parts = dest_plug.split(".")
    if result["error"] is None:
        if len(dest_parts) < 2:
            result["error"] = f"Invalid destination plug format: '{{dest_plug}}'. Expected 'node.attribute'"
        elif not cmds.objExists(dest_parts[0]):
            result["error"] = f"Destination node '{{dest_parts[0]}}' does not exist"
        elif not cmds.attributeQuery(_attribute_query_name(dest_plug), node=dest_parts[0], exists=True):
            result["error"] = f"Destination attribute '{{dest_parts[1]}}' does not exist on '{{dest_parts[0]}}'"

    # Check if destination is locked
    if result["error"] is None:
        if cmds.getAttr(dest_plug, lock=True):
            result["error"] = f"Destination attribute '{{dest_plug}}' is locked"

    # Check for existing connections and handle force disconnect
    if result["error"] is None:
        existing_src = cmds.listConnections(dest_plug, source=True, destination=False,
                                            plugs=True) or []
        if existing_src:
            if force_disconnect:
                # Disconnect existing connections
                for existing in existing_src:
                    cmds.disconnectAttr(existing, dest_plug)
                    result["disconnected"].append(existing)
            else:
                result["error"] = f"Destination '{{dest_plug}}' is already connected to '{{existing_src[0]}}'. Use force=True to replace."

    # Create the connection
    if result["error"] is None:
        cmds.connectAttr(source_plug, dest_plug, force=False)
        result["connected"] = True

except Exception as e:
    result["error"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    return cast("ConnectionsConnectOutput", parsed)

connections_disconnect

connections_disconnect(source: str | None = None, destination: str | None = None) -> ConnectionsDisconnectOutput

Disconnect attributes in Maya.

If both source and destination are provided, disconnects that specific connection. If only source is provided, disconnects all outgoing connections from that plug. If only destination is provided, disconnects all incoming connections to that plug.

PARAMETER DESCRIPTION
source

Source plug in "node.attribute" format. If None, uses destination only.

TYPE: str | None DEFAULT: None

destination

Destination plug in "node.attribute" format. If None, uses source only.

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
ConnectionsDisconnectOutput

Dictionary with: - disconnected: List of disconnected connection pairs [source, destination] - count: Number of connections disconnected - error: Error message if failed, or None

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

ValueError

If plug names are invalid or neither source nor destination provided.

Example

Disconnect specific connection

result = connections_disconnect("ramp1.outColor", "lambert1.color")

Disconnect all incoming connections to an attribute

result = connections_disconnect(destination="pCube1.translateX")

Source code in src/maya_mcp/tools/connections.py
def connections_disconnect(
    source: str | None = None,
    destination: str | None = None,
) -> ConnectionsDisconnectOutput:
    """Disconnect attributes in Maya.

    If both source and destination are provided, disconnects that specific connection.
    If only source is provided, disconnects all outgoing connections from that plug.
    If only destination is provided, disconnects all incoming connections to that plug.

    Args:
        source: Source plug in "node.attribute" format. If None, uses destination only.
        destination: Destination plug in "node.attribute" format. If None, uses source only.

    Returns:
        Dictionary with:
            - disconnected: List of disconnected connection pairs [source, destination]
            - count: Number of connections disconnected
            - error: Error message if failed, or None

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.
        ValueError: If plug names are invalid or neither source nor destination provided.

    Example:
        >>> # Disconnect specific connection
        >>> result = connections_disconnect("ramp1.outColor", "lambert1.color")
        >>> # Disconnect all incoming connections to an attribute
        >>> result = connections_disconnect(destination="pCube1.translateX")
    """
    if source is None and destination is None:
        raise ValueError("At least one of source or destination must be provided")

    if source:
        _validate_plug_name(source)
    if destination:
        _validate_plug_name(destination)

    client = get_client()

    source_escaped = json.dumps(source) if source else "None"
    dest_escaped = json.dumps(destination) if destination else "None"

    command = f"""
import maya.cmds as cmds
import json

source_plug = {source_escaped}
dest_plug = {dest_escaped}

result = {{
    "disconnected": [],
    "count": 0,
    "error": None
}}

try:
    def _attribute_query_name(plug):
        attr_name = plug.split(".", 1)[1]
        return attr_name.split("[", 1)[0]

    if source_plug and dest_plug:
        # Disconnect specific connection
        src_parts = source_plug.split(".")
        dest_parts = dest_plug.split(".")

        # Validate both plugs exist
        if not cmds.objExists(src_parts[0]):
            result["error"] = f"Source node '{{src_parts[0]}}' does not exist"
        elif not cmds.attributeQuery(_attribute_query_name(source_plug), node=src_parts[0], exists=True):
            result["error"] = f"Source attribute '{{src_parts[1]}}' does not exist"
        elif not cmds.objExists(dest_parts[0]):
            result["error"] = f"Destination node '{{dest_parts[0]}}' does not exist"
        elif not cmds.attributeQuery(_attribute_query_name(dest_plug), node=dest_parts[0], exists=True):
            result["error"] = f"Destination attribute '{{dest_parts[1]}}' does not exist"
        else:
            # Check if connection exists
            connected = cmds.isConnected(source_plug, dest_plug)
            if connected:
                cmds.disconnectAttr(source_plug, dest_plug)
                result["disconnected"].append({{"source": source_plug, "destination": dest_plug}})
                result["count"] = 1
            else:
                result["error"] = f"No connection exists between '{{source_plug}}' and '{{dest_plug}}'"

    elif source_plug:
        # Disconnect all outgoing from source
        src_parts = source_plug.split(".")

        if not cmds.objExists(src_parts[0]):
            result["error"] = f"Source node '{{src_parts[0]}}' does not exist"
        elif not cmds.attributeQuery(_attribute_query_name(source_plug), node=src_parts[0], exists=True):
            result["error"] = f"Source attribute '{{src_parts[1]}}' does not exist"
        else:
            destinations = cmds.listConnections(source_plug, source=False, destination=True,
                                                plugs=True) or []
            for dest in destinations:
                cmds.disconnectAttr(source_plug, dest)
                result["disconnected"].append({{"source": source_plug, "destination": dest}})
            result["count"] = len(result["disconnected"])

    elif dest_plug:
        # Disconnect all incoming to destination
        dest_parts = dest_plug.split(".")

        if not cmds.objExists(dest_parts[0]):
            result["error"] = f"Destination node '{{dest_parts[0]}}' does not exist"
        elif not cmds.attributeQuery(_attribute_query_name(dest_plug), node=dest_parts[0], exists=True):
            result["error"] = f"Destination attribute '{{dest_parts[1]}}' does not exist"
        else:
            sources = cmds.listConnections(dest_plug, source=True, destination=False,
                                           plugs=True) or []
            for src in sources:
                cmds.disconnectAttr(src, dest_plug)
                result["disconnected"].append({{"source": src, "destination": dest_plug}})
            result["count"] = len(result["disconnected"])

except Exception as e:
    result["error"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    return cast("ConnectionsDisconnectOutput", parsed)

connections_history

connections_history(node: str, direction: Literal['input', 'output', 'both'] = 'input', depth: int = 10, limit: int | None = DEFAULT_CONNECTIONS_LIMIT) -> ConnectionsHistoryOutput

List construction/deformation history on a node.

Traverses the dependency graph to find upstream (input) or downstream (output) history nodes such as deformers, construction history, shaders, etc.

PARAMETER DESCRIPTION
node

Node name to query history for.

TYPE: str

direction

Direction to traverse: - "input" (default): Upstream history (construction/deformation inputs) - "output": Downstream history (what this node affects) - "both": Both directions

TYPE: Literal['input', 'output', 'both'] DEFAULT: 'input'

depth

Maximum depth to traverse. Default 10.

TYPE: int DEFAULT: 10

limit

Maximum number of history nodes to return. Default 500.

TYPE: int | None DEFAULT: DEFAULT_CONNECTIONS_LIMIT

RETURNS DESCRIPTION
ConnectionsHistoryOutput

Dictionary with: - node: The queried node name - history: List of history node info dicts with name, type, depth, direction - count: Number of history nodes returned - truncated: True if results were truncated - total_count: Total history nodes before limit

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

ValueError

If node name is invalid.

Example

result = connections_history("pCubeShape1", direction="input") for hist in result["history"]: ... print(f"{{hist['type']}}: {{hist['name']}} (depth {{hist['depth']}})")

Source code in src/maya_mcp/tools/connections.py
def connections_history(
    node: str,
    direction: Literal["input", "output", "both"] = "input",
    depth: int = 10,
    limit: int | None = DEFAULT_CONNECTIONS_LIMIT,
) -> ConnectionsHistoryOutput:
    """List construction/deformation history on a node.

    Traverses the dependency graph to find upstream (input) or downstream (output)
    history nodes such as deformers, construction history, shaders, etc.

    Args:
        node: Node name to query history for.
        direction: Direction to traverse:
            - "input" (default): Upstream history (construction/deformation inputs)
            - "output": Downstream history (what this node affects)
            - "both": Both directions
        depth: Maximum depth to traverse. Default 10.
        limit: Maximum number of history nodes to return. Default 500.

    Returns:
        Dictionary with:
            - node: The queried node name
            - history: List of history node info dicts with name, type, depth, direction
            - count: Number of history nodes returned
            - truncated: True if results were truncated
            - total_count: Total history nodes before limit

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.
        ValueError: If node name is invalid.

    Example:
        >>> result = connections_history("pCubeShape1", direction="input")
        >>> for hist in result["history"]:
        ...     print(f"{{hist['type']}}: {{hist['name']}} (depth {{hist['depth']}})")
    """
    _validate_node_name(node)

    client = get_client()

    node_escaped = json.dumps(node)
    direction_escaped = json.dumps(direction)
    depth_val = max(1, min(depth, 50))
    limit_val = limit if limit and limit > 0 else 0

    command = f"""
import maya.cmds as cmds
import json

node = {node_escaped}
direction = {direction_escaped}
max_depth = {depth_val}
limit = {limit_val}

result = {{"node": node, "history": [], "errors": {{}}}}

if not cmds.objExists(node):
    result["errors"]["_node"] = f"Node '{{node}}' does not exist"
else:
    all_history = []

    # Get upstream (input) history
    if direction in ("input", "both"):
        upstream = cmds.listHistory(node, future=False, levels=max_depth, pruneDagObjects=True) or []
        for i, hist_node in enumerate(upstream):
            if hist_node != node:
                all_history.append({{
                    "name": hist_node,
                    "type": cmds.nodeType(hist_node),
                    "depth": i + 1,
                    "direction": "input"
                }})

    # Get downstream (output) history
    if direction in ("output", "both"):
        downstream = cmds.listHistory(node, future=True, levels=max_depth, pruneDagObjects=True) or []
        for i, hist_node in enumerate(downstream):
            if hist_node != node:
                all_history.append({{
                    "name": hist_node,
                    "type": cmds.nodeType(hist_node),
                    "depth": i + 1,
                    "direction": "output"
                }})

    # Sort by depth, then by name
    all_history.sort(key=lambda x: (x["depth"], x["name"]))

    total_count = len(all_history)
    truncated = False

    if limit > 0 and total_count > limit:
        all_history = all_history[:limit]
        truncated = True

    result["history"] = all_history
    result["count"] = len(all_history)

    if truncated:
        result["truncated"] = True
        result["total_count"] = total_count

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    errors = parsed.get("errors") or {}
    if "_node" in errors:
        raise ValueError(errors["_node"])

    parsed["errors"] = errors if errors else None

    parsed = guard_response_size(parsed, list_key="history")

    return cast("ConnectionsHistoryOutput", parsed)

Mesh

mesh

Mesh tools for Maya MCP.

This module provides tools for querying mesh geometry and topology analysis.

MeshInfoOutput

Bases: TypedDict

Return payload for the mesh.info tool.

MeshVerticesOutput

Bases: _GuardedOutput

Return payload for the mesh.vertices tool.

MeshEvaluateOutput

Bases: _GuardedOutput

Return payload for the mesh.evaluate tool.

Check-specific component fields are present only when requested.

mesh_info

mesh_info(node: str) -> MeshInfoOutput

Get mesh statistics for a polygon mesh.

Returns vertex count, face count, edge count, bounding box, and UV set information.

PARAMETER DESCRIPTION
node

Name of the mesh node (transform or shape).

TYPE: str

RETURNS DESCRIPTION
MeshInfoOutput

Dictionary with mesh statistics: - node: The queried node name - exists: Whether the node exists - is_mesh: Whether the node is a mesh - vertex_count: Number of vertices - face_count: Number of faces - edge_count: Number of edges - uv_count: Number of UVs - has_uvs: Whether the mesh has UVs - uv_sets: List of UV set names - bounding_box: [min_x, min_y, min_z, max_x, max_y, max_z] - errors: Error details if any queries failed, or None

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

ValueError

If node name contains invalid characters.

Example

result = mesh_info("pCube1") print(f"Vertices: {result['vertex_count']}, Faces: {result['face_count']}")

Source code in src/maya_mcp/tools/mesh.py
def mesh_info(node: str) -> MeshInfoOutput:
    """Get mesh statistics for a polygon mesh.

    Returns vertex count, face count, edge count, bounding box,
    and UV set information.

    Args:
        node: Name of the mesh node (transform or shape).

    Returns:
        Dictionary with mesh statistics:
            - node: The queried node name
            - exists: Whether the node exists
            - is_mesh: Whether the node is a mesh
            - vertex_count: Number of vertices
            - face_count: Number of faces
            - edge_count: Number of edges
            - uv_count: Number of UVs
            - has_uvs: Whether the mesh has UVs
            - uv_sets: List of UV set names
            - bounding_box: [min_x, min_y, min_z, max_x, max_y, max_z]
            - errors: Error details if any queries failed, or None

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.
        ValueError: If node name contains invalid characters.

    Example:
        >>> result = mesh_info("pCube1")
        >>> print(f"Vertices: {result['vertex_count']}, Faces: {result['face_count']}")
    """
    _validate_node_name(node)

    client = get_client()
    node_escaped = json.dumps(node)

    command = f"""
import maya.cmds as cmds
import json

node = {node_escaped}
result = {{"node": node, "exists": False, "is_mesh": False, "errors": {{}}}}

try:
    if not cmds.objExists(node):
        result["errors"]["_node"] = "Node '" + node + "' does not exist"
    else:
        result["exists"] = True

        # Get shape node if transform was passed
        shapes = cmds.listRelatives(node, shapes=True, fullPath=False) or []
        if shapes:
            shape = shapes[0]
        else:
            shape = node

        node_type = cmds.nodeType(shape)
        if node_type != "mesh":
            result["errors"]["_mesh"] = "Node is not a mesh (type: " + node_type + ")"
        else:
            result["is_mesh"] = True
            result["shape"] = shape

            # Get counts
            result["vertex_count"] = cmds.polyEvaluate(shape, vertex=True)
            result["face_count"] = cmds.polyEvaluate(shape, face=True)
            result["edge_count"] = cmds.polyEvaluate(shape, edge=True)
            result["uv_count"] = cmds.polyEvaluate(shape, uvcoord=True)

            # UV sets
            uv_sets = cmds.polyUVSet(shape, query=True, allUVSets=True) or []
            result["uv_sets"] = uv_sets
            result["has_uvs"] = len(uv_sets) > 0 and result["uv_count"] > 0

            # Bounding box
            bbox = cmds.exactWorldBoundingBox(shape)
            result["bounding_box"] = bbox

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("MeshInfoOutput", parsed)

mesh_vertices

mesh_vertices(node: str, offset: int = 0, limit: int | None = DEFAULT_VERTEX_LIMIT) -> MeshVerticesOutput

Query vertex positions from a mesh with pagination.

Returns vertex positions as [x, y, z] tuples. Use offset/limit pagination for large meshes to avoid token budget issues.

PARAMETER DESCRIPTION
node

Name of the mesh node (transform or shape).

TYPE: str

offset

Starting vertex index (0-based).

TYPE: int DEFAULT: 0

limit

Maximum number of vertices to return. Default 1000. Use 0 for unlimited (use with caution).

TYPE: int | None DEFAULT: DEFAULT_VERTEX_LIMIT

RETURNS DESCRIPTION
MeshVerticesOutput

Dictionary with vertex data: - node: The queried node name - exists: Whether the node exists - is_mesh: Whether the node is a mesh - vertex_count: Total number of vertices in mesh - vertices: List of [x, y, z] position arrays - offset: The offset used - count: Number of vertices returned - truncated: True if results were truncated (only if limit hit) - errors: Error details if any, or None

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

ValueError

If node name contains invalid characters or offset is negative.

Example

result = mesh_vertices("pCube1", offset=0, limit=100) for i, v in enumerate(result['vertices']): ... print(f"v{result['offset'] + i}: {v}")

Source code in src/maya_mcp/tools/mesh.py
def mesh_vertices(
    node: str,
    offset: int = 0,
    limit: int | None = DEFAULT_VERTEX_LIMIT,
) -> MeshVerticesOutput:
    """Query vertex positions from a mesh with pagination.

    Returns vertex positions as [x, y, z] tuples. Use offset/limit
    pagination for large meshes to avoid token budget issues.

    Args:
        node: Name of the mesh node (transform or shape).
        offset: Starting vertex index (0-based).
        limit: Maximum number of vertices to return. Default 1000.
            Use 0 for unlimited (use with caution).

    Returns:
        Dictionary with vertex data:
            - node: The queried node name
            - exists: Whether the node exists
            - is_mesh: Whether the node is a mesh
            - vertex_count: Total number of vertices in mesh
            - vertices: List of [x, y, z] position arrays
            - offset: The offset used
            - count: Number of vertices returned
            - truncated: True if results were truncated (only if limit hit)
            - errors: Error details if any, or None

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.
        ValueError: If node name contains invalid characters or offset is negative.

    Example:
        >>> result = mesh_vertices("pCube1", offset=0, limit=100)
        >>> for i, v in enumerate(result['vertices']):
        ...     print(f"v{result['offset'] + i}: {v}")
    """
    _validate_node_name(node)
    if offset < 0:
        raise ValueError(f"offset must be non-negative, got {offset}")

    client = get_client()
    node_escaped = json.dumps(node)

    command = f"""
import maya.cmds as cmds
import json

node = {node_escaped}
offset = {offset}
limit = {limit}

result = {{"node": node, "exists": False, "is_mesh": False, "errors": {{}}}}

try:
    if not cmds.objExists(node):
        result["errors"]["_node"] = "Node '" + node + "' does not exist"
    else:
        result["exists"] = True

        # Get shape node if transform was passed
        shapes = cmds.listRelatives(node, shapes=True, fullPath=False) or []
        if shapes:
            shape = shapes[0]
        else:
            shape = node

        node_type = cmds.nodeType(shape)
        if node_type != "mesh":
            result["errors"]["_mesh"] = "Node is not a mesh (type: " + node_type + ")"
        else:
            result["is_mesh"] = True
            result["shape"] = shape

            # Get total vertex count
            total_count = cmds.polyEvaluate(shape, vertex=True)
            result["vertex_count"] = total_count

            # Calculate range
            start_idx = offset
            end_idx = total_count

            if limit and limit > 0:
                end_idx = min(offset + limit, total_count)

            # Get vertex positions
            vertices = []
            for i in range(start_idx, end_idx):
                pos = cmds.xform(shape + ".vtx[" + str(i) + "]", query=True, translation=True, worldSpace=True)
                vertices.append(pos)

            result["vertices"] = vertices
            result["offset"] = offset
            result["count"] = len(vertices)

            # Check truncation
            if limit and limit > 0 and total_count > offset + limit:
                result["truncated"] = True

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    # Apply response size guard
    if "vertices" in parsed:
        parsed = guard_response_size(parsed, list_key="vertices")

    return cast("MeshVerticesOutput", parsed)

mesh_evaluate

mesh_evaluate(node: str, checks: list[Literal['non_manifold', 'lamina', 'holes', 'border']] | None = None, limit: int | None = DEFAULT_TOPOLOGY_LIMIT) -> MeshEvaluateOutput

Analyze mesh topology for issues.

Performs topology analysis to find non-manifold edges, lamina faces, holes, and border edges. Returns lists of problematic components.

PARAMETER DESCRIPTION
node

Name of the mesh node (transform or shape).

TYPE: str

checks

List of checks to perform. Options: - "non_manifold": Find non-manifold edges - "lamina": Find lamina faces (faces sharing all edges) - "holes": Find faces with holes - "border": Find border edges If None, performs all checks.

TYPE: list[Literal['non_manifold', 'lamina', 'holes', 'border']] | None DEFAULT: None

limit

Maximum number of components to return per check. Default 500. Use 0 for unlimited.

TYPE: int | None DEFAULT: DEFAULT_TOPOLOGY_LIMIT

RETURNS DESCRIPTION
MeshEvaluateOutput

Dictionary with topology analysis: - node: The queried node name - exists: Whether the node exists - is_mesh: Whether the node is a mesh - non_manifold_edges: List of non-manifold edge names (if checked) - non_manifold_count: Count of non-manifold edges - lamina_faces: List of lamina face names (if checked) - lamina_count: Count of lamina faces - holes: List of faces with holes (if checked) - hole_count: Count of faces with holes - border_edges: List of border edge names (if checked) - border_count: Count of border edges - is_clean: True if mesh has no topology issues - errors: Error details if any, or None

RAISES DESCRIPTION
MayaUnavailableError

If not connected to Maya.

MayaCommandError

If Maya command execution fails.

ValueError

If node name contains invalid characters.

Example

result = mesh_evaluate("pCube1", checks=["non_manifold", "holes"]) if not result['is_clean']: ... print(f"Found {result['non_manifold_count']} non-manifold edges")

Source code in src/maya_mcp/tools/mesh.py
def mesh_evaluate(
    node: str,
    checks: list[Literal["non_manifold", "lamina", "holes", "border"]] | None = None,
    limit: int | None = DEFAULT_TOPOLOGY_LIMIT,
) -> MeshEvaluateOutput:
    """Analyze mesh topology for issues.

    Performs topology analysis to find non-manifold edges, lamina faces,
    holes, and border edges. Returns lists of problematic components.

    Args:
        node: Name of the mesh node (transform or shape).
        checks: List of checks to perform. Options:
            - "non_manifold": Find non-manifold edges
            - "lamina": Find lamina faces (faces sharing all edges)
            - "holes": Find faces with holes
            - "border": Find border edges
            If None, performs all checks.
        limit: Maximum number of components to return per check. Default 500.
            Use 0 for unlimited.

    Returns:
        Dictionary with topology analysis:
            - node: The queried node name
            - exists: Whether the node exists
            - is_mesh: Whether the node is a mesh
            - non_manifold_edges: List of non-manifold edge names (if checked)
            - non_manifold_count: Count of non-manifold edges
            - lamina_faces: List of lamina face names (if checked)
            - lamina_count: Count of lamina faces
            - holes: List of faces with holes (if checked)
            - hole_count: Count of faces with holes
            - border_edges: List of border edge names (if checked)
            - border_count: Count of border edges
            - is_clean: True if mesh has no topology issues
            - errors: Error details if any, or None

    Raises:
        MayaUnavailableError: If not connected to Maya.
        MayaCommandError: If Maya command execution fails.
        ValueError: If node name contains invalid characters.

    Example:
        >>> result = mesh_evaluate("pCube1", checks=["non_manifold", "holes"])
        >>> if not result['is_clean']:
        ...     print(f"Found {result['non_manifold_count']} non-manifold edges")
    """
    _validate_node_name(node)

    if checks is None:
        checks = ["non_manifold", "lamina", "holes", "border"]

    valid_checks = {"non_manifold", "lamina", "holes", "border"}
    for check in checks:
        if check not in valid_checks:
            raise ValueError(
                f"Invalid check: {check!r}. Must be one of: {', '.join(sorted(valid_checks))}"
            )

    client = get_client()
    node_escaped = json.dumps(node)
    checks_escaped = json.dumps(checks)

    command = f"""
import maya.cmds as cmds
import json

node = {node_escaped}
checks = {checks_escaped}
limit = {limit}

result = {{
    "node": node,
    "exists": False,
    "is_mesh": False,
    "is_clean": True,
    "errors": {{}}
}}

try:
    if not cmds.objExists(node):
        result["errors"]["_node"] = "Node '" + node + "' does not exist"
    else:
        result["exists"] = True

        # Get shape node if transform was passed
        shapes = cmds.listRelatives(node, shapes=True, fullPath=False) or []
        if shapes:
            shape = shapes[0]
        else:
            shape = node

        node_type = cmds.nodeType(shape)
        if node_type != "mesh":
            result["errors"]["_mesh"] = "Node is not a mesh (type: " + node_type + ")"
        else:
            result["is_mesh"] = True
            result["shape"] = shape

            # Non-manifold edges
            if "non_manifold" in checks:
                try:
                    nm_edges = cmds.polyInfo(shape, nonManifoldEdges=True) or []
                    # polyInfo returns strings like "EDGE 0 1 2\\n"
                    # Parse to get edge names
                    edge_list = []
                    for line in nm_edges:
                        parts = line.split()
                        for idx in parts[1:]:
                            edge_name = shape + ".e[" + idx + "]"
                            edge_list.append(edge_name)
                            if limit and limit > 0 and len(edge_list) >= limit:
                                break
                        if limit and limit > 0 and len(edge_list) >= limit:
                            break
                    result["non_manifold_edges"] = edge_list
                    result["non_manifold_count"] = len(edge_list)
                    if len(edge_list) > 0:
                        result["is_clean"] = False
                except Exception as e:
                    result["errors"]["non_manifold"] = str(e)

            # Lamina faces
            if "lamina" in checks:
                try:
                    lam_faces = cmds.polyInfo(shape, laminaFaces=True) or []
                    face_list = []
                    for line in lam_faces:
                        parts = line.split()
                        for idx in parts[1:]:
                            face_name = shape + ".f[" + idx + "]"
                            face_list.append(face_name)
                            if limit and limit > 0 and len(face_list) >= limit:
                                break
                        if limit and limit > 0 and len(face_list) >= limit:
                            break
                    result["lamina_faces"] = face_list
                    result["lamina_count"] = len(face_list)
                    if len(face_list) > 0:
                        result["is_clean"] = False
                except Exception as e:
                    result["errors"]["lamina"] = str(e)

            # Holes (using polySelectConstraint to find faces with holes)
            if "holes" in checks:
                try:
                    # Store current selection
                    orig_sel = cmds.ls(selection=True, flatten=True) or []

                    # Select all faces then filter with polySelectConstraint
                    cmds.select(shape + ".f[*]", replace=True)
                    cmds.polySelectConstraint(mode=3, type=8, holes=1)
                    holed_faces = cmds.ls(selection=True, flatten=True) or []

                    # Reset constraint and restore selection
                    cmds.polySelectConstraint(holes=0)
                    cmds.polySelectConstraint(disable=True)
                    if orig_sel:
                        cmds.select(orig_sel, replace=True)
                    else:
                        cmds.select(clear=True)

                    # Filter to only include faces from this shape
                    face_list = []
                    for face in holed_faces:
                        if shape in face and ".f[" in face:
                            face_list.append(face)
                            if limit and limit > 0 and len(face_list) >= limit:
                                break

                    result["holes"] = face_list
                    result["hole_count"] = len(face_list)
                    if len(face_list) > 0:
                        result["is_clean"] = False
                except Exception as e:
                    result["errors"]["holes"] = str(e)

            # Border edges (edges with only one adjacent face)
            if "border" in checks:
                try:
                    edge_to_face = cmds.polyInfo(shape, edgeToFace=True) or []
                    edge_list = []
                    for i, line in enumerate(edge_to_face):
                        # Format: "EDGE  0:  0  1\\n" (interior) or "EDGE  0:  0\\n" (border)
                        parts = line.split(":")
                        if len(parts) == 2:
                            faces = parts[1].strip().split()
                            if len(faces) == 1:
                                edge_list.append(shape + ".e[" + str(i) + "]")
                                if limit and limit > 0 and len(edge_list) >= limit:
                                    break

                    result["border_edges"] = edge_list
                    result["border_count"] = len(edge_list)
                    if len(edge_list) > 0:
                        result["is_clean"] = False
                except Exception as e:
                    result["errors"]["border"] = str(e)

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    # Apply response size guard for each list field
    for key in ["non_manifold_edges", "lamina_faces", "holes", "border_edges"]:
        if key in parsed:
            parsed = guard_response_size(parsed, list_key=key)

    return cast("MeshEvaluateOutput", parsed)

Viewport

viewport

Viewport capture tools for Maya MCP.

This module provides a read-only viewport capture tool that uses Maya playblast to write a temporary image file and returns it as inline MCP image content.

ViewportCaptureOutput

Bases: TypedDict

Structured metadata returned alongside viewport image content.

viewport_capture

viewport_capture(format: ViewportFormat = 'jpeg', width: int = 1024, height: int = 576, quality: int = 85, offscreen: bool = False, show_ornaments: bool = True, panel: str | None = None, frame: float | None = None) -> Image

Capture the Maya viewport as an inline MCP image.

Source code in src/maya_mcp/tools/viewport.py
def viewport_capture(
    format: ViewportFormat = "jpeg",
    width: int = 1024,
    height: int = 576,
    quality: int = 85,
    offscreen: bool = False,
    show_ornaments: bool = True,
    panel: str | None = None,
    frame: float | None = None,
) -> Image:
    """Capture the Maya viewport as an inline MCP image."""
    image, _ = _capture_viewport_image(
        format=format,
        width=width,
        height=height,
        quality=quality,
        offscreen=offscreen,
        show_ornaments=show_ornaments,
        panel=panel,
        frame=frame,
    )
    return image

Modeling

modeling

Polygon modeling tools for Maya MCP.

This module provides tools for creating polygon primitives, extruding, booleans, combining/separating meshes, beveling, bridging, edge loops, component manipulation, and cleanup operations.

ModelingCreatePolygonPrimitiveOutput

Bases: TypedDict

Return payload for the modeling.create_polygon_primitive tool.

ModelingExtrudeFacesOutput

Bases: TypedDict

Return payload for the modeling.extrude_faces tool.

ModelingBooleanOutput

Bases: TypedDict

Return payload for the modeling.boolean tool.

ModelingCombineOutput

Bases: TypedDict

Return payload for the modeling.combine tool.

ModelingSeparateOutput

Bases: _GuardedOutput

Return payload for the modeling.separate tool.

ModelingMergeVerticesOutput

Bases: TypedDict

Return payload for the modeling.merge_vertices tool.

ModelingDeleteHistoryOutput

Bases: _GuardedOutput

Return payload for the modeling.delete_history tool.

ModelingFreezeTransformsOutput

Bases: TypedDict

Return payload for the modeling.freeze_transforms tool.

ModelingCenterPivotOutput

Bases: TypedDict

Return payload for the modeling.center_pivot tool.

ModelingSetPivotOutput

Bases: TypedDict

Return payload for the modeling.set_pivot tool.

ModelingMoveComponentsOutput

Bases: TypedDict

Return payload for the modeling.move_components tool.

ModelingBevelOutput

Bases: TypedDict

Return payload for the modeling.bevel tool.

ModelingBridgeOutput

Bases: TypedDict

Return payload for the modeling.bridge tool.

ModelingInsertEdgeLoopOutput

Bases: TypedDict

Return payload for the modeling.insert_edge_loop tool.

ModelingDeleteFacesOutput

Bases: TypedDict

Return payload for the modeling.delete_faces tool.

modeling_create_polygon_primitive

modeling_create_polygon_primitive(primitive_type: Literal['cube', 'sphere', 'cylinder', 'cone', 'torus', 'plane'], name: str | None = None, width: float = 1.0, height: float = 1.0, depth: float = 1.0, radius: float = 0.5, subdivisions_width: int | None = None, subdivisions_height: int | None = None, subdivisions_depth: int | None = None, subdivisions_axis: int | None = None, axis: Literal['x', 'y', 'z'] = 'y') -> ModelingCreatePolygonPrimitiveOutput

Create a polygon primitive.

PARAMETER DESCRIPTION
primitive_type

Type of primitive to create.

TYPE: Literal['cube', 'sphere', 'cylinder', 'cone', 'torus', 'plane']

name

Optional name for the transform node.

TYPE: str | None DEFAULT: None

width

Width of the primitive (cube/plane).

TYPE: float DEFAULT: 1.0

height

Height of the primitive (cube/cylinder/cone).

TYPE: float DEFAULT: 1.0

depth

Depth of the primitive (cube).

TYPE: float DEFAULT: 1.0

radius

Radius of the primitive (sphere/cylinder/cone/torus).

TYPE: float DEFAULT: 0.5

subdivisions_width

Width subdivisions.

TYPE: int | None DEFAULT: None

subdivisions_height

Height subdivisions.

TYPE: int | None DEFAULT: None

subdivisions_depth

Depth subdivisions.

TYPE: int | None DEFAULT: None

subdivisions_axis

Axis subdivisions (sphere/cylinder/cone/torus).

TYPE: int | None DEFAULT: None

axis

Up axis for the primitive.

TYPE: Literal['x', 'y', 'z'] DEFAULT: 'y'

RETURNS DESCRIPTION
ModelingCreatePolygonPrimitiveOutput

Dictionary with transform, shape, constructor, primitive_type,

ModelingCreatePolygonPrimitiveOutput

vertex_count, face_count, and errors.

RAISES DESCRIPTION
ValueError

If primitive_type or axis is invalid, or name contains invalid characters.

Source code in src/maya_mcp/tools/modeling.py
def modeling_create_polygon_primitive(
    primitive_type: Literal["cube", "sphere", "cylinder", "cone", "torus", "plane"],
    name: str | None = None,
    width: float = 1.0,
    height: float = 1.0,
    depth: float = 1.0,
    radius: float = 0.5,
    subdivisions_width: int | None = None,
    subdivisions_height: int | None = None,
    subdivisions_depth: int | None = None,
    subdivisions_axis: int | None = None,
    axis: Literal["x", "y", "z"] = "y",
) -> ModelingCreatePolygonPrimitiveOutput:
    """Create a polygon primitive.

    Args:
        primitive_type: Type of primitive to create.
        name: Optional name for the transform node.
        width: Width of the primitive (cube/plane).
        height: Height of the primitive (cube/cylinder/cone).
        depth: Depth of the primitive (cube).
        radius: Radius of the primitive (sphere/cylinder/cone/torus).
        subdivisions_width: Width subdivisions.
        subdivisions_height: Height subdivisions.
        subdivisions_depth: Depth subdivisions.
        subdivisions_axis: Axis subdivisions (sphere/cylinder/cone/torus).
        axis: Up axis for the primitive.

    Returns:
        Dictionary with transform, shape, constructor, primitive_type,
        vertex_count, face_count, and errors.

    Raises:
        ValueError: If primitive_type or axis is invalid, or name contains
            invalid characters.
    """
    if primitive_type not in VALID_PRIMITIVES:
        raise ValueError(
            f"Invalid primitive_type: {primitive_type!r}. "
            f"Must be one of: {', '.join(sorted(VALID_PRIMITIVES))}"
        )
    if axis not in VALID_AXES:
        raise ValueError(f"Invalid axis: {axis!r}. Must be one of: {', '.join(sorted(VALID_AXES))}")
    if name is not None:
        _validate_node_name(name)

    client = get_client()
    name_escaped = json.dumps(name) if name is not None else "None"
    ptype_escaped = json.dumps(primitive_type)
    axis_escaped = json.dumps(axis)

    # Build optional kwargs for subdivisions
    subdiv_parts: list[str] = []
    if subdivisions_width is not None:
        subdiv_parts.append(f"    kwargs['subdivisionsWidth'] = {int(subdivisions_width)}")
    if subdivisions_height is not None:
        subdiv_parts.append(f"    kwargs['subdivisionsHeight'] = {int(subdivisions_height)}")
    if subdivisions_depth is not None:
        subdiv_parts.append(f"    kwargs['subdivisionsDepth'] = {int(subdivisions_depth)}")
    if subdivisions_axis is not None:
        subdiv_parts.append(f"    kwargs['subdivisionsAxis'] = {int(subdivisions_axis)}")
    subdiv_block = "\n".join(subdiv_parts) if subdiv_parts else "    pass"

    command = f"""
import maya.cmds as cmds
import json

ptype = {ptype_escaped}
name = {name_escaped}
width = {float(width)}
height = {float(height)}
depth = {float(depth)}
radius = {float(radius)}
axis_str = {axis_escaped}

axis_map = {{"x": [1, 0, 0], "y": [0, 1, 0], "z": [0, 0, 1]}}
ax = axis_map[axis_str]

result = {{"transform": None, "shape": None, "constructor_node": None, "primitive_type": ptype, "vertex_count": 0, "face_count": 0, "errors": {{}}}}

try:
    kwargs = {{}}
    if name:
        kwargs["name"] = name
{subdiv_block}

    if ptype == "cube":
        kwargs["width"] = width
        kwargs["height"] = height
        kwargs["depth"] = depth
        kwargs["axis"] = ax
        created = cmds.polyCube(**kwargs)
    elif ptype == "sphere":
        kwargs["radius"] = radius
        kwargs["axis"] = ax
        if "subdivisionsAxis" not in kwargs and "subdivisionsWidth" not in kwargs:
            pass
        created = cmds.polySphere(**kwargs)
    elif ptype == "cylinder":
        kwargs["radius"] = radius
        kwargs["height"] = height
        kwargs["axis"] = ax
        created = cmds.polyCylinder(**kwargs)
    elif ptype == "cone":
        kwargs["radius"] = radius
        kwargs["height"] = height
        kwargs["axis"] = ax
        created = cmds.polyCone(**kwargs)
    elif ptype == "torus":
        kwargs["radius"] = radius
        kwargs["axis"] = ax
        created = cmds.polyTorus(**kwargs)
    elif ptype == "plane":
        kwargs["width"] = width
        kwargs["height"] = height
        kwargs["axis"] = ax
        created = cmds.polyPlane(**kwargs)
    else:
        result["errors"]["_type"] = "Unknown primitive type: " + ptype
        created = None

    if created:
        transform = created[0]
        constructor = created[1] if len(created) > 1 else None
        result["transform"] = transform
        result["constructor_node"] = constructor

        shapes = cmds.listRelatives(transform, shapes=True, fullPath=False) or []
        if shapes:
            result["shape"] = shapes[0]

        result["vertex_count"] = cmds.polyEvaluate(transform, vertex=True)
        result["face_count"] = cmds.polyEvaluate(transform, face=True)

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("ModelingCreatePolygonPrimitiveOutput", parsed)

modeling_extrude_faces

modeling_extrude_faces(faces: list[str], local_translate_z: float | None = None, local_translate_x: float | None = None, local_translate_y: float | None = None, offset: float | None = None, divisions: int = 1, keep_faces_together: bool = True) -> ModelingExtrudeFacesOutput

Extrude polygon faces.

PARAMETER DESCRIPTION
faces

Component strings for faces to extrude (e.g., ['pCube1.f[0]']).

TYPE: list[str]

local_translate_z

Local Z translation (thickness/extrusion amount).

TYPE: float | None DEFAULT: None

local_translate_x

Local X translation.

TYPE: float | None DEFAULT: None

local_translate_y

Local Y translation.

TYPE: float | None DEFAULT: None

offset

Offset amount for the extrusion.

TYPE: float | None DEFAULT: None

divisions

Number of divisions for the extrusion.

TYPE: int DEFAULT: 1

keep_faces_together

Keep faces together during extrusion.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
ModelingExtrudeFacesOutput

Dictionary with node, faces_extruded, new_face_count, and errors.

RAISES DESCRIPTION
ValueError

If faces list is empty or contains invalid characters.

Source code in src/maya_mcp/tools/modeling.py
def modeling_extrude_faces(
    faces: list[str],
    local_translate_z: float | None = None,
    local_translate_x: float | None = None,
    local_translate_y: float | None = None,
    offset: float | None = None,
    divisions: int = 1,
    keep_faces_together: bool = True,
) -> ModelingExtrudeFacesOutput:
    """Extrude polygon faces.

    Args:
        faces: Component strings for faces to extrude (e.g., ['pCube1.f[0]']).
        local_translate_z: Local Z translation (thickness/extrusion amount).
        local_translate_x: Local X translation.
        local_translate_y: Local Y translation.
        offset: Offset amount for the extrusion.
        divisions: Number of divisions for the extrusion.
        keep_faces_together: Keep faces together during extrusion.

    Returns:
        Dictionary with node, faces_extruded, new_face_count, and errors.

    Raises:
        ValueError: If faces list is empty or contains invalid characters.
    """
    if not faces:
        raise ValueError("faces list cannot be empty")
    for face in faces:
        _validate_component_name(face)

    client = get_client()
    faces_escaped = json.dumps(faces)

    # Build optional kwargs
    kwarg_lines: list[str] = []
    if local_translate_z is not None:
        kwarg_lines.append(f"        kwargs['localTranslateZ'] = {float(local_translate_z)}")
    if local_translate_x is not None:
        kwarg_lines.append(f"        kwargs['localTranslateX'] = {float(local_translate_x)}")
    if local_translate_y is not None:
        kwarg_lines.append(f"        kwargs['localTranslateY'] = {float(local_translate_y)}")
    if offset is not None:
        kwarg_lines.append(f"        kwargs['offset'] = {float(offset)}")
    kwarg_block = "\n".join(kwarg_lines) if kwarg_lines else "        pass"

    kft_val = "True" if keep_faces_together else "False"

    command = f"""
import maya.cmds as cmds
import json

faces = {faces_escaped}
divisions = {int(divisions)}
keep_together = {kft_val}

result = {{"node": None, "faces_extruded": len(faces), "new_face_count": 0, "errors": {{}}}}

try:
    # Validate faces exist
    missing = [f for f in faces if not cmds.objExists(f)]
    if missing:
        result["errors"]["_faces"] = "Components do not exist: " + ", ".join(missing)
    else:
        kwargs = {{"divisions": divisions, "keepFacesTogether": keep_together}}
{kwarg_block}

        extrude_result = cmds.polyExtrudeFacet(faces, **kwargs)
        if extrude_result:
            result["node"] = extrude_result[0] if isinstance(extrude_result, list) else extrude_result

        # Get the mesh from the first face component
        mesh = faces[0].split(".")[0]
        result["new_face_count"] = cmds.polyEvaluate(mesh, face=True)

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("ModelingExtrudeFacesOutput", parsed)

modeling_boolean

modeling_boolean(mesh_a: str, mesh_b: str, operation: Literal['union', 'difference', 'intersection'] = 'union') -> ModelingBooleanOutput

Perform a boolean operation on two meshes.

PARAMETER DESCRIPTION
mesh_a

First mesh (the base mesh).

TYPE: str

mesh_b

Second mesh (the operand).

TYPE: str

operation

Boolean operation type.

TYPE: Literal['union', 'difference', 'intersection'] DEFAULT: 'union'

RETURNS DESCRIPTION
ModelingBooleanOutput

Dictionary with result_mesh, operation, vertex_count,

ModelingBooleanOutput

face_count, and errors.

RAISES DESCRIPTION
ValueError

If mesh names contain invalid characters or operation is invalid.

Source code in src/maya_mcp/tools/modeling.py
def modeling_boolean(
    mesh_a: str,
    mesh_b: str,
    operation: Literal["union", "difference", "intersection"] = "union",
) -> ModelingBooleanOutput:
    """Perform a boolean operation on two meshes.

    Args:
        mesh_a: First mesh (the base mesh).
        mesh_b: Second mesh (the operand).
        operation: Boolean operation type.

    Returns:
        Dictionary with result_mesh, operation, vertex_count,
        face_count, and errors.

    Raises:
        ValueError: If mesh names contain invalid characters or
            operation is invalid.
    """
    _validate_node_name(mesh_a)
    _validate_node_name(mesh_b)

    valid_ops = {"union", "difference", "intersection"}
    if operation not in valid_ops:
        raise ValueError(
            f"Invalid operation: {operation!r}. Must be one of: {', '.join(sorted(valid_ops))}"
        )

    op_map = {"union": 1, "difference": 2, "intersection": 3}

    client = get_client()
    mesh_a_escaped = json.dumps(mesh_a)
    mesh_b_escaped = json.dumps(mesh_b)
    op_int = op_map[operation]
    op_escaped = json.dumps(operation)

    command = f"""
import maya.cmds as cmds
import json

mesh_a = {mesh_a_escaped}
mesh_b = {mesh_b_escaped}
op_int = {op_int}
op_name = {op_escaped}

result = {{"result_mesh": None, "operation": op_name, "vertex_count": 0, "face_count": 0, "errors": {{}}}}

try:
    if not cmds.objExists(mesh_a):
        result["errors"]["_mesh_a"] = "Node '" + mesh_a + "' does not exist"
    elif not cmds.objExists(mesh_b):
        result["errors"]["_mesh_b"] = "Node '" + mesh_b + "' does not exist"
    else:
        bool_result = cmds.polyCBoolOp(mesh_a, mesh_b, operation=op_int, constructionHistory=True)
        if bool_result:
            result_mesh = bool_result[0]
            result["result_mesh"] = result_mesh
            result["vertex_count"] = cmds.polyEvaluate(result_mesh, vertex=True)
            result["face_count"] = cmds.polyEvaluate(result_mesh, face=True)

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("ModelingBooleanOutput", parsed)

modeling_combine

modeling_combine(meshes: list[str], name: str | None = None) -> ModelingCombineOutput

Combine multiple meshes into one.

PARAMETER DESCRIPTION
meshes

List of mesh names to combine (minimum 2).

TYPE: list[str]

name

Optional name for the combined mesh.

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
ModelingCombineOutput

Dictionary with result_mesh, source_meshes, vertex_count,

ModelingCombineOutput

face_count, and errors.

RAISES DESCRIPTION
ValueError

If meshes list has fewer than 2 entries or names contain invalid characters.

Source code in src/maya_mcp/tools/modeling.py
def modeling_combine(
    meshes: list[str],
    name: str | None = None,
) -> ModelingCombineOutput:
    """Combine multiple meshes into one.

    Args:
        meshes: List of mesh names to combine (minimum 2).
        name: Optional name for the combined mesh.

    Returns:
        Dictionary with result_mesh, source_meshes, vertex_count,
        face_count, and errors.

    Raises:
        ValueError: If meshes list has fewer than 2 entries or names
            contain invalid characters.
    """
    if len(meshes) < 2:
        raise ValueError("meshes list must contain at least 2 meshes")
    for mesh in meshes:
        _validate_node_name(mesh)
    if name is not None:
        _validate_node_name(name)

    client = get_client()
    meshes_escaped = json.dumps(meshes)
    name_escaped = json.dumps(name) if name is not None else "None"

    command = f"""
import maya.cmds as cmds
import json

meshes = {meshes_escaped}
name = {name_escaped}

result = {{"result_mesh": None, "source_meshes": meshes, "vertex_count": 0, "face_count": 0, "errors": {{}}}}

try:
    missing = [m for m in meshes if not cmds.objExists(m)]
    if missing:
        result["errors"]["_meshes"] = "Nodes do not exist: " + ", ".join(missing)
    else:
        combine_result = cmds.polyUnite(meshes, constructionHistory=True)
        if combine_result:
            result_mesh = combine_result[0]
            if name:
                result_mesh = cmds.rename(result_mesh, name)
            result["result_mesh"] = result_mesh
            result["vertex_count"] = cmds.polyEvaluate(result_mesh, vertex=True)
            result["face_count"] = cmds.polyEvaluate(result_mesh, face=True)

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("ModelingCombineOutput", parsed)

modeling_separate

modeling_separate(mesh: str) -> ModelingSeparateOutput

Separate a combined mesh into individual meshes.

PARAMETER DESCRIPTION
mesh

Name of the mesh to separate.

TYPE: str

RETURNS DESCRIPTION
ModelingSeparateOutput

Dictionary with source_mesh, result_meshes, count, and errors.

RAISES DESCRIPTION
ValueError

If mesh name contains invalid characters.

Source code in src/maya_mcp/tools/modeling.py
def modeling_separate(
    mesh: str,
) -> ModelingSeparateOutput:
    """Separate a combined mesh into individual meshes.

    Args:
        mesh: Name of the mesh to separate.

    Returns:
        Dictionary with source_mesh, result_meshes, count, and errors.

    Raises:
        ValueError: If mesh name contains invalid characters.
    """
    _validate_node_name(mesh)

    client = get_client()
    mesh_escaped = json.dumps(mesh)

    command = f"""
import maya.cmds as cmds
import json

mesh = {mesh_escaped}

result = {{"source_mesh": mesh, "result_meshes": [], "count": 0, "errors": {{}}}}

try:
    if not cmds.objExists(mesh):
        result["errors"]["_mesh"] = "Node '" + mesh + "' does not exist"
    else:
        sep_result = cmds.polySeparate(mesh, constructionHistory=False)
        if sep_result:
            # polySeparate returns transforms under a new group
            result["result_meshes"] = sep_result
            result["count"] = len(sep_result)

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    if "result_meshes" in parsed:
        parsed = guard_response_size(parsed, list_key="result_meshes")

    return cast("ModelingSeparateOutput", parsed)

modeling_merge_vertices

modeling_merge_vertices(mesh: str, threshold: float = 0.001, vertices: list[str] | None = None) -> ModelingMergeVerticesOutput

Merge vertices on a mesh within a distance threshold.

PARAMETER DESCRIPTION
mesh

Name of the mesh.

TYPE: str

threshold

Distance threshold for merging (default 0.001).

TYPE: float DEFAULT: 0.001

vertices

Optional list of specific vertex components to merge. If None, merges all vertices on the mesh within threshold.

TYPE: list[str] | None DEFAULT: None

RETURNS DESCRIPTION
ModelingMergeVerticesOutput

Dictionary with mesh, vertices_merged, vertex_count_before,

ModelingMergeVerticesOutput

vertex_count_after, and errors.

RAISES DESCRIPTION
ValueError

If mesh name or vertex components contain invalid characters.

Source code in src/maya_mcp/tools/modeling.py
def modeling_merge_vertices(
    mesh: str,
    threshold: float = 0.001,
    vertices: list[str] | None = None,
) -> ModelingMergeVerticesOutput:
    """Merge vertices on a mesh within a distance threshold.

    Args:
        mesh: Name of the mesh.
        threshold: Distance threshold for merging (default 0.001).
        vertices: Optional list of specific vertex components to merge.
            If None, merges all vertices on the mesh within threshold.

    Returns:
        Dictionary with mesh, vertices_merged, vertex_count_before,
        vertex_count_after, and errors.

    Raises:
        ValueError: If mesh name or vertex components contain invalid characters.
    """
    _validate_node_name(mesh)
    if vertices is not None:
        for vtx in vertices:
            _validate_component_name(vtx)

    client = get_client()
    mesh_escaped = json.dumps(mesh)
    vertices_escaped = json.dumps(vertices) if vertices is not None else "None"

    command = f"""
import maya.cmds as cmds
import json

mesh = {mesh_escaped}
threshold = {float(threshold)}
vertices = {vertices_escaped}

result = {{"mesh": mesh, "vertices_merged": 0, "vertex_count_before": 0, "vertex_count_after": 0, "errors": {{}}}}

try:
    if not cmds.objExists(mesh):
        result["errors"]["_mesh"] = "Node '" + mesh + "' does not exist"
    else:
        result["vertex_count_before"] = cmds.polyEvaluate(mesh, vertex=True)

        if vertices:
            cmds.polyMergeVertex(vertices, distance=threshold, constructionHistory=True)
        else:
            cmds.polyMergeVertex(mesh, distance=threshold, constructionHistory=True)

        result["vertex_count_after"] = cmds.polyEvaluate(mesh, vertex=True)
        result["vertices_merged"] = result["vertex_count_before"] - result["vertex_count_after"]

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("ModelingMergeVerticesOutput", parsed)

modeling_delete_history

modeling_delete_history(nodes: list[str] | None = None, all_nodes: bool = False) -> ModelingDeleteHistoryOutput

Delete construction history from nodes.

PARAMETER DESCRIPTION
nodes

List of node names to delete history from.

TYPE: list[str] | None DEFAULT: None

all_nodes

If True, delete history from all nodes in the scene. If True, the nodes parameter is ignored.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
ModelingDeleteHistoryOutput

Dictionary with cleaned list, count, and errors.

RAISES DESCRIPTION
ValueError

If neither nodes nor all_nodes is specified, or node names contain invalid characters.

Source code in src/maya_mcp/tools/modeling.py
def modeling_delete_history(
    nodes: list[str] | None = None,
    all_nodes: bool = False,
) -> ModelingDeleteHistoryOutput:
    """Delete construction history from nodes.

    Args:
        nodes: List of node names to delete history from.
        all_nodes: If True, delete history from all nodes in the scene.
            If True, the nodes parameter is ignored.

    Returns:
        Dictionary with cleaned list, count, and errors.

    Raises:
        ValueError: If neither nodes nor all_nodes is specified, or
            node names contain invalid characters.
    """
    if not all_nodes and not nodes:
        raise ValueError("Either nodes must be provided or all_nodes must be True")
    if nodes is not None:
        for node in nodes:
            _validate_node_name(node)

    client = get_client()
    nodes_escaped = json.dumps(nodes) if nodes is not None else "None"
    all_val = "True" if all_nodes else "False"

    command = f"""
import maya.cmds as cmds
import json

nodes = {nodes_escaped}
all_nodes = {all_val}

result = {{"cleaned": [], "count": 0, "errors": {{}}}}

try:
    if all_nodes:
        all_transforms = cmds.ls(type="transform") or []
        # Filter to only DAG transforms with shapes (meshes, etc.)
        targets = []
        for t in all_transforms:
            shapes = cmds.listRelatives(t, shapes=True) or []
            if shapes:
                targets.append(t)
        if targets:
            cmds.delete(targets, constructionHistory=True)
        result["cleaned"] = targets
        result["count"] = len(targets)
    else:
        missing = [n for n in nodes if not cmds.objExists(n)]
        if missing:
            result["errors"]["_nodes"] = "Nodes do not exist: " + ", ".join(missing)
        else:
            cmds.delete(nodes, constructionHistory=True)
            result["cleaned"] = nodes
            result["count"] = len(nodes)

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    if "cleaned" in parsed:
        parsed = guard_response_size(parsed, list_key="cleaned")

    return cast("ModelingDeleteHistoryOutput", parsed)

modeling_freeze_transforms

modeling_freeze_transforms(nodes: list[str], translate: bool = True, rotate: bool = True, scale: bool = True) -> ModelingFreezeTransformsOutput

Freeze (reset) transforms on nodes.

Applies the current transform values as the identity and resets the transform channels to zero/one.

PARAMETER DESCRIPTION
nodes

List of node names to freeze.

TYPE: list[str]

translate

Freeze translation (default True).

TYPE: bool DEFAULT: True

rotate

Freeze rotation (default True).

TYPE: bool DEFAULT: True

scale

Freeze scale (default True).

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
ModelingFreezeTransformsOutput

Dictionary with frozen list, count, and errors.

RAISES DESCRIPTION
ValueError

If nodes list is empty or names contain invalid characters.

Source code in src/maya_mcp/tools/modeling.py
def modeling_freeze_transforms(
    nodes: list[str],
    translate: bool = True,
    rotate: bool = True,
    scale: bool = True,
) -> ModelingFreezeTransformsOutput:
    """Freeze (reset) transforms on nodes.

    Applies the current transform values as the identity and resets
    the transform channels to zero/one.

    Args:
        nodes: List of node names to freeze.
        translate: Freeze translation (default True).
        rotate: Freeze rotation (default True).
        scale: Freeze scale (default True).

    Returns:
        Dictionary with frozen list, count, and errors.

    Raises:
        ValueError: If nodes list is empty or names contain invalid characters.
    """
    if not nodes:
        raise ValueError("nodes list cannot be empty")
    for node in nodes:
        _validate_node_name(node)

    client = get_client()
    nodes_escaped = json.dumps(nodes)
    t_val = "True" if translate else "False"
    r_val = "True" if rotate else "False"
    s_val = "True" if scale else "False"

    command = f"""
import maya.cmds as cmds
import json

nodes = {nodes_escaped}
do_translate = {t_val}
do_rotate = {r_val}
do_scale = {s_val}

result = {{"frozen": [], "count": 0, "errors": {{}}}}

try:
    missing = [n for n in nodes if not cmds.objExists(n)]
    if missing:
        result["errors"]["_nodes"] = "Nodes do not exist: " + ", ".join(missing)
    else:
        cmds.makeIdentity(nodes, apply=True, translate=do_translate, rotate=do_rotate, scale=do_scale)
        result["frozen"] = nodes
        result["count"] = len(nodes)

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("ModelingFreezeTransformsOutput", parsed)

modeling_center_pivot

modeling_center_pivot(nodes: list[str]) -> ModelingCenterPivotOutput

Center the pivot point on nodes.

PARAMETER DESCRIPTION
nodes

List of node names to center pivots on.

TYPE: list[str]

RETURNS DESCRIPTION
ModelingCenterPivotOutput

Dictionary with centered list, count, pivot_positions, and errors.

RAISES DESCRIPTION
ValueError

If nodes list is empty or names contain invalid characters.

Source code in src/maya_mcp/tools/modeling.py
def modeling_center_pivot(
    nodes: list[str],
) -> ModelingCenterPivotOutput:
    """Center the pivot point on nodes.

    Args:
        nodes: List of node names to center pivots on.

    Returns:
        Dictionary with centered list, count, pivot_positions, and errors.

    Raises:
        ValueError: If nodes list is empty or names contain invalid characters.
    """
    if not nodes:
        raise ValueError("nodes list cannot be empty")
    for node in nodes:
        _validate_node_name(node)

    client = get_client()
    nodes_escaped = json.dumps(nodes)

    command = f"""
import maya.cmds as cmds
import json

nodes = {nodes_escaped}

result = {{"centered": [], "count": 0, "pivot_positions": {{}}, "errors": {{}}}}

try:
    missing = [n for n in nodes if not cmds.objExists(n)]
    if missing:
        result["errors"]["_nodes"] = "Nodes do not exist: " + ", ".join(missing)
    else:
        for node in nodes:
            cmds.xform(node, centerPivots=True)
            piv = cmds.xform(node, query=True, pivots=True, worldSpace=True)
            result["pivot_positions"][node] = piv[:3] if piv else [0, 0, 0]
        result["centered"] = nodes
        result["count"] = len(nodes)

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("ModelingCenterPivotOutput", parsed)

modeling_set_pivot

modeling_set_pivot(node: str, position: list[float], world_space: bool = True) -> ModelingSetPivotOutput

Set the pivot point of a node to an explicit position.

PARAMETER DESCRIPTION
node

Node name to set pivot on.

TYPE: str

position

[x, y, z] position for the pivot.

TYPE: list[float]

world_space

If True, position is in world space (default True).

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
ModelingSetPivotOutput

Dictionary with node, pivot, world_space, and errors.

RAISES DESCRIPTION
ValueError

If node name contains invalid characters or position is not a list of 3 floats.

Source code in src/maya_mcp/tools/modeling.py
def modeling_set_pivot(
    node: str,
    position: list[float],
    world_space: bool = True,
) -> ModelingSetPivotOutput:
    """Set the pivot point of a node to an explicit position.

    Args:
        node: Node name to set pivot on.
        position: [x, y, z] position for the pivot.
        world_space: If True, position is in world space (default True).

    Returns:
        Dictionary with node, pivot, world_space, and errors.

    Raises:
        ValueError: If node name contains invalid characters or position
            is not a list of 3 floats.
    """
    _validate_node_name(node)
    if not isinstance(position, list) or len(position) != 3:
        raise ValueError("position must be a list of 3 floats [x, y, z]")

    client = get_client()
    node_escaped = json.dumps(node)
    pos_escaped = json.dumps([float(p) for p in position])
    ws_val = "True" if world_space else "False"

    command = f"""
import maya.cmds as cmds
import json

node = {node_escaped}
position = {pos_escaped}
world_space = {ws_val}

result = {{"node": node, "pivot": position, "world_space": world_space, "errors": {{}}}}

try:
    if not cmds.objExists(node):
        result["errors"]["_node"] = "Node '" + node + "' does not exist"
    else:
        cmds.xform(node, pivots=position, worldSpace=world_space)
        # Read back the actual pivot
        piv = cmds.xform(node, query=True, pivots=True, worldSpace=world_space)
        result["pivot"] = piv[:3] if piv else position

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("ModelingSetPivotOutput", parsed)

modeling_move_components

modeling_move_components(components: list[str], translate: list[float] | None = None, absolute: list[float] | None = None, world_space: bool = True) -> ModelingMoveComponentsOutput

Move mesh components (vertices, edges, faces).

Exactly one of translate (relative) or absolute must be provided.

PARAMETER DESCRIPTION
components

Component strings (e.g., ['pCube1.vtx[0:3]']).

TYPE: list[str]

translate

Relative [x, y, z] translation.

TYPE: list[float] | None DEFAULT: None

absolute

Absolute [x, y, z] position.

TYPE: list[float] | None DEFAULT: None

world_space

Use world space coordinates (default True).

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
ModelingMoveComponentsOutput

Dictionary with components_moved, translate or absolute,

ModelingMoveComponentsOutput

world_space, and errors.

RAISES DESCRIPTION
ValueError

If components are empty, both or neither of translate/absolute provided, or values are invalid.

Source code in src/maya_mcp/tools/modeling.py
def modeling_move_components(
    components: list[str],
    translate: list[float] | None = None,
    absolute: list[float] | None = None,
    world_space: bool = True,
) -> ModelingMoveComponentsOutput:
    """Move mesh components (vertices, edges, faces).

    Exactly one of translate (relative) or absolute must be provided.

    Args:
        components: Component strings (e.g., ['pCube1.vtx[0:3]']).
        translate: Relative [x, y, z] translation.
        absolute: Absolute [x, y, z] position.
        world_space: Use world space coordinates (default True).

    Returns:
        Dictionary with components_moved, translate or absolute,
        world_space, and errors.

    Raises:
        ValueError: If components are empty, both or neither of
            translate/absolute provided, or values are invalid.
    """
    if not components:
        raise ValueError("components list cannot be empty")
    for comp in components:
        _validate_component_name(comp)

    if translate is not None and absolute is not None:
        raise ValueError("Only one of translate or absolute can be provided, not both")
    if translate is None and absolute is None:
        raise ValueError("Either translate or absolute must be provided")

    if translate is not None and (not isinstance(translate, list) or len(translate) != 3):
        raise ValueError("translate must be a list of 3 floats [x, y, z]")
    if absolute is not None and (not isinstance(absolute, list) or len(absolute) != 3):
        raise ValueError("absolute must be a list of 3 floats [x, y, z]")

    client = get_client()
    components_escaped = json.dumps(components)
    ws_val = "True" if world_space else "False"

    if translate is not None:
        move_vals = json.dumps([float(v) for v in translate])
        mode = "relative"
    else:
        assert absolute is not None
        move_vals = json.dumps([float(v) for v in absolute])
        mode = "absolute"

    mode_escaped = json.dumps(mode)

    command = f"""
import maya.cmds as cmds
import json

components = {components_escaped}
move_vals = {move_vals}
mode = {mode_escaped}
world_space = {ws_val}

result = {{"components_moved": len(components), "world_space": world_space, "errors": {{}}}}

try:
    missing = [c for c in components if not cmds.objExists(c)]
    if missing:
        result["errors"]["_components"] = "Components do not exist: " + ", ".join(missing)
    else:
        if mode == "relative":
            result["translate"] = move_vals
            cmds.move(move_vals[0], move_vals[1], move_vals[2], components, relative=True, worldSpace=world_space)
        else:
            result["absolute"] = move_vals
            cmds.move(move_vals[0], move_vals[1], move_vals[2], components, absolute=True, worldSpace=world_space)

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("ModelingMoveComponentsOutput", parsed)

modeling_bevel

modeling_bevel(components: list[str], offset: float = 0.5, segments: int = 1, fraction: float = 0.5) -> ModelingBevelOutput

Bevel edges or vertices.

PARAMETER DESCRIPTION
components

Edge or vertex component strings to bevel.

TYPE: list[str]

offset

Bevel offset distance (default 0.5).

TYPE: float DEFAULT: 0.5

segments

Number of bevel segments (default 1).

TYPE: int DEFAULT: 1

fraction

Bevel fraction (default 0.5).

TYPE: float DEFAULT: 0.5

RETURNS DESCRIPTION
ModelingBevelOutput

Dictionary with node, components_beveled, new_vertex_count,

ModelingBevelOutput

new_face_count, and errors.

RAISES DESCRIPTION
ValueError

If components list is empty or contains invalid characters.

Source code in src/maya_mcp/tools/modeling.py
def modeling_bevel(
    components: list[str],
    offset: float = 0.5,
    segments: int = 1,
    fraction: float = 0.5,
) -> ModelingBevelOutput:
    """Bevel edges or vertices.

    Args:
        components: Edge or vertex component strings to bevel.
        offset: Bevel offset distance (default 0.5).
        segments: Number of bevel segments (default 1).
        fraction: Bevel fraction (default 0.5).

    Returns:
        Dictionary with node, components_beveled, new_vertex_count,
        new_face_count, and errors.

    Raises:
        ValueError: If components list is empty or contains invalid characters.
    """
    if not components:
        raise ValueError("components list cannot be empty")
    for comp in components:
        _validate_component_name(comp)

    client = get_client()
    components_escaped = json.dumps(components)

    command = f"""
import maya.cmds as cmds
import json

components = {components_escaped}
offset_val = {float(offset)}
segments = {int(segments)}
fraction = {float(fraction)}

result = {{"node": None, "components_beveled": len(components), "new_vertex_count": 0, "new_face_count": 0, "errors": {{}}}}

try:
    missing = [c for c in components if not cmds.objExists(c)]
    if missing:
        result["errors"]["_components"] = "Components do not exist: " + ", ".join(missing)
    else:
        bevel_result = cmds.polyBevel3(components, offset=offset_val, segments=segments, fraction=fraction)
        if bevel_result:
            result["node"] = bevel_result[0] if isinstance(bevel_result, list) else bevel_result

        # Get mesh name from component
        mesh = components[0].split(".")[0]
        result["new_vertex_count"] = cmds.polyEvaluate(mesh, vertex=True)
        result["new_face_count"] = cmds.polyEvaluate(mesh, face=True)

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("ModelingBevelOutput", parsed)

modeling_bridge

modeling_bridge(edge_loops: list[str], divisions: int = 0, twist: int = 0, taper: float = 1.0) -> ModelingBridgeOutput

Bridge between edge loops.

PARAMETER DESCRIPTION
edge_loops

Edge component strings for the edge loops to bridge.

TYPE: list[str]

divisions

Number of divisions in the bridge (default 0).

TYPE: int DEFAULT: 0

twist

Twist amount (default 0).

TYPE: int DEFAULT: 0

taper

Taper amount (default 1.0).

TYPE: float DEFAULT: 1.0

RETURNS DESCRIPTION
ModelingBridgeOutput

Dictionary with node, new_face_count, and errors.

RAISES DESCRIPTION
ValueError

If edge_loops list is empty or contains invalid characters.

Source code in src/maya_mcp/tools/modeling.py
def modeling_bridge(
    edge_loops: list[str],
    divisions: int = 0,
    twist: int = 0,
    taper: float = 1.0,
) -> ModelingBridgeOutput:
    """Bridge between edge loops.

    Args:
        edge_loops: Edge component strings for the edge loops to bridge.
        divisions: Number of divisions in the bridge (default 0).
        twist: Twist amount (default 0).
        taper: Taper amount (default 1.0).

    Returns:
        Dictionary with node, new_face_count, and errors.

    Raises:
        ValueError: If edge_loops list is empty or contains invalid characters.
    """
    if not edge_loops:
        raise ValueError("edge_loops list cannot be empty")
    for edge in edge_loops:
        _validate_component_name(edge)

    client = get_client()
    edges_escaped = json.dumps(edge_loops)

    command = f"""
import maya.cmds as cmds
import json

edges = {edges_escaped}
divisions = {int(divisions)}
twist = {int(twist)}
taper = {float(taper)}

result = {{"node": None, "new_face_count": 0, "errors": {{}}}}

try:
    missing = [e for e in edges if not cmds.objExists(e)]
    if missing:
        result["errors"]["_edges"] = "Components do not exist: " + ", ".join(missing)
    else:
        cmds.select(edges, replace=True)
        bridge_result = cmds.polyBridgeEdge(divisions=divisions, twist=twist, taper=taper)
        if bridge_result:
            result["node"] = bridge_result[0] if isinstance(bridge_result, list) else bridge_result

        mesh = edges[0].split(".")[0]
        result["new_face_count"] = cmds.polyEvaluate(mesh, face=True)

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("ModelingBridgeOutput", parsed)

modeling_insert_edge_loop

modeling_insert_edge_loop(edge: str, divisions: int = 1, weight: float = 0.5) -> ModelingInsertEdgeLoopOutput

Insert an edge loop at the specified edge.

PARAMETER DESCRIPTION
edge

Single edge component (e.g., 'pCube1.e[4]').

TYPE: str

divisions

Number of edge loops to insert (default 1).

TYPE: int DEFAULT: 1

weight

Position weight along the edge (0-1, default 0.5).

TYPE: float DEFAULT: 0.5

RETURNS DESCRIPTION
ModelingInsertEdgeLoopOutput

Dictionary with node, edge, new_edge_count,

ModelingInsertEdgeLoopOutput

new_vertex_count, and errors.

RAISES DESCRIPTION
ValueError

If edge contains invalid characters.

Source code in src/maya_mcp/tools/modeling.py
def modeling_insert_edge_loop(
    edge: str,
    divisions: int = 1,
    weight: float = 0.5,
) -> ModelingInsertEdgeLoopOutput:
    """Insert an edge loop at the specified edge.

    Args:
        edge: Single edge component (e.g., 'pCube1.e[4]').
        divisions: Number of edge loops to insert (default 1).
        weight: Position weight along the edge (0-1, default 0.5).

    Returns:
        Dictionary with node, edge, new_edge_count,
        new_vertex_count, and errors.

    Raises:
        ValueError: If edge contains invalid characters.
    """
    _validate_component_name(edge)

    client = get_client()
    edge_escaped = json.dumps(edge)

    command = f"""
import maya.cmds as cmds
import json

edge = {edge_escaped}
divisions = {int(divisions)}
weight = {float(weight)}

result = {{"node": None, "edge": edge, "new_edge_count": 0, "new_vertex_count": 0, "errors": {{}}}}

try:
    if not cmds.objExists(edge):
        result["errors"]["_edge"] = "Component '" + edge + "' does not exist"
    else:
        split_result = cmds.polySplitRing(edge, divisions=divisions, weight=weight)
        if split_result:
            result["node"] = split_result[0] if isinstance(split_result, list) else split_result

        mesh = edge.split(".")[0]
        result["new_edge_count"] = cmds.polyEvaluate(mesh, edge=True)
        result["new_vertex_count"] = cmds.polyEvaluate(mesh, vertex=True)

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("ModelingInsertEdgeLoopOutput", parsed)

modeling_delete_faces

modeling_delete_faces(faces: list[str]) -> ModelingDeleteFacesOutput

Delete polygon faces from a mesh.

PARAMETER DESCRIPTION
faces

Component strings for faces to delete (e.g., ['pCube1.f[0]']).

TYPE: list[str]

RETURNS DESCRIPTION
ModelingDeleteFacesOutput

Dictionary with faces_deleted, mesh, remaining_face_count, and errors.

RAISES DESCRIPTION
ValueError

If faces list is empty or contains invalid characters.

Source code in src/maya_mcp/tools/modeling.py
def modeling_delete_faces(
    faces: list[str],
) -> ModelingDeleteFacesOutput:
    """Delete polygon faces from a mesh.

    Args:
        faces: Component strings for faces to delete (e.g., ['pCube1.f[0]']).

    Returns:
        Dictionary with faces_deleted, mesh, remaining_face_count, and errors.

    Raises:
        ValueError: If faces list is empty or contains invalid characters.
    """
    if not faces:
        raise ValueError("faces list cannot be empty")
    for face in faces:
        _validate_component_name(face)

    client = get_client()
    faces_escaped = json.dumps(faces)

    command = f"""
import maya.cmds as cmds
import json

faces = {faces_escaped}

result = {{"faces_deleted": len(faces), "mesh": None, "remaining_face_count": 0, "errors": {{}}}}

try:
    missing = [f for f in faces if not cmds.objExists(f)]
    if missing:
        result["errors"]["_faces"] = "Components do not exist: " + ", ".join(missing)
    else:
        mesh = faces[0].split(".")[0]
        result["mesh"] = mesh
        cmds.delete(faces)
        result["remaining_face_count"] = cmds.polyEvaluate(mesh, face=True)

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("ModelingDeleteFacesOutput", parsed)

Shading

shading

Shading and material tools for Maya MCP.

This module provides tools for creating materials, assigning them to meshes or face components, and setting material attributes.

ShadingCreateMaterialOutput

Bases: TypedDict

Return payload for the shading.create_material tool.

ShadingAssignMaterialOutput

Bases: TypedDict

Return payload for the shading.assign_material tool.

ShadingSetMaterialColorOutput

Bases: TypedDict

Return payload for the shading.set_material_color tool.

shading_create_material

shading_create_material(material_type: Literal['lambert', 'blinn', 'phong', 'standardSurface'] = 'lambert', name: str | None = None, color: list[float] | None = None) -> ShadingCreateMaterialOutput

Create a new material with an associated shading group.

PARAMETER DESCRIPTION
material_type

Type of material shader to create.

TYPE: Literal['lambert', 'blinn', 'phong', 'standardSurface'] DEFAULT: 'lambert'

name

Optional name for the material node.

TYPE: str | None DEFAULT: None

color

Optional [r, g, b] color values (0-1 range).

TYPE: list[float] | None DEFAULT: None

RETURNS DESCRIPTION
ShadingCreateMaterialOutput

Dictionary with material, shading_group, material_type, and errors.

RAISES DESCRIPTION
ValueError

If material_type is invalid, name contains invalid characters, or color is not a list of 3 floats.

Source code in src/maya_mcp/tools/shading.py
def shading_create_material(
    material_type: Literal["lambert", "blinn", "phong", "standardSurface"] = "lambert",
    name: str | None = None,
    color: list[float] | None = None,
) -> ShadingCreateMaterialOutput:
    """Create a new material with an associated shading group.

    Args:
        material_type: Type of material shader to create.
        name: Optional name for the material node.
        color: Optional [r, g, b] color values (0-1 range).

    Returns:
        Dictionary with material, shading_group, material_type, and errors.

    Raises:
        ValueError: If material_type is invalid, name contains invalid
            characters, or color is not a list of 3 floats.
    """
    if material_type not in VALID_MATERIAL_TYPES:
        raise ValueError(
            f"Invalid material_type: {material_type!r}. "
            f"Must be one of: {', '.join(sorted(VALID_MATERIAL_TYPES))}"
        )
    if name is not None:
        _validate_node_name(name)
    if color is not None and (not isinstance(color, list) or len(color) != 3):
        raise ValueError("color must be a list of 3 floats [r, g, b]")

    client = get_client()
    mtype_escaped = json.dumps(material_type)
    name_escaped = json.dumps(name) if name is not None else "None"
    color_escaped = json.dumps([float(c) for c in color]) if color is not None else "None"

    command = f"""
import maya.cmds as cmds
import json

mtype = {mtype_escaped}
name = {name_escaped}
color = {color_escaped}

result = {{"material": None, "shading_group": None, "material_type": mtype, "errors": {{}}}}

try:
    kwargs = {{"asShader": True}}
    if name:
        kwargs["name"] = name

    mat = cmds.shadingNode(mtype, **kwargs)
    result["material"] = mat

    # Create shading group
    sg = cmds.sets(renderable=True, noSurfaceShader=True, empty=True, name=mat + "SG")
    result["shading_group"] = sg

    # Connect material to shading group
    cmds.connectAttr(mat + ".outColor", sg + ".surfaceShader", force=True)

    # Set color if provided
    if color:
        if mtype == "standardSurface":
            cmds.setAttr(mat + ".baseColor", color[0], color[1], color[2], type="double3")
        else:
            cmds.setAttr(mat + ".color", color[0], color[1], color[2], type="double3")

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("ShadingCreateMaterialOutput", parsed)

shading_assign_material

shading_assign_material(targets: list[str], material: str) -> ShadingAssignMaterialOutput

Assign a material to meshes or face components.

Resolves the material's shading group automatically. Accepts both material names and shading group names.

PARAMETER DESCRIPTION
targets

List of mesh names or face component strings to assign to.

TYPE: list[str]

material

Name of the material (or shading group) to assign.

TYPE: str

RETURNS DESCRIPTION
ShadingAssignMaterialOutput

Dictionary with assigned list, material, shading_group, and errors.

RAISES DESCRIPTION
ValueError

If targets list is empty or material name contains invalid characters.

Source code in src/maya_mcp/tools/shading.py
def shading_assign_material(
    targets: list[str],
    material: str,
) -> ShadingAssignMaterialOutput:
    """Assign a material to meshes or face components.

    Resolves the material's shading group automatically. Accepts
    both material names and shading group names.

    Args:
        targets: List of mesh names or face component strings to assign to.
        material: Name of the material (or shading group) to assign.

    Returns:
        Dictionary with assigned list, material, shading_group, and errors.

    Raises:
        ValueError: If targets list is empty or material name contains
            invalid characters.
    """
    if not targets:
        raise ValueError("targets list cannot be empty")
    _validate_node_name(material)

    client = get_client()
    targets_escaped = json.dumps(targets)
    material_escaped = json.dumps(material)

    command = f"""
import maya.cmds as cmds
import json

targets = {targets_escaped}
material = {material_escaped}

result = {{"assigned": [], "material": material, "shading_group": None, "errors": {{}}}}

try:
    if not cmds.objExists(material):
        result["errors"]["_material"] = "Node '" + material + "' does not exist"
    else:
        node_type = cmds.nodeType(material)

        # Determine the shading group
        sg = None
        if node_type == "shadingEngine":
            sg = material
        else:
            # Find connected shading group
            connections = cmds.listConnections(material + ".outColor", type="shadingEngine") or []
            if connections:
                sg = connections[0]
            else:
                result["errors"]["_sg"] = "No shading group found for material '" + material + "'"

        if sg:
            result["shading_group"] = sg
            assigned = []
            for target in targets:
                if not cmds.objExists(target):
                    result["errors"][target] = "Target does not exist: " + target
                else:
                    cmds.sets(target, forceElement=sg)
                    assigned.append(target)
            result["assigned"] = assigned

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("ShadingAssignMaterialOutput", parsed)

shading_set_material_color

shading_set_material_color(material: str, color: list[float], attribute: str = 'color') -> ShadingSetMaterialColorOutput

Set a color attribute on a material.

PARAMETER DESCRIPTION
material

Name of the material node.

TYPE: str

color

[r, g, b] color values (0-1 range).

TYPE: list[float]

attribute

Color attribute name (default "color"). Common values: - "color": Diffuse color (lambert/blinn/phong) - "baseColor": Base color (standardSurface) - "transparency": Transparency color - "incandescence": Incandescence color

TYPE: str DEFAULT: 'color'

RETURNS DESCRIPTION
ShadingSetMaterialColorOutput

Dictionary with material, attribute, color, and errors.

RAISES DESCRIPTION
ValueError

If material name contains invalid characters or color is not a list of 3 floats.

Source code in src/maya_mcp/tools/shading.py
def shading_set_material_color(
    material: str,
    color: list[float],
    attribute: str = "color",
) -> ShadingSetMaterialColorOutput:
    """Set a color attribute on a material.

    Args:
        material: Name of the material node.
        color: [r, g, b] color values (0-1 range).
        attribute: Color attribute name (default "color"). Common values:
            - "color": Diffuse color (lambert/blinn/phong)
            - "baseColor": Base color (standardSurface)
            - "transparency": Transparency color
            - "incandescence": Incandescence color

    Returns:
        Dictionary with material, attribute, color, and errors.

    Raises:
        ValueError: If material name contains invalid characters or
            color is not a list of 3 floats.
    """
    _validate_node_name(material)
    if not isinstance(color, list) or len(color) != 3:
        raise ValueError("color must be a list of 3 floats [r, g, b]")

    client = get_client()
    material_escaped = json.dumps(material)
    color_escaped = json.dumps([float(c) for c in color])
    attr_escaped = json.dumps(attribute)

    command = f"""
import maya.cmds as cmds
import json

material = {material_escaped}
color = {color_escaped}
attr = {attr_escaped}

result = {{"material": material, "attribute": attr, "color": color, "errors": {{}}}}

try:
    if not cmds.objExists(material):
        result["errors"]["_material"] = "Node '" + material + "' does not exist"
    else:
        full_attr = material + "." + attr
        if not cmds.attributeQuery(attr, node=material, exists=True):
            result["errors"]["_attribute"] = "Attribute '" + attr + "' does not exist on '" + material + "'"
        else:
            cmds.setAttr(full_attr, color[0], color[1], color[2], type="double3")

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("ShadingSetMaterialColorOutput", parsed)

Skinning

skin

Skinning tools for Maya MCP.

This module provides tools for skin binding, weight management, and weight transfer for character rigging workflows.

SkinBindOutput

Bases: TypedDict

Return payload for the skin.bind tool.

SkinUnbindOutput

Bases: TypedDict

Return payload for the skin.unbind tool.

SkinInfluenceEntry

Bases: TypedDict

Influence entry returned by skin.influences.

SkinInfluencesOutput

Bases: TypedDict

Return payload for the skin.influences tool.

SkinWeightEntry

Bases: TypedDict

Per-component weights returned by skin.weights.get.

SkinWeightsGetOutput

Bases: _GuardedOutput

Return payload for the skin.weights.get tool.

SkinWeightsSetOutput

Bases: TypedDict

Return payload for the skin.weights.set tool.

SkinCopyWeightsOutput

Bases: TypedDict

Return payload for the skin.copy_weights tool.

skin_bind

skin_bind(mesh: str, joints: list[str], max_influences: int = 4, bind_method: Literal['closestDistance', 'heatMap', 'geodesicVoxel'] = 'closestDistance') -> SkinBindOutput

Bind a mesh to a skeleton using a skin cluster.

Creates a skinCluster binding the mesh to the specified joints with the given binding options.

PARAMETER DESCRIPTION
mesh

Name of the mesh to bind.

TYPE: str

joints

List of joint names to use as influences.

TYPE: list[str]

max_influences

Maximum number of influences per vertex (default 4).

TYPE: int DEFAULT: 4

bind_method

Binding algorithm to use. Options: - "closestDistance": Closest distance (default) - "heatMap": Heat map based - "geodesicVoxel": Geodesic voxel

TYPE: Literal['closestDistance', 'heatMap', 'geodesicVoxel'] DEFAULT: 'closestDistance'

RETURNS DESCRIPTION
SkinBindOutput

Dictionary with binding result: - mesh: The mesh that was bound - skin_cluster: Name of the created skinCluster - influences: List of influence joint names - influence_count: Number of influences - errors: Error details if any, or None

RAISES DESCRIPTION
ValueError

If mesh or joint names contain invalid characters, or if joints list is empty.

Source code in src/maya_mcp/tools/skin.py
def skin_bind(
    mesh: str,
    joints: list[str],
    max_influences: int = 4,
    bind_method: Literal["closestDistance", "heatMap", "geodesicVoxel"] = "closestDistance",
) -> SkinBindOutput:
    """Bind a mesh to a skeleton using a skin cluster.

    Creates a skinCluster binding the mesh to the specified joints
    with the given binding options.

    Args:
        mesh: Name of the mesh to bind.
        joints: List of joint names to use as influences.
        max_influences: Maximum number of influences per vertex (default 4).
        bind_method: Binding algorithm to use. Options:
            - "closestDistance": Closest distance (default)
            - "heatMap": Heat map based
            - "geodesicVoxel": Geodesic voxel

    Returns:
        Dictionary with binding result:
            - mesh: The mesh that was bound
            - skin_cluster: Name of the created skinCluster
            - influences: List of influence joint names
            - influence_count: Number of influences
            - errors: Error details if any, or None

    Raises:
        ValueError: If mesh or joint names contain invalid characters,
            or if joints list is empty.
    """
    _validate_node_name(mesh)
    if not joints:
        raise ValueError("joints list cannot be empty")
    for joint in joints:
        _validate_node_name(joint)

    if bind_method not in BIND_METHOD_MAP:
        raise ValueError(
            f"Invalid bind_method: {bind_method!r}. "
            f"Must be one of: {', '.join(sorted(BIND_METHOD_MAP))}"
        )

    client = get_client()
    mesh_escaped = json.dumps(mesh)
    joints_escaped = json.dumps(joints)
    bind_method_int = BIND_METHOD_MAP[bind_method]

    command = f"""
import maya.cmds as cmds
import json

mesh = {mesh_escaped}
joints = {joints_escaped}
max_influences = {max_influences}
bind_method = {bind_method_int}

result = {{"mesh": mesh, "skin_cluster": None, "influences": [], "influence_count": 0, "errors": {{}}}}

try:
    if not cmds.objExists(mesh):
        result["errors"]["_mesh"] = "Node '" + mesh + "' does not exist"
    else:
        # Validate all joints exist
        missing = [j for j in joints if not cmds.objExists(j)]
        if missing:
            result["errors"]["_joints"] = "Joints do not exist: " + ", ".join(missing)
        else:
            # Create skin cluster
            sc = cmds.skinCluster(
                joints + [mesh],
                toSelectedBones=False,
                maximumInfluences=max_influences,
                bindMethod=bind_method,
            )
            skin_cluster = sc[0] if isinstance(sc, list) else sc

            result["skin_cluster"] = skin_cluster

            # Query influences
            influences = cmds.skinCluster(skin_cluster, query=True, influence=True) or []
            result["influences"] = influences
            result["influence_count"] = len(influences)

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("SkinBindOutput", parsed)

skin_unbind

skin_unbind(mesh: str) -> SkinUnbindOutput

Unbind (detach) a skin cluster from a mesh.

Finds the skinCluster on the mesh and unbinds it, removing the skin deformation.

PARAMETER DESCRIPTION
mesh

Name of the mesh to unbind.

TYPE: str

RETURNS DESCRIPTION
SkinUnbindOutput

Dictionary with unbind result: - mesh: The mesh that was unbound - unbound: Whether unbinding succeeded - skin_cluster: Name of the removed skinCluster - errors: Error details if any, or None

RAISES DESCRIPTION
ValueError

If mesh name contains invalid characters.

Source code in src/maya_mcp/tools/skin.py
def skin_unbind(mesh: str) -> SkinUnbindOutput:
    """Unbind (detach) a skin cluster from a mesh.

    Finds the skinCluster on the mesh and unbinds it, removing
    the skin deformation.

    Args:
        mesh: Name of the mesh to unbind.

    Returns:
        Dictionary with unbind result:
            - mesh: The mesh that was unbound
            - unbound: Whether unbinding succeeded
            - skin_cluster: Name of the removed skinCluster
            - errors: Error details if any, or None

    Raises:
        ValueError: If mesh name contains invalid characters.
    """
    _validate_node_name(mesh)

    client = get_client()
    mesh_escaped = json.dumps(mesh)

    command = f"""
import maya.cmds as cmds
import json

mesh = {mesh_escaped}

result = {{"mesh": mesh, "unbound": False, "skin_cluster": None, "errors": {{}}}}

try:
    if not cmds.objExists(mesh):
        result["errors"]["_mesh"] = "Node '" + mesh + "' does not exist"
    else:
        # Find skin cluster in history
        history = cmds.ls(cmds.listHistory(mesh) or [], type="skinCluster") or []
        if not history:
            result["errors"]["_skin"] = "No skinCluster found on '" + mesh + "'"
        else:
            skin_cluster = history[0]
            result["skin_cluster"] = skin_cluster

            # Unbind the skin
            cmds.skinCluster(skin_cluster, edit=True, unbind=True)
            result["unbound"] = True

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("SkinUnbindOutput", parsed)

skin_influences

skin_influences(skin_cluster: str) -> SkinInfluencesOutput

List influences on a skin cluster.

Returns the list of joints/transforms influencing the skin cluster, along with their index mapping.

PARAMETER DESCRIPTION
skin_cluster

Name of the skinCluster node.

TYPE: str

RETURNS DESCRIPTION
SkinInfluencesOutput

Dictionary with influence data: - skin_cluster: The queried skinCluster name - influences: List of dicts with name and index - count: Number of influences - errors: Error details if any, or None

RAISES DESCRIPTION
ValueError

If skin_cluster name contains invalid characters.

Source code in src/maya_mcp/tools/skin.py
def skin_influences(skin_cluster: str) -> SkinInfluencesOutput:
    """List influences on a skin cluster.

    Returns the list of joints/transforms influencing the skin cluster,
    along with their index mapping.

    Args:
        skin_cluster: Name of the skinCluster node.

    Returns:
        Dictionary with influence data:
            - skin_cluster: The queried skinCluster name
            - influences: List of dicts with name and index
            - count: Number of influences
            - errors: Error details if any, or None

    Raises:
        ValueError: If skin_cluster name contains invalid characters.
    """
    _validate_node_name(skin_cluster)

    client = get_client()
    sc_escaped = json.dumps(skin_cluster)

    command = f"""
import maya.cmds as cmds
import json

sc = {sc_escaped}

result = {{"skin_cluster": sc, "influences": [], "count": 0, "errors": {{}}}}

try:
    if not cmds.objExists(sc):
        result["errors"]["_node"] = "Node '" + sc + "' does not exist"
    elif cmds.nodeType(sc) != "skinCluster":
        result["errors"]["_type"] = "Node '" + sc + "' is not a skinCluster (type: " + cmds.nodeType(sc) + ")"
    else:
        influences = cmds.skinCluster(sc, query=True, influence=True) or []
        inf_list = []
        for i, inf in enumerate(influences):
            inf_list.append({{"name": inf, "index": i}})
        result["influences"] = inf_list
        result["count"] = len(inf_list)

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("SkinInfluencesOutput", parsed)

skin_weights_get

skin_weights_get(skin_cluster: str, offset: int = 0, limit: int | None = DEFAULT_WEIGHT_LIMIT) -> SkinWeightsGetOutput

Get skin weights with pagination.

Returns per-vertex weight data for the specified range of vertices. Uses offset/limit pagination because skin weight data can be very large (4-15MB for production meshes).

PARAMETER DESCRIPTION
skin_cluster

Name of the skinCluster node.

TYPE: str

offset

Starting vertex index (0-based).

TYPE: int DEFAULT: 0

limit

Maximum number of vertices to return. Default 100. Use 0 for unlimited (use with caution on large meshes).

TYPE: int | None DEFAULT: DEFAULT_WEIGHT_LIMIT

RETURNS DESCRIPTION
SkinWeightsGetOutput

Dictionary with weight data: - skin_cluster: The queried skinCluster name - mesh: The bound mesh name - vertex_count: Total number of vertices - influence_count: Number of influences - influences: List of influence names - vertices: List of vertex weight entries - offset: The offset used - count: Number of vertices returned - truncated: True if more vertices remain - errors: Error details if any, or None

RAISES DESCRIPTION
ValueError

If skin_cluster name contains invalid characters or offset is negative.

Source code in src/maya_mcp/tools/skin.py
def skin_weights_get(
    skin_cluster: str,
    offset: int = 0,
    limit: int | None = DEFAULT_WEIGHT_LIMIT,
) -> SkinWeightsGetOutput:
    """Get skin weights with pagination.

    Returns per-vertex weight data for the specified range of vertices.
    Uses offset/limit pagination because skin weight data can be very
    large (4-15MB for production meshes).

    Args:
        skin_cluster: Name of the skinCluster node.
        offset: Starting vertex index (0-based).
        limit: Maximum number of vertices to return. Default 100.
            Use 0 for unlimited (use with caution on large meshes).

    Returns:
        Dictionary with weight data:
            - skin_cluster: The queried skinCluster name
            - mesh: The bound mesh name
            - vertex_count: Total number of vertices
            - influence_count: Number of influences
            - influences: List of influence names
            - vertices: List of vertex weight entries
            - offset: The offset used
            - count: Number of vertices returned
            - truncated: True if more vertices remain
            - errors: Error details if any, or None

    Raises:
        ValueError: If skin_cluster name contains invalid characters
            or offset is negative.
    """
    _validate_node_name(skin_cluster)
    if offset < 0:
        raise ValueError(f"offset must be non-negative, got {offset}")

    client = get_client()
    sc_escaped = json.dumps(skin_cluster)

    command = f"""
import maya.cmds as cmds
import json

sc = {sc_escaped}
offset = {offset}
limit = {limit}

result = {{
    "skin_cluster": sc,
    "mesh": None,
    "vertex_count": 0,
    "influence_count": 0,
    "influences": [],
    "vertices": [],
    "offset": offset,
    "count": 0,
    "errors": {{}}
}}

try:
    if not cmds.objExists(sc):
        result["errors"]["_node"] = "Node '" + sc + "' does not exist"
    elif cmds.nodeType(sc) != "skinCluster":
        result["errors"]["_type"] = "Node '" + sc + "' is not a skinCluster (type: " + cmds.nodeType(sc) + ")"
    else:
        # Get the mesh connected to this skin cluster
        geometry = cmds.skinCluster(sc, query=True, geometry=True) or []
        if not geometry:
            result["errors"]["_geometry"] = "No geometry connected to skinCluster '" + sc + "'"
        else:
            mesh = geometry[0]
            result["mesh"] = mesh

            # Get influence list
            influences = cmds.skinCluster(sc, query=True, influence=True) or []
            result["influences"] = influences
            result["influence_count"] = len(influences)

            # Detect geometry type for correct component prefix
            shapes = cmds.listRelatives(mesh, shapes=True, fullPath=False) or []
            shape = shapes[0] if shapes else mesh
            geo_type = cmds.nodeType(shape)
            result["geometry_type"] = geo_type

            if geo_type in ("nurbsCurve", "nurbsSurface"):
                comp_prefix = ".cv["
                vtx_count = len(cmds.ls(shape + ".cv[*]", flatten=True))
            else:
                comp_prefix = ".vtx["
                vtx_count = cmds.polyEvaluate(mesh, vertex=True)

            result["vertex_count"] = vtx_count

            # Calculate range
            start_idx = offset
            end_idx = vtx_count
            if limit and limit > 0:
                end_idx = min(offset + limit, vtx_count)

            # Get weights per vertex/CV
            vertices = []
            for i in range(start_idx, end_idx):
                comp = mesh + comp_prefix + str(i) + "]"
                weights = {{}}
                for inf in influences:
                    w = cmds.skinPercent(sc, comp, query=True, transform=inf, value=True)
                    if w is not None and w > 0.001:
                        weights[inf] = round(w, 6)
                vertices.append({{"vertex_id": i, "weights": weights}})

            result["vertices"] = vertices
            result["count"] = len(vertices)

            # Check truncation
            if limit and limit > 0 and vtx_count > offset + limit:
                result["truncated"] = True

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    # Apply response size guard
    if "vertices" in parsed:
        parsed = guard_response_size(parsed, list_key="vertices")

    return cast("SkinWeightsGetOutput", parsed)

skin_weights_set

skin_weights_set(skin_cluster: str, weights: list[dict[str, Any]], normalize: bool = True) -> SkinWeightsSetOutput

Set skin weights on vertices.

Sets per-vertex skin weights on the specified skin cluster. Each entry in the weights list specifies a vertex_id and a weights dict mapping joint names to weight values.

PARAMETER DESCRIPTION
skin_cluster

Name of the skinCluster node.

TYPE: str

weights

List of weight entries, each a dict with: - vertex_id (int): Vertex index - weights (dict): Map of joint name to weight value

TYPE: list[dict[str, Any]]

normalize

Whether to normalize weights after setting (default True).

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
SkinWeightsSetOutput

Dictionary with set result: - skin_cluster: The skinCluster name - set_count: Number of vertices updated - errors: Error details if any, or None

RAISES DESCRIPTION
ValueError

If skin_cluster name contains invalid characters, weights list is empty, or too many entries.

Source code in src/maya_mcp/tools/skin.py
def skin_weights_set(
    skin_cluster: str,
    weights: list[dict[str, Any]],
    normalize: bool = True,
) -> SkinWeightsSetOutput:
    """Set skin weights on vertices.

    Sets per-vertex skin weights on the specified skin cluster.
    Each entry in the weights list specifies a vertex_id and
    a weights dict mapping joint names to weight values.

    Args:
        skin_cluster: Name of the skinCluster node.
        weights: List of weight entries, each a dict with:
            - vertex_id (int): Vertex index
            - weights (dict): Map of joint name to weight value
        normalize: Whether to normalize weights after setting (default True).

    Returns:
        Dictionary with set result:
            - skin_cluster: The skinCluster name
            - set_count: Number of vertices updated
            - errors: Error details if any, or None

    Raises:
        ValueError: If skin_cluster name contains invalid characters,
            weights list is empty, or too many entries.
    """
    _validate_node_name(skin_cluster)
    if not weights:
        raise ValueError("weights list cannot be empty")
    if len(weights) > MAX_WEIGHT_SET_ENTRIES:
        raise ValueError(
            f"Too many weight entries: {len(weights)}. "
            f"Maximum is {MAX_WEIGHT_SET_ENTRIES} per call."
        )

    # Validate joint names in all weight entries
    for entry in weights:
        if "weights" in entry and isinstance(entry["weights"], dict):
            for joint_name in entry["weights"]:
                _validate_node_name(joint_name)

    client = get_client()
    sc_escaped = json.dumps(skin_cluster)
    weights_escaped = json.dumps(weights)
    normalize_val = "True" if normalize else "False"

    command = f"""
import maya.cmds as cmds
import json

sc = {sc_escaped}
weight_data = {weights_escaped}
normalize = {normalize_val}

result = {{"skin_cluster": sc, "set_count": 0, "errors": {{}}}}

try:
    if not cmds.objExists(sc):
        result["errors"]["_node"] = "Node '" + sc + "' does not exist"
    elif cmds.nodeType(sc) != "skinCluster":
        result["errors"]["_type"] = "Node '" + sc + "' is not a skinCluster (type: " + cmds.nodeType(sc) + ")"
    else:
        geometry = cmds.skinCluster(sc, query=True, geometry=True) or []
        if not geometry:
            result["errors"]["_geometry"] = "No geometry connected to skinCluster '" + sc + "'"
        else:
            mesh = geometry[0]
            set_count = 0
            vertex_errors = {{}}

            # Detect geometry type for correct component prefix
            shapes = cmds.listRelatives(mesh, shapes=True, fullPath=False) or []
            shape = shapes[0] if shapes else mesh
            geo_type = cmds.nodeType(shape)

            if geo_type in ("nurbsCurve", "nurbsSurface"):
                comp_prefix = ".cv["
            else:
                comp_prefix = ".vtx["

            for entry in weight_data:
                vid = entry.get("vertex_id")
                w = entry.get("weights", {{}})
                comp = mesh + comp_prefix + str(vid) + "]"

                try:
                    tv_list = [(joint, weight) for joint, weight in w.items()]
                    cmds.skinPercent(sc, comp, transformValue=tv_list, normalize=normalize)
                    set_count += 1
                except Exception as e:
                    vertex_errors[str(vid)] = str(e)

            result["set_count"] = set_count
            if vertex_errors:
                result["errors"].update(vertex_errors)

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("SkinWeightsSetOutput", parsed)

skin_copy_weights

skin_copy_weights(source_mesh: str, target_mesh: str, surface_association: Literal['closestPoint', 'closestComponent', 'rayCast'] = 'closestPoint', influence_association: Literal['closestJoint', 'closestBone', 'oneToOne', 'name', 'label'] = 'closestJoint') -> SkinCopyWeightsOutput

Copy skin weights from one mesh to another.

Transfers skin weights between two meshes using surface and influence association methods.

PARAMETER DESCRIPTION
source_mesh

Name of the source mesh (must have a skinCluster).

TYPE: str

target_mesh

Name of the target mesh (must have a skinCluster).

TYPE: str

surface_association

Method for matching surface points. Options: - "closestPoint": Closest point on surface (default) - "closestComponent": Closest component - "rayCast": Ray casting

TYPE: Literal['closestPoint', 'closestComponent', 'rayCast'] DEFAULT: 'closestPoint'

influence_association

Method for matching influences. Options: - "closestJoint": Closest joint (default) - "closestBone": Closest bone - "oneToOne": One to one mapping - "name": Match by name - "label": Match by label

TYPE: Literal['closestJoint', 'closestBone', 'oneToOne', 'name', 'label'] DEFAULT: 'closestJoint'

RETURNS DESCRIPTION
SkinCopyWeightsOutput

Dictionary with copy result: - source_mesh: The source mesh name - target_mesh: The target mesh name - source_skin_cluster: Source skinCluster name - target_skin_cluster: Target skinCluster name - success: Whether the copy succeeded - errors: Error details if any, or None

RAISES DESCRIPTION
ValueError

If mesh names contain invalid characters.

Source code in src/maya_mcp/tools/skin.py
def skin_copy_weights(
    source_mesh: str,
    target_mesh: str,
    surface_association: Literal["closestPoint", "closestComponent", "rayCast"] = "closestPoint",
    influence_association: Literal[
        "closestJoint", "closestBone", "oneToOne", "name", "label"
    ] = "closestJoint",
) -> SkinCopyWeightsOutput:
    """Copy skin weights from one mesh to another.

    Transfers skin weights between two meshes using surface
    and influence association methods.

    Args:
        source_mesh: Name of the source mesh (must have a skinCluster).
        target_mesh: Name of the target mesh (must have a skinCluster).
        surface_association: Method for matching surface points. Options:
            - "closestPoint": Closest point on surface (default)
            - "closestComponent": Closest component
            - "rayCast": Ray casting
        influence_association: Method for matching influences. Options:
            - "closestJoint": Closest joint (default)
            - "closestBone": Closest bone
            - "oneToOne": One to one mapping
            - "name": Match by name
            - "label": Match by label

    Returns:
        Dictionary with copy result:
            - source_mesh: The source mesh name
            - target_mesh: The target mesh name
            - source_skin_cluster: Source skinCluster name
            - target_skin_cluster: Target skinCluster name
            - success: Whether the copy succeeded
            - errors: Error details if any, or None

    Raises:
        ValueError: If mesh names contain invalid characters.
    """
    _validate_node_name(source_mesh)
    _validate_node_name(target_mesh)

    valid_surface = {"closestPoint", "closestComponent", "rayCast"}
    if surface_association not in valid_surface:
        raise ValueError(
            f"Invalid surface_association: {surface_association!r}. "
            f"Must be one of: {', '.join(sorted(valid_surface))}"
        )

    valid_influence = {"closestJoint", "closestBone", "oneToOne", "name", "label"}
    if influence_association not in valid_influence:
        raise ValueError(
            f"Invalid influence_association: {influence_association!r}. "
            f"Must be one of: {', '.join(sorted(valid_influence))}"
        )

    client = get_client()
    src_escaped = json.dumps(source_mesh)
    tgt_escaped = json.dumps(target_mesh)
    sa_escaped = json.dumps(surface_association)
    ia_escaped = json.dumps(influence_association)

    command = f"""
import maya.cmds as cmds
import json

source_mesh = {src_escaped}
target_mesh = {tgt_escaped}
surface_assoc = {sa_escaped}
influence_assoc = {ia_escaped}

result = {{
    "source_mesh": source_mesh,
    "target_mesh": target_mesh,
    "source_skin_cluster": None,
    "target_skin_cluster": None,
    "success": False,
    "errors": {{}}
}}

try:
    # Validate meshes exist
    if not cmds.objExists(source_mesh):
        result["errors"]["_source"] = "Node '" + source_mesh + "' does not exist"
    elif not cmds.objExists(target_mesh):
        result["errors"]["_target"] = "Node '" + target_mesh + "' does not exist"
    else:
        # Find skin clusters
        src_history = cmds.ls(cmds.listHistory(source_mesh) or [], type="skinCluster") or []
        if not src_history:
            result["errors"]["_source_skin"] = "No skinCluster found on '" + source_mesh + "'"
        else:
            src_sc = src_history[0]
            result["source_skin_cluster"] = src_sc

            tgt_history = cmds.ls(cmds.listHistory(target_mesh) or [], type="skinCluster") or []
            if not tgt_history:
                result["errors"]["_target_skin"] = "No skinCluster found on '" + target_mesh + "'"
            else:
                tgt_sc = tgt_history[0]
                result["target_skin_cluster"] = tgt_sc

                # Copy weights
                cmds.copySkinWeights(
                    sourceSkin=src_sc,
                    destinationSkin=tgt_sc,
                    surfaceAssociation=surface_assoc,
                    influenceAssociation=[influence_assoc],
                    noMirror=True,
                )
                result["success"] = True

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("SkinCopyWeightsOutput", parsed)

Animation

animation

Animation tools for Maya MCP.

This module provides tools for keyframing, timeline control, and playback range management for animation workflows.

AnimationSetTimeOutput

Bases: TypedDict

Return payload for the animation.set_time tool.

AnimationGetTimeRangeOutput

Bases: TypedDict

Return payload for the animation.get_time_range tool.

AnimationSetTimeRangeOutput

Bases: TypedDict

Return payload for the animation.set_time_range tool.

AnimationSetKeyframeOutput

Bases: TypedDict

Return payload for the animation.set_keyframe tool.

KeyframeEntry

Bases: TypedDict

A single animation keyframe.

AnimationGetKeyframesOutput

Bases: _GuardedOutput

Return payload for the animation.get_keyframes tool.

AnimationDeleteKeyframesOutput

Bases: TypedDict

Return payload for the animation.delete_keyframes tool.

animation_set_time

animation_set_time(time: float, update: bool = True) -> AnimationSetTimeOutput

Set the current time (go to a specific frame).

PARAMETER DESCRIPTION
time

The frame number to set as current time.

TYPE: float

update

Whether to update the viewport (default True).

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
AnimationSetTimeOutput

Dictionary with time result: - time: The time that was set - errors: Error details if any, or None

Source code in src/maya_mcp/tools/animation.py
def animation_set_time(
    time: float,
    update: bool = True,
) -> AnimationSetTimeOutput:
    """Set the current time (go to a specific frame).

    Args:
        time: The frame number to set as current time.
        update: Whether to update the viewport (default True).

    Returns:
        Dictionary with time result:
            - time: The time that was set
            - errors: Error details if any, or None
    """
    client = get_client()
    update_val = "True" if update else "False"

    command = f"""
import maya.cmds as cmds
import json

t = {float(time)}
update = {update_val}

result = {{"time": None, "errors": {{}}}}

try:
    result["time"] = cmds.currentTime(t, update=update)
except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("AnimationSetTimeOutput", parsed)

animation_get_time_range

animation_get_time_range() -> AnimationGetTimeRangeOutput

Get playback range, animation range, and current time.

RETURNS DESCRIPTION
AnimationGetTimeRangeOutput

Dictionary with time range data: - current_time: Current frame - min_time: Playback start time - max_time: Playback end time - animation_start: Animation range start - animation_end: Animation range end - fps: Current FPS setting - errors: Error details if any, or None

Source code in src/maya_mcp/tools/animation.py
def animation_get_time_range() -> AnimationGetTimeRangeOutput:
    """Get playback range, animation range, and current time.

    Returns:
        Dictionary with time range data:
            - current_time: Current frame
            - min_time: Playback start time
            - max_time: Playback end time
            - animation_start: Animation range start
            - animation_end: Animation range end
            - fps: Current FPS setting
            - errors: Error details if any, or None
    """
    client = get_client()

    command = """
import maya.cmds as cmds
import json

result = {
    "current_time": None,
    "min_time": None,
    "max_time": None,
    "animation_start": None,
    "animation_end": None,
    "fps": None,
    "errors": {}
}

try:
    result["current_time"] = cmds.currentTime(query=True)
    result["min_time"] = cmds.playbackOptions(query=True, minTime=True)
    result["max_time"] = cmds.playbackOptions(query=True, maxTime=True)
    result["animation_start"] = cmds.playbackOptions(query=True, animationStartTime=True)
    result["animation_end"] = cmds.playbackOptions(query=True, animationEndTime=True)
    result["fps"] = cmds.currentUnit(query=True, time=True)
except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    # Resolve FPS unit string to numeric value Python-side
    if parsed.get("fps") and isinstance(parsed["fps"], str):
        parsed["fps"] = TIME_UNIT_TO_FPS.get(parsed["fps"], parsed["fps"])

    return cast("AnimationGetTimeRangeOutput", parsed)

animation_set_time_range

animation_set_time_range(min_time: float, max_time: float, animation_start: float | None = None, animation_end: float | None = None) -> AnimationSetTimeRangeOutput

Set the playback and animation range.

PARAMETER DESCRIPTION
min_time

Playback start time.

TYPE: float

max_time

Playback end time.

TYPE: float

animation_start

Animation range start (defaults to min_time).

TYPE: float | None DEFAULT: None

animation_end

Animation range end (defaults to max_time).

TYPE: float | None DEFAULT: None

RETURNS DESCRIPTION
AnimationSetTimeRangeOutput

Dictionary with range result: - min_time: The playback start time that was set - max_time: The playback end time that was set - animation_start: The animation start that was set - animation_end: The animation end that was set - errors: Error details if any, or None

RAISES DESCRIPTION
ValueError

If min_time >= max_time, animation_start > min_time, or animation_end < max_time.

Source code in src/maya_mcp/tools/animation.py
def animation_set_time_range(
    min_time: float,
    max_time: float,
    animation_start: float | None = None,
    animation_end: float | None = None,
) -> AnimationSetTimeRangeOutput:
    """Set the playback and animation range.

    Args:
        min_time: Playback start time.
        max_time: Playback end time.
        animation_start: Animation range start (defaults to min_time).
        animation_end: Animation range end (defaults to max_time).

    Returns:
        Dictionary with range result:
            - min_time: The playback start time that was set
            - max_time: The playback end time that was set
            - animation_start: The animation start that was set
            - animation_end: The animation end that was set
            - errors: Error details if any, or None

    Raises:
        ValueError: If min_time >= max_time, animation_start > min_time,
            or animation_end < max_time.
    """
    if min_time >= max_time:
        raise ValueError(f"min_time ({min_time}) must be less than max_time ({max_time})")

    anim_start = animation_start if animation_start is not None else min_time
    anim_end = animation_end if animation_end is not None else max_time

    if anim_start > min_time:
        raise ValueError(f"animation_start ({anim_start}) must be <= min_time ({min_time})")
    if anim_end < max_time:
        raise ValueError(f"animation_end ({anim_end}) must be >= max_time ({max_time})")

    client = get_client()

    command = f"""
import maya.cmds as cmds
import json

min_t = {float(min_time)}
max_t = {float(max_time)}
anim_start = {float(anim_start)}
anim_end = {float(anim_end)}

result = {{
    "min_time": None,
    "max_time": None,
    "animation_start": None,
    "animation_end": None,
    "errors": {{}}
}}

try:
    cmds.playbackOptions(
        minTime=min_t,
        maxTime=max_t,
        animationStartTime=anim_start,
        animationEndTime=anim_end,
    )
    result["min_time"] = cmds.playbackOptions(query=True, minTime=True)
    result["max_time"] = cmds.playbackOptions(query=True, maxTime=True)
    result["animation_start"] = cmds.playbackOptions(query=True, animationStartTime=True)
    result["animation_end"] = cmds.playbackOptions(query=True, animationEndTime=True)
except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("AnimationSetTimeRangeOutput", parsed)

animation_set_keyframe

animation_set_keyframe(node: str, attributes: list[str] | None = None, time: float | None = None, value: float | None = None, in_tangent_type: TangentType = 'auto', out_tangent_type: TangentType = 'auto') -> AnimationSetKeyframeOutput

Set keyframe on attribute(s) at current or specified time.

PARAMETER DESCRIPTION
node

Name of the node to keyframe.

TYPE: str

attributes

List of attribute names to keyframe (None = all keyable).

TYPE: list[str] | None DEFAULT: None

time

Time/frame to set the keyframe at (None = current time).

TYPE: float | None DEFAULT: None

value

Value to set (None = current value).

TYPE: float | None DEFAULT: None

in_tangent_type

In-tangent type. Options: auto, linear, flat, step, stepnext, spline, clamped, plateau, fast, slow.

TYPE: TangentType DEFAULT: 'auto'

out_tangent_type

Out-tangent type. Same options as in_tangent_type.

TYPE: TangentType DEFAULT: 'auto'

RETURNS DESCRIPTION
AnimationSetKeyframeOutput

Dictionary with keyframe result: - node: The node that was keyed - attributes: List of attributes that were keyed - time: The time the keyframe was set at - keyframe_count: Number of keyframes set - errors: Error details if any, or None

RAISES DESCRIPTION
ValueError

If node name or attribute names contain invalid characters, or tangent types are invalid.

Source code in src/maya_mcp/tools/animation.py
def animation_set_keyframe(
    node: str,
    attributes: list[str] | None = None,
    time: float | None = None,
    value: float | None = None,
    in_tangent_type: TangentType = "auto",
    out_tangent_type: TangentType = "auto",
) -> AnimationSetKeyframeOutput:
    """Set keyframe on attribute(s) at current or specified time.

    Args:
        node: Name of the node to keyframe.
        attributes: List of attribute names to keyframe (None = all keyable).
        time: Time/frame to set the keyframe at (None = current time).
        value: Value to set (None = current value).
        in_tangent_type: In-tangent type. Options: auto, linear, flat,
            step, stepnext, spline, clamped, plateau, fast, slow.
        out_tangent_type: Out-tangent type. Same options as in_tangent_type.

    Returns:
        Dictionary with keyframe result:
            - node: The node that was keyed
            - attributes: List of attributes that were keyed
            - time: The time the keyframe was set at
            - keyframe_count: Number of keyframes set
            - errors: Error details if any, or None

    Raises:
        ValueError: If node name or attribute names contain invalid characters,
            or tangent types are invalid.
    """
    _validate_node_name(node)
    _validate_optional_attributes(attributes)

    if in_tangent_type not in VALID_TANGENT_TYPES:
        raise ValueError(
            f"Invalid in_tangent_type: {in_tangent_type!r}. "
            f"Must be one of: {', '.join(sorted(VALID_TANGENT_TYPES))}"
        )
    if out_tangent_type not in VALID_TANGENT_TYPES:
        raise ValueError(
            f"Invalid out_tangent_type: {out_tangent_type!r}. "
            f"Must be one of: {', '.join(sorted(VALID_TANGENT_TYPES))}"
        )

    client = get_client()
    node_escaped = json.dumps(node)
    attrs_escaped = json.dumps(attributes) if attributes is not None else "None"
    itt_escaped = json.dumps(in_tangent_type)
    ott_escaped = json.dumps(out_tangent_type)

    # Build conditional kwargs for time and value
    kwarg_lines: list[str] = []
    if time is not None:
        kwarg_lines.append(f"        kwargs['time'] = {float(time)}")
    if value is not None:
        kwarg_lines.append(f"        kwargs['value'] = {float(value)}")
    kwarg_block = "\n".join(kwarg_lines) if kwarg_lines else "        pass"

    command = f"""
import maya.cmds as cmds
import json

node = {node_escaped}
attrs = {attrs_escaped}
itt = {itt_escaped}
ott = {ott_escaped}

result = {{"node": node, "attributes": [], "time": None, "keyframe_count": 0, "errors": {{}}}}

try:
    if not cmds.objExists(node):
        result["errors"]["_node"] = "Node '" + node + "' does not exist"
    else:
        kwargs = {{"inTangentType": itt, "outTangentType": ott}}
{kwarg_block}

        if attrs is not None:
            kwargs["attribute"] = attrs
        count = cmds.setKeyframe(node, **kwargs)
        count = count if count else 0

        keyed_attrs = attrs if attrs is not None else (cmds.listAttr(node, keyable=True) or [])

        result["attributes"] = keyed_attrs
        result["keyframe_count"] = count
        result["time"] = cmds.currentTime(query=True)
except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("AnimationSetKeyframeOutput", parsed)

animation_get_keyframes

animation_get_keyframes(node: str, attributes: list[str] | None = None, time_range_start: float | None = None, time_range_end: float | None = None) -> AnimationGetKeyframesOutput

Query keyframes for attribute(s) on a node within optional time range.

PARAMETER DESCRIPTION
node

Name of the node to query.

TYPE: str

attributes

List of attribute names to query (None = all animated).

TYPE: list[str] | None DEFAULT: None

time_range_start

Start of time range to query (None = all time).

TYPE: float | None DEFAULT: None

time_range_end

End of time range to query (None = all time).

TYPE: float | None DEFAULT: None

RETURNS DESCRIPTION
AnimationGetKeyframesOutput

Dictionary with keyframe data: - node: The queried node name - keyframes: Dict mapping attribute names to lists of {time, value} entries - attribute_count: Number of attributes with keyframes - total_keyframe_count: Total number of keyframes found - errors: Error details if any, or None

RAISES DESCRIPTION
ValueError

If node name or attribute names contain invalid characters, or time_range_start/time_range_end are not both provided or both None.

Source code in src/maya_mcp/tools/animation.py
def animation_get_keyframes(
    node: str,
    attributes: list[str] | None = None,
    time_range_start: float | None = None,
    time_range_end: float | None = None,
) -> AnimationGetKeyframesOutput:
    """Query keyframes for attribute(s) on a node within optional time range.

    Args:
        node: Name of the node to query.
        attributes: List of attribute names to query (None = all animated).
        time_range_start: Start of time range to query (None = all time).
        time_range_end: End of time range to query (None = all time).

    Returns:
        Dictionary with keyframe data:
            - node: The queried node name
            - keyframes: Dict mapping attribute names to lists of
              {time, value} entries
            - attribute_count: Number of attributes with keyframes
            - total_keyframe_count: Total number of keyframes found
            - errors: Error details if any, or None

    Raises:
        ValueError: If node name or attribute names contain invalid characters,
            or time_range_start/time_range_end are not both provided or both None.
    """
    _validate_node_name(node)
    _validate_optional_attributes(attributes)
    _validate_time_range(time_range_start, time_range_end)

    client = get_client()
    node_escaped = json.dumps(node)
    attrs_escaped = json.dumps(attributes) if attributes is not None else "None"
    time_range_init = _build_time_range_code(time_range_start, time_range_end)
    attr_discovery = _build_anim_attr_discovery_code("query_attrs")

    command = f"""
import maya.cmds as cmds
import json

node = {node_escaped}
attrs = {attrs_escaped}
{time_range_init}

result = {{
    "node": node,
    "keyframes": {{}},
    "attribute_count": 0,
    "total_keyframe_count": 0,
    "errors": {{}}
}}

try:
    if not cmds.objExists(node):
        result["errors"]["_node"] = "Node '" + node + "' does not exist"
    else:
        if attrs is not None:
            query_attrs = attrs
        else:
{attr_discovery}

        keyframes = {{}}
        total_count = 0

        for attr in query_attrs:
            kw = {{"query": True, "attribute": attr}}
            if time_range is not None:
                kw["time"] = time_range

            times = cmds.keyframe(node, timeChange=True, **kw) or []
            values = cmds.keyframe(node, valueChange=True, **kw) or []

            if times:
                entries = []
                for t, v in zip(times, values):
                    entries.append({{"time": t, "value": v}})
                keyframes[attr] = entries
                total_count += len(entries)

        result["keyframes"] = keyframes
        result["attribute_count"] = len(keyframes)
        result["total_keyframe_count"] = total_count

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    guarded = _guard_keyframe_response(parsed)

    return guarded

animation_delete_keyframes

animation_delete_keyframes(node: str, attributes: list[str] | None = None, time_range_start: float | None = None, time_range_end: float | None = None) -> AnimationDeleteKeyframesOutput

Delete keyframes in a time range for attribute(s).

PARAMETER DESCRIPTION
node

Name of the node to delete keyframes from.

TYPE: str

attributes

List of attribute names (None = all animated attributes).

TYPE: list[str] | None DEFAULT: None

time_range_start

Start of time range (None = all time).

TYPE: float | None DEFAULT: None

time_range_end

End of time range (None = all time).

TYPE: float | None DEFAULT: None

RETURNS DESCRIPTION
AnimationDeleteKeyframesOutput

Dictionary with delete result: - node: The node that was modified - deleted_count: Number of keyframes deleted - attributes: Attributes that were affected - time_range: The time range used (or "all") - errors: Error details if any, or None

RAISES DESCRIPTION
ValueError

If node name or attribute names contain invalid characters, or time_range_start/time_range_end are not both provided or both None.

Source code in src/maya_mcp/tools/animation.py
def animation_delete_keyframes(
    node: str,
    attributes: list[str] | None = None,
    time_range_start: float | None = None,
    time_range_end: float | None = None,
) -> AnimationDeleteKeyframesOutput:
    """Delete keyframes in a time range for attribute(s).

    Args:
        node: Name of the node to delete keyframes from.
        attributes: List of attribute names (None = all animated attributes).
        time_range_start: Start of time range (None = all time).
        time_range_end: End of time range (None = all time).

    Returns:
        Dictionary with delete result:
            - node: The node that was modified
            - deleted_count: Number of keyframes deleted
            - attributes: Attributes that were affected
            - time_range: The time range used (or "all")
            - errors: Error details if any, or None

    Raises:
        ValueError: If node name or attribute names contain invalid characters,
            or time_range_start/time_range_end are not both provided or both None.
    """
    _validate_node_name(node)
    _validate_optional_attributes(attributes)
    _validate_time_range(time_range_start, time_range_end)

    client = get_client()
    node_escaped = json.dumps(node)
    attrs_escaped = json.dumps(attributes) if attributes is not None else "None"
    time_range_code = _build_time_range_code(time_range_start, time_range_end)
    attr_discovery = _build_anim_attr_discovery_code("target_attrs")

    command = f"""
import maya.cmds as cmds
import json

node = {node_escaped}
attrs = {attrs_escaped}
{time_range_code}

result = {{
    "node": node,
    "deleted_count": 0,
    "attributes": [],
    "time_range": "all",
    "errors": {{}}
}}

try:
    if not cmds.objExists(node):
        result["errors"]["_node"] = "Node '" + node + "' does not exist"
    else:
        if attrs is not None:
            target_attrs = attrs
        else:
{attr_discovery}

        total_deleted = 0
        affected_attrs = []

        for attr in target_attrs:
            # Count keyframes before deleting (cutKey return value unreliable)
            count_kw = {{"query": True, "keyframeCount": True, "attribute": attr}}
            if time_range is not None:
                count_kw["time"] = time_range
            pre_count = cmds.keyframe(node, **count_kw) or 0

            cut_kw = {{"attribute": attr, "clear": True}}
            if time_range is not None:
                cut_kw["time"] = time_range
            cmds.cutKey(node, **cut_kw)

            if pre_count > 0:
                total_deleted += pre_count
                affected_attrs.append(attr)

        result["deleted_count"] = total_deleted
        result["attributes"] = affected_attrs
        if time_range is not None:
            result["time_range"] = list(time_range)

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("AnimationDeleteKeyframesOutput", parsed)

Curves

curve

Curve tools for Maya MCP.

This module provides tools for querying NURBS curve geometry.

CurveInfoOutput

Bases: TypedDict

Return payload for the curve.info tool.

CurveCvsOutput

Bases: _GuardedOutput

Return payload for the curve.cvs tool.

curve_info

curve_info(node: str) -> CurveInfoOutput

Get information about a NURBS curve.

Returns degree, spans, form, CV count, knots, length, and bounding box.

PARAMETER DESCRIPTION
node

Name of the curve node (transform or shape).

TYPE: str

RETURNS DESCRIPTION
CurveInfoOutput

Dictionary with curve information: - node: The queried node name - exists: Whether the node exists - is_curve: Whether the node is a nurbsCurve - degree: Curve degree - spans: Number of spans - form: Curve form (open, closed, periodic) - cv_count: Number of CVs - knots: List of knot values - length: Arc length of the curve - bounding_box: [min_x, min_y, min_z, max_x, max_y, max_z] - errors: Error details if any, or None

RAISES DESCRIPTION
ValueError

If node name contains invalid characters.

Source code in src/maya_mcp/tools/curve.py
def curve_info(node: str) -> CurveInfoOutput:
    """Get information about a NURBS curve.

    Returns degree, spans, form, CV count, knots, length, and bounding box.

    Args:
        node: Name of the curve node (transform or shape).

    Returns:
        Dictionary with curve information:
            - node: The queried node name
            - exists: Whether the node exists
            - is_curve: Whether the node is a nurbsCurve
            - degree: Curve degree
            - spans: Number of spans
            - form: Curve form (open, closed, periodic)
            - cv_count: Number of CVs
            - knots: List of knot values
            - length: Arc length of the curve
            - bounding_box: [min_x, min_y, min_z, max_x, max_y, max_z]
            - errors: Error details if any, or None

    Raises:
        ValueError: If node name contains invalid characters.
    """
    _validate_node_name(node)

    client = get_client()
    node_escaped = json.dumps(node)

    command = f"""
import maya.cmds as cmds
import json
from maya.api import OpenMaya as om2
node = {node_escaped}
result = {{"node": node, "exists": False, "is_curve": False, "errors": {{}}}}
try:
    if not cmds.objExists(node):
        result["errors"]["_node"] = "Node '" + node + "' does not exist"
    else:
        result["exists"] = True
        shapes = cmds.listRelatives(node, shapes=True, fullPath=False) or []
        if shapes:
            shape = shapes[0]
        else:
            shape = node
        node_type = cmds.nodeType(shape)
        if node_type != "nurbsCurve":
            result["errors"]["_curve"] = "Node is not a nurbsCurve (type: " + node_type + ")"
        else:
            result["is_curve"] = True
            result["shape"] = shape
            result["degree"] = cmds.getAttr(shape + ".degree")
            result["spans"] = cmds.getAttr(shape + ".spans")
            form_val = cmds.getAttr(shape + ".form")
            form_map = {{0: "open", 1: "closed", 2: "periodic"}}
            result["form"] = form_map.get(form_val, "unknown")
            cvs = cmds.ls(shape + ".cv[*]", flatten=True)
            result["cv_count"] = len(cvs)
            sel = om2.MSelectionList()
            sel.add(shape)
            fn_curve = om2.MFnNurbsCurve(sel.getDagPath(0))
            result["knots"] = [float(k) for k in fn_curve.knots()]
            result["length"] = fn_curve.length()
            bbox = cmds.exactWorldBoundingBox(shape)
            result["bounding_box"] = bbox
except Exception as e:
    result["errors"]["_exception"] = str(e)
print(json.dumps(result))
"""

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("CurveInfoOutput", parsed)

curve_cvs

curve_cvs(node: str, offset: int = 0, limit: int | None = DEFAULT_CV_LIMIT) -> CurveCvsOutput

Query CV positions from a NURBS curve with pagination.

Returns CV positions as [x, y, z] arrays in world space.

PARAMETER DESCRIPTION
node

Name of the curve node (transform or shape).

TYPE: str

offset

Starting CV index (0-based).

TYPE: int DEFAULT: 0

limit

Maximum number of CVs to return. Default 1000. Use 0 for unlimited.

TYPE: int | None DEFAULT: DEFAULT_CV_LIMIT

RETURNS DESCRIPTION
CurveCvsOutput

Dictionary with CV data: - node: The queried node name - exists: Whether the node exists - is_curve: Whether the node is a nurbsCurve - cv_count: Total number of CVs - cvs: List of [x, y, z] position arrays - offset: The offset used - count: Number of CVs returned - truncated: True if more CVs remain - errors: Error details if any, or None

RAISES DESCRIPTION
ValueError

If node name contains invalid characters or offset is negative.

Source code in src/maya_mcp/tools/curve.py
def curve_cvs(
    node: str,
    offset: int = 0,
    limit: int | None = DEFAULT_CV_LIMIT,
) -> CurveCvsOutput:
    """Query CV positions from a NURBS curve with pagination.

    Returns CV positions as [x, y, z] arrays in world space.

    Args:
        node: Name of the curve node (transform or shape).
        offset: Starting CV index (0-based).
        limit: Maximum number of CVs to return. Default 1000.
            Use 0 for unlimited.

    Returns:
        Dictionary with CV data:
            - node: The queried node name
            - exists: Whether the node exists
            - is_curve: Whether the node is a nurbsCurve
            - cv_count: Total number of CVs
            - cvs: List of [x, y, z] position arrays
            - offset: The offset used
            - count: Number of CVs returned
            - truncated: True if more CVs remain
            - errors: Error details if any, or None

    Raises:
        ValueError: If node name contains invalid characters or offset is negative.
    """
    _validate_node_name(node)
    if offset < 0:
        raise ValueError(f"offset must be non-negative, got {offset}")

    client = get_client()
    node_escaped = json.dumps(node)

    # Maya's commandPort has scoping issues with deeply nested f-string
    # templates (variables become undefined mid-execution). Building the
    # code as an explicit string and running it via exec() with a clean
    # namespace avoids this. curve_info doesn't need this because its
    # command is shorter and doesn't hit the problematic threshold.
    inner_code = (
        "import maya.cmds as cmds\n"
        "import json\n"
        f"node = {node_escaped}\n"
        f"offset = {offset}\n"
        f"limit = {limit}\n"
        'result = {"node": node, "exists": False, "is_curve": False, "errors": {}}\n'
        "try:\n"
        "    if not cmds.objExists(node):\n"
        """        result["errors"]["_node"] = "Node '" + node + "' does not exist"\n"""
        "    else:\n"
        '        result["exists"] = True\n'
        "        shapes = cmds.listRelatives(node, shapes=True, fullPath=False) or []\n"
        "        shape = shapes[0] if shapes else node\n"
        "        node_type = cmds.nodeType(shape)\n"
        '        if node_type != "nurbsCurve":\n'
        '            result["errors"]["_curve"] = '
        '"Node is not a nurbsCurve (type: " + node_type + ")"\n'
        "        else:\n"
        '            result["is_curve"] = True\n'
        '            result["shape"] = shape\n'
        '            all_cvs = cmds.ls(shape + ".cv[*]", flatten=True)\n'
        "            total_count = len(all_cvs)\n"
        '            result["cv_count"] = total_count\n'
        "            end_idx = min(offset + limit, total_count) "
        "if limit and limit > 0 else total_count\n"
        '            cv_range = shape + ".cv[" + str(offset) '
        '+ ":" + str(end_idx - 1) + "]"\n'
        "            pos_data = cmds.xform(cv_range, query=True, "
        "worldSpace=True, translation=True) or []\n"
        "            cvs = [[pos_data[i], pos_data[i+1], pos_data[i+2]] "
        "for i in range(0, len(pos_data), 3)]\n"
        '            result["cvs"] = cvs\n'
        '            result["offset"] = offset\n'
        '            result["count"] = len(cvs)\n'
        "            if limit and limit > 0 and total_count > offset + limit:\n"
        '                result["truncated"] = True\n'
        "except Exception as e:\n"
        '    result["errors"]["_exception"] = str(e)\n'
        "print(json.dumps(result))\n"
    )
    code_escaped = json.dumps(inner_code)
    command = f'_c = {code_escaped}\nexec(_c, {{"__name__": "__main__"}})\n'

    response = client.execute(command)
    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    if "cvs" in parsed:
        parsed = guard_response_size(parsed, list_key="cvs")

    return cast("CurveCvsOutput", parsed)

Scripts

scripts

Script execution tools for Maya MCP.

This module provides tools for listing, executing, and running scripts in Maya with a three-tier trust model: - Tier 1 (script.list): Read-only listing of scripts from allowed dirs - Tier 2 (script.execute): Execute validated .py files from allowed dirs - Tier 3 (script.run): Execute raw Python/MEL code (requires opt-in)

ScriptListEntry

Bases: TypedDict

A discovered script file.

ScriptListOutput

Bases: _GuardedOutput

Return payload for the script.list tool.

ScriptExecuteOutput

Bases: TypedDict

Return payload for the script.execute tool.

ScriptRunOutput

Bases: TypedDict

Return payload for the script.run tool.

script_list

script_list() -> ScriptListOutput

List available Python scripts from configured directories.

Scans MAYA_MCP_SCRIPT_DIRS for .py files, excluding underscore-prefixed files. This is a server-side operation that does not require a Maya connection.

RETURNS DESCRIPTION
ScriptListOutput

Dictionary with script listing: - scripts: List of script info dicts (name, path, size_bytes, relative_path) - count: Number of scripts found - directories: List of configured script directories - errors: Error details if any, or None

Source code in src/maya_mcp/tools/scripts.py
def script_list() -> ScriptListOutput:
    """List available Python scripts from configured directories.

    Scans MAYA_MCP_SCRIPT_DIRS for .py files, excluding underscore-prefixed
    files. This is a server-side operation that does not require a Maya
    connection.

    Returns:
        Dictionary with script listing:
            - scripts: List of script info dicts (name, path, size_bytes, relative_path)
            - count: Number of scripts found
            - directories: List of configured script directories
            - errors: Error details if any, or None
    """
    config = get_script_config()

    result: dict[str, Any] = {
        "scripts": [],
        "count": 0,
        "directories": [str(d) for d in config.script_dirs],
        "errors": None,
    }

    if not config.script_dirs:
        result["errors"] = {
            "_config": "No script directories configured (set MAYA_MCP_SCRIPT_DIRS)"
        }
        return cast("ScriptListOutput", result)

    scripts: list[dict[str, Any]] = []
    scan_errors: dict[str, str] = {}
    capped = False

    for script_dir in config.script_dirs:
        if capped:
            break
        try:
            resolved_dir = script_dir.resolve()
            for py_file in resolved_dir.rglob("*.py"):
                if len(scripts) >= _MAX_SCRIPT_SCAN:
                    capped = True
                    break
                # Skip underscore-prefixed files
                if py_file.name.startswith("_"):
                    continue
                try:
                    rel_path = py_file.relative_to(resolved_dir)
                    scripts.append(
                        {
                            "name": py_file.name,
                            "path": str(py_file),
                            "size_bytes": py_file.stat().st_size,
                            "relative_path": str(rel_path),
                        }
                    )
                except (OSError, ValueError):
                    continue
        except OSError as e:
            scan_errors[str(script_dir)] = str(e)

    result["scripts"] = scripts
    result["count"] = len(scripts)

    if scan_errors:
        result["errors"] = scan_errors

    return cast("ScriptListOutput", guard_response_size(result, list_key="scripts"))

script_execute

script_execute(file_path: str, args: dict[str, Any] | None = None, timeout: int | None = None) -> ScriptExecuteOutput

Execute a Python script file in Maya.

The script must be within a configured MAYA_MCP_SCRIPT_DIRS directory. The script is read server-side and sent to Maya for execution. An __args__ dict is injected into the script namespace.

PARAMETER DESCRIPTION
file_path

Absolute path to the .py script file.

TYPE: str

args

Optional arguments dict injected as __args__ in the script.

TYPE: dict[str, Any] | None DEFAULT: None

timeout

Optional timeout override in seconds.

TYPE: int | None DEFAULT: None

RETURNS DESCRIPTION
ScriptExecuteOutput

Dictionary with execution result: - success: Whether execution completed without error - script: Path of the executed script - output: Captured stdout from the script - errors: Error details if any, or None

RAISES DESCRIPTION
ValidationError

If the path fails security validation.

Source code in src/maya_mcp/tools/scripts.py
def script_execute(
    file_path: str,
    args: dict[str, Any] | None = None,
    timeout: int | None = None,
) -> ScriptExecuteOutput:
    """Execute a Python script file in Maya.

    The script must be within a configured MAYA_MCP_SCRIPT_DIRS directory.
    The script is read server-side and sent to Maya for execution. An
    ``__args__`` dict is injected into the script namespace.

    Args:
        file_path: Absolute path to the .py script file.
        args: Optional arguments dict injected as ``__args__`` in the script.
        timeout: Optional timeout override in seconds.

    Returns:
        Dictionary with execution result:
            - success: Whether execution completed without error
            - script: Path of the executed script
            - output: Captured stdout from the script
            - errors: Error details if any, or None

    Raises:
        ValidationError: If the path fails security validation.
    """
    config = get_script_config()
    resolved = validate_script_path(file_path, config.script_dirs)

    # Read script content server-side (single read avoids stat+read TOCTOU)
    raw_bytes = resolved.read_bytes()
    if len(raw_bytes) > MAX_SCRIPT_FILE_BYTES:
        raise ValidationError(
            message=f"Script file too large ({len(raw_bytes)} bytes > {MAX_SCRIPT_FILE_BYTES} bytes)",
            field_name="file_path",
            value=str(resolved)[:50],
            constraint=f"max {MAX_SCRIPT_FILE_BYTES} bytes",
        )

    script_content = raw_bytes.decode("utf-8")
    effective_timeout = timeout if timeout is not None else config.script_timeout

    client = get_client()
    code_escaped = json.dumps(script_content)
    args_escaped = json.dumps(args) if args is not None else "None"
    path_escaped = json.dumps(str(resolved))

    command = f"""
import sys
import json
from io import StringIO

_code = {code_escaped}
_args = {args_escaped}
_script_path = {path_escaped}

result = {{"success": False, "script": _script_path, "output": "", "errors": {{}}}}

try:
    _old_stdout = sys.stdout
    _capture = StringIO()
    sys.stdout = _capture

    try:
        _globals = {{"__args__": _args if _args is not None else {{}}, "__name__": "__main__"}}
        exec(_code, _globals)
        result["success"] = True
    except Exception as e:
        result["errors"]["_exception"] = type(e).__name__ + ": " + str(e)
    finally:
        sys.stdout = _old_stdout
        result["output"] = _capture.getvalue()

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    with _override_timeout(client, float(effective_timeout)):
        response = client.execute(command)

    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("ScriptExecuteOutput", parsed)

script_run

script_run(code: str, language: Literal['python', 'mel'] = 'python', timeout: int | None = None) -> ScriptRunOutput

Execute raw Python or MEL code in Maya.

This tool requires MAYA_MCP_ENABLE_RAW_EXECUTION=true to be set. Use with caution — raw execution has no sandboxing.

PARAMETER DESCRIPTION
code

The code to execute.

TYPE: str

language

Code language ("python" or "mel").

TYPE: Literal['python', 'mel'] DEFAULT: 'python'

timeout

Optional timeout override in seconds.

TYPE: int | None DEFAULT: None

RETURNS DESCRIPTION
ScriptRunOutput

Dictionary with execution result: - success: Whether execution completed without error - output: Captured stdout from execution - language: The language used - errors: Error details if any, or None

RAISES DESCRIPTION
ValidationError

If raw execution is disabled or code exceeds size limit.

Source code in src/maya_mcp/tools/scripts.py
def script_run(
    code: str,
    language: Literal["python", "mel"] = "python",
    timeout: int | None = None,
) -> ScriptRunOutput:
    """Execute raw Python or MEL code in Maya.

    This tool requires MAYA_MCP_ENABLE_RAW_EXECUTION=true to be set.
    Use with caution — raw execution has no sandboxing.

    Args:
        code: The code to execute.
        language: Code language ("python" or "mel").
        timeout: Optional timeout override in seconds.

    Returns:
        Dictionary with execution result:
            - success: Whether execution completed without error
            - output: Captured stdout from execution
            - language: The language used
            - errors: Error details if any, or None

    Raises:
        ValidationError: If raw execution is disabled or code exceeds size limit.
    """
    config = get_script_config()

    if not config.raw_execution_enabled:
        raise ValidationError(
            message="Raw code execution is disabled. "
            "Set MAYA_MCP_ENABLE_RAW_EXECUTION=true to enable.",
            field_name="code",
            value="",
            constraint="raw execution enabled",
        )

    validate_raw_code(code, MAX_RAW_CODE_BYTES)

    effective_timeout = timeout if timeout is not None else config.script_timeout
    client = get_client()
    code_escaped = json.dumps(code)

    if language == "mel":
        # Wrap MEL execution in Python
        command = f"""
import sys
import json
import maya.mel
from io import StringIO

_code = {code_escaped}

result = {{"success": False, "output": "", "language": "mel", "errors": {{}}}}

try:
    _old_stdout = sys.stdout
    _capture = StringIO()
    sys.stdout = _capture

    try:
        mel_result = maya.mel.eval(_code)
        if mel_result is not None:
            _capture.write(str(mel_result))
        result["success"] = True
    except Exception as e:
        result["errors"]["_exception"] = type(e).__name__ + ": " + str(e)
    finally:
        sys.stdout = _old_stdout
        result["output"] = _capture.getvalue()

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""
    else:
        command = f"""
import sys
import json
from io import StringIO

_code = {code_escaped}

result = {{"success": False, "output": "", "language": "python", "errors": {{}}}}

try:
    _old_stdout = sys.stdout
    _capture = StringIO()
    sys.stdout = _capture

    try:
        exec(_code, {{"__name__": "__main__"}})
        result["success"] = True
    except Exception as e:
        result["errors"]["_exception"] = type(e).__name__ + ": " + str(e)
    finally:
        sys.stdout = _old_stdout
        result["output"] = _capture.getvalue()

except Exception as e:
    result["errors"]["_exception"] = str(e)

print(json.dumps(result))
"""

    with _override_timeout(client, float(effective_timeout)):
        response = client.execute(command)

    parsed: dict[str, Any] = parse_json_response(response)

    if not parsed.get("errors"):
        parsed["errors"] = None

    return cast("ScriptRunOutput", parsed)

Maya Panel

maya_panel

Maya MCP Control Panel.

This package provides a dockable Qt widget inside Maya for controlling the MCP server connection via commandPort.

Note

This module is designed to run INSIDE Maya's Python interpreter. It imports maya.cmds and uses PySide2 (Maya's Qt binding).

Example

Show the panel in Maya::

from maya_mcp.maya_panel import show_panel
show_panel()

Or from Maya's script editor::

import maya_mcp.maya_panel
maya_mcp.maya_panel.show_panel()

auto_start_if_enabled

auto_start_if_enabled() -> None

Auto-start commandPort if enabled in preferences.

This function should be called from userSetup.py to automatically open the commandPort when Maya starts.

Example

In userSetup.py::

from maya_mcp.maya_panel import auto_start_if_enabled
auto_start_if_enabled()
Source code in src/maya_mcp/maya_panel/panel.py
def auto_start_if_enabled() -> None:
    """Auto-start commandPort if enabled in preferences.

    This function should be called from userSetup.py to automatically
    open the commandPort when Maya starts.

    Example:
        In userSetup.py::

            from maya_mcp.maya_panel import auto_start_if_enabled
            auto_start_if_enabled()
    """
    if get_auto_start():
        port = get_port()
        try:
            open_command_port(port)
            logger.info("Auto-started commandPort on port %d", port)
        except Exception:
            logger.exception("Failed to auto-start commandPort")

show_panel

show_panel() -> Any

Show the MCP Control Panel.

Creates and shows the dockable panel. If the panel already exists, it will be deleted and recreated.

RETURNS DESCRIPTION
Any

The MCPControlPanel widget instance.

Example

from maya_mcp.maya_panel import show_panel panel = show_panel()

Source code in src/maya_mcp/maya_panel/panel.py
def show_panel() -> Any:
    """Show the MCP Control Panel.

    Creates and shows the dockable panel. If the panel already exists,
    it will be deleted and recreated.

    Returns:
        The MCPControlPanel widget instance.

    Example:
        >>> from maya_mcp.maya_panel import show_panel
        >>> panel = show_panel()
    """
    import maya.cmds as cmds

    global _PanelClass, _panel_instance

    # Create panel class if needed
    if _PanelClass is None:
        _PanelClass = _create_panel_class()

    # Delete existing workspace control if it exists
    if cmds.workspaceControl(PANEL_WORKSPACE_NAME, exists=True):
        cmds.deleteUI(PANEL_WORKSPACE_NAME)

    # Create and show the panel
    _panel_instance = _PanelClass()
    _panel_instance.show(dockable=True, floating=True, width=300, height=400)

    return _panel_instance

controller

CommandPort controller for Maya.

This module provides functions for managing Maya's commandPort from within Maya. It handles opening, closing, and querying the status of the commandPort.

Note

This module MUST be run inside Maya's Python interpreter as it imports maya.cmds.

Example

Open commandPort on default port::

from maya_mcp.maya_panel.controller import open_command_port
open_command_port()

Check if commandPort is open::

from maya_mcp.maya_panel.controller import is_command_port_open
if is_command_port_open():
    print("CommandPort is running")

get_open_ports

get_open_ports() -> list[str]

Get a list of currently open commandPorts.

RETURNS DESCRIPTION
list[str]

List of port names (e.g., [":7001", ":7002"]).

Example

ports = get_open_ports() print(ports) [':7001']

Source code in src/maya_mcp/maya_panel/controller.py
def get_open_ports() -> list[str]:
    """Get a list of currently open commandPorts.

    Returns:
        List of port names (e.g., [":7001", ":7002"]).

    Example:
        >>> ports = get_open_ports()
        >>> print(ports)
        [':7001']
    """
    import maya.cmds as cmds

    ports = cmds.commandPort(query=True, listPorts=True)
    return ports if ports else []

is_command_port_open

is_command_port_open(port: int = DEFAULT_PORT) -> bool

Check if commandPort is open on the specified port.

PARAMETER DESCRIPTION
port

Port number to check.

TYPE: int DEFAULT: DEFAULT_PORT

RETURNS DESCRIPTION
bool

True if the commandPort is open on the specified port.

Example

is_command_port_open(7001) True

Source code in src/maya_mcp/maya_panel/controller.py
def is_command_port_open(port: int = DEFAULT_PORT) -> bool:
    """Check if commandPort is open on the specified port.

    Args:
        port: Port number to check.

    Returns:
        True if the commandPort is open on the specified port.

    Example:
        >>> is_command_port_open(7001)
        True
    """
    port_name = f":{port}"
    return port_name in get_open_ports()

open_command_port

open_command_port(port: int = DEFAULT_PORT, source_type: str = 'python', echo_output: bool = True) -> bool

Open Maya's commandPort on the specified port.

If the port is already open, this function returns True without reopening.

PARAMETER DESCRIPTION
port

Port number to open (1-65535).

TYPE: int DEFAULT: DEFAULT_PORT

source_type

Command interpreter ("python" or "mel").

TYPE: str DEFAULT: 'python'

echo_output

If True, send command output back to client.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
bool

True if the port is now open (either opened or was already open).

RAISES DESCRIPTION
ValueError

If port is out of valid range.

RuntimeError

If commandPort could not be opened.

Example

open_command_port(7001) True

Source code in src/maya_mcp/maya_panel/controller.py
def open_command_port(
    port: int = DEFAULT_PORT,
    source_type: str = "python",
    echo_output: bool = True,
) -> bool:
    """Open Maya's commandPort on the specified port.

    If the port is already open, this function returns True without reopening.

    Args:
        port: Port number to open (1-65535).
        source_type: Command interpreter ("python" or "mel").
        echo_output: If True, send command output back to client.

    Returns:
        True if the port is now open (either opened or was already open).

    Raises:
        ValueError: If port is out of valid range.
        RuntimeError: If commandPort could not be opened.

    Example:
        >>> open_command_port(7001)
        True
    """
    import maya.cmds as cmds

    if not 1 <= port <= 65535:
        msg = f"Port must be between 1 and 65535, got {port}"
        raise ValueError(msg)

    port_name = f":{port}"

    # Check if already open
    if is_command_port_open(port):
        logger.info("CommandPort already open on %s", port_name)
        return True

    # Open the port
    try:
        cmds.commandPort(
            name=port_name,
            sourceType=source_type,
            echoOutput=echo_output,
        )
        logger.info("CommandPort opened on %s", port_name)
        return True
    except RuntimeError as e:
        logger.exception("Failed to open commandPort on %s", port_name)
        msg = f"Failed to open commandPort on {port_name}: {e}"
        raise RuntimeError(msg) from e

close_command_port

close_command_port(port: int = DEFAULT_PORT) -> bool

Close Maya's commandPort on the specified port.

If the port is not open, this function returns True.

PARAMETER DESCRIPTION
port

Port number to close.

TYPE: int DEFAULT: DEFAULT_PORT

RETURNS DESCRIPTION
bool

True if the port is now closed (either closed or was already closed).

Example

close_command_port(7001) True

Source code in src/maya_mcp/maya_panel/controller.py
def close_command_port(port: int = DEFAULT_PORT) -> bool:
    """Close Maya's commandPort on the specified port.

    If the port is not open, this function returns True.

    Args:
        port: Port number to close.

    Returns:
        True if the port is now closed (either closed or was already closed).

    Example:
        >>> close_command_port(7001)
        True
    """
    import maya.cmds as cmds

    port_name = f":{port}"

    # Check if open
    if not is_command_port_open(port):
        logger.info("CommandPort already closed on %s", port_name)
        return True

    # Close the port
    try:
        cmds.commandPort(name=port_name, close=True)
        logger.info("CommandPort closed on %s", port_name)
        return True
    except RuntimeError as e:
        logger.exception("Failed to close commandPort on %s", port_name)
        # Port might have been closed by something else
        if not is_command_port_open(port):
            return True
        msg = f"Failed to close commandPort on {port_name}: {e}"
        raise RuntimeError(msg) from e

toggle_command_port

toggle_command_port(port: int = DEFAULT_PORT) -> bool

Toggle commandPort on/off.

PARAMETER DESCRIPTION
port

Port number to toggle.

TYPE: int DEFAULT: DEFAULT_PORT

RETURNS DESCRIPTION
bool

True if the port is now open, False if closed.

Example

toggle_command_port(7001) True # Port was closed, now open toggle_command_port(7001) False # Port was open, now closed

Source code in src/maya_mcp/maya_panel/controller.py
def toggle_command_port(port: int = DEFAULT_PORT) -> bool:
    """Toggle commandPort on/off.

    Args:
        port: Port number to toggle.

    Returns:
        True if the port is now open, False if closed.

    Example:
        >>> toggle_command_port(7001)
        True  # Port was closed, now open
        >>> toggle_command_port(7001)
        False  # Port was open, now closed
    """
    if is_command_port_open(port):
        close_command_port(port)
        return False
    else:
        open_command_port(port)
        return True

get_port_status

get_port_status(port: int = DEFAULT_PORT) -> dict[str, object]

Get detailed status of the commandPort.

PARAMETER DESCRIPTION
port

Port number to check.

TYPE: int DEFAULT: DEFAULT_PORT

RETURNS DESCRIPTION
dict[str, object]

Dictionary with status information: - is_open: Whether the port is open - port: Port number - port_name: Port name string (e.g., ":7001") - all_ports: List of all open ports

Example

get_port_status(7001)

Source code in src/maya_mcp/maya_panel/controller.py
def get_port_status(port: int = DEFAULT_PORT) -> dict[str, object]:
    """Get detailed status of the commandPort.

    Args:
        port: Port number to check.

    Returns:
        Dictionary with status information:
            - is_open: Whether the port is open
            - port: Port number
            - port_name: Port name string (e.g., ":7001")
            - all_ports: List of all open ports

    Example:
        >>> get_port_status(7001)
        {'is_open': True, 'port': 7001, 'port_name': ':7001', 'all_ports': [':7001']}
    """
    all_ports = get_open_ports()
    port_name = f":{port}"

    return {
        "is_open": port_name in all_ports,
        "port": port,
        "port_name": port_name,
        "all_ports": all_ports,
    }

preferences

Preferences management for Maya MCP Panel.

This module handles saving and loading user preferences for the MCP panel, including auto-start settings and port configuration.

Preferences are stored using Maya's optionVar system, which persists across Maya sessions.

Example

Get and set port::

from maya_mcp.maya_panel.preferences import get_port, set_port
port = get_port()  # Returns 7001 by default
set_port(7002)

Get and set auto-start::

from maya_mcp.maya_panel.preferences import get_auto_start, set_auto_start
set_auto_start(True)

get_port

get_port() -> int

Get the configured commandPort port number.

RETURNS DESCRIPTION
int

Port number from preferences, or DEFAULT_PORT if not set.

Example

get_port() 7001

Source code in src/maya_mcp/maya_panel/preferences.py
def get_port() -> int:
    """Get the configured commandPort port number.

    Returns:
        Port number from preferences, or DEFAULT_PORT if not set.

    Example:
        >>> get_port()
        7001
    """
    import maya.cmds as cmds

    if cmds.optionVar(exists=OPTION_PORT):
        return int(cmds.optionVar(query=OPTION_PORT))
    return DEFAULT_PORT

set_port

set_port(port: int) -> None

Set the commandPort port number in preferences.

PARAMETER DESCRIPTION
port

Port number to save (1-65535).

TYPE: int

RAISES DESCRIPTION
ValueError

If port is out of valid range.

Example

set_port(7002)

Source code in src/maya_mcp/maya_panel/preferences.py
def set_port(port: int) -> None:
    """Set the commandPort port number in preferences.

    Args:
        port: Port number to save (1-65535).

    Raises:
        ValueError: If port is out of valid range.

    Example:
        >>> set_port(7002)
    """
    import maya.cmds as cmds

    if not 1 <= port <= 65535:
        msg = f"Port must be between 1 and 65535, got {port}"
        raise ValueError(msg)

    cmds.optionVar(intValue=(OPTION_PORT, port))
    logger.debug("Saved port preference: %d", port)

get_auto_start

get_auto_start() -> bool

Get the auto-start preference.

RETURNS DESCRIPTION
bool

True if commandPort should auto-start on Maya launch.

Example

get_auto_start() False

Source code in src/maya_mcp/maya_panel/preferences.py
def get_auto_start() -> bool:
    """Get the auto-start preference.

    Returns:
        True if commandPort should auto-start on Maya launch.

    Example:
        >>> get_auto_start()
        False
    """
    import maya.cmds as cmds

    if cmds.optionVar(exists=OPTION_AUTO_START):
        return bool(cmds.optionVar(query=OPTION_AUTO_START))
    return DEFAULT_AUTO_START

set_auto_start

set_auto_start(enabled: bool) -> None

Set the auto-start preference.

PARAMETER DESCRIPTION
enabled

Whether to auto-start commandPort on Maya launch.

TYPE: bool

Example

set_auto_start(True)

Source code in src/maya_mcp/maya_panel/preferences.py
def set_auto_start(enabled: bool) -> None:
    """Set the auto-start preference.

    Args:
        enabled: Whether to auto-start commandPort on Maya launch.

    Example:
        >>> set_auto_start(True)
    """
    import maya.cmds as cmds

    cmds.optionVar(intValue=(OPTION_AUTO_START, int(enabled)))
    logger.debug("Saved auto-start preference: %s", enabled)

reset_preferences

reset_preferences() -> None

Reset all MCP preferences to defaults.

Example

reset_preferences()

Source code in src/maya_mcp/maya_panel/preferences.py
def reset_preferences() -> None:
    """Reset all MCP preferences to defaults.

    Example:
        >>> reset_preferences()
    """
    import maya.cmds as cmds

    if cmds.optionVar(exists=OPTION_PORT):
        cmds.optionVar(remove=OPTION_PORT)

    if cmds.optionVar(exists=OPTION_AUTO_START):
        cmds.optionVar(remove=OPTION_AUTO_START)

    logger.info("Reset all MCP preferences to defaults")

get_all_preferences

get_all_preferences() -> dict[str, object]

Get all MCP preferences as a dictionary.

RETURNS DESCRIPTION
dict[str, object]

Dictionary with all preference values.

Example

get_all_preferences()

Source code in src/maya_mcp/maya_panel/preferences.py
def get_all_preferences() -> dict[str, object]:
    """Get all MCP preferences as a dictionary.

    Returns:
        Dictionary with all preference values.

    Example:
        >>> get_all_preferences()
        {'port': 7001, 'auto_start': False}
    """
    return {
        "port": get_port(),
        "auto_start": get_auto_start(),
    }