instantclips-mcp 1.1.0 → 1.2.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
@@ -10,9 +10,10 @@ dirección creativa y renderizar el vídeo.
10
10
 
11
11
  **El servidor del producto permanece alojado.** Este repositorio contiene su guía de conexión, los
12
12
  metadatos de registro, un cliente HTTP de ejemplo y un pequeño adaptador stdio de código abierto
13
- para los clientes que no pueden conectarse directamente a un servidor remoto. El adaptador se
14
- limita a transportar mensajes MCP al endpoint alojado; las definiciones de las herramientas y la
15
- implementación del producto permanecen en un único lugar.
13
+ para los clientes que no pueden conectarse directamente a un servidor remoto. El adaptador responde
14
+ localmente a la inicialización, al ping y al descubrimiento de herramientas desde una instantánea
15
+ generada, y envía al endpoint alojado únicamente las llamadas autenticadas. El servidor alojado
16
+ sigue siendo la fuente de verdad; la implementación del producto no se duplica aquí.
16
17
 
17
18
  ## Endpoint
18
19
 
@@ -59,8 +60,9 @@ Para no guardar el token en el archivo, sustituye el encabezado por
59
60
 
60
61
  ### Clientes que solo admiten stdio y procesos sin interfaz
61
62
 
62
- El paquete npm `instantclips-mcp` es un adaptador ligero de stdio a HTTPS. Lee el token del entorno,
63
- fija InstantClips como endpoint de destino y reenvía el protocolo sin modificarlo:
63
+ El paquete npm `instantclips-mcp` es un adaptador ligero de stdio a HTTPS. Sirve localmente la
64
+ inicialización y el descubrimiento de herramientas para arrancar rápido y sin credenciales; después
65
+ lee el token del entorno y envía las llamadas de herramientas a InstantClips:
64
66
 
65
67
  ```json
66
68
  {
@@ -84,7 +86,8 @@ INSTANTCLIPS_TOKEN="your-token" npx -y instantclips-mcp --check --json
84
86
  ```
85
87
 
86
88
  El token solo se acepta mediante `INSTANTCLIPS_TOKEN`, nunca como argumento de línea de comandos,
87
- por lo que no aparece en la lista de procesos. Se requiere Node.js 20 o posterior.
89
+ por lo que no aparece en la lista de procesos. Es obligatorio para llamar a una herramienta, pero
90
+ no para `initialize`, `ping` ni `tools/list`. Se requiere Node.js 20 o posterior.
88
91
 
89
92
  ### Cursor y VS Code
90
93
 
@@ -124,10 +127,12 @@ Las marcas funcionan de la misma manera: `list_brands`, `create_brand`, `set_pro
124
127
  vídeo se prepara con la voz de una marca; por eso, si el escaparate de un producto importado no
125
128
  coincide con ninguna marca existente, el proceso se detiene y pregunta en lugar de adivinar.
126
129
 
127
- El propio servidor publica los parámetros exactos de cada herramienta. Este repositorio no los
128
- repite deliberadamente: ejecuta `python example.py tools`, como se indica abajo, para mostrar los
129
- esquemas actuales. Así, lo que construyas no puede quedar desfasado respecto a lo que acepta el
130
- servidor.
130
+ El servidor alojado publica los parámetros exactos de cada herramienta. La instantánea generada
131
+ [`manifest/instantclips-mcp.json`](manifest/instantclips-mcp.json) permite que clientes stdio y
132
+ registros inspeccionen los mismos esquemas sin credenciales. Quienes mantienen el repositorio la
133
+ actualizan con `INSTANTCLIPS_TOKEN="..." npm run sync:manifest`; `npm run check:manifest` falla si
134
+ la copia incluida difiere del servidor en producción. Ejecuta `python example.py tools` cuando
135
+ quieras imprimir específicamente los esquemas actuales mediante HTTP.
131
136
 
132
137
  ## Créditos
133
138
 
@@ -185,9 +190,12 @@ commit es una clave publicada.
185
190
  `glama.json` es el archivo independiente y específico de Glama que permite reclamar allí la ficha.
186
191
  Un servidor perteneciente a una organización, en lugar de una cuenta personal, solo puede
187
192
  reclamarse si ese archivo está presente.
188
- El archivo solo acredita la propiedad: configura `npm ci` como compilación en Glama, ejecuta
189
- `node bin/instantclips-mcp.js` y proporciona `INSTANTCLIPS_TOKEN` como secreto desde la interfaz de
190
- administración del servidor.
193
+ El archivo solo acredita la propiedad. En el formulario Dockerfile de Glama, usa
194
+ `["npm install --omit=dev"]` como pasos de compilación,
195
+ `["node", "./bin/instantclips-mcp.js"]` como argumentos de CMD y cualquier valor ficticio para el
196
+ marcador obligatorio `INSTANTCLIPS_TOKEN`. Las comprobaciones de inicialización y calidad de Glama
197
+ usan el manifiesto incluido y nunca envían ese valor al servidor alojado. No introduzcas un token
198
+ real de una cuenta en el entorno de compilación de un tercero.
191
199
 
192
200
  ## Licencia
193
201
 
package/README.md CHANGED
@@ -9,8 +9,9 @@ does: import a product, draft the creative direction, and render the video.
9
9
 
10
10
  **The product server stays hosted.** This repository contains its connection guide, registry
11
11
  metadata, example HTTP client and a small open-source stdio adapter for clients that cannot connect
12
- to a remote server directly. The adapter only carries MCP messages to the hosted endpoint; the
13
- tool definitions and product implementation remain in one place.
12
+ to a remote server directly. The adapter answers initialization, ping and tool discovery from a
13
+ generated snapshot, then sends authenticated tool calls to the hosted endpoint. The hosted server
14
+ remains the source of truth; the product implementation is not duplicated here.
14
15
 
15
16
  ## Endpoint
16
17
 
@@ -54,8 +55,9 @@ and export it in your shell instead.
54
55
 
55
56
  ### Stdio-only clients and headless runners
56
57
 
57
- The `instantclips-mcp` npm package is a thin stdio-to-HTTPS adapter. It reads the token from the
58
- environment, fixes the upstream endpoint to InstantClips and forwards the protocol unchanged:
58
+ The `instantclips-mcp` npm package is a thin stdio-to-HTTPS adapter. It serves initialization and
59
+ tool discovery locally for a fast, credential-free cold start, then reads the token from the
60
+ environment and sends tool calls to InstantClips:
59
61
 
60
62
  ```json
61
63
  {
@@ -78,7 +80,8 @@ INSTANTCLIPS_TOKEN="your-token" npx -y instantclips-mcp --check --json
78
80
  ```
79
81
 
80
82
  The token is accepted only through `INSTANTCLIPS_TOKEN`, never as a command-line argument, so it
81
- does not appear in the process list. Node.js 20 or newer is required.
83
+ does not appear in the process list. It is required for tool calls, but not for `initialize`,
84
+ `ping`, or `tools/list`. Node.js 20 or newer is required.
82
85
 
83
86
  ### Cursor and VS Code
84
87
 
@@ -116,9 +119,12 @@ Brands work the same way: `list_brands`, `create_brand`, `set_product_brand`. Ev
116
119
  drafted in a brand's voice, so an import whose storefront matches no existing brand stops and asks
117
120
  rather than guessing.
118
121
 
119
- Each tool's exact parameters are published by the server itself. This repository deliberately does
120
- not restate them — run `python example.py tools` below to print the live schemas, so what you build
121
- against cannot drift from what the server accepts.
122
+ Each tool's exact parameters are published by the hosted server. The generated
123
+ [`manifest/instantclips-mcp.json`](manifest/instantclips-mcp.json) snapshot lets stdio clients and
124
+ registries inspect those same schemas without a credential. Maintainers refresh it with
125
+ `INSTANTCLIPS_TOKEN="..." npm run sync:manifest`; `npm run check:manifest` fails when the committed
126
+ snapshot differs from the live server. Run `python example.py tools` below when you specifically
127
+ want to print the live schemas over HTTP.
122
128
 
123
129
  ## Credits
124
130
 
@@ -170,8 +176,11 @@ of the repository — `.gitignore` covers `*.pem`, and a committed private key i
170
176
 
171
177
  `glama.json` is the separate, Glama-specific file that claims the listing there. A server under an
172
178
  organisation rather than a personal account can only be claimed with that file present.
173
- It carries ownership only: configure Glama's build as `npm ci`, run
174
- `node bin/instantclips-mcp.js`, and supply `INSTANTCLIPS_TOKEN` as a secret in the server admin UI.
179
+ It carries ownership only. In Glama's Dockerfile form, use build steps
180
+ `["npm install --omit=dev"]`, CMD arguments `["node", "./bin/instantclips-mcp.js"]`, and any dummy
181
+ value for the required `INSTANTCLIPS_TOKEN` placeholder. Glama's initialization and tool-quality
182
+ checks use the bundled manifest and never transmit that placeholder upstream. Do not put a real
183
+ account token into a third-party build sandbox.
175
184
 
176
185
  ## License
177
186
 
package/README.zh-CN.md CHANGED
@@ -8,8 +8,9 @@ VS Code、Claude 应用、ChatGPT 或其他任何 MCP 客户端都能完成网
8
8
  起草创意方向并渲染视频。
9
9
 
10
10
  **产品服务器仍以托管方式运行。** 本仓库包含连接指南、注册表元数据、示例 HTTP 客户端,以及一个
11
- 面向无法直接连接远程服务器的客户端的小型开源 stdio 适配器。适配器只负责将 MCP 消息转发到托管
12
- 端点;工具定义和产品实现仍然只保留在一个地方。
11
+ 面向无法直接连接远程服务器的客户端的小型开源 stdio 适配器。适配器通过生成的快照在本地响应
12
+ 初始化、ping 和工具发现,只有经过身份验证的工具调用才会发送到托管端点。托管服务器仍是唯一的
13
+ 事实来源;本仓库不会复制产品实现。
13
14
 
14
15
  ## 端点
15
16
 
@@ -52,8 +53,8 @@ http_headers = { Authorization = "Bearer YOUR_TOKEN" }
52
53
 
53
54
  ### 仅支持 stdio 的客户端和无界面自动化
54
55
 
55
- `instantclips-mcp` npm 软件包是一个轻量的 stdio 到 HTTPS 适配器。它从环境变量读取令牌,将上游
56
- 端点固定为 InstantClips,并原样转发协议消息:
56
+ `instantclips-mcp` npm 软件包是一个轻量的 stdio 到 HTTPS 适配器。它在本地提供初始化和工具发现,
57
+ 从而无需凭据即可快速启动;随后从环境变量读取令牌,并将工具调用发送到 InstantClips
57
58
 
58
59
  ```json
59
60
  {
@@ -76,7 +77,7 @@ INSTANTCLIPS_TOKEN="your-token" npx -y instantclips-mcp --check --json
76
77
  ```
77
78
 
78
79
  令牌只能通过 `INSTANTCLIPS_TOKEN` 提供,不能作为命令行参数传入,因此不会出现在进程列表中。
79
- 需要 Node.js 20 或更高版本。
80
+ 工具调用需要令牌,但 `initialize`、`ping` 和 `tools/list` 不需要。需要 Node.js 20 或更高版本。
80
81
 
81
82
  ### Cursor 和 VS Code
82
83
 
@@ -112,8 +113,12 @@ INSTANTCLIPS_TOKEN="your-token" npx -y instantclips-mcp --check --json
112
113
  的表达风格进行起草。因此,当导入商品的店铺与任何现有品牌都不匹配时,流程会暂停并询问,而不是
113
114
  自行猜测。
114
115
 
115
- 每个工具的确切参数均由服务器自身发布。本仓库有意不重复列出这些参数;请运行下方的
116
- `python example.py tools` 来输出实时 schema,确保你构建的集成始终与服务器实际接受的参数一致。
116
+ 每个工具的确切参数均由托管服务器发布。生成的
117
+ [`manifest/instantclips-mcp.json`](manifest/instantclips-mcp.json) 快照让 stdio 客户端和注册表无需
118
+ 凭据即可检查同一组 schema。维护者使用
119
+ `INSTANTCLIPS_TOKEN="..." npm run sync:manifest` 更新快照;如果已提交的快照与线上服务器不同,
120
+ `npm run check:manifest` 会失败。如需通过 HTTP 输出实时 schema,请运行下方的
121
+ `python example.py tools`。
117
122
 
118
123
  ## 点数
119
124
 
@@ -163,8 +168,11 @@ npm 软件包中的 `mcpName` 必须与该注册表名称完全一致。仓库
163
168
 
164
169
  `glama.json` 是 Glama 专用的独立文件,用于认领该平台上的条目。归属于组织而非个人账户的服务器,
165
170
  只有在该文件存在时才能完成认领。
166
- 该文件只用于证明所有权:在 Glama 中将构建命令设置为 `npm ci`,运行
167
- `node bin/instantclips-mcp.js`,并在服务器管理界面中将 `INSTANTCLIPS_TOKEN` 配置为密钥。
171
+ 该文件只用于证明所有权。在 Glama Dockerfile 表单中,将构建步骤设为
172
+ `["npm install --omit=dev"]`,将 CMD 参数设为
173
+ `["node", "./bin/instantclips-mcp.js"]`,并为必填的 `INSTANTCLIPS_TOKEN` 占位参数填写任意虚拟值。
174
+ Glama 的初始化与工具质量检查只读取随包提供的清单,不会将该占位值发送到托管服务器。请勿把真实
175
+ 账户令牌放入第三方构建沙箱。
168
176
 
169
177
  ## 许可证
170
178
 
@@ -7,6 +7,7 @@ import {
7
7
  StreamableHTTPClientTransport,
8
8
  UnauthorizedError,
9
9
  } from "@modelcontextprotocol/client";
10
+ import { Server } from "@modelcontextprotocol/server";
10
11
  import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
11
12
 
12
13
  const DEFAULT_ENDPOINT = "https://app.instantclips.ai/mcp";
@@ -14,6 +15,12 @@ const SETTINGS_URL = "https://app.instantclips.ai/settings#ai-access";
14
15
  const packageJson = JSON.parse(
15
16
  readFileSync(new URL("../package.json", import.meta.url), "utf8"),
16
17
  );
18
+ const manifest = JSON.parse(
19
+ readFileSync(
20
+ new URL("../manifest/instantclips-mcp.json", import.meta.url),
21
+ "utf8",
22
+ ),
23
+ );
17
24
  const VERSION = packageJson.version;
18
25
 
19
26
  function usage() {
@@ -28,15 +35,15 @@ Usage:
28
35
  instantclips-mcp --version
29
36
 
30
37
  Environment:
31
- INSTANTCLIPS_TOKEN Required bearer token. Create one at ${SETTINGS_URL}
38
+ INSTANTCLIPS_TOKEN Bearer token required for tool calls. Create one at ${SETTINGS_URL}
32
39
 
33
- The upstream endpoint is ${DEFAULT_ENDPOINT}.
40
+ Initialization and tool discovery work offline. Tool calls use ${DEFAULT_ENDPOINT}.
34
41
  `;
35
42
  }
36
43
 
37
- function configuration(env = process.env) {
44
+ function configuration(env = process.env, { requireToken = true } = {}) {
38
45
  const token = env.INSTANTCLIPS_TOKEN?.trim();
39
- if (!token) {
46
+ if (!token && requireToken) {
40
47
  throw new CliError(
41
48
  "missing_token",
42
49
  `INSTANTCLIPS_TOKEN is required. Create a token at ${SETTINGS_URL}.`,
@@ -72,37 +79,25 @@ function configuration(env = process.env) {
72
79
  }
73
80
 
74
81
  function remoteTransport({ endpoint, token }) {
82
+ if (!token) {
83
+ throw new CliError(
84
+ "missing_token",
85
+ `INSTANTCLIPS_TOKEN is required for tool calls. Create a token at ${SETTINGS_URL}.`,
86
+ 78,
87
+ );
88
+ }
75
89
  return new StreamableHTTPClientTransport(endpoint, {
76
90
  authProvider: { token: async () => token },
77
91
  });
78
92
  }
79
93
 
80
- function isRequest(message) {
81
- return Boolean(
82
- message &&
83
- !Array.isArray(message) &&
84
- typeof message === "object" &&
85
- typeof message.method === "string" &&
86
- Object.hasOwn(message, "id"),
87
- );
88
- }
89
-
90
- function isResponse(message) {
91
- return Boolean(
92
- message &&
93
- !Array.isArray(message) &&
94
- typeof message === "object" &&
95
- Object.hasOwn(message, "id") &&
96
- (Object.hasOwn(message, "result") || Object.hasOwn(message, "error")),
97
- );
98
- }
99
-
100
94
  function safeFailure(error, token) {
95
+ if (error instanceof CliError) {
96
+ return { code: error.code, message: error.message };
97
+ }
101
98
  const status = error?.data?.status;
102
- const text = String(error?.message || error || "Unknown error").replaceAll(
103
- token,
104
- "[redacted]",
105
- );
99
+ const rawText = String(error?.message || error || "Unknown error");
100
+ const text = token ? rawText.replaceAll(token, "[redacted]") : rawText;
106
101
 
107
102
  if (
108
103
  error instanceof UnauthorizedError ||
@@ -136,10 +131,52 @@ function writeDiagnostic(failure) {
136
131
 
137
132
  async function startBridge(config) {
138
133
  const stdio = new StdioServerTransport();
139
- const remote = remoteTransport(config);
140
- const initializeRequestIds = new Set();
134
+ const server = new Server(manifest.serverInfo, {
135
+ capabilities: manifest.capabilities,
136
+ instructions: manifest.instructions,
137
+ });
138
+ let remoteClient;
139
+ let remoteClientPromise;
141
140
  let closing = false;
142
141
 
142
+ const connectRemote = () => {
143
+ if (!remoteClientPromise) {
144
+ remoteClientPromise = (async () => {
145
+ const client = new Client({
146
+ name: "instantclips-mcp-stdio-adapter",
147
+ version: VERSION,
148
+ });
149
+ try {
150
+ await client.connect(remoteTransport(config));
151
+ remoteClient = client;
152
+ return client;
153
+ } catch (error) {
154
+ await client.close().catch(() => {});
155
+ remoteClientPromise = undefined;
156
+ throw error;
157
+ }
158
+ })();
159
+ }
160
+ return remoteClientPromise;
161
+ };
162
+
163
+ server.setRequestHandler("tools/list", async () => ({
164
+ tools: manifest.tools,
165
+ }));
166
+
167
+ server.setRequestHandler("tools/call", async (request) => {
168
+ try {
169
+ const client = await connectRemote();
170
+ return await client.callTool(request.params);
171
+ } catch (error) {
172
+ const failure = safeFailure(error, config.token);
173
+ return {
174
+ content: [{ type: "text", text: failure.message }],
175
+ isError: true,
176
+ };
177
+ }
178
+ });
179
+
143
180
  const shutdown = async (exitCode = process.exitCode || 0) => {
144
181
  if (closing) return;
145
182
  closing = true;
@@ -148,68 +185,18 @@ async function startBridge(config) {
148
185
  process.stdin.off("close", onStdinEnd);
149
186
  process.off("SIGINT", onSigint);
150
187
  process.off("SIGTERM", onSigterm);
151
- await Promise.allSettled([remote.close(), stdio.close()]);
188
+ await Promise.allSettled([remoteClient?.close(), server.close()]);
152
189
  };
153
190
 
154
191
  const onStdinEnd = () => void shutdown();
155
192
  const onSigint = () => void shutdown(130);
156
193
  const onSigterm = () => void shutdown(143);
157
-
158
- stdio.onmessage = (message) => {
159
- if (isRequest(message) && message.method === "initialize") {
160
- initializeRequestIds.add(message.id);
161
- }
162
-
163
- void remote.send(message).catch(async (error) => {
164
- if (isRequest(message)) {
165
- initializeRequestIds.delete(message.id);
166
- const failure = safeFailure(error, config.token);
167
- await stdio
168
- .send({
169
- jsonrpc: "2.0",
170
- id: message.id,
171
- error: {
172
- code: -32000,
173
- message: failure.message,
174
- data: { code: failure.code },
175
- },
176
- })
177
- .catch((sendError) => {
178
- writeDiagnostic(safeFailure(sendError, config.token));
179
- void shutdown(74);
180
- });
181
- }
182
- });
183
- };
184
-
185
- remote.onmessage = (message) => {
186
- if (isResponse(message) && initializeRequestIds.delete(message.id)) {
187
- const protocolVersion = message.result?.protocolVersion;
188
- if (typeof protocolVersion === "string") {
189
- remote.setProtocolVersion(protocolVersion);
190
- }
191
- }
192
- void stdio.send(message).catch((error) => {
193
- writeDiagnostic(safeFailure(error, config.token));
194
- void shutdown(74);
195
- });
196
- };
197
-
198
- remote.onerror = (error) => writeDiagnostic(safeFailure(error, config.token));
199
- remote.onclose = () => void shutdown();
200
- stdio.onerror = (error) => {
201
- writeDiagnostic(safeFailure(error, config.token));
202
- void shutdown(74);
203
- };
204
- stdio.onclose = () => void shutdown();
205
-
206
194
  process.stdin.once("end", onStdinEnd);
207
195
  process.stdin.once("close", onStdinEnd);
208
196
  process.once("SIGINT", onSigint);
209
197
  process.once("SIGTERM", onSigterm);
210
198
 
211
- await remote.start();
212
- await stdio.start();
199
+ await server.connect(stdio);
213
200
  }
214
201
 
215
202
  async function checkConnection(config, json) {
@@ -286,7 +273,7 @@ async function main(args) {
286
273
 
287
274
  let config;
288
275
  try {
289
- config = configuration();
276
+ config = configuration(process.env, { requireToken: check });
290
277
  } catch (error) {
291
278
  if (check && json && error instanceof CliError) {
292
279
  process.stdout.write(
@@ -0,0 +1,326 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "source": "https://app.instantclips.ai/mcp",
4
+ "capabilities": {
5
+ "tools": {}
6
+ },
7
+ "serverInfo": {
8
+ "name": "instantclips",
9
+ "title": "InstantClips",
10
+ "version": "0.2.0",
11
+ "websiteUrl": "https://instantclips.ai"
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 storefront does not match any brand on the account. Stop and put the\n choice to the user: create a brand for it (`create_brand`) or attach\n it to one they already have (`set_product_brand`). Do not choose for\n them. 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` — this SPENDS THE USER'S CREDITS. Get the user's\n explicit go-ahead first, and tell them the credit cost that\n `get_product` reports.\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\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
+ "tools": [
15
+ {
16
+ "name": "list_brands",
17
+ "title": "List the account's brands",
18
+ "description": "List the brands on this account, with the plan's brand limit and whether\nanother brand can be created.\n\nA brand carries the identity every video is drafted against: its voice,\nits target market and its keywords. A product must belong to the brand it\nactually comes from — a product drafted under another company's voice is\nwrong even though it renders fine.\n\nCall this before answering a `brand_decision_required` from\n`get_product`, and whenever the user needs to choose or name a brand.\n\nThis does not spend credits.\n",
19
+ "inputSchema": {
20
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
21
+ "properties": {},
22
+ "required": [],
23
+ "type": "object"
24
+ },
25
+ "annotations": {
26
+ "destructiveHint": false,
27
+ "idempotentHint": true,
28
+ "openWorldHint": false,
29
+ "readOnlyHint": true
30
+ }
31
+ },
32
+ {
33
+ "name": "import_product_from_url",
34
+ "title": "Import a product from its page URL",
35
+ "description": "Start a new InstantClips product from a product page URL (a storefront\nlisting, e.g. a Shopify product page).\n\nReturns immediately with a product_id — the scrape, the image download\nand the first video-direction draft all run in the background. Poll\n`get_product` until `import_status` is \"imported\" and\n`video_direction.drafting` is false, which usually takes under a minute.\n\nPasting a URL that was already imported on this account returns that\nexisting product instead of creating a duplicate.\n\nIf the storefront name does not exactly match a brand this account has\nalready reviewed, the import stops on a brand decision instead of\nguessing: `get_product` will report `brand_decision_required`, and no\nvideo direction is drafted until it is resolved with `create_brand` or\n`set_product_brand`. Do not assume the account's existing brand — a\nproduct from a different company drafted under the wrong brand's voice\nis the failure this prevents. Pass `brand_id` only when the user has\ntold you which brand this product belongs to.\n\nThis does not spend credits. Only `generate_video` does.\n",
36
+ "inputSchema": {
37
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
38
+ "properties": {
39
+ "url": {
40
+ "type": "string",
41
+ "description": "The product page URL, e.g. https://store.example.com/products/wool-runner"
42
+ },
43
+ "brand_id": {
44
+ "type": "string",
45
+ "description": "Optional, and only when the user has said which brand this is. Omit it to let the import match the storefront against the account's brands and ask when it cannot. Use `list_brands` to see what exists."
46
+ }
47
+ },
48
+ "required": [
49
+ "url"
50
+ ],
51
+ "type": "object"
52
+ },
53
+ "annotations": {
54
+ "destructiveHint": false,
55
+ "idempotentHint": false,
56
+ "openWorldHint": true,
57
+ "readOnlyHint": false
58
+ }
59
+ },
60
+ {
61
+ "name": "create_product_from_images",
62
+ "title": "Create a product from image URLs",
63
+ "description": "Start a new InstantClips product from hosted product photos, for a\nproduct that has no public page to scrape.\n\nImages must be publicly reachable URLs — this endpoint cannot read files\nfrom the caller's machine. Images larger than\n8MB are skipped; at most\n9 are used in a render.\n\nReturns immediately with a product_id; the downloads and the first\nvideo-direction draft run in the background. Poll `get_product` until\n`import_status` is \"imported\".\n\nPrefer `import_product_from_url` when a product page exists — the scrape\nalso collects the name, description, price and brand identity, which\nmake for a far better direction draft than images alone.\n\nThis does not spend credits. Only `generate_video` does.\n",
64
+ "inputSchema": {
65
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
66
+ "properties": {
67
+ "image_urls": {
68
+ "type": "array",
69
+ "items": {
70
+ "type": "string"
71
+ },
72
+ "description": "Publicly reachable image URLs, most representative first.",
73
+ "minItems": 1
74
+ },
75
+ "name": {
76
+ "type": "string",
77
+ "description": "Product name."
78
+ },
79
+ "description": {
80
+ "type": "string",
81
+ "description": "Optional. What the product is, in a sentence or two — feeds the direction draft."
82
+ },
83
+ "creator_note": {
84
+ "type": "string",
85
+ "description": "Optional. A short free-text note to the director: the angle, occasion or message this video should carry."
86
+ },
87
+ "brand_id": {
88
+ "type": "string",
89
+ "description": "Which brand this product belongs to, from `list_brands`. There is no page to scrape here, so nothing can detect the brand for you: confirm it with the user rather than letting it fall through to the account's default brand."
90
+ }
91
+ },
92
+ "required": [
93
+ "image_urls",
94
+ "name"
95
+ ],
96
+ "type": "object"
97
+ },
98
+ "annotations": {
99
+ "destructiveHint": false,
100
+ "idempotentHint": false,
101
+ "openWorldHint": true,
102
+ "readOnlyHint": false
103
+ }
104
+ },
105
+ {
106
+ "name": "get_product",
107
+ "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\" AND `video_direction.drafting` is false.\nPoll every 20-30 seconds; the whole thing normally settles inside a\nminute.\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",
109
+ "inputSchema": {
110
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
111
+ "properties": {
112
+ "product_id": {
113
+ "type": "string",
114
+ "description": "The product's id."
115
+ }
116
+ },
117
+ "required": [
118
+ "product_id"
119
+ ],
120
+ "type": "object"
121
+ },
122
+ "annotations": {
123
+ "destructiveHint": false,
124
+ "idempotentHint": true,
125
+ "openWorldHint": false,
126
+ "readOnlyHint": true
127
+ }
128
+ },
129
+ {
130
+ "name": "create_brand",
131
+ "title": "Create a brand for a product",
132
+ "description": "Create a new brand from the identity detected on a product that is\nwaiting on `brand_decision_required`, and attach the product to it.\n\nUse this when the product belongs to a company the account has no brand\nfor yet — the usual case when someone brings a new store. The brand's\nvoice, target market and keywords are taken from what the storefront\nscrape drafted; the user can rename it with `name`.\n\nAsk the user before calling this. It consumes one of the plan's brand\nslots, and on a plan that has run out this fails with a brand-limit\nerror — at which point the choices are to attach the product to an\nexisting brand with `set_product_brand`, to re-point an existing brand\nat this identity with `set_product_brand` and `replace_identity: true`,\nor to upgrade.\n\nOnce this returns, the video direction starts drafting: poll\n`get_product` until `video_direction.drafting` is false.\n\nThis does not spend credits.\n",
133
+ "inputSchema": {
134
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
135
+ "properties": {
136
+ "product_id": {
137
+ "type": "string",
138
+ "description": "The product waiting on a brand decision."
139
+ },
140
+ "name": {
141
+ "type": "string",
142
+ "description": "Optional. Overrides the detected storefront name for the new brand."
143
+ }
144
+ },
145
+ "required": [
146
+ "product_id"
147
+ ],
148
+ "type": "object"
149
+ },
150
+ "annotations": {
151
+ "destructiveHint": false,
152
+ "idempotentHint": false,
153
+ "openWorldHint": false,
154
+ "readOnlyHint": false
155
+ }
156
+ },
157
+ {
158
+ "name": "set_product_brand",
159
+ "title": "Attach a product to an existing brand",
160
+ "description": "Attach a product waiting on `brand_decision_required` to one of the\naccount's existing brands. Use `list_brands` to see them.\n\nOnly do this when the user has confirmed the product really belongs to\nthat brand. Attaching a product to an unrelated brand is not a cosmetic\nmislabel: every video is drafted in that brand's voice, to its target\nmarket, with its keywords.\n\nBy default the brand's existing identity is left untouched. Pass\n`replace_identity: true` to instead overwrite that brand's voice, target\nmarket and keywords with the identity detected on this product — the\n\"re-point my brand at a different company\" move, for an account at its\nbrand limit. This rewrites a brand every other product on it shares, so\nconfirm it with the user explicitly first.\n\nOnce this returns, the video direction starts drafting: poll\n`get_product` until `video_direction.drafting` is false.\n\nThis does not spend credits.\n",
161
+ "inputSchema": {
162
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
163
+ "properties": {
164
+ "product_id": {
165
+ "type": "string",
166
+ "description": "The product waiting on a brand decision."
167
+ },
168
+ "brand_id": {
169
+ "type": "string",
170
+ "description": "The brand to attach it to, from `list_brands`."
171
+ },
172
+ "replace_identity": {
173
+ "type": "boolean",
174
+ "description": "Optional, default false. Overwrite the brand's voice, target market and keywords with this product's detected identity. Affects every product on that brand — confirm with the user."
175
+ }
176
+ },
177
+ "required": [
178
+ "product_id",
179
+ "brand_id"
180
+ ],
181
+ "type": "object"
182
+ },
183
+ "annotations": {
184
+ "destructiveHint": true,
185
+ "idempotentHint": false,
186
+ "openWorldHint": false,
187
+ "readOnlyHint": false
188
+ }
189
+ },
190
+ {
191
+ "name": "update_video_direction",
192
+ "title": "Edit the video direction and render settings",
193
+ "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.\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 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\nOnly works before generation starts. This does not spend credits.\n",
194
+ "inputSchema": {
195
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
196
+ "properties": {
197
+ "product_id": {
198
+ "type": "string",
199
+ "description": "The product's id."
200
+ },
201
+ "creative_direction": {
202
+ "type": "string",
203
+ "description": "The full creative plan, max 3000 characters. Pass an empty string to clear it."
204
+ },
205
+ "creator_note": {
206
+ "type": "string",
207
+ "description": "The user's short note to the director, max 1000 characters."
208
+ },
209
+ "ratio": {
210
+ "type": "string",
211
+ "enum": [
212
+ "9:16",
213
+ "3:4",
214
+ "16:9",
215
+ "1:1"
216
+ ],
217
+ "description": "Aspect ratio. 9:16 (vertical) is the default and what social feeds want."
218
+ },
219
+ "resolution": {
220
+ "type": "string",
221
+ "enum": [
222
+ "480P",
223
+ "720P",
224
+ "1080P"
225
+ ],
226
+ "description": "1080P needs a paid plan; a pick above the plan's ceiling quietly becomes that ceiling (the response reports what was actually saved)."
227
+ },
228
+ "enable_audio": {
229
+ "type": "boolean",
230
+ "description": "Whether the render has sound."
231
+ },
232
+ "target_market": {
233
+ "type": "string",
234
+ "description": "Free text, e.g. \"US/North America\" or \"Japan\". Drives the spoken language unless one is set explicitly. Saved on the brand, so it carries to later videos."
235
+ }
236
+ },
237
+ "required": [
238
+ "product_id"
239
+ ],
240
+ "type": "object"
241
+ },
242
+ "annotations": {
243
+ "destructiveHint": false,
244
+ "idempotentHint": true,
245
+ "openWorldHint": false,
246
+ "readOnlyHint": false
247
+ }
248
+ },
249
+ {
250
+ "name": "redraft_video_direction",
251
+ "title": "Draft a new creative direction",
252
+ "description": "Ask InstantClips to draft a fresh creative direction for this product's\nnext video, using the product's facts, its images and the brand's\nidentity. Use it to get a first draft, or to try a different angle when\nthe user does not like the current one.\n\nThis OVERWRITES the current direction — including any edits. Confirm with\nthe user before re-rolling a direction they have already worked on.\n\nThe draft runs in the background: this returns with `drafting` true, and\nyou poll `get_product` until `video_direction.drafting` is false (a few\nseconds). Rolling a fresh angle is the point, so calling it twice gives\ntwo different drafts, not the same one.\n\nThis does not spend credits.\n",
253
+ "inputSchema": {
254
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
255
+ "properties": {
256
+ "product_id": {
257
+ "type": "string",
258
+ "description": "The product's id."
259
+ },
260
+ "format": {
261
+ "type": "string",
262
+ "description": "Optional. Pin a specific video format instead of letting the drafter pick one — e.g. \"unboxing\", \"before_after\". Unknown values are ignored, so leave it out unless the user asked for a particular kind of video."
263
+ }
264
+ },
265
+ "required": [
266
+ "product_id"
267
+ ],
268
+ "type": "object"
269
+ },
270
+ "annotations": {
271
+ "destructiveHint": true,
272
+ "idempotentHint": false,
273
+ "openWorldHint": true,
274
+ "readOnlyHint": false
275
+ }
276
+ },
277
+ {
278
+ "name": "generate_video",
279
+ "title": "Generate the video (spends credits)",
280
+ "description": "Render the video. THIS SPENDS THE USER'S CREDITS.\n\nAsk the user before calling this, every time. Tell them the cost first —\n`get_product` reports it as `video_direction.credit_cost`, and the\nuser's balance as `credits_remaining`. Credits are charged at launch,\nnot on completion; a failed render is refunded automatically.\n\nShow the user the creative direction and let them approve or edit it\nbefore you call this. Do not call it to \"see what happens\", to retry a\nrender that is still in progress, or as part of a batch you decided on\nyour own.\n\nReturns as soon as the render is queued. Poll `get_video` with the\nreturned video_request_id every 20-30 seconds until its status is\n\"done\" (a few minutes), then give the user `output_url` and\n`share_url`.\n\nIf the account cannot afford it, nothing is charged and the response\nsays so — tell the user to top up at the credits page rather than\nretrying.\n",
281
+ "inputSchema": {
282
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
283
+ "properties": {
284
+ "product_id": {
285
+ "type": "string",
286
+ "description": "The product to render. Its current direction and settings are used as-is."
287
+ }
288
+ },
289
+ "required": [
290
+ "product_id"
291
+ ],
292
+ "type": "object"
293
+ },
294
+ "annotations": {
295
+ "destructiveHint": false,
296
+ "idempotentHint": false,
297
+ "openWorldHint": true,
298
+ "readOnlyHint": false
299
+ }
300
+ },
301
+ {
302
+ "name": "get_video",
303
+ "title": "Check a video's render status",
304
+ "description": "Check one video's render.\n\n`status` values:\n \"generating\" — still rendering, keep polling every 20-30 seconds.\n \"done\" — finished; `output_url` is the MP4 and `share_url` is a\n public page to send someone.\n \"failed\" — `failed_reason` says why. The credits were refunded\n automatically; the user can retry from the product page.\n \"insufficient_credit\" — never launched; nothing was charged.\n \"pending\" — not launched yet; call `generate_video`.\n\nA render normally takes a few minutes. Tell the user what you are\nwaiting on rather than polling silently in a tight loop.\n",
305
+ "inputSchema": {
306
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
307
+ "properties": {
308
+ "video_request_id": {
309
+ "type": "string",
310
+ "description": "Returned by generate_video, and listed under `videos` in get_product."
311
+ }
312
+ },
313
+ "required": [
314
+ "video_request_id"
315
+ ],
316
+ "type": "object"
317
+ },
318
+ "annotations": {
319
+ "destructiveHint": false,
320
+ "idempotentHint": true,
321
+ "openWorldHint": false,
322
+ "readOnlyHint": true
323
+ }
324
+ }
325
+ ]
326
+ }
package/package.json CHANGED
@@ -1,18 +1,22 @@
1
1
  {
2
2
  "name": "instantclips-mcp",
3
3
  "mcpName": "ai.instantclips/instantclips",
4
- "version": "1.1.0",
4
+ "version": "1.2.0",
5
5
  "description": "Use the hosted InstantClips MCP server from stdio-only clients.",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "instantclips-mcp": "bin/instantclips-mcp.js"
9
9
  },
10
10
  "files": [
11
- "bin/"
11
+ "bin/",
12
+ "manifest/",
13
+ "scripts/"
12
14
  ],
13
15
  "scripts": {
14
16
  "test": "node --test",
15
17
  "check:pack": "npm pack --dry-run",
18
+ "sync:manifest": "node scripts/sync-manifest.mjs",
19
+ "check:manifest": "node scripts/sync-manifest.mjs --check",
16
20
  "prepublishOnly": "npm test && npm run check:pack"
17
21
  },
18
22
  "repository": {
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+
5
+ import {
6
+ Client,
7
+ StreamableHTTPClientTransport,
8
+ } from "@modelcontextprotocol/client";
9
+
10
+ const DEFAULT_ENDPOINT = "https://app.instantclips.ai/mcp";
11
+ const manifestUrl = new URL("../manifest/instantclips-mcp.json", import.meta.url);
12
+ const checkOnly = process.argv.slice(2).includes("--check");
13
+ const token = process.env.INSTANTCLIPS_TOKEN?.trim();
14
+
15
+ if (!token) {
16
+ throw new Error(
17
+ "INSTANTCLIPS_TOKEN is required to refresh the public tool manifest.",
18
+ );
19
+ }
20
+
21
+ const endpoint = new URL(
22
+ process.env.INSTANTCLIPS_MCP_URL || DEFAULT_ENDPOINT,
23
+ );
24
+ const transport = new StreamableHTTPClientTransport(endpoint, {
25
+ authProvider: { token: async () => token },
26
+ });
27
+ const client = new Client({
28
+ name: "instantclips-manifest-sync",
29
+ version: "1.0.0",
30
+ });
31
+
32
+ try {
33
+ await client.connect(transport);
34
+
35
+ const tools = [];
36
+ let cursor;
37
+ do {
38
+ const page = await client.listTools(cursor ? { cursor } : undefined);
39
+ tools.push(...page.tools);
40
+ cursor = page.nextCursor;
41
+ } while (cursor);
42
+
43
+ if (!tools.length) {
44
+ throw new Error("The hosted server returned an empty tool list.");
45
+ }
46
+
47
+ const manifest = {
48
+ schemaVersion: 1,
49
+ source: endpoint.href,
50
+ capabilities: client.getServerCapabilities(),
51
+ serverInfo: client.getServerVersion(),
52
+ instructions: client.getInstructions(),
53
+ tools,
54
+ };
55
+ const next = `${JSON.stringify(manifest, null, 2)}\n`;
56
+
57
+ if (checkOnly) {
58
+ const current = await readFile(manifestUrl, "utf8");
59
+ if (current !== next) {
60
+ process.stderr.write(
61
+ "The bundled manifest differs from the hosted InstantClips MCP server. Run npm run sync:manifest and commit the result.\n",
62
+ );
63
+ process.exitCode = 1;
64
+ } else {
65
+ process.stdout.write(
66
+ `Manifest is current: ${tools.length} tools from ${manifest.serverInfo?.name || endpoint.hostname}.\n`,
67
+ );
68
+ }
69
+ } else {
70
+ await writeFile(manifestUrl, next);
71
+ process.stdout.write(
72
+ `Updated ${manifestUrl.pathname}: ${tools.length} tools from ${manifest.serverInfo?.name || endpoint.hostname}.\n`,
73
+ );
74
+ }
75
+ } finally {
76
+ await client.close().catch(() => {});
77
+ }