@clipwright/mcp-server 0.34.0 → 0.36.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 39 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
- `delete_asset`, `get_account`, `create_actor`, `quote_actor`, `delete_actor`, `get_actor_defaults`,
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
- `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,707 +152,305 @@ 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_v3`, `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: the Russian presets
455
- `owner_ru_clone` and `daria_ru_female` and the catalog voices named `ru_*` speak
456
- `eleven_v4`, a voice of kind `model_voice` speaks its own model `gemini-3.8-flash-tts`,
457
- every other preset and catalog voice speaks `eleven_v3`; a raw `voice_id`
458
- speaks `eleven_v4` when at least 60% of the script's Cyrillic and Latin letters are
459
- Cyrillic, and `eleven_v3` otherwise; an explicit `tts_model` always wins;
460
- `eleven_flash_v2_5` and `eleven_turbo_v2_5` cost less and suit
461
- languages other than Russian), optionally `person` OR
462
- `image` (a public `https` URL) with `actor_gender` (`female`/`male`, only next to
463
- `image`), plus `name`, `captions` (defaults to `false` —
464
- **captions are opt-in, ASK THE USER FIRST**), `caption_style`
465
- (`hormozi`/`tiktok`/`minimal`), `look` (`natural`/`commercial`/`raw_iphone`),
466
- `aspect_ratio` (`9:16`/`1:1`, with NO default — silence and an explicit choice
467
- differ), `resolution` (`720p`/`1080p`/`4k`), `voice`/`voice_id`.
468
-
469
- **Russian stress marks.** Tell a user who writes in Russian that they can fix stress:
470
- write the stressed vowel as a capital inside a lowercase word — `потОм`, `зАмок` — and
471
- Clipwright sends `eleven_v4`, `eleven_v3` and `gemini-3.8-flash-tts` the stress mark U+0301
472
- (`пото́м`). A capital that starts a
473
- word stays a capital, a word with a second capital or an inner capital consonant
474
- (`ВУЗы`, all caps) stays as written, and a U+0301 typed directly is kept. A single
475
- inner capital vowel always reads as stress, so `ЯндексЕда` needs a space. Keep Russian
476
- scripts on one of those three models: the 2.5 models misread the mark, so a script
477
- carrying it draws a warning there, and so does any Cyrillic script. With a preset, a
478
- catalog voice or `voice_id` the choice is `eleven_v4` or `eleven_v3`, because
479
- `gemini-3.8-flash-tts` speaks only its own voices. Retrying never
480
- fixes stress; only the text does.
481
-
482
- **Break tags on `eleven_v4` and `gemini-3.8-flash-tts`.** These two models give no pause
483
- for `<break time="1.5s" />`.
484
- Clipwright cuts every tag of that form (`time` in `s` or `ms`) from the text it sends to
485
- them, leaves it out of the character count, and names the cut in `warnings[]` of
486
- the quote and the run. A script that holds nothing but break tags is refused before any
487
- charge. The other models get the tag as written.
488
-
489
- The exact list is always available from the tool schema itself: `tools/list` is
490
- generated from the same shape, and fields that are not honoured carry
491
- `NOT HONORED YET` right in their own `description`.
492
-
493
- Spends no credits. The response is the raw `quote` in full:
248
+ Its price fields for a 12-second script:
494
249
 
495
250
  ```json
496
251
  {
497
252
  "skill": "make_ugc",
498
- "credits_estimate": 360,
253
+ "credits_estimate": 120,
499
254
  "duration_estimate_sec": 12,
255
+ "tariff": "parts-v2",
256
+ "price_lines": [
257
+ { "label": "presenter, HeyGen Avatar IV", "unit": "face_second", "quantity": 12, "unit_credits": 9, "credits": 108 },
258
+ { "label": "production (voice and render)", "unit": "second", "quantity": 12, "unit_credits": 1, "credits": 12 }
259
+ ],
260
+ "minimum_credits": 50,
500
261
  "warnings": []
501
262
  }
502
263
  ```
503
264
 
504
- `warnings[]` IS present here (passed through unfiltered) — read it: parameters
505
- that will not be honoured (for example, `captions` ignored in this build) are
506
- declared there rather than dropped silently.
507
-
508
- **Always call `quote_ugc` first and show the user the price before `make_ugc`**
509
- (the tool description says so in the imperative).
510
-
511
- ### `make_ugc` — paid, creates a run
512
-
513
- The input is the same shape plus `attempt` (an integer ≥1, optional; see section
514
- 5). Money: this is ONE paid render per call, unless idempotency deduplication kicks
515
- in (section 2). **The tool does NOT wait for the video** — it starts the run and
516
- returns a `run_id` immediately:
517
-
518
- ```json
519
- {
520
- "status": "IN_PROGRESS",
521
- "run_id": "run_...",
522
- "state": "queued",
523
- "video_url": null,
524
- "warnings": [],
525
- "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."
526
- }
527
- ```
528
-
529
- The shape is the one `get_run` returns: `make_ugc` calls the same formatter, so
530
- `warnings[]` is declared HERE, on the very first response, and not one poll later
531
- — by then the paid run is already going. Read it: this is where a format snap or
532
- an ignored field is announced.
533
-
534
- After that you MUST poll `get_run` with that `run_id` every ~5–10 seconds until it
535
- reaches a terminal state. Do not tell the user the video is ready until you have a
536
- `video_url`.
537
-
538
- #### Choosing a voice (`voice` / `voice_id`)
539
-
540
- Two mutually exclusive optional fields in the `make_ugc` input:
541
-
542
- - `voice` — the `name` of a voice from the free `list_voices` tool: one of the
543
- presets (`owner_ru_clone`, `sarah`, `george`, `eric`, `daria_ru_female`), one of
544
- the thirty voices of `gemini-3.8-flash-tts` (kind `model_voice`), or a
545
- catalog voice. **The recommended path.** Validated at the boundary: an unknown
546
- or retired name returns 400 immediately and no paid call is made. The two
547
- refusals differ: a retired voice's message names the date it left the catalog.
548
- - **A `model_voice` voice belongs to its model.** Naming one (`kore`, `puck`, …)
549
- makes the run speak `gemini-3.8-flash-tts` with no `tts_model`, and the quote
550
- names that model in `tts_model`. A voice and a `tts_model` that does not speak
551
- it return 400 before any charge, and the message names the way out: for a
552
- `model_voice` voice with an `eleven_*` model, omit `tts_model`; for a preset, a
553
- catalog voice or `voice_id` with `tts_model: "gemini-3.8-flash-tts"`, take a
554
- voice from `list_voices` with `model: "gemini-3.8-flash-tts"`. That model
555
- returns no word timings, so `captions: true` is refused with it too.
556
- - `voice_id` — a **raw ElevenLabs id** (20 characters), an escape hatch for voices
557
- OUTSIDE the catalog, cloned ones included. It is checked for free against the
558
- account at the start of the run (not during `quote`): a non-existent id fails the
559
- run BEFORE any paid call.
560
- - **The default voice follows the actor's gender.** Without `voice` or `voice_id`,
561
- a woman speaks `sarah` and a man speaks `george`. The gender comes from the
562
- `actor_id` catalog entry, or from `actor_gender` next to `image`. The default
563
- actor speaks `george`. With `tts_model: "gemini-3.8-flash-tts"` and no voice, a
564
- woman speaks `kore` and everyone else `charon`. An explicit voice always wins.
565
- - **With `image`, pass `actor_gender` or a `voice`.** Clipwright does not detect
566
- gender from the photo. Without either, the voice is `george` and `warnings[]`
567
- says so. `actor_gender` next to `actor_id`, or without `image`, returns 400
568
- before any charge.
569
- - **Read `actor` in the quote before rendering.** It describes the selected
570
- actor (`null` means the client sent `image`). An explicit voice is used as
571
- requested whatever the actor's gender; `actor_gender` next to it changes nothing,
572
- and a warning says so.
573
-
574
- **How to pick a voice when the user asks for one:**
575
-
576
- 1. Call `list_voices` with the filters the user gave: `language`, `gender`,
577
- `age`, `use_case`. Language is a filter only: any voice speaks any supported
578
- language, so a Spanish script can use an English voice.
579
- 2. Play the `preview_url` of two or three candidates to the user. A catalog
580
- voice's sample is in its native language. A `model_voice` voice's sample is in
581
- English; call `list_voices` with `language: "ru"` to get it in Russian.
582
- 3. Pass the chosen `name` as `voice` to `quote_ugc` and then `make_ugc`.
583
- 4. Next time, reuse that same `name`. A name always means the same voice.
584
-
585
- When the user sends a photo and asks for no particular voice, pass the gender of
586
- the person in it as `actor_gender`. Ask the user only when you cannot tell.
587
-
588
- For a Russian script, leave `tts_model` out or name `eleven_v4` or `eleven_v3` — both
589
- read stress marks — and mark stress with a capital vowel (see **Russian stress marks**
590
- above). A Russian voice speaks `eleven_v4` on its own. A `model_voice` voice reads
591
- stress marks too, on its own model `gemini-3.8-flash-tts`.
592
-
593
- ### `get_run` — status polling
594
-
595
- Input: `{ "run_id": "run_..." }`. The response shape is implemented in
596
- `packages/mcp-server/src/format.ts` (`formatGetRun`) and is given below as it
597
- actually is.
598
-
599
- **A non-terminal run**
600
- (`queued`/`scripting`/`tts`/`avatar`/`compositing`/`uploading`) carries the stage
601
- in `state`, with `status` always `"IN_PROGRESS"`:
602
-
603
- ```json
604
- {
605
- "status": "IN_PROGRESS",
606
- "run_id": "run_...",
607
- "state": "avatar",
608
- "video_url": null,
609
- "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."
610
- }
611
- ```
612
-
613
- **Terminal success** carries BOTH `status: "SUCCEEDED"` AND `state: "succeeded"`:
614
-
615
- ```json
616
- {
617
- "status": "SUCCEEDED",
618
- "run_id": "run_...",
619
- "state": "succeeded",
620
- "video_url": "https://...",
621
- "duration_seconds": 13.08,
622
- "ai_disclosure": "This video was generated with AI: ..."
623
- }
624
- ```
625
-
626
- `ai_disclosure` names what is synthetic in that video: a faceless video and a video with a presenter carry
627
- different sentences. Pass it to the user as written when they ask how to label the publication.
628
-
629
- **Terminal failure** carries BOTH `status: "FAILED"` AND `state: "failed"`:
630
-
631
- ```json
632
- {
633
- "status": "FAILED",
634
- "run_id": "run_...",
635
- "state": "failed",
636
- "error": "<error text>"
637
- }
638
- ```
639
-
640
- **`warnings[]` in `get_run`:** every branch of the response (`IN_PROGRESS`,
641
- `SUCCEEDED`, `FAILED`) carries the run's `warnings` — parameters that were not
642
- honoured are always declared, never dropped silently. `make_ugc` shares the
643
- formatter, so its first response carries them too. Read it on the terminal
644
- response without fail: for instance, missing captions in the prototype are
645
- announced exactly there.
646
-
647
- **An unknown terminal status on the server** (say a hypothetical `"canceled"` that
648
- is not in `TERMINAL_STATES`) is a deliberate degradation: `isTerminal` returns
649
- `false`, so such a run lands in the `IN_PROGRESS` branch and the agent is told to
650
- keep polling a run that has already finished. That is accepted prototype behaviour
651
- — MCP and the API ship as one version — not a formatter bug.
652
-
653
- ### `get_run_shots` — free, read-only
654
-
655
- Takes a `run_id` and returns the shots of that video as the API states them
656
- (`GET /v1/runs/{id}/shots`). It changes nothing and starts nothing.
657
-
658
- - `planned: true` — the answer holds `frame`, `timebase.fps`, `duration_frames`,
659
- the narration `words` with their timing, and `tracks[]`. Each shot has an `id`,
660
- `start_frame` and `end_frame`, a `kind`, the words it covers, a `description`
661
- and a `preview_url`. Spans are whole frames at `timebase.fps`.
662
- - `planned: false` — a normal answer, not an error. The run has no shots to show,
663
- and `editable.state` with `editable.reason` says why: the run did not succeed,
664
- its `line` is one the API reads no shots for, or it kept no record of its
665
- media. Tell the user that reason; do not retry.
666
-
667
- `preview_url` is a signed link to the shot's source picture or clip, which is a
668
- large file. It expires: call the tool again for a fresh link and never store it.
669
- A field named in `withheld` is null on every shot, so a null `description` or
670
- `preview_url` there means "not shown", not "missing".
671
-
672
- `editable.media_available_until` is how long the media of the run are stored.
673
- `paid_edits.enabled` says whether a video can be made from an edit; while it is
674
- `false`, none can, and `revise_run` answers `editing_disabled`. Read `kind`, `line`,
675
- `editable.state` and each shot's `operations` as open strings, because a later
676
- API may name values this text does not list.
677
-
678
- ### `quote_revision` — free; `revise_run` — paid
679
-
680
- Both take a `run_id` and exactly one of `changes` (shots to generate anew over
681
- the run as it is: each a `shot_id` and its `operation`, such as
682
- `{ "mode": "retake" }`) or `document` (the whole edited document; retimes,
683
- deletions and uploads go this way). `quote_revision` answers what the revision
684
- would change (`summary`) and cost (`billing.total_credits`) and spends nothing.
685
- Show the user that price before `revise_run`.
686
-
687
- `revise_run` starts a NEW run and returns it at once; poll it with `get_run`.
688
- Once it succeeded it is editable under its own `run_id`. A revision that
689
- delivers is charged its quoted total; one that fails is charged nothing, even
690
- when its paid work was already done. An identical request returns the same run
691
- it started before, so a call repeated after a lost answer starts nothing new;
692
- pass `attempt: 2` to start a second revision with the same request on purpose.
693
-
694
- The refusals are terminal; do not retry them unchanged:
695
-
696
- - `editing_disabled`, `editing_not_priced`, `editing_window_not_set` (503) —
697
- paid edits are not open. Nothing was charged and no run was created.
698
- - `edit_ceiling_reached` (409) — `ceiling: "in_flight"` means a revision of the
699
- original is still running: follow `run_ids` with `get_run` and start the next
700
- one after it finishes; if it is held for review, contact support.
701
- `"failed_or_unfinished"` means support has to raise the limit.
702
- - `edit_invalid` (422) — the document breaks a rule of the run; `issues` lists
703
- each, and the rules are in the run's shots.
704
- - `run_not_editable` (409) with `state: "edit_window_closed"` — the paid window
705
- of the original has closed.
706
-
707
- ### `quote_candidate` — free; `create_candidate` — paid
708
-
709
- A shot candidate is one picture or clip made for one shot of a `make_faceless`
710
- run, to keep or discard in a later revision. Both tools take a `run_id`, a
711
- `shot_id` and an `operation` the shot lists in `get_run_shots` (such as
712
- `{ "mode": "retake" }`), and an optional `take`, 1 by default.
713
- `quote_candidate` answers the `unit`, its `credits` and `usable_until`, the last
714
- moment the candidate can enter a revision, and spends nothing. Show the user
715
- that price before `create_candidate`.
716
-
717
- `create_candidate` starts a NEW run of skill `edit_shot` and returns it at once;
718
- poll it with `get_run`. Once it succeeded, `candidate.preview_url` shows what was
719
- made. A delivered candidate is charged its one unit whether it is kept or
720
- discarded; one that fails is charged nothing, even when its paid work was
721
- already done. An identical request returns the same run it started before, so a
722
- call repeated after a lost answer starts nothing new; pass `take: 2` to make
723
- another candidate with the same request on purpose, charged again.
724
-
725
- The refusals are those of `revise_run` above. The candidate limits of an
726
- original, running at once and failed or unfinished, are counted apart from its
727
- revisions, and support raises only the revision limit. An operation the shot
728
- does not offer is `edit_invalid` with the code `action_not_allowed`.
729
-
730
- ### `quote_image`, `quote_video` — free; `generate_image`, `generate_video` — paid
731
-
732
- One image or one silent clip from a prompt, each a run of its own.
733
- `list_generation_models` lists the models, modes, aspect ratios, resolutions,
734
- durations and `unit_credits`: credits per image, or per second of a clip. Any
735
- other combination is `unsupported_combination` before any charge. The quotes
736
- answer the credits, the width and height, and warnings; show the price first.
737
-
738
- A reference is `{ "upload_id": ..., "rights_confirmed": true }` for a picture
739
- from `upload_image` or `create_upload`, `{ "run_id": ... }` of a succeeded `generate_image` run
740
- of this account until its `reusable_until`, or `{ "asset_id": ... }` of a picture asset from
741
- `create_asset`, whose rights were confirmed when it was made. In `first_frame` mode the frame must
742
- match the requested aspect ratio within 2% (`source_aspect_mismatch`).
743
-
744
- The run tools return the run at once; poll it with `get_run`, and once it
745
- succeeded `generation.url` is the file. A delivered generation is charged its
746
- quoted credits; one that fails is charged nothing, even when the vendor was
747
- already paid. An identical request returns the same run; pass `attempt: 2` for
748
- another. While generations are closed the tools answer `generations_disabled`
749
- or `generations_marking_pending` and charge nothing.
750
-
751
- ### `list_actors` — free, no input
752
-
753
- Returns the permanent actor catalog from `/v1/actors`, including stable
754
- `actor_id`, version, name, gender, approximate age and verified image formats.
755
- Each variant has dimensions, `preview_url` and `preview_expires_at`. Preview
756
- links expire; retain `actor_id` for reuse and call the catalog again for fresh previews.
757
-
758
- Pass `actor_id` to both `quote_ugc` and `make_ugc`. It cannot be combined with
759
- `image` or `person`. An unavailable actor or format returns an error before paid
760
- work; it never substitutes the default face. Existing runs retain the selected
761
- version and image hash even if the catalog changes later.
762
-
763
- ### `list_voices` — free, optional filters
764
-
765
- Returns the voices for the `voice` field of `make_ugc`, or of `make_faceless`
766
- when called with `skill: "make_faceless"`. Each voice names the one skill that
767
- speaks it in `skill`, and the other skill refuses it before any charge. The
768
- `make_ugc` voices are the presets, the thirty voices of `gemini-3.8-flash-tts`
769
- and the catalog voices, by language and then by rank. The seventeen
770
- `make_faceless` voices are a list of their own. Costs nothing and creates no
771
- run.
772
-
773
- All filters are optional:
774
-
775
- - `skill` — `make_ugc` or `make_faceless`: the skill whose voices to list.
776
- Without it the list holds the `make_ugc` voices only, so call it with
777
- `skill: "make_faceless"` before a faceless video.
778
- - `language` — the voice's native language. It is a filter, not a limit: any
779
- voice speaks any supported language. A `model_voice` voice has no language of
780
- its own and matches the languages measured on its model, `en` and `ru`; the
781
- filter also picks the language of its sample.
782
- - `gender` — `female` or `male`.
783
- - `age` and `use_case` — labels as the entries print them, such as `young` or
784
- `narrative_story`.
785
- - `model` — keeps the voices that speech model speaks: the presets and catalog
786
- voices whose language it supports, or the model's own voices.
787
- `model: "gemini-3.8-flash-tts"` returns its thirty voices.
788
-
789
- A voice without a label matches no value of that filter; `language` on a
790
- `model_voice` voice is the one exception above. An unknown value is
791
- refused with the allowed values, and an unknown filter name with
792
- `unknown_field`; neither returns an empty list.
793
-
794
- Each entry carries `name`, `kind` (`preset`, `model_voice`, `faceless_voice` or
795
- `catalog`), `skill`, `language`, `description`, `model` (its default speech
796
- model) and `supported_models`. A `model_voice` entry has no `language`, names one model in
797
- both `model` and `supported_models` and carries `gender` only where it is known
798
- (`kore` is female, `charon` is male). A `faceless_voice` entry carries its
799
- `language` and `gender` and the model label `narrator-v1`, which is not a
800
- `tts_model`: the `model` filter never returns it. A
801
- catalog voice also carries `verified_models` and whichever of `locale`,
802
- `accent`, `gender`, `age` and `use_case` the catalog has. The tool returns
803
- `{ "voices": [...] }` only. REST `GET /v1/voices` also returns `models`, and the
804
- script limit of each model is in `tools/list`.
805
-
806
- There is still a network call: the tool hits OUR `/v1/voices` and needs a working
807
- `CLIPWRIGHT_API_KEY`, like the other three. Nobody calls the vendor — the list
808
- lives in `@clipwright/core` and the samples in our own bucket, so this surface
809
- needs no vendor key.
810
-
811
- Call it before `make_ugc` when the user asks for "a different voice": a voice
812
- name is preferable to a raw `voice_id`, because a name survives a change of
813
- vendor and an identifier does not.
814
-
815
- **Let the user HEAR the voice before paying for it.** Every voice with a recorded
816
- sample carries `preview_url`: a short clip spoken by that voice with its own
817
- model, so it matches what the render will sound like. A catalog voice speaks its
818
- sample in its native language. A `model_voice` voice has two samples: the English
819
- one comes by default and the Russian one with `language: "ru"`. Descriptions convey neither
820
- diction nor accent nor pace; the owner caught a voice-face mismatch by ear, on a
821
- finished clip. Offer the sample whenever the user asks for a different voice.
822
-
823
- The link is signed and expires at `preview_expires_at`, an hour out. For a fresh
824
- one call `list_voices` again; never store it. A voice with no sample carries no
825
- `preview_url` at all — the sample does not exist, rather than having been dropped
826
- from the answer.
827
-
828
- ### `get_account` — free, no input
829
-
830
- Returns the body of `GET /v1/account`: `balance_credits`, `debt_credits`,
831
- `holds_credits` and the unexpired `grants`. It costs nothing and creates no run.
832
-
833
- - `debt_credits` above zero blocks `make_ugc` with `debt_outstanding`, whatever
834
- the balance, until the account buys credits.
835
- - With no debt, `balance_credits` is what a new run can spend.
836
- - `holds_credits` are reserved for runs in progress; they settle when those
837
- runs finish.
838
-
839
- `quote_ugc` already warns in `warnings[]` when `make_ugc` with the same input
840
- would be refused for money or access, so a render rarely needs this call first.
841
- Call it when the user asks how many credits are left, or to show the numbers
842
- behind such a warning.
843
-
844
- ### `quote_faceless` — free, and `make_faceless` — paid, creates a run
845
-
846
- A faceless video is narration over an opening animated clip and image scenes,
847
- without an on-camera presenter. You select 30 to 90 seconds; the video ends with
848
- its narration, runs at least 25 seconds and at most a quarter past the selection,
849
- never beyond 90. Captions are on by default; pass
850
- `captions: false` to turn them off.
851
-
852
- Give the narration one of two ways, and name which with `input_mode`:
853
-
854
- - `input_mode: "script"` with `script` — your exact text is read as written.
855
- The video ends with the narration, runs at least 25 seconds and may exceed
856
- `duration_seconds` by up to a quarter, never beyond 90 seconds; a video more
857
- than 5 seconds longer says so in `warnings[]`. A script is accepted when its
858
- estimated length is 21 seconds to `duration_seconds` plus 5, at most 90. The charge follows the delivered duration and never
859
- exceeds the quote; the minimum is 200 base credits plus 150 for the opener.
860
- - `input_mode: "brief"` with `brief` — a short description; the narration is
861
- written from it.
862
-
863
- Send exactly one of `script` and `brief`, the one that matches `input_mode`. The
864
- tool schema lists both as optional because MCP cannot express "one or the
865
- other"; the server and this client both refuse a mismatch, and the client
866
- refuses it before any request is sent.
867
-
868
- `duration_seconds` (30 to 90, on a 1/25 s frame boundary) is required. Optional:
869
- `style_reference` (a public https image the scenes follow in style),
870
- `character_reference` (a public https image of a person or figure to keep
871
- consistent) and `scene_images` (your own images, each anchored to a word range
872
- or a quote of the narration). An uploaded image must belong to this account.
873
-
874
- `voice` picks the narration voice: a `name` from `list_voices` with
875
- `skill: "make_faceless"`. There are seventeen, in English, Russian, Spanish,
876
- Portuguese, French and Italian; each entry has a `language` and a `gender`.
877
- Without `voice` the narration is read by `narrator_en_wise_lady`, an English
878
- voice, whatever the language of the text, so pick a voice in the language of
879
- the narration. A voice reads any language with its own accent; when the
880
- language of a voice you named is written in another alphabet than the script,
881
- the quote and the run say so in `warnings[]`: the voice may mispronounce the
882
- text, and the video may come out longer than its length limit and fail. A
883
- brief draws that line, worded as a condition, only when it is written in the
884
- other alphabet and holds no letter of the voice's: the narration is written in the language of the brief
885
- unless the brief asks for another, so name the narration's language in the
886
- brief when it differs from the brief's own. An unknown name and a `make_ugc`
887
- voice such as `george` are refused before any charge. `voice_id` is not
888
- accepted: `make_faceless` speaks only the voices listed for it. Both faceless
889
- tools refuse `voice_id` and any other key they do not declare before any
890
- request is sent. These voices have no `preview_url` yet.
891
-
892
- Call `quote_faceless` first and show the user the price: it spends nothing and
893
- warns when `make_faceless` would be refused for money or access. `make_faceless`
894
- returns a `run_id` at once; poll `get_run` until `succeeded` (with `video_url`)
895
- or `failed`. As with `make_ugc`, a repeated call with the same input returns the
896
- same run, and `attempt` 2, 3, … deliberately starts a new paid one. `attempt`
897
- belongs to `make_faceless`: a quote takes none, and `quote_faceless` refuses
898
- it. When faceless generation is switched off on the server, both tools answer
899
- `paid_render_disabled` and nothing is charged.
900
-
901
- A `succeeded` faceless run can carry a remark from our own delivery check in
902
- `warnings[]`: that the check could not confirm the captions or a scene change at
903
- a given second, or that it did not run. The video is delivered and charged as
904
- any other. Read the warning to the user and let them watch the video before
905
- they publish it.
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.
305
+
306
+ The styles: `hormozi` (the default), `accent`, `tiktok`, `youtube`, `instagram`,
307
+ `minimalist`, `neon`, `genz`, `campfire`, `midnight`, `plottwist`, `headline`,
308
+ `promo`, `lecture`, `highlighter`, `studio`, `broadcast`, `arcade`, `clutch`,
309
+ `bubblegum`, `crayon`, `lyrics`, `bassdrop`, `punch`; `minimal` is an older name
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.
906
414
 
907
415
  ## 5. Retrying a failed run is NOT a permanent refusal
908
416
 
909
- If `get_run` returned a terminal `FAILED` and you call `make_ugc` AGAIN with the
910
- SAME input (same `script` and options) and WITHOUT changing `attempt`, the server
911
- returns THAT SAME terminal failed run as it is (same error, same `run_id`) instead
912
- of starting a new paid render. That is idempotency deduplication (section 2), not
913
- a sign that the video is forever out of reach.
914
-
915
- **To actually retry, pass `attempt` one higher than before** (the first call is
916
- `attempt=1` by default; the second attempt is `attempt: 2`, the third `attempt: 3`,
917
- and so on). This is the behaviour documented verbatim in the `make_ugc` tool
918
- 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:
919
421
 
920
422
  > "Pass attempt=2,3,… to deliberately start a NEW run for the same input
921
423
  > (retry after a failure)."
922
424
 
923
- Technically `attempt` does not go into the render request body but into the SDK
924
- options, and becomes a `:N` suffix on the idempotency key — a different suffix
925
- gives a different key, so the server sees a NEW request, which means a new
926
- `run_id` and a new paid vendor call.
927
-
928
- Do not confuse this with `CLIPWRIGHT_CLIENT_ID`: `attempt` is the agent's
929
- deliberate "this is a new attempt", while `clientId` is the fixed identity of the
930
- 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.
931
427
 
932
428
  ### ⚠️ EVERY `attempt` BUMP COSTS MONEY
933
429
 
934
- A new `attempt` is a new run and a **new paid vendor call**, not a free resend.
935
- The loop "failed → bump → failed → bump" burns real credits on every iteration.
936
-
937
- So:
938
-
939
- - **read the `error` of the failed run before retrying.** Refusals such as
940
- `rejected_field`, `aspect_conflict` or `script_encoding_lost` are NOT fixed by
941
- retrying — they are determined by the input, and the next attempt fails the
942
- same way, only now for money. `script_encoding_lost` in particular cannot be
943
- fixed at all by sending the same text again: the bytes were lost before the
944
- request reached us, so re-send the script decoded as UTF-8;
945
- - **`speech_too_short_for_script` is almost never fixed by retrying either.**
946
- The vendor's pronunciation and stress are determined by the text; only the
947
- length varies, and only within the vendor's measured spread. A take just under
948
- the threshold may pass on a second try, but a take far under it will not —
949
- change the text or the voice instead;
950
- - **bump `attempt` only for a refusal that looks temporary** (vendor timeout, 5xx,
951
- a dropped connection);
952
- - **no daily cap stands between you and your credits (0.7.0).** Not on your
953
- account, and not on the service either: `global_cap_exceeded` is no longer
954
- produced. What still stops a run before any paid call is your own balance, an
955
- unpaid debt, and our killswitch.
956
-
957
- ## 5.1. Server refusals are read from fields, not guessed (0.2.0)
958
-
959
- Since 0.2.0 an API refusal carries structured fields instead of one free-form
960
- sentence. It still arrives the way every tool result does — as JSON **inside the
961
- 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`:
962
447
 
963
448
  ```ts
964
449
  { content: [{ type: "text", text: "{\"status\":\"FAILED\", …}" }], isError: true }
965
450
  ```
966
451
 
967
- So `JSON.parse(content[0].text)` first, then read the fields below. Reading
968
- `content[0].balance_credits` gives `undefined`: the numbers are inside the parsed
969
- text, not beside it. Do not relay the raw text to the user and do not guess from
970
- 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.
971
454
 
972
455
  ```json
973
456
  {
@@ -983,165 +466,92 @@ it.
983
466
  }
984
467
  ```
985
468
 
986
- Numeric fields appear only where the refusal actually has them:
987
-
988
- | Field | Appears on |
989
- | --- | --- |
990
- | `retry_after_seconds` | `rate_limited`, and any `http_5xx` where the server named a pause |
991
- | `limit`, `window_seconds` | only when the 429 body followed the Clipwright contract; a 429 whose body did not carries neither |
992
- | `balance_credits`, `required_credits` | `insufficient_credits` only |
993
- | `debt_credits` | `debt_outstanding` only |
994
-
995
- Treat every one of them as optional and read what is there. In particular, do not
996
- expect balance numbers on `debt_outstanding`: that refusal reports the debt, and
997
- 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.
998
474
 
999
475
  **One rule: look at `retryable`.**
1000
476
 
1001
- - `retryable: true` (`rate_limited`, 5xx) — wait `retry_after_seconds` and repeat
1002
- **THE SAME call with the same arguments**. Do not bump `attempt`: this is not a
1003
- failed run but a refused request, and a new `attempt` would mean a second paid
1004
- render (section 5).
1005
- - `retryable: false` (`insufficient_credits`, `debt_outstanding`,
1006
- `account_not_admitted`, other 4xx) — **stop and tell the human**. A repeat
1007
- gives the same answer while eating into the rate limit. For money refusals,
1008
- quote the NUMBERS: "the account has 3 credits, this needs 8" — without them the
1009
- human cannot tell how much to buy.
1010
-
1011
- `account_not_admitted` (403) is not a money refusal and carries no numbers: the
1012
- account has no beta access. Buying credits will not lift it — access is granted
1013
- by the operator. Say that, and do not offer to top up.
1014
-
1015
- Do NOT promise a position in line or an upcoming batch. The refusal says one
1016
- thing only: this account has no access right now. An account whose access was
1017
- REVOKED gets the same 403, and promising it a turn would be a lie at the worst
1018
- possible moment.
1019
-
1020
- The two money codes are cured the same way — buy credits — but they must not be
1021
- conflated, because they describe different situations. `insufficient_credits`
1022
- means the balance is too small for this run. `debt_outstanding` means the account
1023
- owes credits, and it can owe them while still holding a balance, so telling the
1024
- human "you do not have enough credits" would be false. Credits bought while a
1025
- debt stands clear the debt first and only then reach the balance. Clearing the
1026
- 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
1027
488
  credits of its own, so a purchase equal to the debt clears it and adds nothing to
1028
- the balance. Quote the debt figure, and say that credits go to the debt first: a
1029
- purchase adds to the balance only the part that exceeds the debt. Whether the
1030
- human has to buy more than the debt depends on what the balance already holds,
1031
- and this refusal does not say — the balance is not what blocks the run.
1032
-
1033
- Do not read a source into the refusal. A `429` or a `5xx` can come from the
1034
- Clipwright API itself or from anything between you and it, and the fields above
1035
- cannot tell you which — so the report does not claim one, and neither should you
1036
- 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.
1037
491
 
1038
- About `429` specifically: it does **not** mean no run was created. The rate limit
1039
- sits in front of the handler, so a repeat carrying an already-used idempotency key
1040
- gets one too. Retry with THE SAME key — a new key is a new paid render.
1041
-
1042
- **Nobody waits on your behalf here — you do the polling.** `get_run` makes one
1043
- call and hands you whatever came back, refusals included. So a `429` on a status
1044
- check is yours to sit out: wait `retry_after_seconds`, then call `get_run` again
1045
- with the same `run_id`. Do not treat it as a failed run and do not start a new
1046
- one — the run is already running and already paid for.
1047
-
1048
- (The SDK's own `makeUgc` does wait these out internally, which is why the CLI and
1049
- direct SDK users never see them. The MCP tools are deliberately thin: `make_ugc`
1050
- 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.
1051
495
 
1052
- ## 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.
1053
501
 
1054
- If the MCP path does not get you to a playable video, hit the same public REST
1055
- directly with curl, bypassing MCP, SDK and CLI entirely. If curl gets through, the
1056
- packaging layer is at fault; if it does not, production or the network is.
502
+ ## 7. On failure — plain curl splits the diagnosis
1057
503
 
1058
- **Two commands, no repository needed** (the package is installed from npm and
1059
- 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.
1060
507
 
1061
508
  ```bash
1062
- TOKEN=cw_... # the same value as CLIPWRIGHT_API_KEY, section 1.3
509
+ TOKEN=cw_... # the same value as CLIPWRIGHT_API_KEY
1063
510
  API=https://api.clipwright.io
1064
511
 
1065
- # 1. Start a run. The idempotency key is required and must be UNIQUE per call:
1066
- # a repeated key returns the earlier run instead of a new one (section 2).
1067
- # Generate it with Node, not uuidgen: Node is certainly here (the package is
1068
- # installed via npx), while uuidgen may be missing on a bare Linux image — and
1069
- # then the shell silently substitutes an empty string, the key becomes
1070
- # constant, and a second run returns the earlier one, ruining exactly the
1071
- # 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.
1072
520
  curl -sS -X POST "$API/v1/skills/make_ugc/run" \
1073
521
  -H "Authorization: Bearer $TOKEN" \
1074
522
  -H "Content-Type: application/json" \
1075
523
  -H "Idempotency-Key: cw-$(node -e 'process.stdout.write(crypto.randomUUID())')" \
1076
524
  -d '{"script":"Hello! This is a test line."}'
1077
525
 
1078
- # 2. Poll until terminal, every ~5–10 s.
526
+ # 2. Free: poll until terminal, every ~5–10 s.
1079
527
  curl -sS "$API/v1/runs/<run_id>" -H "Authorization: Bearer $TOKEN"
1080
528
  ```
1081
529
 
1082
- Reading the result:
1083
-
1084
- - **curl reaches `succeeded` with its own `run_id` and a playable link, but MCP
1085
- does not:** the root cause is in the MCP packaging layer (build, host config,
1086
- environment — section 1), not in the REST API and not in access or the token.
1087
- - **curl fails too:** the problem is not MCP. Check `$TOKEN` from the commands
1088
- above (shaped `cw_*`, not truncated when copied), network access to `$API`, and —
1089
- for the cloned-run story — that `CLIPWRIGHT_CLIENT_ID` really is unique on this
1090
- machine (section 2). If you get a 200 with a `run_id` that is not yours in
1091
- substance, that is the symptom of the collapse described in section 2 on the MCP
1092
- path; curl (which uses its own random `Idempotency-Key` per call) will not
1093
- reproduce it directly, but it is worth checking separately via `get_run` on that
1094
- `run_id`.
1095
-
1096
- The second request above — `GET /v1/runs/{id}` — is free: you can poll an existing
1097
- run as often as you like without making a new paid call.
1098
-
1099
- ## The whole sequence (what must reproduce)
1100
-
1101
- 1. Install the server: `npx -y -p @clipwright/mcp-server@latest clipwright-mcp`
1102
- starts and prints `running on stdio` (section 1.1). From source:
1103
- `pnpm install && pnpm build` in the repository root, then `dist/index.js`
1104
- exists.
1105
- 2. Generate a unique `CLIPWRIGHT_CLIENT_ID` (section 2) and put it, together with
1106
- `CLIPWRIGHT_API_KEY`, into the MCP host config (section 1.3).
1107
- 3. Restart the host and confirm `tools/list` returns 4 tools (`make_ugc`,
1108
- `quote_ugc`, `get_run`, `list_voices`).
1109
- 4. `quote_ugc` with a script → show the price AND read `source`,
1110
- `resolved_aspect_ratio`, `warnings[]` (section 3).
1111
- 5. `make_ugc` → get a `run_id` immediately.
1112
- 6. `get_run` every ~5–10 s, reading `state` along the way and `status` + `state` on
1113
- the terminal response, until `SUCCEEDED` (`video_url`) or `FAILED` (`error`).
1114
- 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).
1115
534
 
1116
535
  ### If you are a second user evaluating the product
1117
536
 
1118
- You have one task: **reach a playable video using this file, without spoken help
1119
- from the owner.** What is being tested is the completeness of the guide, not you —
1120
- so do not ask, write down where you got stuck.
1121
-
1122
- 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.
1123
540
 
1124
541
  1. **The token was issued on YOUR account, not merely as a separate token.** The
1125
- idempotency axis is `account_id`, and one account can hold several live tokens.
1126
- Your own account means another person's run cannot reach you at all. You cannot
1127
- check this from your side: the account behind a token is visible only to the
1128
- service. So the confirmation that the token was issued on a SEPARATE account
1129
- comes WITH the token — it is part of handing it over, not a question for the
1130
- owner. If it did not come, that is a gap in the instructions: write it down and
1131
- move on, no need to ask.
1132
- 2. **`CLIPWRIGHT_CLIENT_ID` was not copied from anyone's machine** (section 2). It
1133
- matters WITHIN an account: if you and the owner ended up on one account (even
1134
- under different tokens) AND your `clientId` matched, the server returns THE
1135
- OWNER'S RUN with a 200 and a playable link — a false success that looks more
1136
- convincing than a real one. It can match through a shared `HOME`, a copied
1137
- value, or a cloned image; a genuinely separate installation has its own even
1138
- without the environment variable.
1139
-
1140
- Tell two classes of failure apart, because they are fixed differently:
1141
-
1142
- - **`rejected_field`, `unknown_field`, `aspect_conflict`** — a contract doing its job, not a
1143
- defect. Read the error text: it names both the reason and the replacement;
1144
- - **everything else** (timeout, 5xx, empty response, tool not found) — a candidate
1145
- for a real defect. Here, run the same two curl commands from section 6: if curl
1146
- reaches a playable link and MCP does not, the cause is in the MCP packaging
1147
- 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.