instantclips-mcp 1.2.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 +142 -47
- package/README.md +134 -45
- package/README.zh-CN.md +117 -41
- package/bin/instantclips-mcp.js +153 -1
- package/manifest/instantclips-mcp.json +226 -60
- package/package.json +1 -1
package/README.zh-CN.md
CHANGED
|
@@ -2,10 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) · [Español](README.es.md) · **简体中文**
|
|
4
4
|
|
|
5
|
+
[](https://m8ven.ai/mcp/instantclips-mcp-1k56q7)
|
|
6
|
+
|
|
5
7
|
[InstantClips](https://instantclips.ai) 可将电商商品转化为适用于 TikTok、Instagram Reels 和
|
|
6
8
|
Stories 的竖屏短视频。它提供托管的 **MCP 服务器**,因此 Claude Code、Codex、Cursor、
|
|
7
9
|
VS Code、Claude 应用、ChatGPT 或其他任何 MCP 客户端都能完成网页应用中的工作:导入商品、
|
|
8
|
-
|
|
10
|
+
撰写视频方案并生成视频。
|
|
11
|
+
|
|
12
|
+
**这不是文本生成视频工具。** InstantClips 会读取商品页面(图片、价格、详情),基于页面上真实存在的
|
|
13
|
+
内容制作广告。方案先写好并展示给你,视频再按方案生成。正因如此,它便宜到可以跑完整个商品目录,
|
|
14
|
+
而且结果就是你在售的商品,而不是凭空猜测。
|
|
15
|
+
|
|
16
|
+
免费开始:赠送的算力足够生成第一条视频,无需绑定银行卡。之后可购买一次性算力包,或为同时运营
|
|
17
|
+
多个品牌的用户提供 Agency 会员。详见[价格](https://instantclips.ai/#pricing)。
|
|
18
|
+
|
|
19
|
+
- **适合搭配。** 搭配 Postiz、Buffer 之类的排期工具,直接发布生成的视频;搭配归因工具,看哪个
|
|
20
|
+
开场钩子有效;在只有商品页面、没有素材的情况下,替代剪辑师。
|
|
21
|
+
- **不适合。** 电影级品牌大片、照稿念词的出镜主持人、横屏 4K、既没有页面也没有图片的商品。
|
|
22
|
+
- **为谁而建。** Shopify 卖家、dropshipping 卖家、品牌,以及同时为多家店铺运营社媒的代理机构。
|
|
9
23
|
|
|
10
24
|
**产品服务器仍以托管方式运行。** 本仓库包含连接指南、注册表元数据、示例 HTTP 客户端,以及一个
|
|
11
25
|
面向无法直接连接远程服务器的客户端的小型开源 stdio 适配器。适配器通过生成的快照在本地响应
|
|
@@ -14,30 +28,36 @@ VS Code、Claude 应用、ChatGPT 或其他任何 MCP 客户端都能完成网
|
|
|
14
28
|
|
|
15
29
|
## 端点
|
|
16
30
|
|
|
17
|
-
| |
|
|
18
|
-
| -------- |
|
|
19
|
-
| 端点 | `https://app.instantclips.ai/mcp`
|
|
20
|
-
| 传输方式 | Streamable HTTP,无状态
|
|
21
|
-
| 方法 | `POST`,JSON-RPC 2.0
|
|
22
|
-
| 身份验证 | `Authorization: Bearer <token>`
|
|
31
|
+
| | |
|
|
32
|
+
| -------- | ---------------------------------------------------------------------------- |
|
|
33
|
+
| 端点 | `https://app.instantclips.ai/mcp` |
|
|
34
|
+
| 传输方式 | Streamable HTTP,无状态 |
|
|
35
|
+
| 方法 | `POST`,JSON-RPC 2.0 |
|
|
36
|
+
| 身份验证 | 客户端提示时登录(OAuth 2.1);无浏览器的调用方使用 `Authorization: Bearer <token>` |
|
|
37
|
+
|
|
38
|
+
把一个地址粘贴进你的助手,提示时登录即可。设置就这么多。第一次调用时,服务端会带你登录
|
|
39
|
+
InstantClips 并请你授权,无需复制任何密钥。你授权过的每个助手都列在设置里的**已授权应用**中,
|
|
40
|
+
可以随时断开。
|
|
23
41
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
账户,登录时会自动创建账户,并赠送可用于开始体验的免费点数。
|
|
42
|
+
连接的是你自己的账户:与网页应用相同的品牌、商品、算力和方案限额。如果你还没有账户,登录时会
|
|
43
|
+
自动创建账户,并赠送可用于第一条视频的算力。
|
|
27
44
|
|
|
28
|
-
|
|
45
|
+
在浏览器中打开端点时会显示[设置页面](https://app.instantclips.ai/mcp),而不是协议错误;页面上有
|
|
46
|
+
Cursor 和 VS Code 的一键安装按钮。
|
|
29
47
|
|
|
30
48
|
## 安装
|
|
31
49
|
|
|
32
|
-
如果客户端支持 Streamable HTTP
|
|
33
|
-
|
|
50
|
+
如果客户端支持 Streamable HTTP,请直接连接托管端点,首次使用时会引导你登录。只有脚本和无法打开
|
|
51
|
+
登录页的自动化运行环境,才需要使用令牌和下方“没有浏览器?”一节中的 stdio 适配器。
|
|
34
52
|
|
|
35
53
|
### Claude Code
|
|
36
54
|
|
|
37
55
|
```bash
|
|
38
|
-
claude mcp add --transport http instantclips https://app.instantclips.ai/mcp
|
|
56
|
+
claude mcp add --transport http instantclips https://app.instantclips.ai/mcp
|
|
39
57
|
```
|
|
40
58
|
|
|
59
|
+
然后在 Claude Code 里运行 `/mcp`,选择 InstantClips 登录。
|
|
60
|
+
|
|
41
61
|
### Codex
|
|
42
62
|
|
|
43
63
|
将以下配置添加到 `~/.codex/config.toml`。该配置同时适用于 CLI、应用和 IDE 扩展:
|
|
@@ -45,13 +65,53 @@ claude mcp add --transport http instantclips https://app.instantclips.ai/mcp --h
|
|
|
45
65
|
```toml
|
|
46
66
|
[mcp_servers.instantclips]
|
|
47
67
|
url = "https://app.instantclips.ai/mcp"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
然后运行 `codex mcp login instantclips` 登录。
|
|
71
|
+
|
|
72
|
+
### Cursor 和 VS Code
|
|
73
|
+
|
|
74
|
+
[设置页面](https://app.instantclips.ai/mcp)提供一键安装按钮。按钮会打开应用并添加 InstantClips,
|
|
75
|
+
首次使用时登录。
|
|
76
|
+
|
|
77
|
+
### Claude 应用和 ChatGPT
|
|
78
|
+
|
|
79
|
+
Claude 应用:用这个地址添加自定义连接器,按提示登录。网页版 ChatGPT:在 设置 › Apps › 高级 中
|
|
80
|
+
开启开发者模式,再把地址添加为连接器;Business 或 Enterprise 工作区则由管理员发布为全员可用的
|
|
81
|
+
应用。ChatGPT 桌面版在 设置 › MCP 服务器 中填入同一地址,并与 Codex 共享配置。
|
|
82
|
+
|
|
83
|
+
### 其他 MCP 客户端或智能体
|
|
84
|
+
|
|
85
|
+
OpenClaw、Hermes,或你自己写的 agent:通过 Streamable HTTP 指向这个地址。服务端按标准方式公布
|
|
86
|
+
登录流程,遵循规范的客户端无需其他配置。无法打开登录页的,改用下方的令牌。
|
|
87
|
+
|
|
88
|
+
### 没有浏览器?使用访问令牌
|
|
89
|
+
|
|
90
|
+
脚本、CI 任务和无法打开登录页的 agent,改用长期有效的令牌鉴权。前往
|
|
91
|
+
**[app.instantclips.ai/settings#ai-access](https://app.instantclips.ai/settings#ai-access)**
|
|
92
|
+
生成令牌。它拥有你账号的全部权限,请勿提交到代码仓库。
|
|
93
|
+
|
|
94
|
+
使用令牌时,同样的客户端配置如下:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Claude Code
|
|
98
|
+
claude mcp add --transport http instantclips https://app.instantclips.ai/mcp --header "Authorization: Bearer YOUR_TOKEN"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```toml
|
|
102
|
+
# Codex,位于 ~/.codex/config.toml
|
|
103
|
+
[mcp_servers.instantclips]
|
|
104
|
+
url = "https://app.instantclips.ai/mcp"
|
|
48
105
|
http_headers = { Authorization = "Bearer YOUR_TOKEN" }
|
|
49
106
|
```
|
|
50
107
|
|
|
51
|
-
|
|
52
|
-
`bearer_token_env_var = "INSTANTCLIPS_TOKEN"`,然后在 shell 中导出该环境变量。
|
|
108
|
+
如果不想把令牌写入 Codex 的配置文件,请将请求头配置替换为
|
|
109
|
+
`bearer_token_env_var = "INSTANTCLIPS_TOKEN"`,然后在 shell 中导出该环境变量。Claude 应用可在
|
|
110
|
+
连接器中以请求头的形式使用令牌(自定义请求头仍处于测试阶段);ChatGPT 连接器通过登录流程
|
|
111
|
+
鉴权,而不是粘贴密钥。其他任何客户端发送 `Authorization: Bearer` 请求头即可。传输协议中没有
|
|
112
|
+
InstantClips 专用内容。
|
|
53
113
|
|
|
54
|
-
|
|
114
|
+
#### 仅支持 stdio 的客户端和无界面自动化
|
|
55
115
|
|
|
56
116
|
`instantclips-mcp` npm 软件包是一个轻量的 stdio 到 HTTPS 适配器。它在本地提供初始化和工具发现,
|
|
57
117
|
从而无需凭据即可快速启动;随后从环境变量读取令牌,并将工具调用发送到 InstantClips:
|
|
@@ -79,23 +139,21 @@ INSTANTCLIPS_TOKEN="your-token" npx -y instantclips-mcp --check --json
|
|
|
79
139
|
令牌只能通过 `INSTANTCLIPS_TOKEN` 提供,不能作为命令行参数传入,因此不会出现在进程列表中。
|
|
80
140
|
工具调用需要令牌,但 `initialize`、`ping` 和 `tools/list` 不需要。需要 Node.js 20 或更高版本。
|
|
81
141
|
|
|
82
|
-
|
|
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
|
+
然后说出你想要的视频即可。
|
|
83
147
|
|
|
84
|
-
|
|
85
|
-
为你填入令牌。
|
|
148
|
+
## 入门提示语
|
|
86
149
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
这些客户端通过各自的连接器设置进行连接,而不是读取配置文件。将连接地址设为
|
|
90
|
-
`https://app.instantclips.ai/mcp`,并使用同一个令牌进行身份验证:Claude 使用请求头,ChatGPT
|
|
91
|
-
使用 API 密钥。Claude 的自定义请求头功能仍处于测试阶段,而 ChatGPT 需要开启开发者模式;具体
|
|
92
|
-
是否可用取决于你的账户和工作区政策。
|
|
93
|
-
|
|
94
|
-
### 其他客户端
|
|
150
|
+
先从这五句开始,替换成你的链接或商品名即可。
|
|
95
151
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
152
|
+
1. “给这个商品做一条视频广告:[URL]”
|
|
153
|
+
2. “把这个系列页面上的所有商品都导入,并为每一个起草方案。先不要生成。”
|
|
154
|
+
3. “给我看 [商品] 的方案,把开场钩子改成先说价格。”
|
|
155
|
+
4. “用三个不同的开场钩子,给 [URL] 做三条视频,方便我测试。”
|
|
156
|
+
5. “这个商品属于我的哪个品牌?确认后再做视频。”
|
|
99
157
|
|
|
100
158
|
## 工具
|
|
101
159
|
|
|
@@ -103,12 +161,17 @@ INSTANTCLIPS_TOKEN="your-token" npx -y instantclips-mcp --check --json
|
|
|
103
161
|
|
|
104
162
|
1. **导入** — 如果有店铺商品页面,使用 `import_product_from_url`;如果没有可读取的页面,则使用
|
|
105
163
|
`create_product_from_images`。
|
|
106
|
-
2. **等待草稿** — 轮询 `get_product
|
|
107
|
-
3. **查看并调整** —
|
|
164
|
+
2. **等待草稿** — 轮询 `get_product`,直到商品导入和方案起草完成。
|
|
165
|
+
3. **查看并调整** — 方案以文本返回,包含开场钩子、内容重点、形式、执行指南和限制条件。
|
|
108
166
|
使用 `update_video_direction` 进行编辑,或使用 `redraft_video_direction` 获取另一个方向。
|
|
109
|
-
4.
|
|
167
|
+
4. **生成** — 使用 `generate_video`,并传入 `expected_credit_cost`:即 `get_product` 报告并已告知用户的
|
|
168
|
+
算力费用。费用不一致时会拒绝执行,不会扣费。
|
|
110
169
|
5. **获取结果** — 轮询 `get_video`,获取完成的 MP4 文件和公开分享链接。
|
|
111
170
|
|
|
171
|
+
为同一商品制作另一个视频,走的是同一流程:编辑或重新起草会打开下一个视频的草稿;没有草稿时调用
|
|
172
|
+
`generate_video` 会按上一方案再生成一版。已生成的视频本身无法修改。每次 `get_product` 的响应都带有
|
|
173
|
+
`next_step`,说明下一步该做什么。
|
|
174
|
+
|
|
112
175
|
品牌的操作方式相同:`list_brands`、`create_brand`、`set_product_brand`。每个视频都会采用对应品牌
|
|
113
176
|
的表达风格进行起草。因此,当导入商品的店铺与任何现有品牌都不匹配时,流程会暂停并询问,而不是
|
|
114
177
|
自行猜测。
|
|
@@ -120,16 +183,16 @@ INSTANTCLIPS_TOKEN="your-token" npx -y instantclips-mcp --check --json
|
|
|
120
183
|
`npm run check:manifest` 会失败。如需通过 HTTP 输出实时 schema,请运行下方的
|
|
121
184
|
`python example.py tools`。
|
|
122
185
|
|
|
123
|
-
##
|
|
186
|
+
## 算力
|
|
124
187
|
|
|
125
|
-
|
|
126
|
-
|
|
188
|
+
导入商品、起草方案和编辑方案均为**免费**。只有 `generate_video` 会消耗算力,而且必须获得你的
|
|
189
|
+
明确许可;工具会事先报告费用,`generate_video` 再以 `expected_credit_cost` 接收这一数字,费用变动时拒绝执行。智能体无法在你不知情的情况下产生费用。详见
|
|
127
190
|
[价格](https://instantclips.ai/#pricing)。
|
|
128
191
|
|
|
129
192
|
## example.py
|
|
130
193
|
|
|
131
194
|
这是一个无第三方依赖的 MCP 客户端,仅需 Python 3.9 或更高版本和标准库,无需运行
|
|
132
|
-
`pip install
|
|
195
|
+
`pip install`。它使用令牌鉴权,因为脚本没有可用于登录的浏览器。
|
|
133
196
|
|
|
134
197
|
```bash
|
|
135
198
|
export INSTANTCLIPS_TOKEN="your-token"
|
|
@@ -142,8 +205,9 @@ python example.py call list_brands '{}' # 使用 JSON 参数调用一个工具
|
|
|
142
205
|
|
|
143
206
|
## 链接
|
|
144
207
|
|
|
145
|
-
- [instantclips.ai/automate](https://instantclips.ai/automate/) —
|
|
146
|
-
|
|
208
|
+
- [instantclips.ai/automate](https://instantclips.ai/automate/) — 介绍自动化的用途:一个由店铺页面
|
|
209
|
+
生成的真实结果、入门提示语、按顺序排列的工具,以及相关规则。该页面不会重复设置说明;设置方法
|
|
210
|
+
请参阅本文档和应用中的设置页面。
|
|
147
211
|
- [app.instantclips.ai/llms.txt](https://app.instantclips.ai/llms.txt) — 适合机器读取的产品说明和工具
|
|
148
212
|
调用顺序。
|
|
149
213
|
- [服务条款](https://app.instantclips.ai/terms) · [隐私政策](https://app.instantclips.ai/privacy)
|
|
@@ -162,10 +226,22 @@ npm 软件包中的 `mcpName` 必须与该注册表名称完全一致。仓库
|
|
|
162
226
|
使用 GitHub 身份验证。改用 GitHub 身份验证会强制使用 `io.github.instantstudioai/...`,从而失去
|
|
163
227
|
品牌命名空间。
|
|
164
228
|
|
|
165
|
-
|
|
229
|
+
先提升 `version` 并发布 npm 软件包,然后运行 `mcp-publisher publish` 重新发布同一注册表条目。
|
|
166
230
|
域名身份验证会保留品牌命名空间 `ai.instantclips`;请勿将其替换为 `io.github.*` 名称。签名密钥不
|
|
167
231
|
存放在仓库中:`.gitignore` 已忽略 `*.pem`,因为一旦提交私钥,就等于公开了私钥。
|
|
168
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
|
+
|
|
169
245
|
`glama.json` 是 Glama 专用的独立文件,用于认领该平台上的条目。归属于组织而非个人账户的服务器,
|
|
170
246
|
只有在该文件存在时才能完成认领。
|
|
171
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) {
|