instantclips-mcp 1.3.0 → 1.5.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 CHANGED
@@ -94,7 +94,10 @@ Aplicación de Claude: añade un conector personalizado con esta dirección e in
94
94
  lo pida. ChatGPT en la web: activa el modo de desarrollador en Ajustes, Apps, Avanzado y añade la
95
95
  dirección como conector; en un espacio de trabajo Business o Enterprise, un administrador la
96
96
  publica como aplicación para todo el equipo. La aplicación de escritorio de ChatGPT acepta la misma
97
- dirección en Ajustes, Servidores MCP, y la comparte con Codex.
97
+ dirección en Ajustes, Servidores MCP, y la comparte con Codex. En ChatGPT, adjunta las fotos del
98
+ producto a la conversación y pide el vídeo: el plugin recibe los adjuntos directamente
99
+ (`image_files` en `create_product_from_images`, `add_image_files` en `update_product`), así que
100
+ allí las fotos no necesitan adaptador ni token.
98
101
 
99
102
  ### Cualquier otro cliente o agente MCP
100
103
 
@@ -162,6 +165,16 @@ El token solo se acepta mediante `INSTANTCLIPS_TOKEN`, nunca como argumento de l
162
165
  por lo que no aparece en la lista de procesos. Es obligatorio para llamar a una herramienta, pero
163
166
  no para `initialize`, `ping` ni `tools/list`. Se requiere Node.js 20 o posterior.
164
167
 
168
+ Fotos en este equipo: el adaptador puede subirlas por sí mismo, así que se puede hacer un vídeo
169
+ con archivos que nunca salieron de tu portátil. A través del adaptador, `create_product_from_images`
170
+ acepta `image_paths` (rutas a los archivos, hasta 9, de 8 MB cada uno) en lugar de `image_urls`, y
171
+ `update_product` acepta `add_image_paths`. Los archivos se empaquetan y se envían directamente a
172
+ InstantClips como fotos del producto; no se descarga nada de ningún sitio. Arrastra los archivos a
173
+ un cliente que le pase sus rutas al asistente (Claude Code, Cursor, VS Code, agentes de terminal) y
174
+ di qué quieres crear. Envía `image_paths` solo: si en la misma llamada van también `image_urls`, el
175
+ adaptador la rechaza en lugar de descartar las URL; añade las fotos alojadas después con
176
+ `update_product`.
177
+
165
178
  ## Instrucciones para empezar
166
179
 
167
180
  Cinco para arrancar. Sustituye el enlace o el nombre del producto.
@@ -260,6 +273,20 @@ de nombres de marca `ai.instantclips`; no lo sustituyas por un nombre `io.github
260
273
  firma no se guarda en el repositorio: `.gitignore` cubre `*.pem`, y una clave privada incluida en un
261
274
  commit es una clave publicada.
262
275
 
276
+ Inicia sesión justo antes de publicar — el token del registro caduca en menos de una hora — y el
277
+ dominio se verifica por HTTP, no por DNS: `instantclips.ai/.well-known/mcp-registry-auth`, en el
278
+ sitio de marketing, sirve la mitad pública de esta clave (`v=MCPv1; k=ed25519; p=…`); no hay
279
+ registro TXT, así que `login dns` falla con "no MCP public key found".
280
+
281
+ ```bash
282
+ mcp-publisher login http --domain instantclips.ai \
283
+ --private-key "$(openssl pkey -in key.pem -text -noout | awk '/priv:/{f=1;next} /pub:/{f=0} f' | tr -d ' :\n')"
284
+ mcp-publisher publish
285
+ ```
286
+
287
+ `test/shim.test.js` fija la versión del servidor de la instantánea y la `version` de `server.json`;
288
+ cada publicación actualiza ambas.
289
+
263
290
  `glama.json` es el archivo independiente y específico de Glama que permite reclamar allí la ficha.
264
291
  Un servidor perteneciente a una organización, en lugar de una cuenta personal, solo puede
265
292
  reclamarse si ese archivo está presente.
package/README.md CHANGED
@@ -88,6 +88,9 @@ Claude app: add a custom connector with this address and sign in when it asks. C
88
88
  web: turn on Developer mode under Settings, Apps, Advanced, then add the address as a connector;
89
89
  on a Business or Enterprise workspace an admin publishes it as an app for everyone instead. The
90
90
  ChatGPT desktop app takes the same address under Settings, MCP servers, and shares it with Codex.
91
+ In ChatGPT, attach the product photos to the conversation and ask for the video: the plugin takes
92
+ attachments directly (`image_files` on `create_product_from_images`, `add_image_files` on
93
+ `update_product`), so photos need no adapter and no token there.
91
94
 
92
95
  ### Any other MCP client or agent
93
96
 
@@ -152,6 +155,15 @@ The token is accepted only through `INSTANTCLIPS_TOKEN`, never as a command-line
152
155
  does not appear in the process list. It is required for tool calls, but not for `initialize`,
153
156
  `ping`, or `tools/list`. Node.js 20 or newer is required.
154
157
 
158
+ Photos on this machine: the adapter can upload them itself, so a video can be made from files
159
+ that never left your laptop. Through the adapter, `create_product_from_images` takes `image_paths`
160
+ (paths to the files, up to 9, 8 MB each) in place of `image_urls`, and `update_product` takes
161
+ `add_image_paths`. The files are packaged and posted straight to InstantClips as the product's
162
+ photos — nothing is downloaded from anywhere. Drag the files into a client that hands the
163
+ assistant their paths (Claude Code, Cursor, VS Code, terminal agents) and say what you want made.
164
+ Send `image_paths` on its own: with hosted `image_urls` in the same call the adapter refuses
165
+ rather than dropping the URLs; add hosted photos afterwards with `update_product`.
166
+
155
167
  ## Starter prompts
156
168
 
157
169
  Five to begin with. Swap in a link or a product name.
@@ -242,6 +254,20 @@ Publish the npm package first, then re-publish this same registry entry with
242
254
  `ai.instantclips` namespace; do not replace it with an `io.github.*` name. The signing key stays out
243
255
  of the repository — `.gitignore` covers `*.pem`, and a committed private key is a published one.
244
256
 
257
+ Log in right before publishing — the registry token expires within the hour — and the domain is
258
+ verified over HTTP, not DNS: `instantclips.ai/.well-known/mcp-registry-auth` on the marketing site
259
+ serves this key's public half (`v=MCPv1; k=ed25519; p=…`), and there is no TXT record, so
260
+ `login dns` fails with "no MCP public key found".
261
+
262
+ ```bash
263
+ mcp-publisher login http --domain instantclips.ai \
264
+ --private-key "$(openssl pkey -in key.pem -text -noout | awk '/priv:/{f=1;next} /pub:/{f=0} f' | tr -d ' :\n')"
265
+ mcp-publisher publish
266
+ ```
267
+
268
+ `test/shim.test.js` pins the snapshot's server version and `server.json`'s `version`; a release
269
+ moves both.
270
+
245
271
  `glama.json` is the separate, Glama-specific file that claims the listing there. A server under an
246
272
  organisation rather than a personal account can only be claimed with that file present.
247
273
  It carries ownership only. In Glama's Dockerfile form, use build steps
package/README.zh-CN.md CHANGED
@@ -78,7 +78,9 @@ url = "https://app.instantclips.ai/mcp"
78
78
 
79
79
  Claude 应用:用这个地址添加自定义连接器,按提示登录。网页版 ChatGPT:在 设置 › Apps › 高级 中
80
80
  开启开发者模式,再把地址添加为连接器;Business 或 Enterprise 工作区则由管理员发布为全员可用的
81
- 应用。ChatGPT 桌面版在 设置 › MCP 服务器 中填入同一地址,并与 Codex 共享配置。
81
+ 应用。ChatGPT 桌面版在 设置 › MCP 服务器 中填入同一地址,并与 Codex 共享配置。在 ChatGPT 里,把商品照片
82
+ 作为附件添加到对话中并提出需求即可:插件会直接接收附件(`create_product_from_images` 的 `image_files`、
83
+ `update_product` 的 `add_image_files`),因此在那里传照片不需要适配器,也不需要令牌。
82
84
 
83
85
  ### 其他 MCP 客户端或智能体
84
86
 
@@ -139,6 +141,13 @@ INSTANTCLIPS_TOKEN="your-token" npx -y instantclips-mcp --check --json
139
141
  令牌只能通过 `INSTANTCLIPS_TOKEN` 提供,不能作为命令行参数传入,因此不会出现在进程列表中。
140
142
  工具调用需要令牌,但 `initialize`、`ping` 和 `tools/list` 不需要。需要 Node.js 20 或更高版本。
141
143
 
144
+ 本机上的照片:适配器可以自行上传,因此可以用从未离开你笔记本电脑的文件制作视频。通过适配器,
145
+ `create_product_from_images` 可用 `image_paths`(文件路径,最多 9 个,每个不超过 8 MB)代替
146
+ `image_urls`,`update_product` 可用 `add_image_paths`。文件会被打包并直接发送到 InstantClips 作为商品照片,
147
+ 不会从任何地方下载。把文件拖进会把路径交给助手的客户端(Claude Code、Cursor、VS Code、终端智能体),
148
+ 然后说出你想要的视频即可。`image_paths` 请单独发送:同一次调用里若还带有 `image_urls`,适配器会拒绝,
149
+ 而不是悄悄丢弃这些 URL;托管照片可以之后用 `update_product` 添加。
150
+
142
151
  ## 入门提示语
143
152
 
144
153
  先从这五句开始,替换成你的链接或商品名即可。
@@ -224,6 +233,18 @@ npm 软件包中的 `mcpName` 必须与该注册表名称完全一致。仓库
224
233
  域名身份验证会保留品牌命名空间 `ai.instantclips`;请勿将其替换为 `io.github.*` 名称。签名密钥不
225
234
  存放在仓库中:`.gitignore` 已忽略 `*.pem`,因为一旦提交私钥,就等于公开了私钥。
226
235
 
236
+ 发布前再登录一次——注册表令牌不到一小时就会过期——域名验证走的是 HTTP 而非 DNS:营销站点上的
237
+ `instantclips.ai/.well-known/mcp-registry-auth` 提供这把密钥的公钥部分(`v=MCPv1; k=ed25519; p=…`),
238
+ 没有 TXT 记录,所以 `login dns` 会报 "no MCP public key found"。
239
+
240
+ ```bash
241
+ mcp-publisher login http --domain instantclips.ai \
242
+ --private-key "$(openssl pkey -in key.pem -text -noout | awk '/priv:/{f=1;next} /pub:/{f=0} f' | tr -d ' :\n')"
243
+ mcp-publisher publish
244
+ ```
245
+
246
+ `test/shim.test.js` 固定了快照的服务器版本和 `server.json` 的 `version`;每次发布都要同时更新这两处。
247
+
227
248
  `glama.json` 是 Glama 专用的独立文件,用于认领该平台上的条目。归属于组织而非个人账户的服务器,
228
249
  只有在该文件存在时才能完成认领。
229
250
  该文件只用于证明所有权。在 Glama 的 Dockerfile 表单中,将构建步骤设为
@@ -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,200 @@ 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
+ //
145
+ // The hosted server also takes the conversation's attachments — `image_files`
146
+ // and `add_image_files`, declared for ChatGPT through `_meta["openai/fileParams"]`
147
+ // — which a stdio client has no way to fill. Those two parameters and the
148
+ // `_meta` are removed from the schemas served here, so the model sees one
149
+ // local-file parameter and not a hosted one it cannot use. A call that sends
150
+ // `image_paths` together with hosted URLs is refused rather than uploading
151
+ // the files and dropping the URLs, which is what 1.4.0 did.
152
+ const MAX_LOCAL_IMAGES = 9;
153
+ const MAX_LOCAL_IMAGE_BYTES = 8 * 1024 * 1024;
154
+ const LOCAL_IMAGE_TYPES = {
155
+ ".jpg": "image/jpeg",
156
+ ".jpeg": "image/jpeg",
157
+ ".png": "image/png",
158
+ ".webp": "image/webp",
159
+ ".gif": "image/gif",
160
+ ".bmp": "image/bmp",
161
+ ".tif": "image/tiff",
162
+ ".tiff": "image/tiff",
163
+ ".heic": "image/heic",
164
+ ".heif": "image/heif",
165
+ };
166
+ const LOCAL_UPLOAD_PARAMETERS = {
167
+ create_product_from_images: {
168
+ name: "image_paths",
169
+ replaces: "image_urls",
170
+ hides: "image_files",
171
+ description:
172
+ `Paths to photos on this machine, up to ${MAX_LOCAL_IMAGES}, ${MAX_LOCAL_IMAGE_BYTES / 1024 / 1024} MB each. ` +
173
+ "This adapter uploads the files itself; use it instead of image_urls for local photos, never with them.",
174
+ note:
175
+ "Through this adapter, `image_paths` (files on this machine) replaces `image_urls`; the files are uploaded directly. " +
176
+ "Send one or the other, not both.",
177
+ },
178
+ update_product: {
179
+ name: "add_image_paths",
180
+ hides: "add_image_files",
181
+ description:
182
+ `Paths to photos on this machine to add, up to ${MAX_LOCAL_IMAGES}, ${MAX_LOCAL_IMAGE_BYTES / 1024 / 1024} MB each. ` +
183
+ "This adapter uploads the files itself. Send it with product_id alone; other fields go in a separate call.",
184
+ note:
185
+ "Through this adapter, `add_image_paths` (files on this machine) adds photos; the files are uploaded directly.",
186
+ },
187
+ };
188
+
189
+ function withLocalUploads(tools) {
190
+ return tools.map((tool) => {
191
+ const extra = LOCAL_UPLOAD_PARAMETERS[tool.name];
192
+ if (!extra) return tool;
193
+ const schema = tool.inputSchema || { type: "object", properties: {} };
194
+ const { [extra.hides]: _hidden, ...kept } = schema.properties || {};
195
+ const properties = {
196
+ ...kept,
197
+ [extra.name]: { type: "array", items: { type: "string" }, minItems: 1, description: extra.description },
198
+ };
199
+ const inputSchema = { ...schema, properties };
200
+ if (extra.replaces && Array.isArray(schema.required)) {
201
+ inputSchema.required = schema.required.filter((key) => key !== extra.replaces);
202
+ }
203
+ const { _meta, ...rest } = tool;
204
+ return {
205
+ ...rest,
206
+ ...withoutFileParam(_meta, extra.hides),
207
+ description: `${tool.description}\n\n${extra.note}`,
208
+ inputSchema,
209
+ };
210
+ });
211
+ }
212
+
213
+ // → `{ _meta }` with the hidden file parameter's declaration removed, or `{}`
214
+ // when nothing else was in it.
215
+ function withoutFileParam(meta, hidden) {
216
+ if (!meta) return {};
217
+ const { "openai/fileParams": fileParams, ...others } = meta;
218
+ const remaining = (fileParams || []).filter((name) => name !== hidden);
219
+ const kept = remaining.length ? { ...others, "openai/fileParams": remaining } : others;
220
+ return Object.keys(kept).length ? { _meta: kept } : {};
221
+ }
222
+
223
+ function hasPaths(value) {
224
+ return Array.isArray(value) && value.length > 0;
225
+ }
226
+
227
+ // A CliError, so safeFailure relays the message verbatim. Built at call
228
+ // time: CliError is declared further down and class declarations do not hoist.
229
+ function uploadError(message) {
230
+ return new CliError("upload_failed", message, 1);
231
+ }
232
+
233
+ async function appendLocalImages(form, paths) {
234
+ const list = paths.map((path) => String(path).trim()).filter(Boolean);
235
+ if (list.length === 0) throw uploadError("No image paths were given.");
236
+ if (list.length > MAX_LOCAL_IMAGES) {
237
+ throw uploadError(`At most ${MAX_LOCAL_IMAGES} images per call.`);
238
+ }
239
+ for (const path of list) {
240
+ const type = LOCAL_IMAGE_TYPES[extname(path).toLowerCase()];
241
+ if (!type) {
242
+ throw uploadError(`${path} is not an image type the video model takes (jpg, png, webp, gif, bmp, tiff, heic).`);
243
+ }
244
+ let bytes;
245
+ try {
246
+ bytes = await readFile(path);
247
+ } catch (error) {
248
+ throw uploadError(`${path}: ${error.code === "ENOENT" ? "no such file" : error.message}.`);
249
+ }
250
+ if (bytes.byteLength > MAX_LOCAL_IMAGE_BYTES) {
251
+ throw uploadError(`${path} is larger than ${MAX_LOCAL_IMAGE_BYTES / 1024 / 1024} MB.`);
252
+ }
253
+ form.append("images[]", new Blob([bytes], { type }), basename(path));
254
+ }
255
+ }
256
+
257
+ function uploadsUrl(endpoint, suffix) {
258
+ const base = endpoint.pathname.replace(/\/$/, "");
259
+ return new URL(`${base}/${suffix}`, endpoint);
260
+ }
261
+
262
+ async function uploadForm(config, suffix, form) {
263
+ if (!config.token) {
264
+ throw new CliError(
265
+ "missing_token",
266
+ `INSTANTCLIPS_TOKEN is required for tool calls. Create a token at ${SETTINGS_URL}.`,
267
+ 78,
268
+ );
269
+ }
270
+ const response = await fetch(uploadsUrl(config.endpoint, suffix), {
271
+ method: "POST",
272
+ headers: { Authorization: `Bearer ${config.token}` },
273
+ body: form,
274
+ });
275
+ const text = await response.text();
276
+ if (response.status === 401) {
277
+ const error = new Error(`401 ${text}`);
278
+ error.data = { status: 401 };
279
+ throw error;
280
+ }
281
+ if (!response.ok) return { content: [{ type: "text", text }], isError: true };
282
+ // The hosted tools declare output schemas, and a validating client refuses
283
+ // a result that has one but no structuredContent — so the upload's payload
284
+ // (the same product object get_product returns) travels both ways.
285
+ const result = { content: [{ type: "text", text }], isError: false };
286
+ try {
287
+ const parsed = JSON.parse(text);
288
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) result.structuredContent = parsed;
289
+ } catch {
290
+ // Not JSON: the text block stands alone, as it always did.
291
+ }
292
+ return result;
293
+ }
294
+
295
+ // → a tool result when the call is one this adapter answers itself, else null.
296
+ async function callLocally(config, params) {
297
+ const args = params.arguments ?? {};
298
+ if (params.name === "create_product_from_images" && hasPaths(args.image_paths)) {
299
+ const hosted = ["image_urls", "image_files"].filter((key) => Array.isArray(args[key]) && args[key].length > 0);
300
+ if (hosted.length > 0) {
301
+ throw uploadError(
302
+ `Send image_paths alone: ${hosted.join(", ")} would be dropped. Create the product from image_paths, ` +
303
+ "then add hosted photos with update_product's add_image_urls.",
304
+ );
305
+ }
306
+ const form = new FormData();
307
+ for (const key of ["name", "description", "creator_note", "brand_id"]) {
308
+ if (args[key] !== undefined && args[key] !== null && args[key] !== "") form.append(key, String(args[key]));
309
+ }
310
+ await appendLocalImages(form, args.image_paths);
311
+ return uploadForm(config, "products", form);
312
+ }
313
+ if (params.name === "update_product" && hasPaths(args.add_image_paths)) {
314
+ const others = Object.keys(args).filter((key) => !["product_id", "add_image_paths"].includes(key));
315
+ if (!args.product_id) throw uploadError("product_id is required with add_image_paths.");
316
+ if (others.length > 0) {
317
+ throw uploadError(
318
+ `Send add_image_paths with product_id alone; ${others.join(", ")} go in a separate update_product call.`,
319
+ );
320
+ }
321
+ const form = new FormData();
322
+ await appendLocalImages(form, args.add_image_paths);
323
+ return uploadForm(config, `products/${encodeURIComponent(String(args.product_id))}/images`, form);
324
+ }
325
+ return null;
326
+ }
327
+
132
328
  async function startBridge(config) {
133
329
  const stdio = new StdioServerTransport();
134
330
  const server = new Server(manifest.serverInfo, {
@@ -161,11 +357,13 @@ async function startBridge(config) {
161
357
  };
162
358
 
163
359
  server.setRequestHandler("tools/list", async () => ({
164
- tools: manifest.tools,
360
+ tools: withLocalUploads(manifest.tools),
165
361
  }));
166
362
 
167
363
  server.setRequestHandler("tools/call", async (request) => {
168
364
  try {
365
+ const local = await callLocally(config, request.params);
366
+ if (local) return local;
169
367
  const client = await connectRemote();
170
368
  return await client.callTool(request.params);
171
369
  } catch (error) {