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 +28 -1
- package/README.md +26 -0
- package/README.zh-CN.md +22 -1
- package/bin/instantclips-mcp.js +199 -1
- package/manifest/instantclips-mcp.json +2643 -66
- package/package.json +1 -1
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 表单中,将构建步骤设为
|
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,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) {
|