Metadata-Version: 2.5
Name: recreator-mcp
Version: 0.1.2
Summary: re:Creator MCP server — thin protocol wrapper over recreator-core
Project-URL: Homepage, https://recreator.ilvs.space
Project-URL: Documentation, https://recreator.ilvs.space/llms.txt
Author: Thuy Hoang
License: MIT
Keywords: agent,capcut,mcp,timeline,video-editing
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Multimedia :: Video :: Non-Linear Editor
Requires-Python: >=3.11
Requires-Dist: mcp>=2.1.1
Requires-Dist: recreator-core<0.2,>=0.1.1
Description-Content-Type: text/markdown

# recreator-mcp

MCP server exposing `recreator-core` operations as typed tools.

A thin protocol wrapper: schema translation only, zero editing logic. Every tool
translates JSON arguments into a `recreator-core` call and translates the result
back. No transcription, no detection, no thresholds, no editorial judgment —
the calling agent decides what to cut, caption, or animate and passes explicit
ranges and values. Core remains fully usable as a library and CLI without this
package installed.

## Boundary conventions

- **All times are seconds** (floating point) at the tool boundary. Core's
  microsecond `Time` type is constructed at the edge.
- **Ranges** are `[[start, end], ...]` pairs, e.g. `[[12.4, 15.9]]`.
- **Keyframe offsets** are measured from the start of the clip, not the timeline.
- `.recreator/project.json` is the source of truth and the default for every
  tool's `project` argument. Tools load it, apply the op, and save atomically
  (write to a temporary file in the same directory, then rename).
- A CapCut draft is a build artifact, produced only by `recreator.export.capcut`.

## Tools

| Tool | Purpose |
| --- | --- |
| `recreator.project.load` | Read timeline state; source of clip ids |
| `recreator.project.save` | Update project name and metadata |
| `recreator.probe.media` | ffprobe duration, resolution, streams |
| `recreator.probe.loudness` | Windowed RMS/peak dB series (raw measurements only) |
| `recreator.timeline.init_from_media` | Construct a timeline seeded from probed media files |
| `recreator.timeline.remove_ranges` | Delete ranges, rippling by default |
| `recreator.timeline.keep_ranges` | Keep only the given ranges |
| `recreator.timeline.mute_ranges` | Silence ranges without changing timing |
| `recreator.timeline.bleep_ranges` | Censor ranges: `mute`, `bleep`, or `cut` |
| `recreator.timeline.add_text_clips` | Add caption/text clips with character-offset styles |
| `recreator.timeline.add_media_clips` | Add b-roll, overlays, music |
| `recreator.timeline.add_keyframes` | Animate one property of one clip |
| `recreator.timeline.set_transform` | Static scale/position/rotation/opacity/crop |
| `recreator.export.capcut` | Build a CapCut draft into a caller-specified directory |
| `recreator.validate` | Report structural issues before exporting |
| `recreator.catalog.list` | Browse every tool this server exposes, optionally by namespace |
| `recreator.catalog.get` | Full parameter schema, return shape, and example for one tool id |
| `recreator.templates.list` | Browse HyperFrames route templates, optionally filtered by a query substring |
| `recreator.templates.get` | Full contract for one route: input, output, triggers, interview shape, pre-fill table |
| `recreator.templates.recommend` | Rank routes against a free-text prompt by counted lexical evidence |
| `recreator.jobs.submit` | Submit a registered async job kind; optionally block until done |
| `recreator.jobs.get` | Read one job's current record, non-blocking |
| `recreator.jobs.wait` | Rejoin an existing job and block until terminal |
| `recreator.jobs.list` | List recent jobs, newest first, optionally filtered by status |
| `recreator.jobs.cancel` | Request cooperative cancellation of a queued or running job |

### Async jobs

Every tool above is fast and synchronous — this is not that. It is scaffolding
for **future** generative-media operations (b-roll generation, TTS, image gen,
cloud STT) that call out to paid third-party APIs and genuinely take minutes,
mirroring the shape of Higgsfield's async job API (`generate create --wait`,
`generate get`, `generate wait`).

A job *kind* is a plain Python callable registered by name in
`recreator_mcp.jobs`; the runner itself is pure mechanism (dispatch,
persistence, status) and knows nothing about what any kind does. Registering a
future kind — e.g. `broll.generate` — is one call to `register_kind`, with no
changes to the runner.

One real kind ships today: **`probe.batch`**, which probes N media files via
core's `probe_media` and reports progress as it goes. Each probe is a real
`ffprobe` subprocess, so it earns async honestly for large batches.

```
recreator.jobs.submit(kind="probe.batch", params={"paths": [...]})
  -> {job_id, kind, status: "queued", submitted_at}

recreator.jobs.submit(kind="probe.batch", params={"paths": [...]}, wait=true)
  -> full record with status: "completed" and result: [...]

recreator.jobs.wait(job_id="...")       # rejoin, block until terminal
recreator.jobs.get(job_id="...")        # non-blocking status check
recreator.jobs.list(status="running")   # recent jobs, newest first
recreator.jobs.cancel(job_id="...")     # cooperative — stops between units
```

**Job record**: `job_id`, `kind`, `status` (`queued` / `running` / `completed`
/ `failed` / `cancelled`), `params`, `submitted_at`, `started_at`,
`completed_at`, `progress` (0.0–1.0), `message`, `result` (on success),
`error` (on failure).

**Persistence**: jobs live under `.recreator/jobs/`, one JSON file per job,
written atomically the same way as `project.json` (tempfile in the same
directory, `fsync`, then `Path.replace`). A job submitted in one MCP session
is visible to `recreator.jobs.get`/`recreator.jobs.wait` in a later one — that
cross-session rejoin is the entire point of the job/status split. Cancelling a
job from a session that did not submit it is rejected with an actionable error
rather than silently doing nothing, since there is no in-process handle to
signal.

**Cancellation** is cooperative: a job kind calls `cancel.raise_if_cancelled()`
between units of work (e.g. between files in a batch), so it takes effect at a
clean boundary rather than killing a thread mid-unit.

### Catalog

`recreator.catalog.list` and `recreator.catalog.get` let an agent discover
operations and their parameters at runtime instead of a skill hardcoding them
in markdown — the same affordance as `higgsfield model list` / `model get`.
The catalog is generated by introspecting the live registered tools
(`mcp.list_tools()`), so it can never drift from the actual tool set; only
each tool's namespace and whether it mutates `project.json` are curated,
everything else (params, types, defaults, descriptions) is reflected straight
from the tool's JSON input schema.

```
recreator.catalog.list                                   # every tool, grouped
recreator.catalog.list(namespace="timeline")              # one namespace
recreator.catalog.get(id="recreator.timeline.remove_ranges")  # full schema
```

### Templates

`recreator.templates.list` and `recreator.templates.get` expose the ten
HyperFrames route templates — the same affordance as `higgsfield preset list` /
`preset resolve`, so an agent can browse and resolve a route contract at
runtime instead of a skill hardcoding it in markdown. Both tools parse
`skills/recreator/hyperframes/references/routes/*.md` directly (never
hand-copied) and cache the result in-process, keyed by the routes directory's
mtime so edits on disk are picked up without a restart.

```
recreator.templates.list                                       # all 10 routes
recreator.templates.list(query="launch")                        # substring filter
recreator.templates.get(id="talking-head-recut")                # full contract
```

`recreator.templates.recommend` occupies the slot where Higgsfield has
`generate cost` — re:Creator has no metered spend to estimate, so it
recommends a route instead. Its ranking is **counted, attributable lexical
evidence only**: a trigger phrase found verbatim in the prompt, a keyword
overlap with a route's input/output text, or an exact `production_type` row
hit from `production-map.md`. Candidates are ordered lexicographically by the
per-kind evidence count, most specific signal first — `trigger_phrase` (a
route author's own curated wording) outranks `production_type_row` (an exact
lookup-table hit), which outranks `keyword_overlap` (incidental shared words)
— because a curated phrase is structurally a stronger match than a stray
common word, not because of any tuned weight: nothing is scaled or
multiplied, only compared count-by-count. `score` is the total evidence
count; `signals` is the `{trigger_phrase, production_type_row,
keyword_overlap}` breakdown the ordering is actually computed from, so a
caller can reproduce the ranking by hand from what is returned. Ties break on
route id for byte-stable output. This is measurement in the sense CLAUDE.md
allows, not a recommendation engine: it returns ranked candidates plus a
`note`, never a single verdict, and an empty list rather than a fallback
guess when nothing matches.

```
recreator.templates.recommend(prompt="turn this website into a video")
  -> {
       candidates: [
         {
           route: "product-launch-video",
           score: 3,
           signals: {trigger_phrase: 1, production_type_row: 0, keyword_overlap: 2},
           evidence: [
             {kind: "trigger_phrase", detail: "turn this website into a video"},
             {kind: "keyword_overlap:input", detail: "website"},
             {kind: "keyword_overlap:output", detail: "video"}
           ],
           contract_summary: "A product promo, launch video, site tour, or showcase MP4."
         },
         ...
       ],
       note: "Ranking orders candidates by counted evidence, most specific signal first ..."
     }
```

The routes directory is located relative to the installed package or by
walking up from the current working directory to the repo root; set
`RECREATOR_SKILLS_DIR` to override it explicitly (e.g. under HTTP transport
running from an unrelated cwd). `RECREATOR_MCP_*` is reserved for transport and
auth configuration, not this lookup.

### HyperFrames template registry

The `recreator.hyperframes.templates.*` tools front a Cloudflare Worker that
serves the mirrored HyperFrames template catalog (metadata in D1, source zips in
R2). The worker is the **only** source: there is no local-file fallback and no
direct call to `hyperframes.dev` or `static.heygen.ai`, so an agent is never
uncertain which registry answered it.

```
recreator.hyperframes.templates.tags()
recreator.hyperframes.templates.list(aspect="9:16", type="block", tags=["ad-template"])
recreator.hyperframes.templates.get(name="ai-chat-reveal")
recreator.hyperframes.templates.pull(name="ai-chat-reveal", dest="./templates/hero")
```

Discovery runs widest to narrowest. `tags` publishes the vocabulary a filter may
use, each with its template count; `list` filters; `get` returns everything
known about the few finalists chosen.

`list` filters on **hard facts only** — `aspect`, `type`, `tags` (ANDed),
`min_duration`, `max_duration`, `origin`, `limit` — and returns the worker's
candidates in the worker's order (`name ASC`), untouched. It does no scoring, no
relevance sort, and picks no winner: the response carries a `ranking` note
saying so. Which few templates are worth showing the user, and in what order, is
the agent's judgment — it has the brief, the footage, and the user's taste, and
the registry has none of those. This is the same boundary
`recreator.templates.recommend` respects, taken one step further: here there is
not even counted lexical evidence to report, because filtering on facts is all
the registry is asked to do.

Each candidate carries six fields — `name`, `title`, `type`, `aspect`,
`duration_s`, `tags` — and nothing else. The `description`, dimensions,
provenance and `raw_tags` live on `get`, which is called on a handful of
finalists rather than on all 380; on the live registry the former full shape was
143.7 KB against 54.1 KB now, and `description` alone was half of it.

Only **canonical** tags filter — those carried by at least 8 templates. An
unknown or merely-rare tag is **rejected** with near matches and the canonical
tag it folds into, never answered with an empty list, because an empty list
reads as "no such template exists" and sends an agent away from templates that
were there all along. Call `tags` when unsure. Filtering on a concrete `aspect`
also returns aspect-agnostic (`any`) templates, which adopt whatever canvas
hosts them — 219 of 380 are such components, and excluding them would hide most
of the usable pieces.

`pull` is the one tool in this namespace that writes to disk, so it is
deliberately cautious:

- It **refuses a non-empty destination** unless `overwrite=true`, the same
  consent rule the CapCut install path follows.
- It **rejects the whole archive** if any member would write outside the
  destination — a `../` path, an absolute path, or a symlink — before extracting
  a single byte, so a rejected pull leaves nothing half-unpacked.
- A template whose source lives with the HyperFrames CLI rather than in R2
  raises with the exact `npx hyperframes catalog --query "<name>"` command to
  run instead. That is a different install path, not a failure.

Presigned R2 download URLs are short-lived (300s TTL) and single-use. Where
this client mints one itself it is followed once, immediately, and never logged
or written to disk.

#### `pull` without a registry credential

Downloading a template is two steps with different credential needs. Minting
the presigned URL requires the registry bearer token — a **server-side**
credential. Fetching that URL requires nothing at all. So `pull` splits them:

```
recreator.registry.get_source_url(name="ai-chat-reveal")   # remote MCP, holds the token
  -> { name, url, bytes, expires_in_seconds: 300 }

recreator.hyperframes.templates.pull(                      # here, no credential read
    name="ai-chat-reveal",
    dest="./templates/hero",
    source_url="https://...",   # from the call above
    declared_bytes=48213,       # its `bytes`, for the size check
)
```

With `source_url` supplied, `resolve_registry_config()` is never called and
neither `RECREATOR_REGISTRY_URL` nor `RECREATOR_REGISTRY_TOKEN` is read. The URL
is the whole authorization. It must be `https://` — a caller-supplied URL
fetched blind is an SSRF vector, so anything else is refused outright. Because
the URL expires after 300s and is single-use, a failed download means mint a
fresh one, not retry the same one.

Omit `source_url` and `pull` resolves the URL itself, exactly as before. That
path still needs the credential locally, and is the one to use when you are
developing against the registry directly.

| Flag | Env var | Default |
| --- | --- | --- |
| `--registry-url` | `RECREATOR_REGISTRY_URL` | none — required |
| `--registry-token-file` | `RECREATOR_REGISTRY_TOKEN` | none — required |

Both halves are required for every tool in the namespace except a `pull` given a
`source_url`, and are resolved CLI flag > env var, inside the call. With either
missing, those tools raise a `ToolError` naming both environment variables; they
never fall back silently. The base URL must be `https://` — the client refuses
to put a bearer token on a plaintext request.

```bash
RECREATOR_REGISTRY_URL=https://<worker>.workers.dev \
RECREATOR_REGISTRY_TOKEN=<token> recreator-mcp
```

### Safety

`recreator.export.capcut` writes to the `out_dir` you specify and never defaults
anywhere inside CapCut's own library. Passing `install=true` additionally copies
the built draft into the user's **real CapCut drafts folder**, where CapCut will
show it as a project; that step needs `overwrite=true` to replace an existing
draft of the same name. Leave `install` off unless the user asked for it.

## Configuration

### Claude Code — `.mcp.json`

Place at the repository root (or merge into an existing `.mcp.json`):

```json
{
  "mcpServers": {
    "recreator": {
      "command": "uv",
      "args": ["run", "--package", "recreator-mcp", "recreator-mcp"],
      "cwd": "/absolute/path/to/ReCreator"
    }
  }
}
```

### Codex — `~/.codex/config.toml`

```toml
[mcp_servers.recreator]
command = "uv"
args = ["run", "--package", "recreator-mcp", "recreator-mcp"]
cwd = "/absolute/path/to/ReCreator"
```

Replace `/absolute/path/to/ReCreator` with the workspace root. Both hosts launch
the server over stdio. If the package is installed into an environment already on
`PATH`, `command = "recreator-mcp"` with no `args` works as well.

### Install

From the workspace root:

```
uv sync --all-packages --all-groups
```

`uv sync --all-groups` alone installs only the root dev group, which does not
include this package; `--all-packages` is what puts `recreator-mcp` and the `mcp`
SDK into the environment.

### CLI fallback

Hosts without MCP support reach the same operations through `recreator-core`'s
CLI, which needs no MCP installed:

```
recreator project init --media <path>
recreator probe <path>
recreator timeline remove-ranges --ranges '[[12.4,15.9]]'
recreator export capcut --draft-name "my-cut"
```

The effect on `project.json` is identical either way.

## Transports

`recreator-mcp` runs over **stdio** (default, unchanged) or **streamable HTTP**.
Local files stay local either way: HTTP mode does not move `project.json` or
media off the machine the server runs on, it just changes how a client talks
to the same server.

```
recreator-mcp                                    # stdio, exactly as before
recreator-mcp --transport http                   # HTTP, 127.0.0.1:8000/mcp
recreator-mcp --transport http --host 0.0.0.0 --port 9000 --path /recreator
```

| Flag | Env var | Default |
| --- | --- | --- |
| `--transport {stdio,http}` | `RECREATOR_MCP_TRANSPORT` | `stdio` |
| `--host` | `RECREATOR_MCP_HOST` | `127.0.0.1` |
| `--port` | `RECREATOR_MCP_PORT` | `8000` |
| `--path` | `RECREATOR_MCP_PATH` | `/mcp` |

CLI flags win over environment variables, which win over the defaults above.
Binding `0.0.0.0` is never the default — it takes an explicit `--host 0.0.0.0`
or `RECREATOR_MCP_HOST=0.0.0.0`.

## Authentication

HTTP mode requires a bearer token; stdio needs none (the host process already
controls who can talk to it). Set the token via `RECREATOR_MCP_TOKEN` or
`--token-file <path>`, then send it as `Authorization: Bearer <token>`.

```
RECREATOR_MCP_TOKEN=$(openssl rand -hex 32) recreator-mcp --transport http
```

Starting `--transport http` with no token configured **refuses to start**:

```
Refusing to start HTTP transport without a bearer token: an unauthenticated
HTTP MCP server would expose filesystem-touching tools to the network.
Fix it by either setting RECREATOR_MCP_TOKEN (or passing --token-file), or, if
you understand the risk and are only binding to a trusted network, pass
--insecure.
```

Pass `--insecure` only for a deliberately trusted network (e.g. `127.0.0.1`
loopback during local development) — it accepts every request unauthenticated
and prints a warning on startup. Token comparison uses a constant-time compare
so a network attacker cannot recover the token by timing responses.

This is a static pre-shared token, not full OAuth. It is not the token-based
auth Higgsfield's hosted MCP server uses against a user account — there is no
provisioning, rotation, or per-client identity. The verifier is structured so
a real `OAuthAuthorizationServerProvider` can replace it later without
reshaping how transports are wired up.
