Skip to content
Docs menu

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:

FieldWhat it fixesWhere init gets it
joint_orderthe policy's index spacederived: the model's named hinge/slide joints, in model order
base, init_base_height_mfixed vs floatingderived from the free joint; a URDF must be told (--base)
default_pose, default_keyframewhere the robot starts; what position_offset is relative toderived: 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_sthe rates a policy is trained and run at--control-hz is required; the timestep is the model's
actionposition_offset | position_absolute | velocity | torque, scale, clip--action is required
actuation[]per joint group: mode, kp/kd, effort limit, armature, frictionderived from the MJCF's position actuators, else --kp/--kd/--armature
proprioception[]what the real robot can observeasked; suggested from the MJCF's sensors
parts[], feet[], end_effectors[], cameras[]named slices and placescandidates from the kinematic tree, to confirm
command_space, safetywhat is commanded, how the robot stopsasked (--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:

CommandWhat it doesWhat it proves
lucen robot init PATHcompiles the model with real MuJoCo and drafts robot_card.jsonthe model compiles; joint order, base type, default pose, ranges, mass and timestep are read off it, not typed
lucen robot validate DIRchecks the card against the model, then runs itevery 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 DIRcreates the repo, pushes the files, sets the cardthe 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-hz and --action. A URDF adds a third, --base;
  • needs_you — everything else, listed in robot_card.json itself (a model with no keyframe whose qpos0 sits outside a joint's soft range gets an entry carrying a suggested pose and the exact --default-pose flag; the card keeps qpos0 until you pass it back). Each entry names the field, says why a program cannot know it, and has a severity: blocking fails validation until you decide, recommended means a schema default was written that is probably not yours, confirm means 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:

CheckRefused when
mjcf.existsmjcf_path is not a file of the repo
mjcf.parsesit is not MuJoCo XML, or is larger than 4 MiB
assets.closurea referenced mesh, texture or <include> is not in the repo, or climbs out of it with ../
interface.joint_ordera name is not a joint of the model, or is a ball or free joint
interface.basefixed/floating contradicts the model's free joint
interface.keyframedefault_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 init needs, and the Sim tab sets only the default pose; answering any other needs_you item 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.
  • validate is 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.