@infimind/video-mcp-cli 1.0.7 → 1.0.9

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
@@ -124,6 +147,8 @@ Use `dryRun: true` to validate parameters, VIP access, estimated credits, and me
124
147
 
125
148
  #### 极睿Sa专业版 - 标准生成
126
149
 
150
+ S2 uses `sourceImage`, `seconds`, and `size`; do not pass `taskMode` or `resolution`.
151
+
127
152
  ```json
128
153
  {
129
154
  "prompt": "一个高质感的产品展示视频,柔和布光,镜头缓慢推进",
@@ -132,7 +157,8 @@ Use `dryRun: true` to validate parameters, VIP access, estimated credits, and me
132
157
  "seconds": 10,
133
158
  "size": "720x1280",
134
159
  "language": "zh-CN",
135
- "count": 1
160
+ "count": 1,
161
+ "dryRun": true
136
162
  }
137
163
  ```
138
164
 
@@ -150,7 +176,8 @@ Use `dryRun: true` to validate parameters, VIP access, estimated credits, and me
150
176
  "size": "720x1280",
151
177
  "resolution": "720p",
152
178
  "generateAudio": true,
153
- "webSearchEnabled": false
179
+ "webSearchEnabled": false,
180
+ "dryRun": true
154
181
  }
155
182
  ```
156
183
 
@@ -164,7 +191,8 @@ Use `dryRun: true` to validate parameters, VIP access, estimated credits, and me
164
191
  "sourceImages": ["/path/to/first-frame.jpg", "/path/to/end-frame.jpg"],
165
192
  "seconds": 5,
166
193
  "size": "1280x720",
167
- "resolution": "720p"
194
+ "resolution": "720p",
195
+ "dryRun": true
168
196
  }
169
197
  ```
170
198
 
@@ -180,7 +208,8 @@ Text-to-video:
180
208
  "seconds": 5,
181
209
  "size": "1280x720",
182
210
  "ratio": "16:9",
183
- "resolution": "720p"
211
+ "resolution": "720p",
212
+ "dryRun": true
184
213
  }
185
214
  ```
186
215
 
@@ -194,7 +223,8 @@ Image-to-video:
194
223
  "sourceImage": "/path/to/product.jpg",
195
224
  "seconds": 5,
196
225
  "size": "720x1280",
197
- "resolution": "720p"
226
+ "resolution": "720p",
227
+ "dryRun": true
198
228
  }
199
229
  ```
200
230
 
@@ -209,7 +239,8 @@ Image-to-video:
209
239
  "seconds": 10,
210
240
  "size": "720x1280",
211
241
  "ratio": "9:16",
212
- "resolution": "720p"
242
+ "resolution": "720p",
243
+ "dryRun": true
213
244
  }
214
245
  ```
215
246
 
@@ -222,11 +253,12 @@ Image-to-video:
222
253
  "taskMode": "edit",
223
254
  "sourceVideos": ["/path/to/input.mp4"],
224
255
  "sourceImages": ["/path/to/reference.jpg"],
225
- "resolution": "720p"
256
+ "resolution": "720p",
257
+ "dryRun": true
226
258
  }
227
259
  ```
228
260
 
229
- After creating a task, use `get_video_status` with the returned `taskId` to query progress and video links.
261
+ 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
262
 
231
263
  ## Development
232
264
 
@@ -245,7 +277,7 @@ npm run build
245
277
  npm run test:smoke
246
278
 
247
279
  # Run in development mode
248
- node dist/index.js --token=YOUR_TOKEN
280
+ SORA_MCP_TOKEN=YOUR_TOKEN node dist/index.js
249
281
  ```
250
282
 
251
283
  ### Local MCP testing
@@ -255,7 +287,7 @@ node dist/index.js --token=YOUR_TOKEN
255
287
  ```bash
256
288
  cd mcp-server
257
289
  npm run build
258
- npx @modelcontextprotocol/inspector node dist/index.js --token=YOUR_TOKEN
290
+ SORA_MCP_TOKEN=YOUR_TOKEN npx @modelcontextprotocol/inspector node dist/index.js
259
291
  ```
260
292
 
261
293
  Set `SORA_API_URL=http://localhost:3000/api` when testing against a local app server.
@@ -267,8 +299,9 @@ Set `SORA_API_URL=http://localhost:3000/api` when testing against a local app se
267
299
  "mcpServers": {
268
300
  "sora-video-generator-local": {
269
301
  "command": "node",
270
- "args": ["/absolute/path/to/sora_video_generator/mcp-server/dist/index.js", "--token=YOUR_TOKEN"],
302
+ "args": ["/absolute/path/to/sora_video_generator/mcp-server/dist/index.js"],
271
303
  "env": {
304
+ "SORA_MCP_TOKEN": "YOUR_TOKEN",
272
305
  "SORA_API_URL": "http://localhost:3000/api"
273
306
  }
274
307
  }
@@ -283,8 +316,9 @@ Set `SORA_API_URL=http://localhost:3000/api` when testing against a local app se
283
316
  "mcpServers": {
284
317
  "sora-video-generator": {
285
318
  "command": "npx",
286
- "args": ["-y", "@infimind/video-mcp-cli", "--token=YOUR_TOKEN"],
319
+ "args": ["-y", "@infimind/video-mcp-cli"],
287
320
  "env": {
321
+ "SORA_MCP_TOKEN": "YOUR_TOKEN",
288
322
  "SORA_API_URL": "https://your-api-domain.com/api"
289
323
  }
290
324
  }
@@ -298,16 +332,23 @@ Set `SORA_API_URL=http://localhost:3000/api` when testing against a local app se
298
332
  - If `get_video_status` cannot connect, verify `SORA_API_URL` includes `/api`, for example `http://localhost:3000/api`.
299
333
  - If S5/S6 returns VIP errors, confirm the MCP token belongs to a VIP user.
300
334
  - 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.
335
+ - If local file inputs fail, use absolute paths and supported media types: jpg/png/webp/gif, mp4/mov, mp3/wav.
302
336
 
303
337
  ## Publishing
304
338
 
305
339
  ```bash
306
- # Build the package
340
+ # Build and verify local dist
307
341
  npm run build
342
+ npm run test:smoke
343
+
344
+ # Verify the package contents that npm will publish
345
+ npm run test:smoke:package
308
346
 
309
347
  # Publish to npm
310
348
  npm publish --access public
349
+
350
+ # Optional after publishing: verify latest through npx
351
+ npm run test:smoke:published
311
352
  ```
312
353
 
313
354
  ## 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
  },
@@ -570,26 +591,32 @@ function validateCreateVideoArgs(args) {
570
591
  */
571
592
  async function handleCreateVideoTask(apiBaseUrl, token, args) {
572
593
  const normalized = validateCreateVideoArgs(args);
573
- const response = await callRestApi(apiBaseUrl, "/mcp/video/create", {
594
+ const isProfessionalRoute = normalized.route === "S5" || normalized.route === "S6";
595
+ const requestBody = {
574
596
  token,
575
597
  prompt: args.prompt,
576
- sourceImage: args.sourceImage ? (await processFileInput(args.sourceImage, "image")).data : undefined,
577
- sourceImages: await processFileInputs(args.sourceImages, "image"),
578
- sourceVideos: await processFileInputs(args.sourceVideos, "video"),
579
- sourceAudios: await processFileInputs(args.sourceAudios, "audio"),
598
+ sourceImage: args.sourceImage
599
+ ? (await processFileInput(args.sourceImage, "image")).data
600
+ : undefined,
580
601
  size: args.size || "1280x720",
581
602
  seconds: args.seconds || 10,
582
603
  language: args.language || "zh-CN",
583
604
  count: args.count || 1,
584
605
  route: normalized.route,
585
- taskMode: normalized.taskMode,
586
- isFirstLastMode: normalized.taskMode === "firstlast",
587
- resolution: normalized.resolution,
588
- ratio: args.ratio || undefined,
589
- generateAudio: args.generateAudio,
590
- webSearchEnabled: args.webSearchEnabled,
591
606
  dryRun: normalized.dryRun,
592
- }, "POST");
607
+ };
608
+ if (isProfessionalRoute) {
609
+ requestBody.sourceImages = await processFileInputs(args.sourceImages, "image");
610
+ requestBody.sourceVideos = await processFileInputs(args.sourceVideos, "video");
611
+ requestBody.sourceAudios = await processFileInputs(args.sourceAudios, "audio");
612
+ requestBody.taskMode = normalized.taskMode;
613
+ requestBody.isFirstLastMode = normalized.taskMode === "firstlast";
614
+ requestBody.resolution = normalized.resolution;
615
+ requestBody.ratio = args.ratio || undefined;
616
+ requestBody.generateAudio = args.generateAudio;
617
+ requestBody.webSearchEnabled = args.webSearchEnabled;
618
+ }
619
+ const response = await callRestApi(apiBaseUrl, "/mcp/video/create", requestBody, "POST");
593
620
  const route = response.route || normalized.route;
594
621
  const engineName = response.engineName || getMcpEngineName(route);
595
622
  const taskMode = response.taskMode || normalized.taskMode;
@@ -650,8 +677,21 @@ async function handleGetVideoStatus(apiBaseUrl, token, args) {
650
677
  `- **任务 ID**: ${response.taskId}`,
651
678
  `- **状态**: ${statusText}`,
652
679
  `- **进度**: ${response.progress}% (${response.completedVideos}/${response.totalVideos} 个视频)`,
680
+ response.engineName ? `- **生成引擎**: ${response.engineName}` : undefined,
681
+ response.taskMode ? `- **模式**: ${response.taskMode}` : undefined,
682
+ response.actualRoute ? `- **实际线路**: ${response.actualRoute}` : undefined,
683
+ typeof response.estimatedCredits === "number" ? `- **预估积分**: ${response.estimatedCredits}` : undefined,
684
+ typeof response.chargedVideos === "number" ? `- **已扣费视频**: ${response.chargedVideos}/${response.totalVideos}` : undefined,
685
+ typeof response.retryCount === "number" ? `- **重试次数**: ${response.retryCount}` : undefined,
653
686
  ``,
654
- ];
687
+ ].filter((line) => line !== undefined);
688
+ if (response.errorMessage) {
689
+ result.push(`- **错误**: ${response.errorMessage}`);
690
+ if (response.errorCode) {
691
+ result.push(`- **错误代码**: ${response.errorCode}`);
692
+ }
693
+ result.push(``);
694
+ }
655
695
  if (response.videos && response.videos.length > 0) {
656
696
  result.push(`## 视频列表:`);
657
697
  response.videos.forEach((video, index) => {
@@ -669,8 +709,23 @@ async function handleGetVideoStatus(apiBaseUrl, token, args) {
669
709
  if (video.errorMessage) {
670
710
  result.push(` ❌ 错误: ${video.errorMessage}`);
671
711
  }
712
+ if (video.errorCode) {
713
+ result.push(` 错误代码: ${video.errorCode}`);
714
+ }
715
+ if (video.actualRoute) {
716
+ result.push(` 实际线路: ${video.actualRoute}`);
717
+ }
718
+ if (typeof video.retryCount === "number" && video.retryCount > 0) {
719
+ result.push(` 重试次数: ${video.retryCount}`);
720
+ }
721
+ if (typeof video.creditsCharged === "boolean") {
722
+ result.push(` 已扣费: ${video.creditsCharged ? "是" : "否"}`);
723
+ }
672
724
  });
673
725
  }
726
+ if (response.statusHint) {
727
+ result.push(``, response.statusHint);
728
+ }
674
729
  return {
675
730
  content: [
676
731
  {
@@ -685,6 +740,21 @@ async function handleGetVideoStatus(apiBaseUrl, token, args) {
685
740
  progress: response.progress,
686
741
  totalVideos: response.totalVideos,
687
742
  completedVideos: response.completedVideos,
743
+ route: response.route,
744
+ engineName: response.engineName,
745
+ taskMode: response.taskMode,
746
+ actualRoute: response.actualRoute,
747
+ retryCount: response.retryCount,
748
+ chargedVideos: response.chargedVideos,
749
+ estimatedCredits: response.estimatedCredits,
750
+ statusHint: response.statusHint,
751
+ createdAt: response.createdAt,
752
+ updatedAt: response.updatedAt,
753
+ completedAt: response.completedAt,
754
+ errorMessage: response.errorMessage,
755
+ errorCode: response.errorCode,
756
+ errorCategory: response.errorCategory,
757
+ retryable: response.retryable,
688
758
  videos: response.videos || [],
689
759
  },
690
760
  };
@@ -854,7 +924,11 @@ async function callRestApi(apiBaseUrl, path, params, method = "GET") {
854
924
  const data = await response.json();
855
925
  // Check for error in response
856
926
  if (data.error) {
857
- throw new Error(data.error.message || "API 返回错误");
927
+ const toolError = new McpToolError(data.error.message || "API 返回错误", data.error.code || "API_ERROR", data.error.route);
928
+ if (data.error.engineName) {
929
+ toolError.engineName = data.error.engineName;
930
+ }
931
+ throw toolError;
858
932
  }
859
933
  return data;
860
934
  }
@@ -1,6 +1,6 @@
1
1
  export type McpRoute = "S2" | "S5" | "S6";
2
2
  export type McpTaskMode = "allinone" | "firstlast" | "standard" | "reference" | "edit";
3
- export declare const MCP_SERVER_VERSION = "1.0.5";
3
+ export declare const MCP_SERVER_VERSION = "1.0.9";
4
4
  export declare const MCP_VIDEO_STATUS_HINT = "\u4EFB\u52A1\u5DF2\u521B\u5EFA\u540E\uFF0C\u4F7F\u7528 get_video_status \u5E76\u4F20\u5165 taskId \u67E5\u8BE2\u751F\u6210\u8FDB\u5EA6\u548C\u89C6\u9891\u94FE\u63A5\u3002";
5
5
  export declare const MCP_VIDEO_DRY_RUN_HINT = "dryRun \u4EC5\u5B8C\u6210\u53C2\u6570\u3001\u6743\u9650\u3001\u79EF\u5206\u548C\u7D20\u6750\u89C4\u5219\u9884\u68C0\uFF0C\u4E0D\u4F1A\u521B\u5EFA\u4EFB\u52A1\u6216\u6263\u8D39\u3002";
6
6
  export declare const MCP_VIDEO_ENGINES: Record<McpRoute, {
@@ -1,4 +1,4 @@
1
- export const MCP_SERVER_VERSION = "1.0.5";
1
+ export const MCP_SERVER_VERSION = "1.0.9";
2
2
  export const MCP_VIDEO_STATUS_HINT = "任务已创建后,使用 get_video_status 并传入 taskId 查询生成进度和视频链接。";
3
3
  export const MCP_VIDEO_DRY_RUN_HINT = "dryRun 仅完成参数、权限、积分和素材规则预检,不会创建任务或扣费。";
4
4
  export const MCP_VIDEO_ENGINES = {
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.9",
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",