instantclips-mcp 1.4.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
 
@@ -168,7 +171,9 @@ acepta `image_paths` (rutas a los archivos, hasta 9, de 8 MB cada uno) en lugar
168
171
  `update_product` acepta `add_image_paths`. Los archivos se empaquetan y se envían directamente a
169
172
  InstantClips como fotos del producto; no se descarga nada de ningún sitio. Arrastra los archivos a
170
173
  un cliente que le pase sus rutas al asistente (Claude Code, Cursor, VS Code, agentes de terminal) y
171
- di qué quieres crear.
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`.
172
177
 
173
178
  ## Instrucciones para empezar
174
179
 
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
 
@@ -158,6 +161,8 @@ that never left your laptop. Through the adapter, `create_product_from_images` t
158
161
  `add_image_paths`. The files are packaged and posted straight to InstantClips as the product's
159
162
  photos — nothing is downloaded from anywhere. Drag the files into a client that hands the
160
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`.
161
166
 
162
167
  ## Starter prompts
163
168
 
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
 
@@ -143,7 +145,8 @@ INSTANTCLIPS_TOKEN="your-token" npx -y instantclips-mcp --check --json
143
145
  `create_product_from_images` 可用 `image_paths`(文件路径,最多 9 个,每个不超过 8 MB)代替
144
146
  `image_urls`,`update_product` 可用 `add_image_paths`。文件会被打包并直接发送到 InstantClips 作为商品照片,
145
147
  不会从任何地方下载。把文件拖进会把路径交给助手的客户端(Claude Code、Cursor、VS Code、终端智能体),
146
- 然后说出你想要的视频即可。
148
+ 然后说出你想要的视频即可。`image_paths` 请单独发送:同一次调用里若还带有 `image_urls`,适配器会拒绝,
149
+ 而不是悄悄丢弃这些 URL;托管照片可以之后用 `update_product` 添加。
147
150
 
148
151
  ## 入门提示语
149
152
 
@@ -141,6 +141,14 @@ function writeDiagnostic(failure) {
141
141
  // serves. The files are read and posted as multipart to the hosted upload
142
142
  // endpoints under the same bearer — packaged and uploaded, never downloaded
143
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.
144
152
  const MAX_LOCAL_IMAGES = 9;
145
153
  const MAX_LOCAL_IMAGE_BYTES = 8 * 1024 * 1024;
146
154
  const LOCAL_IMAGE_TYPES = {
@@ -159,14 +167,17 @@ const LOCAL_UPLOAD_PARAMETERS = {
159
167
  create_product_from_images: {
160
168
  name: "image_paths",
161
169
  replaces: "image_urls",
170
+ hides: "image_files",
162
171
  description:
163
172
  `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.",
173
+ "This adapter uploads the files itself; use it instead of image_urls for local photos, never with them.",
165
174
  note:
166
- "Through this adapter, `image_paths` (files on this machine) can replace `image_urls`; the files are uploaded directly.",
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.",
167
177
  },
168
178
  update_product: {
169
179
  name: "add_image_paths",
180
+ hides: "add_image_files",
170
181
  description:
171
182
  `Paths to photos on this machine to add, up to ${MAX_LOCAL_IMAGES}, ${MAX_LOCAL_IMAGE_BYTES / 1024 / 1024} MB each. ` +
172
183
  "This adapter uploads the files itself. Send it with product_id alone; other fields go in a separate call.",
@@ -180,18 +191,35 @@ function withLocalUploads(tools) {
180
191
  const extra = LOCAL_UPLOAD_PARAMETERS[tool.name];
181
192
  if (!extra) return tool;
182
193
  const schema = tool.inputSchema || { type: "object", properties: {} };
194
+ const { [extra.hides]: _hidden, ...kept } = schema.properties || {};
183
195
  const properties = {
184
- ...schema.properties,
185
- [extra.name]: { type: "array", items: { type: "string" }, description: extra.description },
196
+ ...kept,
197
+ [extra.name]: { type: "array", items: { type: "string" }, minItems: 1, description: extra.description },
186
198
  };
187
199
  const inputSchema = { ...schema, properties };
188
200
  if (extra.replaces && Array.isArray(schema.required)) {
189
201
  inputSchema.required = schema.required.filter((key) => key !== extra.replaces);
190
202
  }
191
- return { ...tool, description: `${tool.description}\n\n${extra.note}`, inputSchema };
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
+ };
192
210
  });
193
211
  }
194
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
+
195
223
  function hasPaths(value) {
196
224
  return Array.isArray(value) && value.length > 0;
197
225
  }
@@ -250,13 +278,31 @@ async function uploadForm(config, suffix, form) {
250
278
  error.data = { status: 401 };
251
279
  throw error;
252
280
  }
253
- return { content: [{ type: "text", text }], isError: !response.ok };
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;
254
293
  }
255
294
 
256
295
  // → a tool result when the call is one this adapter answers itself, else null.
257
296
  async function callLocally(config, params) {
258
297
  const args = params.arguments ?? {};
259
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
+ }
260
306
  const form = new FormData();
261
307
  for (const key of ["name", "description", "creator_note", "brand_id"]) {
262
308
  if (args[key] !== undefined && args[key] !== null && args[key] !== "") form.append(key, String(args[key]));