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 +21 -13
- package/README.md +19 -10
- package/README.zh-CN.md +17 -9
- package/bin/instantclips-mcp.js +70 -83
- package/manifest/instantclips-mcp.json +326 -0
- package/package.json +6 -2
- package/scripts/sync-manifest.mjs +77 -0
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
|
|
14
|
-
|
|
15
|
-
|
|
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.
|
|
63
|
-
|
|
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.
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
189
|
-
`
|
|
190
|
-
|
|
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
|
|
13
|
-
tool
|
|
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
|
|
58
|
-
|
|
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.
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
174
|
-
`node bin/instantclips-mcp.js`, and
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
167
|
-
`
|
|
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
|
|
package/bin/instantclips-mcp.js
CHANGED
|
@@ -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
|
|
38
|
+
INSTANTCLIPS_TOKEN Bearer token required for tool calls. Create one at ${SETTINGS_URL}
|
|
32
39
|
|
|
33
|
-
|
|
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
|
|
103
|
-
|
|
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
|
|
140
|
-
|
|
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([
|
|
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
|
|
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.
|
|
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
|
+
}
|