Skip to main content

Errors

Raise motionmcp.ProtocolError (or one of the typed convenience constructors) for any protocol-level failure. The SDK catches it, serialises it as the standard MMCP error envelope, and returns the correct HTTP status.

from motionmcp import ProtocolError
from motionmcp.errors import (
unknown_model, unknown_joint, frame_out_of_range,
unsupported_constraint, retargeting_unsupported,
version_unsupported,
)

Anything that isn't a ProtocolError becomes 500 internal_error.

The wire envelope

Every error response has the same shape:

{
"error": {
"code": "unknown_joint",
"message": "joint 'LefHand' is not in the request skeleton",
"details": { "skeleton_joints": ["Hips", "LeftHand", "RightHand"] }
}
}

See Errors reference → for the full code table and HTTP status mapping.

What the SDK raises for you

The server already raises these — you don't need to:

CodeWhen
schema_validationRequest body fails Pydantic validation
unknown_modelrequest.model is not in the registry
version_unsupportedprotocol_version major doesn't match this server
retargeting_unsupportedCustom skeleton sent to a supports_retargeting=False model
unsupported_constraintConstraint type isn't in model.supported_constraints
unknown_jointConstraint references a joint name not in the skeleton
frame_out_of_rangeConstraint frame outside [0, total_frames - 1]
invalid_optionsnum_samples / prompt length / duration / constraint count exceeds limits

What you raise

Conditions only your backbone knows about:

CodeHelperUse for
model_unavailableProtocolError("model_unavailable", …)Weights still loading at request time
resource_exhaustedProtocolError("resource_exhausted", …)GPU full, queue saturated
timeoutProtocolError("timeout", …)Generation exceeded your deadline
invalid_optionsProtocolError("invalid_options", …)Backbone-specific option validation
unauthorized / forbiddenProtocolError("unauthorized", …)If you wrap auth in your backbone
from motionmcp import ProtocolError

async def generate(self, req):
if not self.weights_loaded:
raise ProtocolError(
"model_unavailable",
"weights are still loading; retry in a few seconds",
)

if not self.gpu_pool.acquire(timeout=5.0):
raise ProtocolError(
"resource_exhausted",
"GPU pool exhausted",
details={"queue_depth": self.gpu_pool.depth},
)
try:
return await self._run(req)
finally:
self.gpu_pool.release()

The SDK will return the right HTTP status (503 / 503 here) and serialise the envelope.

Retry semantics (for client authors)

When a client sees an error, the code dictates whether to retry:

CodeRetry?
Anything in the 4xx "fix the request" family — schema_validation, unknown_*, unsupported_*, invalid_*, version_unsupported, payload_too_large, retargeting_unsupportedNo — fix the request
rate_limited, resource_exhausted, model_unavailable, timeoutYes — honor Retry-After, exponential backoff
internal_errorNo — backbone failure, not transient
unauthorized, forbiddenNo — fix credentials

Convenience constructors

The typed helpers in motionmcp.errors return pre-shaped ProtocolError instances. They include sensible default details so clients can recover automatically.

from motionmcp.errors import (
unknown_model, unknown_joint, frame_out_of_range,
unsupported_constraint, retargeting_unsupported,
version_unsupported,
)

# unknown_model("foo", available=["good", "models"])
# unknown_joint("LefHand", skeleton_joints=["Hips", "LeftHand", "RightHand"])
# frame_out_of_range(frame=999, total_frames=120)
# unsupported_constraint("audio_alignment", supported=["pose_keyframe"])
# retargeting_unsupported()
# version_unsupported("2.0", supported_majors=["1"])

Use these in your backbone whenever they fit — the resulting details block is what clients expect.

Custom details

For codes the helpers don't cover, pass your own details:

raise ProtocolError(
"invalid_options",
"diffusion_steps must be a multiple of 10 for this model",
details={"got": req.options.diffusion_steps, "stride": 10},
)

The details payload is JSON-serialisable and goes verbatim to the client. Don't put PII or auth secrets here.