# Reelsaw (oci · ghcr.io/osbytes/reelsaw)

Drive a self-hosted Reelsaw: turn long videos into vertical clips, edit, re-render and export them.

- Trust score: 52/100 (low)
- Registry status: active
- Liveness: live
- Owner verified: no
- Last scored: 2026-11-05

## Components

- oci · `ghcr.io/osbytes/reelsaw`: 52/100 (this document), [markdown](https://verifymcp.io/servers/osbytes-reelsaw/ghcr-io-osbytes-reelsaw.md), [page](https://verifymcp.io/servers/osbytes-reelsaw/ghcr-io-osbytes-reelsaw)

## Channel facts

- Registry: `oci`
- Package: `ghcr.io/osbytes/reelsaw`
- Version: `0.32.0`
- Transport: `stdio`

## Trust breakdown

How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. Scores are 0–100 per category. Scoring method: https://verifymcp.io/docs/scoring (what has changed: https://verifymcp.io/docs/scoring/changelog)

Scored 2026-11-05.

- **Supply Chain Security**: 0/100
  - No malware scan is available for this kind of package: the supply-chain vendors we use do not cover it. This is a permanent gap in our coverage, not a finding about the package.
  - Known CVEs could not be checked: this image's dependency list comes from an SBOM its publisher attached, which nothing verifies, so we will not report it as a clean result.
  - Install-script risk not yet assessed.
  - Dependency health could not be checked: this image's dependency list comes from an SBOM its publisher attached. It names packages and versions but not whether each is deprecated, still maintained or linked to its source, so there is nothing we can fairly grade.
- **Provenance & Transparency**: 100/100
  - Source repository is publicly reachable at the declared URL.
  - Cryptographically verified build provenance (signed, bound to osbytes/reelsaw).
  - Clear OSI-approved license (AGPL-3.0).
  - Actively maintained (last published 0 days ago).
  - Publishes a security disclosure policy (SECURITY.md).
- **Schema Quality & AI Usability**: 66/100
  - AI-judged instruction clarity (excellent).
  - Context-footprint check failed: tool/resource definitions use about 15138 tokens (~180/item across 84 items; 84 tools + 0 resources), over budget; trim descriptions and params.
  - Usage-examples check failed: none of the tools include examples.
- **Stability & Change Management**: 17/100
  - Stability observed for 5 of 30 days with no destabilising changes; credit accrues until the full window elapses.
- **Tool Coverage**: 100/100
  - 100% of tools have a non-trivial description (not blank, and not just the tool's name).
  - 100% of tool parameters carry a description.
- **Tool Safety**: 95/100
  - No prompt-injection markers were found in the server instructions, tool names or descriptions we captured.
  - 11 of 14 tool(s) whose name or description implies an irreversible operation declare an MCP destructiveHint annotation; "test_webhook" implies "send" and declares no destructiveHint at all, which the MCP spec reads as destructive by default.
  - An AI judge read all 85 captured unit(s) of tool text and found none that tries to manipulate the model reading it.
- **Capabilities**: 100/100
  - Implements a supported MCP spec version (2025-11-25); the latest is 2026-07-28.

**Unverified: 1 category.** A category scored 0 because we could not verify it: a data source with nothing on this package, evidence we could not reach, or a check we could not run. We only credit what we can confirm.

## Install

### How do I install the Reelsaw MCP server?

Reelsaw runs locally as a container image, launched with docker run --rm -i ghcr.io/osbytes/reelsaw:0.32.0. Ready-made configuration for Claude, Cursor, VS Code, Codex and 3 more is on this page, copied from each client's own documentation.

### Claude

```bash
claude mcp add osbytes-reelsaw -- docker run --rm -i ghcr.io/osbytes/reelsaw:0.32.0
```

### Cursor

```json
{
  "mcpServers": {
    "osbytes-reelsaw": {
      "command": "docker",
      "args": [
        "run",
        "--rm",
        "-i",
        "ghcr.io/osbytes/reelsaw:0.32.0"
      ]
    }
  }
}
```

### VS Code

```json
{
  "servers": {
    "osbytes-reelsaw": {
      "command": "docker",
      "args": [
        "run",
        "--rm",
        "-i",
        "ghcr.io/osbytes/reelsaw:0.32.0"
      ]
    }
  }
}
```

### Codex

```bash
codex mcp add osbytes-reelsaw -- docker run --rm -i ghcr.io/osbytes/reelsaw:0.32.0
```

### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "osbytes-reelsaw": {
      "type": "local",
      "command": [
        "docker",
        "run",
        "--rm",
        "-i",
        "ghcr.io/osbytes/reelsaw:0.32.0"
      ],
      "enabled": true
    }
  }
}
```

### Hermes

```yaml
mcp_servers:
  osbytes-reelsaw:
    command: "docker"
    args: ["run", "--rm", "-i", "ghcr.io/osbytes/reelsaw:0.32.0"]
```

### Netclaw

```json
{
  "McpServers": {
    "osbytes-reelsaw": {
      "Transport": "stdio",
      "Command": "docker",
      "Arguments": [
        "run",
        "--rm",
        "-i",
        "ghcr.io/osbytes/reelsaw:0.32.0"
      ]
    }
  }
}
```

### Other

```json
{
  "mcpServers": {
    "osbytes-reelsaw": {
      "command": "docker",
      "args": [
        "run",
        "--rm",
        "-i",
        "ghcr.io/osbytes/reelsaw:0.32.0"
      ]
    }
  }
}
```

## Changelog

Every change recorded for this component, newest first. Days that predate change tracking, or that we cannot explain, say so: "we were watching and nothing happened" and "we were not watching" are different claims.

### 2026-11-05 (score 52, +1)

- [functional] Package version: 0.31.0 → 0.32.0

### 2026-11-02 (score 51, +1)

- [security regression] Stability: 0.03 → unverified
- [security regression] Tool safety: pass → unverified
- [functional regression] Capabilities: pass → unverified
- [functional regression] Tool coverage: 100 → unverified
- [functional] First check of Schema quality: unverified
- [functional] Package version: 0.30.3 → 0.31.0
- [functional] Package version: 0.30.3 → 0.30.4

### 2026-11-01 (score 50, 0)

- [security regression] Stability: 0.00 → unverified
- [security regression] Tool safety: pass → unverified
- [functional regression] Capabilities: pass → unverified
- [functional regression] Tool coverage: 100 → unverified
- [functional] First check of Schema quality: unverified
- [functional] Package version: 0.30.1 → 0.30.3
- [functional] Package version: 0.30.1 → 0.30.2

### 2026-10-31 (score 50)

First indexed and scored.

## MCP tools (84)

### `list_projects` (~162 tokens)

List projects

Your projects, most recently updated first. Returns { projects: [{ id, title, status, stage, clips: "n/m rendered", url }], total, nextCursor }; pass nextCursor as cursor for the next page. deleted: true lists restorable deletes (storedBytes still count until purge_project). For every kind of restorable delete (projects, clips, library, templates) use list_deleted. Use get_project for one project's clips, search_clips to find by words.

Input parameters:

- `cursor` (string): nextCursor from the previous page; omit for the first page
- `deleted` (boolean): true = only deleted projects that restore_project can still bring back
- `limit` (integer): Projects per page (1-100, default 24)

### `get_project` (~106 tokens)

Get project

One of your projects: status, settings, effective intro/outro, transcript summary, and every clip (id, title, hook, bounds, score, status, download links); replacedClips lists deleted or replaced clips for restore_clip. Returns the project object. For one clip's full detail use get_clip, for the words get_transcript, for the pipeline history get_project_events.

Input parameters:

- `id` (string, required): Project id (from list_projects, search_clips or create_project)

### `create_project` (~402 tokens)

Create project

Start clipping a video from a URL (anything yt-dlp fetches) or a file you uploaded: POST /api/upload with the file as the body, header x-filename and your API key as Authorization: Bearer, then pass the uploadId it returns. Returns { id, title, status: "queued", next } at once; the pipeline (download, transcribe, pick, render) runs in the background for minutes, so follow with wait_for_project(id). Intro/outro default to your library defaults unless introAssetId/outroAssetId say otherwise.

Input parameters:

- `introAssetId`: Library asset id (search_assets with source library) for the intro of every clip; null = no intro; omitted = your default intro
- `introSec` (number): Seconds an intro image shows (1-10)
- `metadata` (object): Your own key/value strings (≤20 keys, 1 KB), returned by get_project and sent in every webhook for this project, e.g. { runId: "n8n-123" }
- `outroAssetId`: Same for the outro
- `outroSec` (number): Seconds an outro image shows (1-10)
- `settings` (object): Project settings; omit for the defaults (automatic clip count, 20–60 s clips, natural pauses, name fixes and the on-screen text scan on, filler removal off). Set refineDisfluency / disfluencyLlm true…
- `sourceUrl` (string): http(s) URL of the video; give this or uploadId
- `title` (string): Project title (default: the video's own title)
- `uploadId` (string): Id returned by POST /api/upload (the file body, x-filename header, API key as Bearer); give this or sourceUrl

### `update_project` (~223 tokens)

Update project

Rename a project and/or change its saved settings (focus, instructions, clip length, maxClips, caption style, framing, pauses, refineEntities, refineDisfluency, disfluencyLlm, refineScenes); only the fields you pass change. Does not re-pick clips or re-render: reanalyze_project picks again with the new selection settings, refine_project re-runs refine steps, rerender_clips applies style changes. Returns { changed, needsReanalyze, needsRefine, needsRerender, hint }.

Input parameters:

- `clear` (array): Optional settings to reset to their default, e.g. maxClips (back to automatic) or focus
- `id` (string, required): Project id (from list_projects, search_clips or create_project)
- `metadata` (object): Replace the project's metadata (the whole map; {} clears it)
- `settings` (object): Settings to change; keys left out keep their value. Intro/outro go through set_project_bumpers
- `title` (string): New title

### `wait_for_project` (~133 tokens)

Wait for project

Block until the project is done or failed, or timeoutSeconds pass. Returns { id, status, stage, stageProgress, stageDetail ({ done, total, etaSec } while a stage works in parts, else null), error, clips: [{ id, title, status }] }; on timeout the same with hint "Still running": call it again. Use get_project instead for a non-blocking look with full clip details.

Input parameters:

- `id` (string, required): Project id (from create_project or list_projects)
- `timeoutSeconds` (integer): Longest to wait, seconds (1-120, default 60)

### `get_transcript` (~139 tokens)

Get transcript

The project's transcript as timed lines "[m:ss] text", optionally only a time range. Returns text; output longer than maxChars is cut with a note saying which fromSec to continue from. Use it to pick your own moments for create_clip.

Input parameters:

- `fromSec` (number): Start of the range, seconds from the start of the video
- `id` (string, required): Project id (from list_projects, search_clips or create_project)
- `maxChars` (integer): Cut the output at this many characters (1000-200000, default 60000)
- `toSec` (number): End of the range, seconds

### `get_clip_captions` (~141 tokens)

Get clip captions

The captions of a clip's last render as an SRT or WebVTT file: the same lines and times as the video (intro included), even when the style has captions off. Returns the file text; VTT with words: true adds karaoke word timestamps. A clip rendered before caption files existed answers that it needs a re-render (rerender_clip).

Input parameters:

- `format` (string): srt (default) or vtt
- `id` (string, required): Clip id (from get_project or search_clips)
- `words` (boolean): VTT only: add a <timestamp> before every word after the first in each cue

### `get_project_transcript_file` (~124 tokens)

Get transcript file

The project transcript as an SRT or WebVTT subtitle file, one cue per transcript segment, in source-video seconds. Returns the file text; VTT with words: true adds word timestamps. Use get_transcript to read it as plain timed lines instead.

Input parameters:

- `format` (string): srt (default) or vtt
- `id` (string, required): Project id (from list_projects, search_clips or create_project)
- `words` (boolean): VTT only: add a <timestamp> before every word after the first in each cue

### `import_transcript` (~221 tokens)

Import transcript

Use an SRT or WebVTT file as the project's transcript instead of transcribing: right after create_project (only the video is fetched) or on a done or failed project (it then re-cleans the transcript and picks clips again). Give exactly one of text, uploadId or url. Returns { format, cues, words, warnings, message }; follow with wait_for_project.

Input parameters:

- `format` (string): auto (default) decides from the WEBVTT header
- `id` (string, required): Project id (from list_projects, search_clips or create_project)
- `language` (string): Language code of the text, e.g. en, es, ja (default: the existing transcript's, else unknown)
- `text` (string): The contents of the .srt or .vtt file
- `uploadId` (string): Upload id of an .srt/.vtt file sent to POST /api/upload
- `url` (string): http(s) link to a public .srt/.vtt file

### `search_clips` (~107 tokens)

Search clips

Find your clips and projects by words in their title, hook or caption (every word must match; case-insensitive, typo-tolerant; best first). Returns { projects (max 8), clips (max limit) with ids, bounds and links, truncated? }. Use list_projects to browse instead.

Input parameters:

- `limit` (integer): Most clips to return (1-50, default 15)
- `q` (string, required): Words to find, e.g. "budget tips"

### `get_project_events` (~115 tokens)

Project activity

The project's pipeline history, newest first (the same events the Activity panel shows; live progress ticks are not stored): stage starts and completions, clip renders, retries, failures with reasons. Returns [{ at, type, ...details }]. Use it to find out why something failed or is slow; get_project shows the current state.

Input parameters:

- `id` (string, required): Project id (from list_projects, search_clips or create_project)
- `limit` (integer): Most events to return (1-200, default 50)

### `get_clip` (~136 tokens)

Get clip

Everything about one clip: text, bounds, scores, emphasis, layers (assets, text, sounds), render diagnostics, video/thumbnail/editor links, and what the next render will use with where each comes from: style { own, effective, from: clip|project|default }, framing { own, effective: { mode, adjust, sections }, from }, bumpers { own, effective } (intro/outro). Returns the clip object. get_project lists all clips in brief; poll this after an edit to see the render finish (status done).

Input parameters:

- `id` (string, required): Clip id (from get_project or search_clips)

### `update_clip` (~459 tokens)

Update clip

Edit one clip (3 s-10 min): only the fields you pass change; style, layoutSections and layers replace the whole value (null = project's / none). Title, caption and hashtags apply at once; bounds, hook, framing, style, emphasis, layers and intro/outro are burned in and re-render (~1 min) unless rerender is false. Returns the clip summary plus { changed, needsRerender, rerendered, hint }; poll get_clip until done.

Input parameters:

- `caption` (string): Post caption, not burned in (max 300 chars)
- `emphasis` (array): Words the captions colour (max 30); replaces the list
- `endSec` (number): New end, seconds from the start of the source; clip must stay ≥3 s
- `hashtags` (array): Hashtags without # (max 20); replaces the list
- `hook` (string): On-screen opening line (max 80 chars)
- `id` (string, required): Clip id (from get_project or search_clips)
- `layers`: Everything laid over or played under the clip, one list (max 30): kind asset = a library image/GIF/video (search_assets with source library; logos, stickers, b-roll), kind text = a line of text, kind…
- `layoutOverride`: Framing for the whole clip; null = automatic
- `layoutSections`: Framing per time range (max 50), source seconds; replaces all sections; null or [] = automatic
- `rerender` (boolean): Re-render when a burned-in field changed (default true, same as the app); false = keep the old video until rerender_clip
- `startSec` (number): New start, seconds from the start of the source video
- `style`: Caption/hook style for this clip on top of the project's; replaces the clip's override; null = follow the project
- `title` (string): Clip title (1-120 chars)

### `create_clip` (~182 tokens)

Create clip

Add a clip from a time range of the source (needs a transcript). mode exact keeps your bounds (3 s-10 min); mode ai lets the model pick the best moment inside them (≥8 s). Returns the new clip summary { id, status: "preparing", queued: "prepare", hint }; preparation (title, hook, scores) is queued and the render follows by itself (about a minute), so poll get_clip until done. To change an existing clip use update_clip instead.

Input parameters:

- `endSec` (number, required): Range end, seconds
- `mode` (string): exact = these bounds (default); ai = best moment inside them
- `projectId` (string, required): Project id (from list_projects, search_clips or create_project)
- `startSec` (number, required): Range start, seconds from the start of the source video

### `delete_clip` (~113 tokens)

Delete clip

Delete one clip of a live project; a render in progress stops. It shows up on Deleted (list_deleted). Reversible: restore_clip brings it back for DELETED_RETENTION_DAYS (7 days by default). Storage still counts until purge_clip. For several clips of one project use delete_clips. Returns { changed, stoppedRender (a render in flight was stopped), renderQueued: false, restorable, storedBytes }.

Input parameters:

- `id` (string, required): Clip id (from get_project or search_clips)

### `delete_clips` (~132 tokens)

Delete clips

Delete several clips of one live project in one step (the selection bar's Delete); renders in progress stop. Each shows up on Deleted (list_deleted) and restore_clip brings it back for DELETED_RETENTION_DAYS. Already-deleted ids are skipped. Storage still counts until purge_clip. Returns { deleted, skipped, stoppedRenders, renderQueued: false, restorable, storedBytes }.

Input parameters:

- `clipIds` (array, required): Clip ids of that project (get_project), 1-200
- `projectId` (string, required): Project id (from list_projects, search_clips or create_project)

### `rerender_clip` (~117 tokens)

Re-render clip

Queue one clip for rendering again (after rerender: false edits, a failure, or an intro/outro change). Async: the new video replaces the old when done (usually under a minute); poll get_clip. Returns { step: render | prepare, renderQueued, hint } (prepare = it never got its AI title, which runs first and renders afterwards). For several clips of a project use rerender_clips.

Input parameters:

- `id` (string, required): Clip id, not a project id (from get_project or search_clips)

### `rerender_clips` (~125 tokens)

Re-render clips

Queue several clips of one project for rendering again in one step (after edits or failures). Finished and failed clips are queued, clips still rendering are skipped; each new video replaces the old when done. Returns { requested, rerendered, skipped, renderQueued, hint }; poll get_project or wait_for_project. For a single clip rerender_clip is enough.

Input parameters:

- `clipIds` (array, required): Clip ids of that project (get_project), 1-200
- `projectId` (string, required): Project id (from list_projects, search_clips or create_project)

### `get_media_links` (~261 tokens)

Get share links

Links anyone can download without an Authorization header, for a scheduler (Buffer, Metricool), Instagram's video_url, Zapier/Make or a person. Returns { clips: [{ clipId, links: { clip, thumb, srt, vtt } }], export } with each link's { linkId, url, expiresAt }; unrendered clips say why. The active link is reused unless label or ttlDays ask for a separate one. A link serves the current render until it expires or revoke_media_link turns it off.

Input parameters:

- `clipIds` (array): Only these clips; omitted = every rendered clip
- `export` (boolean): Also a link to the zip of these clips with YouTube/TikTok metadata
- `kinds` (array): Which files (default: clip): clip = the MP4, thumb = its JPEG, srt / vtt = its captions
- `label` (string): A name for a separate link you can revoke on its own, e.g. "Buffer queue"
- `projectId` (string, required): Project id (from list_projects, search_clips or create_project)
- `ttlDays`: Days the links last (capped by the instance); null = never expires where allowed

### `list_media_links` (~138 tokens)

List share links

Your share links, newest first. Returns { items: [{ id, kind, projectId, clipId, title, url (masked), label, source, state (active | expired | revoked), expiresAt, lastUsedAt, useCount }], nextCursor, total }. Use get_media_links for a full URL.

Input parameters:

- `active` (boolean): Only links that still work
- `clipId` (string): Only this clip's links
- `cursor` (string): nextCursor from the previous page
- `limit` (integer): Links per page (1-100)
- `projectId` (string): Only this project's links

### `revoke_media_link` (~125 tokens)

Revoke share links

Turn share links off now: one (linkId), a clip's (clipId), a project's (projectId) or all of yours (all: true). Give exactly one. Returns { revoked, message }. Anyone holding a revoked link gets a 404 from then on.

Input parameters:

- `all` (boolean): Every share link you have
- `clipId` (string): Every link to this clip
- `linkId` (string): Link id (from get_media_links or list_media_links)
- `projectId` (string): Every link to this project's clips and zip

### `list_webhooks` (~74 tokens)

List webhooks

Your webhook endpoints. Returns [{ id, url (masked), description, events, status (active | failing | off), disabledReason, failingSince, lastDelivery }]. Reelsaw POSTs signed JSON (Standard Webhooks headers webhook-id, webhook-timestamp, webhook-signature) to each when a subscribed event happens.

### `create_webhook` (~144 tokens)

Create webhook

Subscribe a URL to events: project.created, project.done (with every clip, share links and the zip), project.failed, clip.done (also re-renders), clip.failed. This is how a Zapier or Make app subscribes (delete_webhook unsubscribes). Returns { endpoint, secret }: the whsec_ signing secret is shown only here; verify deliveries with any Standard Webhooks library. The URL must be public https unless the admin allows private networks.

Input parameters:

- `description` (string): Your label, e.g. "n8n clips workflow"
- `events` (array, required): Events to send
- `url` (string, required): Where to POST (https)

### `update_webhook` (~134 tokens)

Update webhook

Change a webhook's url, events or description, or turn it on or off (enabled). Turning it on clears a failure streak; turning it off cancels deliveries waiting to be sent (resend_webhook_delivery brings them back). Returns { endpoint, changed, message }.

Input parameters:

- `description`: New label; null clears it
- `enabled` (boolean): Turn it on (true) or off (false)
- `events` (array): Replace the events it gets
- `id` (string, required): Webhook id (from list_webhooks)
- `url` (string): New URL to POST to (https)

### `delete_webhook` (~51 tokens)

Delete webhook

Remove a webhook (unsubscribe); nothing more is sent to it and its delivery log goes with it. Returns { ok, message }.

Input parameters:

- `id` (string, required): Webhook id (from list_webhooks)

### `rotate_webhook_secret` (~75 tokens)

Rotate webhook secret

A new signing secret for a webhook. Deliveries are signed with both the new and the old one for 24 h, so the receiver can switch; refused while a rotation is still open. Returns { secret, previousStopsAt, message }.

Input parameters:

- `id` (string, required): Webhook id (from list_webhooks)

### `test_webhook` (~58 tokens)

Test webhook

Send a webhook.test delivery now and wait for the answer. Returns { ok, code, message, response (first 2 KB of what the receiver said) }.

Input parameters:

- `id` (string, required): Webhook id (from list_webhooks)

### `list_webhook_deliveries` (~156 tokens)

List webhook deliveries

The delivery log, newest first. Returns { items: [{ id, endpointId, eventId, type, projectId, status (pending | sending | delivered | failed | cancelled), attempt, lastStatusCode, lastError, nextAttemptAt, deliveredAt }], nextCursor, total }. Failed deliveries were retried 8 times over about 22 h.

Input parameters:

- `cursor` (integer): nextCursor from the previous page
- `endpointId` (string): Only this webhook's deliveries (from list_webhooks)
- `limit` (integer): Deliveries per page (1-100)
- `projectId` (string): Only deliveries about this project
- `status` (string): Only deliveries in this state

### `resend_webhook_delivery` (~65 tokens)

Resend webhook delivery

Send a delivery again (a new series of attempts, same webhook-id and body, signed afresh). The webhook must be on. Returns { ok, deliveryId, message }.

Input parameters:

- `deliveryId` (integer, required): Delivery id (from list_webhook_deliveries)

### `export_project` (~147 tokens)

Export clips

A download link for a zip of the project's rendered clips with YouTube/TikTok metadata. Same conditions as the app's Export: the project must be live and at least one picked clip rendered, else the answer says why (not found, or nothing rendered yet). Returns { url, clips (rendered, in the zip), notRendered (picked but not finished, left out) }; the link needs the same Authorization header as this server.

Input parameters:

- `clipIds` (array): Only these clips (max 200; must be rendered clips of the project); omitted = every rendered clip
- `id` (string, required): Project id (from list_projects, search_clips or create_project)

### `report_issue` (~278 tokens)

Report an issue

A download link for an issue-report zip (the same file Report an issue in the app saves; admins may include instance-wide diagnostics). Returns { url, filename }; GET the url with the same Authorization header as this server. Admins may set instance true to include unfinished work, failed jobs, usage and error logs across every account, for queue, disk or machine problems that are not just one project. On hosted instances with telemetry, non-admins cannot download a zip.

Input parameters:

- `allClips` (boolean): Include every rendered clip (default false)
- `clipIds` (array): Clips the problem shows up in (from get_project)
- `description` (string, required): What went wrong (a sentence is enough)
- `instance` (boolean): Admins only: include unfinished work, failed jobs, usage and error logs across every account
- `logs` (boolean): Include this project's log lines (default true)
- `previews` (boolean): Include thumbnails and the selected clips' videos (default true)
- `projectId` (string): Project the report is about (from list_projects); omit for an account- or instance-level report
- `source` (boolean): Include the source video file (default false)
- `system` (boolean): Include versions, machine resources, queue counts and recent errors (default true; ignored when not allowed)

### `retry_project` (~96 tokens)

Retry project

Resume a failed or stuck project from its furthest completed stage: re-render unfinished clips if there are clips, else pick clips, refine or transcribe again. Finished clips, the transcript and the source are kept. Returns { queue } (the stage restarted); then wait_for_project. To pick different clips use reanalyze_project instead.

Input parameters:

- `id` (string, required): Project id (from list_projects, search_clips or create_project)

### `refine_project` (~147 tokens)

Refine project

Re-run refine steps on the existing transcript: entities (name spelling), disfluency (filler and repeat cuts), scenes (shot cuts); all three when steps is omitted. Optionally set the vocabulary (names and terms, comma separated; empty string clears it) first. Fixes you accepted or rejected may be replaced. Clips re-render afterwards. Returns { ok, steps }; then wait_for_project.

Input parameters:

- `id` (string, required): Project id (from list_projects, search_clips or create_project)
- `steps` (array): Which refine steps to re-run; default all
- `vocabulary` (string): Names and terms the transcript should spell this way; empty string clears it

### `reanalyze_project` (~186 tokens)

Re-analyze project

Pick clips again from the existing transcript (needs one), optionally with new settings. Clips you created or edited are kept; untouched AI picks are replaced and restorable with restore_clip for a while. Returns { kept, replaced } at once; the picking runs in the background, so follow with wait_for_project. To only save settings use update_project; to resume a failure use retry_project.

Input parameters:

- `id` (string, required): Project id (from list_projects, search_clips or create_project)
- `settings` (object): Project settings; omit for the defaults (automatic clip count, 20–60 s clips, natural pauses, name fixes and the on-screen text scan on, filler removal off). Set refineDisfluency / disfluencyLlm true…

### `search_assets` (~264 tokens)

Search library, catalog and providers

Find assets. source "library" = your own uploads (name and tags; empty q lists all); omitted or "catalog" = the Reelsaw catalog (CC0, works offline); or a provider an admin enabled. Returns { provider, items: [{ provider, externalId, name, kind, license, durationSec }], nextCursor }. Adds nothing: a catalog / provider hit's provider + externalId go to add_asset.

Input parameters:

- `cursor` (string): nextCursor from the previous page; omit for the first page
- `deleted` (boolean): library only: list your deleted items instead (their ids go to restore_asset)
- `kind` (string): Only this kind of result
- `license` (string): freesound only: cc0 (default) or by (CC0 and CC-BY, attribution is stored)
- `limit` (integer): Results per page (1-60, default 24)
- `q` (string): What to look for, e.g. "whoosh", "arrow", "city at night"; empty lists everything in the source
- `source` (string): library, catalog (default), pexels, pixabay, freesound, iconify, openverse, giphy or klipy

### `add_asset` (~377 tokens)

Add library asset

Add from exactly one source: a public url or an uploadId (video, image, GIF, SVG or sound) becomes one of your library uploads; a search_assets catalog / provider hit (provider + externalId) materializes one instance-shared copy (checksum verified, license recorded; not listed in your library). Returns { id, name, kind, mime, durationSec, duplicate, restored, owner } (the same file twice returns the existing item). setDefault on an upload makes it your default intro/outro for NEW projects only.

Input parameters:

- `color` (string): iconify hits only: icon color as hex, e.g. #ffffff (default white)
- `externalId` (string): A search_assets hit's externalId, together with provider
- `imageSeconds` (number): How long a still image shows as an intro/outro (1-10 s)
- `name` (string): Library name (url or uploadId; default: the file name). update_asset changes it later
- `provider` (string): A search_assets hit's provider (catalog or an enabled provider), together with externalId; materializes the instance-shared file instead of adding an upload
- `setDefault` (string): Also make it your default intro or outro for new projects
- `tags` (array): Search tags (lowercase words; max 20)
- `uploadId` (string): An existing upload id (from POST /api/upload) instead of a url
- `url` (string): http(s) link to a video (MP4/MOV/WebM), image (PNG/JPG/WebP/GIF/SVG) or sound (MP3/M4A/AAC/WAV/OGG/FLAC); the server downloads it into your library (max 50 MB, 60 s)

### `update_asset` (~156 tokens)

Update library asset

Rename one of your own library items, replace its tags, or change how long a still image shows (deleted items too; shared catalog and provider items cannot be changed). Only labels change; nothing re-renders, except that a new image length applies on each project's next render. Returns { id, name, tags, durationSec, previousName }.

Input parameters:

- `assetId` (string, required): Library asset id (from search_assets)
- `imageSeconds` (number): Seconds a still image shows as an intro/outro (1-10); images only
- `name` (string): New name (1-120 chars)
- `tags` (array): Replaces the tags (lowercase words; max 20); [] clears them

### `delete_asset` (~76 tokens)

Delete library asset

Delete one of your uploads: it leaves the library and your defaults and goes to Deleted. Projects and clips already using it keep rendering with it. Undo with restore_asset (list_deleted says how long it is kept); check asset_usage first. Returns { ok }.

Input parameters:

- `assetId` (string, required): Library asset id (from search_assets)

### `restore_asset` (~64 tokens)

Restore library asset

Bring one of your deleted library items back (the id from delete_asset or list_deleted); restoring an item that is not deleted changes nothing. It does not become a default again. Returns { ok }.

Input parameters:

- `assetId` (string, required): Library asset id (from delete_asset)

### `asset_usage` (~112 tokens)

Where a library asset is used

Where a library item is used: as intro, outro, overlay or music on projects and clips (replaced clips left out), and in layer templates. Returns { projects: [{ id, title, slots, clips: [{ id, index, title, slots, via, overlays, music }] }], templates: [{ id, name, layers }] }. A deleted asset's layer is skipped when a template is applied. Check before deleting one.

Input parameters:

- `assetId` (string, required): Library asset id (from search_assets)

### `set_default_bumper` (~85 tokens)

Set default intro/outro

Make a library item your default intro or outro for projects you create from now on (assetId null clears it). Existing projects keep theirs; to change one use set_project_bumpers. Returns { ok }.

Input parameters:

- `assetId` (required): Library asset id (search_assets with source library), or null for no default
- `slot` (string, required): Which default to set

### `set_project_bumpers` (~269 tokens)

Set a project's intro/outro

Set one project's intro and/or outro: "default" (your current default), "none", or a library asset id; a slot left out is untouched, and introSec/outroSec are saved with the project. Clips with their own choice (set_clip_bumpers) keep it. Applies on each clip's next render; rerender: true re-renders finished clips now. Returns { rerendered }. set_default_bumper changes future projects instead.

Input parameters:

- `intro`: "default" = your current default intro/outro, "none" = no bumper, or a library asset id (see list_bumpers)
- `introSec` (number): Seconds an intro image shows in this project (1-10); videos keep their own length. Saved with the project
- `outro`: "default" = your current default intro/outro, "none" = no bumper, or a library asset id (see list_bumpers)
- `outroSec` (number): Seconds an outro image shows in this project (1-10). Saved with the project
- `projectId` (string, required): Project id (list_projects or create_project)
- `rerender` (boolean, required): true = re-render the project's finished clips now; false = apply on their next render

### `set_clip_bumpers` (~240 tokens)

Set clips' intro/outro

Override the intro and/or outro of up to 200 clips of one project: "inherit" (follow the project), "none", or a library asset id; a slot left out is untouched. rerender: true re-renders only these clips. Returns { updated, rerendered }.

Input parameters:

- `clipIds` (array, required): Clip ids of that project (get_project), 1-200 per call
- `intro`: "inherit" (alias "default") = follow the project, "none" = no bumper on this clip, or a library asset id
- `introSec` (number): Seconds an intro image shows on these clips (1-10)
- `outro`: "inherit" (alias "default") = follow the project, "none" = no bumper on this clip, or a library asset id
- `outroSec` (number): Seconds an outro image shows on these clips (1-10)
- `projectId` (string, required): Project the clips belong to (list_projects)
- `rerender` (boolean, required): true = re-render only these clips now; false = apply on their next render

### `list_layer_templates` (~175 tokens)

List layer templates

Your layer templates (layers that fit any clip length, plus an optional intro / outro), newest first. Returns { items: [{ id, name, layerCount, layers, bumpers, updatedAt }], nextCursor, total, deleted } (nextCursor goes in cursor; deleted = restorable). A layer has the clip-layer fields plus timing { mode, start, end, minSec, maxSec }: head = seconds after the start; tail = seconds before the end (end 0 = the very end); fill = whole clip; relative = fractions 0-1 of the length, held between minSec and maxSec

Input parameters:

- `cursor` (string): nextCursor from the previous page; omit for the first page
- `limit` (integer): Templates per page (1-500, default 100)

### `save_layer_template` (~300 tokens)

Save layer template

Create a template from exactly one of clipId (that clip's layers: spanning = fill, touching the start = head, touching the end = tail, else anchored to the nearer edge; plus the intro and outro the clip renders with) or layers (each with a timing, see list_layer_templates; [] with bumpers = an intro / outro only template). Returns the template { id, name, layerCount, layers, bumpers }. Use apply_layer_template to put it on clips

Input parameters:

- `bumpers` (object): With layers: the intro / outro the template puts on clips ({ intro, introSec, outro, outroSec }; a slot left out = clips keep their own, null = none, an id = that video or image). With clipId the cli…
- `clipId` (string): Build the template from this clip's layers (get_project or search_clips). Pass clipId or layers, not both
- `layers` (array): Or the layers themselves, each with a timing: { kind: asset|text|audio, id, timing: { mode: head|tail|fill|relative, start, end, minSec, maxSec }, ...the layer's own fields (assetId, x, y, w / text,…
- `name` (string, required): Template name (1-120 chars)

### `update_layer_template` (~130 tokens)

Update layer template

Rename a layer template, replace its layers (the whole list, each with a timing) and/or replace its intro / outro (bumpers; null = it sets neither); a field left out is untouched. Clips that already got the template keep what they have; apply_layer_template again to refresh them. Returns the template.

Input parameters:

- `bumpers`
- `layers` (array): The template's layers, each with a timing (max 30)
- `name` (string): Template name (1-120 chars)
- `templateId` (string, required): Layer template id (list_layer_templates)

### `delete_layer_template` (~82 tokens)

Delete layer template

Delete a template (it goes to Deleted) and drop it from your project defaults; clips that already got its layers keep them. Undo with restore_layer_template (list_deleted says how long it is kept). Returns { ok, clearedDefault } (it was your default and no longer is)

Input parameters:

- `templateId` (string, required): Layer template id (list_layer_templates)

### `restore_layer_template` (~54 tokens)

Restore layer template

Bring back a deleted layer template (the id from list_layer_templates deleted or list_deleted). It does not become a default again. Returns the template.

Input parameters:

- `templateId` (string, required): Layer template id (list_layer_templates)

### `apply_framing` (~217 tokens)

Apply framing to the project

Make one clip's framing the whole project's, for a recording whose layout never changes (a slide deck with a webcam tile, a fixed camera): the clip's section at `at` (else its first framed section) gives the mode and its nudge / subject / crop, which become the project's reframe and reframeAdjust; every clip's own sections are cleared (unless keepOverrides) and the finished clips re-render. New clips get it too. Returns { reframe, adjust, cleared, rerendered }.

Input parameters:

- `at` (number): Seconds from the start of the source: which section of the clip to copy (default: its first section that is not plain automatic)
- `clipId` (string, required): Clip whose framing to copy (from get_project or search_clips)
- `keepOverrides` (boolean): Leave other clips' own sections in place; they still win over the project's framing (default false: clear them)
- `rerender` (boolean): Re-render the finished clips now (default true)

### `apply_layer_template` (~248 tokens)

Apply layer template to clips

Write a template onto clips of one project (up to 200 clipIds, or all): its layers fitted to each clip's length, and the intro / outro it sets as each clip's own. Not linked afterwards. mode replace (default) swaps the clips' layers, add keeps them and adds. rerender true re-renders finished clips now, else the next render shows them. update_project settings.layerTemplateId only affects clips created later. Returns { updated, rerendered, skipped: [{ clipId, reason }], layersSkipped, bumpers: slots set }

Input parameters:

- `clipIds` (array): Clip ids of that project (get_project), 1-200; omitted = every clip of the project
- `mode` (string): "replace" (default) = the template becomes the clip's layers; "add" = its layers join the ones the clip already has
- `projectId` (string, required): Project the clips belong to (list_projects)
- `rerender` (boolean): true = re-render the affected finished clips now; false = the layers show on their next render
- `templateId` (string, required): Layer template id (list_layer_templates)

### `delete_project` (~152 tokens)

Delete project

Delete one of your projects and all its clips; work in progress stops. Reversible: restore_project brings it back for DELETED_RETENTION_DAYS (7 days by default). Storage still counts until purge_project (or the retention sweep) removes it for good; minutes already used stay counted. Returns { changed, restorable, storedBytes, forgetDecisions, message }.

Input parameters:

- `forgetDecisions` (boolean): Forget what Reelsaw learned from it (its decision log entries) when it is deleted for good, by purge_project or the retention sweep; restoring it cancels that. Default false keeps them
- `id` (string, required): Project id (from list_projects, search_clips or create_project)

### `purge_project` (~156 tokens)

Delete project forever

Permanently remove a deleted project and its files so the storage is freed (find it with list_deleted or list_projects deleted: true). Minutes, hourly creates and first-video-free are not refunded. A project that is not deleted is refused: call delete_project first. What you decided about it (accepted and rejected fixes, clip edits) stays in your decision log so Reelsaw keeps learning from it, unless forgetDecisions is true. Returns { storedBytes, forgottenDecisions, message }.

Input parameters:

- `forgetDecisions` (boolean): Also forget what Reelsaw learned from this project (its entries in the decision log); default false keeps them
- `id` (string, required): Project id (from list_projects with deleted: true)

### `restore_project` (~92 tokens)

Restore project

Bring back a deleted project with its clips and files (find it with list_deleted or list_projects deleted: true); restoring a project that is not deleted changes nothing. Refused if the account is already at its live project cap. A project that was processing comes back failed: retry_project resumes it. Returns { changed, status }.

Input parameters:

- `id` (string, required): Project id (from list_projects with deleted: true)

### `restore_clip` (~87 tokens)

Restore clip

Bring back a deleted clip or one a re-analyze replaced (list_deleted or get_project → replacedClips); it goes to the end of the project's clips. Returns { changed, status, needsRerender }; needsRerender means it never finished, so call rerender_clip.

Input parameters:

- `id` (string, required): Clip id (from list_deleted or get_project → replacedClips)

### `list_deleted` (~121 tokens)

List deleted items

Restorable deletes (the Deleted page): one newest-first list of projects, clips of live projects, library items and templates. Returns { items: [{ kind, id, title, deletedAt, purgeAt, storedBytes, canPurge, … }], total, truncated } (max 200) plus keepDays / libraryDays / templateDays. purgeAt null = kept until deleted forever. restore_project / restore_clip / restore_asset / restore_layer_template bring them back; purge_project / purge_clip / purge_asset / purge_layer_template remove them for good.

### `purge_clip` (~79 tokens)

Delete clip forever

Permanently remove a deleted clip of a live project and its rendered file so the storage is freed (find it with list_deleted). A clip that is not deleted is refused: call delete_clip first. Clips of a deleted project go with purge_project. Returns { storedBytes, message }.

Input parameters:

- `id` (string, required): Clip id (from list_deleted)

### `purge_asset` (~83 tokens)

Delete library item forever

Permanently remove an already-deleted library upload and its files so the storage is freed (find it with list_deleted). Refused while a project, clip or live template still names it. An item that is not deleted is refused: call delete_asset first. Returns { storedBytes, message }.

Input parameters:

- `assetId` (string, required): Library asset id (from list_deleted)

### `purge_layer_template` (~74 tokens)

Delete layer template forever

Permanently remove an already-deleted layer template (find it with list_deleted). Templates hold no media files; this only drops the row. A template that is not deleted is refused: call delete_layer_template first. Returns { ok, message }.

Input parameters:

- `templateId` (string, required): Layer template id (list_layer_templates)

### `check_for_update` (~109 tokens)

Check for update

Admins only: is a newer Reelsaw published? Returns { current, latest: { version, url, publishedAt }, available, checkedAt, error, upgrade: { command, note, docsUrl } }; available is null when the running build is not a release (from source). The server checks every few hours; refresh: true asks GitHub again now.

Input parameters:

- `refresh` (boolean): true = ask GitHub now instead of returning the last answer (at most once a minute)

### `list_releases` (~123 tokens)

List releases

Published Reelsaw versions from GitHub (reelsaw.com/releases): tag, date, a short summary and the change notes. Returns { releases, total, windowDays, commits, commitDays, releaseDays, truncated, fetchedAt, sourceUrl, error }; pass tag for one version. Cached for an hour.

Input parameters:

- `limit` (integer): How many releases to return (1-100, default 20, newest first)
- `tag` (string): Only this tag or version (v0.19.1 or 0.19.1)

### `get_instance_settings` (~95 tokens)

Instance settings

Admins only: the instance settings in force (sign-up mode, email verification, captcha, legal links, quotas, upload limits, retention, stall watchdog, update check, health details) and where each comes from. Returns { values, env, override, source: { KEY: "env" | "db" }, ignored: { KEY: reason }, groups, help, updatedAt, updatedBy }; secrets are masked.

### `list_workers` (~138 tokens)

List workers

Admins only: every Reelsaw process that has checked in (API, worker, combined app): CPU, memory, disk (free/total MB, -1 = unmeasurable; diskLeasedMB = claimed by jobs in flight), GPU, render slots, queues it listens on, and its jobs. A worker out of disk drops the media queues from `queues` until it recovers. `paused` = what it was short of (memory, CPU, disk), null when it has room. Returns { items, live, stale }; stale = no check-in for 45 seconds. Hosted non-admins are refused.

### `get_failure` (~102 tokens)

Get a failed project or clip

Admins only: one failed project or clip (title, owner email, stage, full recorded reason, when). A clip whose project also failed is not returned — retry the project. Returns the item or null if it is gone (retried, deleted). Hosted non-admins are refused.

Input parameters:

- `id` (string, required): Project id or clip id
- `kind` (string, required): project = the whole pipeline stopped; clip = one clip failed to prepare or render

### `get_queue` (~86 tokens)

Queue summary

Admins only: waiting, running and retrying job counts per pipeline stage with when the longest wait began (oldestWaitingAt), plus the jobs currently in flight (project title, attempt, started, and a retrying job's lastError). Returns { queues, active, truncated, error? }. Failed work is list_failures, not this. Hosted non-admins are refused.

### `list_failures` (~158 tokens)

List failed work

Admins only: failed projects and clips across the instance (owner email, stage, reason, when). A failed project's clips are not listed separately — retry the project. Returns { items, nextCursor, total, limit, truncated }. Filter with q (substring of the error), stage, since (ISO time). Hosted non-admins are refused.

Input parameters:

- `cursor` (string): nextCursor from the previous page
- `limit` (integer): Page size, default 50
- `q` (string): Case-insensitive substring of the recorded error (e.g. disk, encoder, model)
- `since` (string): ISO timestamp; only failures updated at or after this time
- `stage` (string): Pipeline stage that failed

### `retry_failures` (~167 tokens)

Retry failed work

Admins only: retry matching failed projects and clips through the same lifecycle as retry_project / rerender_clip (never a raw queue poke). Each item is its own transaction so live-job caps still apply. Cap 50 per call. Returns { retried, skipped, nextCursor }. Writes an admin event failures.retried. Hosted non-admins are refused.

Input parameters:

- `ids` (array): Retry these rows instead of the current filter; omitted = every match of q/stage/since
- `limit` (integer): Max items to retry this call, default 50
- `q` (string): Same error substring as list_failures
- `since` (string): Same since filter as list_failures
- `stage` (string): Same stage filter as list_failures

### `set_instance_settings` (~2114 tokens)

Change instance settings

Admins only: change instance settings without a restart; every process applies them within seconds and the change is audited. Pass only the keys to change; null returns a key to the environment's value; an empty string clears an optional one (URLs, captcha). Validated as a whole (a captcha provider needs both keys; email verification needs an email driver). Returns the same state as get_instance_settings.

Input parameters:

- `ADMIN_EVENT_RETENTION_DAYS`: Prune the admin event log after this many days. 0 = keep forever.
- `ALLOW_PRIVATE_SOURCE_URLS` (boolean|null): Let link sources point at LAN or loopback hosts (a NAS). Off, they are blocked (SSRF guard).
- `ALLOW_PRIVATE_WEBHOOK_URLS` (boolean|null): Let webhook URLs point at LAN or loopback hosts (a self-hosted n8n next to Reelsaw), over http too. Off, they must be public https (SSRF guard).
- `BLOCK_DISPOSABLE_EMAILS` (boolean|null): Refuse sign-ups from known temporary-address domains (a vendored list of about 120k domains).
- `CAPTCHA_PROVIDER`: Bot protection on sign-up, sign-in and password reset: cloudflare-turnstile, google-recaptcha or hcaptcha; empty = off. Needs both keys.
- `CAPTCHA_SECRET_KEY`: Secret captcha key the server verifies with; stored encrypted, never shown again; empty = unset.
- `CAPTCHA_SITE_KEY`: Public captcha key the browser widget uses; empty = unset.
- `CATALOG_URL`: Where the Reelsaw catalog manifest (sounds, icons, stickers) is fetched from, hourly; the starter pack bundled with Reelsaw is used until a newer one arrives.
- `CLIP_RETENTION_DAYS`: Delete rendered clip files this many days after rendering; the clip stays and can be re-rendered. 0 = keep forever.
- `DECISION_RETENTION_DAYS`: Prune users' recorded decisions (accepts, rejects, edits Reelsaw learns from) older than this. They outlive deleted projects until then. 0 = keep forever.
- `DELETED_RETENTION_DAYS`: Deleted projects and replaced clips stay restorable this long, then rows and files go. 0 = keep forever.
- `DISFLUENCY_LLM` (boolean|null): Model pass on top of rule-based disfluency detection. Off by default. Ignored when disfluency removal is off. Turn on so projects may opt in via disfluencyLlm.
- `DISFLUENCY_REMOVAL` (boolean|null): Detect fillers, repetitions and false starts after transcription. Off by default; turn on so projects may opt in via refineDisfluency.
- `EVENT_PROJECT_DONE_READY_EMAIL_DELAY_MINUTES`: Minutes to wait after the first project in a batch finishes before emailing that the clips are ready, so several projects land in one note. Sent only when an email driver is set and the account has n…
- `EVENT_RETENTION_DAYS`: Prune project events older than this; each project keeps its run skeleton. 0 = keep forever.
- `FREESOUND_API_KEY`: Freesound API key (free): enables sound effects in Browse. Stored encrypted; empty = provider hidden.
- `GIPHY_API_KEY`: Giphy API key: enables GIFs and stickers in Browse. Stored encrypted; empty = provider hidden.
- `HEALTH_DETAILS` (boolean|null): Include machine numbers in GET /api/health for anonymous callers (private networks only). Admins always get them.
- `ICONIFY_API_URL`: Iconify API used for icon search; the public one by default, or your own self-hosted instance (https).
- `KLIPY_API_KEY`: Klipy API key: enables GIF search in Browse (experimental). Stored encrypted; empty = provider hidden.
- `LLM_CALL_TIMEOUT_SECONDS`: Longest one model call may take (all its HTTP attempts together) before its step is stopped and retried. 0 = auto: 120 s for a hosted model, 600 s for a self-hosted one, and for a self-hosted transcr…
- `LLM_USAGE_RETENTION_DAYS`: Prune model and transcription usage rows older than this. 0 = keep forever.
- `MAX_LIVE_JOBS_PER_USER`: Queued plus running jobs an account's own requests may add to. 0 = unlimited.
- `MAX_NEW_PROJECTS_PER_HOUR`: Projects an account may create per hour. 0 = unlimited.
- `MAX_PROJECTS_PER_USER`: Projects an account may keep. 0 = unlimited. Admins can override per user on the Users page.
- `MAX_REQUESTS_PER_HOUR`: Changes an account may make per hour. 0 = unlimited.
- `MAX_REQUESTS_PER_MINUTE`: Changes (API mutations, MCP tool calls) an account may make per minute. 0 = unlimited. Admins can override per user on the Users page.
- `MAX_RERENDERS_PER_HOUR`: Re-renders, hand-made clips and re-runs (re-analyze, refine) an account may ask for per hour. 0 = unlimited.
- `MAX_SIGNUPS_PER_IP_PER_DAY`: Accounts one network address may create per day. 0 = unlimited. Needs TRUST_PROXY behind a reverse proxy.
- `MAX_SOURCE_FILE_MB`: Largest file a link source may download (yt-dlp --max-filesize), in MB.
- `MAX_SOURCE_MINUTES_PER_DAY`: Minutes of source video an account may add per day (UTC). 0 = unlimited.
- `MAX_SOURCE_MINUTES_PER_MONTH`: Minutes of source video an account may add per period: its billing period when it has a plan, else the calendar month (UTC). 0 = unlimited.
- `MAX_STORAGE_MB`: MB of sources, uploads, renders and intros/outros an account may keep (deleted items count until they are deleted forever). 0 = unlimited.
- `MAX_UPLOAD_MB`: Largest accepted file upload, in MB.
- `MAX_WEBHOOKS_PER_USER`: Webhook endpoints an account may add. 0 = unlimited. Admins can override per user on the Users page.
- `MEDIA_LINK_ALLOW_NEVER` (boolean|null): Owners may make a share link that never expires (they can still revoke it).
- `MEDIA_LINK_RETENTION_DAYS`: Delete revoked and expired share links this many days after they stopped working. 0 = keep forever.
- `MEDIA_LINK_TTL_DAYS`: Share links last this many days unless the owner picks less.
- `OPENVERSE_ENABLED` (boolean|null): Show Openverse (Creative Commons images and audio, no key) in Browse. Off by default until you have checked its limits for your use.
- `PEXELS_API_KEY`: Pexels API key (free): enables stock photos and portrait videos in Browse. Stored encrypted; empty = provider hidden.
- `PIXABAY_API_KEY`: Pixabay API key (free): enables photos, illustrations, vectors and videos in Browse. Stored encrypted; empty = provider hidden.
- `PRIVACY_URL`: Privacy policy link shown on the sign-up form; empty = none.
- `REFINE_SCENES` (boolean|null): Scan the video for on-screen text (burned-in captions, watermarks, slides) during refine. On by default; off closes the ceiling (projects opt out via refineScenes). Shot cuts are always detected.
- `REQUIRE_EMAIL_VERIFICATION` (boolean|null): New users must click the verification link before they can sign in. The link works for 24 hours and is single-use; an expired one offers a new send. Needs an email driver in .env.
- `SCRATCH_CACHE_HOURS`: With an S3 store, drop a worker's local copies of the sources and rendered clips of finished projects after this many idle hours; a job pulls them back from the bucket when it needs them, and a worke…
- `SIGNUP_MODE`: Who may sign up: open (anyone, right for a public instance with email confirmation), approval (new accounts wait for an admin; a private box) or closed (nobody). The first account is always the admin.
- `SOURCE_RETENTION_DAYS`: Delete stored source videos this many days after upload; the project stays and Retry re-fetches. 0 = keep forever.
- `STAGE_IDLE_TIMEOUT_MINUTES`: A stage made of checkpointed steps (finding moments) is stopped when no step has finished for this long; Retry continues where it stopped. 0 = auto: 1.5 times the model call timeout.
- `STALL_MINUTES`: A running job with no progress for this long is reported as stalled (log and UI).
- `STEP_RETENTION_DAYS`: Drop the saved checkpoints (finding-moments windows) of a finished project this many days after it last changed; a Retry sooner than that reuses them. 0 = keep forever.
- `TERMS_URL`: Terms of service link shown on the sign-up form; empty = none.
- `TRANSCRIPT_CORRECTION` (boolean|null): After transcription, fix misheard names with the language model. On by default; off closes the ceiling so no project runs it (projects opt out via refineEntities).
- `UPDATE_CHECK` (boolean|null): Ask GitHub for the latest release and tell admins when a newer Reelsaw exists. Only the request leaves the machine.
- `WATERMARK_NON_ADMINS` (boolean|null): Burn a small watermark into the clips of accounts that are not admins. A paid plan removes it, and an admin can switch it per user on the Users page.
- `WATERMARK_TEXT`: The watermark text, e.g. your site name.
- `WEBHOOK_DELIVERY_RETENTION_DAYS`: Delete webhook delivery log rows older than this. 0 = keep forever.

### `get_email_preferences` (~55 tokens)

Email preferences

Which optional emails you get, by topic. Returns { emailEnabled, emailPreferences: { ready } }; ready is the note sent when projects finish while you are away. emailEnabled false means this instance sends no mail at all.

### `set_email_preferences` (~97 tokens)

Set email preferences

Turn topics of optional email on or off; omitted topics stay as they are. ready = the note sent when projects finish while you are away (Settings → Account, or the link in the email). Mail you need (password reset, verification) is not affected. Returns { ok, emailPreferences }.

Input parameters:

- `ready` (boolean): the note sent when projects finish while you are away: true to get it, false to stop it

### `get_billing` (~101 tokens)

Billing

Hosted instances: your plan and subscription, the billing period, the limits in force with where each comes from (instance, plan, an admin's override), what you have used this period, whether renders carry a watermark, and the plans on offer. Returns { enabled, plan, reason, subscription, period, limits, limitSource, usage, watermark, canManage, plans }. On a self-hosted Reelsaw this answers that billing is not enabled.

### `billing_checkout_url` (~111 tokens)

Checkout URL

Hosted instances: a Stripe Checkout URL for a plan (or, when the account already pays, the Billing Portal page that confirms the plan change). Returns { url, kind: "checkout" | "change" }; a person must open it in a browser, an agent cannot pay. Plans and intervals come from get_billing.

Input parameters:

- `interval` (string): Billing interval: month (default) or year
- `plan` (string, required): Plan name from get_billing (e.g. creator, pro)

### `list_transcript_flags` (~151 tokens)

List transcript fixes

The fixes Reelsaw made or suggests in a project's transcript: misheard names, fillers and repetitions to cut, and your own cuts and corrections. Returns { total, flags: [{ id, kind, subkind, status, start, end, heard, suggested, replacement, confidence }] }; status pending waits for your decision. Use the ids with review_transcript_flags.

Input parameters:

- `id` (string, required): Project id (from list_projects, search_clips or create_project)
- `limit` (integer): Most fixes to return (1-500, default 100)
- `status` (string): Only fixes in this state; omitted = all (pending = waiting for review, auto = applied automatically)

### `review_transcript_flags` (~150 tokens)

Review transcript fixes

Accept or reject suggested transcript fixes (ids from list_transcript_flags), optionally with your own replacement text for an accepted name fix. The transcript is rebuilt; with rerender true (default) clips whose captions overlap a change are re-rendered. Returns { changed, rerendered, needsRerender, flags: [{ id, status, replacement }], hint }.

Input parameters:

- `decisions` (array, required): One decision per fix
- `id` (string, required): Project id (from list_projects, search_clips or create_project)
- `rerender` (boolean): Re-render the finished clips whose captions overlap a change (default true); false only saves, re-render later with rerender_clips

### `add_transcript_correction` (~166 tokens)

Correct transcript words

Make captions show "text" instead of the words spoken between startSec and endSec (a misheard name, say); the span must cover whole words. Returns { ok, flagId, rerendered, hint }. Undo it by passing the flagId to review_transcript_flags with status rejected.

Input parameters:

- `endSec` (number, required): End of the words to correct, seconds
- `id` (string, required): Project id (from list_projects, search_clips or create_project)
- `rerender` (boolean): Re-render finished clips whose captions overlap the span (default true)
- `startSec` (number, required): Start of the words to correct, seconds from the start of the source video
- `text` (string, required): What the captions should say for that span

### `add_transcript_cut` (~148 tokens)

Cut transcript words

Remove the words spoken between startSec and endSec from the transcript, so captions and the rendered video skip them; the span must cover whole words. Returns { ok, flagId, rerendered, hint }. Undo it by passing the flagId to review_transcript_flags with status rejected.

Input parameters:

- `endSec` (number, required): End of the words to cut, seconds
- `id` (string, required): Project id (from list_projects, search_clips or create_project)
- `rerender` (boolean): Re-render finished clips whose captions overlap the span (default true)
- `startSec` (number, required): Start of the words to cut, seconds from the start of the source video

### `get_project_defaults` (~66 tokens)

Get project defaults

The settings every new project starts with (Settings, Project defaults): language, vocabulary, instructions, focus, clip lengths, pauses, caption look and refine steps. Returns { projectDefaults }; keys you never set are absent and use the built-in value. Change them with set_project_defaults.

### `set_project_defaults` (~100 tokens)

Set project defaults

Replace the settings every new project starts with; existing projects keep theirs. Send the whole set you want: keys left out are cleared, {} clears all (read get_project_defaults first). Intro and outro change through set_default_bumper. Returns { ok, projectDefaults, hint }.

Input parameters:

- `defaults` (object, required): The complete set of defaults, e.g. { language: "en", maxClips: 5, pauses: "tight" }

### `set_project_source_text` (~158 tokens)

Set source text handling

Hide or keep text burned into the source video (captions, logos) for the whole project: hide covers it, keep leaves it, always shows it. Re-renders every finished clip so the set agrees. Returns { changed, needsRerender, rerendered, skipped, hint }; rerendered is how many clips were queued, so follow with wait_for_project; skipped clips were still preparing or rendering and need rerender_clips once done (needsRerender is true).

Input parameters:

- `id` (string, required): Project id (from list_projects, search_clips or create_project)
- `sourceText` (string, required): hide = cover on-screen text in the source, keep = leave it, always = always show it

### `update_project_style` (~197 tokens)

Update project style

Change the caption/hook look for the whole project (font, colors, size, position, words per line). Clears per-clip style overrides unless keepOverrides, and re-renders every finished clip unless rerender is false. Returns { changed, needsRerender, rerendered, skipped, hint }; rerendered is how many clips were queued, skipped how many were still preparing or rendering (they need rerender_clips once done). For one clip use update_clip { style }.

Input parameters:

- `id` (string, required): Project id (from list_projects, search_clips or create_project)
- `keepOverrides` (boolean): Keep clips' own style overrides instead of clearing them (default false)
- `rerender` (boolean): Re-render finished clips now (default true); false saves the style and the next render shows it
- `style` (object, required): The style fields to change; fields left out keep their value

### `list_decisions` (~184 tokens)

List decisions

Your decision log: what you accepted, rejected, cut, corrected, edited, deleted or restored, newest first, kept after a project is deleted so Reelsaw learns from it. Returns { summary: { enabled, total, projects, byKind }, items: [{ id, kind, explicit, projectId, clipId, before, after, context, models, source, createdAt }], nextCursor, total }. kinds are fix.* (transcript fixes) and clip.*; clip.replaced (explicit false) is a pick a re-analyze replaced.

Input parameters:

- `cursor` (integer): nextCursor from the previous page
- `kind` (string): Only this kind of decision
- `limit` (integer): Page size (1-200, default 50)
- `projectId` (string): Only decisions about this project (it may already be deleted)

### `export_decisions` (~151 tokens)

Export decisions

A page of your decision log in export form, oldest first, for evals. Returns { items (one decision each, the same objects `reelsaw decisions export` writes as JSON lines), cases: { disfluency: [eval cases in eval/fixtures/disfluency/cases.json shape, split dev/heldout by project] }, nextCursor }. Pass nextCursor back until it is null.

Input parameters:

- `cursor` (integer): nextCursor from the previous page
- `kind` (string): Only this kind of decision
- `limit` (integer): Page size (1-1000, default 200)
- `since` (string): Only decisions made at or after this ISO time

### `forget_decisions` (~73 tokens)

Forget decisions

Delete entries from your decision log so Reelsaw stops learning from them: one project's (projectId, live or already deleted forever) or, without projectId, all of them. Cannot be undone. Returns { forgotten, message }.

Input parameters:

- `projectId` (string): Only this project's decisions; omitted = everything

### `set_learning` (~70 tokens)

Turn learning on or off

Whether Reelsaw records your decisions (accepts, rejects, cuts, clip edits) to learn from (Settings → Learning, on by default). Off stops recording; what is already kept stays until forget_decisions. Returns { enabled }.

Input parameters:

- `enabled` (boolean, required): true records decisions, false stops

### `billing_portal_url` (~60 tokens)

Billing portal URL

Hosted instances: a Stripe Billing Portal URL to see invoices, change the card or cancel at period end. Returns { url }; a person must open it in a browser. On a self-hosted Reelsaw this answers that billing is not enabled.

## Diagnostics

Captured diagnostic sections: Provenance. The full working is on the page: https://verifymcp.io/servers/osbytes-reelsaw/ghcr-io-osbytes-reelsaw#diagnostics

## Score history

- 2026-11-05: 52
- 2026-11-04: 51
- 2026-11-03: 51
- 2026-11-02: 51
- 2026-11-01: 50
- 2026-10-31: 50

## Common questions

### What is the Reelsaw MCP server?

Reelsaw is an MCP server listed in the public MCP registry as io.github.osbytes/reelsaw. Drive a self-hosted Reelsaw: turn long videos into vertical clips, edit, re-render and export them. This page covers its container image (ghcr.io/osbytes/reelsaw).

### Is the Reelsaw MCP server safe to use?

Reelsaw scores 52 out of 100 on VerifyMCP. Its build provenance is signed and verified. That is a record of what we were able to check automatically, not an endorsement. The category breakdown on this page shows every signal behind the number, including the ones we could not confirm.

### What tools does the Reelsaw MCP server expose?

Reelsaw exposes 84 tools: list_projects, get_project, create_project, update_project, wait_for_project, and 79 more. Their descriptions and schemas cost roughly 14,095 tokens of context every time the server is loaded.

### Is the Reelsaw MCP server still maintained?

Reelsaw is still listed as active in the MCP registry. We last reached this channel on 5 November 2026. Those dates come from our own scans of the registry and the channel itself, not from anything the publisher announced.

### What licence is the Reelsaw MCP server under?

Reelsaw declares the AGPL-3.0 licence, which is OSI-approved. That covers the source only, and says nothing about the cost of any service it calls.

## Links

- GitHub Container Registry: https://ghcr.io/osbytes/reelsaw
- Repository: https://github.com/osbytes/reelsaw
- Website: https://reelsaw.com/
- Changelog RSS feed: https://verifymcp.io/servers/osbytes-reelsaw/ghcr-io-osbytes-reelsaw.xml
- Changelog JSON feed: https://verifymcp.io/servers/osbytes-reelsaw/ghcr-io-osbytes-reelsaw.json
- HTML version of this page: https://verifymcp.io/servers/osbytes-reelsaw/ghcr-io-osbytes-reelsaw
