@clipwright/mcp-server 0.35.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -12,137 +12,72 @@ author: Clipwright
12
12
  homepage: https://clipwright.io/?ref=clawhub
13
13
  ---
14
14
 
15
- # SKILL.md — installing and operating `clipwright-mcp`
15
+ # SKILL.md — install `clipwright-mcp`, then make a video
16
16
 
17
- A self-contained guide for an AGENT (or a human in Claude Code) that needs no
18
- spoken help from a developer. Follow the steps in order. Not to be confused with
19
- `apps/web/.claude/skills/*` — those are dashboard skills for the Next.js app and
20
- have nothing to do with the MCP server.
17
+ This file is for an agent. It holds what the tool schemas cannot tell you: the
18
+ places where a call answers without an error and the answer is still wrong for
19
+ the user. Each such trap carries the date it was last checked.
21
20
 
22
- The package `@clipwright/mcp-server` (`bin: clipwright-mcp`) is published to npm
23
- under the MIT license, so you do NOT need the repository to install it — `npx` is
24
- enough. Building from source is kept below as the development path.
21
+ Everything else is reference, kept in one place so it does not go stale here:
25
22
 
26
- What changed in each version — above all what was removed or changed
27
- incompatibly — is in `CHANGELOG.md`, shipped next to this file. Read its
28
- **Breaking:** entries before you move to a newer version.
23
+ - the tool schemas (`tools/list`): every field, its limits and defaults. A field
24
+ the service accepts but does not honor says `NOT HONORED YET` in its own
25
+ description;
26
+ - https://clipwright.io/docs: the REST API, refusal codes and prices;
27
+ - https://api.clipwright.io/openapi.json: the REST contract as OpenAPI 3.1;
28
+ - https://clipwright.io/changelog: read its **Breaking:** entries before you
29
+ move to a newer version.
29
30
 
30
- Four client-side packages — `@clipwright/core`, `@clipwright/sdk`,
31
- `@clipwright/mcp-server` and `@clipwright/cli` — are MIT; the service itself
32
- (REST API, render pipeline, dashboard) is closed. That split is deliberate, not
33
- an oversight: see `LICENSE` in the repository root.
31
+ The client packages `@clipwright/core`, `@clipwright/sdk`,
32
+ `@clipwright/mcp-server` and `@clipwright/cli` are MIT; the service is closed.
34
33
 
35
- ## 1. Installation
34
+ # Part 1 — Install
36
35
 
37
- There are two ways to connect, and both serve the same tools:
36
+ ## 1. Connect
38
37
 
39
- - **the hosted server** at `https://mcp.clipwright.io/mcp` (Streamable HTTP),
40
- signed in with OAuth or with a `cw_*` key — section 1.4. Nothing to install;
41
- - **this package run locally** over stdio with `CLIPWRIGHT_API_KEY` — sections
42
- 1.1 to 1.3, and section 2.
38
+ The hosted server and this package run locally serve the same tools.
43
39
 
44
- ### 1.1 From npm — the normal path
40
+ ### 1.1 The hosted server — nothing to install
45
41
 
46
- ```bash
47
- npx -y -p @clipwright/mcp-server@latest clipwright-mcp
48
- ```
49
-
50
- Expected: the process does NOT exit, prints `clipwright mcp server running on
51
- stdio` to stderr, and hangs waiting for JSON-RPC. That is what a healthy server
52
- looks like, not a freeze. Stop it with `Ctrl+C`.
42
+ Add `https://mcp.clipwright.io/mcp` (Streamable HTTP) to the client. On first
43
+ use the client opens a browser; the account holder signs in and approves the app.
53
44
 
54
- Exiting immediately with `CLIPWRIGHT_API_KEY env var is required` is also
55
- expected when the key is empty (`src/index.ts`) — it is not a broken build.
56
-
57
- Go straight to section 1.3: the host config for this variant differs only in the
58
- command (`npx` instead of `node` with an absolute path); both are shown there.
59
-
60
- ### 1.2 From source — development only
45
+ | Client | How to add | Checked |
46
+ |---|---|---|
47
+ | Claude Code | `claude mcp add --transport http clipwright https://mcp.clipwright.io/mcp`, then `/mcp` in a session to sign in | verified live 2026-10-18 |
48
+ | Claude.ai, Claude Desktop | Customize → Connectors → Add custom connector, paste the URL | from the client's docs |
49
+ | ChatGPT | the URL as the remote MCP server of a plugin | from the client's docs |
50
+ | Cursor | `~/.cursor/mcp.json`: `{"mcpServers": {"clipwright": {"url": "https://mcp.clipwright.io/mcp"}}}` | from the client's docs |
51
+ | VS Code | `.mcp.json`: `{"mcpServers": {"clipwright": {"type": "http", "url": "https://mcp.clipwright.io/mcp"}}}` | from the client's docs |
52
+ | Gemini CLI | `gemini mcp add --transport http clipwright https://mcp.clipwright.io/mcp`, then `/mcp auth clipwright` | from the client's docs |
53
+ | Codex | `codex mcp add clipwright --url https://mcp.clipwright.io/mcp`, then `codex mcp login clipwright` | from the client's docs |
61
54
 
62
- Requirements: Node ≥24, pnpm (`packageManager: pnpm@11.13.1` in the root
63
- `package.json` — with corepack available: `corepack enable`).
55
+ For CI and scripts, a `cw_*` key replaces OAuth and holds both permissions:
64
56
 
65
57
  ```bash
66
- git clone <repo-url> clipwright # or use a checkout you already have
67
- cd clipwright
68
- pnpm install
69
- pnpm build # pnpm -r build — builds every package in topological order
58
+ claude mcp add --transport http clipwright https://mcp.clipwright.io/mcp --header "Authorization: Bearer cw_..."
70
59
  ```
71
60
 
72
- If you only need this surface (faster, but `@clipwright/core` and
73
- `@clipwright/sdk` must be built first — `...` pulls them in automatically):
74
-
75
- ```bash
76
- pnpm --filter @clipwright/mcp-server... build
77
- ```
61
+ What differs on the hosted server is in section 1.4.
78
62
 
79
- After the build, `packages/mcp-server/dist/index.js` must exist. Check it:
63
+ ### 1.2 The local server from npm
80
64
 
81
65
  ```bash
82
- ls packages/mcp-server/dist/index.js
66
+ npx -y -p @clipwright/mcp-server@latest clipwright-mcp
83
67
  ```
84
68
 
85
- #### Pre-flight check of the built binary
69
+ A healthy server does not exit: it prints `clipwright mcp server running on
70
+ stdio` to stderr and waits for JSON-RPC. Exiting at once with
71
+ `CLIPWRIGHT_API_KEY env var is required` means the key did not reach it, not a
72
+ broken build. Both answers verified 2026-10-31. `@latest` is deliberate: it
73
+ keeps the client in step with the API.
86
74
 
87
- This applies to source builds. Before wiring the server into a host, make sure it
88
- starts at all and does not die on imports:
89
-
90
- ```bash
91
- CLIPWRIGHT_API_KEY=cw_test node packages/mcp-server/dist/index.js
92
- ```
93
-
94
- Expected: the process does NOT exit on its own, prints `clipwright mcp server
95
- running on stdio` to stderr, and waits on stdio for JSON-RPC. Stop it with
96
- `Ctrl+C`. If it instead exits immediately with `CLIPWRIGHT_API_KEY env var is
97
- required`, the variable was not passed through (expected behaviour on an empty
98
- key, see `src/index.ts`) — not a bug. If it dies on a module import, the build is
99
- incomplete: go back to step 1.2 and run `pnpm build` from the root rather than a
100
- targeted `--filter`, so `@clipwright/core` and `@clipwright/sdk` are definitely
101
- rebuilt.
75
+ From a source checkout: `pnpm install && pnpm build` at the root, then run
76
+ `node packages/mcp-server/dist/index.js` with the same environment.
102
77
 
103
78
  ### 1.3 Connecting it to an MCP host
104
79
 
105
- Variants A and B below run the server FROM SOURCE and therefore need the ABSOLUTE
106
- path to `dist/index.js`:
107
-
108
- ```bash
109
- realpath packages/mcp-server/dist/index.js
110
- ```
111
-
112
- If you installed from npm (section 1.1), you do not need a path — take variant C
113
- at the end of this section; it differs only in `command`/`args`.
114
-
115
- **Variant A — `.mcp.json` (project-level host config):**
116
-
117
- ```json
118
- {
119
- "mcpServers": {
120
- "clipwright": {
121
- "type": "stdio",
122
- "command": "node",
123
- "args": ["/absolute/path/to/clipwright/packages/mcp-server/dist/index.js"],
124
- "env": {
125
- "CLIPWRIGHT_API_KEY": "cw_...",
126
- "CLIPWRIGHT_API_URL": "https://api.clipwright.io",
127
- "CLIPWRIGHT_CLIENT_ID": "<see section 2 — generate BEFORE the first run>"
128
- }
129
- }
130
- }
131
- }
132
- ```
133
-
134
- **Variant B — `claude mcp add`** (variant A as a single command):
135
-
136
- ```bash
137
- claude mcp add clipwright \
138
- -e CLIPWRIGHT_API_KEY=cw_... \
139
- -e CLIPWRIGHT_API_URL=https://api.clipwright.io \
140
- -e CLIPWRIGHT_CLIENT_ID="$(node -e "console.log(crypto.randomUUID().replaceAll('-',''))")" \
141
- -- node /absolute/path/to/clipwright/packages/mcp-server/dist/index.js
142
- ```
143
-
144
- **Variant C — from npm, without the repository.** Same thing, but the command is
145
- `npx`:
80
+ Generate `CLIPWRIGHT_CLIENT_ID` BEFORE the first run (section 2):
146
81
 
147
82
  ```json
148
83
  {
@@ -153,113 +88,63 @@ claude mcp add clipwright \
153
88
  "args": ["-y", "-p", "@clipwright/mcp-server@latest", "clipwright-mcp"],
154
89
  "env": {
155
90
  "CLIPWRIGHT_API_KEY": "cw_...",
156
- "CLIPWRIGHT_API_URL": "https://api.clipwright.io",
157
- "CLIPWRIGHT_CLIENT_ID": "<see section 2 — generate BEFORE the first run>"
91
+ "CLIPWRIGHT_CLIENT_ID": "<a unique value, section 2>"
158
92
  }
159
93
  }
160
94
  }
161
95
  }
162
96
  ```
163
97
 
164
- As a single command:
98
+ As one command:
165
99
 
166
100
  ```bash
167
101
  claude mcp add clipwright \
168
102
  -e CLIPWRIGHT_API_KEY=cw_... \
169
- -e CLIPWRIGHT_API_URL=https://api.clipwright.io \
170
103
  -e CLIPWRIGHT_CLIENT_ID="$(node -e "console.log(crypto.randomUUID().replaceAll('-',''))")" \
171
104
  -- npx -y -p @clipwright/mcp-server@latest clipwright-mcp
172
105
  ```
173
106
 
174
- **`@latest` here is a deliberate choice, not sloppiness.** It pulls a fresh
175
- version on every start: right for a prototype, because the contract still moves
176
- and a client out of sync with the server costs more than an unexpected upgrade.
177
- Once the contract settles, the version should be pinned.
178
-
179
- Environment variables:
107
+ For a source build, the command is `node` with the absolute path to
108
+ `packages/mcp-server/dist/index.js`.
180
109
 
181
110
  | Variable | Required | Meaning |
182
111
  |---|---|---|
183
- | `CLIPWRIGHT_API_KEY` | YES | Bearer token shaped `cw_*`. Without it the process exits immediately with an error (`src/index.ts`). The token is ISSUED TO YOU PERSONALLY: it lives in the `api_tokens` table, is bound to an account, and is revoked independently of anyone else's. Do not forward it — a second person gets their own. What separates people is the ACCOUNT, not the token: accounts cannot see each other's runs, each has its own daily cap and its own idempotency axis, and several tokens on one account share all of it. |
184
- | `CLIPWRIGHT_API_URL` | no | Production URL. Left unset, the SDK supplies `https://api.clipwright.io` itself. Set it explicitly only when you target another environment. |
185
- | `CLIPWRIGHT_CLIENT_ID` | YES (see section 2) | Unique identifier of this INSTALLATION. |
112
+ | `CLIPWRIGHT_API_KEY` | YES | A `cw_*` key issued on your account at https://app.clipwright.io/api-keys. It is yours: a second person gets an account and a key of their own. Several keys on one account share its runs, balance and idempotency. |
113
+ | `CLIPWRIGHT_CLIENT_ID` | YES | This installation's own identifier, section 2. |
114
+ | `CLIPWRIGHT_API_URL` | no | Defaults to `https://api.clipwright.io`; set it only for another environment. |
186
115
 
187
- After connecting, restart or reopen the MCP host and confirm that `tools/list`
188
- returns 44 tools: `make_ugc`, `get_run`, `get_run_shots`, `quote_revision`, `revise_run`, `quote_candidate`, `create_candidate`,
116
+ After a restart, confirm that `tools/list`
117
+ returns 45 tools: `make_ugc`, `get_run`, `get_run_shots`, `quote_revision`, `revise_run`, `quote_candidate`, `create_candidate`,
189
118
  `list_generation_models`, `quote_image`, `generate_image`, `quote_video`, `generate_video`, `quote_ugc`, `list_actors`,
190
119
  `list_voices`, `list_template_sounds`, `upload_image`, `create_upload`, `get_upload`, `create_asset`, `list_assets`, `get_asset`, `update_asset`,
191
120
  `delete_asset`, `quote_analysis`, `analyze_reference`, `get_reference_analysis`, `get_account`, `list_prices`, `create_actor`, `quote_actor`, `delete_actor`, `get_actor_defaults`,
192
121
  `set_actor_defaults`, `quote_faceless`, `make_faceless`, `list_workflows`, `get_workflow`, `prepare_workflow_plan`,
193
- `plan_reference_remake`, `get_workflow_plan`, `approve_workflow_plan`, `quote_workflow_plan`, `run_workflow_plan`.
194
-
195
- Workflow calls are explicit steps: list/read a method, prepare its complete
196
- document, read all readiness issues and warnings, approve the chosen revision,
197
- then quote and show its native price before a paid launch. Preparation, approval
198
- and quoting start no run. To save, pass both `plan_id` and `base_version` with
199
- the document; omit both to create. Unknown write fields are refused.
200
- `run_workflow_plan` requires the quote's revision, content digest, semantic input
201
- digest and quote id, plus an explicit idempotency key. It returns the captured
202
- receipt and native run id at once. Poll `get_run`; its optional workflow field
203
- keeps the purchased revision and available outcome evidence. Technical evidence
204
- does not mean a person accepted the creative result. After an uncertain paid
205
- answer, preserve the original request and key; a new key can pay twice.
206
-
207
- ### 1.4 The hosted server — OAuth or a `cw_*` key
208
-
209
- The hosted server runs the same tools at `https://mcp.clipwright.io/mcp`. It
210
- keeps no session: every message is its own `POST`. Add the URL to the client;
211
- on first use the client opens a browser, the account holder signs in to
212
- Clipwright and approves the app.
213
-
214
- **Status: Claude Code and the device-code flow were verified live on
215
- 2026-10-18;** the other commands come from each client's own documentation:
216
-
217
- | Client | How to add | Verified |
218
- |---|---|---|
219
- | Claude Code | `claude mcp add --transport http clipwright https://mcp.clipwright.io/mcp`, then `/mcp` in a session to sign in | verified live 2026-10-18 |
220
- | Claude.ai, Claude Desktop | Customize → Connectors → Add custom connector, paste the URL | not yet verified live |
221
- | ChatGPT | the URL as the remote MCP server of a plugin (OpenAI's guide "Connect and test your plugin") | not yet verified live |
222
- | Cursor | `~/.cursor/mcp.json`: `{"mcpServers": {"clipwright": {"url": "https://mcp.clipwright.io/mcp"}}}` | not yet verified live |
223
- | VS Code | `.mcp.json` at the workspace root: `{"mcpServers": {"clipwright": {"type": "http", "url": "https://mcp.clipwright.io/mcp"}}}` | not yet verified live |
224
- | Gemini CLI | `gemini mcp add --transport http clipwright https://mcp.clipwright.io/mcp`, then `/mcp auth clipwright` | not yet verified live |
225
- | Codex | `codex mcp add clipwright --url https://mcp.clipwright.io/mcp`, then `codex mcp login clipwright` | not yet verified live |
226
-
227
- **Permissions.** The approval page lists what the app asks for:
228
-
229
- - `mcp:read` — the tools with `readOnlyHint: true`: account, runs, shots,
230
- voices, actors, workflows and every quote. Free.
231
- - `mcp:write` — every other tool: `make_ugc`, `make_faceless`, `revise_run`,
232
- `create_candidate`, `generate_image`, `generate_video`, `create_actor` and
233
- `run_workflow_plan` **spend credits**;
234
- `upload_image`, `create_upload`, `create_asset`, `update_asset`, `delete_asset`, `delete_actor`,
235
- `set_actor_defaults`, `prepare_workflow_plan` and `approve_workflow_plan` change the account.
236
-
237
- The account holder approves or declines the whole request; only an admitted
238
- account can approve. A connection without `mcp:write` that calls one of those
239
- tools gets HTTP `403` with `error: "insufficient_scope"` and
240
- `WWW-Authenticate: Bearer error="insufficient_scope", scope="mcp:write", resource_metadata="…"`.
241
- Nothing is charged and no run is created. Reconnect the app and approve
242
- spending to fix it.
243
-
244
- **Revoking.** The account holder revokes an app at
245
- `https://app.clipwright.io/api-keys`, under **Connected apps**. Its next request
246
- gets `401`. A connection lasts 90 days from approval and is not extended by
247
- use: after that the app must sign in again. An access token lasts one hour;
248
- clients renew it themselves.
249
-
250
- **Files.** The hosted server cannot read files on the agent's machine, so
251
- `upload_image` refuses a `path` there. Pass a public https URL as
252
- `upload_image({url})` and the server downloads it, or call `create_upload`,
253
- PUT the file to its `put_url` with curl and confirm with `get_upload`. Either
254
- way the `upload_id` works in `first_frame`, `last_frame` and `references`.
255
-
256
- **Idempotency.** Section 2 does not apply here. Each connected app (one
257
- grant) is its own client identity, and so is each `cw_*` key, so two
258
- connections of one account never collapse into one run. A repeated request
259
- within one connection returns the same run, as on stdio.
122
+ `plan_reference_remake`, `get_workflow_plan`, `approve_workflow_plan`, `quote_workflow_plan`, `run_workflow_plan`, `send_feedback`.
123
+
124
+ When no tool does the job, or an answer is confusing or wrong, file it with
125
+ `send_feedback` (free) instead of rephrasing the request and giving up.
126
+
127
+ ### 1.4 What differs on the hosted server
128
+
129
+ - **Permissions.** `mcp:read` covers the tools marked `readOnlyHint: true`,
130
+ every quote included; `mcp:write` covers every other tool. A connection
131
+ without `mcp:write` that calls one gets HTTP `403` with
132
+ `error: "insufficient_scope"`; nothing is charged. Reconnect and approve spending.
133
+ - **Files.** The hosted server cannot read the agent's disk, so `upload_image`
134
+ refuses a `path` there. Pass a public https `url`, or call `create_upload`,
135
+ PUT the file to its `put_url` and confirm with `get_upload`.
136
+ - **Idempotency.** Each connected app and each `cw_*` key is its own client
137
+ identity, so section 2 does not apply. A repeated request within one
138
+ connection returns the same run.
139
+ - **Sign-in.** A connection lasts 90 days from approval and use does not extend
140
+ it; access tokens last an hour and clients renew them. The account holder
141
+ revokes an app at https://app.clipwright.io/api-keys, under Connected apps.
142
+ - **Endpoint errors.** No token, or a revoked or unknown one: `401` with a
143
+ `WWW-Authenticate` header the client follows to sign in. `GET /mcp`: `405`. A
144
+ body over 1 MB: `413`.
260
145
 
261
146
  **An agent with no browser** signs in with a device code (verified live
262
- 2026-10-18). Only the first-party client `clipwright-agent` may use this flow:
147
+ 2026-10-18). Only the client id `clipwright-agent` may use this flow:
263
148
 
264
149
  ```bash
265
150
  curl -s https://mcp.clipwright.io/device/auth -d client_id=clipwright-agent \
@@ -267,238 +152,109 @@ curl -s https://mcp.clipwright.io/device/auth -d client_id=clipwright-agent \
267
152
  --data-urlencode "resource=https://mcp.clipwright.io/mcp"
268
153
  ```
269
154
 
270
- 1. Show the person `user_code` and `verification_uri`
271
- (`https://mcp.clipwright.io/device`). They open the page on any device,
272
- enter the code, sign in and approve. The approval page repeats the code:
273
- they approve only if it matches.
274
- 2. Meanwhile poll `POST https://mcp.clipwright.io/token` with
275
- `grant_type=urn:ietf:params:oauth:grant-type:device_code`, `device_code` and
276
- `client_id=clipwright-agent`, no more often than every 5 seconds. Until
277
- approval the answer is `authorization_pending`; a faster poll gets
278
- `slow_down`.
279
- 3. Send `access_token` as `Authorization: Bearer` to `/mcp`. Renew it with
280
- `grant_type=refresh_token`, the refresh token and the same `client_id`. Each
281
- renewal returns a new refresh token: keep the newest one.
282
-
283
- **With a `cw_*` key instead of OAuth** — for CI and scripts. A key holds both
284
- permissions:
285
-
286
- ```bash
287
- claude mcp add --transport http clipwright https://mcp.clipwright.io/mcp --header "Authorization: Bearer cw_..."
288
- ```
289
-
290
- **Errors of the endpoint itself:** no token, or a revoked or unknown key —
291
- `401` with
292
- `WWW-Authenticate: Bearer resource_metadata="https://mcp.clipwright.io/.well-known/oauth-protected-resource/mcp", scope="mcp:read mcp:write"`,
293
- which a client follows to sign in; an expired or revoked OAuth token — the same
294
- `401` with `error="invalid_token"` added; `GET /mcp` with a valid token — `405`;
295
- a body over 1 MB — `413`.
155
+ Show the person `user_code` and `verification_uri`; they approve on any device
156
+ if the code matches. Meanwhile poll `POST https://mcp.clipwright.io/token` with
157
+ `grant_type=urn:ietf:params:oauth:grant-type:device_code`, `device_code` and
158
+ `client_id=clipwright-agent`, no more often than every 5 seconds
159
+ (`authorization_pending` until approval, `slow_down` when too fast). Send
160
+ `access_token` as `Authorization: Bearer`. Renew with
161
+ `grant_type=refresh_token`; each renewal returns a new refresh token, keep the newest.
296
162
 
297
163
  ## 2. `CLIPWRIGHT_CLIENT_ID` — required, and WHY
298
164
 
299
- This section is about the local server. The hosted server takes the client
300
- identity from the connection itself (section 1.4).
165
+ This section is about the local server; the hosted server takes the identity
166
+ from the connection (section 1.4).
301
167
 
302
- **Rule: EVERY installation must get its OWN unique `CLIPWRIGHT_CLIENT_ID` BEFORE
303
- the first `make_ugc` call.** Do not copy the value from another machine, and do
304
- not leave it empty hoping the default will "work out somehow".
168
+ **Rule: EVERY installation gets its OWN unique `CLIPWRIGHT_CLIENT_ID` BEFORE the
169
+ first paid call.** Do not copy it from another machine and do not leave it to chance.
305
170
 
306
171
  ### Why this is not optional
307
172
 
308
- The idempotency key the SDK stamps on every paid `POST /v1/skills/make_ugc/run`
309
- is derived deterministically from **the hash of the input** (`script`, `person`,
310
- `image`, … after canonicalisation) **and the client identity**
311
- (`idempotencyKeyFor` in `packages/core/src/idempotency.ts`, used by
312
- `packages/sdk/src/index.ts`). The server deduplicates on that key: if a stored
313
- key matches on `request_hash`, it returns 200 with the run **created by the first
314
- request that used the key**, instead of starting a new paid render.
315
-
316
- That is the right behaviour when the SAME user retries — you should not pay twice
317
- for an agent's accidental double click. Its boundaries are these.
318
-
319
- Collapsing requires BOTH to match: the account and the `clientId`. Separating
320
- either one is enough.
321
-
322
- - **The account separates by construction.** Both the unique index
323
- (`runs_account_idempotency_key_unique`) and the lookup use the pair
324
- `(account_id, idempotency_key)`. A run from ANOTHER account can never reach you,
325
- and nobody can take your key — whatever sits in `CLIPWRIGHT_CLIENT_ID`.
326
- **The axis is the account, not the token.** A separate token does not by itself
327
- mean a separate account: several live tokens on one account are normal (laptop,
328
- CI, smoke script), and they all share one idempotency axis. The account is not
329
- visible from the token — only the service side knows it, so "is this my own
330
- account?" is confirmed by whoever issued the token.
331
- - **`clientId` separates within one account.** On its own it comes from
332
- `~/.clipwright/client-id` and is born random on first run, so two genuinely
333
- separate installations diverge even without the environment variable. It can
334
- only match if it was MADE to match: a shared `HOME`, a value copied from
335
- someone else's machine, an image cloned together with the file.
336
-
337
- The dangerous case is exactly the crossing of those two conditions: **one account
338
- AND one `clientId`**. Then a request whose input matches an earlier one — for
339
- example, both people copied a sample from this SKILL.md verbatim — returns 200, a
340
- `run_id` and a playable link. But it is the video rendered for the first person,
341
- presented to the second as their own success. A diagnostic tool would read as
342
- "works" while no paid render ever ran for the second person.
343
-
344
- Hence the rule: **a second person gets their own ACCOUNT and a token issued on
345
- it**, and the value of `CLIPWRIGHT_CLIENT_ID` is not copied from anyone's machine.
346
- Setting it explicitly is mandatory wherever the file resolver cannot give a stable
347
- value of its own: a read-only or shared `HOME`, a container from a cloned image,
348
- CI.
173
+ The idempotency key the SDK stamps on every paid call is derived from **the hash
174
+ of the input** and **the client identity**. The server deduplicates on that key:
175
+ a repeated key returns 200 with the run **created by the first request**, and no
176
+ new paid render starts. That is right when the same user retries.
177
+
178
+ Collapsing requires BOTH to match: the account and the `clientId`.
179
+
180
+ - **The account separates by construction.** The server matches a key only
181
+ within your account (its `account_id`). A separate token does not by itself
182
+ mean a separate account: several live tokens on one account share one
183
+ idempotency axis, and only the service knows which account a token is on.
184
+ - **`clientId` separates within one account.** Without the variable it comes
185
+ from `~/.clipwright/client-id`, born random on first run. It matches only when
186
+ made to match: a shared `HOME`, a copied value, an image cloned with the file.
187
+
188
+ With one account AND one `clientId`, a request whose input matches an earlier
189
+ one (both people copied the same sample) returns 200, a `run_id` and a playable
190
+ link to the video rendered for the FIRST person. Nothing errors, and no render
191
+ ran for the second.
349
192
 
350
193
  ### How to set it
351
194
 
352
- An explicit `CLIPWRIGHT_CLIENT_ID` in the host environment (section 1.3, variants
353
- A/B/C) is the most reliable path, because it does not depend on the state of the
354
- file system. The value is any unique string; for consistency with the SDK's own
355
- generator use the same shape (a UUID without dashes). Generate it with Node, not
356
- `uuidgen`: Node is certainly present here, whereas `uuidgen` may be missing on a
357
- bare Linux image — and then the shell silently substitutes an EMPTY string, which
358
- switches off precisely the protection the variable exists for.
195
+ Set it explicitly in the host environment (section 1.3). Generate it with Node,
196
+ not `uuidgen`: on a bare image without `uuidgen` the shell silently substitutes
197
+ an empty string and switches the protection off.
359
198
 
360
199
  ```bash
361
200
  node -e "console.log(crypto.randomUUID().replaceAll('-',''))"
362
201
  ```
363
202
 
364
- If the variable is NOT set, `resolveClientId()` (`packages/sdk/src/client-id.ts`)
365
- falls back to the file `~/.clipwright/client-id`: on first run it generates a
366
- random id and stores it there, then reads it back. That is a workable fallback for
367
- a single installation on a clean machine, but do NOT lean on it as your only
368
- protection when an agent does the install: home directories carried along when
369
- images, containers or dev environments are cloned can take
370
- `~/.clipwright/client-id` with them and produce a collision this fallback will not
371
- notice. When installing by this guide, **always set `CLIPWRIGHT_CLIENT_ID`
372
- explicitly in the host environment** rather than relying on auto-generation.
373
-
374
- If neither the variable nor writing the file is possible (read-only `$HOME`),
375
- `resolveClientId()` deliberately THROWS instead of inventing a throwaway id on
376
- every start — a silent fallback would disable deduplication entirely
377
- (`packages/sdk/src/client-id.ts`).
378
-
379
- **The server does NOT die in that case, and this is verified by a test run.** The
380
- identity resolves lazily — only on the first `make_ugc`. In an environment where
381
- `~/.clipwright` is not writable the server starts, `tools/list` returns every
382
- tool, and `quote_ugc`, `get_run` and `list_voices` work: they do not need the
383
- identity. Only `make_ugc` refuses, and its refusal names the variable that fixes
384
- it. So the action to take is to set `CLIPWRIGHT_CLIENT_ID` explicitly — not to
385
- hunt for a bug and not to reinstall the package.
386
-
387
- ## 3. CALL ORDER — `quote_ugc` first, `make_ugc` second
388
-
389
- This is not a politeness convention but a mechanism, without which the contract
390
- stays silent about its own decisions.
203
+ Without the variable and with a read-only `HOME`, the server still starts:
204
+ `tools/list`, `quote_ugc`, `get_run` and `list_voices` work; paid calls, uploads
205
+ and new assets refuse, naming the variable. Set it; do not reinstall.
206
+
207
+ # Part 2 — Make a video
208
+
209
+ ## 3. CALL ORDER — quote first, paid call second
391
210
 
392
211
  ```
393
212
  quote_ugc → read source / resolved_aspect_ratio / warnings → make_ugc → get_run (polling)
394
213
  ```
395
214
 
396
- **What `quote_ugc` gives you that nothing else does.** It is free, reserves
397
- nothing, and answers the questions that would otherwise surface only after money
398
- is spent:
399
-
400
- | Response field | What it tells you |
401
- |---|---|
402
- | `credits_estimate` | the price — show it to the user BEFORE generating |
403
- | `source` | dimensions of the selected actor variant, a probed `image`, or the default actor (432×768) |
404
- | `actor` | selected `actor_id`, `version` and `gender`; default `{gender}` when no face is selected; `null` for caller-supplied `image` |
405
- | `resolved_aspect_ratio` | the aspect ratio that will ACTUALLY be rendered for this input |
406
- | `tts_model` | the speech model this run will use; the script's character ceiling depends on it |
407
- | `warnings[]` | which of the parameters you passed will not be honoured |
408
- | `contract_version` | which contract version this price was quoted under |
409
-
410
- **The verdict of `quote_ugc` and the verdict of `make_ugc` agree by
411
- construction** — both surfaces call the same function. If quote returns 400, the
412
- run returns the same 400; if quote showed `resolved_aspect_ratio: "9:16"` for a
413
- requested `1:1`, then 9:16 is what gets rendered.
414
-
415
- **Unknown fields are refused, not ignored.** A key the contract does not have —
416
- `aspectRatio`, `aspect`, `ttsModel` — gets 400 `unknown_field` before any charge.
417
- `fields` names each key, and the message suggests the matching field
418
- (`aspect_ratio`). Until clipwright#166 such keys were dropped silently, so a
419
- misspelled `aspect_ratio` rendered the default 9:16.
420
-
421
- **Rejected fields — do not try to pass them.** They are not declared in the tool
422
- schema, and the server answers 400 `rejected_field` before a run is even created:
423
-
424
- | Field | Use instead |
425
- |---|---|
426
- | `character` | `actor_id` from `list_actors`, an `image`, or nothing (default actor); `person` does not select a face |
427
- | `webhook_url` | poll `get_run` — webhooks are not delivered at all |
428
- | `segments`, `broll_url` | a single `script`; segmented clips are not available yet |
429
- | `disclosure_overlay` | nothing: no video carries a visible label, and the AI disclosure is in `ai_disclosure` of the finished run |
430
- | `image` and `actor_gender` with the killswitch off | omit both — the render runs on the default actor |
431
-
432
- **The aspect ratio can be refused.** Requesting `aspect_ratio: "1:1"` without an
433
- `image` or `actor_id` returns a refusal: the default actor is vertical (432×768), and a square
434
- frame could only come from the vendor's centre crop, which we do not do silently.
435
- To get `1:1`, select an actor with a verified square variant or pass an `image`
436
- with a square public https source. An unavailable actor format is refused before payment.
437
-
438
- ## 4. The tools
439
-
440
- Schemas come from the single source of truth `@clipwright/core`
441
- (`packages/core/src/skills.ts`, barrel `packages/core/src/index.ts`); MCP does not
442
- retype them by hand.
215
+ Every paid tool has a free quote that reserves nothing. Show the user the
216
+ credits and start the paid call only after they agree. Take prices from the
217
+ quote or `list_prices`, never from memory or this page: they change.
218
+
219
+ | What | Free quote | Paid call | Poll with | The result |
220
+ |---|---|---|---|---|
221
+ | UGC video with a presenter | `quote_ugc` | `make_ugc` | `get_run` | `video_url` |
222
+ | Faceless video | `quote_faceless` | `make_faceless` | `get_run` | `video_url` |
223
+ | One image / one silent clip | `quote_image` / `quote_video` | `generate_image` / `generate_video` | `get_run` | `generation.url` |
224
+ | A personal actor | `quote_actor` | `create_actor` | `get_run` | `created_actor.actor_id` |
225
+ | A reading of a reference reel | `quote_analysis` | `analyze_reference` | `get_reference_analysis` | `reference_profile` or `data` |
226
+ | A revision of a faceless video | `quote_revision` | `revise_run` | `get_run` | `video_url` |
227
+ | A new picture or clip for one shot | `quote_candidate` | `create_candidate` | `get_run` | `candidate.preview_url` |
228
+ | A plan from a workflow method | `quote_workflow_plan` | `run_workflow_plan` | `get_run` | `workflow` |
229
+
230
+ - The paid call returns a `run_id` at once and does not wait. Poll every 5–10
231
+ seconds until `state` is `succeeded` or `failed`. Do not tell the user the
232
+ video is ready before you hold its link.
233
+ - Read `warnings[]` on the first answer, on each poll and on the terminal one.
234
+ What the service could not honor is announced there, not refused (section 4).
235
+ - A key the contract does not have (`aspectRatio`) is refused with 400
236
+ `unknown_field`, and the message names the right spelling. The removed fields
237
+ `character`, `webhook_url`, `broll_url` and `disclosure_overlay` are refused by
238
+ name with the replacement: `actor_id`, polling `get_run`, nothing, and the
239
+ `ai_disclosure` of the finished run. Nothing is charged.
240
+ - Before choosing a model for an image or a clip, call `list_generation_models`;
241
+ any other combination is `unsupported_combination` before any charge.
242
+ - A workflow launch needs the quote's revision, content digest, semantic input
243
+ digest and quote id, plus an explicit idempotency key, and the quote expires
244
+ five minutes after it is issued. Checked 2026-10-31.
443
245
 
444
246
  ### `quote_ugc` — free
445
247
 
446
- The input is `offeredUgcInputShape` (`packages/mcp-server/src/server.ts`), that
447
- is, the contract MINUS the rejected fields: they simply are not there, and listing
448
- them here would mean offering what the server punishes you for (section 3).
449
-
450
- `script` (required while `segments` is rejected; length is limited by the resolved
451
- TTS model, as reported by `tools/list`: `eleven_v4` and `gemini-3.8-flash-tts`
452
- allow 5000 characters, `eleven_flash_v2_5` and `eleven_turbo_v2_5` allow 10000. Spaces, audio tags and stress
453
- marks count; emoji may count as two characters. There is no word-count limit or fixed
454
- duration implied by it), `tts_model` (omit it and the voice decides: a voice of kind
455
- `model_voice` speaks its own model `gemini-3.8-flash-tts`, every preset, catalog voice and
456
- raw `voice_id` speaks `eleven_v4`; an explicit `tts_model` wins, except `eleven_v3`,
457
- which is no longer spoken: a run naming it speaks `eleven_v4` and says so in `warnings[]`;
458
- `eleven_flash_v2_5` and `eleven_turbo_v2_5` cost less and suit
459
- languages other than Russian), optionally `person` OR
460
- `image` (a public `https` URL) with `actor_gender` (`female`/`male`, only next to
461
- `image`), plus `name`, `captions` (defaults to `false` —
462
- **captions are opt-in, ASK THE USER FIRST**), `caption_style` and
463
- `caption_emphasis` (see *Captions* under `make_ugc`), `look` (`natural`/`commercial`/`raw_iphone`),
464
- `aspect_ratio` (`9:16`/`1:1`, with NO default — silence and an explicit choice
465
- differ), `resolution` (`720p`/`1080p`/`4k`), `voice`/`voice_id`.
466
-
467
- **Russian stress marks.** Tell a user who writes in Russian that they can fix stress:
468
- write the stressed vowel as a capital inside a lowercase word — `потОм`, `зАмок` — and
469
- Clipwright sends `eleven_v4` and `gemini-3.8-flash-tts` the stress mark U+0301
470
- (`пото́м`). A capital that starts a
471
- word stays a capital, a word with a second capital or an inner capital consonant
472
- (`ВУЗы`, all caps) stays as written, and a U+0301 typed directly is kept. A single
473
- inner capital vowel always reads as stress, so `ЯндексЕда` needs a space. Keep Russian
474
- scripts on one of those two models: the 2.5 models misread the mark, so a script
475
- carrying it draws a warning there, and so does any Cyrillic script. With a preset, a
476
- catalog voice or `voice_id` the choice is `eleven_v4`, because
477
- `gemini-3.8-flash-tts` speaks only its own voices. Retrying never
478
- fixes stress; only the text does.
479
-
480
- **Break tags on `eleven_v4` and `gemini-3.8-flash-tts`.** These two models give no pause
481
- for `<break time="1.5s" />`.
482
- Clipwright cuts every tag of that form (`time` in `s` or `ms`) from the text it sends to
483
- them, leaves it out of the character count, and names the cut in `warnings[]` of
484
- the quote and the run. A script that holds nothing but break tags is refused before any
485
- charge. The other models get the tag as written.
486
-
487
- The exact list is always available from the tool schema itself: `tools/list` is
488
- generated from the same shape, and fields that are not honoured carry
489
- `NOT HONORED YET` right in their own `description`.
490
-
491
- Spends no credits. The response is the raw `quote`; its price fields for a
492
- 12-second script read:
248
+ Its price fields for a 12-second script:
493
249
 
494
250
  ```json
495
251
  {
496
252
  "skill": "make_ugc",
497
- "credits_estimate": 156,
253
+ "credits_estimate": 120,
498
254
  "duration_estimate_sec": 12,
499
- "tariff": "parts-v1",
255
+ "tariff": "parts-v2",
500
256
  "price_lines": [
501
- { "label": "presenter, HeyGen Avatar IV", "unit": "face_second", "quantity": 12, "unit_credits": 12, "credits": 144 },
257
+ { "label": "presenter, HeyGen Avatar IV", "unit": "face_second", "quantity": 12, "unit_credits": 9, "credits": 108 },
502
258
  { "label": "production (voice and render)", "unit": "second", "quantity": 12, "unit_credits": 1, "credits": 12 }
503
259
  ],
504
260
  "minimum_credits": 50,
@@ -506,512 +262,195 @@ Spends no credits. The response is the raw `quote`; its price fields for a
506
262
  }
507
263
  ```
508
264
 
509
- Show the user `credits_estimate` and the lines; prices change, so never repeat
510
- them from this page.
511
-
512
- `warnings[]` IS present here (passed through unfiltered) — read it: parameters
513
- that will not be honoured (for example, a `caption_emphasis` quote spoken entirely
514
- under a full-frame insert, where no captions are drawn) are
515
- declared there rather than dropped silently.
516
-
517
- **Always call `quote_ugc` first and show the user the price before `make_ugc`**
518
- (the tool description says so in the imperative).
519
-
520
- ### `make_ugc` — paid, creates a run
521
-
522
- The input is the same shape plus `attempt` (an integer ≥1, optional; see section
523
- 5). Money: this is ONE paid render per call, unless idempotency deduplication kicks
524
- in (section 2). **The tool does NOT wait for the video** — it starts the run and
525
- returns a `run_id` immediately:
526
-
527
- ```json
528
- {
529
- "status": "IN_PROGRESS",
530
- "run_id": "run_...",
531
- "state": "queued",
532
- "video_url": null,
533
- "warnings": [],
534
- "next_action": "Video is NOT ready. Call get_run with run_id=run_... again in ~5 seconds. Repeat until state is 'succeeded' or 'failed'. Do NOT tell the user the video is done until you have a video_url."
535
- }
536
- ```
537
-
538
- The shape is the one `get_run` returns: `make_ugc` calls the same formatter, so
539
- `warnings[]` is declared HERE, on the very first response, and not one poll later
540
- — by then the paid run is already going. Read it: this is where a format snap or
541
- an ignored field is announced.
542
-
543
- After that you MUST poll `get_run` with that `run_id` every ~5–10 seconds until it
544
- reaches a terminal state. Do not tell the user the video is ready until you have a
545
- `video_url`.
546
-
547
- #### Captions (`captions`, `caption_style`, `caption_emphasis`)
548
-
549
- `make_ugc` burns word-timed captions into the video when `captions: true`. They
550
- are off by default and do not change the price. Whether a video has them is the
551
- user's call:
552
-
553
- 1. **Ask whether they want captions** before the quote. Never turn them on
554
- unprompted.
555
- 2. **If yes, pick a style with them.** When the user gave a reference video, take
556
- its look: bold uppercase cues with one coloured keyword is `hormozi`, one or
557
- two words popping in colour is `tiktok`, a sentence on a dark box is `youtube`
558
- or `lecture`. Otherwise offer three or four styles that fit the video, one line
559
- each, and link the gallery, which shows every style on the same clip:
560
- https://clipwright.io/captions. Without a choice the server draws `hormozi`.
561
- 3. **Mark key words with `caption_emphasis`**: quotes from `script` drawn larger or
562
- in the style's accent colour. `caption_mode: "highlights"` captions only the
563
- lines around those quotes.
265
+ Also read `source`, `actor`, `resolved_aspect_ratio` (what will ACTUALLY be
266
+ rendered) and `tts_model`. The quote and the run share one verdict: a 400 on the
267
+ quote is the same 400 on the run, and a money or access refusal the run would
268
+ meet is named in the quote's `warnings[]`. The free curl form is in section 7.
269
+
270
+ ## 4. Traps: a wrong answer without an error
271
+
272
+ ### Silent: the answer looks right and is not
273
+
274
+ **An omitted `aspect_ratio` follows the source, not 9:16.** With `image`, the
275
+ video takes the supported format nearest the photo: a 1600×900 photo gives a
276
+ 16:9 video, and the format draws no warning. Only the default actor is always
277
+ 9:16. Pass `aspect_ratio` whenever you pass `image`. Checked live 2026-10-31.
278
+
279
+ **A format up to 15% away from the source is snapped, not refused.** The frame
280
+ is padded with white bars or center-cropped at the sides, and `warnings[]` says
281
+ which; within 2% there is no warning. Over 15% on an explicit `aspect_ratio` is
282
+ 400 `aspect_conflict` before any charge, so `1:1` on the default actor is refused.
283
+ A photo that could not be measured renders as requested, or 9:16, with a warning
284
+ that the format was not verified. Checked 2026-10-31.
285
+
286
+ **`person`, `name` and `look` change nothing.** They are accepted and quoted; the
287
+ default actor renders, and only `warnings[]` says so. The `character` refusal
288
+ suggests `person`, which still chooses no face. Choose one with `actor_id` from
289
+ `list_actors`, an `image`, or a new actor from `create_actor`. Checked live 2026-10-31.
290
+
291
+ **Captions are off in `make_ugc` and on in `make_faceless`.** `make_ugc` draws
292
+ word-timed captions only with `captions: true`; `make_faceless` draws them unless
293
+ `captions: false`. `make_faceless` takes only `captions`; the style, emphasis and
294
+ mode fields below are `make_ugc` fields, and `make_faceless` refuses them. Whether a
295
+ video has captions is the user's call:
296
+
297
+ 1. **Ask whether they want captions** before the quote. Never turn them on unprompted.
298
+ 2. **If yes, pick a style with them.** From a reference video take its look:
299
+ bold uppercase cues with one coloured keyword is `hormozi`, one or two words
300
+ popping in colour is `tiktok`, a sentence on a dark box is `youtube` or
301
+ `lecture`. Otherwise offer three or four styles that fit the video, one line
302
+ each, and link https://clipwright.io/captions. Without a choice the server draws `hormozi`.
303
+ 3. **Mark key words with `caption_emphasis`**: quotes from `script` drawn larger
304
+ or in the accent colour; `caption_mode: "highlights"` captions only around them.
564
305
 
565
306
  The styles: `hormozi` (the default), `accent`, `tiktok`, `youtube`, `instagram`,
566
307
  `minimalist`, `neon`, `genz`, `campfire`, `midnight`, `plottwist`, `headline`,
567
308
  `promo`, `lecture`, `highlighter`, `studio`, `broadcast`, `arcade`, `clutch`,
568
309
  `bubblegum`, `crayon`, `lyrics`, `bassdrop`, `punch`; `minimal` is an older name
569
- for `minimalist`. Each style's description is in `caption_style` of `tools/list`:
570
- quote that, not your memory of it. Captions are refused with `segments` and with
571
- `gemini-3.8-flash-tts`, which returns no word timings.
572
-
573
- #### Choosing a voice (`voice` / `voice_id`)
574
-
575
- Two mutually exclusive optional fields in the `make_ugc` input:
576
-
577
- - `voice` — the `name` of a voice from the free `list_voices` tool: one of the
578
- presets (`owner_ru_clone`, `sarah`, `george`, `eric`, `daria_ru_female`), one of
579
- the thirty voices of `gemini-3.8-flash-tts` (kind `model_voice`), or a
580
- catalog voice. **The recommended path.** Validated at the boundary: an unknown
581
- or retired name returns 400 immediately and no paid call is made. The two
582
- refusals differ: a retired voice's message names the date it left the catalog.
583
- - **A `model_voice` voice belongs to its model.** Naming one (`kore`, `puck`, …)
584
- makes the run speak `gemini-3.8-flash-tts` with no `tts_model`, and the quote
585
- names that model in `tts_model`. A voice and a `tts_model` that does not speak
586
- it return 400 before any charge, and the message names the way out: for a
587
- `model_voice` voice with an `eleven_*` model, omit `tts_model`; for a preset, a
588
- catalog voice or `voice_id` with `tts_model: "gemini-3.8-flash-tts"`, take a
589
- voice from `list_voices` with `model: "gemini-3.8-flash-tts"`. That model
590
- returns no word timings, so `captions: true` is refused with it too.
591
- - `voice_id` — a **raw ElevenLabs id** (20 characters), an escape hatch for voices
592
- OUTSIDE the catalog, cloned ones included. It is checked for free against the
593
- account at the start of the run (not during `quote`): a non-existent id fails the
594
- run BEFORE any paid call.
595
- - **The default voice follows the actor's gender.** Without `voice` or `voice_id`,
596
- a woman speaks `sarah` and a man speaks `george`. The gender comes from the
597
- `actor_id` catalog entry, or from `actor_gender` next to `image`. The default
598
- actor speaks `george`. With `tts_model: "gemini-3.8-flash-tts"` and no voice, a
599
- woman speaks `kore` and everyone else `charon`. An explicit voice always wins.
600
- - **With `image`, pass `actor_gender` or a `voice`.** Clipwright does not detect
601
- gender from the photo. Without either, the voice is `george` and `warnings[]`
602
- says so. `actor_gender` next to `actor_id`, or without `image`, returns 400
603
- before any charge.
604
- - **Read `actor` in the quote before rendering.** It describes the selected
605
- actor (`null` means the client sent `image`). An explicit voice is used as
606
- requested whatever the actor's gender; `actor_gender` next to it changes nothing,
607
- and a warning says so.
608
-
609
- **How to pick a voice when the user asks for one:**
610
-
611
- 1. Call `list_voices` with the filters the user gave: `language`, `gender`,
612
- `age`, `use_case`. Language is a filter only: any voice speaks any supported
613
- language, so a Spanish script can use an English voice.
614
- 2. Play the `preview_url` of two or three candidates to the user. A catalog
615
- voice's sample is in its native language. A `model_voice` voice's sample is in
616
- English; call `list_voices` with `language: "ru"` to get it in Russian.
617
- 3. Pass the chosen `name` as `voice` to `quote_ugc` and then `make_ugc`.
618
- 4. Next time, reuse that same `name`. A name always means the same voice.
619
-
620
- When the user sends a photo and asks for no particular voice, pass the gender of
621
- the person in it as `actor_gender`. Ask the user only when you cannot tell.
622
-
623
- For a Russian script, leave `tts_model` out or name `eleven_v4` — it reads stress
624
- marks — and mark stress with a capital vowel (see **Russian stress marks**
625
- above). A Russian voice speaks `eleven_v4` on its own. A `model_voice` voice reads
626
- stress marks too, on its own model `gemini-3.8-flash-tts`.
627
-
628
- ### `get_run` — status polling
629
-
630
- Input: `{ "run_id": "run_..." }`. The response shape is implemented in
631
- `packages/mcp-server/src/format.ts` (`formatGetRun`) and is given below as it
632
- actually is.
633
-
634
- **A non-terminal run**
635
- (`queued`/`scripting`/`tts`/`avatar`/`compositing`/`uploading`) carries the stage
636
- in `state`, with `status` always `"IN_PROGRESS"`:
637
-
638
- ```json
639
- {
640
- "status": "IN_PROGRESS",
641
- "run_id": "run_...",
642
- "state": "avatar",
643
- "video_url": null,
644
- "next_action": "Video is NOT ready. Call get_run with run_id=run_... again in ~5 seconds. Repeat until state is 'succeeded' or 'failed'. Do NOT tell the user the video is done until you have a video_url."
645
- }
646
- ```
647
-
648
- **Terminal success** carries BOTH `status: "SUCCEEDED"` AND `state: "succeeded"`:
649
-
650
- ```json
651
- {
652
- "status": "SUCCEEDED",
653
- "run_id": "run_...",
654
- "state": "succeeded",
655
- "video_url": "https://...",
656
- "duration_seconds": 13.08,
657
- "ai_disclosure": "This video was generated with AI: ..."
658
- }
659
- ```
660
-
661
- `ai_disclosure` names what is synthetic in that video: a faceless video and a video with a presenter carry
662
- different sentences. Pass it to the user as written when they ask how to label the publication.
663
-
664
- **Terminal failure** carries BOTH `status: "FAILED"` AND `state: "failed"`:
665
-
666
- ```json
667
- {
668
- "status": "FAILED",
669
- "run_id": "run_...",
670
- "state": "failed",
671
- "error": "<error text>"
672
- }
673
- ```
674
-
675
- **`warnings[]` in `get_run`:** every branch of the response (`IN_PROGRESS`,
676
- `SUCCEEDED`, `FAILED`) carries the run's `warnings` — parameters that were not
677
- honoured are always declared, never dropped silently. `make_ugc` shares the
678
- formatter, so its first response carries them too. Read it on the terminal
679
- response without fail: for instance, captions laid out evenly because the speech
680
- model returned no word timings are announced exactly there.
681
-
682
- **An unknown terminal status on the server** (say a hypothetical `"canceled"` that
683
- is not in `TERMINAL_STATES`) is a deliberate degradation: `isTerminal` returns
684
- `false`, so such a run lands in the `IN_PROGRESS` branch and the agent is told to
685
- keep polling a run that has already finished. That is accepted prototype behaviour
686
- — MCP and the API ship as one version — not a formatter bug.
687
-
688
- ### `get_run_shots` — free, read-only
689
-
690
- Takes a `run_id` and returns the shots of that video as the API states them
691
- (`GET /v1/runs/{id}/shots`). It changes nothing and starts nothing.
692
-
693
- - `planned: true` — the answer holds `frame`, `timebase.fps`, `duration_frames`,
694
- the narration `words` with their timing, and `tracks[]`. Each shot has an `id`,
695
- `start_frame` and `end_frame`, a `kind`, the words it covers, a `description`
696
- and a `preview_url`. Spans are whole frames at `timebase.fps`.
697
- - `planned: false` — a normal answer, not an error. The run has no shots to show,
698
- and `editable.state` with `editable.reason` says why: the run did not succeed,
699
- its `line` is one the API reads no shots for, or it kept no record of its
700
- media. Tell the user that reason; do not retry.
701
-
702
- `preview_url` is a signed link to the shot's source picture or clip, which is a
703
- large file. It expires: call the tool again for a fresh link and never store it.
704
- A field named in `withheld` is null on every shot, so a null `description` or
705
- `preview_url` there means "not shown", not "missing".
706
-
707
- `editable.media_available_until` is how long the media of the run are stored.
708
- `paid_edits.enabled` says whether a video can be made from an edit; while it is
709
- `false`, none can, and `revise_run` answers `editing_disabled`. Read `kind`, `line`,
710
- `editable.state` and each shot's `operations` as open strings, because a later
711
- API may name values this text does not list.
712
-
713
- ### `quote_revision` — free; `revise_run` — paid
714
-
715
- Both take a `run_id` and exactly one of `changes` (shots to generate anew over
716
- the run as it is: each a `shot_id` and its `operation`, such as
717
- `{ "mode": "retake" }`) or `document` (the whole edited document; retimes,
718
- deletions and uploads go this way). `quote_revision` answers what the revision
719
- would change (`summary`) and cost (`billing.total_credits`) and spends nothing.
720
- Show the user that price before `revise_run`.
721
-
722
- `revise_run` starts a NEW run and returns it at once; poll it with `get_run`.
723
- Once it succeeded it is editable under its own `run_id`. A revision that
724
- delivers is charged its quoted total; one that fails is charged nothing, even
725
- when its paid work was already done. An identical request returns the same run
726
- it started before, so a call repeated after a lost answer starts nothing new;
727
- pass `attempt: 2` to start a second revision with the same request on purpose.
728
-
729
- The refusals are terminal; do not retry them unchanged:
730
-
731
- - `editing_disabled`, `editing_not_priced`, `editing_window_not_set` (503) —
732
- paid edits are not open. Nothing was charged and no run was created.
733
- - `edit_ceiling_reached` (409) — `ceiling: "in_flight"` means a revision of the
734
- original is still running: follow `run_ids` with `get_run` and start the next
735
- one after it finishes; if it is held for review, contact support.
736
- `"failed_or_unfinished"` means support has to raise the limit.
737
- - `edit_invalid` (422) — the document breaks a rule of the run; `issues` lists
738
- each, and the rules are in the run's shots.
739
- - `run_not_editable` (409) with `state: "edit_window_closed"` — the paid window
740
- of the original has closed.
741
-
742
- ### `quote_candidate` — free; `create_candidate` — paid
743
-
744
- A shot candidate is one picture or clip made for one shot of a `make_faceless`
745
- run, to keep or discard in a later revision. Both tools take a `run_id`, a
746
- `shot_id` and an `operation` the shot lists in `get_run_shots` (such as
747
- `{ "mode": "retake" }`), and an optional `take`, 1 by default.
748
- `quote_candidate` answers the `unit`, its `credits` and `usable_until`, the last
749
- moment the candidate can enter a revision, and spends nothing. Show the user
750
- that price before `create_candidate`.
751
-
752
- `create_candidate` starts a NEW run of skill `edit_shot` and returns it at once;
753
- poll it with `get_run`. Once it succeeded, `candidate.preview_url` shows what was
754
- made. A delivered candidate is charged its one unit whether it is kept or
755
- discarded; one that fails is charged nothing, even when its paid work was
756
- already done. An identical request returns the same run it started before, so a
757
- call repeated after a lost answer starts nothing new; pass `take: 2` to make
758
- another candidate with the same request on purpose, charged again.
759
-
760
- The refusals are those of `revise_run` above. The candidate limits of an
761
- original, running at once and failed or unfinished, are counted apart from its
762
- revisions, and support raises only the revision limit. An operation the shot
763
- does not offer is `edit_invalid` with the code `action_not_allowed`.
764
-
765
- ### `quote_image`, `quote_video` — free; `generate_image`, `generate_video` — paid
766
-
767
- One image or one silent clip from a prompt, each a run of its own.
768
- `list_generation_models` lists the models, modes, aspect ratios, resolutions,
769
- durations and `unit_credits`: credits per image, or per second of a clip. Any
770
- other combination is `unsupported_combination` before any charge. The quotes
771
- answer the credits, the width and height, and warnings; show the price first.
772
-
773
- A reference is `{ "upload_id": ..., "rights_confirmed": true }` for a picture
774
- from `upload_image` or `create_upload`, `{ "run_id": ... }` of a succeeded `generate_image` run
775
- of this account until its `reusable_until`, or `{ "asset_id": ... }` of a picture asset from
776
- `create_asset`, whose rights were confirmed when it was made. In `first_frame` mode the frame must
777
- match the requested aspect ratio within 2% (`source_aspect_mismatch`).
778
-
779
- The run tools return the run at once; poll it with `get_run`, and once it
780
- succeeded `generation.url` is the file. A delivered generation is charged its
781
- quoted credits; one that fails is charged nothing, even when the vendor was
782
- already paid. An identical request returns the same run; pass `attempt: 2` for
783
- another. While generations are closed the tools answer `generations_disabled`
784
- or `generations_marking_pending` and charge nothing.
785
-
786
- ### `list_actors` — free, no input
787
-
788
- Returns the permanent actor catalog from `/v1/actors`, including stable
789
- `actor_id`, version, name, gender, approximate age and verified image formats.
790
- Each variant has dimensions, `preview_url` and `preview_expires_at`. Preview
791
- links expire; retain `actor_id` for reuse and call the catalog again for fresh previews.
792
-
793
- Pass `actor_id` to both `quote_ugc` and `make_ugc`. It cannot be combined with
794
- `image` or `person`. An unavailable actor or format returns an error before paid
795
- work; it never substitutes the default face. Existing runs retain the selected
796
- version and image hash even if the catalog changes later.
797
-
798
- ### `list_voices` — free, optional filters
799
-
800
- Returns the voices for the `voice` field of `make_ugc`, or of `make_faceless`
801
- when called with `skill: "make_faceless"`. Each voice names the one skill that
802
- speaks it in `skill`, and the other skill refuses it before any charge. The
803
- `make_ugc` voices are the presets, the thirty voices of `gemini-3.8-flash-tts`
804
- and the catalog voices, by language and then by rank. The seventeen
805
- `make_faceless` voices are a list of their own. Costs nothing and creates no
806
- run.
807
-
808
- All filters are optional:
809
-
810
- - `skill` — `make_ugc` or `make_faceless`: the skill whose voices to list.
811
- Without it the list holds the `make_ugc` voices only, so call it with
812
- `skill: "make_faceless"` before a faceless video.
813
- - `language` — the voice's native language. It is a filter, not a limit: any
814
- voice speaks any supported language. A `model_voice` voice has no language of
815
- its own and matches the languages measured on its model, `en` and `ru`; the
816
- filter also picks the language of its sample.
817
- - `gender` — `female` or `male`.
818
- - `age` and `use_case` — labels as the entries print them, such as `young` or
819
- `narrative_story`.
820
- - `model` — keeps the voices that speech model speaks: the presets and catalog
821
- voices whose language it supports, or the model's own voices.
822
- `model: "gemini-3.8-flash-tts"` returns its thirty voices.
823
-
824
- A voice without a label matches no value of that filter; `language` on a
825
- `model_voice` voice is the one exception above. An unknown value is
826
- refused with the allowed values, and an unknown filter name with
827
- `unknown_field`; neither returns an empty list.
828
-
829
- Each entry carries `name`, `kind` (`preset`, `model_voice`, `faceless_voice` or
830
- `catalog`), `skill`, `language`, `description`, `model` (its default speech
831
- model) and `supported_models`. A `model_voice` entry has no `language`, names one model in
832
- both `model` and `supported_models` and carries `gender` only where it is known
833
- (`kore` is female, `charon` is male). A `faceless_voice` entry carries its
834
- `language` and `gender` and the model label `narrator-v1`, which is not a
835
- `tts_model`: the `model` filter never returns it. A
836
- catalog voice also carries `verified_models` and whichever of `locale`,
837
- `accent`, `gender`, `age` and `use_case` the catalog has. The tool returns
838
- `{ "voices": [...] }` only. REST `GET /v1/voices` also returns `models`, and the
839
- script limit of each model is in `tools/list`.
840
-
841
- There is still a network call: the tool hits OUR `/v1/voices` and needs a working
842
- `CLIPWRIGHT_API_KEY`, like the other three. Nobody calls the vendor — the list
843
- lives in `@clipwright/core` and the samples in our own bucket, so this surface
844
- needs no vendor key.
845
-
846
- Call it before `make_ugc` when the user asks for "a different voice": a voice
847
- name is preferable to a raw `voice_id`, because a name survives a change of
848
- vendor and an identifier does not.
849
-
850
- **Let the user HEAR the voice before paying for it.** Every voice with a recorded
851
- sample carries `preview_url`: a short clip spoken by that voice with its own
852
- model, so it matches what the render will sound like. A catalog voice speaks its
853
- sample in its native language. A `model_voice` voice has two samples: the English
854
- one comes by default and the Russian one with `language: "ru"`. Descriptions convey neither
855
- diction nor accent nor pace; the owner caught a voice-face mismatch by ear, on a
856
- finished clip. Offer the sample whenever the user asks for a different voice.
857
-
858
- The link is signed and expires at `preview_expires_at`, an hour out. For a fresh
859
- one call `list_voices` again; never store it. A voice with no sample carries no
860
- `preview_url` at all — the sample does not exist, rather than having been dropped
861
- from the answer.
862
-
863
- ### `get_account` — free, no input
864
-
865
- Returns the body of `GET /v1/account`: `balance_credits`, `debt_credits`,
866
- `holds_credits` and the unexpired `grants`. It costs nothing and creates no run.
867
-
868
- - `debt_credits` above zero blocks `make_ugc` with `debt_outstanding`, whatever
869
- the balance, until the account buys credits.
870
- - With no debt, `balance_credits` is what a new run can spend.
871
- - `holds_credits` are reserved for runs in progress; they settle when those
872
- runs finish.
873
-
874
- `quote_ugc` already warns in `warnings[]` when `make_ugc` with the same input
875
- would be refused for money or access, so a render rarely needs this call first.
876
- Call it when the user asks how many credits are left, or to show the numbers
877
- behind such a warning.
878
-
879
- ### `list_prices` — free, no input
880
-
881
- Returns the body of `GET /v1/public/prices`: one entry per tool and variant with
882
- its price as `lines` of `credits` per `unit`, added up, and `minimum_credits`
883
- when there is one; where the price depends on the input, `lines` is empty and
884
- `formula` states the rule. Free tools are listed at 0. Each paid tool's description ends with
885
- the same price line. For the exact number of one call, use the quote tool the
886
- entry names in `quote_tool`.
887
-
888
- ### `quote_faceless` — free, and `make_faceless` — paid, creates a run
889
-
890
- A faceless video is narration over an opening animated clip and image scenes,
891
- without an on-camera presenter. You select 30 to 90 seconds; the video ends with
892
- its narration, runs at least 25 seconds and at most a quarter past the selection,
893
- never beyond 90. Captions are on by default; pass
894
- `captions: false` to turn them off.
895
-
896
- Give the narration one of two ways, and name which with `input_mode`:
897
-
898
- - `input_mode: "script"` with `script` — your exact text is read as written.
899
- The video ends with the narration, runs at least 25 seconds and may exceed
900
- `duration_seconds` by up to a quarter, never beyond 90 seconds; a video more
901
- than 5 seconds longer says so in `warnings[]`. A script is accepted when its
902
- estimated length is 21 seconds to `duration_seconds` plus 5, at most 90. The charge follows the delivered duration and never
903
- exceeds the quote; `list_prices` gives the rates and the minimum.
904
- - `input_mode: "brief"` with `brief` — a short description; the narration is
905
- written from it.
906
-
907
- Send exactly one of `script` and `brief`, the one that matches `input_mode`. The
908
- tool schema lists both as optional because MCP cannot express "one or the
909
- other"; the server and this client both refuse a mismatch, and the client
910
- refuses it before any request is sent.
911
-
912
- `duration_seconds` (30 to 90, on a 1/25 s frame boundary) is required. Optional:
913
- `style_reference` (a public https image the scenes follow in style),
914
- `character_reference` (a public https image of a person or figure to keep
915
- consistent) and `scene_images` (your own images, each anchored to a word range
916
- or a quote of the narration). An uploaded image must belong to this account.
917
-
918
- `voice` picks the narration voice: a `name` from `list_voices` with
919
- `skill: "make_faceless"`. There are seventeen, in English, Russian, Spanish,
920
- Portuguese, French and Italian; each entry has a `language` and a `gender`.
921
- Without `voice` the narration is read by `narrator_en_wise_lady`, an English
922
- voice, whatever the language of the text, so pick a voice in the language of
923
- the narration. A voice reads any language with its own accent; when the
924
- language of a voice you named is written in another alphabet than the script,
925
- the quote and the run say so in `warnings[]`: the voice may mispronounce the
926
- text, and the video may come out longer than its length limit and fail. A
927
- brief draws that line, worded as a condition, only when it is written in the
928
- other alphabet and holds no letter of the voice's: the narration is written in the language of the brief
929
- unless the brief asks for another, so name the narration's language in the
930
- brief when it differs from the brief's own. An unknown name and a `make_ugc`
931
- voice such as `george` are refused before any charge. `voice_id` is not
932
- accepted: `make_faceless` speaks only the voices listed for it. Both faceless
933
- tools refuse `voice_id` and any other key they do not declare before any
934
- request is sent. These voices have no `preview_url` yet.
935
-
936
- Call `quote_faceless` first and show the user the price: it spends nothing and
937
- warns when `make_faceless` would be refused for money or access. `make_faceless`
938
- returns a `run_id` at once; poll `get_run` until `succeeded` (with `video_url`)
939
- or `failed`. As with `make_ugc`, a repeated call with the same input returns the
940
- same run, and `attempt` 2, 3, … deliberately starts a new paid one. `attempt`
941
- belongs to `make_faceless`: a quote takes none, and `quote_faceless` refuses
942
- it. When faceless generation is switched off on the server, both tools answer
943
- `paid_render_disabled` and nothing is charged.
944
-
945
- A `succeeded` faceless run can carry a remark from our own delivery check in
946
- `warnings[]`: that the check could not confirm the captions or a scene change at
947
- a given second, or that it did not run. The video is delivered and charged as
948
- any other. Read the warning to the user and let them watch the video before
949
- they publish it.
310
+ for `minimalist`. Quote each style's description from `caption_style` in
311
+ `tools/list`, not from memory. Captions are refused with `gemini-3.8-flash-tts`,
312
+ which returns no word timings. Checked 2026-10-31.
313
+
314
+ **`make_faceless` without `voice` narrates in English.** The default
315
+ `narrator_en_wise_lady` reads a Russian or Spanish script with an English accent,
316
+ and `warnings[]` stays empty: the language warning fires only for a voice you
317
+ named. Pick a voice in the narration's language from `list_voices` with
318
+ `skill: "make_faceless"`. Without `skill`, `list_voices` lists only `make_ugc`
319
+ voices, and `make_faceless` refuses those. A brief is narrated in its own
320
+ language unless it asks for another. Checked live 2026-10-31.
321
+
322
+ **A photo's gender is not detected.** With `image` and neither `actor_gender`
323
+ nor `voice`, the male voice `george` speaks and `warnings[]` says so. Pass
324
+ `actor_gender` or a `voice`. With `actor_id` the voice follows the actor's gender.
325
+ Checked 2026-10-31.
326
+
327
+ **`voice_pace` does nothing on `eleven_v4`, the default model of presets and
328
+ catalog voices.** The voice speaks at its own pace, and the run's `warnings[]`
329
+ says so; the quote does not. To change pace, pick a voice by `words_per_second`
330
+ in `list_voices`. Checked 2026-10-31.
331
+
332
+ **`eleven_v3` is spoken as `eleven_v4`, and break tags give no pause** on
333
+ `eleven_v4` and `gemini-3.8-flash-tts`: `<break time="1.5s" />` is cut from the
334
+ text. `warnings[]` names both. A script of nothing but break tags is refused.
335
+ Checked 2026-10-31.
336
+
337
+ **Russian stress is written, not retried.** On `eleven_v4` and
338
+ `gemini-3.8-flash-tts` a capital vowel inside a lowercase word marks stress:
339
+ `потОм`. A capital that starts a word, an all-caps word, or a word with a second
340
+ capital stays as written. The 2.5 models misread the mark and warn on Cyrillic.
341
+ A retry never fixes stress; only the text does. Checked 2026-10-31.
342
+
343
+ **`make_faceless` `duration_seconds` is a target, not a length.** The video ends
344
+ with its narration and runs at least 25 seconds. It usually ends within a
345
+ quarter past the selection and 90 seconds. A longer narration runs to its end,
346
+ and the charge never exceeds the quote. A video more than 5 seconds past the
347
+ selection names both lengths in `warnings[]`. A
348
+ script whose estimated length is under 21 seconds or over the selection plus 5
349
+ is refused before any charge; the message names the window, the code is the
350
+ broader `unusable_source`. Checked live 2026-10-31.
351
+
352
+ **A `make_ugc` quote is an estimate; a `make_faceless` quote is a ceiling.**
353
+ `make_ugc` charges the measured duration rounded up to whole seconds, which can
354
+ exceed `credits_estimate`. `make_faceless` charges the delivered length and never
355
+ more than its quote. Read `credits_charged` on the finished run: it is `null`
356
+ until the run settles, while `credits_reserved` holds the reserve. Checked 2026-10-31.
357
+
358
+ **The price field has a different name in each quote.** `credits_estimate` in
359
+ `quote_ugc`, `quote_actor` and `quote_analysis`; `credits` in `quote_image`,
360
+ `quote_video` and `quote_candidate`; `totalCredits` in `quote_faceless`;
361
+ `billing.total_credits` in `quote_revision`. A missing field is a wrong name, not
362
+ a free call. Checked live 2026-10-31.
363
+
364
+ **The same input after a failure returns the same failed run.** A repeated paid
365
+ call with the same input and the same `attempt` answers 200 with the earlier run,
366
+ terminal failure included, and starts nothing. It is not a permanent refusal: to
367
+ try again, raise `attempt` (section 5). `create_candidate` takes `take` instead.
368
+ Checked 2026-10-31.
369
+
370
+ **`failed` is not always final.** Rarely, a run that ended `failed` is reopened
371
+ when its video arrives late: `get_run` shows `queued` with a warning, then
372
+ `succeeded` with a link and a charge for the delivered video, and `warnings[]`
373
+ names the recovery. A retry started meanwhile is a second video and a second
374
+ charge. Before reporting a failure as final, read the run once more. Checked 2026-10-31.
375
+
376
+ **A new Idempotency-Key on the same input is a second paid run.** Over REST the
377
+ key is the only thing that collapses a repeat; the server does not compare inputs
378
+ across keys. After an uncertain answer (a timeout, `429`, `5xx`), resend with THE
379
+ SAME key. The same key with a different body is 409 `idempotency_key_reused`.
380
+ Checked 2026-10-31.
381
+
382
+ **A `succeeded` faceless run can carry a remark from the delivery check** in
383
+ `warnings[]`: captions or a scene change it could not confirm at a given second,
384
+ or that the check did not run. The video is delivered and charged. Read the
385
+ remark to the user and let them watch before they publish. Checked 2026-10-31.
386
+
387
+ ### Errors whose shape hides the cause
388
+
389
+ **`curl -I` on `video_url` answers 403.** The link is signed for GET only. Check
390
+ it with a ranged GET (`curl -r 0-0`). Each `get_run` signs a fresh link valid for
391
+ seven days; never store it. Preview links of voices, actors and shots expire too:
392
+ call the tool again. Checked 2026-10-31.
393
+
394
+ **A raw `voice_id` the account does not have passes the quote and fails the run.**
395
+ It is checked at the start of the run, before any paid call, so the failure costs
396
+ nothing; a `voice_id` the account has is spoken. A `voice`
397
+ name from `list_voices` is checked at the quote. Checked 2026-10-31.
398
+
399
+ **`planned: false` from `get_run_shots` is an answer.** `editable.state` and
400
+ `editable.reason` say why the run has no shots to show; tell the user, do not
401
+ retry. A field named in `withheld` is null on every shot: "not shown", not
402
+ "missing". Checked 2026-10-31.
403
+
404
+ **A reference reel is polled with `get_reference_analysis`, not `get_run`.** Pass
405
+ an Instagram or TikTok link as `source_url` (no `create_asset` first); a YouTube
406
+ link is refused, so ask for the MP4. The same reel by its full link, in the same
407
+ mode, answers its earlier analysis for free; a short `vm.tiktok.com` link always
408
+ starts a new, paid one. `mode: "data"` reads many reels cheaply, without a
409
+ `reference_profile`. Checked 2026-10-31.
410
+
411
+ **An unknown terminal state reads as in progress.** The MCP formatter reports a
412
+ state it does not know as `IN_PROGRESS`, so polling would not end. Update the
413
+ client: MCP and the API ship as one version. Checked 2026-10-31.
950
414
 
951
415
  ## 5. Retrying a failed run is NOT a permanent refusal
952
416
 
953
- If `get_run` returned a terminal `FAILED` and you call `make_ugc` AGAIN with the
954
- SAME input (same `script` and options) and WITHOUT changing `attempt`, the server
955
- returns THAT SAME terminal failed run as it is (same error, same `run_id`) instead
956
- of starting a new paid render. That is idempotency deduplication (section 2), not
957
- a sign that the video is forever out of reach.
958
-
959
- **To actually retry, pass `attempt` one higher than before** (the first call is
960
- `attempt=1` by default; the second attempt is `attempt: 2`, the third `attempt: 3`,
961
- and so on). This is the behaviour documented verbatim in the `make_ugc` tool
962
- description:
417
+ A repeated call with the SAME input and WITHOUT a new `attempt` returns THAT SAME
418
+ failed run (section 4). **To actually retry, pass `attempt` one higher than
419
+ before** (the first call is `attempt: 1`). The `make_ugc` tool description says
420
+ the same:
963
421
 
964
422
  > "Pass attempt=2,3,… to deliberately start a NEW run for the same input
965
423
  > (retry after a failure)."
966
424
 
967
- Technically `attempt` does not go into the render request body but into the SDK
968
- options, and becomes a `:N` suffix on the idempotency key — a different suffix
969
- gives a different key, so the server sees a NEW request, which means a new
970
- `run_id` and a new paid vendor call.
971
-
972
- Do not confuse this with `CLIPWRIGHT_CLIENT_ID`: `attempt` is the agent's
973
- deliberate "this is a new attempt", while `clientId` is the fixed identity of the
974
- installation and must NOT change between attempts.
425
+ `attempt` is the agent's "this is a new try"; `CLIPWRIGHT_CLIENT_ID` is the
426
+ installation's fixed identity and must NOT change between attempts.
975
427
 
976
428
  ### ⚠️ EVERY `attempt` BUMP COSTS MONEY
977
429
 
978
- A new `attempt` is a new run and a **new paid vendor call**, not a free resend.
979
- The loop "failed → bump → failed → bump" burns real credits on every iteration.
980
-
981
- So:
982
-
983
- - **read the `error` of the failed run before retrying.** Refusals such as
984
- `rejected_field`, `aspect_conflict` or `script_encoding_lost` are NOT fixed by
985
- retrying — they are determined by the input, and the next attempt fails the
986
- same way, only now for money. `script_encoding_lost` in particular cannot be
987
- fixed at all by sending the same text again: the bytes were lost before the
988
- request reached us, so re-send the script decoded as UTF-8;
989
- - **`speech_too_short_for_script` is almost never fixed by retrying either.**
990
- The vendor's pronunciation and stress are determined by the text; only the
991
- length varies, and only within the vendor's measured spread. A take just under
992
- the threshold may pass on a second try, but a take far under it will not —
993
- change the text or the voice instead;
994
- - **bump `attempt` only for a refusal that looks temporary** (vendor timeout, 5xx,
995
- a dropped connection);
996
- - **no daily cap stands between you and your credits (0.7.0).** Not on your
997
- account, and not on the service either: `global_cap_exceeded` is no longer
998
- produced. What still stops a run before any paid call is your own balance, an
999
- unpaid debt, and our killswitch.
1000
-
1001
- ## 5.1. Server refusals are read from fields, not guessed (0.2.0)
1002
-
1003
- Since 0.2.0 an API refusal carries structured fields instead of one free-form
1004
- sentence. It still arrives the way every tool result does — as JSON **inside the
1005
- text block**, with `isError: true`:
430
+ A new `attempt` is a **new paid run**, not a free resend. The loop "failed → bump
431
+ → failed → bump" burns credits on every iteration.
432
+
433
+ - **Read the `error` of the failed run first.** `rejected_field`,
434
+ `aspect_conflict` and `script_encoding_lost` come from the input, and the next
435
+ attempt fails the same way for money. For `script_encoding_lost`, re-send the
436
+ script decoded as UTF-8.
437
+ - **`speech_too_short_for_script` is almost never fixed by a retry**: change the
438
+ text or the voice.
439
+ - **Bump `attempt` only for a failure that looks temporary** (a timeout, `5xx`, a
440
+ dropped connection).
441
+ - **No daily cap stands between you and the credits.** What stops a run before
442
+ any paid call is the balance, an unpaid debt, or a pause on our side.
443
+
444
+ ## 6. Server refusals are read from fields, not guessed
445
+
446
+ A refusal arrives as JSON **inside the text block**, with `isError: true`:
1006
447
 
1007
448
  ```ts
1008
449
  { content: [{ type: "text", text: "{\"status\":\"FAILED\", …}" }], isError: true }
1009
450
  ```
1010
451
 
1011
- So `JSON.parse(content[0].text)` first, then read the fields below. Reading
1012
- `content[0].balance_credits` gives `undefined`: the numbers are inside the parsed
1013
- text, not beside it. Do not relay the raw text to the user and do not guess from
1014
- it.
452
+ So `JSON.parse(content[0].text)` first; `content[0].balance_credits` is
453
+ `undefined`. Do not relay the raw text and do not guess from it.
1015
454
 
1016
455
  ```json
1017
456
  {
@@ -1027,165 +466,92 @@ it.
1027
466
  }
1028
467
  ```
1029
468
 
1030
- Numeric fields appear only where the refusal actually has them:
1031
-
1032
- | Field | Appears on |
1033
- | --- | --- |
1034
- | `retry_after_seconds` | `rate_limited`, and any `http_5xx` where the server named a pause |
1035
- | `limit`, `window_seconds` | only when the 429 body followed the Clipwright contract; a 429 whose body did not carries neither |
1036
- | `balance_credits`, `required_credits` | `insufficient_credits` only |
1037
- | `debt_credits` | `debt_outstanding` only |
1038
-
1039
- Treat every one of them as optional and read what is there. In particular, do not
1040
- expect balance numbers on `debt_outstanding`: that refusal reports the debt, and
1041
- the balance is not what blocks the run.
469
+ The numbers appear only where the refusal has them: `retry_after_seconds` on
470
+ `rate_limited` and on a `5xx` that named a pause; `limit` and `window_seconds`
471
+ only when the 429 body followed the Clipwright contract; `balance_credits` and
472
+ `required_credits` on `insufficient_credits` only; `debt_credits` on
473
+ `debt_outstanding` only. Treat each as optional.
1042
474
 
1043
475
  **One rule: look at `retryable`.**
1044
476
 
1045
- - `retryable: true` (`rate_limited`, 5xx) — wait `retry_after_seconds` and repeat
1046
- **THE SAME call with the same arguments**. Do not bump `attempt`: this is not a
1047
- failed run but a refused request, and a new `attempt` would mean a second paid
1048
- render (section 5).
1049
- - `retryable: false` (`insufficient_credits`, `debt_outstanding`,
1050
- `account_not_admitted`, other 4xx) — **stop and tell the human**. A repeat
1051
- gives the same answer while eating into the rate limit. For money refusals,
1052
- quote the NUMBERS: "the account has 3 credits, this needs 8" — without them the
1053
- human cannot tell how much to buy.
1054
-
1055
- `account_not_admitted` (403) is not a money refusal and carries no numbers: the
1056
- account has no beta access. Buying credits will not lift it — access is granted
1057
- by the operator. Say that, and do not offer to top up.
1058
-
1059
- Do NOT promise a position in line or an upcoming batch. The refusal says one
1060
- thing only: this account has no access right now. An account whose access was
1061
- REVOKED gets the same 403, and promising it a turn would be a lie at the worst
1062
- possible moment.
1063
-
1064
- The two money codes are cured the same way — buy credits — but they must not be
1065
- conflated, because they describe different situations. `insufficient_credits`
1066
- means the balance is too small for this run. `debt_outstanding` means the account
1067
- owes credits, and it can owe them while still holding a balance, so telling the
1068
- human "you do not have enough credits" would be false. Credits bought while a
1069
- debt stands clear the debt first and only then reach the balance. Clearing the
1070
- debt lifts this particular refusal and nothing else: the run still needs enough
477
+ - `retryable: true` (`rate_limited`, `5xx`) — wait `retry_after_seconds` and
478
+ repeat **THE SAME call with the same arguments**. Do not bump `attempt`: this
479
+ was a refused request, not a failed run.
480
+ - `retryable: false` — **stop and tell the human**. For money refusals quote the
481
+ NUMBERS: "the account has 3 credits, this needs 8".
482
+
483
+ The two money codes are cured the same way — buy credits — but they describe
484
+ different situations. `insufficient_credits` means the balance is too small for
485
+ this run. `debt_outstanding` means the account owes credits, possibly while still
486
+ holding a balance, so "you do not have enough credits" would be false. Credits
487
+ bought while a debt stands clear the debt first; the run still needs enough
1071
488
  credits of its own, so a purchase equal to the debt clears it and adds nothing to
1072
- the balance. Quote the debt figure, and say that credits go to the debt first: a
1073
- purchase adds to the balance only the part that exceeds the debt. Whether the
1074
- human has to buy more than the debt depends on what the balance already holds,
1075
- and this refusal does not say — the balance is not what blocks the run.
1076
-
1077
- Do not read a source into the refusal. A `429` or a `5xx` can come from the
1078
- Clipwright API itself or from anything between you and it, and the fields above
1079
- cannot tell you which — so the report does not claim one, and neither should you
1080
- when relaying it. What is certain is the code and what to do about it.
489
+ the balance. Quote the debt figure and say that a purchase adds to the balance
490
+ only the part that exceeds the debt; this refusal does not report the balance.
1081
491
 
1082
- About `429` specifically: it does **not** mean no run was created. The rate limit
1083
- sits in front of the handler, so a repeat carrying an already-used idempotency key
1084
- gets one too. Retry with THE SAME key — a new key is a new paid render.
1085
-
1086
- **Nobody waits on your behalf here — you do the polling.** `get_run` makes one
1087
- call and hands you whatever came back, refusals included. So a `429` on a status
1088
- check is yours to sit out: wait `retry_after_seconds`, then call `get_run` again
1089
- with the same `run_id`. Do not treat it as a failed run and do not start a new
1090
- one — the run is already running and already paid for.
1091
-
1092
- (The SDK's own `makeUgc` does wait these out internally, which is why the CLI and
1093
- direct SDK users never see them. The MCP tools are deliberately thin: `make_ugc`
1094
- returns as soon as the run starts, so a long render never blocks your turn.)
492
+ `account_not_admitted` (403) is not a money refusal and carries no numbers: the
493
+ account has no access. Buying credits does not lift it; contact support. Promise
494
+ no date and no position: an account whose access was REVOKED gets the same 403.
1095
495
 
1096
- ## 6. On failure — plain curl as a diagnosis splitter
496
+ A `429` or `5xx` can come from the Clipwright API or from anything between you
497
+ and it; do not claim a source. A `429` does **not** mean no run was created: a
498
+ repeat with an already-used key can get one too, so retry with THE SAME key. On a
499
+ status check, a `429` is yours to sit out: wait, then call `get_run` again with
500
+ the same `run_id`. The run is already going and already paid for.
1097
501
 
1098
- If the MCP path does not get you to a playable video, hit the same public REST
1099
- directly with curl, bypassing MCP, SDK and CLI entirely. If curl gets through, the
1100
- packaging layer is at fault; if it does not, production or the network is.
502
+ ## 7. On failure — plain curl splits the diagnosis
1101
503
 
1102
- **Two commands, no repository needed** (the package is installed from npm and
1103
- carries no scripts):
504
+ If the MCP path does not reach a playable video, call the same REST API directly.
505
+ If curl gets through, the MCP setup is at fault (section 1); if not, the key,
506
+ the network or the service is.
1104
507
 
1105
508
  ```bash
1106
- TOKEN=cw_... # the same value as CLIPWRIGHT_API_KEY, section 1.3
509
+ TOKEN=cw_... # the same value as CLIPWRIGHT_API_KEY
1107
510
  API=https://api.clipwright.io
1108
511
 
1109
- # 1. Start a run. The idempotency key is required and must be UNIQUE per call:
1110
- # a repeated key returns the earlier run instead of a new one (section 2).
1111
- # Generate it with Node, not uuidgen: Node is certainly here (the package is
1112
- # installed via npx), while uuidgen may be missing on a bare Linux image — and
1113
- # then the shell silently substitutes an empty string, the key becomes
1114
- # constant, and a second run returns the earlier one, ruining exactly the
1115
- # diagnosis you started it for.
512
+ # 0. Free: the quote. Verified 2026-10-31 (200, 50 credits, resolved 9:16).
513
+ curl -sS -X POST "$API/v1/skills/make_ugc/quote" \
514
+ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
515
+ -d '{"script":"Hello! This is a test line."}'
516
+
517
+ # 1. Paid: start a run. The idempotency key is required and must be UNIQUE per
518
+ # call: a repeated key returns the earlier run. Generate it with Node, not
519
+ # uuidgen, which may be missing and leave the key empty and constant.
1116
520
  curl -sS -X POST "$API/v1/skills/make_ugc/run" \
1117
521
  -H "Authorization: Bearer $TOKEN" \
1118
522
  -H "Content-Type: application/json" \
1119
523
  -H "Idempotency-Key: cw-$(node -e 'process.stdout.write(crypto.randomUUID())')" \
1120
524
  -d '{"script":"Hello! This is a test line."}'
1121
525
 
1122
- # 2. Poll until terminal, every ~5–10 s.
526
+ # 2. Free: poll until terminal, every ~5–10 s.
1123
527
  curl -sS "$API/v1/runs/<run_id>" -H "Authorization: Bearer $TOKEN"
1124
528
  ```
1125
529
 
1126
- Reading the result:
1127
-
1128
- - **curl reaches `succeeded` with its own `run_id` and a playable link, but MCP
1129
- does not:** the root cause is in the MCP packaging layer (build, host config,
1130
- environment — section 1), not in the REST API and not in access or the token.
1131
- - **curl fails too:** the problem is not MCP. Check `$TOKEN` from the commands
1132
- above (shaped `cw_*`, not truncated when copied), network access to `$API`, and —
1133
- for the cloned-run story — that `CLIPWRIGHT_CLIENT_ID` really is unique on this
1134
- machine (section 2). If you get a 200 with a `run_id` that is not yours in
1135
- substance, that is the symptom of the collapse described in section 2 on the MCP
1136
- path; curl (which uses its own random `Idempotency-Key` per call) will not
1137
- reproduce it directly, but it is worth checking separately via `get_run` on that
1138
- `run_id`.
1139
-
1140
- The second request above — `GET /v1/runs/{id}` — is free: you can poll an existing
1141
- run as often as you like without making a new paid call.
1142
-
1143
- ## The whole sequence (what must reproduce)
1144
-
1145
- 1. Install the server: `npx -y -p @clipwright/mcp-server@latest clipwright-mcp`
1146
- starts and prints `running on stdio` (section 1.1). From source:
1147
- `pnpm install && pnpm build` in the repository root, then `dist/index.js`
1148
- exists.
1149
- 2. Generate a unique `CLIPWRIGHT_CLIENT_ID` (section 2) and put it, together with
1150
- `CLIPWRIGHT_API_KEY`, into the MCP host config (section 1.3).
1151
- 3. Restart the host and confirm `tools/list` returns every tool named in
1152
- section 1.3.
1153
- 4. `quote_ugc` with a script → show the price AND read `source`,
1154
- `resolved_aspect_ratio`, `warnings[]` (section 3).
1155
- 5. `make_ugc` → get a `run_id` immediately.
1156
- 6. `get_run` every ~5–10 s, reading `state` along the way and `status` + `state` on
1157
- the terminal response, until `SUCCEEDED` (`video_url`) or `FAILED` (`error`).
1158
- 7. On failure — the same two curl commands from section 6 to split the diagnosis.
530
+ If curl reaches `succeeded`
531
+ with its own `run_id` and MCP does not, look at the MCP build, host config and
532
+ environment. If curl fails too, check that `$TOKEN` is a whole `cw_*` key, that
533
+ `$API` is reachable, and that `CLIPWRIGHT_CLIENT_ID` is unique (section 2).
1159
534
 
1160
535
  ### If you are a second user evaluating the product
1161
536
 
1162
- You have one task: **reach a playable video using this file, without spoken help
1163
- from the owner.** What is being tested is the completeness of the guide, not you —
1164
- so do not ask, write down where you got stuck.
1165
-
1166
- Before you start, make sure of two different things.
537
+ Your task: **reach a playable video using this file, without spoken help from
538
+ whoever gave you access.** What is tested is the guide, not you: do not ask,
539
+ write down where you got stuck.
1167
540
 
1168
541
  1. **The token was issued on YOUR account, not merely as a separate token.** The
1169
- idempotency axis is `account_id`, and one account can hold several live tokens.
1170
- Your own account means another person's run cannot reach you at all. You cannot
1171
- check this from your side: the account behind a token is visible only to the
1172
- service. So the confirmation that the token was issued on a SEPARATE account
1173
- comes WITH the token — it is part of handing it over, not a question for the
1174
- owner. If it did not come, that is a gap in the instructions: write it down and
1175
- move on, no need to ask.
1176
- 2. **`CLIPWRIGHT_CLIENT_ID` was not copied from anyone's machine** (section 2). It
1177
- matters WITHIN an account: if you and the owner ended up on one account (even
1178
- under different tokens) AND your `clientId` matched, the server returns THE
1179
- OWNER'S RUN with a 200 and a playable link — a false success that looks more
1180
- convincing than a real one. It can match through a shared `HOME`, a copied
1181
- value, or a cloned image; a genuinely separate installation has its own even
1182
- without the environment variable.
1183
-
1184
- Tell two classes of failure apart, because they are fixed differently:
1185
-
1186
- - **`rejected_field`, `unknown_field`, `aspect_conflict`** — a contract doing its job, not a
1187
- defect. Read the error text: it names both the reason and the replacement;
1188
- - **everything else** (timeout, 5xx, empty response, tool not found) — a candidate
1189
- for a real defect. Here, run the same two curl commands from section 6: if curl
1190
- reaches a playable link and MCP does not, the cause is in the MCP packaging
1191
- layer, and that should be written down separately from the rest.
542
+ idempotency axis is `account_id`, and one account can hold several live
543
+ tokens. You cannot check this from your side, so the confirmation that the
544
+ token is on a SEPARATE account comes WITH the token. If it did not, write that
545
+ down and move on.
546
+ 2. **`CLIPWRIGHT_CLIENT_ID` was not copied from anyone's machine** (section 2).
547
+ If you and the person who issued your token are on one account AND your
548
+ `clientId` matches theirs, the server returns THAT OWNER'S RUN with a 200 and
549
+ a playable link: a false success that looks more convincing than a real one.
550
+
551
+ Tell two classes of failure apart:
552
+
553
+ - **`rejected_field`, `unknown_field`, `aspect_conflict`** — the contract doing
554
+ its job; the message names the reason and the replacement.
555
+ - **everything else** (a timeout, `5xx`, an empty response, a missing tool) — a
556
+ candidate for a real defect. Run the curl commands of section 7 and write down
557
+ separately whether curl reached a playable link where MCP did not.