@clipwright/mcp-server 0.1.0 → 0.2.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/LICENSE +1 -1
- package/SKILL.md +454 -337
- package/dist/format.js +44 -0
- package/dist/format.js.map +1 -1
- package/dist/server.js +10 -10
- package/dist/server.js.map +1 -1
- package/package.json +4 -4
package/SKILL.md
CHANGED
|
@@ -1,96 +1,93 @@
|
|
|
1
|
-
# SKILL.md —
|
|
1
|
+
# SKILL.md — installing and operating `clipwright-mcp`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
`apps/web/.claude/skills/*` —
|
|
6
|
-
|
|
3
|
+
A self-contained guide for an AGENT (or a human in Claude Code) that needs no
|
|
4
|
+
spoken help from a developer. Follow the steps in order. Not to be confused with
|
|
5
|
+
`apps/web/.claude/skills/*` — those are dashboard skills for the Next.js app and
|
|
6
|
+
have nothing to do with the MCP server.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
The package `@clipwright/mcp-server` (`bin: clipwright-mcp`) is published to npm
|
|
9
|
+
under the MIT license, so you do NOT need the repository to install it — `npx` is
|
|
10
|
+
enough. Building from source is kept below as the development path.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
`@clipwright/mcp-server` —
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
Four client-side packages — `@clipwright/core`, `@clipwright/sdk`,
|
|
13
|
+
`@clipwright/mcp-server` and `@clipwright/cli` — are MIT; the service itself
|
|
14
|
+
(REST API, render pipeline, dashboard) is closed. That split is deliberate, not
|
|
15
|
+
an oversight: see `LICENSE` in the repository root.
|
|
16
16
|
|
|
17
|
-
## 1.
|
|
17
|
+
## 1. Installation
|
|
18
18
|
|
|
19
|
-
### 1.1
|
|
19
|
+
### 1.1 From npm — the normal path
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
22
|
npx -y -p @clipwright/mcp-server@latest clipwright-mcp
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
`
|
|
27
|
-
|
|
25
|
+
Expected: the process does NOT exit, prints `clipwright mcp server running on
|
|
26
|
+
stdio` to stderr, and hangs waiting for JSON-RPC. That is what a healthy server
|
|
27
|
+
looks like, not a freeze. Stop it with `Ctrl+C`.
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
Exiting immediately with `CLIPWRIGHT_API_KEY env var is required` is also
|
|
30
|
+
expected when the key is empty (`src/index.ts`) — it is not a broken build.
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
Go straight to section 1.3: the host config for this variant differs only in the
|
|
33
|
+
command (`npx` instead of `node` with an absolute path); both are shown there.
|
|
34
34
|
|
|
35
|
-
### 1.2
|
|
35
|
+
### 1.2 From source — development only
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
`package.json` —
|
|
37
|
+
Requirements: Node ≥24, pnpm (`packageManager: pnpm@11.13.1` in the root
|
|
38
|
+
`package.json` — with corepack available: `corepack enable`).
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
git clone <repo-url> ugc-video-platform #
|
|
41
|
+
git clone <repo-url> ugc-video-platform # or use a checkout you already have
|
|
42
42
|
cd ugc-video-platform
|
|
43
43
|
pnpm install
|
|
44
|
-
pnpm build # pnpm -r build —
|
|
44
|
+
pnpm build # pnpm -r build — builds every package in topological order
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
автоматически):
|
|
47
|
+
If you only need this surface (faster, but `@clipwright/core` and
|
|
48
|
+
`@clipwright/sdk` must be built first — `...` pulls them in automatically):
|
|
50
49
|
|
|
51
50
|
```bash
|
|
52
51
|
pnpm --filter @clipwright/mcp-server... build
|
|
53
52
|
```
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
Проверь:
|
|
54
|
+
After the build, `packages/mcp-server/dist/index.js` must exist. Check it:
|
|
57
55
|
|
|
58
56
|
```bash
|
|
59
57
|
ls packages/mcp-server/dist/index.js
|
|
60
58
|
```
|
|
61
59
|
|
|
62
|
-
####
|
|
60
|
+
#### Pre-flight check of the built binary
|
|
63
61
|
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
This applies to source builds. Before wiring the server into a host, make sure it
|
|
63
|
+
starts at all and does not die on imports:
|
|
66
64
|
|
|
67
65
|
```bash
|
|
68
66
|
CLIPWRIGHT_API_KEY=cw_test node packages/mcp-server/dist/index.js
|
|
69
67
|
```
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
69
|
+
Expected: the process does NOT exit on its own, prints `clipwright mcp server
|
|
70
|
+
running on stdio` to stderr, and waits on stdio for JSON-RPC. Stop it with
|
|
71
|
+
`Ctrl+C`. If it instead exits immediately with `CLIPWRIGHT_API_KEY env var is
|
|
72
|
+
required`, the variable was not passed through (expected behaviour on an empty
|
|
73
|
+
key, see `src/index.ts`) — not a bug. If it dies on a module import, the build is
|
|
74
|
+
incomplete: go back to step 1.2 and run `pnpm build` from the root rather than a
|
|
75
|
+
targeted `--filter`, so `@clipwright/core` and `@clipwright/sdk` are definitely
|
|
76
|
+
rebuilt.
|
|
79
77
|
|
|
80
|
-
### 1.3
|
|
78
|
+
### 1.3 Connecting it to an MCP host
|
|
81
79
|
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
Variants A and B below run the server FROM SOURCE and therefore need the ABSOLUTE
|
|
81
|
+
path to `dist/index.js`:
|
|
84
82
|
|
|
85
83
|
```bash
|
|
86
84
|
realpath packages/mcp-server/dist/index.js
|
|
87
85
|
```
|
|
88
86
|
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
If you installed from npm (section 1.1), you do not need a path — take variant C
|
|
88
|
+
at the end of this section; it differs only in `command`/`args`.
|
|
91
89
|
|
|
92
|
-
|
|
93
|
-
`apps/web/.mcp.json` этого репо):**
|
|
90
|
+
**Variant A — `.mcp.json` (project-level host config):**
|
|
94
91
|
|
|
95
92
|
```json
|
|
96
93
|
{
|
|
@@ -98,28 +95,29 @@ realpath packages/mcp-server/dist/index.js
|
|
|
98
95
|
"clipwright": {
|
|
99
96
|
"type": "stdio",
|
|
100
97
|
"command": "node",
|
|
101
|
-
"args": ["
|
|
98
|
+
"args": ["/absolute/path/to/ugc-video-platform/packages/mcp-server/dist/index.js"],
|
|
102
99
|
"env": {
|
|
103
100
|
"CLIPWRIGHT_API_KEY": "cw_...",
|
|
104
101
|
"CLIPWRIGHT_API_URL": "https://api-production-7e69.up.railway.app",
|
|
105
|
-
"CLIPWRIGHT_CLIENT_ID": "
|
|
102
|
+
"CLIPWRIGHT_CLIENT_ID": "<see section 2 — generate BEFORE the first run>"
|
|
106
103
|
}
|
|
107
104
|
}
|
|
108
105
|
}
|
|
109
106
|
}
|
|
110
107
|
```
|
|
111
108
|
|
|
112
|
-
|
|
109
|
+
**Variant B — `claude mcp add`** (variant A as a single command):
|
|
113
110
|
|
|
114
111
|
```bash
|
|
115
112
|
claude mcp add clipwright \
|
|
116
113
|
-e CLIPWRIGHT_API_KEY=cw_... \
|
|
117
114
|
-e CLIPWRIGHT_API_URL=https://api-production-7e69.up.railway.app \
|
|
118
|
-
-e CLIPWRIGHT_CLIENT_ID="$(
|
|
119
|
-
-- node
|
|
115
|
+
-e CLIPWRIGHT_CLIENT_ID="$(node -e "console.log(crypto.randomUUID().replaceAll('-',''))")" \
|
|
116
|
+
-- node /absolute/path/to/ugc-video-platform/packages/mcp-server/dist/index.js
|
|
120
117
|
```
|
|
121
118
|
|
|
122
|
-
|
|
119
|
+
**Variant C — from npm, without the repository.** Same thing, but the command is
|
|
120
|
+
`npx`:
|
|
123
121
|
|
|
124
122
|
```json
|
|
125
123
|
{
|
|
@@ -131,171 +129,193 @@ claude mcp add clipwright \
|
|
|
131
129
|
"env": {
|
|
132
130
|
"CLIPWRIGHT_API_KEY": "cw_...",
|
|
133
131
|
"CLIPWRIGHT_API_URL": "https://api-production-7e69.up.railway.app",
|
|
134
|
-
"CLIPWRIGHT_CLIENT_ID": "
|
|
132
|
+
"CLIPWRIGHT_CLIENT_ID": "<see section 2 — generate BEFORE the first run>"
|
|
135
133
|
}
|
|
136
134
|
}
|
|
137
135
|
}
|
|
138
136
|
}
|
|
139
137
|
```
|
|
140
138
|
|
|
141
|
-
|
|
139
|
+
As a single command:
|
|
142
140
|
|
|
143
141
|
```bash
|
|
144
142
|
claude mcp add clipwright \
|
|
145
143
|
-e CLIPWRIGHT_API_KEY=cw_... \
|
|
146
144
|
-e CLIPWRIGHT_API_URL=https://api-production-7e69.up.railway.app \
|
|
147
|
-
-e CLIPWRIGHT_CLIENT_ID="$(
|
|
145
|
+
-e CLIPWRIGHT_CLIENT_ID="$(node -e "console.log(crypto.randomUUID().replaceAll('-',''))")" \
|
|
148
146
|
-- npx -y -p @clipwright/mcp-server@latest clipwright-mcp
|
|
149
147
|
```
|
|
150
148
|
|
|
151
|
-
**`@latest`
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
**`@latest` here is a deliberate choice, not sloppiness.** It pulls a fresh
|
|
150
|
+
version on every start: right for a prototype, because the contract still moves
|
|
151
|
+
and a client out of sync with the server costs more than an unexpected upgrade.
|
|
152
|
+
Once the contract settles, the version should be pinned.
|
|
155
153
|
|
|
156
|
-
|
|
154
|
+
Environment variables:
|
|
157
155
|
|
|
158
|
-
|
|
|
156
|
+
| Variable | Required | Meaning |
|
|
159
157
|
|---|---|---|
|
|
160
|
-
| `CLIPWRIGHT_API_KEY` |
|
|
161
|
-
| `CLIPWRIGHT_API_URL` |
|
|
162
|
-
| `CLIPWRIGHT_CLIENT_ID` |
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
## 2. `CLIPWRIGHT_CLIENT_ID` —
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
`
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
(`
|
|
179
|
-
|
|
180
|
-
`
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
158
|
+
| `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. |
|
|
159
|
+
| `CLIPWRIGHT_API_URL` | no | Production URL. Left unset, the SDK supplies `https://api-production-7e69.up.railway.app` itself. Set it explicitly only when you target another environment. |
|
|
160
|
+
| `CLIPWRIGHT_CLIENT_ID` | YES (see section 2) | Unique identifier of this INSTALLATION. |
|
|
161
|
+
|
|
162
|
+
After connecting, restart or reopen the MCP host and confirm that `tools/list`
|
|
163
|
+
returns four tools: `make_ugc`, `get_run`, `quote_ugc`, `list_voices`.
|
|
164
|
+
|
|
165
|
+
## 2. `CLIPWRIGHT_CLIENT_ID` — required, and WHY
|
|
166
|
+
|
|
167
|
+
**Rule: EVERY installation must get its OWN unique `CLIPWRIGHT_CLIENT_ID` BEFORE
|
|
168
|
+
the first `make_ugc` call.** Do not copy the value from another machine, and do
|
|
169
|
+
not leave it empty hoping the default will "work out somehow".
|
|
170
|
+
|
|
171
|
+
### Why this is not optional
|
|
172
|
+
|
|
173
|
+
The idempotency key the SDK stamps on every paid `POST /v1/skills/make_ugc/run`
|
|
174
|
+
is derived deterministically from **the hash of the input** (`script`, `person`,
|
|
175
|
+
`image`, … after canonicalisation) **and the client identity**
|
|
176
|
+
(`idempotencyKeyFor` in `packages/core/src/idempotency.ts`, used by
|
|
177
|
+
`packages/sdk/src/index.ts`). The server deduplicates on that key: if a stored
|
|
178
|
+
key matches on `request_hash`, it returns 200 with the run **created by the first
|
|
179
|
+
request that used the key**, instead of starting a new paid render.
|
|
180
|
+
|
|
181
|
+
That is the right behaviour when the SAME user retries — you should not pay twice
|
|
182
|
+
for an agent's accidental double click. Its boundaries are these.
|
|
183
|
+
|
|
184
|
+
Collapsing requires BOTH to match: the account and the `clientId`. Separating
|
|
185
|
+
either one is enough.
|
|
186
|
+
|
|
187
|
+
- **The account separates by construction.** Both the unique index
|
|
188
|
+
(`runs_account_idempotency_key_unique`) and the lookup use the pair
|
|
189
|
+
`(account_id, idempotency_key)`. A run from ANOTHER account can never reach you,
|
|
190
|
+
and nobody can take your key — whatever sits in `CLIPWRIGHT_CLIENT_ID`.
|
|
191
|
+
**The axis is the account, not the token.** A separate token does not by itself
|
|
192
|
+
mean a separate account: several live tokens on one account are normal (laptop,
|
|
193
|
+
CI, smoke script), and they all share one idempotency axis. The account is not
|
|
194
|
+
visible from the token — only the service side knows it, so "is this my own
|
|
195
|
+
account?" is confirmed by whoever issued the token.
|
|
196
|
+
- **`clientId` separates within one account.** On its own it comes from
|
|
197
|
+
`~/.clipwright/client-id` and is born random on first run, so two genuinely
|
|
198
|
+
separate installations diverge even without the environment variable. It can
|
|
199
|
+
only match if it was MADE to match: a shared `HOME`, a value copied from
|
|
200
|
+
someone else's machine, an image cloned together with the file.
|
|
201
|
+
|
|
202
|
+
The dangerous case is exactly the crossing of those two conditions: **one account
|
|
203
|
+
AND one `clientId`**. Then a request whose input matches an earlier one — for
|
|
204
|
+
example, both people copied a sample from this SKILL.md verbatim — returns 200, a
|
|
205
|
+
`run_id` and a playable link. But it is the video rendered for the first person,
|
|
206
|
+
presented to the second as their own success. A diagnostic tool would read as
|
|
207
|
+
"works" while no paid render ever ran for the second person.
|
|
208
|
+
|
|
209
|
+
Hence the rule: **a second person gets their own ACCOUNT and a token issued on
|
|
210
|
+
it**, and the value of `CLIPWRIGHT_CLIENT_ID` is not copied from anyone's machine.
|
|
211
|
+
Setting it explicitly is mandatory wherever the file resolver cannot give a stable
|
|
212
|
+
value of its own: a read-only or shared `HOME`, a container from a cloned image,
|
|
213
|
+
CI.
|
|
214
|
+
|
|
215
|
+
### How to set it
|
|
216
|
+
|
|
217
|
+
An explicit `CLIPWRIGHT_CLIENT_ID` in the host environment (section 1.3, variants
|
|
218
|
+
A/B/C) is the most reliable path, because it does not depend on the state of the
|
|
219
|
+
file system. The value is any unique string; for consistency with the SDK's own
|
|
220
|
+
generator use the same shape (a UUID without dashes). Generate it with Node, not
|
|
221
|
+
`uuidgen`: Node is certainly present here, whereas `uuidgen` may be missing on a
|
|
222
|
+
bare Linux image — and then the shell silently substitutes an EMPTY string, which
|
|
223
|
+
switches off precisely the protection the variable exists for.
|
|
211
224
|
|
|
212
225
|
```bash
|
|
213
|
-
|
|
226
|
+
node -e "console.log(crypto.randomUUID().replaceAll('-',''))"
|
|
214
227
|
```
|
|
215
228
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
`~/.clipwright/client-id`
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
229
|
+
If the variable is NOT set, `resolveClientId()` (`packages/sdk/src/client-id.ts`)
|
|
230
|
+
falls back to the file `~/.clipwright/client-id`: on first run it generates a
|
|
231
|
+
random id and stores it there, then reads it back. That is a workable fallback for
|
|
232
|
+
a single installation on a clean machine, but do NOT lean on it as your only
|
|
233
|
+
protection when an agent does the install: home directories carried along when
|
|
234
|
+
images, containers or dev environments are cloned can take
|
|
235
|
+
`~/.clipwright/client-id` with them and produce a collision this fallback will not
|
|
236
|
+
notice. When installing by this guide, **always set `CLIPWRIGHT_CLIENT_ID`
|
|
237
|
+
explicitly in the host environment** rather than relying on auto-generation.
|
|
238
|
+
|
|
239
|
+
If neither the variable nor writing the file is possible (read-only `$HOME`),
|
|
240
|
+
`resolveClientId()` deliberately THROWS instead of inventing a throwaway id on
|
|
241
|
+
every start — a silent fallback would disable deduplication entirely
|
|
242
|
+
(`packages/sdk/src/client-id.ts`).
|
|
243
|
+
|
|
244
|
+
**The server does NOT die in that case, and this is verified by a test run.** The
|
|
245
|
+
identity resolves lazily — only on the first `make_ugc`. In an environment where
|
|
246
|
+
`~/.clipwright` is not writable the server starts, `tools/list` returns all four
|
|
247
|
+
tools, and `quote_ugc`, `get_run` and `list_voices` work: they do not need the
|
|
248
|
+
identity. Only `make_ugc` refuses, and its refusal names the variable that fixes
|
|
249
|
+
it. So the action to take is to set `CLIPWRIGHT_CLIENT_ID` explicitly — not to
|
|
250
|
+
hunt for a bug and not to reinstall the package.
|
|
251
|
+
|
|
252
|
+
## 3. CALL ORDER — `quote_ugc` first, `make_ugc` second
|
|
253
|
+
|
|
254
|
+
This is not a politeness convention but a mechanism, without which the contract
|
|
255
|
+
stays silent about its own decisions.
|
|
236
256
|
|
|
237
257
|
```
|
|
238
|
-
quote_ugc →
|
|
258
|
+
quote_ugc → read source / resolved_aspect_ratio / warnings → make_ugc → get_run (polling)
|
|
239
259
|
```
|
|
240
260
|
|
|
241
|
-
|
|
242
|
-
|
|
261
|
+
**What `quote_ugc` gives you that nothing else does.** It is free, reserves
|
|
262
|
+
nothing, and answers the questions that would otherwise surface only after money
|
|
263
|
+
is spent:
|
|
243
264
|
|
|
244
|
-
|
|
|
265
|
+
| Response field | What it tells you |
|
|
245
266
|
|---|---|
|
|
246
|
-
| `credits_estimate` |
|
|
247
|
-
| `source` |
|
|
248
|
-
| `resolved_aspect_ratio` |
|
|
249
|
-
| `warnings[]` |
|
|
250
|
-
| `contract_version` |
|
|
267
|
+
| `credits_estimate` | the price — show it to the user BEFORE generating |
|
|
268
|
+
| `source` | dimensions of the source frame: a probed `image`, or the default actor (432×768) |
|
|
269
|
+
| `resolved_aspect_ratio` | the aspect ratio that will ACTUALLY be rendered for this input |
|
|
270
|
+
| `warnings[]` | which of the parameters you passed will not be honoured |
|
|
271
|
+
| `contract_version` | which contract version this price was quoted under |
|
|
251
272
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
273
|
+
**The verdict of `quote_ugc` and the verdict of `make_ugc` agree by
|
|
274
|
+
construction** — both surfaces call the same function. If quote returns 400, the
|
|
275
|
+
run returns the same 400; if quote showed `resolved_aspect_ratio: "9:16"` for a
|
|
276
|
+
requested `1:1`, then 9:16 is what gets rendered.
|
|
256
277
|
|
|
257
|
-
|
|
258
|
-
|
|
278
|
+
**Rejected fields — do not try to pass them.** They are not declared in the tool
|
|
279
|
+
schema, and the server answers 400 `rejected_field` before a run is even created:
|
|
259
280
|
|
|
260
|
-
|
|
|
281
|
+
| Field | Use instead |
|
|
261
282
|
|---|---|
|
|
262
|
-
| `character` | `person` —
|
|
263
|
-
| `webhook_url` |
|
|
264
|
-
| `segments`, `broll_url` |
|
|
265
|
-
| `disclosure_overlay` |
|
|
266
|
-
| `image`
|
|
283
|
+
| `character` | `person` — describe the actor in words, or nothing (default actor) |
|
|
284
|
+
| `webhook_url` | poll `get_run` — webhooks are not delivered at all |
|
|
285
|
+
| `segments`, `broll_url` | a single `script`; segmented clips are not available yet |
|
|
286
|
+
| `disclosure_overlay` | nothing: the AI disclosure is already in the run response and in the file |
|
|
287
|
+
| `image` with the killswitch off | omit it — the render runs on the default actor |
|
|
267
288
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
289
|
+
**The aspect ratio can be refused.** Requesting `aspect_ratio: "1:1"` without an
|
|
290
|
+
`image` returns a refusal: the default actor is vertical (432×768), and a square
|
|
291
|
+
frame could only come from the vendor's centre crop, which we do not do silently.
|
|
292
|
+
To get `1:1`, pass an `image` with a square public https source.
|
|
272
293
|
|
|
273
|
-
## 4.
|
|
294
|
+
## 4. The four tools
|
|
274
295
|
|
|
275
|
-
|
|
276
|
-
(`packages/core/src/skills.ts`,
|
|
277
|
-
|
|
296
|
+
Schemas come from the single source of truth `@clipwright/core`
|
|
297
|
+
(`packages/core/src/skills.ts`, barrel `packages/core/src/index.ts`); MCP does not
|
|
298
|
+
retype them by hand.
|
|
278
299
|
|
|
279
|
-
### `quote_ugc` —
|
|
300
|
+
### `quote_ugc` — free
|
|
280
301
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
302
|
+
The input is `offeredUgcInputShape` (`packages/mcp-server/src/server.ts`), that
|
|
303
|
+
is, the contract MINUS the rejected fields: they simply are not there, and listing
|
|
304
|
+
them here would mean offering what the server punishes you for (section 3).
|
|
284
305
|
|
|
285
|
-
`script` (≤10000
|
|
286
|
-
`.optional()`,
|
|
287
|
-
|
|
288
|
-
|
|
306
|
+
`script` (≤10000 chars, word count ≤`MAX_SCRIPT_WORDS` ≈45 s of speech; formally
|
|
307
|
+
`.optional()`, but required while `segments` is rejected), optionally `person` OR
|
|
308
|
+
`image` (a public `https` URL), plus `name`, `captions` (defaults to `false` —
|
|
309
|
+
**captions are opt-in, ASK THE USER FIRST**), `caption_style`
|
|
289
310
|
(`hormozi`/`tiktok`/`minimal`), `look` (`natural`/`commercial`/`raw_iphone`),
|
|
290
|
-
`aspect_ratio` (`9:16`/`1:1`,
|
|
291
|
-
`resolution` (`720p`/`1080p`/`4k`), `voice`/`voice_id`.
|
|
311
|
+
`aspect_ratio` (`9:16`/`1:1`, with NO default — silence and an explicit choice
|
|
312
|
+
differ), `resolution` (`720p`/`1080p`/`4k`), `voice`/`voice_id`.
|
|
292
313
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
314
|
+
The exact list is always available from the tool schema itself: `tools/list` is
|
|
315
|
+
generated from the same shape, and fields that are not honoured carry
|
|
316
|
+
`NOT HONORED YET` right in their own `description`.
|
|
296
317
|
|
|
297
|
-
|
|
298
|
-
(`packages/mcp-server/src/server.ts`):
|
|
318
|
+
Spends no credits. The response is the raw `quote` in full:
|
|
299
319
|
|
|
300
320
|
```json
|
|
301
321
|
{
|
|
@@ -306,20 +326,19 @@ quote_ugc → прочитать source / resolved_aspect_ratio / warnings
|
|
|
306
326
|
}
|
|
307
327
|
```
|
|
308
328
|
|
|
309
|
-
`warnings[]`
|
|
310
|
-
|
|
311
|
-
|
|
329
|
+
`warnings[]` IS present here (passed through unfiltered) — read it: parameters
|
|
330
|
+
that will not be honoured (for example, `captions` ignored in this build) are
|
|
331
|
+
declared there rather than dropped silently.
|
|
312
332
|
|
|
313
|
-
|
|
314
|
-
(
|
|
333
|
+
**Always call `quote_ugc` first and show the user the price before `make_ugc`**
|
|
334
|
+
(the tool description says so in the imperative).
|
|
315
335
|
|
|
316
|
-
### `make_ugc` —
|
|
336
|
+
### `make_ugc` — paid, creates a run
|
|
317
337
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
(`packages/mcp-server/src/index.ts:28-29,39-40`):
|
|
338
|
+
The input is the same shape plus `attempt` (an integer ≥1, optional; see section
|
|
339
|
+
5). Money: this is ONE paid render per call, unless idempotency deduplication kicks
|
|
340
|
+
in (section 2). **The tool does NOT wait for the video** — it starts the run and
|
|
341
|
+
returns a `run_id` immediately:
|
|
323
342
|
|
|
324
343
|
```json
|
|
325
344
|
{
|
|
@@ -331,36 +350,35 @@ quote_ugc → прочитать source / resolved_aspect_ratio / warnings
|
|
|
331
350
|
}
|
|
332
351
|
```
|
|
333
352
|
|
|
334
|
-
|
|
335
|
-
|
|
353
|
+
After that you MUST poll `get_run` with that `run_id` every ~5–10 seconds until it
|
|
354
|
+
reaches a terminal state. Do not tell the user the video is ready until you have a
|
|
336
355
|
`video_url`.
|
|
337
356
|
|
|
338
|
-
####
|
|
339
|
-
|
|
340
|
-
Два взаимоисключающих опциональных поля во входе `make_ugc`:
|
|
357
|
+
#### Choosing a voice (`voice` / `voice_id`)
|
|
341
358
|
|
|
342
|
-
|
|
343
|
-
`list_voices` (бесплатный): `owner_ru_clone`, `sarah`, `george`, `eric`,
|
|
344
|
-
`daria_ru_female`. Валидируется на границе: неизвестное имя → 400 сразу,
|
|
345
|
-
платный вызов не создаётся.
|
|
346
|
-
- `voice_id` — **сырой ElevenLabs id** (20-символьный), escape hatch для
|
|
347
|
-
голосов ВНЕ каталога, включая клонированные. Проверяется бесплатно против
|
|
348
|
-
аккаунта в начале рана (не на `quote`): несуществующий id → ран `failed` ДО
|
|
349
|
-
единого платного вызова.
|
|
350
|
-
- **Ни одного не задавай без просьбы пользователя.** Голос по умолчанию уже
|
|
351
|
-
подобран под текущий аватар; менять его не нужно. `list_voices` — курируемый
|
|
352
|
-
каталог по дизайну, не полный список голосов вендора.
|
|
359
|
+
Two mutually exclusive optional fields in the `make_ugc` input:
|
|
353
360
|
|
|
354
|
-
|
|
361
|
+
- `voice` — the name of a **curated preset**. **The recommended path.** The list
|
|
362
|
+
comes from the free `list_voices` tool: `owner_ru_clone`, `sarah`, `george`,
|
|
363
|
+
`eric`, `daria_ru_female`. Validated at the boundary: an unknown name returns
|
|
364
|
+
400 immediately and no paid call is made.
|
|
365
|
+
- `voice_id` — a **raw ElevenLabs id** (20 characters), an escape hatch for voices
|
|
366
|
+
OUTSIDE the catalog, cloned ones included. It is checked for free against the
|
|
367
|
+
account at the start of the run (not during `quote`): a non-existent id fails the
|
|
368
|
+
run BEFORE any paid call.
|
|
369
|
+
- **Set neither unless the user asks.** The default voice is already matched to
|
|
370
|
+
the current avatar; there is no need to change it. `list_voices` is a curated
|
|
371
|
+
catalog by design, not the vendor's full voice list.
|
|
355
372
|
|
|
356
|
-
### `get_run` —
|
|
373
|
+
### `get_run` — status polling
|
|
357
374
|
|
|
358
|
-
|
|
359
|
-
`packages/mcp-server/src/format.ts` (`formatGetRun`)
|
|
360
|
-
|
|
375
|
+
Input: `{ "run_id": "run_..." }`. The response shape is implemented in
|
|
376
|
+
`packages/mcp-server/src/format.ts` (`formatGetRun`) and is given below as it
|
|
377
|
+
actually is.
|
|
361
378
|
|
|
362
|
-
|
|
363
|
-
|
|
379
|
+
**A non-terminal run**
|
|
380
|
+
(`queued`/`scripting`/`tts`/`avatar`/`compositing`/`uploading`) carries the stage
|
|
381
|
+
in `state`, with `status` always `"IN_PROGRESS"`:
|
|
364
382
|
|
|
365
383
|
```json
|
|
366
384
|
{
|
|
@@ -372,9 +390,7 @@ quote_ugc → прочитать source / resolved_aspect_ratio / warnings
|
|
|
372
390
|
}
|
|
373
391
|
```
|
|
374
392
|
|
|
375
|
-
|
|
376
|
-
(до US-501 `state` в терминальной успешной ветке отсутствовал — грабля №4
|
|
377
|
-
`docs/14-gate4-heygen.md:75-79` — теперь это исправлено, поле есть всегда):
|
|
393
|
+
**Terminal success** carries BOTH `status: "SUCCEEDED"` AND `state: "succeeded"`:
|
|
378
394
|
|
|
379
395
|
```json
|
|
380
396
|
{
|
|
@@ -386,155 +402,256 @@ quote_ugc → прочитать source / resolved_aspect_ratio / warnings
|
|
|
386
402
|
}
|
|
387
403
|
```
|
|
388
404
|
|
|
389
|
-
|
|
405
|
+
**Terminal failure** carries BOTH `status: "FAILED"` AND `state: "failed"`:
|
|
390
406
|
|
|
391
407
|
```json
|
|
392
408
|
{
|
|
393
409
|
"status": "FAILED",
|
|
394
410
|
"run_id": "run_...",
|
|
395
411
|
"state": "failed",
|
|
396
|
-
"error": "
|
|
412
|
+
"error": "<error text>"
|
|
397
413
|
}
|
|
398
414
|
```
|
|
399
415
|
|
|
400
|
-
**`warnings[]`
|
|
401
|
-
`FAILED`)
|
|
402
|
-
|
|
403
|
-
|
|
416
|
+
**`warnings[]` in `get_run`:** every branch of the response (`IN_PROGRESS`,
|
|
417
|
+
`SUCCEEDED`, `FAILED`) carries the run's `warnings` — parameters that were not
|
|
418
|
+
honoured are always declared, never dropped silently. Read it on the terminal
|
|
419
|
+
response without fail: for instance, missing captions in the prototype are
|
|
420
|
+
announced exactly there.
|
|
404
421
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
`
|
|
408
|
-
|
|
409
|
-
|
|
422
|
+
**An unknown terminal status on the server** (say a hypothetical `"canceled"` that
|
|
423
|
+
is not in `TERMINAL_STATES`) is a deliberate degradation: `isTerminal` returns
|
|
424
|
+
`false`, so such a run lands in the `IN_PROGRESS` branch and the agent is told to
|
|
425
|
+
keep polling a run that has already finished. That is accepted prototype behaviour
|
|
426
|
+
— MCP and the API ship as one version — not a formatter bug.
|
|
410
427
|
|
|
411
|
-
### `list_voices` —
|
|
428
|
+
### `list_voices` — free, no input
|
|
412
429
|
|
|
413
|
-
|
|
414
|
-
(`
|
|
415
|
-
аргументов нет). Денег не стоит и рана не создаёт.
|
|
430
|
+
Returns the curated preset catalog for the `voice` field of `make_ugc`
|
|
431
|
+
(`inputSchema: {}` — it takes no arguments). Costs nothing and creates no run.
|
|
416
432
|
|
|
417
|
-
|
|
418
|
-
`CLIPWRIGHT_API_KEY`,
|
|
419
|
-
|
|
420
|
-
|
|
433
|
+
There is still a network call: the tool hits OUR `/v1/voices` and needs a working
|
|
434
|
+
`CLIPWRIGHT_API_KEY`, like the other three. Nobody calls the vendor — the list is
|
|
435
|
+
static and lives in `packages/core/src/voices.ts`, so no vendor key is needed on
|
|
436
|
+
this surface.
|
|
421
437
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
438
|
+
Call it before `make_ugc` when the user asks for "a different voice": a preset
|
|
439
|
+
name is preferable to a raw `voice_id`, because a preset survives a change of
|
|
440
|
+
vendor and an identifier does not.
|
|
425
441
|
|
|
426
|
-
## 5.
|
|
442
|
+
## 5. Retrying a failed run is NOT a permanent refusal
|
|
427
443
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
недостижимо.
|
|
444
|
+
If `get_run` returned a terminal `FAILED` and you call `make_ugc` AGAIN with the
|
|
445
|
+
SAME input (same `script` and options) and WITHOUT changing `attempt`, the server
|
|
446
|
+
returns THAT SAME terminal failed run as it is (same error, same `run_id`) instead
|
|
447
|
+
of starting a new paid render. That is idempotency deduplication (section 2), not
|
|
448
|
+
a sign that the video is forever out of reach.
|
|
434
449
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
(`packages/mcp-server/src/index.ts:29-30`):
|
|
450
|
+
**To actually retry, pass `attempt` one higher than before** (the first call is
|
|
451
|
+
`attempt=1` by default; the second attempt is `attempt: 2`, the third `attempt: 3`,
|
|
452
|
+
and so on). This is the behaviour documented verbatim in the `make_ugc` tool
|
|
453
|
+
description:
|
|
440
454
|
|
|
441
455
|
> "Pass attempt=2,3,… to deliberately start a NEW run for the same input
|
|
442
456
|
> (retry after a failure)."
|
|
443
457
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
458
|
+
Technically `attempt` does not go into the render request body but into the SDK
|
|
459
|
+
options, and becomes a `:N` suffix on the idempotency key — a different suffix
|
|
460
|
+
gives a different key, so the server sees a NEW request, which means a new
|
|
461
|
+
`run_id` and a new paid vendor call.
|
|
462
|
+
|
|
463
|
+
Do not confuse this with `CLIPWRIGHT_CLIENT_ID`: `attempt` is the agent's
|
|
464
|
+
deliberate "this is a new attempt", while `clientId` is the fixed identity of the
|
|
465
|
+
installation and must NOT change between attempts.
|
|
466
|
+
|
|
467
|
+
### ⚠️ EVERY `attempt` BUMP COSTS MONEY
|
|
468
|
+
|
|
469
|
+
A new `attempt` is a new run and a **new paid vendor call**, not a free resend.
|
|
470
|
+
The loop "failed → bump → failed → bump" burns real credits on every iteration.
|
|
471
|
+
|
|
472
|
+
So:
|
|
473
|
+
|
|
474
|
+
- **read the `error` of the failed run before retrying.** Refusals such as
|
|
475
|
+
`rejected_field`, `aspect_conflict` or `daily_cap_exceeded` are NOT fixed by
|
|
476
|
+
retrying — they are determined by the input or by a cap, and the next attempt
|
|
477
|
+
fails the same way, only now for money;
|
|
478
|
+
- **bump `attempt` only for a refusal that looks temporary** (vendor timeout, 5xx,
|
|
479
|
+
a dropped connection);
|
|
480
|
+
- **the cap exists and it is daily.** Exhausting it gives you
|
|
481
|
+
`daily_cap_exceeded` — that is a protection doing its job, not a broken product.
|
|
482
|
+
Wait for the next day or ask the owner to raise the limit.
|
|
483
|
+
|
|
484
|
+
## 5.1. Server refusals are read from fields, not guessed (0.2.0)
|
|
485
|
+
|
|
486
|
+
Since 0.2.0 an API refusal carries structured fields instead of one free-form
|
|
487
|
+
sentence. It still arrives the way every tool result does — as JSON **inside the
|
|
488
|
+
text block**, with `isError: true`:
|
|
489
|
+
|
|
490
|
+
```ts
|
|
491
|
+
{ content: [{ type: "text", text: "{\"status\":\"FAILED\", …}" }], isError: true }
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
So `JSON.parse(content[0].text)` first, then read the fields below. Reading
|
|
495
|
+
`content[0].balance_credits` gives `undefined`: the numbers are inside the parsed
|
|
496
|
+
text, not beside it. Do not relay the raw text to the user and do not guess from
|
|
497
|
+
it.
|
|
498
|
+
|
|
499
|
+
```json
|
|
500
|
+
{
|
|
501
|
+
"status": "RETRY_LATER" | "FAILED",
|
|
502
|
+
"code": "rate_limited" | "insufficient_credits" | "debt_outstanding" | "http_503" | …,
|
|
503
|
+
"message": "<human-readable text from the server>",
|
|
504
|
+
"retryable": true | false,
|
|
505
|
+
"next_action": "<what to do right now>",
|
|
506
|
+
"retry_after_seconds": 7,
|
|
507
|
+
"balance_credits": 3,
|
|
508
|
+
"required_credits": 8,
|
|
509
|
+
"debt_credits": 120
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Numeric fields appear only where the refusal actually has them:
|
|
514
|
+
|
|
515
|
+
| Field | Appears on |
|
|
516
|
+
| --- | --- |
|
|
517
|
+
| `retry_after_seconds` | `rate_limited`, and any `http_5xx` where the server named a pause |
|
|
518
|
+
| `limit`, `window_seconds` | only when the 429 body followed the Clipwright contract; a 429 whose body did not carries neither |
|
|
519
|
+
| `balance_credits`, `required_credits` | `insufficient_credits` only |
|
|
520
|
+
| `debt_credits` | `debt_outstanding` only |
|
|
521
|
+
|
|
522
|
+
Treat every one of them as optional and read what is there. In particular, do not
|
|
523
|
+
expect balance numbers on `debt_outstanding`: that refusal reports the debt, and
|
|
524
|
+
the balance is not what blocks the run.
|
|
525
|
+
|
|
526
|
+
**One rule: look at `retryable`.**
|
|
527
|
+
|
|
528
|
+
- `retryable: true` (`rate_limited`, 5xx) — wait `retry_after_seconds` and repeat
|
|
529
|
+
**THE SAME call with the same arguments**. Do not bump `attempt`: this is not a
|
|
530
|
+
failed run but a refused request, and a new `attempt` would mean a second paid
|
|
531
|
+
render (section 5).
|
|
532
|
+
- `retryable: false` (`insufficient_credits`, `debt_outstanding`, other 4xx) —
|
|
533
|
+
**stop and tell the human**. A repeat gives the same answer while eating into
|
|
534
|
+
the rate limit. For money refusals, quote the NUMBERS: "the account has 3
|
|
535
|
+
credits, this needs 8" — without them the human cannot tell how much to buy.
|
|
536
|
+
|
|
537
|
+
The two money codes differ in their cure and must not be conflated:
|
|
538
|
+
`insufficient_credits` means "top up the balance", `debt_outstanding` means
|
|
539
|
+
"settle the debt", and topping up does NOT clear a debt. Advising "buy credits" on
|
|
540
|
+
the second code sends the human to spend money for nothing.
|
|
449
541
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
542
|
+
Do not read a source into the refusal. A `429` or a `5xx` can come from the
|
|
543
|
+
Clipwright API itself or from anything between you and it, and the fields above
|
|
544
|
+
cannot tell you which — so the report does not claim one, and neither should you
|
|
545
|
+
when relaying it. What is certain is the code and what to do about it.
|
|
453
546
|
|
|
454
|
-
|
|
547
|
+
About `429` specifically: it does **not** mean no run was created. The rate limit
|
|
548
|
+
sits in front of the handler, so a repeat carrying an already-used idempotency key
|
|
549
|
+
gets one too. Retry with THE SAME key — a new key is a new paid render.
|
|
455
550
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
551
|
+
**Nobody waits on your behalf here — you do the polling.** `get_run` makes one
|
|
552
|
+
call and hands you whatever came back, refusals included. So a `429` on a status
|
|
553
|
+
check is yours to sit out: wait `retry_after_seconds`, then call `get_run` again
|
|
554
|
+
with the same `run_id`. Do not treat it as a failed run and do not start a new
|
|
555
|
+
one — the run is already running and already paid for.
|
|
459
556
|
|
|
460
|
-
|
|
557
|
+
(The SDK's own `makeUgc` does wait these out internally, which is why the CLI and
|
|
558
|
+
direct SDK users never see them. The MCP tools are deliberately thin: `make_ugc`
|
|
559
|
+
returns as soon as the run starts, so a long render never blocks your turn.)
|
|
461
560
|
|
|
462
|
-
|
|
463
|
-
`rejected_field`, `aspect_conflict`, `daily_cap_exceeded` повтором НЕ
|
|
464
|
-
чинятся — они детерминированы входом или потолком, и следующая попытка
|
|
465
|
-
упадёт так же, только уже за деньги;
|
|
466
|
-
- **бампай `attempt` только при отказе, похожем на временный** (таймаут
|
|
467
|
-
вендора, 5xx, обрыв сети);
|
|
468
|
-
- **потолок существует и он суточный.** Исчерпав его, ты получишь
|
|
469
|
-
`daily_cap_exceeded` — это сработавшая защита, а не поломка продукта.
|
|
470
|
-
Дожидаться следующих суток или просить владельца поднять лимит.
|
|
561
|
+
## 6. On failure — plain curl as a diagnosis splitter
|
|
471
562
|
|
|
472
|
-
|
|
563
|
+
If the MCP path does not get you to a playable video, hit the same public REST
|
|
564
|
+
directly with curl, bypassing MCP, SDK and CLI entirely. If curl gets through, the
|
|
565
|
+
packaging layer is at fault; if it does not, production or the network is.
|
|
473
566
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
`POST /v1/skills/make_ugc/run` → `GET /v1/runs/{id}` напрямую curl'ом, в обход
|
|
477
|
-
MCP/SDK/CLI целиком:
|
|
567
|
+
**Two commands, no repository needed** (the package is installed from npm and
|
|
568
|
+
carries no scripts):
|
|
478
569
|
|
|
479
570
|
```bash
|
|
480
|
-
|
|
571
|
+
TOKEN=cw_... # the same value as CLIPWRIGHT_API_KEY, section 1.3
|
|
572
|
+
API=https://api-production-7e69.up.railway.app
|
|
573
|
+
|
|
574
|
+
# 1. Start a run. The idempotency key is required and must be UNIQUE per call:
|
|
575
|
+
# a repeated key returns the earlier run instead of a new one (section 2).
|
|
576
|
+
# Generate it with Node, not uuidgen: Node is certainly here (the package is
|
|
577
|
+
# installed via npx), while uuidgen may be missing on a bare Linux image — and
|
|
578
|
+
# then the shell silently substitutes an empty string, the key becomes
|
|
579
|
+
# constant, and a second run returns the earlier one, ruining exactly the
|
|
580
|
+
# diagnosis you started it for.
|
|
581
|
+
curl -sS -X POST "$API/v1/skills/make_ugc/run" \
|
|
582
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
583
|
+
-H "Content-Type: application/json" \
|
|
584
|
+
-H "Idempotency-Key: cw-$(node -e 'process.stdout.write(crypto.randomUUID())')" \
|
|
585
|
+
-d '{"script":"Hello! This is a test line."}'
|
|
586
|
+
|
|
587
|
+
# 2. Poll until terminal, every ~5–10 s.
|
|
588
|
+
curl -sS "$API/v1/runs/<run_id>" -H "Authorization: Bearer $TOKEN"
|
|
481
589
|
```
|
|
482
590
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
- **curl
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
(
|
|
494
|
-
|
|
495
|
-
`
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
`
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
591
|
+
Reading the result:
|
|
592
|
+
|
|
593
|
+
- **curl reaches `succeeded` with its own `run_id` and a playable link, but MCP
|
|
594
|
+
does not:** the root cause is in the MCP packaging layer (build, host config,
|
|
595
|
+
environment — section 1), not in the REST API and not in access or the token.
|
|
596
|
+
- **curl fails too:** the problem is not MCP. Check `$TOKEN` from the commands
|
|
597
|
+
above (shaped `cw_*`, not truncated when copied), network access to `$API`, and —
|
|
598
|
+
for the cloned-run story — that `CLIPWRIGHT_CLIENT_ID` really is unique on this
|
|
599
|
+
machine (section 2). If you get a 200 with a `run_id` that is not yours in
|
|
600
|
+
substance, that is the symptom of the collapse described in section 2 on the MCP
|
|
601
|
+
path; curl (which uses its own random `Idempotency-Key` per call) will not
|
|
602
|
+
reproduce it directly, but it is worth checking separately via `get_run` on that
|
|
603
|
+
`run_id`.
|
|
604
|
+
|
|
605
|
+
The second request above — `GET /v1/runs/{id}` — is free: you can poll an existing
|
|
606
|
+
run as often as you like without making a new paid call.
|
|
607
|
+
|
|
608
|
+
## The whole sequence (what must reproduce)
|
|
609
|
+
|
|
610
|
+
1. Install the server: `npx -y -p @clipwright/mcp-server@latest clipwright-mcp`
|
|
611
|
+
starts and prints `running on stdio` (section 1.1). From source:
|
|
612
|
+
`pnpm install && pnpm build` in the repository root, then `dist/index.js`
|
|
613
|
+
exists.
|
|
614
|
+
2. Generate a unique `CLIPWRIGHT_CLIENT_ID` (section 2) and put it, together with
|
|
615
|
+
`CLIPWRIGHT_API_KEY`, into the MCP host config (section 1.3).
|
|
616
|
+
3. Restart the host and confirm `tools/list` returns 4 tools (`make_ugc`,
|
|
617
|
+
`quote_ugc`, `get_run`, `list_voices`).
|
|
618
|
+
4. `quote_ugc` with a script → show the price AND read `source`,
|
|
619
|
+
`resolved_aspect_ratio`, `warnings[]` (section 3).
|
|
620
|
+
5. `make_ugc` → get a `run_id` immediately.
|
|
621
|
+
6. `get_run` every ~5–10 s, reading `state` along the way and `status` + `state` on
|
|
622
|
+
the terminal response, until `SUCCEEDED` (`video_url`) or `FAILED` (`error`).
|
|
623
|
+
7. On failure — the same two curl commands from section 6 to split the diagnosis.
|
|
624
|
+
|
|
625
|
+
### If you are a second user evaluating the product
|
|
626
|
+
|
|
627
|
+
You have one task: **reach a playable video using this file, without spoken help
|
|
628
|
+
from the owner.** What is being tested is the completeness of the guide, not you —
|
|
629
|
+
so do not ask, write down where you got stuck.
|
|
630
|
+
|
|
631
|
+
Before you start, make sure of two different things.
|
|
632
|
+
|
|
633
|
+
1. **The token was issued on YOUR account, not merely as a separate token.** The
|
|
634
|
+
idempotency axis is `account_id`, and one account can hold several live tokens.
|
|
635
|
+
Your own account means another person's run cannot reach you at all. You cannot
|
|
636
|
+
check this from your side: the account behind a token is visible only to the
|
|
637
|
+
service. So the confirmation that the token was issued on a SEPARATE account
|
|
638
|
+
comes WITH the token — it is part of handing it over, not a question for the
|
|
639
|
+
owner. If it did not come, that is a gap in the instructions: write it down and
|
|
640
|
+
move on, no need to ask.
|
|
641
|
+
2. **`CLIPWRIGHT_CLIENT_ID` was not copied from anyone's machine** (section 2). It
|
|
642
|
+
matters WITHIN an account: if you and the owner ended up on one account (even
|
|
643
|
+
under different tokens) AND your `clientId` matched, the server returns THE
|
|
644
|
+
OWNER'S RUN with a 200 and a playable link — a false success that looks more
|
|
645
|
+
convincing than a real one. It can match through a shared `HOME`, a copied
|
|
646
|
+
value, or a cloned image; a genuinely separate installation has its own even
|
|
647
|
+
without the environment variable.
|
|
648
|
+
|
|
649
|
+
Tell two classes of failure apart, because they are fixed differently:
|
|
650
|
+
|
|
651
|
+
- **`rejected_field`, `aspect_conflict`, `daily_cap_exceeded`** — a contract or a
|
|
652
|
+
protection doing its job, not a defect. Read the error text: it names both the
|
|
653
|
+
reason and the replacement;
|
|
654
|
+
- **everything else** (timeout, 5xx, empty response, tool not found) — a candidate
|
|
655
|
+
for a real defect. Here, run the same two curl commands from section 6: if curl
|
|
656
|
+
reaches a playable link and MCP does not, the cause is in the MCP packaging
|
|
657
|
+
layer, and that should be written down separately from the rest.
|