@infimind/video-mcp-cli 1.0.7 → 1.0.8

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.md CHANGED
@@ -21,7 +21,10 @@ npm install -g @infimind/video-mcp-cli
21
21
  "mcpServers": {
22
22
  "sora-video-generator": {
23
23
  "command": "npx",
24
- "args": ["-y", "@infimind/video-mcp-cli", "--token=YOUR_TOKEN"]
24
+ "args": ["-y", "@infimind/video-mcp-cli"],
25
+ "env": {
26
+ "SORA_MCP_TOKEN": "YOUR_TOKEN"
27
+ }
25
28
  }
26
29
  }
27
30
  }
@@ -48,8 +51,8 @@ Add the following to your Claude Desktop configuration file:
48
51
  "mcpServers": {
49
52
  "sora-video-generator": {
50
53
  "command": "sora-mcp",
51
- "args": ["--token=YOUR_TOKEN_HERE"],
52
54
  "env": {
55
+ "SORA_MCP_TOKEN": "YOUR_TOKEN_HERE",
53
56
  "SORA_API_URL": "https://your-api-domain.com/api"
54
57
  }
55
58
  }
@@ -59,8 +62,11 @@ Add the following to your Claude Desktop configuration file:
59
62
 
60
63
  #### Environment Variables
61
64
 
65
+ - `SORA_MCP_TOKEN` (recommended): Your MCP API token. This avoids exposing the token in process arguments.
62
66
  - `SORA_API_URL` (optional): Your API base URL. Defaults to `http://localhost:3000/api`
63
67
 
68
+ `--token=YOUR_TOKEN` remains supported for backward compatibility. `--token-env=CUSTOM_ENV_NAME` can be used when the token is stored in a different environment variable.
69
+
64
70
  ### 3. Restart Claude Desktop
65
71
 
66
72
  After saving the configuration, restart Claude Desktop for the changes to take effect.
@@ -105,6 +111,23 @@ Local file paths and public URLs are both supported. Local files are converted t
105
111
 
106
112
  Video inputs support MP4/MOV. Audio inputs support MP3/WAV. The server validates media duration, size, count, resolution, and URL safety before estimating credits or creating tasks.
107
113
 
114
+ Media limits:
115
+
116
+ - Local video files: MP4/MOV, up to 50MB each, 2-15 seconds.
117
+ - Local audio files: MP3/WAV, up to 15MB each, 2-15 seconds.
118
+ - S5 allinone: up to 9 images, 3 videos, and 3 audio files.
119
+ - S5 firstlast: up to 2 images; the first frame is required.
120
+ - S6 reference: up to 9 images.
121
+ - S6 edit: up to 1 video and 5 images.
122
+ - `inputDurationTier` is not accepted from clients; the server calculates it from the media.
123
+
124
+ URL safety rules:
125
+
126
+ - Only `http` and `https` URLs are accepted.
127
+ - `localhost`, `127.0.0.1`, private network addresses, reserved IP ranges, and `file:` URLs are rejected.
128
+ - Redirect targets are revalidated.
129
+ - Remote files that are too large, not media, or cannot be safely probed are rejected.
130
+
108
131
  Use `dryRun: true` to validate parameters, VIP access, estimated credits, and media rules without creating a task or deducting credits. The result includes `success`, `dryRun`, `route`, `engineName`, `taskMode`, `estimatedCredits`, `remainingCredits`, and `validationWarnings`. Specific credit tables are server-side only; use dry run to get the current estimated consumption.
109
132
 
110
133
  #### Dry run before creating a task
@@ -132,7 +155,8 @@ Use `dryRun: true` to validate parameters, VIP access, estimated credits, and me
132
155
  "seconds": 10,
133
156
  "size": "720x1280",
134
157
  "language": "zh-CN",
135
- "count": 1
158
+ "count": 1,
159
+ "dryRun": true
136
160
  }
137
161
  ```
138
162
 
@@ -150,7 +174,8 @@ Use `dryRun: true` to validate parameters, VIP access, estimated credits, and me
150
174
  "size": "720x1280",
151
175
  "resolution": "720p",
152
176
  "generateAudio": true,
153
- "webSearchEnabled": false
177
+ "webSearchEnabled": false,
178
+ "dryRun": true
154
179
  }
155
180
  ```
156
181
 
@@ -164,7 +189,8 @@ Use `dryRun: true` to validate parameters, VIP access, estimated credits, and me
164
189
  "sourceImages": ["/path/to/first-frame.jpg", "/path/to/end-frame.jpg"],
165
190
  "seconds": 5,
166
191
  "size": "1280x720",
167
- "resolution": "720p"
192
+ "resolution": "720p",
193
+ "dryRun": true
168
194
  }
169
195
  ```
170
196
 
@@ -180,7 +206,8 @@ Text-to-video:
180
206
  "seconds": 5,
181
207
  "size": "1280x720",
182
208
  "ratio": "16:9",
183
- "resolution": "720p"
209
+ "resolution": "720p",
210
+ "dryRun": true
184
211
  }
185
212
  ```
186
213
 
@@ -194,7 +221,8 @@ Image-to-video:
194
221
  "sourceImage": "/path/to/product.jpg",
195
222
  "seconds": 5,
196
223
  "size": "720x1280",
197
- "resolution": "720p"
224
+ "resolution": "720p",
225
+ "dryRun": true
198
226
  }
199
227
  ```
200
228
 
@@ -209,7 +237,8 @@ Image-to-video:
209
237
  "seconds": 10,
210
238
  "size": "720x1280",
211
239
  "ratio": "9:16",
212
- "resolution": "720p"
240
+ "resolution": "720p",
241
+ "dryRun": true
213
242
  }
214
243
  ```
215
244
 
@@ -222,11 +251,12 @@ Image-to-video:
222
251
  "taskMode": "edit",
223
252
  "sourceVideos": ["/path/to/input.mp4"],
224
253
  "sourceImages": ["/path/to/reference.jpg"],
225
- "resolution": "720p"
254
+ "resolution": "720p",
255
+ "dryRun": true
226
256
  }
227
257
  ```
228
258
 
229
- After creating a task, use `get_video_status` with the returned `taskId` to query progress and video links.
259
+ After dry run succeeds and `estimatedCredits` looks correct, remove `dryRun` or set it to `false` to create the real task. After creating a task, use `get_video_status` with the returned `taskId` to query progress and video links.
230
260
 
231
261
  ## Development
232
262
 
@@ -245,7 +275,7 @@ npm run build
245
275
  npm run test:smoke
246
276
 
247
277
  # Run in development mode
248
- node dist/index.js --token=YOUR_TOKEN
278
+ SORA_MCP_TOKEN=YOUR_TOKEN node dist/index.js
249
279
  ```
250
280
 
251
281
  ### Local MCP testing
@@ -255,7 +285,7 @@ node dist/index.js --token=YOUR_TOKEN
255
285
  ```bash
256
286
  cd mcp-server
257
287
  npm run build
258
- npx @modelcontextprotocol/inspector node dist/index.js --token=YOUR_TOKEN
288
+ SORA_MCP_TOKEN=YOUR_TOKEN npx @modelcontextprotocol/inspector node dist/index.js
259
289
  ```
260
290
 
261
291
  Set `SORA_API_URL=http://localhost:3000/api` when testing against a local app server.
@@ -267,8 +297,9 @@ Set `SORA_API_URL=http://localhost:3000/api` when testing against a local app se
267
297
  "mcpServers": {
268
298
  "sora-video-generator-local": {
269
299
  "command": "node",
270
- "args": ["/absolute/path/to/sora_video_generator/mcp-server/dist/index.js", "--token=YOUR_TOKEN"],
300
+ "args": ["/absolute/path/to/sora_video_generator/mcp-server/dist/index.js"],
271
301
  "env": {
302
+ "SORA_MCP_TOKEN": "YOUR_TOKEN",
272
303
  "SORA_API_URL": "http://localhost:3000/api"
273
304
  }
274
305
  }
@@ -283,8 +314,9 @@ Set `SORA_API_URL=http://localhost:3000/api` when testing against a local app se
283
314
  "mcpServers": {
284
315
  "sora-video-generator": {
285
316
  "command": "npx",
286
- "args": ["-y", "@infimind/video-mcp-cli", "--token=YOUR_TOKEN"],
317
+ "args": ["-y", "@infimind/video-mcp-cli"],
287
318
  "env": {
319
+ "SORA_MCP_TOKEN": "YOUR_TOKEN",
288
320
  "SORA_API_URL": "https://your-api-domain.com/api"
289
321
  }
290
322
  }
@@ -298,16 +330,23 @@ Set `SORA_API_URL=http://localhost:3000/api` when testing against a local app se
298
330
  - If `get_video_status` cannot connect, verify `SORA_API_URL` includes `/api`, for example `http://localhost:3000/api`.
299
331
  - If S5/S6 returns VIP errors, confirm the MCP token belongs to a VIP user.
300
332
  - If S6 reference/edit prompts contain `@图片N`, keep the original markers; those modes skip prompt translation to preserve image references.
301
- - If local file inputs fail, use absolute paths and supported media types: jpg/png/webp/gif, mp4/mov/avi/mkv, mp3/wav/m4a/aac/ogg.
333
+ - If local file inputs fail, use absolute paths and supported media types: jpg/png/webp/gif, mp4/mov, mp3/wav.
302
334
 
303
335
  ## Publishing
304
336
 
305
337
  ```bash
306
- # Build the package
338
+ # Build and verify local dist
307
339
  npm run build
340
+ npm run test:smoke
341
+
342
+ # Verify the package contents that npm will publish
343
+ npm run test:smoke:package
308
344
 
309
345
  # Publish to npm
310
346
  npm publish --access public
347
+
348
+ # Optional after publishing: verify latest through npx
349
+ npm run test:smoke:published
311
350
  ```
312
351
 
313
352
  ## Security
package/dist/index.js CHANGED
@@ -4,23 +4,28 @@ import { createServer } from "./server.js";
4
4
  /**
5
5
  * CLI Entry Point for Sora MCP Server
6
6
  *
7
- * Usage: sora-mcp --token=YOUR_TOKEN
7
+ * Usage: sora-mcp
8
8
  *
9
9
  * Environment Variables:
10
+ * SORA_MCP_TOKEN - MCP API token
10
11
  * SORA_API_URL - API base URL (default: http://localhost:3000/api)
11
12
  */
12
13
  async function main() {
13
14
  // Parse command line arguments
14
15
  const args = process.argv.slice(2);
15
16
  const tokenArg = args.find(arg => arg.startsWith("--token="));
16
- if (!tokenArg) {
17
- console.error("Error: --token argument is required");
18
- console.error("Usage: sora-mcp --token=YOUR_TOKEN");
17
+ const tokenEnvArg = args.find(arg => arg.startsWith("--token-env="));
18
+ const explicitTokenEnvName = tokenEnvArg?.split("=")[1];
19
+ const tokenEnvName = explicitTokenEnvName || "SORA_MCP_TOKEN";
20
+ if (tokenEnvArg && !explicitTokenEnvName) {
21
+ console.error("Error: --token-env value is empty");
22
+ console.error("Usage: sora-mcp --token-env=SORA_MCP_TOKEN");
19
23
  process.exit(1);
20
24
  }
21
- const token = tokenArg.split("=")[1];
25
+ const tokenFromEnv = process.env[tokenEnvName];
26
+ const token = tokenFromEnv || tokenArg?.split("=")[1];
22
27
  if (!token) {
23
- console.error("Error: Token value is empty");
28
+ console.error(`Error: token is required. Set ${tokenEnvName} or pass --token=YOUR_TOKEN.`);
24
29
  process.exit(1);
25
30
  }
26
31
  // Get API URL from environment variable or use default
package/dist/server.js CHANGED
@@ -198,6 +198,19 @@ export function createServer(token, apiBaseUrl) {
198
198
  progress: { type: "number" },
199
199
  totalVideos: { type: "number" },
200
200
  completedVideos: { type: "number" },
201
+ estimatedCredits: { type: "number" },
202
+ chargedVideos: { type: "number" },
203
+ actualRoute: { type: "string" },
204
+ taskMode: { type: ["string", "null"] },
205
+ retryCount: { type: "number" },
206
+ statusHint: { type: "string" },
207
+ createdAt: { type: ["string", "null"] },
208
+ updatedAt: { type: ["string", "null"] },
209
+ completedAt: { type: ["string", "null"] },
210
+ errorMessage: { type: "string" },
211
+ errorCode: { type: "string" },
212
+ errorCategory: { type: "string" },
213
+ retryable: { type: "boolean" },
201
214
  code: { type: "string" },
202
215
  message: { type: "string" },
203
216
  route: { type: "string" },
@@ -212,6 +225,14 @@ export function createServer(token, apiBaseUrl) {
212
225
  videoUrl: { type: ["string", "null"] },
213
226
  thumbnailUrl: { type: ["string", "null"] },
214
227
  errorMessage: { type: ["string", "null"] },
228
+ errorCode: { type: ["string", "null"] },
229
+ errorCategory: { type: ["string", "null"] },
230
+ retryable: { type: ["boolean", "null"] },
231
+ actualRoute: { type: ["string", "null"] },
232
+ retryCount: { type: "number" },
233
+ creditsCharged: { type: "boolean" },
234
+ createdAt: { type: ["string", "null"] },
235
+ completedAt: { type: ["string", "null"] },
215
236
  },
216
237
  required: ["id", "status", "videoUrl", "thumbnailUrl", "errorMessage"],
217
238
  },
@@ -650,8 +671,21 @@ async function handleGetVideoStatus(apiBaseUrl, token, args) {
650
671
  `- **任务 ID**: ${response.taskId}`,
651
672
  `- **状态**: ${statusText}`,
652
673
  `- **进度**: ${response.progress}% (${response.completedVideos}/${response.totalVideos} 个视频)`,
674
+ response.engineName ? `- **生成引擎**: ${response.engineName}` : undefined,
675
+ response.taskMode ? `- **模式**: ${response.taskMode}` : undefined,
676
+ response.actualRoute ? `- **实际线路**: ${response.actualRoute}` : undefined,
677
+ typeof response.estimatedCredits === "number" ? `- **预估积分**: ${response.estimatedCredits}` : undefined,
678
+ typeof response.chargedVideos === "number" ? `- **已扣费视频**: ${response.chargedVideos}/${response.totalVideos}` : undefined,
679
+ typeof response.retryCount === "number" ? `- **重试次数**: ${response.retryCount}` : undefined,
653
680
  ``,
654
- ];
681
+ ].filter((line) => line !== undefined);
682
+ if (response.errorMessage) {
683
+ result.push(`- **错误**: ${response.errorMessage}`);
684
+ if (response.errorCode) {
685
+ result.push(`- **错误代码**: ${response.errorCode}`);
686
+ }
687
+ result.push(``);
688
+ }
655
689
  if (response.videos && response.videos.length > 0) {
656
690
  result.push(`## 视频列表:`);
657
691
  response.videos.forEach((video, index) => {
@@ -669,8 +703,23 @@ async function handleGetVideoStatus(apiBaseUrl, token, args) {
669
703
  if (video.errorMessage) {
670
704
  result.push(` ❌ 错误: ${video.errorMessage}`);
671
705
  }
706
+ if (video.errorCode) {
707
+ result.push(` 错误代码: ${video.errorCode}`);
708
+ }
709
+ if (video.actualRoute) {
710
+ result.push(` 实际线路: ${video.actualRoute}`);
711
+ }
712
+ if (typeof video.retryCount === "number" && video.retryCount > 0) {
713
+ result.push(` 重试次数: ${video.retryCount}`);
714
+ }
715
+ if (typeof video.creditsCharged === "boolean") {
716
+ result.push(` 已扣费: ${video.creditsCharged ? "是" : "否"}`);
717
+ }
672
718
  });
673
719
  }
720
+ if (response.statusHint) {
721
+ result.push(``, response.statusHint);
722
+ }
674
723
  return {
675
724
  content: [
676
725
  {
@@ -685,6 +734,21 @@ async function handleGetVideoStatus(apiBaseUrl, token, args) {
685
734
  progress: response.progress,
686
735
  totalVideos: response.totalVideos,
687
736
  completedVideos: response.completedVideos,
737
+ route: response.route,
738
+ engineName: response.engineName,
739
+ taskMode: response.taskMode,
740
+ actualRoute: response.actualRoute,
741
+ retryCount: response.retryCount,
742
+ chargedVideos: response.chargedVideos,
743
+ estimatedCredits: response.estimatedCredits,
744
+ statusHint: response.statusHint,
745
+ createdAt: response.createdAt,
746
+ updatedAt: response.updatedAt,
747
+ completedAt: response.completedAt,
748
+ errorMessage: response.errorMessage,
749
+ errorCode: response.errorCode,
750
+ errorCategory: response.errorCategory,
751
+ retryable: response.retryable,
688
752
  videos: response.videos || [],
689
753
  },
690
754
  };
@@ -854,7 +918,11 @@ async function callRestApi(apiBaseUrl, path, params, method = "GET") {
854
918
  const data = await response.json();
855
919
  // Check for error in response
856
920
  if (data.error) {
857
- throw new Error(data.error.message || "API 返回错误");
921
+ const toolError = new McpToolError(data.error.message || "API 返回错误", data.error.code || "API_ERROR", data.error.route);
922
+ if (data.error.engineName) {
923
+ toolError.engineName = data.error.engineName;
924
+ }
925
+ throw toolError;
858
926
  }
859
927
  return data;
860
928
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infimind/video-mcp-cli",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "MCP Server for Sora AI Video Generator - Access video generation features through Claude Desktop",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -15,7 +15,9 @@
15
15
  "build": "tsc",
16
16
  "dev": "tsc --watch",
17
17
  "test:smoke": "node scripts/smoke-test.mjs",
18
- "prepublishOnly": "npm run build"
18
+ "test:smoke:package": "MCP_SMOKE_TARGET=package node scripts/smoke-test.mjs",
19
+ "test:smoke:published": "MCP_SMOKE_TARGET=published node scripts/smoke-test.mjs",
20
+ "prepublishOnly": "npm run build && npm run test:smoke:package"
19
21
  },
20
22
  "keywords": [
21
23
  "mcp",