Add your robot
From an MJCF or a URDF to a validated robot page — a zip dropped in the browser, or three commands: what is derived, what you must declare, and what gets refused.
A robot on this hub is declared, not programmed. You bring a MuJoCo model
(an MJCF, or a URDF that is converted for you) and take it to a robot page with a
model that stands in the Sim tab. In the browser that is a zip dropped on
Add your robot — step 1 of the Quickstart —
and from a terminal it is three commands. Both run the same lucen robot init
and lucen robot validate, and the hub checks what they produce the same way.
Drop a zip
On Add your robot, drop a .zip of the folder that holds the model
and its meshes. The zip is read in your browser before anything uploads: a path
that climbs out of it (../), an absolute path, a symlink or a file that inflates
far past its packed size is refused and named. Pick the model file if there are
several, and answer what lucen robot init needs — the control rate and the action,
and the base for a URDF; nothing is filled in for you. Gains, armature and friction
are optional flags — the gains are needed when the model states none (torque motors,
or no actuators at all).
The default pose is read, not typed. The page compiles the model with MuJoCo in
the browser: a model with a keyframe starts from it, and there is nothing to declare.
Without one, every joint would start at qpos0, and a joint whose qpos0 lies
outside its soft range (on a hard limit, say) fails every sim-gate cell however long
a policy trains. So the page proposes a pose — each such joint a quarter of its range
inside the nearer limit, every other joint left where it is — draws it live, and lets
you change any value; what you send reaches init as --default-pose. A URDF's
ranges are read from its <limit> elements but it is converted on the check, so it
gets the table without the drawing. Once the robot is published, its Sim tab can
set the default pose again from the sliders (Use this pose as the default); it says
first that the interface fingerprint changes and that policies trained for the old
pose will read mismatch until they are retrained.
Upload and check hashes every file and uploads it straight to storage into a
private robot repo; a file the hub already holds is sent as 0 bytes. The hub never
runs MuJoCo, so the check is a job — the same lucen robot init and lucen robot validate as below — on a hosted CPU sandbox where the deployment has one, or on a
machine of yours that runs this (the check page prints the install and sign-in lines
too; they are the Quickstart's jobs on your own machine):
lucen robot worker
The report shows the checks, the draft card and what needs you. Publish links what the check wrote into the repo — the hub hashes every file itself — and stores the card with the job's report, as you. A failing check says what to change; Drop again with these answers brings your answers back, and only what changed uploads.
No model file yet? Let your agent write it
A robot that exists only as CAD, a datasheet or a bag of parts has no URDF or MJCF
to drop, and the hub does not read a STEP file. Add your robot → with your
agent hands you a prompt for your own AI agent (Claude Code,
Cursor, ChatGPT, any agent that writes files): what a Lucen robot file is — one
model file and its STL/OBJ meshes zipped, within the drop's limits, .dae refused,
package:// resolved, what a URDF conversion drops, every moving link with an
<inertial>, joint names as the policy's index space, a floating or fixed base, a
default pose — what the file cannot carry and you will declare at the drop (the
interface fields above, by name), the checks it will face with their codes, how to
hand the zip back (the drop, the three commands below, or the MCP tools), and the
rule that a model is fixed and a check is never gamed. A few fields — the robot's
name, kind, base, joint count, motors and gains, control rate, a reference — shape
the text. The agent hands back a zip and a note; the zip goes through the same drop
and the same checks as any other.
What you are declaring
A robot card describes the hardware: degrees of freedom, actuator groups,
mass, per-joint limits, and mjcf_path — the model file inside the repo.
The card's interface is what the robot is to a policy. A URDF or an MJCF
describes geometry and mass; it does not say what index 3 of an action vector
is, how hard the motors push, or how fast the control loop runs. A wrong value
of any of those fails silently, at deploy time. The interface says them once:
| Field | What it fixes | Where init gets it |
|---|---|---|
joint_order | the policy's index space | derived: the model's named hinge/slide joints, in model order |
base, init_base_height_m | fixed vs floating | derived from the free joint; a URDF must be told (--base) |
default_pose, default_keyframe | where the robot starts; what position_offset is relative to | derived: keyframe 0, else qpos0; --keyframe NAME reads another (a model whose first keyframe is not its standing pose); --default-pose NAME=VALUE,… declares one — what the drop sends, and what init suggests when qpos0 puts a joint outside its soft range |
control_rate_hz, physics_dt_s | the rates a policy is trained and run at | --control-hz is required; the timestep is the model's |
action | position_offset | position_absolute | velocity | torque, scale, clip | --action is required |
actuation[] | per joint group: mode, kp/kd, effort limit, armature, friction | derived from the MJCF's position actuators, else --kp/--kd/--armature |
proprioception[] | what the real robot can observe | asked; suggested from the MJCF's sensors |
parts[], feet[], end_effectors[], cameras[] | named slices and places | candidates from the kinematic tree, to confirm |
command_space, safety | what is commanded, how the robot stops | asked (--command); safety has documented defaults |
Its sha256 is the interface_fingerprint. Swap two joints and it changes:
that is the point. A policy or a dataset built for one index space must never
be accepted for another.
From a terminal
The same flow as three commands:
| Command | What it does | What it proves |
|---|---|---|
lucen robot init PATH | compiles the model with real MuJoCo and drafts robot_card.json | the model compiles; joint order, base type, default pose, ranges, mass and timestep are read off it, not typed |
lucen robot validate DIR | checks the card against the model, then runs it | every file is there, every name is a real joint, the robot holds its default pose under gravity on the gains you declared, and every joint actually moves |
lucen robot publish OWNER/SLUG DIR | creates the repo, pushes the files, sets the card | the hub checked the files it now holds and accepted the card |
Everything below was run verbatim against a local hub, with three robots that are not in this repository: a quadruped and an arm from MuJoCo Menagerie, and a URDF arm from its vendor's own repository.
The robot commands compile and simulate, so they need the CLI's rollout
extra — MuJoCo 3.12.0, the same build the Sim tab runs in your browser, so a
model that compiles here compiles there:
curl -fsSL https://api.mousemouse.ai/install.sh | sh -s -- "cli[rollout]"
Without the extra every lucen robot command refuses and prints that line.
You also need to be signed in (From a terminal) with a key
that has the write scope. Nothing on this page needs train: adding a robot moves
data, never money.
Nothing is guessed
init writes three kinds of value, and says which is which:
- derived — read off the compiled model;
- declared — what no model file says and you passed as a flag. Two flags
have no default because no default is defensible:
--control-hzand--action. A URDF adds a third,--base; needs_you— everything else, listed inrobot_card.jsonitself (a model with no keyframe whoseqpos0sits outside a joint's soft range gets an entry carrying a suggested pose and the exact--default-poseflag; the card keepsqpos0until you pass it back). Each entry names the field, says why a program cannot know it, and has aseverity:blockingfails validation until you decide,recommendedmeans a schema default was written that is probably not yours,confirmmeans a candidate was derived from topology.
Resolve an entry by editing the field and deleting the entry. The list is stripped before the card is sent to the hub, so it is a worklist, not metadata.
A floating-base robot: Unitree Go2
Menagerie keeps each robot in one file (go2.xml) and a scene with a floor in
another (scene.xml, which <include>s it). Point init at the scene: a
floating base with nothing to stand on falls forever, and validate warns about
exactly that.
git clone --depth 1 --filter=blob:none --sparse https://github.com/google-deepmind/mujoco_menagerie.git
git -C mujoco_menagerie sparse-checkout set unitree_go2 universal_robots_ur5e
lucen robot init mujoco_menagerie/unitree_go2/scene.xml --control-hz 50 --action position_offset --action-scale 0.25 --kp 20 --kd 0.5 --proprioception joint_pos,joint_vel,base_ang_vel,projected_gravity --command velocity2d
The Go2's MJCF has torque motors, which state no gains — so without --kp and
--kd the draft carries a blocking entry and validate fails on it. The
gains above are a starting point, not a recommendation: validate is what tells
you what they do on this robot.
lucen robot validate mujoco_menagerie/unitree_go2
The report goes to stderr for a person and, with --json, to stdout for a
program; the exit code is non-zero on any fail:
lucen robot validate mujoco_menagerie/unitree_go2 --json
The interesting line is the one that ran the model:
WARN stability.settle Held the default pose for 3 s under gravity: the base dropped
0.074 m from the declared 0.270 m — `init_base_height_m` is higher
than the robot stands; `RL_calf_joint` drifted 0.442 from its default.
That is a true statement about kp = 20 on a 15 kg quadruped, found before any
training, and it is a warn: the robot did not fall. Publishing is allowed.
lucen robot publish you/go2 mujoco_menagerie/unitree_go2 --public
Run it twice: the second run uploads 0 bytes and stores the same fingerprint.
The report validate just produced is stored with the card
(RobotCard.validation, dated): the robot page prints its pass · warn · fail
counts and each warning from it, labelled as this publisher's report — the hub
never runs MuJoCo, so it repeats only the static checks itself.
Then train for it — Train a policy on the robot's page, or from here. The
generic trainer reads the interface — a floating base gets a velocity task — and
this one line queues a run a lucen worker run picks up:
lucen runs create --template ppo_mujoco --robot you/go2 --executor worker
A fixed-base robot: UR5e
The UR5e's MJCF has position actuators, so its gains, force limits and the
home keyframe are all derived and there is nothing blocking:
lucen robot init mujoco_menagerie/universal_robots_ur5e/ur5e.xml --control-hz 125 --action position_absolute --proprioception joint_pos,joint_vel --command joint_target
lucen robot validate mujoco_menagerie/universal_robots_ur5e
lucen robot publish you/ur5e mujoco_menagerie/universal_robots_ur5e --public
A URDF: SO-101
A URDF is converted through MuJoCo's own compiler into an MJCF written next
to it, and the conversion says what it lost. URDF has no actuators, MJCF joints
have no velocity limit (the card's limits keep it), transmissions and
<gazebo> blocks mean nothing outside ROS. Meshes named by package:// are
resolved by walking up from the URDF; one that lives outside the directory you
publish is copied flat into meshes/. A mesh MuJoCo cannot read (.dae) is
named, with --discard-visual as the way through when only the visuals use it.
A moving link with no <inertial> is named, with the mass MuJoCo made up for it.
git clone --depth 1 --filter=blob:none --sparse https://github.com/TheRobotStudio/SO-ARM100.git
git -C SO-ARM100 sparse-checkout set Simulation/SO101
mkdir so101 && cp SO-ARM100/Simulation/SO101/so101_new_calib.urdf so101/ && cp -R SO-ARM100/Simulation/SO101/assets so101/assets && cp SO-ARM100/LICENSE so101/
lucen robot init so101/so101_new_calib.urdf --base fixed --control-hz 50 --action position_absolute --kp 17.8 --kd 0.6 --armature 0.028 --friction-loss 0.052 --proprioception joint_pos,joint_vel --command joint_target
--armature matters here. A URDF cannot carry the rotor inertia a gearbox
reflects onto a joint, and without it the SO-101's light wrist makes kp = 17.8
unstable at a 2 ms timestep — validate reports that as model.gains. The
four numbers are the STS3215 servo's, read from the vendor's own MuJoCo file
(Simulation/SO101/joints_properties.xml).
lucen robot validate so101
This one fails, and says why (the same report as JSON, for an agent to
iterate on, is lucen robot validate so101 --json):
lucen robot validate so101 --json
WARN model.self_collision 1 body pair(s) already interpenetrate at the default pose:
['base_link / shoulder_link (27.9 mm)'].
fix: If the overlap is only mesh detail at a joint, exclude the pair in the MJCF:
<contact><exclude body1="base_link" body2="shoulder_link"/></contact>.
FAIL stability.settle `shoulder_pan` drifted 1.274 from its default under gravity: the
declared gains do not hold the pose.
fix: … The pose starts in contact — read `model.self_collision` first.
FAIL stability.joint_response 1 joint(s) barely move under the declared gains: `shoulder_pan`
reached 4% of a +0.20 step (below 25%). No policy can use a joint
that does not move.
fix: Bodies already touching at the default pose usually explain it: base_link / shoulder_link
(27.9 mm). If that is mesh detail at a joint, exclude the pair: <contact>…</contact>.
The two failures are one problem seen from two sides: the shoulder does not merely sag, it can barely turn at all. The URDF's collision meshes are its visual meshes, and the base and the shoulder overlap at the joint. MuJoCo filters parent–child contacts except when the parent is welded to the world, which is every fixed-base robot's first link. Nothing is repaired for you — the report names the line to add, and you add it:
sed -i.bak 's#</mujoco>#<contact><exclude body1="base_link" body2="shoulder_link"/></contact></mujoco>#' so101/so101_new_calib.xml && rm so101/so101_new_calib.xml.bak
lucen robot validate so101
lucen robot publish you/so101 so101 --public
A fixed base gets a reach task from the same generic trainer:
lucen runs create --template ppo_mujoco --robot you/so101 --executor worker
The reference robots
The public robots under lucen/ went through exactly these three commands. Each
one's flags, where every value came from, the commit its model is pinned to and its licence
are data in tools/reference_robots.yaml, and each robot's README ends with that table. A
robot validate fails is left out, with the validator's words, rather than tuned until it
passes.
What the hub checks
publish pushes the files before it sets the card, because the hub refuses
a card whose mjcf_path is not already a file of the repo. Before storing a
card the hub checks, statically — hardened XML parsing, never MuJoCo:
| Check | Refused when |
|---|---|
mjcf.exists | mjcf_path is not a file of the repo |
mjcf.parses | it is not MuJoCo XML, or is larger than 4 MiB |
assets.closure | a referenced mesh, texture or <include> is not in the repo, or climbs out of it with ../ |
interface.joint_order | a name is not a joint of the model, or is a ball or free joint |
interface.base | fixed/floating contradicts the model's free joint |
interface.keyframe | default_keyframe is not a keyframe of the model |
A refusal is a 422 whose errors[] carry the check code as type, and
nothing is written. POST /v1/robots/{owner}/{repo}/validate runs the same
checks without writing and answers 200 with the report either way — that is
the call an agent iterates against. It needs only the read scope, and a
private robot you cannot see is a 404.
Editing a card later
lucen robot card get you/go2 -o card.json
lucen robot card set you/go2 -f card.json
card set is a full replace, checked exactly like publish.
From an agent
The MCP server has the same flow as tools. init_robot and
publish_robot work on a local path, because an MCP server runs on your
machine next to the model files; publish_robot is a dry run by default and
returns the full MuJoCo report. validate_robot asks the hub for its report
without writing, and set_robot_card / create_repo are the raw writes. A
refused card reaches the model as one line per failed check, each with its code.
What is not here
- No card editor. The drop asks only what
initneeds, and the Sim tab sets only the default pose; answering any otherneeds_youitem means editing the model or the card and checking again, from the CLI (lucen robot card set) or with a new drop. A STEP file is not read yet. - The hub never runs MuJoCo. Its checks are static. Whether a robot stands is answered by a job — on your machine, or a hosted sandbox — and the report is stored with the card as the publisher's dated claim, never as a verdict of the hub's.
validateis a smoke test, not a controller review. Three seconds of holding a pose finds a wrong default height, gains that cannot carry the arm, a timestep too coarse for kp, links that start inside each other. It does not tell you the robot will walk.