Backbone
Backbone is the only thing you implement. It has two required methods —
capabilities() and generate() — plus two optional lifecycle hooks.
from motionmcp import Backbone, ModelSpec, GenerateRequest, MotionResult
class MyBackbone(Backbone):
def setup(self) -> None: ... # optional
def teardown(self) -> None: ... # optional
def capabilities(self) -> ModelSpec: ...
async def generate(self, req: GenerateRequest) -> MotionResult: ...
capabilities()
Returns a ModelSpec describing the model. The SDK serialises it into
the per-model entry under /capabilities.models[].
ModelSpec is a Pydantic model with sensible defaults — only id,
fps, and canonical_skeleton are required.
from motionmcp import ModelSpec, Skeleton, Joint
ModelSpec(
id="my-model-v1",
fps=30.0,
canonical_skeleton=Skeleton(joints=[
Joint(name="Hips", parent=None,
rest_translation=(0.0, 0.95, 0.0),
rest_rotation=(0.0, 0.0, 0.0, 1.0)),
]),
# All fields below are optional with the defaults shown:
supports_retargeting=False,
supports_async=False,
supported_constraints=[], # empty = the model accepts no constraints
supported_guidance_types=["nocfg", "regular", "separated"],
predicted_contact_joints=[],
native_clip_seconds=10.0,
chunking="none", # or "stitched"
recommended_max_duration_seconds=12.0,
# limits=Limits(max_duration_seconds=30.0, max_num_samples=16, …)
)
See Capabilities reference → for what every field controls on the wire.
The SDK calls capabilities() on every /capabilities and /generate
request to look up the spec. Keep it cheap — return a cached object if
your spec depends on slow lookups.
generate(request)
Runs the model on a validated GenerateRequest and returns a
MotionResult. May be async def or sync — the SDK detects the form
and awaits when needed.
async def generate(self, req: GenerateRequest) -> MotionResult:
# req.skeleton, req.segments, req.constraints — parsed Pydantic models
# req.total_frames — int helper
# req.fps(spec.fps) — effective fps
# req.options.num_samples — None-safe access via .options
rotations = run_my_model(req)
translations = run_my_root(req)
return MotionResult(
rotations=rotations,
root_translations=translations,
)
The shape of MotionResult is documented in
Results →. The SDK encodes it to glTF for you.
Sync vs async
Both work. Async is preferred when generation does I/O (queueing on a GPU worker, calling out to another service). Sync is fine for a self-contained model:
class FastBackbone(Backbone):
def generate(self, req: GenerateRequest) -> MotionResult:
...
The SDK uses inspect.iscoroutinefunction to pick the right path.
Raising errors
Raise motionmcp.ProtocolError (or one of the typed convenience
constructors) for protocol-level failures. The SDK converts them to the
standard error envelope with the right HTTP status:
from motionmcp.errors import unknown_joint, ProtocolError
if user_joint not in self.skeleton_names:
raise unknown_joint(user_joint, sorted(self.skeleton_names))
if not self.weights_loaded:
raise ProtocolError("model_unavailable", "weights still loading")
Anything that isn't a ProtocolError becomes 500 internal_error.
See Errors → for the full list.
setup() and teardown()
Optional hooks called once at server startup and shutdown. Use them for model loading, GPU allocation, warm caches.
class MyBackbone(Backbone):
def setup(self) -> None:
self.model = torch.load("weights.pt").to("cuda").eval()
def teardown(self) -> None:
del self.model
torch.cuda.empty_cache()
Default implementations are no-ops; override only when needed.
Putting it together
A real backbone with a loaded model:
import numpy as np
import torch
from motionmcp import (
Backbone, ModelSpec, GenerateRequest, MotionResult,
Skeleton, serve,
)
from motionmcp.errors import unknown_joint
class SomaBackbone(Backbone):
def __init__(self, weights_path: str) -> None:
self.weights_path = weights_path
self.model = None
def setup(self) -> None:
self.model = torch.load(self.weights_path).to("cuda").eval()
self._spec = ModelSpec(
id="soma-v1",
fps=30.0,
canonical_skeleton=Skeleton.model_validate(
self.model.canonical_skeleton_dict
),
supports_retargeting=True,
supported_constraints=["root_path", "pose_keyframe"],
)
def capabilities(self) -> ModelSpec:
return self._spec
async def generate(self, req: GenerateRequest) -> MotionResult:
with torch.no_grad():
out = self.model.run(req) # your I/O here
return MotionResult(
rotations=out.rotations.cpu().numpy(),
root_translations=out.root_translations.cpu().numpy(),
)
if __name__ == "__main__":
serve(SomaBackbone("weights.pt"))