instantclips-mcp 1.3.0 → 1.4.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/README.es.md +22 -0
- package/README.md +21 -0
- package/README.zh-CN.md +18 -0
- package/bin/instantclips-mcp.js +153 -1
- package/manifest/instantclips-mcp.json +151 -5
- package/package.json +1 -1
package/README.es.md
CHANGED
|
@@ -162,6 +162,14 @@ El token solo se acepta mediante `INSTANTCLIPS_TOKEN`, nunca como argumento de l
|
|
|
162
162
|
por lo que no aparece en la lista de procesos. Es obligatorio para llamar a una herramienta, pero
|
|
163
163
|
no para `initialize`, `ping` ni `tools/list`. Se requiere Node.js 20 o posterior.
|
|
164
164
|
|
|
165
|
+
Fotos en este equipo: el adaptador puede subirlas por sí mismo, así que se puede hacer un vídeo
|
|
166
|
+
con archivos que nunca salieron de tu portátil. A través del adaptador, `create_product_from_images`
|
|
167
|
+
acepta `image_paths` (rutas a los archivos, hasta 9, de 8 MB cada uno) en lugar de `image_urls`, y
|
|
168
|
+
`update_product` acepta `add_image_paths`. Los archivos se empaquetan y se envían directamente a
|
|
169
|
+
InstantClips como fotos del producto; no se descarga nada de ningún sitio. Arrastra los archivos a
|
|
170
|
+
un cliente que le pase sus rutas al asistente (Claude Code, Cursor, VS Code, agentes de terminal) y
|
|
171
|
+
di qué quieres crear.
|
|
172
|
+
|
|
165
173
|
## Instrucciones para empezar
|
|
166
174
|
|
|
167
175
|
Cinco para arrancar. Sustituye el enlace o el nombre del producto.
|
|
@@ -260,6 +268,20 @@ de nombres de marca `ai.instantclips`; no lo sustituyas por un nombre `io.github
|
|
|
260
268
|
firma no se guarda en el repositorio: `.gitignore` cubre `*.pem`, y una clave privada incluida en un
|
|
261
269
|
commit es una clave publicada.
|
|
262
270
|
|
|
271
|
+
Inicia sesión justo antes de publicar — el token del registro caduca en menos de una hora — y el
|
|
272
|
+
dominio se verifica por HTTP, no por DNS: `instantclips.ai/.well-known/mcp-registry-auth`, en el
|
|
273
|
+
sitio de marketing, sirve la mitad pública de esta clave (`v=MCPv1; k=ed25519; p=…`); no hay
|
|
274
|
+
registro TXT, así que `login dns` falla con "no MCP public key found".
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
mcp-publisher login http --domain instantclips.ai \
|
|
278
|
+
--private-key "$(openssl pkey -in key.pem -text -noout | awk '/priv:/{f=1;next} /pub:/{f=0} f' | tr -d ' :\n')"
|
|
279
|
+
mcp-publisher publish
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
`test/shim.test.js` fija la versión del servidor de la instantánea y la `version` de `server.json`;
|
|
283
|
+
cada publicación actualiza ambas.
|
|
284
|
+
|
|
263
285
|
`glama.json` es el archivo independiente y específico de Glama que permite reclamar allí la ficha.
|
|
264
286
|
Un servidor perteneciente a una organización, en lugar de una cuenta personal, solo puede
|
|
265
287
|
reclamarse si ese archivo está presente.
|
package/README.md
CHANGED
|
@@ -152,6 +152,13 @@ The token is accepted only through `INSTANTCLIPS_TOKEN`, never as a command-line
|
|
|
152
152
|
does not appear in the process list. It is required for tool calls, but not for `initialize`,
|
|
153
153
|
`ping`, or `tools/list`. Node.js 20 or newer is required.
|
|
154
154
|
|
|
155
|
+
Photos on this machine: the adapter can upload them itself, so a video can be made from files
|
|
156
|
+
that never left your laptop. Through the adapter, `create_product_from_images` takes `image_paths`
|
|
157
|
+
(paths to the files, up to 9, 8 MB each) in place of `image_urls`, and `update_product` takes
|
|
158
|
+
`add_image_paths`. The files are packaged and posted straight to InstantClips as the product's
|
|
159
|
+
photos — nothing is downloaded from anywhere. Drag the files into a client that hands the
|
|
160
|
+
assistant their paths (Claude Code, Cursor, VS Code, terminal agents) and say what you want made.
|
|
161
|
+
|
|
155
162
|
## Starter prompts
|
|
156
163
|
|
|
157
164
|
Five to begin with. Swap in a link or a product name.
|
|
@@ -242,6 +249,20 @@ Publish the npm package first, then re-publish this same registry entry with
|
|
|
242
249
|
`ai.instantclips` namespace; do not replace it with an `io.github.*` name. The signing key stays out
|
|
243
250
|
of the repository — `.gitignore` covers `*.pem`, and a committed private key is a published one.
|
|
244
251
|
|
|
252
|
+
Log in right before publishing — the registry token expires within the hour — and the domain is
|
|
253
|
+
verified over HTTP, not DNS: `instantclips.ai/.well-known/mcp-registry-auth` on the marketing site
|
|
254
|
+
serves this key's public half (`v=MCPv1; k=ed25519; p=…`), and there is no TXT record, so
|
|
255
|
+
`login dns` fails with "no MCP public key found".
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
mcp-publisher login http --domain instantclips.ai \
|
|
259
|
+
--private-key "$(openssl pkey -in key.pem -text -noout | awk '/priv:/{f=1;next} /pub:/{f=0} f' | tr -d ' :\n')"
|
|
260
|
+
mcp-publisher publish
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`test/shim.test.js` pins the snapshot's server version and `server.json`'s `version`; a release
|
|
264
|
+
moves both.
|
|
265
|
+
|
|
245
266
|
`glama.json` is the separate, Glama-specific file that claims the listing there. A server under an
|
|
246
267
|
organisation rather than a personal account can only be claimed with that file present.
|
|
247
268
|
It carries ownership only. In Glama's Dockerfile form, use build steps
|
package/README.zh-CN.md
CHANGED
|
@@ -139,6 +139,12 @@ INSTANTCLIPS_TOKEN="your-token" npx -y instantclips-mcp --check --json
|
|
|
139
139
|
令牌只能通过 `INSTANTCLIPS_TOKEN` 提供,不能作为命令行参数传入,因此不会出现在进程列表中。
|
|
140
140
|
工具调用需要令牌,但 `initialize`、`ping` 和 `tools/list` 不需要。需要 Node.js 20 或更高版本。
|
|
141
141
|
|
|
142
|
+
本机上的照片:适配器可以自行上传,因此可以用从未离开你笔记本电脑的文件制作视频。通过适配器,
|
|
143
|
+
`create_product_from_images` 可用 `image_paths`(文件路径,最多 9 个,每个不超过 8 MB)代替
|
|
144
|
+
`image_urls`,`update_product` 可用 `add_image_paths`。文件会被打包并直接发送到 InstantClips 作为商品照片,
|
|
145
|
+
不会从任何地方下载。把文件拖进会把路径交给助手的客户端(Claude Code、Cursor、VS Code、终端智能体),
|
|
146
|
+
然后说出你想要的视频即可。
|
|
147
|
+
|
|
142
148
|
## 入门提示语
|
|
143
149
|
|
|
144
150
|
先从这五句开始,替换成你的链接或商品名即可。
|
|
@@ -224,6 +230,18 @@ npm 软件包中的 `mcpName` 必须与该注册表名称完全一致。仓库
|
|
|
224
230
|
域名身份验证会保留品牌命名空间 `ai.instantclips`;请勿将其替换为 `io.github.*` 名称。签名密钥不
|
|
225
231
|
存放在仓库中:`.gitignore` 已忽略 `*.pem`,因为一旦提交私钥,就等于公开了私钥。
|
|
226
232
|
|
|
233
|
+
发布前再登录一次——注册表令牌不到一小时就会过期——域名验证走的是 HTTP 而非 DNS:营销站点上的
|
|
234
|
+
`instantclips.ai/.well-known/mcp-registry-auth` 提供这把密钥的公钥部分(`v=MCPv1; k=ed25519; p=…`),
|
|
235
|
+
没有 TXT 记录,所以 `login dns` 会报 "no MCP public key found"。
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
mcp-publisher login http --domain instantclips.ai \
|
|
239
|
+
--private-key "$(openssl pkey -in key.pem -text -noout | awk '/priv:/{f=1;next} /pub:/{f=0} f' | tr -d ' :\n')"
|
|
240
|
+
mcp-publisher publish
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`test/shim.test.js` 固定了快照的服务器版本和 `server.json` 的 `version`;每次发布都要同时更新这两处。
|
|
244
|
+
|
|
227
245
|
`glama.json` 是 Glama 专用的独立文件,用于认领该平台上的条目。归属于组织而非个人账户的服务器,
|
|
228
246
|
只有在该文件存在时才能完成认领。
|
|
229
247
|
该文件只用于证明所有权。在 Glama 的 Dockerfile 表单中,将构建步骤设为
|
package/bin/instantclips-mcp.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { basename, extname } from "node:path";
|
|
4
6
|
|
|
5
7
|
import {
|
|
6
8
|
Client,
|
|
@@ -129,6 +131,154 @@ function writeDiagnostic(failure) {
|
|
|
129
131
|
process.stderr.write(`instantclips-mcp: ${failure.message}\n`);
|
|
130
132
|
}
|
|
131
133
|
|
|
134
|
+
// ---- Photos on this machine ------------------------------------------------
|
|
135
|
+
//
|
|
136
|
+
// The hosted server cannot read the caller's disk; this adapter runs on it and
|
|
137
|
+
// can. So two parameters exist only here: `image_paths` on
|
|
138
|
+
// create_product_from_images and `add_image_paths` on update_product. They
|
|
139
|
+
// are added to the advertised schemas at serve time and handled before any
|
|
140
|
+
// proxying, which keeps the bundled manifest exactly what the hosted server
|
|
141
|
+
// serves. The files are read and posted as multipart to the hosted upload
|
|
142
|
+
// endpoints under the same bearer — packaged and uploaded, never downloaded
|
|
143
|
+
// from anywhere.
|
|
144
|
+
const MAX_LOCAL_IMAGES = 9;
|
|
145
|
+
const MAX_LOCAL_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
146
|
+
const LOCAL_IMAGE_TYPES = {
|
|
147
|
+
".jpg": "image/jpeg",
|
|
148
|
+
".jpeg": "image/jpeg",
|
|
149
|
+
".png": "image/png",
|
|
150
|
+
".webp": "image/webp",
|
|
151
|
+
".gif": "image/gif",
|
|
152
|
+
".bmp": "image/bmp",
|
|
153
|
+
".tif": "image/tiff",
|
|
154
|
+
".tiff": "image/tiff",
|
|
155
|
+
".heic": "image/heic",
|
|
156
|
+
".heif": "image/heif",
|
|
157
|
+
};
|
|
158
|
+
const LOCAL_UPLOAD_PARAMETERS = {
|
|
159
|
+
create_product_from_images: {
|
|
160
|
+
name: "image_paths",
|
|
161
|
+
replaces: "image_urls",
|
|
162
|
+
description:
|
|
163
|
+
`Paths to photos on this machine, up to ${MAX_LOCAL_IMAGES}, ${MAX_LOCAL_IMAGE_BYTES / 1024 / 1024} MB each. ` +
|
|
164
|
+
"This adapter uploads the files itself; use it instead of image_urls for local photos.",
|
|
165
|
+
note:
|
|
166
|
+
"Through this adapter, `image_paths` (files on this machine) can replace `image_urls`; the files are uploaded directly.",
|
|
167
|
+
},
|
|
168
|
+
update_product: {
|
|
169
|
+
name: "add_image_paths",
|
|
170
|
+
description:
|
|
171
|
+
`Paths to photos on this machine to add, up to ${MAX_LOCAL_IMAGES}, ${MAX_LOCAL_IMAGE_BYTES / 1024 / 1024} MB each. ` +
|
|
172
|
+
"This adapter uploads the files itself. Send it with product_id alone; other fields go in a separate call.",
|
|
173
|
+
note:
|
|
174
|
+
"Through this adapter, `add_image_paths` (files on this machine) adds photos; the files are uploaded directly.",
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
function withLocalUploads(tools) {
|
|
179
|
+
return tools.map((tool) => {
|
|
180
|
+
const extra = LOCAL_UPLOAD_PARAMETERS[tool.name];
|
|
181
|
+
if (!extra) return tool;
|
|
182
|
+
const schema = tool.inputSchema || { type: "object", properties: {} };
|
|
183
|
+
const properties = {
|
|
184
|
+
...schema.properties,
|
|
185
|
+
[extra.name]: { type: "array", items: { type: "string" }, description: extra.description },
|
|
186
|
+
};
|
|
187
|
+
const inputSchema = { ...schema, properties };
|
|
188
|
+
if (extra.replaces && Array.isArray(schema.required)) {
|
|
189
|
+
inputSchema.required = schema.required.filter((key) => key !== extra.replaces);
|
|
190
|
+
}
|
|
191
|
+
return { ...tool, description: `${tool.description}\n\n${extra.note}`, inputSchema };
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function hasPaths(value) {
|
|
196
|
+
return Array.isArray(value) && value.length > 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// A CliError, so safeFailure relays the message verbatim. Built at call
|
|
200
|
+
// time: CliError is declared further down and class declarations do not hoist.
|
|
201
|
+
function uploadError(message) {
|
|
202
|
+
return new CliError("upload_failed", message, 1);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function appendLocalImages(form, paths) {
|
|
206
|
+
const list = paths.map((path) => String(path).trim()).filter(Boolean);
|
|
207
|
+
if (list.length === 0) throw uploadError("No image paths were given.");
|
|
208
|
+
if (list.length > MAX_LOCAL_IMAGES) {
|
|
209
|
+
throw uploadError(`At most ${MAX_LOCAL_IMAGES} images per call.`);
|
|
210
|
+
}
|
|
211
|
+
for (const path of list) {
|
|
212
|
+
const type = LOCAL_IMAGE_TYPES[extname(path).toLowerCase()];
|
|
213
|
+
if (!type) {
|
|
214
|
+
throw uploadError(`${path} is not an image type the video model takes (jpg, png, webp, gif, bmp, tiff, heic).`);
|
|
215
|
+
}
|
|
216
|
+
let bytes;
|
|
217
|
+
try {
|
|
218
|
+
bytes = await readFile(path);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
throw uploadError(`${path}: ${error.code === "ENOENT" ? "no such file" : error.message}.`);
|
|
221
|
+
}
|
|
222
|
+
if (bytes.byteLength > MAX_LOCAL_IMAGE_BYTES) {
|
|
223
|
+
throw uploadError(`${path} is larger than ${MAX_LOCAL_IMAGE_BYTES / 1024 / 1024} MB.`);
|
|
224
|
+
}
|
|
225
|
+
form.append("images[]", new Blob([bytes], { type }), basename(path));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function uploadsUrl(endpoint, suffix) {
|
|
230
|
+
const base = endpoint.pathname.replace(/\/$/, "");
|
|
231
|
+
return new URL(`${base}/${suffix}`, endpoint);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function uploadForm(config, suffix, form) {
|
|
235
|
+
if (!config.token) {
|
|
236
|
+
throw new CliError(
|
|
237
|
+
"missing_token",
|
|
238
|
+
`INSTANTCLIPS_TOKEN is required for tool calls. Create a token at ${SETTINGS_URL}.`,
|
|
239
|
+
78,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
const response = await fetch(uploadsUrl(config.endpoint, suffix), {
|
|
243
|
+
method: "POST",
|
|
244
|
+
headers: { Authorization: `Bearer ${config.token}` },
|
|
245
|
+
body: form,
|
|
246
|
+
});
|
|
247
|
+
const text = await response.text();
|
|
248
|
+
if (response.status === 401) {
|
|
249
|
+
const error = new Error(`401 ${text}`);
|
|
250
|
+
error.data = { status: 401 };
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
return { content: [{ type: "text", text }], isError: !response.ok };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// → a tool result when the call is one this adapter answers itself, else null.
|
|
257
|
+
async function callLocally(config, params) {
|
|
258
|
+
const args = params.arguments ?? {};
|
|
259
|
+
if (params.name === "create_product_from_images" && hasPaths(args.image_paths)) {
|
|
260
|
+
const form = new FormData();
|
|
261
|
+
for (const key of ["name", "description", "creator_note", "brand_id"]) {
|
|
262
|
+
if (args[key] !== undefined && args[key] !== null && args[key] !== "") form.append(key, String(args[key]));
|
|
263
|
+
}
|
|
264
|
+
await appendLocalImages(form, args.image_paths);
|
|
265
|
+
return uploadForm(config, "products", form);
|
|
266
|
+
}
|
|
267
|
+
if (params.name === "update_product" && hasPaths(args.add_image_paths)) {
|
|
268
|
+
const others = Object.keys(args).filter((key) => !["product_id", "add_image_paths"].includes(key));
|
|
269
|
+
if (!args.product_id) throw uploadError("product_id is required with add_image_paths.");
|
|
270
|
+
if (others.length > 0) {
|
|
271
|
+
throw uploadError(
|
|
272
|
+
`Send add_image_paths with product_id alone; ${others.join(", ")} go in a separate update_product call.`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
const form = new FormData();
|
|
276
|
+
await appendLocalImages(form, args.add_image_paths);
|
|
277
|
+
return uploadForm(config, `products/${encodeURIComponent(String(args.product_id))}/images`, form);
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
132
282
|
async function startBridge(config) {
|
|
133
283
|
const stdio = new StdioServerTransport();
|
|
134
284
|
const server = new Server(manifest.serverInfo, {
|
|
@@ -161,11 +311,13 @@ async function startBridge(config) {
|
|
|
161
311
|
};
|
|
162
312
|
|
|
163
313
|
server.setRequestHandler("tools/list", async () => ({
|
|
164
|
-
tools: manifest.tools,
|
|
314
|
+
tools: withLocalUploads(manifest.tools),
|
|
165
315
|
}));
|
|
166
316
|
|
|
167
317
|
server.setRequestHandler("tools/call", async (request) => {
|
|
168
318
|
try {
|
|
319
|
+
const local = await callLocally(config, request.params);
|
|
320
|
+
if (local) return local;
|
|
169
321
|
const client = await connectRemote();
|
|
170
322
|
return await client.callTool(request.params);
|
|
171
323
|
} catch (error) {
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
"serverInfo": {
|
|
8
8
|
"name": "instantclips",
|
|
9
9
|
"title": "InstantClips",
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "0.5.0",
|
|
11
11
|
"websiteUrl": "https://instantclips.ai"
|
|
12
12
|
},
|
|
13
|
-
"instructions": "InstantClips turns a product into a short vertical marketing video.\n\nEvery video is drafted against a BRAND — a voice, a target market and a\nset of keywords. Getting the brand right matters more than anything else\nhere: a product drafted under the wrong company's voice renders perfectly\nand is still unusable. Never assume a product belongs to whatever brand\nthe account already has.\n\nStart by asking the user for their store's URL if you do not already know\nthe brand, and call `list_brands` to see what the account has. Unlike the\nwebsite, which takes a product link and gets out of the way, you can\nsimply ask — a short question here is cheaper than a wrongly branded\nvideo.\n\nThe workflow, in order:\n\n1. `import_product_from_url` with the product's page URL (or\n `create_product_from_images` with hosted image URLs). Both return\n immediately and import in the background.\n2. Poll `get_product` until `import_status` is \"imported\".\n - If it reports `brand_decision_required` with `drafting: false`, the\n brand could not be settled: the storefront does not match any brand\n on the account, or (photos) there was no page to detect one from.\n Stop and put the choice to the user: create a brand for it\n (`create_brand`) or attach it to one they already have\n (`set_product_brand`). Do not choose for them. For a photos product\n nothing was detected, so `create_brand` needs the name from the\n user, and the voice, target market and keywords they can give you.\n No direction is drafted until this is settled.\n - Otherwise the import also drafts a video direction — a six-section\n creative plan (Hook / Content Focus / Format / Suggesting Visual\n Aesthetic / Execution Guidelines /\n Strict Guidelines & Restrictions) — so keep polling until\n `video_direction.drafting` is false.\n3. Show the drafted direction to the user. Edit it with\n `update_video_direction`, or roll a completely different angle with\n `redraft_video_direction`. The direction is optional: an empty one is\n valid and generation still works.\n4. `generate_video`, passing `expected_credit_cost` — this SPENDS THE\n USER'S CREDITS. Get the user's explicit go-ahead first, and tell\n them the credit cost that `get_product` reports; the launch is\n refused, uncharged, if that number no longer matches.\n5. Poll `get_video` until status is \"done\", then give the user\n `output_url` (the finished MP4) and `share_url` (a public page).\n On a free account the MP4 carries the InstantClips watermark\n (`watermarked` is true); buying credits removes it from every video\n on the account.\n\nEvery get_product response carries `next_step`: what to do now and the\ntool to do it with. Read it before choosing a tool — it covers the\nstates this list does not: a failed import, a failed or stalled\ndirection draft, photos the video model will not take, a failed\nrender, a balance short of the cost.\n\nA product whose video has been generated is not finished with, but a\ngenerated video cannot be changed: its direction locks the moment\ngeneration starts, and re-importing the same URL returns the same\nproduct rather than a fresh one. Editing or redrafting such a product\nopens its next video's draft, seeded from the last one (the response\nsays `opened_new_video: true`), and `generate_video` with no draft\nrenders another take of the last plan as a new video. Continue from\nstep 3 either way.\n\nA render takes a few minutes. Poll every 20-30 seconds rather than in a\ntight loop, and tell the user what you are waiting on.\n",
|
|
13
|
+
"instructions": "InstantClips turns a product into a short vertical marketing video.\n\nEvery video is drafted against a BRAND — a voice, a target market and a\nset of keywords. Getting the brand right matters more than anything else\nhere: a product drafted under the wrong company's voice renders perfectly\nand is still unusable. Never assume a product belongs to whatever brand\nthe account already has.\n\nStart by asking the user for their store's URL if you do not already know\nthe brand, and call `list_brands` to see what the account has. Unlike the\nwebsite, which takes a product link and gets out of the way, you can\nsimply ask — a short question here is cheaper than a wrongly branded\nvideo.\n\nThe workflow, in order:\n\n1. `import_product_from_url` with the product's page URL (or\n `create_product_from_images` with hosted image URLs). Both return\n immediately and import in the background. If the user already made\n the product on the website — dropped photos on the workbench, pasted\n a link there — find it with `list_products` (newest first) and\n continue from step 2.\n2. Poll `get_product` until `import_status` is \"imported\".\n - If it reports `brand_decision_required` with `drafting: false`, the\n brand could not be settled: the storefront does not match any brand\n on the account, or (photos) there was no page to detect one from.\n Stop and put the choice to the user: create a brand for it\n (`create_brand`) or attach it to one they already have\n (`set_product_brand`). Do not choose for them. For a photos product\n nothing was detected, so `create_brand` needs the name from the\n user, and the voice, target market and keywords they can give you.\n No direction is drafted until this is settled.\n - Otherwise the import also drafts a video direction — a six-section\n creative plan (Hook / Content Focus / Format / Suggesting Visual\n Aesthetic / Execution Guidelines /\n Strict Guidelines & Restrictions) — so keep polling until\n `video_direction.drafting` is false.\n3. Show the drafted direction to the user. Edit it with\n `update_video_direction`, or roll a completely different angle with\n `redraft_video_direction`. The direction is optional: an empty one is\n valid and generation still works. The render settings — ratio,\n resolution, duration_seconds, enable_audio — are parameters of\n `update_video_direction` too, and are never read from the direction\n text. So are the photos: `images` in `get_product` lists every one\n with `usable` and `selected`, and `selected_image_ids` chooses which\n the render uses. `format` and `format_options` name the angle a\n redraft can pin. `update_product` corrects the facts a draft is\n written from (name, description, price, the posted link, the photos)\n and `update_brand` the identity (voice, market, keywords); both feed\n the next draft, so redraft after.\n4. `generate_video`, passing `expected_credit_cost` — this SPENDS THE\n USER'S CREDITS. Get the user's explicit go-ahead first, and tell\n them the credit cost that `get_product` reports; the launch is\n refused, uncharged, if that number no longer matches.\n5. Poll `get_video` until status is \"done\", then give the user\n `output_url` (the finished MP4) and `share_url` (a public page).\n On a free account the MP4 carries the InstantClips watermark\n (`watermarked` is true); buying credits removes it from every video\n on the account.\n\nEvery get_product response carries `next_step`: what to do now and the\ntool to do it with. Read it before choosing a tool — it covers the\nstates this list does not: a failed import, a failed or stalled\ndirection draft, photos the video model will not take, a failed\nrender, a balance short of the cost.\n\nA product whose video has been generated is not finished with, but a\ngenerated video cannot be changed: its direction locks the moment\ngeneration starts, and re-importing the same URL returns the same\nproduct rather than a fresh one. Editing or redrafting such a product\nopens its next video's draft, seeded from the last one (the response\nsays `opened_new_video: true`), and `generate_video` with no draft\nrenders another take of the last plan as a new video. Continue from\nstep 3 either way.\n\nA render takes a few minutes. Poll every 20-30 seconds rather than in a\ntight loop, and tell the user what you are waiting on.\n",
|
|
14
14
|
"tools": [
|
|
15
15
|
{
|
|
16
16
|
"name": "list_brands",
|
|
@@ -29,6 +29,38 @@
|
|
|
29
29
|
"openWorldHint": false
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
|
+
{
|
|
33
|
+
"name": "list_products",
|
|
34
|
+
"title": "List the account's products",
|
|
35
|
+
"description": "List the account's products, newest first — the ones imported here, on\nthe website, or from photos dropped on the workbench. Use it to find a\nproduct_id you do not have: a product the user made on the website, or\none from an earlier conversation. Then `get_product` for its full state.\n\n`query` matches the name or the source URL, `brand_id` narrows to one\nbrand (see `list_brands`), and `limit` caps the list (default 20, at\nmost 50). A product still waiting on its brand decision shows\n`brand_decision_required: true` and no brand.\n\nThis does not spend credits.\n",
|
|
36
|
+
"inputSchema": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {
|
|
39
|
+
"query": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "Matches the product's name or source URL, case-insensitively."
|
|
42
|
+
},
|
|
43
|
+
"brand_id": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "Only this brand's products."
|
|
46
|
+
},
|
|
47
|
+
"limit": {
|
|
48
|
+
"type": "integer",
|
|
49
|
+
"minimum": 1,
|
|
50
|
+
"maximum": 50,
|
|
51
|
+
"description": "How many, newest first. Default 20."
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"required": [],
|
|
55
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema"
|
|
56
|
+
},
|
|
57
|
+
"annotations": {
|
|
58
|
+
"readOnlyHint": true,
|
|
59
|
+
"destructiveHint": false,
|
|
60
|
+
"idempotentHint": true,
|
|
61
|
+
"openWorldHint": false
|
|
62
|
+
}
|
|
63
|
+
},
|
|
32
64
|
{
|
|
33
65
|
"name": "import_product_from_url",
|
|
34
66
|
"title": "Import a product from its page URL",
|
|
@@ -105,7 +137,7 @@
|
|
|
105
137
|
{
|
|
106
138
|
"name": "get_product",
|
|
107
139
|
"title": "Get a product and its video direction",
|
|
108
|
-
"description": "Read a product: what the import found, the current video direction and\nsettings, and every video generated from it.\n\nUse this to poll after `import_product_from_url` or\n`create_product_from_images`. The product is ready to work with when\n`import_status` is \"imported\", there is no `brand_decision_required`,\nAND `video_direction.drafting` is false. If a brand decision is present\nwith `drafting: false`, ask the user to choose a brand and resolve it\nwith `create_brand` or `set_product_brand` before waiting for a direction.\nPoll every 20-30 seconds while import or drafting is in progress.\n\n`import_status` values: \"pending\" and \"importing\" mean keep polling;\n\"imported\" means done; \"failed\" means it did not work and\n`import_failed_reason` says why.\n\n`video_direction.credit_cost` is what `generate_video` will charge for\nthe current settings.\n\nOnce generation has started the direction is locked and\n`video_direction.editable` is false. Editing or redrafting then opens\nthe next video's draft, and `next_step` says so.\n",
|
|
140
|
+
"description": "Read a product: what the import found, the current video direction and\nsettings, and every video generated from it.\n\nUse this to poll after `import_product_from_url` or\n`create_product_from_images`. The product is ready to work with when\n`import_status` is \"imported\", there is no `brand_decision_required`,\nAND `video_direction.drafting` is false. If a brand decision is present\nwith `drafting: false`, ask the user to choose a brand and resolve it\nwith `create_brand` or `set_product_brand` before waiting for a direction.\nPoll every 20-30 seconds while import or drafting is in progress.\n\n`import_status` values: \"pending\" and \"importing\" mean keep polling;\n\"imported\" means done; \"failed\" means it did not work and\n`import_failed_reason` says why.\n\n`video_direction.credit_cost` is what `generate_video` will charge for\nthe current settings. `duration_seconds`, `ratio`, `resolution` and\n`enable_audio` are those settings; change them with\n`update_video_direction`, not in the direction text.\n\nOnce generation has started the direction is locked and\n`video_direction.editable` is false. Editing or redrafting then opens\nthe next video's draft, and `next_step` says so.\n",
|
|
109
141
|
"inputSchema": {
|
|
110
142
|
"type": "object",
|
|
111
143
|
"properties": {
|
|
@@ -202,10 +234,107 @@
|
|
|
202
234
|
"openWorldHint": true
|
|
203
235
|
}
|
|
204
236
|
},
|
|
237
|
+
{
|
|
238
|
+
"name": "update_brand",
|
|
239
|
+
"title": "Edit a brand's identity",
|
|
240
|
+
"description": "Edit a brand: `name`, `voice` (how it sounds — a few sentences),\n`target_market` (where it sells, e.g. \"US/North America\"; it drives the\nspoken language) and `keywords` (the themes drafts lean on; the list you\npass replaces the old one). This is the identity every video for the\nbrand's products is drafted against. Drafts already written keep their\ntext — `redraft_video_direction` on a product to use the new identity.\n\nOnly the fields you pass change. `list_brands` has the ids and the\ncurrent values. This does not spend credits.\n",
|
|
241
|
+
"inputSchema": {
|
|
242
|
+
"type": "object",
|
|
243
|
+
"properties": {
|
|
244
|
+
"brand_id": {
|
|
245
|
+
"type": "string",
|
|
246
|
+
"description": "The brand's id, from list_brands."
|
|
247
|
+
},
|
|
248
|
+
"name": {
|
|
249
|
+
"type": "string",
|
|
250
|
+
"description": "The brand's name."
|
|
251
|
+
},
|
|
252
|
+
"voice": {
|
|
253
|
+
"type": "string",
|
|
254
|
+
"description": "How the brand sounds, in a few sentences."
|
|
255
|
+
},
|
|
256
|
+
"target_market": {
|
|
257
|
+
"type": "string",
|
|
258
|
+
"description": "Free text, e.g. \"US/North America\" or \"Japan\"."
|
|
259
|
+
},
|
|
260
|
+
"keywords": {
|
|
261
|
+
"type": "array",
|
|
262
|
+
"items": {
|
|
263
|
+
"type": "string"
|
|
264
|
+
},
|
|
265
|
+
"description": "The themes drafts lean on. Replaces the whole list."
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
"required": [
|
|
269
|
+
"brand_id"
|
|
270
|
+
],
|
|
271
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema"
|
|
272
|
+
},
|
|
273
|
+
"annotations": {
|
|
274
|
+
"readOnlyHint": false,
|
|
275
|
+
"destructiveHint": true,
|
|
276
|
+
"idempotentHint": true,
|
|
277
|
+
"openWorldHint": false
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"name": "update_product",
|
|
282
|
+
"title": "Edit a product's facts, link and photos",
|
|
283
|
+
"description": "Edit what a product says about itself. `name`, `description` and\n`price` are the facts the direction draft is written from — fix a\nscrape that got them wrong, then `redraft_video_direction` so the next\ndraft uses them (the response's `next_step` says so when a direction\nalready exists). `link_url` is the product link posted with the video\nand shown on its share page: http(s) only, or empty to clear it.\n\n`add_image_urls` downloads hosted photos and adds them to the product,\nup to 9 per call and 8 MB each; the usable ones join the\nrender's selection while there is room, as the workbench's \"+\" tile\ndoes. `remove_image_ids` deletes photos by the image_id `get_product`\nlists; a removed photo leaves the selection by itself. Only the fields\nyou pass change.\n\nThis does not spend credits.\n",
|
|
284
|
+
"inputSchema": {
|
|
285
|
+
"type": "object",
|
|
286
|
+
"properties": {
|
|
287
|
+
"product_id": {
|
|
288
|
+
"type": "string",
|
|
289
|
+
"description": "The product's id."
|
|
290
|
+
},
|
|
291
|
+
"name": {
|
|
292
|
+
"type": "string",
|
|
293
|
+
"description": "The product's name."
|
|
294
|
+
},
|
|
295
|
+
"description": {
|
|
296
|
+
"type": "string",
|
|
297
|
+
"description": "What the product is; the draft reads it. Empty clears it."
|
|
298
|
+
},
|
|
299
|
+
"price": {
|
|
300
|
+
"type": "string",
|
|
301
|
+
"description": "Free text as the store shows it, e.g. \"$29\" or \"¥3,980\". Empty clears it."
|
|
302
|
+
},
|
|
303
|
+
"link_url": {
|
|
304
|
+
"type": "string",
|
|
305
|
+
"description": "The product link posted with the video. http(s) only; empty clears it."
|
|
306
|
+
},
|
|
307
|
+
"add_image_urls": {
|
|
308
|
+
"type": "array",
|
|
309
|
+
"items": {
|
|
310
|
+
"type": "string"
|
|
311
|
+
},
|
|
312
|
+
"description": "Hosted image URLs to download and add, in the order they should appear."
|
|
313
|
+
},
|
|
314
|
+
"remove_image_ids": {
|
|
315
|
+
"type": "array",
|
|
316
|
+
"items": {
|
|
317
|
+
"type": "string"
|
|
318
|
+
},
|
|
319
|
+
"description": "image_id values from get_product's images to delete."
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
"required": [
|
|
323
|
+
"product_id"
|
|
324
|
+
],
|
|
325
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema"
|
|
326
|
+
},
|
|
327
|
+
"annotations": {
|
|
328
|
+
"readOnlyHint": false,
|
|
329
|
+
"destructiveHint": true,
|
|
330
|
+
"idempotentHint": false,
|
|
331
|
+
"openWorldHint": true
|
|
332
|
+
}
|
|
333
|
+
},
|
|
205
334
|
{
|
|
206
335
|
"name": "update_video_direction",
|
|
207
336
|
"title": "Edit the video direction and render settings",
|
|
208
|
-
"description": "Edit the creative direction and render settings for a product's next\nvideo. Only the fields you pass are changed; everything else keeps its\ncurrent value. Supplied text replaces the saved text; an empty creative\ndirection clears it. A target_market change updates the shared brand\nand affects future videos for its other products.\n\n`creative_direction` is the plan the render is built from. It is free\ntext, but the drafts follow a six-section shape that works well and is\nworth preserving when editing:\n\n Hook: ...\n Content Focus: ...\n Format: ...\n Suggesting Visual Aesthetic: ...\n Execution Guidelines: ...\n Strict Guidelines & Restrictions: ...\n\nDo not invent a direction from nothing when one has not been drafted\nyet — call `redraft_video_direction` and edit what comes back. Leaving\nit empty is also valid: generation works without a direction.\n\n`creator_note` is different and smaller: the user's own short note to\nthe director (\"mention it is machine washable\", \"for Father's Day\"). It\nis carried through to the render verbatim, so put the user's words in\nit, not your paraphrase.\n\nA target_market change after a direction exists gets a `next_step` in\nthe response: the direction's wording sets the spoken language, so it\nneeds a redraft (or an edit) to match the new market.\n\nA launched video cannot change: if the product's last video has\nalready launched, this opens the next video's draft (seeded from that\nvideo) and edits that; the response says `opened_new_video: true` and\ncarries the new video_request_id.\n\nThis does not spend credits.\n",
|
|
337
|
+
"description": "Edit the creative direction and render settings for a product's next\nvideo. Only the fields you pass are changed; everything else keeps its\ncurrent value. Supplied text replaces the saved text; an empty creative\ndirection clears it. A target_market change updates the shared brand\nand affects future videos for its other products.\n\n`creative_direction` is the plan the render is built from. It is free\ntext, but the drafts follow a six-section shape that works well and is\nworth preserving when editing:\n\n Hook: ...\n Content Focus: ...\n Format: ...\n Suggesting Visual Aesthetic: ...\n Execution Guidelines: ...\n Strict Guidelines & Restrictions: ...\n\nDo not invent a direction from nothing when one has not been drafted\nyet — call `redraft_video_direction` and edit what comes back. Leaving\nit empty is also valid: generation works without a direction.\n\n`creator_note` is different and smaller: the user's own short note to\nthe director (\"mention it is machine washable\", \"for Father's Day\"). It\nis carried through to the render verbatim, so put the user's words in\nit, not your paraphrase.\n\n`selected_image_ids` chooses which of the product's photos the render\nuses, in order, by the image_id `get_product` lists under `images`\n(usable ones only, at most 9); an empty list restores the\ndefault, the first usable ones. The poll's `selected_image_ids` shows\nwhat would go out now.\n\n`duration_seconds` is the render length — 15, 20, 25 or 30 — and a\nsetting, not part of the direction: writing \"20 seconds\" into the text\nchanges nothing. Longer costs more; the response's `credit_cost` is\nthe new price, and the user must hear it before `generate_video`.\nAbove the plan's ceiling it clamps like resolution (free: 20s).\n\nA target_market change after a direction exists gets a `next_step` in\nthe response: the direction's wording sets the spoken language, so it\nneeds a redraft (or an edit) to match the new market.\n\nA launched video cannot change: if the product's last video has\nalready launched, this opens the next video's draft (seeded from that\nvideo) and edits that; the response says `opened_new_video: true` and\ncarries the new video_request_id.\n\nThis does not spend credits.\n",
|
|
209
338
|
"inputSchema": {
|
|
210
339
|
"type": "object",
|
|
211
340
|
"properties": {
|
|
@@ -240,6 +369,23 @@
|
|
|
240
369
|
],
|
|
241
370
|
"description": "1080P is available to credit-pack purchasers and eligible tiers; a pick above the account's ceiling becomes that ceiling (the response reports what was actually saved)."
|
|
242
371
|
},
|
|
372
|
+
"duration_seconds": {
|
|
373
|
+
"type": "integer",
|
|
374
|
+
"enum": [
|
|
375
|
+
15,
|
|
376
|
+
20,
|
|
377
|
+
25,
|
|
378
|
+
30
|
|
379
|
+
],
|
|
380
|
+
"description": "Render length in seconds. Priced per second, so the response's credit_cost changes with it; a pick above the plan's ceiling becomes that ceiling (free accounts: 20). The direction text never sets the length — this does."
|
|
381
|
+
},
|
|
382
|
+
"selected_image_ids": {
|
|
383
|
+
"type": "array",
|
|
384
|
+
"items": {
|
|
385
|
+
"type": "string"
|
|
386
|
+
},
|
|
387
|
+
"description": "The photos the render uses, in order, by image_id from get_product's images; usable ones only, at most 9. An empty list restores the default (the first usable ones)."
|
|
388
|
+
},
|
|
243
389
|
"enable_audio": {
|
|
244
390
|
"type": "boolean",
|
|
245
391
|
"description": "Whether the render has sound."
|
|
@@ -274,7 +420,7 @@
|
|
|
274
420
|
},
|
|
275
421
|
"format": {
|
|
276
422
|
"type": "string",
|
|
277
|
-
"description": "Optional. Pin
|
|
423
|
+
"description": "Optional. Pin the angle instead of letting the drafter pick one: a key from video_direction.format_options in get_product (this product's shortlist, or the whole catalogue, each with a one-line summary). Unknown keys are ignored, so leave it out unless the user asked for a particular kind of video."
|
|
278
424
|
}
|
|
279
425
|
},
|
|
280
426
|
"required": [
|