# Platform API, SDK & MCP

CoreReflex is a **full API + full-model solution**, not just a UI. Everything the
studio does — discover the model catalog, query/command a composition with the
programmable JSON engine, generate (image/video/music/voice), render, upscale, and
**fetch the finished asset back** — is reachable over a documented HTTP surface, a
zero-dependency SDK, and an MCP server for agents. This is the integrator's guide;
the per-route ladder lives in [API reference](./api-reference.md), the engine
internals in [JSON engine](./json-engine.md).

## Drive CoreReflex from any LLM — no browser, no login

The platform has **three no-browser front doors**, all authenticated by a single
`cr_live_…` API key (or a session JWT). An agent can discover the catalog, run a
generation, poll the render, and read the bytes back **entirely headless**:

| Front door | What it is | Run it with |
|---|---|---|
| **REST API** | The raw HTTP surface (`src/api.js`). Bearer `cr_live_…`. | `curl` / any HTTP client |
| **MCP server** | `@corereflex/mcp` (`mcp/server.js`) — 21 agent tools over JSON-RPC 2.0 stdio | `node mcp/server.js` |
| **SDK** | `@corereflex/sdk` (`sdk/index.js`) — `class CoreReflex` | `npm i` / `import` |

The MCP server and SDK are both **thin clients over the same REST endpoints** — the
MCP server literally delegates to the SDK, which calls the documented HTTP routes.
Nothing here needs a signed-in browser. The marketing framing lives in the blog
posts under `content/blog/` (`connect-claude-to-corereflex-mcp.md`,
`rest-api-vs-mcp-server.md`, `autonomous-video-agent-mcp.md`).

## Authentication

Two principals, both presented as `Authorization: Bearer <token>`:

| Principal | Token | Use |
|---|---|---|
| **Session** | a user session JWT (or the `corereflex_session` cookie) | the app + editor; key management |
| **API key** | `cr_live_…` (created at `POST /api/keys`) | server-to-server (SDK / MCP / CI) |

The **programmatic endpoints** (catalog, JSON engine, generation, render, upscale,
captions, enhance) accept *either*. **Key management** (`/api/keys`) requires a real
session — a key can't mint or revoke keys. A key acts with its creating member's
workspace + role, so all workspace scoping and credit metering apply unchanged.

### API keys

```http
POST /api/keys            # body: { "name": "CI", "expiresAt": "2027-01-01" }  -> { key, keyPrefix, ... }
GET  /api/keys            # list (metadata only — never the secret or its hash)
DELETE /api/keys/:id      # revoke (idempotent)
```

The secret is returned **once** on create — store it immediately; only its
`sha256` hash + a short `cr_live_…` prefix are persisted. Keys are workspace-scoped;
a key whose creator was removed is treated as orphaned and rejected.

> **Deploy gate.** API keys require migration `018_api_keys.sql`. Until it's applied,
> presenting a `cr_live_` bearer returns `401` and session auth is entirely
> unaffected (the `api_keys` table is queried *only* for a `cr_live_` bearer).

## Discovery

```http
GET /api/catalog          # live model IDs, capabilities + params, camera moves,
                          # upscale tiers, per-operation credit cost, endpoint list
GET /api/json/schema      # the JSON-engine contract: targets, operators, commands
```

`GET /api/catalog` is the single self-describing document an SDK/agent introspects
instead of hard-coding what the platform can do.

## Programmable JSON engine

```http
POST /api/json
# stored composition:  { "compositionId": "<uuid>", "request": { ... } }
# inline manifest:      { "state": { ...UndoableState }, "request": { ... } }
```

`request.kind` is `query` | `command` | `describe` | `batch`. Queries are read-only;
commands and batch mutate and are persisted atomically (row-locked) — a no-op
command writes nothing. See [JSON engine](./json-engine.md) for the full grammar.

```jsonc
// "find every audio clip, return id + start + length, ordered"
{ "compositionId": "…", "request": {
  "kind": "query", "target": "items",
  "where": { "field": "kind", "op": "eq", "value": "audio" },
  "select": ["id", "from", "durationInFrames"], "orderBy": { "field": "from", "dir": "asc" } } }
```

## Generation, render & upscale

All of these accept the `cr_live_…` API key (they route through `requireApiUser`).

| Endpoint | Does | Meters |
|---|---|---|
| `POST /api/generate` | video (spectrum-routed) → `202 { jobId }`; poll `GET /api/generate/:id` | the provider that runs (`usage:veo` / `usage:kling` / `usage:wan` / …) |
| `POST /api/imagen` | image | `usage:imagen` (a self-hosted image engine bills `usage:image-own`) |
| `POST /api/director/produce` | a planned film (Veo/Kling) | `usage:veo` **× shots** |
| `POST /api/music` | music | `usage:lyria` (self-hosted lane bills `usage:music-own`) |
| `POST /api/voice` | voiceover | `usage:voice` — **pin a self-hosted model** (`{ "model": "kokoro" }`) to run the self-hosted CoreReflex Voice and bill the cheaper `usage:voice-own` |
| `POST /api/render` | enqueue a self-hosted render → `{ jobId }` | `usage:render` |
| `GET /api/render/:jobId` | poll → `status / progress / outputUrl / upscaledUrl / qa` | — |
| `POST /api/upscale` | own-tech upscale of any asset → sibling | `usage:upscale` |

> `qa` and fine-grained `progress` are populated by the in-house render path
> (`src/render-worker.js`); on the self-hosted fleet worker `qa` may be `null` and
> `progress` is the coarse `queued → 0 / running → 0.5 / succeeded → 1`.

### Own-tech upscaler & tiers

`POST /api/upscale` takes `{ sourceUrl, target, maxTier }`. `target` is `auto`
(lift sub-1080 → 1080), `720p`, `1080p`, `4k`, or `8k`. The result is always a
**sibling asset** — the native source is never overwritten. Auto-magnification is
capped at 4× (interpolation beyond that invents detail).

- **Backends.** `ffmpeg` (own-tech v1: lanczos + detail-preserving unsharp) is the
  default. The flagship is a **self-hosted super-resolution model** on the GPU fleet,
  selected with `UPSCALER=sr` and reached via `SR_SERVICE_URL`. If `sr` is selected
  but the service isn't configured, the request returns an **honest passthrough**
  (`backend: "sr-unavailable"`) — never a fabricated upscale.
- **8K governance.** 8K is opt-in per deploy via `UPSCALE_MAX_TIER=8k` (default cap
  is `4k`). A request above the ceiling is clamped, not rejected, and the plan
  reports the tier it actually produced.
- **Auto-ENHANCE on render.** Pass `upscaleTarget` to `POST /api/render` and the
  worker upscales the finished master to that tier as a sibling **after** the native
  render succeeds (best-effort — it can never fail the render). The result surfaces
  as `upscaledUrl` on `GET /api/render/:jobId`.

## Headless asset round-trip

Generation and render responses hand back the asset as a **signed capability URL**
(`…/api/assets/<key>?t=<token>`). That `?t=` token is an HMAC over the object key
(`signedAssetUrl` / `assetToken` in `src/storage.js`, keyed on
`ASSET_TOKEN_SECRET || JWT_SECRET`) and is **non-expiring**, so a saved composition
keeps working — and it self-authorizes with **no session**. This is what lets an
agent read back its own output.

`GET /api/assets/:key` (`streamAsset` in `src/api.js`) authorizes **three ways,
cheapest first** — so an agent can fetch either the signed URL *or* a bare object
key:

1. the signed `?t=` capability token (no DB touch — the hot playback path);
2. else a session cookie whose workspace owns the key;
3. else a `cr_live_…` **API key** whose workspace owns the key.

Way 3 is the headless one: send your API key as the bearer and fetch the asset by
its **bare object key**, no signed URL and no browser session required.

> **The fix that made this work** (commit `0b73b41`). `optionalSession()` in
> `src/session.js` used to call `verifyJwt` on *any* bearer and throw when it saw a
> non-JWT — so an agent that fetched its own asset while sending its `cr_live_`
> key got a **500**. `optionalSession` now **ignores API-key bearers** (they start
> with the `cr_live_` prefix) and falls through to the `?t=` token / session, and
> `streamAsset` gained the third (API-key-by-bare-key) authorization branch.
> Verified live: signed URL + bearer → `200`, bare key + API key → `200`, no auth
> → `403`.

The SDK wraps all of this in one call — `cr.fetchAsset(urlOrKey)` (below).

## SDK — `@corereflex/sdk`

Zero-dependency, isomorphic (Node 18+ / browser). See [`sdk/README.md`](../../sdk/README.md).

```js
import { CoreReflex } from '@corereflex/sdk';
const cr = new CoreReflex({ baseUrl: 'https://corereflex.com', token }); // token = a cr_live_… key

const catalog = await cr.catalog();
await cr.command(compId, 'setFps', { fps: 60 });
const job = await cr.render({ compositionId: compId, upscaleTarget: '4k' });
const { upscaledUrl } = await cr.renderStatus(job.jobId);

// Read the bytes back — fully headless. Pass the signed URL a response returned,
// or a bare workspace object key (authorized by this client's API key).
const { bytes, contentType } = await cr.fetchAsset(upscaledUrl);
```

Methods: `catalog`, `schema`, `evaluate` / `query` / `command` / `batch` (JSON
engine), `image`, `video`, `music`, `voice`, `soundscape`, `enhance`, `render`,
`renderStatus`, `upscale`, `runRecipe`, `createKey` / `listKeys` / `revokeKey`, and
`fetchAsset(urlOrKey)` → `{ bytes: Uint8Array, contentType }`.

## MCP server — `@corereflex/mcp`

A zero-dependency MCP (Model Context Protocol) server exposes the API as agent
tools so Claude or any MCP client can drive the studio. It speaks **JSON-RPC 2.0
over newline-delimited stdio** and **delegates to `@corereflex/sdk`**, so every
tool is just a documented REST call under the hood. See
[`mcp/README.md`](../../mcp/README.md).

> **Coverage status (2026-07-31).** `MCP-001` + `MCP-002` + `MCP-012` shipped:
> **21 tools**, including the speech-native wedge (`corereflex_cut`,
> `corereflex_captions`, `corereflex_auto_clips`) —
> and SDK↔MCP parity is now enforced by
> [`test/mcp-parity.test.js`](../../test/mcp-parity.test.js) — **that test, not
> this paragraph, is the source of truth.** Every public SDK method must have a
> tool or an entry in `MCP_EXCLUDED` explaining why not, or CI fails.
>
> Still open (see [`planning/EDITABILITY_AND_MCP.md`](../../planning/EDITABILITY_AND_MCP.md)):
> 1. **stdio is the only transport**, so no remote/hosted MCP client can connect (`MCP-010`).
> 2. `PROTOCOL_VERSION` is hard-pinned to `2024-11-05`; verify against the current spec rather than assuming (`MCP-011`).
> 3. ~~The wedge has no SDK method yet~~ ✅ **done** — `MCP-012` shipped `cut`,
>    `captions` and `auto_clips` as SDK methods and tools on 2026-07-31.

### The video lane: use `generate`, not `video`

`sdk.video()` → `/api/director/produce` produces a whole film **synchronously**,
running up to `MAX_SHOTS` vendor generations inline. It exceeds typical proxy
timeouts (observed `502` from the browser) and is the most expensive call in the
product. It is deliberately **not** an MCP tool, and the parity test pins that.

Agents and scripts should use the async job lane instead:

```js
const job = await client.estimate({ prompt });        // free — price it first
const { id } = await client.generate({ prompt });     // 202, returns immediately
const done  = await client.generation(id);            // poll until it settles
```

A failed job refunds its credits automatically.

```bash
COREREFLEX_API_URL=https://corereflex.com COREREFLEX_TOKEN=cr_live_… node mcp/server.js
```

Wire it into an MCP client (Claude Desktop, etc.):

```jsonc
{
  "mcpServers": {
    "corereflex": {
      "command": "node",
      "args": ["mcp/server.js"],
      "env": {
        "COREREFLEX_API_URL": "https://corereflex.com",
        "COREREFLEX_TOKEN": "cr_live_…"
      }
    }
  }
}
```

The **7 tools** (`TOOLS` in `mcp/server.js`):

| Tool | Delegates to |
|---|---|
| `corereflex_catalog` | `catalog()` — discover models, capabilities, credit costs, endpoints |
| `corereflex_schema` | `schema()` — the JSON-engine contract (targets/operators/commands) |
| `corereflex_json` | `evaluate()` — query / command / describe / batch |
| `corereflex_generate_image` | `image()` — text → image (the description is deliberately **brand-neutral** — it names no vendor model; pass `model` to pin an engine, or omit for the platform default) |
| `corereflex_render` | `render()` — enqueue a render, returns a `jobId` |
| `corereflex_render_status` | `renderStatus()` — poll a render job |
| `corereflex_upscale` | `upscale()` — own-tech upscale to a sibling asset |

Tool failures come back as MCP `isError` results (readable, recoverable), not
protocol errors.

## Consumption-confirm gate (`402`)

A credit-spending call may come back `402` with
`{ "error": …, "code": "premium_confirm_required", "estimate": { credits, opLabel, requiresConfirm } }`
**before** any billable work runs. This is the credit-consumption confirmation
gate (`src/consumption.js`): an operation whose credit cost clears the confirm
threshold, for a user/workspace that requires confirmation, must opt in. **Retry
the same request with `confirm: true` merged into the body** to proceed. (The
video path uses the sibling dollar-threshold gate — the same `code` with
`confirmPremium: true`.) Full contract in [Billing](./billing.md#consumption-confirmation-migration-038).

## Errors & limits

JSON errors are `{ "error": "<message>" }` with a conventional status (`400`
validation, `401` auth, `402` confirm/out-of-credits, `404` scope, `429` rate
limit). Each programmatic endpoint is independently rate-limited per principal;
credit metering (the workspace quota) is enforced on the paid operations above —
though the **platform owner and app-admins are never metered**
([Billing](./billing.md#owner--app-admins-are-never-metered)).
