# Moonsail tool belt

Use these nine MCP tools for the current Moonsail prototype. A Space contains
immutable Objects and asynchronous Jobs. Credentials come from the MCP client
configuration, never from tool arguments.

Use hosted WorkOS OAuth login when the MCP client supports it. For scripts,
sign in to `/studio/account`, select the intended workspace, and create your
own read/write agent key. Store it as `MOONSAIL_API_KEY` in the client environment;
never use the WorkOS server key. A read-only key can inspect resources but cannot
create or advance work. Changing an input ID cannot change the credential's
workspace. Revoke unused keys from the account page.

Object URLs require the same workspace access. Send bearer credentials only to
the matching Moonsail origin; never attach them to external signed URLs. Keep
credentials out of prompts, tool arguments, scripts, and scene files.

## Models and cost

- `list_models`: list curated fal, World Labs, and Runway models for image,
  video, 3D, and world generation. Read each model's output kind, limits, and
  price before dispatch.

```json
{ "kind": "image | video | model3d | world (optional)" }
```

The response is `{ "items": [...] }`. Each returned item has `id`, `provider`
(`fal`, `worldlabs`, or `runway`), `providerModelId`, `operation`,
`outputKind`, `description`, `limits`, `parameterSchema`, and `price`.
`parameterSchema` is the exact JSON Schema for that model's generation
`parameters`. Read its `required`, `properties`, enum values, and numeric or
string limits before dispatch. Unknown parameters are rejected. `price.kind` is `fixed`,
`megapixel`, `per_second`, or `bounded`; read the matching fields
(`microusd`, `microusdPerMegapixel`/`maxUnits`,
`microusdPerSecond`/`minimumMicrousd`/`parameterSurcharges`, or
`minimumMicrousd`/`maximumMicrousd`) to estimate cost before dispatch.

Before a generation or a review, estimate the total and repair reserve. Ask
the user to approve that total. Pass the approved ceiling as integer
`maxCostMicrousd`. A US dollar is 1,000,000 micro-dollars. The server rejects
a job whose estimate exceeds the ceiling. `create_job` requires
`maxCostMicrousd` for both `kind: "generate"` and `kind: "review"`; the
runtime estimates the reviewer's token cost from the object (video estimates
scale with duration) and rejects the job when that estimate exceeds the cap.

## Spaces

- `create_space`: create the container for one production. Put the stable brief
  in `brief` and keep task-local prompts and treatment decisions with the job.

```json
{ "name": "1-120 characters", "brief": "optional, up to 10000 characters" }
```

Record the returned `id` and use it as `spaceId` in later calls. There is no Project mutation, knowledge store,
campaign, workflow, or billing tool in the prototype.

## Objects

- `put_object`: copy a public HTTPS media file or store up to 256 KiB of inline
  text or JSON. The server hashes the bytes and returns an immutable Object.
- `get_object`: read an Object's SHA-256, media metadata, source Job, and durable
  URL.

Use one of these exact `put_object` inputs:

```json
{
  "spaceId": "UUID",
  "kind": "image | video | model3d | splat",
  "source": { "type": "url", "url": "https://public.example/file" }
}
```

```json
{
  "spaceId": "UUID",
  "kind": "text | json",
  "source": {
    "type": "inline",
    "text": "content",
    "mimeType": "text/plain | application/json"
  }
}
```

Call `get_object` with `{ "objectId": "UUID" }`.
Its response has `id`, `spaceId`, `sha256`, `kind`, `mimeType`, `sizeBytes`,
`width`, `height`, `durationMs`, `videoFrameRate`, `videoTransfer`,
`videoCodec`, `storageKey`, `url`, `sourceJobId`, and `createdAt`.

Use Objects for every reference and final output that must survive an expiring
provider URL. Record each Object ID, role, and source. The prototype rejects
local URLs and files larger than 100 MB. For inline sources, match `kind: "text"`
with `text/plain` and `kind: "json"` with `application/json`.

## Jobs

- `create_job`: create a generation, review, or local Blender Job.
- `blender_job`: claim, heartbeat, upload artifacts, finish, or fail a local Blender Job.
- `get_job`: read and advance one Job. Poll until `succeeded`, `failed`, or
  `submission_unknown`.
- `list_jobs`: recover recent Job IDs in a Space after an interrupted session.
- `convert_video_to_hdr`: convert one stored SDR video to HDR10 or HLG with
  Runway Ruby (`runway/ruby`).

Every `create_job` and `convert_video_to_hdr` call needs an
`idempotencyKey`. Reuse the same key only when retrying the same logical
request. A different request needs a new key.

For local Blender, use `kind: "blender"`, `spaceId`, `instructions`, and an
`idempotencyKey`. Optional settings are `frames`, `width`, `height`, and
`samples`; `parentJobId` creates a repair from a completed revision in the same
Space. This local CPU operation has no provider charge or `maxCostMicrousd`.
Run the installed Blender skill's `scripts/moonsail.py` on a Mac with Blender.
The agent supplies reviewed Python. Studio repair requests queue work but do
not start an unattended authoring agent. Use the Blender skill's execution
module for the command and upload contract.

`blender_job` uses one UUID `executionId` as the lease token. Heartbeat every
30 seconds; ownership expires after 120 seconds without renewal. An expired
execution cannot upload or finish. Create a new Job instead of taking over.
Uploads use short-lived direct PUT URLs. The server validates stored bytes,
source and preview coverage, and creates an immutable revision manifest.
A successful local run is not visual approval or a review Receipt.

For generation, pass `kind: "generate"`, a catalog model ID, the prompt,
provider `parameters`, and `maxCostMicrousd`:

```json
{
  "kind": "generate",
  "spaceId": "UUID",
  "modelId": "exact list_models ID",
  "prompt": "1-20000 characters",
  "parameters": {},
  "maxCostMicrousd": 50000,
  "idempotencyKey": "at least 8 characters"
}
```

Copy parameter names and values from the selected model's `parameterSchema`.
For example, Runway Gen-4.5 text-to-video requires integer `duration` from 2
through 10 and `ratio` with one of the advertised enum values. Image-to-video
and video-to-video models require `sourceObjectId`, a UUID for an Object in the
same Space. FAL fields, enums, and limits are also listed in their model schemas.

When the Job succeeds, its
`outputObjectIds` already point to durable copies. Read each with `get_object`.
World generation models return a world manifest Object first in
`outputObjectIds`, followed by its splat, collider, panorama, and thumbnail
asset Objects.

For review, pass `kind: "review"`, the exact final `objectId`, and
`maxCostMicrousd`. A successful review Job returns a receipt and stores the
same receipt as a JSON output Object:

```json
{
  "kind": "review",
  "spaceId": "UUID",
  "objectId": "UUID",
  "acceptance": {},
  "maxCostMicrousd": 20000,
  "idempotencyKey": "at least 8 characters"
}
```

The optional `acceptance` value is a JSON object with task-specific observable
claims. For an image, useful keys include `requiredComposition`,
`requiredVisibleText`, `forbiddenVisibleText`, `requiredObjects`, and
`blockers`. These are reviewer instructions, not separate server gates.

For HDR conversion, pass `kind: "convert_to_hdr"`, the stored SDR video
`objectId`, `outputFormat` (`hdr10` or `hlg`), and `maxCostMicrousd`:

```json
{
  "kind": "convert_to_hdr",
  "spaceId": "UUID",
  "objectId": "UUID",
  "outputFormat": "hdr10",
  "maxCostMicrousd": 500000,
  "idempotencyKey": "at least 8 characters"
}
```

Call `get_job` with `{ "jobId": "UUID" }`. The response has `id`, `spaceId`,
`kind` (`generate`, `review`, `convert_video_to_hdr`, or `blender`), `status`, `modelId`,
`idempotencyKey`, `estimatedCostMicrousd`, `estimatedCostMinMicrousd`,
`actualCostMicrousd`, `costOverrunMicrousd`, `input`, `providerRequestId`,
`outputObjectIds`, `receipt`, `error`, `createdAt`, and `updatedAt`.
`actualCostMicrousd` is null until the Job settles. `costOverrunMicrousd` is
null unless the settled actual cost exceeded `maxCostMicrousd`; the Job still
succeeds when that happens, so surface the overrun rather than treating it as
a failure. Active status values are `submitting`, `queued`, and `running`.
Terminal values are `succeeded`, `failed`, and `submission_unknown`. If the value
is `submission_unknown`, do not submit again. Reconcile the provider account for
an accepted task or charge before an operator decides the next action. For any
active Job, wait about two seconds before the next poll.

Call `list_jobs` with
`{ "spaceId": "UUID", "limit": 20 }`; `limit` is optional and ranges from 1
to 100. Its response is `{ "items": [...] }` with the same Job fields. The
receipt has `reviewedObjectId`, `reviewedSha256`, `coverage`, `probes`,
`findings`, `deliveryEligible`, and `reviewer`. Each finding has `severity`,
`summary`, `evidence`, `location`, `cause`, `repair`, `strengthToPreserve`, and
`unresolvedBlocker`. The runtime selects a Gemini reviewer named
`google/<model>` (`google/gemini-3.8-flash` by default) for images and
videos. Images get native image review; videos get native agentic video
review that includes audio. When token usage is available, `probes.usage`
carries the Gemini token counts. Image and video review include deterministic
probes plus native visual or agentic review. Review Jobs for 3D, splat, text,
JSON, and objects over 13 MiB fail before the runtime reads their bytes. They
cannot pass the delivery gate.

## Safe recovery

- If submission returns a Job, poll that Job. Do not create another Job.
- If the client loses the response, call `create_job` again with the same
  idempotency key.
- If the Job ID is lost, use `list_jobs` for its Space.
- A failed Job exposes only a safe category, message, and retryability flag.
  Do not infer or repeat hidden provider details.
