@clipwright/mcp-server 0.1.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 +21 -0
- package/README.md +42 -0
- package/SKILL.md +540 -0
- package/dist/format.js +77 -0
- package/dist/format.js.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/server.js +102 -0
- package/dist/server.js.map +1 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dimantika LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @clipwright/mcp-server
|
|
2
|
+
|
|
3
|
+
MCP server for [Clipwright](https://clipwright.io): generate UGC videos from
|
|
4
|
+
your coding agent. A talking-head clip is rendered from a script you pass in;
|
|
5
|
+
the agent gets a `run_id` and polls until a video URL is ready.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
claude mcp add clipwright \
|
|
11
|
+
-e CLIPWRIGHT_API_KEY="cw_your_token" \
|
|
12
|
+
-e CLIPWRIGHT_CLIENT_ID="$(uuidgen | tr -d '-')" \
|
|
13
|
+
-- npx -y @clipwright/mcp-server
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Any MCP host works — the server speaks JSON-RPC over stdio.
|
|
17
|
+
|
|
18
|
+
## Environment
|
|
19
|
+
|
|
20
|
+
| Variable | Required | Meaning |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `CLIPWRIGHT_API_KEY` | yes | Your `cw_*` token. The process exits immediately without it. |
|
|
23
|
+
| `CLIPWRIGHT_CLIENT_ID` | yes | Identifies this **installation**. It is mixed into the idempotency key, so two installations never collapse into one run. Falls back to `~/.clipwright/client-id`, and fails loudly rather than inventing a throwaway value. |
|
|
24
|
+
| `CLIPWRIGHT_API_URL` | no | Defaults to the hosted API. |
|
|
25
|
+
|
|
26
|
+
## Tools
|
|
27
|
+
|
|
28
|
+
- `list_voices` — voice catalog. Free, spends no credits.
|
|
29
|
+
- `quote_ugc` — estimated duration and price. Free.
|
|
30
|
+
- `make_ugc` — **renders a clip; this is the call that costs money.** Returns a
|
|
31
|
+
`run_id` immediately instead of blocking, because a render takes minutes.
|
|
32
|
+
- `get_run` — status of a run. Free; poll it every few seconds.
|
|
33
|
+
|
|
34
|
+
Call `quote_ugc` before `make_ugc`: the quote is where the contract tells you
|
|
35
|
+
what it will and will not honor. Anything the API cannot deliver comes back in
|
|
36
|
+
`warnings[]` rather than being dropped silently, and unsupported fields are
|
|
37
|
+
rejected up front instead of being accepted and ignored.
|
|
38
|
+
|
|
39
|
+
Bumping the `attempt` field starts a **new paid render**. It is the way to retry
|
|
40
|
+
a failed run on purpose, not a free refresh.
|
|
41
|
+
|
|
42
|
+
`SKILL.md` ships inside the package with the full operating instructions.
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
# SKILL.md — установка и эксплуатация `clipwright-mcp`
|
|
2
|
+
|
|
3
|
+
Самодостаточная инструкция для АГЕНТА (или человека в Claude Code) без устной
|
|
4
|
+
помощи от разработчика. Следуй шагам по порядку. Не путать с
|
|
5
|
+
`apps/web/.claude/skills/*` — это скиллы дашборда Next.js, к MCP-серверу
|
|
6
|
+
отношения не имеют.
|
|
7
|
+
|
|
8
|
+
Пакет `@clipwright/mcp-server` (`bin: clipwright-mcp`) опубликован в npm под
|
|
9
|
+
лицензией MIT, поэтому репозиторий для установки НЕ НУЖЕН — хватит `npx`.
|
|
10
|
+
Сборка из исходников осталась ниже как путь для разработки.
|
|
11
|
+
|
|
12
|
+
Три пакета клиентской поверхности — `@clipwright/core`, `@clipwright/sdk`,
|
|
13
|
+
`@clipwright/mcp-server` — открыты по MIT; сам сервис (REST API, рендер-пайплайн,
|
|
14
|
+
дашборд) закрыт. Это намеренное разделение, а не недосмотр: см. `LICENSE` в
|
|
15
|
+
корне репозитория.
|
|
16
|
+
|
|
17
|
+
## 1. Установка
|
|
18
|
+
|
|
19
|
+
### 1.1 Из npm — обычный путь
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx -y -p @clipwright/mcp-server@latest clipwright-mcp
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Ожидаемо: процесс НЕ завершается, в stderr печатается
|
|
26
|
+
`clipwright mcp server running on stdio` и висит, ожидая JSON-RPC. Это и есть
|
|
27
|
+
признак исправного сервера, а не зависание. Останови `Ctrl+C`.
|
|
28
|
+
|
|
29
|
+
Немедленный выход с `CLIPWRIGHT_API_KEY env var is required` — тоже ожидаемое
|
|
30
|
+
поведение при пустом ключе (`src/index.ts:11-15`), а не поломка сборки.
|
|
31
|
+
|
|
32
|
+
Дальше сразу к разделу 1.3: конфиг хоста для этого варианта отличается только
|
|
33
|
+
командой (`npx` вместо `node` с абсолютным путём), оба показаны там.
|
|
34
|
+
|
|
35
|
+
### 1.2 Из исходников — только для разработки
|
|
36
|
+
|
|
37
|
+
Требования: Node ≥24, pnpm (`packageManager: pnpm@11.13.1` в корневом
|
|
38
|
+
`package.json` — при наличии corepack: `corepack enable`).
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git clone <repo-url> ugc-video-platform # или используй уже готовый чекаут
|
|
42
|
+
cd ugc-video-platform
|
|
43
|
+
pnpm install
|
|
44
|
+
pnpm build # pnpm -r build — собирает ВСЕ пакеты монорепо в топологическом порядке
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Если нужна только эта поверхность (быстрее, но зависимости `@clipwright/core`
|
|
48
|
+
и `@clipwright/sdk` обязаны быть собраны первыми — `...` тянет их
|
|
49
|
+
автоматически):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pnpm --filter @clipwright/mcp-server... build
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
После сборки должен существовать файл `packages/mcp-server/dist/index.js`.
|
|
56
|
+
Проверь:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
ls packages/mcp-server/dist/index.js
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
#### Предполётная проверка собранного бинаря
|
|
63
|
+
|
|
64
|
+
Относится к сборке из исходников. Прежде чем подключать сервер к хосту,
|
|
65
|
+
убедись, что он вообще стартует и не падает на импортах:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
CLIPWRIGHT_API_KEY=cw_test node packages/mcp-server/dist/index.js
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Ожидаемо: процесс НЕ завершается сам, в stderr печатается
|
|
72
|
+
`clipwright mcp server running on stdio` и висит на stdio (ждёт JSON-RPC).
|
|
73
|
+
Останови `Ctrl+C`. Если вместо этого немедленный выход с
|
|
74
|
+
`CLIPWRIGHT_API_KEY env var is required` — значит переменная не была передана
|
|
75
|
+
(это ожидаемое поведение при пустом ключе, см. `src/index.ts:11-15`), не баг.
|
|
76
|
+
Если падает на импорте модуля — сборка неполная, вернись к шагу 1.2
|
|
77
|
+
(`pnpm build` из корня, не адресный `--filter`, чтобы точно пересобрать
|
|
78
|
+
`@clipwright/core`/`@clipwright/sdk`).
|
|
79
|
+
|
|
80
|
+
### 1.3 Подключение к MCP-хосту
|
|
81
|
+
|
|
82
|
+
Варианты A и B ниже запускают сервер ИЗ ИСХОДНИКОВ и потому требуют
|
|
83
|
+
АБСОЛЮТНЫЙ путь до `dist/index.js`:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
realpath packages/mcp-server/dist/index.js
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Если ставил из npm (раздел 1.1), путь не нужен — бери вариант В в конце
|
|
90
|
+
раздела, он отличается только полями `command`/`args`.
|
|
91
|
+
|
|
92
|
+
**Вариант A — `.mcp.json` (проектный конфиг хоста, стиль как в
|
|
93
|
+
`apps/web/.mcp.json` этого репо):**
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"mcpServers": {
|
|
98
|
+
"clipwright": {
|
|
99
|
+
"type": "stdio",
|
|
100
|
+
"command": "node",
|
|
101
|
+
"args": ["/абсолютный/путь/до/ugc-video-platform/packages/mcp-server/dist/index.js"],
|
|
102
|
+
"env": {
|
|
103
|
+
"CLIPWRIGHT_API_KEY": "cw_...",
|
|
104
|
+
"CLIPWRIGHT_API_URL": "https://api-production-7e69.up.railway.app",
|
|
105
|
+
"CLIPWRIGHT_CLIENT_ID": "<см. раздел 2 — сгенерировать ДО первого запуска>"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Вариант B — CLI `claude mcp add`** (эквивалент варианта A одной командой):
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
claude mcp add clipwright \
|
|
116
|
+
-e CLIPWRIGHT_API_KEY=cw_... \
|
|
117
|
+
-e CLIPWRIGHT_API_URL=https://api-production-7e69.up.railway.app \
|
|
118
|
+
-e CLIPWRIGHT_CLIENT_ID="$(uuidgen | tr -d '-')" \
|
|
119
|
+
-- node /абсолютный/путь/до/ugc-video-platform/packages/mcp-server/dist/index.js
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Вариант В — из npm, без репозитория.** То же самое, но команда — `npx`:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"mcpServers": {
|
|
127
|
+
"clipwright": {
|
|
128
|
+
"type": "stdio",
|
|
129
|
+
"command": "npx",
|
|
130
|
+
"args": ["-y", "-p", "@clipwright/mcp-server@latest", "clipwright-mcp"],
|
|
131
|
+
"env": {
|
|
132
|
+
"CLIPWRIGHT_API_KEY": "cw_...",
|
|
133
|
+
"CLIPWRIGHT_API_URL": "https://api-production-7e69.up.railway.app",
|
|
134
|
+
"CLIPWRIGHT_CLIENT_ID": "<см. раздел 2 — сгенерировать ДО первого запуска>"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Одной командой:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
claude mcp add clipwright \
|
|
145
|
+
-e CLIPWRIGHT_API_KEY=cw_... \
|
|
146
|
+
-e CLIPWRIGHT_API_URL=https://api-production-7e69.up.railway.app \
|
|
147
|
+
-e CLIPWRIGHT_CLIENT_ID="$(uuidgen | tr -d '-')" \
|
|
148
|
+
-- npx -y -p @clipwright/mcp-server@latest clipwright-mcp
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**`@latest` здесь — осознанный выбор, а не небрежность.** Он тянет свежую
|
|
152
|
+
версию при каждом запуске: для прототипа это правильно, потому что контракт
|
|
153
|
+
ещё меняется и рассинхрон клиента с сервером дороже, чем неожиданное
|
|
154
|
+
обновление. Когда контракт устоится, версию надо будет прибить.
|
|
155
|
+
|
|
156
|
+
Переменные окружения:
|
|
157
|
+
|
|
158
|
+
| Переменная | Обязательна | Значение |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| `CLIPWRIGHT_API_KEY` | ДА | Bearer-токен формата `cw_*`. Без него процесс завершается сразу с ошибкой (`src/index.ts:11-15`). Токен ВЫДАН ЛИЧНО ТЕБЕ (US-550): он лежит в таблице `api_tokens`, привязан к аккаунту и отзывается отдельно от чужих. Не пересылай его дальше — второму человеку выпускается свой. Разным аккаунтам не видны раны друг друга, и суточный потолок трат у каждого свой. |
|
|
161
|
+
| `CLIPWRIGHT_API_URL` | НЕТ | Прод-URL. Если не задать — SDK сам подставляет `https://api-production-7e69.up.railway.app` (дефолт в `packages/sdk/src/index.ts:18`). Указывай явно только если целишься в другое окружение. |
|
|
162
|
+
| `CLIPWRIGHT_CLIENT_ID` | ДА (см. раздел 2) | Уникальный идентификатор УСТАНОВКИ. |
|
|
163
|
+
|
|
164
|
+
После подключения перезапусти/переоткрой MCP-хост и убедись, что `tools/list`
|
|
165
|
+
отдаёт четыре тула: `make_ugc`, `get_run`, `quote_ugc`, `list_voices`.
|
|
166
|
+
|
|
167
|
+
## 2. `CLIPWRIGHT_CLIENT_ID` — обязательно и ПОЧЕМУ
|
|
168
|
+
|
|
169
|
+
**Правило: КАЖДАЯ установка обязана получить СВОЙ уникальный
|
|
170
|
+
`CLIPWRIGHT_CLIENT_ID` ДО первого вызова `make_ugc`.** Не копировать значение
|
|
171
|
+
с другой машины, не оставлять пустым в расчёте на дефолт "как-нибудь
|
|
172
|
+
сработает".
|
|
173
|
+
|
|
174
|
+
### Почему это не опционально
|
|
175
|
+
|
|
176
|
+
Ключ идемпотентности, которым SDK помечает каждый платный `POST
|
|
177
|
+
/v1/skills/make_ugc/run`, детерминированно вычисляется из **хеша входа
|
|
178
|
+
(`script`/`person`/`image`/… после canonicalize) И идентичности клиента**
|
|
179
|
+
(`idempotencyKeyFor`, `packages/core/src/idempotency.ts`; используется в
|
|
180
|
+
`packages/sdk/src/index.ts:104-115`). Сервер дедуплицирует по этому ключу:
|
|
181
|
+
`apps/api/src/app.ts:103` — если для уже сохранённого ключа
|
|
182
|
+
`existing.request_hash === hash`, он возвращает 200 с раном, который
|
|
183
|
+
**создал первый запрос с этим ключом**, а не запускает новый платный рендер.
|
|
184
|
+
|
|
185
|
+
Это правильное поведение для ретрая ОДНОГО И ТОГО ЖЕ пользователя (не платить
|
|
186
|
+
дважды за случайный дубль-клик агента). Но:
|
|
187
|
+
|
|
188
|
+
- поиск по ключу идемпотентности идёт по ключу, а не по аккаунту
|
|
189
|
+
(`runs_idempotency_key_unique` — частичный индекс по одной колонке);
|
|
190
|
+
- поэтому **разделение аккаунтов токеном (US-550) само по себе НЕ разводит
|
|
191
|
+
одинаковые входы**: `clientId`, примешанный в хеш ключа, остаётся тем, что
|
|
192
|
+
делает ключи разными;
|
|
193
|
+
- и он же нужен внутри одного аккаунта: ноутбук и CI одного человека обязаны
|
|
194
|
+
ретраить независимо.
|
|
195
|
+
|
|
196
|
+
Если второй пользователь не выставил СВОЙ `CLIPWRIGHT_CLIENT_ID` и его запрос
|
|
197
|
+
(тот же скрипт, те же опции — например, оба используют пример из этого
|
|
198
|
+
SKILL.md дословно) совпадает по входу с чужим уже выполненным раном, сервер
|
|
199
|
+
схлопнёт их в ОДИН ран. Второй пользователь получит 200, `run_id` и играющуюся
|
|
200
|
+
ссылку — но это будет ЧУЖОЕ видео, представленное ему как его собственный
|
|
201
|
+
успех. Это дефект 3.2 плана Этапа 5: диагностический инструмент/установка
|
|
202
|
+
прочитались бы как «работает», хотя платный рендер для второго пользователя
|
|
203
|
+
никогда не запускался.
|
|
204
|
+
|
|
205
|
+
### Как выставить
|
|
206
|
+
|
|
207
|
+
Явный `CLIPWRIGHT_CLIENT_ID` в env хоста (раздел 1.3, вариант A/B) — самый
|
|
208
|
+
надёжный путь, не зависящий от состояния файловой системы. Значение — любая
|
|
209
|
+
уникальная строка; для согласованности с генератором SDK используй тот же
|
|
210
|
+
формат (UUID без дефисов):
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
uuidgen | tr -d '-'
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Если переменная НЕ задана, `resolveClientId()`
|
|
217
|
+
(`packages/sdk/src/client-id.ts`) падает на файл `~/.clipwright/client-id`:
|
|
218
|
+
при первом запуске генерирует случайный id и сохраняет его туда, дальше читает
|
|
219
|
+
оттуда. Это рабочий фолбэк для одиночной установки на чистой машине, но НЕ
|
|
220
|
+
полагайся на него как на единственную защиту при установке агентом: домашние
|
|
221
|
+
каталоги при клонировании образов/контейнеров/дев-окружений могут унести файл
|
|
222
|
+
`~/.clipwright/client-id` с собой и породить коллизию, которую этот фолбэк
|
|
223
|
+
не заметит. Для установки по этой инструкции — **всегда выставляй
|
|
224
|
+
`CLIPWRIGHT_CLIENT_ID` явно в env хоста**, не полагайся на автогенерацию.
|
|
225
|
+
|
|
226
|
+
Если ни env, ни запись файла невозможны (read-only `$HOME`), `resolveClientId()`
|
|
227
|
+
намеренно БРОСАЕТ исключение вместо тихого эфемерного id на каждый запуск —
|
|
228
|
+
молчаливый фолбэк убил бы дедупликацию целиком (`packages/sdk/src/client-id.ts:9-19`).
|
|
229
|
+
Если сервер упал с этой ошибкой — это сигнал поставить `CLIPWRIGHT_CLIENT_ID`
|
|
230
|
+
явно, а не искать баг.
|
|
231
|
+
|
|
232
|
+
## 3. ПОРЯДОК ВЫЗОВОВ — сначала `quote_ugc`, потом `make_ugc`
|
|
233
|
+
|
|
234
|
+
Это не совет по вежливости, а механизм, без которого контракт молчит о своих
|
|
235
|
+
решениях.
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
quote_ugc → прочитать source / resolved_aspect_ratio / warnings → make_ugc → get_run (поллинг)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Что даёт `quote_ugc`, чего не даёт ничего другого.** Он бесплатен, ничего не
|
|
242
|
+
резервирует и отвечает на вопросы, которые иначе выяснятся уже после списания:
|
|
243
|
+
|
|
244
|
+
| Поле ответа | Что из него видно |
|
|
245
|
+
|---|---|
|
|
246
|
+
| `credits_estimate` | цена — покажи её пользователю ДО генерации |
|
|
247
|
+
| `source` | размеры источника кадра: пробленный `image` либо дефолтный актёр (432×768) |
|
|
248
|
+
| `resolved_aspect_ratio` | формат, который РЕАЛЬНО будет отрендерен на этом входе |
|
|
249
|
+
| `warnings[]` | какие переданные параметры не будут соблюдены |
|
|
250
|
+
| `contract_version` | под какой версией контракта получена эта цена |
|
|
251
|
+
|
|
252
|
+
**Вердикт `quote_ugc` и вердикт `make_ugc` совпадают по построению** — обе
|
|
253
|
+
поверхности зовут одну функцию. Если quote вернул 400, ран вернёт тот же 400;
|
|
254
|
+
если quote показал `resolved_aspect_ratio: "9:16"` на запрошенном `1:1`, то
|
|
255
|
+
именно 9:16 и отрендерится.
|
|
256
|
+
|
|
257
|
+
**Отклонённые поля — не пытайся их передавать.** Они не объявлены в схеме тула,
|
|
258
|
+
и сервер отвечает 400 `rejected_field` ещё до создания рана:
|
|
259
|
+
|
|
260
|
+
| Поле | Чем заменить |
|
|
261
|
+
|---|---|
|
|
262
|
+
| `character` | `person` — описание актёра словами, или ничего (дефолтный актёр) |
|
|
263
|
+
| `webhook_url` | поллинг `get_run` — вебхуки не доставляются вообще |
|
|
264
|
+
| `segments`, `broll_url` | одним `script`; сегментные ролики ещё не доступны |
|
|
265
|
+
| `disclosure_overlay` | ничем: маркировка уже есть в ответе рана и в самом файле |
|
|
266
|
+
| `image` при выключенном килсвитче | опустить — рендер пойдёт на дефолтном актёре |
|
|
267
|
+
|
|
268
|
+
**Формат может быть отклонён.** Запрос `aspect_ratio: "1:1"` без `image`
|
|
269
|
+
вернёт отказ: дефолтный актёр вертикальный (432×768), и квадратный кадр из
|
|
270
|
+
него получился бы только центр-кропом вендора, чего мы не делаем молча. Чтобы
|
|
271
|
+
получить `1:1`, передай `image` с квадратным публичным https-источником.
|
|
272
|
+
|
|
273
|
+
## 4. Четыре тула
|
|
274
|
+
|
|
275
|
+
Схемы — единый источник правды `@clipwright/core`
|
|
276
|
+
(`packages/core/src/skills.ts`, барррель `packages/core/src/index.ts`), MCP их
|
|
277
|
+
не переписывает руками (`packages/mcp-server/src/index.ts:6-8`).
|
|
278
|
+
|
|
279
|
+
### `quote_ugc` — бесплатно
|
|
280
|
+
|
|
281
|
+
Вход — `offeredUgcInputShape` (`packages/mcp-server/src/server.ts`), то есть
|
|
282
|
+
контракт МИНУС отклонённые поля: их там просто нет, и перечислять их здесь
|
|
283
|
+
значило бы предлагать то, за что сервер накажет (раздел 3).
|
|
284
|
+
|
|
285
|
+
`script` (≤10000 симв., счёт слов ≤`MAX_SCRIPT_WORDS` ≈45 с речи; формально
|
|
286
|
+
`.optional()`, но обязателен, пока `segments` отклонён), опционально `person`
|
|
287
|
+
ИЛИ `image` (публичный `https`-URL), плюс `name`, `captions` (по умолчанию
|
|
288
|
+
`false` — **субтитры opt-in, СНАЧАЛА спроси пользователя**), `caption_style`
|
|
289
|
+
(`hormozi`/`tiktok`/`minimal`), `look` (`natural`/`commercial`/`raw_iphone`),
|
|
290
|
+
`aspect_ratio` (`9:16`/`1:1`, БЕЗ дефолта — молчание и явный выбор различаются),
|
|
291
|
+
`resolution` (`720p`/`1080p`/`4k`), `voice`/`voice_id`.
|
|
292
|
+
|
|
293
|
+
Точный список всегда доступен из самой схемы тула: `tools/list` порождается из
|
|
294
|
+
того же shape, и поля, которые не соблюдаются, несут `NOT HONORED YET` прямо в
|
|
295
|
+
своём `description`.
|
|
296
|
+
|
|
297
|
+
Не тратит кредиты. Ответ — необработанный `quote` целиком
|
|
298
|
+
(`packages/mcp-server/src/server.ts`):
|
|
299
|
+
|
|
300
|
+
```json
|
|
301
|
+
{
|
|
302
|
+
"skill": "make_ugc",
|
|
303
|
+
"credits_estimate": 360,
|
|
304
|
+
"duration_estimate_sec": 12,
|
|
305
|
+
"warnings": []
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
`warnings[]` здесь ПРИСУТСТВУЕТ (передаётся без фильтрации) — читай его:
|
|
310
|
+
несоблюдённые параметры (например, `captions` проигнорирован в этой сборке)
|
|
311
|
+
объявляются сюда, а не молча теряются.
|
|
312
|
+
|
|
313
|
+
**Всегда вызывай `quote_ugc` первым и покажи пользователю цену до `make_ugc`**
|
|
314
|
+
(императив в описании тула, `packages/mcp-server/src/index.ts:83-85`).
|
|
315
|
+
|
|
316
|
+
### `make_ugc` — платный, создаёт ран
|
|
317
|
+
|
|
318
|
+
Вход — тот же `makeUgcInputShape` + `attempt` (целое ≥1, опционален; см.
|
|
319
|
+
раздел 4). Денежный контур: это ОДИН платный рендер за вызов (если не сработал
|
|
320
|
+
дедуп идемпотентности — раздел 4). **Тул НЕ ждёт готовности видео** — он
|
|
321
|
+
стартует ран и немедленно возвращает `run_id`
|
|
322
|
+
(`packages/mcp-server/src/index.ts:28-29,39-40`):
|
|
323
|
+
|
|
324
|
+
```json
|
|
325
|
+
{
|
|
326
|
+
"status": "IN_PROGRESS",
|
|
327
|
+
"run_id": "run_...",
|
|
328
|
+
"state": "queued",
|
|
329
|
+
"video_url": null,
|
|
330
|
+
"next_action": "Video is NOT ready. Call get_run with run_id=run_... 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."
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Дальше ОБЯЗАТЕЛЬНО поллить `get_run` этим `run_id` каждые ~5–10 секунд до
|
|
335
|
+
терминала. Не сообщай пользователю, что видео готово, пока не получил
|
|
336
|
+
`video_url`.
|
|
337
|
+
|
|
338
|
+
#### Выбор голоса (`voice` / `voice_id`)
|
|
339
|
+
|
|
340
|
+
Два взаимоисключающих опциональных поля во входе `make_ugc`:
|
|
341
|
+
|
|
342
|
+
- `voice` — имя **курируемого пресета**. **Рекомендуемый путь.** Список — тул
|
|
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
|
+
каталог по дизайну, не полный список голосов вендора.
|
|
353
|
+
|
|
354
|
+
Детали и инвариант «неверный голос не стоит денег» — `docs/16-voice-selection.md`.
|
|
355
|
+
|
|
356
|
+
### `get_run` — поллинг статуса
|
|
357
|
+
|
|
358
|
+
Вход: `{ "run_id": "run_..." }`. Форма ответа реализована в
|
|
359
|
+
`packages/mcp-server/src/format.ts` (`formatGetRun`) — ниже она приведена
|
|
360
|
+
ФАКТИЧЕСКИ, после исправления US-501.
|
|
361
|
+
|
|
362
|
+
**Нетерминальный ран** (`queued`/`scripting`/`tts`/`avatar`/`compositing`/`uploading`)
|
|
363
|
+
несёт `state` стадии, `status` всегда `"IN_PROGRESS"`:
|
|
364
|
+
|
|
365
|
+
```json
|
|
366
|
+
{
|
|
367
|
+
"status": "IN_PROGRESS",
|
|
368
|
+
"run_id": "run_...",
|
|
369
|
+
"state": "avatar",
|
|
370
|
+
"video_url": null,
|
|
371
|
+
"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."
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**Терминальный успех** несёт И `status: "SUCCEEDED"`, И `state: "succeeded"`
|
|
376
|
+
(до US-501 `state` в терминальной успешной ветке отсутствовал — грабля №4
|
|
377
|
+
`docs/14-gate4-heygen.md:75-79` — теперь это исправлено, поле есть всегда):
|
|
378
|
+
|
|
379
|
+
```json
|
|
380
|
+
{
|
|
381
|
+
"status": "SUCCEEDED",
|
|
382
|
+
"run_id": "run_...",
|
|
383
|
+
"state": "succeeded",
|
|
384
|
+
"video_url": "https://...",
|
|
385
|
+
"duration_seconds": 13.08
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
**Терминальный отказ** несёт И `status: "FAILED"`, И `state: "failed"`:
|
|
390
|
+
|
|
391
|
+
```json
|
|
392
|
+
{
|
|
393
|
+
"status": "FAILED",
|
|
394
|
+
"run_id": "run_...",
|
|
395
|
+
"state": "failed",
|
|
396
|
+
"error": "<текст ошибки>"
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**`warnings[]` в `get_run`:** каждая ветка ответа (`IN_PROGRESS`, `SUCCEEDED`,
|
|
401
|
+
`FAILED`) несёт `warnings` рана (US-512; правило проекта №3 — несоблюдённые
|
|
402
|
+
параметры объявляются, никогда молча). Читай его на терминале обязательно:
|
|
403
|
+
например, отсутствие субтитров в прототипе объявляется именно здесь.
|
|
404
|
+
|
|
405
|
+
**Неизвестный терминальный статус на сервере** (например гипотетический
|
|
406
|
+
`"canceled"`, не входящий в `TERMINAL_STATES`) — осознанная деградация:
|
|
407
|
+
`isTerminal` вернёт `false`, и такой ран попадёт в ветку `IN_PROGRESS` (агенту
|
|
408
|
+
скажут «продолжай поллить» про уже завершённый ран). Это принятое поведение
|
|
409
|
+
прототипа (MCP и API деплоятся одной версией), не баг форматтера.
|
|
410
|
+
|
|
411
|
+
### `list_voices` — бесплатно, без входа
|
|
412
|
+
|
|
413
|
+
Отдаёт курируемый каталог пресетов для поля `voice` у `make_ugc`
|
|
414
|
+
(`packages/mcp-server/src/server.ts`, регистрация четвёртым; `inputSchema: {}` —
|
|
415
|
+
аргументов нет). Денег не стоит и рана не создаёт.
|
|
416
|
+
|
|
417
|
+
Сетевой вызов при этом есть: тула ходит в НАШ `/v1/voices` и требует рабочий
|
|
418
|
+
`CLIPWRIGHT_API_KEY`, как и остальные три. К вендору не ходит никто — список
|
|
419
|
+
статический и живёт в `packages/core/src/voices.ts`, поэтому вендорский ключ на
|
|
420
|
+
этой поверхности не нужен.
|
|
421
|
+
|
|
422
|
+
Зови его до `make_ugc`, если пользователь просит «другой голос»: имя пресета
|
|
423
|
+
предпочтительнее сырого `voice_id`, потому что пресет переживает смену вендора,
|
|
424
|
+
а идентификатор — нет (раздел про `voice`/`voice_id` выше).
|
|
425
|
+
|
|
426
|
+
## 5. Повтор упавшего рана — это НЕ перманентный отказ
|
|
427
|
+
|
|
428
|
+
Если `get_run` вернул терминальный `FAILED` и ты вызываешь `make_ugc` СНОВА с
|
|
429
|
+
ТЕМ ЖЕ входом (тот же `script`/опции) и БЕЗ изменения `attempt` — сервер
|
|
430
|
+
вернёт ТОТ ЖЕ терминальный `failed`-ран как есть (та же ошибка, тот же
|
|
431
|
+
`run_id`), а НЕ запустит новый платный рендер. Это дедупликация по ключу
|
|
432
|
+
идемпотентности (раздел 2), а не признак того, что видео навсегда
|
|
433
|
+
недостижимо.
|
|
434
|
+
|
|
435
|
+
**Чтобы реально повторить попытку, передай `attempt` со значением на единицу
|
|
436
|
+
больше предыдущего** (по умолчанию первый вызов имеет `attempt=1`; для второй
|
|
437
|
+
попытки — `attempt: 2`, для третьей — `attempt: 3`, и т.д.). Это дословно
|
|
438
|
+
поведение, задокументированное в описании тула `make_ugc`
|
|
439
|
+
(`packages/mcp-server/src/index.ts:29-30`):
|
|
440
|
+
|
|
441
|
+
> "Pass attempt=2,3,… to deliberately start a NEW run for the same input
|
|
442
|
+
> (retry after a failure)."
|
|
443
|
+
|
|
444
|
+
Технически `attempt` уходит не в тело рендер-запроса, а в опции SDK
|
|
445
|
+
(`packages/mcp-server/src/index.ts:35-40`) и превращается в суффикс
|
|
446
|
+
`:N` у ключа идемпотентности (`packages/sdk/src/index.ts:104-115`) — другой
|
|
447
|
+
суффикс даёт другой ключ → сервер видит НОВЫЙ запрос → новый `run_id` → новый
|
|
448
|
+
платный вызов вендора.
|
|
449
|
+
|
|
450
|
+
Не путай это с `CLIPWRIGHT_CLIENT_ID`: `attempt` — сознательное решение агента
|
|
451
|
+
«это новая попытка», а `clientId` — фиксированная идентичность установки, её
|
|
452
|
+
менять между попытками НЕ нужно.
|
|
453
|
+
|
|
454
|
+
### ⚠️ КАЖДЫЙ БАМП `attempt` СТОИТ ДЕНЕГ
|
|
455
|
+
|
|
456
|
+
Новый `attempt` — это новый ран и **новый платный вызов вендора**, а не
|
|
457
|
+
бесплатная переотправка. Цикл «упало → бампнул → упало → бампнул» тратит
|
|
458
|
+
реальные кредиты на каждой итерации.
|
|
459
|
+
|
|
460
|
+
Поэтому:
|
|
461
|
+
|
|
462
|
+
- **перед повтором прочитай `error` упавшего рана.** Отказы вида
|
|
463
|
+
`rejected_field`, `aspect_conflict`, `daily_cap_exceeded` повтором НЕ
|
|
464
|
+
чинятся — они детерминированы входом или потолком, и следующая попытка
|
|
465
|
+
упадёт так же, только уже за деньги;
|
|
466
|
+
- **бампай `attempt` только при отказе, похожем на временный** (таймаут
|
|
467
|
+
вендора, 5xx, обрыв сети);
|
|
468
|
+
- **потолок существует и он суточный.** Исчерпав его, ты получишь
|
|
469
|
+
`daily_cap_exceeded` — это сработавшая защита, а не поломка продукта.
|
|
470
|
+
Дожидаться следующих суток или просить владельца поднять лимит.
|
|
471
|
+
|
|
472
|
+
## 6. При отказе — curl-скрипт как разделитель диагнозов
|
|
473
|
+
|
|
474
|
+
Если MCP-путь не доводит до играющегося видео, запусти
|
|
475
|
+
`scripts/clipwright-smoke.sh` (US-506) — он бьёт по тому же публичному REST
|
|
476
|
+
`POST /v1/skills/make_ugc/run` → `GET /v1/runs/{id}` напрямую curl'ом, в обход
|
|
477
|
+
MCP/SDK/CLI целиком:
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
CLIPWRIGHT_API_TOKEN=cw_... ./scripts/clipwright-smoke.sh "Привет! Тестовая фраза."
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
(`CLIPWRIGHT_API_TOKEN` — то же значение, что `CLIPWRIGHT_API_KEY` в env MCP,
|
|
484
|
+
см. раздел 1.3; `CLIPWRIGHT_API_URL` опционален, тот же дефолт-прод.)
|
|
485
|
+
|
|
486
|
+
Диагноз по результату:
|
|
487
|
+
|
|
488
|
+
- **curl доходит до `succeeded` со своим `run_id` и играющейся ссылкой, а MCP —
|
|
489
|
+
нет:** первопричина в слое MCP-пакетирования (сборка/конфиг хоста/env,
|
|
490
|
+
раздел 1) — не в REST API и не в доступе/токене. Это допустимое закрытие с
|
|
491
|
+
документированным остаточным (курл-гейт Architect, US-511).
|
|
492
|
+
- **curl тоже падает:** проблема не в MCP — проверь `CLIPWRIGHT_API_TOKEN`
|
|
493
|
+
(формат `cw_*`, не обрезан при копировании), сетевой доступ к
|
|
494
|
+
`$CLIPWRIGHT_API_URL`, и (для истории про клонированные раны) что
|
|
495
|
+
`CLIPWRIGHT_CLIENT_ID` реально уникален на этой машине (раздел 2) — если
|
|
496
|
+
ответ 200 с ЧУЖИМ по смыслу `run_id`, это симптом дефекта 3.2 на MCP-пути,
|
|
497
|
+
который curl (у него собственный, случайный `Idempotency-Key` на каждый
|
|
498
|
+
вызов — `scripts/clipwright-smoke.sh:112`) не воспроизведёт напрямую, но
|
|
499
|
+
который стоит проверить отдельно через `get_run` на этот `run_id`.
|
|
500
|
+
|
|
501
|
+
Скрипт также поддерживает бесплатный `--poll-only <run_id>` для проверки уже
|
|
502
|
+
существующего рана без нового платного вызова.
|
|
503
|
+
|
|
504
|
+
## Итоговая последовательность (что должно воспроизвестись)
|
|
505
|
+
|
|
506
|
+
1. Установить сервер: `npx -y -p @clipwright/mcp-server@latest clipwright-mcp`
|
|
507
|
+
стартует и печатает `running on stdio` (раздел 1.1). Из исходников —
|
|
508
|
+
`pnpm install && pnpm build` из корня репо → `dist/index.js` существует.
|
|
509
|
+
2. Сгенерировать уникальный `CLIPWRIGHT_CLIENT_ID` (раздел 2) и прописать его
|
|
510
|
+
вместе с `CLIPWRIGHT_API_KEY` в конфиг MCP-хоста (раздел 1.3).
|
|
511
|
+
3. Перезапустить хост, убедиться, что `tools/list` отдаёт 4 тула
|
|
512
|
+
(`make_ugc`, `quote_ugc`, `get_run`, `list_voices`).
|
|
513
|
+
4. `quote_ugc` со скриптом → показать цену И прочитать `source`,
|
|
514
|
+
`resolved_aspect_ratio`, `warnings[]` (раздел 3).
|
|
515
|
+
5. `make_ugc` → получить `run_id` немедленно.
|
|
516
|
+
6. `get_run` каждые ~5–10 с, читая `state` в процессе и `status`+`state` на
|
|
517
|
+
терминале, до `SUCCEEDED` (`video_url`) или `FAILED` (`error`).
|
|
518
|
+
7. При отказе — `scripts/clipwright-smoke.sh` (раздел 6) для разделения
|
|
519
|
+
диагнозов.
|
|
520
|
+
|
|
521
|
+
### Если ты — второй пользователь, проверяющий продукт
|
|
522
|
+
|
|
523
|
+
Задача одна: **дойти до играющегося видео по этому файлу, без устной помощи
|
|
524
|
+
владельца.** Проверяется не ты, а полнота инструкции — поэтому не спрашивай,
|
|
525
|
+
а записывай, где застрял.
|
|
526
|
+
|
|
527
|
+
Перед началом убедись, что в конфиге стоит **твой собственный**
|
|
528
|
+
`CLIPWRIGHT_CLIENT_ID` (раздел 2). Без него сервер вернёт РАН ВЛАДЕЛЬЦА с
|
|
529
|
+
ответом 200 и играющей ссылкой — то есть ложный успех, который выглядит
|
|
530
|
+
убедительнее настоящего.
|
|
531
|
+
|
|
532
|
+
Отличай два класса отказа, они чинятся по-разному:
|
|
533
|
+
|
|
534
|
+
- **`rejected_field`, `aspect_conflict`, `daily_cap_exceeded`** — это
|
|
535
|
+
сработавший контракт или сработавшая защита, а не дефект. Прочитай текст
|
|
536
|
+
ошибки: в нём назван и повод, и замена;
|
|
537
|
+
- **всё остальное** (таймаут, 5xx, пустой ответ, тул не найден) — кандидат в
|
|
538
|
+
настоящие дефекты. Здесь запусти `scripts/clipwright-smoke.sh`: если curl
|
|
539
|
+
доходит до играющейся ссылки, а MCP нет — причина в слое упаковки MCP, и это
|
|
540
|
+
надо записать отдельно от остального.
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { FAILED_STATE, isTerminal } from "@clipwright/core";
|
|
2
|
+
/**
|
|
3
|
+
* Чистое форматирование ответа `get_run` (вынесено из хендлера, чтобы
|
|
4
|
+
* тестировать терминальную форму без подъёма stdio-транспорта и env).
|
|
5
|
+
*
|
|
6
|
+
* ОБЕ терминальные ветки несут И `status` (SUCCEEDED/FAILED — императив для
|
|
7
|
+
* агента), И `state` (фактический `run.state` — исправление US-501, контракт
|
|
8
|
+
* поллинга: терминал теперь виден в самом `state`, не только в `status`).
|
|
9
|
+
*
|
|
10
|
+
* Осознанная деградация (минор Critic №3, зафиксировать в docs/15): неизвестный
|
|
11
|
+
* ТЕРМИНАЛЬНЫЙ статус сервера (напр. `"canceled"`) НЕ входит в `TERMINAL_STATES`
|
|
12
|
+
* → `isTerminal=false` → проваливается в IN_PROGRESS-ветку (агенту скажут
|
|
13
|
+
* «продолжай поллить» про завершённый ран). В прототипе это принято осознанно:
|
|
14
|
+
* MCP и API деплоятся одной версией, а против текущего краша это строгое
|
|
15
|
+
* улучшение.
|
|
16
|
+
*/
|
|
17
|
+
export function formatGetRun(run) {
|
|
18
|
+
if (isTerminal(run.state)) {
|
|
19
|
+
// Терминально: различение исхода — именованной константой core (не
|
|
20
|
+
// позиционным индексом: переупорядочивание массива молча инвертировало бы
|
|
21
|
+
// успех/провал).
|
|
22
|
+
if (run.state === FAILED_STATE) {
|
|
23
|
+
return {
|
|
24
|
+
content: [
|
|
25
|
+
{
|
|
26
|
+
type: "text",
|
|
27
|
+
text: JSON.stringify({
|
|
28
|
+
status: "FAILED",
|
|
29
|
+
run_id: run.run_id,
|
|
30
|
+
state: run.state,
|
|
31
|
+
// Правило проекта №3: расхождения никогда не молчат — warnings
|
|
32
|
+
// пробрасываются в КАЖДУЮ ветку get_run (US-512).
|
|
33
|
+
warnings: run.warnings,
|
|
34
|
+
error: run.error,
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
content: [
|
|
42
|
+
{
|
|
43
|
+
type: "text",
|
|
44
|
+
text: JSON.stringify({
|
|
45
|
+
status: "SUCCEEDED",
|
|
46
|
+
run_id: run.run_id,
|
|
47
|
+
state: run.state,
|
|
48
|
+
video_url: run.final_output?.video_url ?? null,
|
|
49
|
+
duration_seconds: run.final_output?.duration_seconds ?? null,
|
|
50
|
+
warnings: run.warnings,
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Нетерминальное (и неизвестное терминальное — см. деградация выше) состояние:
|
|
57
|
+
// тот же императивный формат, что и у make_ugc.
|
|
58
|
+
return {
|
|
59
|
+
content: [
|
|
60
|
+
{
|
|
61
|
+
type: "text",
|
|
62
|
+
text: JSON.stringify({
|
|
63
|
+
status: "IN_PROGRESS",
|
|
64
|
+
run_id: run.run_id,
|
|
65
|
+
state: run.state,
|
|
66
|
+
video_url: null,
|
|
67
|
+
warnings: run.warnings,
|
|
68
|
+
next_action: "Video is NOT ready. Call get_run with run_id=" +
|
|
69
|
+
run.run_id +
|
|
70
|
+
" again in ~5 seconds. Repeat until state is 'succeeded' or 'failed'. Do NOT tell " +
|
|
71
|
+
"the user the video is done until you have a video_url.",
|
|
72
|
+
}),
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=format.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAgB,MAAM,kBAAkB,CAAC;AAY1E;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,YAAY,CAAC,GAAY;IACvC,IAAI,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,mEAAmE;QACnE,0EAA0E;QAC1E,iBAAiB;QACjB,IAAI,GAAG,CAAC,KAAK,KAAK,YAAY,EAAE,CAAC;YAC/B,OAAO;gBACL,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,MAAM;wBACZ,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;4BACnB,MAAM,EAAE,QAAQ;4BAChB,MAAM,EAAE,GAAG,CAAC,MAAM;4BAClB,KAAK,EAAE,GAAG,CAAC,KAAK;4BAChB,+DAA+D;4BAC/D,kDAAkD;4BAClD,QAAQ,EAAE,GAAG,CAAC,QAAQ;4BACtB,KAAK,EAAE,GAAG,CAAC,KAAK;yBACjB,CAAC;qBACH;iBACF;aACF,CAAC;QACJ,CAAC;QACD,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;wBACnB,MAAM,EAAE,WAAW;wBACnB,MAAM,EAAE,GAAG,CAAC,MAAM;wBAClB,KAAK,EAAE,GAAG,CAAC,KAAK;wBAChB,SAAS,EAAE,GAAG,CAAC,YAAY,EAAE,SAAS,IAAI,IAAI;wBAC9C,gBAAgB,EAAE,GAAG,CAAC,YAAY,EAAE,gBAAgB,IAAI,IAAI;wBAC5D,QAAQ,EAAE,GAAG,CAAC,QAAQ;qBACvB,CAAC;iBACH;aACF;SACF,CAAC;IACJ,CAAC;IACD,+EAA+E;IAC/E,gDAAgD;IAChD,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAM;gBACZ,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,MAAM,EAAE,aAAa;oBACrB,MAAM,EAAE,GAAG,CAAC,MAAM;oBAClB,KAAK,EAAE,GAAG,CAAC,KAAK;oBAChB,SAAS,EAAE,IAAI;oBACf,QAAQ,EAAE,GAAG,CAAC,QAAQ;oBACtB,WAAW,EACT,+CAA+C;wBAC/C,GAAG,CAAC,MAAM;wBACV,mFAAmF;wBACnF,wDAAwD;iBAC3D,CAAC;aACH;SACF;KACF,CAAC;AACJ,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { ClipwrightClient } from "@clipwright/sdk";
|
|
4
|
+
import { createServer } from "./server.js";
|
|
5
|
+
const apiKey = process.env.CLIPWRIGHT_API_KEY;
|
|
6
|
+
if (!apiKey) {
|
|
7
|
+
console.error("CLIPWRIGHT_API_KEY env var is required");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
const client = new ClipwrightClient({ apiKey, baseUrl: process.env.CLIPWRIGHT_API_URL });
|
|
11
|
+
const server = createServer(client);
|
|
12
|
+
const transport = new StdioServerTransport();
|
|
13
|
+
await server.connect(transport);
|
|
14
|
+
console.error("clipwright mcp server running on stdio");
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;AAC9C,IAAI,CAAC,MAAM,EAAE,CAAC;IACZ,OAAO,CAAC,KAAK,CAAC,wCAAwC,CAAC,CAAC;IACxD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AACD,MAAM,MAAM,GAAG,IAAI,gBAAgB,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,CAAC,CAAC;AAEzF,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;AAEpC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;AAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAChC,OAAO,CAAC,KAAK,CAAC,wCAAwC,CAAC,CAAC"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
// Схемы приходят из barrel `@clipwright/core` (единый источник правды, правило №2),
|
|
4
|
+
// а НЕ из `/idempotency` — там живёт только ключ идемпотентности, не shape.
|
|
5
|
+
// `offeredUgcInputShape`, а НЕ полный `makeUgcInputShape`: поля с диспозицией
|
|
6
|
+
// `rejected` не должны предлагаться вызывателю, которого мы за их использование
|
|
7
|
+
// накажем четырёхсотым (блокер B4). Множество выводится из реестра диспозиций,
|
|
8
|
+
// поэтому смена решения по полю переносит его между поверхностями одной строкой.
|
|
9
|
+
import { offeredUgcInputShape, agentRetryShape } from "@clipwright/core";
|
|
10
|
+
import { formatGetRun } from "./format.js";
|
|
11
|
+
/**
|
|
12
|
+
* Строит MCP-сервер с зарегистрированными тулами, БЕЗ подключения транспорта.
|
|
13
|
+
*
|
|
14
|
+
* Вынесено из index.ts (US-516): index.ts раньше и строил сервер, И сразу
|
|
15
|
+
* коннектил его к stdio на верхнем уровне модуля — это делало регистрацию
|
|
16
|
+
* тулов непроверяемой без реального stdio-процесса. Разделение — тот же
|
|
17
|
+
* паттерн, что app.ts/index.ts в apps/api (app — тестируемый, index —
|
|
18
|
+
* тонкий bootstrap).
|
|
19
|
+
*/
|
|
20
|
+
export function createServer(client) {
|
|
21
|
+
const server = new McpServer({ name: "clipwright", version: "0.1.0" });
|
|
22
|
+
// Полная схема публикуется в tools/list (у конкурента она пустая — антипаттерн).
|
|
23
|
+
server.registerTool("make_ugc", {
|
|
24
|
+
description: "Start generation of a lip-synced UGC video. Give a script (any length); optionally describe " +
|
|
25
|
+
"the actor in words with person. Fields the renderer does not honor yet carry a NOT HONORED YET " +
|
|
26
|
+
"note in their own description — read it instead of guessing. " +
|
|
27
|
+
"Captions are OPT-IN: ASK the user first. Call quote_ugc before generating and show the cost. " +
|
|
28
|
+
"This does NOT wait for the video: it starts the run and returns a run_id IMMEDIATELY. You MUST " +
|
|
29
|
+
"then poll get_run with that run_id until the state is 'succeeded' (video_url) or 'failed'. " +
|
|
30
|
+
"Pass attempt=2,3,… to deliberately start a NEW run for the same input (retry after a failure).",
|
|
31
|
+
// [Правило №2] Плоская схема = предлагаемый вход рендера + агентный ретрай,
|
|
32
|
+
// собранная из shape core. Проза описания держится в согласии со схемой
|
|
33
|
+
// намеренно: LLM-вызыватель читает описание РАНЬШЕ схемы, поэтому обещание
|
|
34
|
+
// в тексте («1080p 9:16», «character») было бы тем же дефектом
|
|
35
|
+
// «предлагаем и наказываем», просто слоем выше.
|
|
36
|
+
inputSchema: { ...offeredUgcInputShape, ...agentRetryShape },
|
|
37
|
+
}, async (args) => {
|
|
38
|
+
// [ARB-1] РАСЩЕПЛЯЕМ плоский вход: `attempt` живёт в opts SDK. Если передать
|
|
39
|
+
// его внутрь `input`, `makeUgcInput.parse` в `idempotencyKeyFor` молча срежет
|
|
40
|
+
// лишний ключ → суффикс всегда `:1` → повтор вернёт тот же run → M1 мёртв.
|
|
41
|
+
const { attempt, ...input } = args;
|
|
42
|
+
// [A5] Возврат СРАЗУ после старта (state≈"queued"), без bounded-wait и без цикла.
|
|
43
|
+
const run = await client.startUgc(input, { attempt });
|
|
44
|
+
return {
|
|
45
|
+
content: [
|
|
46
|
+
{
|
|
47
|
+
type: "text",
|
|
48
|
+
text: JSON.stringify({
|
|
49
|
+
status: "IN_PROGRESS",
|
|
50
|
+
run_id: run.run_id,
|
|
51
|
+
state: run.state,
|
|
52
|
+
video_url: null,
|
|
53
|
+
next_action: "Video is NOT ready. Call get_run with run_id=" +
|
|
54
|
+
run.run_id +
|
|
55
|
+
" in ~5 seconds. Repeat until state is 'succeeded' or 'failed'. Do NOT tell the " +
|
|
56
|
+
"user the video is done until you have a video_url.",
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
isError: false,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
server.registerTool("get_run", {
|
|
64
|
+
description: "Check the status of a video generation started by make_ugc. Pass the run_id. While the run " +
|
|
65
|
+
"is still working it returns status IN_PROGRESS with a next_action telling you to poll again; " +
|
|
66
|
+
"repeat every ~5 seconds until it reaches a terminal state — SUCCEEDED (with video_url) or FAILED.",
|
|
67
|
+
inputSchema: { run_id: z.string() },
|
|
68
|
+
}, async ({ run_id }) => {
|
|
69
|
+
// Терминальная форма + деградация вынесены в чистый `formatGetRun` (тест
|
|
70
|
+
// без stdio-транспорта). Терминал несёт И `status`, И `state` (US-501).
|
|
71
|
+
const run = await client.getRun(run_id);
|
|
72
|
+
return formatGetRun(run);
|
|
73
|
+
});
|
|
74
|
+
server.registerTool("quote_ugc", {
|
|
75
|
+
description: "Estimate the credit cost of a make_ugc call WITHOUT spending credits. " +
|
|
76
|
+
"Always call this first and show the user the price before make_ugc.",
|
|
77
|
+
// [Правило №2] Тот же shape core, что и у make_ugc: `script` — единственное
|
|
78
|
+
// обязательное поле (остальные .optional()/.default()), поэтому переиспользуем
|
|
79
|
+
// shape целиком вместо рукописного {script} — цена учитывает captions/look и не расходится со схемой.
|
|
80
|
+
inputSchema: offeredUgcInputShape,
|
|
81
|
+
}, async (args) => {
|
|
82
|
+
const quote = await client.quoteUgc(args);
|
|
83
|
+
return { content: [{ type: "text", text: JSON.stringify(quote) }] };
|
|
84
|
+
});
|
|
85
|
+
server.registerTool("list_voices", {
|
|
86
|
+
description: "List the curated voice catalog available for make_ugc's `voice` field. Presets are the " +
|
|
87
|
+
"RECOMMENDED path: pick one by name (e.g. voice=\"george\"). `voice_id` is an escape hatch for " +
|
|
88
|
+
"voices OUTSIDE this catalog, including cloned voices — pass a raw vendor voice id there instead " +
|
|
89
|
+
"of `voice`. The current avatar already has a matched default voice; do NOT change it unless the " +
|
|
90
|
+
"user explicitly asks for a different one. This catalog is CURATED BY DESIGN, not the vendor's " +
|
|
91
|
+
"full voice list: the paid vendor's full catalog is intentionally not proxied here, so its API " +
|
|
92
|
+
"key does not need to leave the worker process.",
|
|
93
|
+
inputSchema: {},
|
|
94
|
+
}, async () => {
|
|
95
|
+
// Каталог статический (US-513 core), тула лишь ретранслирует /v1/voices —
|
|
96
|
+
// без сети к вендору, без ключа вендора на этой поверхности (§5).
|
|
97
|
+
const voices = await client.listVoices();
|
|
98
|
+
return { content: [{ type: "text", text: JSON.stringify({ voices }) }] };
|
|
99
|
+
});
|
|
100
|
+
return server;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clipwright/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Clipwright: generate UGC videos from your coding agent",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"modelcontextprotocol",
|
|
8
|
+
"ugc",
|
|
9
|
+
"video"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "Dimantika LLC",
|
|
13
|
+
"homepage": "https://clipwright.io",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"SKILL.md"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"bin": {
|
|
26
|
+
"clipwright-mcp": "dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
30
|
+
"zod": "^4.4.3",
|
|
31
|
+
"@clipwright/core": "0.1.0",
|
|
32
|
+
"@clipwright/sdk": "0.1.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^26.1.1"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc -p tsconfig.json",
|
|
39
|
+
"typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
|
|
40
|
+
"dev": "tsx src/index.ts"
|
|
41
|
+
}
|
|
42
|
+
}
|