Metadata-Version: 2.4
Name: lucen-mcp
Version: 0.1.0
Summary: MouseMouse MCP server (`lucen-mcp`) — the hub's tools, for an agent
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Dist: mcp>=2.1
Requires-Dist: lucen-sdk>=0.1.0,<0.2
Requires-Python: >=3.12
Project-URL: Homepage, https://mousemouse.ai
Project-URL: Documentation, https://mousemouse.ai/docs/mcp
Project-URL: Repository, https://github.com/ICE-CUBA/lucen-hub
Description-Content-Type: text/markdown

# `lucen-mcp` — MouseMouse, as an MCP server

The hub's tools over the MouseMouse REST API, for an agent. Every hub tool is one
operation in the hub's OpenAPI contract (served at `/openapi.yaml`, browsable at
[mousemouse.ai/docs/api](https://mousemouse.ai/docs/api)), called through the
generated Python client `lucen-sdk`; nothing here reimplements a query, a
validation rule or an access check.

The reason to attach this server is the **Training Coach**: a corpus of 171
experience cards and 22 numbered doctrine rules distilled from real
robot-learning runs, searchable with no API key and no cost. Everything else
here is a hub you could also browse.

## Install

From PyPI, as a tool on your `PATH` (`uv`, or `pipx install lucen-mcp`):

```bash
uv tool install lucen-mcp
```

That puts a `lucen-mcp` executable on your `PATH`. It speaks MCP over stdio and
takes no arguments. (A MouseMouse hub also serves its own copy of these wheels:
`curl -fsSL <hub API>/install.sh | sh -s -- mcp` installs the version built
from the hub's own commit; see the hub's docs page *Your own AI*.)

The two **local** robot tools (`init_robot`, `publish_robot`, M17a) compile and
simulate a model on this machine, so they need the CLI's `rollout` extra
(MuJoCo 3.12.0) in the same environment; without it they refuse and name this
line. Every other tool works with the plain install above.

```bash
uv tool install --with "lucen-cli[rollout]" lucen-mcp
```

## Configure

### Claude Code

Project-scoped, in `.mcp.json` at the root of the project you want it in:

```json
{
  "mcpServers": {
    "lucen": {
      "command": "lucen-mcp",
      "env": {
        "LUCEN_API_URL": "http://127.0.0.1:8000",
        "LUCEN_API_KEY": "lucen_sk_..."
      }
    }
  }
}
```

Or from the terminal, which writes the same entry:

```bash
claude mcp add lucen --env LUCEN_API_URL=http://127.0.0.1:8000 --env LUCEN_API_KEY=lucen_sk_... -- lucen-mcp
```

### Claude Desktop

`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS,
`%APPDATA%\Claude\claude_desktop_config.json` on Windows:

```json
{
  "mcpServers": {
    "lucen": {
      "command": "lucen-mcp",
      "env": {
        "LUCEN_API_URL": "http://127.0.0.1:8000",
        "LUCEN_API_KEY": "lucen_sk_..."
      }
    }
  }
}
```

If `lucen-mcp` is not on the launcher's `PATH` (Claude Desktop does not read
your shell profile), use the absolute path `uv tool install` printed — typically
`~/.local/bin/lucen-mcp`.

**Leave `LUCEN_API_KEY` out entirely** and the server still runs: every tool
whose scope is `none` (repo search, repo and robot-card reads, dataset
episodes, the whole coach corpus) works anonymously against public data.

## Environment

| Variable | Default | Meaning |
|---|---|---|
| `LUCEN_API_URL` | *(required)* | The hub's API URL. There is no built-in default: without it the server refuses to start and says so on stderr. `http://127.0.0.1:8000` for a local stack (`make dev-local`). |
| `LUCEN_API_KEY` | *(unset)* | A scoped key, `lucen_sk_...`. Optional. |
| `LUCEN_MCP_TIMEOUT_S` | `180` | Per-request timeout. A coach call runs a model server-side; do not set this below ~120. |

The same two names the CLI and the SDKs read, so one exported key serves every
surface.

**The key is only ever an environment variable.** No tool takes a credential as
an argument (it would land in the model's transcript) and there is no
command-line flag for one (it would land in the process table and your shell
history). On a local stack, mint one with
`tools/dev_api_key.py` in the hub's source repository; in a deployment, mint one
in the web app under Settings → API keys.

## Tools

Scopes nest: `read` ⊂ `write` ⊂ `train`. `none` means the tool works with no
key at all, against public data.

| Tool | Scope | What it does |
|---|---|---|
| `search_repos` | none | Search datasets, models and robot repos (substring + tags + kind). |
| `get_repo` | none | One repo with its dataset meta / model meta / robot card. |
| `get_robot_card` | none | A robot's DoF, mass, actuators, joint limits and `mjcf_path`. |
| `get_dataset_episodes` | none | The episodes in a LeRobot-format dataset repo. |
| `search_experience` | none | **IDF-ranked search over the 171 experience cards.** Free. |
| `get_coach_doctrine` | none | The 22 numbered doctrine rules a report cites by id. Free. |
| `get_training_advice` | `train` | Diagnose a run: diagnosis, cited proposals, experiment plan. **Costs money.** |
| `get_training_plan` | `train` | Plan a programme from a task brief, before any run exists. **Costs money.** |
| `get_run_status` | `read` | One training run: status, executor, who claimed it, lineage, artifacts. |
| `get_run_logs` | `read` | The last N lines of a run's log plus every metrics line, replayed once; ANSI escapes stripped unless `strip_ansi=false`. |
| `get_run_config` | `read` | A run's resolved config grouped by RL structure (the hub's one rule table), the diff against its parent — `cross_shape` when the parent is an imported manifest: only the mapped key pairs compared, the rest "no record", never "unchanged" — and a round's declared keys. Readable while the run trains. |
| `get_run_gates` | `read` | The sim gates a run's checkpoints got while it trained: step × cells × verdict, posters, the tripwire. |
| `cancel_training` | `train` | Cancel a run: queued ends now; running is stopped by its runner within seconds. |
| `estimate_training` | `train` | Price a recipe before submitting it. Creates nothing. |
| `recommend_training` | `read` | Which GPU tier a recipe needs — the hub's deterministic planner, with the arithmetic in `why[]`. Creates nothing. |
| `submit_training` | `train` | Queue a run for a **self-hosted `lucen worker`** — dry run by default. |
| `rollout_in_sim` | `train` | Queue a sim rollout — the policy in MuJoCo on the robot's MJCF, scored PASS/FAIL per command cell by the hub — for a **rollout worker** (`lucen rollout worker`). Dry run by default. |
| `get_rollout_status` | `read` | One rollout: status, who claimed it, per-cell verdicts, presigned video / trajectory / scorecard URLs. |

| `request_deploy` | `write` | **Ask** for a policy on a robot — a human must approve in the web app; starts nothing |
| `get_device_status` | `read` | A device's pending requests, approvals and the driver's signed trail |
| `serve_model` | `train` | Create a hub inference endpoint for a **skill-layer** model (a reflex model is the hub's 409); waits for a `lucen serve` you start. Dry run by default. No tool opens a session. |
| `get_endpoint_status` | `read` | One endpoint: status, tier, server, and per-session metering (chunks, GPU-seconds, service and round-trip p50/p95, cost). |
| `plan_task` | `train` | The hub's planner (M16) decomposes a task on a device into a closed step vocabulary and files the approval requests it needs. **A human approves each handoff**; the plan never approves, never opens a session, never names a joint. Costs money per decision. |
| `get_plan_status` | `read` | One plan: status, steps, the timeline with each decision's cost, and what it is waiting on. |
| `create_repo` | `write` | Create an empty `dataset`, `model` or `robot` repo (M17a). |
| `validate_robot` | `read` | The hub checks a robot card against the repo's MJCF **without writing** — `checks[]` with a stable code, the offending names and a fix hint (M17a). |
| `set_robot_card` | `write` | Store a robot card (full replace); a refusal is one line per failed check and nothing is written (M17a). |
| `init_robot` | local | `lucen robot init` on a local MJCF/URDF: compiles with MuJoCo, drafts `robot_card.json` with a `needs_you` list. Needs `lucen-cli[rollout]` in this environment (M17a). |
| `publish_robot` | local, then `write` | `lucen robot validate` + `publish` on a local directory; **dry run by default** (M17a). |

### What is real, and what is not

An agent that calls a tool which silently does nothing is worse than one that
is told "not available yet", so the tools say exactly where things stand:

- **`submit_training`.** `POST /v1/runs` validates the recipe, records a run
  and hands it to an executor. The only executor this hub can drive is
  `worker` (M7-RL): a `lucen worker run` process that a human starts on their
  own GPU machine, with a `train` key for the owner and the trainer installed,
  claims the run, streams its logs, publishes the output model repo and
  reports the result. The hub does **not** track connected runners, so the
  tool cannot say whether one is listening — a run nobody claims stays
  `queued`, with no weights and no bill, and the result says so. `modal`, the
  hub's own GPU backend, runs only where the deployment names a deployed
  Modal app; elsewhere the hub answers 503 naming that setting and records
  nothing. The tool defaults to `dry_run: true` (it
  returns the request body without sending it); `estimate_training` prices
  the same recipe first, and an `idempotency_key` makes a retried submit
  return the same run.
- **`rollout_in_sim`** (M8) queues a *battery* — command cells × seeds × a
  horizon ≤ 30 s — and, like `submit_training`, defaults to `dry_run`. Nothing
  runs on the hub: a rollout worker (`lucen rollout worker`, the CLI installed
  as `lucen-cli[rollout]`, on a human's own CPU machine) claims it, pulls the
  policy and the robot's MJCF + meshes, refuses an `io_contract` mismatch,
  runs the cells, renders an mp4 per cell where it can, and reports
  measurements; **the hub scores them** (PASS = every seed upright and every
  commanded axis inside `[0.85, 1.15]`) into `scorecard.json`. Name a `run_id`
  and the scorecard is attached to that run, which is how the Coach's "PASS
  gates become constraints" happens without a human copying anything. The hub
  does not track workers — a queued rollout is queued, not running — and the
  hosted (Modal) path is not built. `get_rollout_status` reads it back, with
  the per-cell verdicts and presigned playback URLs once it is scored.

`get_run_logs` reads the real stream (`GET /v1/runs/{id}/logs`, replayed with
`follow=false` because a tool call has to end): `logs` is the last `tail`
lines as `{ts, stream, line}` — `stderr` marks a notice the hub wrote into
the log itself, such as a cancel request or a lease re-queue — `metrics` is
every `metrics.jsonl` line, and `status_at_end` is the run's status when the
stream closed. A queued run answers an empty list with a reason ("nothing
yet"); an imported run answers `logs_available: false` because it has no
log by contract. `cancel_training` returns `immediate: true` for a queued
run (it is `canceled` now) and `false` for a running one (the runner kills
the template on its next report, a few seconds later — poll
`get_run_status`).

**The working v0 loop** is: train on your own hardware → `lucen runs import`
→ `get_training_advice`.

### One deliberate absence, and one consent-gated handoff

- **There is no key management.** `/v1/keys` is `session`-scoped, so no API
  key of any scope may mint, list or revoke a key; a server whose only
  credential is an API key could never succeed at those operations, and the
  rule they encode (a leaked `write` key cannot promote itself) is worth
  keeping visibly intact.
- **There is no tool that starts, stops or steers a robot — but there is a way
  to ask (M13).** `request_deploy` files an approval *request* ("model X onto
  device Y for T minutes") that a person must approve in the web app; approvals
  are `session`-only, so no API key — including this server's — can do it.
  Once approved, the robot-side `lucen-device` driver verifies the signed
  approval offline, pulls the exact bytes, refuses a contract mismatch, runs for
  the window and stops on its own clock; `get_device_status` shows the request,
  the approval and the driver's `started` / `heartbeat` / `stopped` / `refused`
  trail. Goal commands and teleop through an agent are not built (v1+), and
  the server's `instructions` say all of this to the model at connect.

## Errors

A tool failure comes back as an MCP error result with a sentence the model can
act on, never a bare status code:

- **403** names the missing scope and what the key does grant —
  *"this API key is missing the `train` scope (it grants: read, write)"* — by
  asking `/v1/auth/me` on the error path only.
- **403 from a key limited to repos** (`forbidden.key_limits`) or **from an AI
  agent's key** asking for paid hosted work beyond the free allowance
  (`forbidden.agent_key_spend`) is relayed in the hub's own words. A limited key
  works only on the repos in its list, runs for a robot in it under an owner it
  names whole (`owner/*`), and sim tests of a policy and a robot both in it;
  `/v1/auth/me` returns the list as `auth.limited_to`.
- **401 for an expired key** says when it expired.
- **404** is always *"not found, or not visible to this credential"*, in the
  same words for a private resource and an absent one. That is the hub's rule,
  not a vagueness: a 403 would confirm the resource exists.
- **429** distinguishes the per-minute rate limit from the coach's daily quota.
- **502** from the coach carries the guarantees its own report broke.
- **503** on a coach call usually means the hub has no model backend configured
  (`ANTHROPIC_API_KEY` server-side) — a deployment gap, not something a client
  can fix.

## Develop

```bash
make install                       # uv sync --all-packages
uv run --no-sync pytest packages/mcp/tests
```

`tests/test_protocol.py` launches the shipped `lucen-mcp` console script as a
subprocess and speaks MCP over its stdin/stdout against a live local API
(uvicorn + SQLite + in-process moto S3, with the coach on M14b's recorded
fixtures). Nothing in the test suite needs a secret.

Apache-2.0. © 2026 Lucen Robotics.
