Metadata-Version: 2.5
Name: recreator-mcp
Version: 0.1.0
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/llm.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.0
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.init` | Create a project seeded from probed media files |
| `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.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.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")
```

`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.

`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 credentials (300s TTL). They are
followed once, immediately, and never logged, returned to the caller, or written
to disk.

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

Both halves are required and resolved CLI flag > env var, inside the call. With
either missing, every tool in the namespace raises a `ToolError` naming both
environment variables; it never falls 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.
