@infimind/video-mcp-cli 1.0.6 → 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.
@@ -90,13 +96,12 @@ Parameter applicability:
90
96
 
91
97
  | Parameter | S2 极睿Sa专业版 | S5 极睿Sd专业版 | S6 极睿Ha专业版 |
92
98
  | --- | --- | --- | --- |
93
- | `sourceImage` | Optional single image | Not recommended | Standard mode single image |
99
+ | `sourceImage` | Optional single image | Not supported | Standard mode single image |
94
100
  | `sourceImages` | Not supported | allinone / firstlast images | reference / edit reference images |
95
101
  | `sourceVideos` | Not supported | allinone reference videos | edit input videos |
96
102
  | `sourceAudios` | Not supported | allinone reference audio | Not supported |
97
103
  | `taskMode` | Not needed | `allinone` / `firstlast` | `standard` / `reference` / `edit` |
98
- | `resolution` | Not supported | `480p` / `720p` / `1080p` | `720p` / `1080p` |
99
- | `inputDurationTier` | Not supported | `short` / `medium` / `long` | edit mode only |
104
+ | `resolution` | Not supported | `720p` / `1080p` | `720p` / `1080p` |
100
105
  | `ratio` | Not supported | Not supported | reference or text-to-video standard mode |
101
106
  | `generateAudio` | Not supported | Optional | Not supported |
102
107
  | `webSearchEnabled` | Not supported | Optional | Not supported |
@@ -104,7 +109,26 @@ Parameter applicability:
104
109
 
105
110
  Local file paths and public URLs are both supported. Local files are converted to base64 by the MCP server before being sent to the API.
106
111
 
107
- 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`.
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.
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
+
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.
108
132
 
109
133
  #### Dry run before creating a task
110
134
 
@@ -131,7 +155,8 @@ Use `dryRun: true` to validate parameters, VIP access, estimated credits, and me
131
155
  "seconds": 10,
132
156
  "size": "720x1280",
133
157
  "language": "zh-CN",
134
- "count": 1
158
+ "count": 1,
159
+ "dryRun": true
135
160
  }
136
161
  ```
137
162
 
@@ -148,9 +173,9 @@ Use `dryRun: true` to validate parameters, VIP access, estimated credits, and me
148
173
  "seconds": 10,
149
174
  "size": "720x1280",
150
175
  "resolution": "720p",
151
- "inputDurationTier": "medium",
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
 
@@ -223,11 +252,11 @@ Image-to-video:
223
252
  "sourceVideos": ["/path/to/input.mp4"],
224
253
  "sourceImages": ["/path/to/reference.jpg"],
225
254
  "resolution": "720p",
226
- "inputDurationTier": "medium"
255
+ "dryRun": true
227
256
  }
228
257
  ```
229
258
 
230
- 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.
231
260
 
232
261
  ## Development
233
262
 
@@ -246,7 +275,7 @@ npm run build
246
275
  npm run test:smoke
247
276
 
248
277
  # Run in development mode
249
- node dist/index.js --token=YOUR_TOKEN
278
+ SORA_MCP_TOKEN=YOUR_TOKEN node dist/index.js
250
279
  ```
251
280
 
252
281
  ### Local MCP testing
@@ -256,7 +285,7 @@ node dist/index.js --token=YOUR_TOKEN
256
285
  ```bash
257
286
  cd mcp-server
258
287
  npm run build
259
- npx @modelcontextprotocol/inspector node dist/index.js --token=YOUR_TOKEN
288
+ SORA_MCP_TOKEN=YOUR_TOKEN npx @modelcontextprotocol/inspector node dist/index.js
260
289
  ```
261
290
 
262
291
  Set `SORA_API_URL=http://localhost:3000/api` when testing against a local app server.
@@ -268,8 +297,9 @@ Set `SORA_API_URL=http://localhost:3000/api` when testing against a local app se
268
297
  "mcpServers": {
269
298
  "sora-video-generator-local": {
270
299
  "command": "node",
271
- "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"],
272
301
  "env": {
302
+ "SORA_MCP_TOKEN": "YOUR_TOKEN",
273
303
  "SORA_API_URL": "http://localhost:3000/api"
274
304
  }
275
305
  }
@@ -284,8 +314,9 @@ Set `SORA_API_URL=http://localhost:3000/api` when testing against a local app se
284
314
  "mcpServers": {
285
315
  "sora-video-generator": {
286
316
  "command": "npx",
287
- "args": ["-y", "@infimind/video-mcp-cli", "--token=YOUR_TOKEN"],
317
+ "args": ["-y", "@infimind/video-mcp-cli"],
288
318
  "env": {
319
+ "SORA_MCP_TOKEN": "YOUR_TOKEN",
289
320
  "SORA_API_URL": "https://your-api-domain.com/api"
290
321
  }
291
322
  }
@@ -299,16 +330,23 @@ Set `SORA_API_URL=http://localhost:3000/api` when testing against a local app se
299
330
  - If `get_video_status` cannot connect, verify `SORA_API_URL` includes `/api`, for example `http://localhost:3000/api`.
300
331
  - If S5/S6 returns VIP errors, confirm the MCP token belongs to a VIP user.
301
332
  - If S6 reference/edit prompts contain `@图片N`, keep the original markers; those modes skip prompt translation to preserve image references.
302
- - 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.
303
334
 
304
335
  ## Publishing
305
336
 
306
337
  ```bash
307
- # Build the package
338
+ # Build and verify local dist
308
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
309
344
 
310
345
  # Publish to npm
311
346
  npm publish --access public
347
+
348
+ # Optional after publishing: verify latest through npx
349
+ npm run test:smoke:published
312
350
  ```
313
351
 
314
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
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
8
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
9
- import { readFileSync, existsSync } from "fs";
9
+ import { readFileSync, existsSync, statSync } from "fs";
10
10
  import { extname, basename } from "path";
11
11
  import { MCP_SERVER_VERSION, getMcpDefaultTaskMode, getMcpEngineName, getMcpVideoStatusHint, isMcpRoute, isValidMcpTaskMode, } from "./videoRules.js";
12
12
  class McpToolError extends Error {
@@ -129,12 +129,7 @@ export function createServer(token, apiBaseUrl) {
129
129
  resolution: {
130
130
  type: "string",
131
131
  description: "S5/S6 输出清晰度(默认:720p)",
132
- enum: ["480p", "720p", "1080p"],
133
- },
134
- inputDurationTier: {
135
- type: "string",
136
- description: "S5 或 S6 edit 模式的输入素材时长档位:short=0-4秒、medium=5-10秒、long=11-15秒(默认:short)",
137
- enum: ["short", "medium", "long"],
132
+ enum: ["720p", "1080p"],
138
133
  },
139
134
  ratio: {
140
135
  type: "string",
@@ -155,6 +150,7 @@ export function createServer(token, apiBaseUrl) {
155
150
  },
156
151
  },
157
152
  required: ["prompt"],
153
+ additionalProperties: false,
158
154
  },
159
155
  outputSchema: {
160
156
  type: "object",
@@ -202,6 +198,19 @@ export function createServer(token, apiBaseUrl) {
202
198
  progress: { type: "number" },
203
199
  totalVideos: { type: "number" },
204
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" },
205
214
  code: { type: "string" },
206
215
  message: { type: "string" },
207
216
  route: { type: "string" },
@@ -216,6 +225,14 @@ export function createServer(token, apiBaseUrl) {
216
225
  videoUrl: { type: ["string", "null"] },
217
226
  thumbnailUrl: { type: ["string", "null"] },
218
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"] },
219
236
  },
220
237
  required: ["id", "status", "videoUrl", "thumbnailUrl", "errorMessage"],
221
238
  },
@@ -402,21 +419,23 @@ async function processFileInput(fileInput, expectedType) {
402
419
  ".webp": "image/webp",
403
420
  ".mp4": "video/mp4",
404
421
  ".mov": "video/quicktime",
405
- ".avi": "video/x-msvideo",
406
- ".mkv": "video/x-matroska",
407
422
  ".mp3": "audio/mpeg",
408
423
  ".wav": "audio/wav",
409
- ".m4a": "audio/mp4",
410
- ".aac": "audio/aac",
411
- ".ogg": "audio/ogg",
412
424
  };
413
425
  if (!(ext in mimeTypes)) {
414
- throw new McpToolError(`不支持的文件类型:${ext || "无扩展名"}。请使用 jpg/png/webp/gif 图片、mp4/mov/avi/mkv 视频或 mp3/wav/m4a/aac/ogg 音频。`, "UNSUPPORTED_FILE_TYPE");
426
+ throw new McpToolError(`不支持的文件类型:${ext || "无扩展名"}。请使用 jpg/png/webp/gif 图片、mp4/mov 视频或 mp3/wav 音频。`, "UNSUPPORTED_FILE_TYPE");
415
427
  }
416
428
  mimeType = mimeTypes[ext];
417
429
  if (expectedType && !mimeType.startsWith(`${expectedType}/`)) {
418
430
  throw new McpToolError(`文件 ${basename(filePath)} 类型不匹配:当前是 ${mimeType},但该字段需要 ${expectedType} 文件。`, "INVALID_FILE_TYPE");
419
431
  }
432
+ const size = statSync(filePath).size;
433
+ if (expectedType === "video" && size > 50 * 1024 * 1024) {
434
+ throw new McpToolError(`视频文件 ${basename(filePath)} 超过 50MB 限制。`, "MEDIA_FILE_TOO_LARGE");
435
+ }
436
+ if (expectedType === "audio" && size > 15 * 1024 * 1024) {
437
+ throw new McpToolError(`音频文件 ${basename(filePath)} 超过 15MB 限制。`, "MEDIA_FILE_TOO_LARGE");
438
+ }
420
439
  // 转换为 base64
421
440
  const base64 = `data:${mimeType};base64,${buffer.toString("base64")}`;
422
441
  return {
@@ -438,6 +457,9 @@ async function processFileInputs(fileInputs, expectedType) {
438
457
  function hasItems(value) {
439
458
  return Array.isArray(value) && value.some((item) => typeof item === "string" && item.length > 0);
440
459
  }
460
+ function itemCount(value) {
461
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.length > 0).length : 0;
462
+ }
441
463
  function normalizeRoute(value) {
442
464
  const route = typeof value === "string" ? value : "S2";
443
465
  if (!isMcpRoute(route)) {
@@ -459,13 +481,6 @@ function normalizeTaskMode(route, value) {
459
481
  }
460
482
  return mode;
461
483
  }
462
- function normalizeInputDurationTier(value) {
463
- const tier = (typeof value === "string" ? value : "short");
464
- if (!["short", "medium", "long"].includes(tier)) {
465
- throw new McpToolError("inputDurationTier 只能是 short、medium 或 long。");
466
- }
467
- return tier;
468
- }
469
484
  function validateCreateVideoArgs(args) {
470
485
  if (!args || typeof args !== "object") {
471
486
  throw new McpToolError("请提供视频生成参数。");
@@ -473,11 +488,19 @@ function validateCreateVideoArgs(args) {
473
488
  if (!args.prompt || typeof args.prompt !== "string" || !args.prompt.trim()) {
474
489
  throw new McpToolError("prompt 不能为空。");
475
490
  }
491
+ if (args.count !== undefined && (!Number.isInteger(args.count) || args.count < 1 || args.count > 12)) {
492
+ throw new McpToolError("count 必须是 1-12 的整数。", "INVALID_COUNT");
493
+ }
476
494
  const route = normalizeRoute(args.route);
477
495
  const taskMode = normalizeTaskMode(route, args.taskMode);
478
- const inputDurationTier = normalizeInputDurationTier(args.inputDurationTier);
479
496
  const resolution = typeof args.resolution === "string" ? args.resolution : "720p";
480
497
  const dryRun = args.dryRun === true;
498
+ if (args.inputDurationTier !== undefined) {
499
+ throw new McpToolError("inputDurationTier 不再支持由 MCP 传入;输入视频时长档位由服务端根据素材自动计算。", "UNSUPPORTED_FIELD", route);
500
+ }
501
+ if ((route === "S5" || route === "S6") && resolution !== "720p" && resolution !== "1080p") {
502
+ throw new McpToolError(`${getMcpEngineName(route)} 仅支持 720p 或 1080p。`, "INVALID_RESOLUTION", route);
503
+ }
481
504
  if (route === "S2" && args.seconds === 5) {
482
505
  throw new McpToolError("极睿Sa专业版不支持 5 秒视频,请使用 10 或 15 秒。", "INVALID_SECONDS", route);
483
506
  }
@@ -487,7 +510,6 @@ function validateCreateVideoArgs(args) {
487
510
  hasItems(args.sourceVideos) ? "sourceVideos" : undefined,
488
511
  hasItems(args.sourceAudios) ? "sourceAudios" : undefined,
489
512
  args.resolution !== undefined ? "resolution" : undefined,
490
- args.inputDurationTier !== undefined ? "inputDurationTier" : undefined,
491
513
  args.ratio !== undefined ? "ratio" : undefined,
492
514
  args.generateAudio !== undefined ? "generateAudio" : undefined,
493
515
  args.webSearchEnabled !== undefined ? "webSearchEnabled" : undefined,
@@ -509,6 +531,21 @@ function validateCreateVideoArgs(args) {
509
531
  if (taskMode === "firstlast" && (hasItems(args.sourceVideos) || hasItems(args.sourceAudios))) {
510
532
  throw new McpToolError("sourceVideos/sourceAudios 仅适用于极睿Sd专业版全能参考模式;首尾帧模式只使用 sourceImages。", "UNSUPPORTED_FIELD", route);
511
533
  }
534
+ if (taskMode === "firstlast" && itemCount(args.sourceImages) > 2) {
535
+ throw new McpToolError("极睿Sd专业版首尾帧模式最多支持 2 张图片。", "MEDIA_COUNT_EXCEEDED", route);
536
+ }
537
+ if (taskMode === "allinone" && itemCount(args.sourceImages) > 9) {
538
+ throw new McpToolError("极睿Sd专业版全能参考模式最多支持 9 张参考图片。", "MEDIA_COUNT_EXCEEDED", route);
539
+ }
540
+ if (taskMode === "allinone" && itemCount(args.sourceVideos) > 3) {
541
+ throw new McpToolError("极睿Sd专业版全能参考模式最多支持 3 个参考视频。", "MEDIA_COUNT_EXCEEDED", route);
542
+ }
543
+ if (taskMode === "allinone" && itemCount(args.sourceAudios) > 3) {
544
+ throw new McpToolError("极睿Sd专业版全能参考模式最多支持 3 个参考音频。", "MEDIA_COUNT_EXCEEDED", route);
545
+ }
546
+ if (taskMode === "allinone" && hasItems(args.sourceAudios) && !hasItems(args.sourceImages) && !hasItems(args.sourceVideos)) {
547
+ throw new McpToolError("极睿Sd专业版全能参考模式中,音频素材必须搭配至少一张参考图片或一个参考视频。", "MISSING_VISUAL_REFERENCE", route);
548
+ }
512
549
  }
513
550
  if (route === "S6") {
514
551
  const unsupported = [
@@ -531,12 +568,20 @@ function validateCreateVideoArgs(args) {
531
568
  if (taskMode === "edit" && !hasItems(args.sourceVideos)) {
532
569
  throw new McpToolError("极睿Ha专业版视频编辑模式需要在 sourceVideos 中提供至少 1 个参考视频。", "MISSING_SOURCE_VIDEO", route);
533
570
  }
571
+ if (taskMode === "reference" && itemCount(args.sourceImages) > 9) {
572
+ throw new McpToolError("极睿Ha专业版图片参考模式最多支持 9 张参考图片。", "MEDIA_COUNT_EXCEEDED", route);
573
+ }
574
+ if (taskMode === "edit" && itemCount(args.sourceVideos) > 1) {
575
+ throw new McpToolError("极睿Ha专业版视频编辑模式最多支持 1 个参考视频。", "MEDIA_COUNT_EXCEEDED", route);
576
+ }
577
+ if (taskMode === "edit" && itemCount(args.sourceImages) > 5) {
578
+ throw new McpToolError("极睿Ha专业版视频编辑模式最多支持 5 张参考图片。", "MEDIA_COUNT_EXCEEDED", route);
579
+ }
534
580
  }
535
581
  return {
536
582
  route,
537
583
  taskMode,
538
584
  resolution,
539
- inputDurationTier,
540
585
  dryRun,
541
586
  statusHint: getMcpVideoStatusHint(dryRun),
542
587
  };
@@ -561,7 +606,6 @@ async function handleCreateVideoTask(apiBaseUrl, token, args) {
561
606
  taskMode: normalized.taskMode,
562
607
  isFirstLastMode: normalized.taskMode === "firstlast",
563
608
  resolution: normalized.resolution,
564
- inputDurationTier: normalized.inputDurationTier,
565
609
  ratio: args.ratio || undefined,
566
610
  generateAudio: args.generateAudio,
567
611
  webSearchEnabled: args.webSearchEnabled,
@@ -627,8 +671,21 @@ async function handleGetVideoStatus(apiBaseUrl, token, args) {
627
671
  `- **任务 ID**: ${response.taskId}`,
628
672
  `- **状态**: ${statusText}`,
629
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,
630
680
  ``,
631
- ];
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
+ }
632
689
  if (response.videos && response.videos.length > 0) {
633
690
  result.push(`## 视频列表:`);
634
691
  response.videos.forEach((video, index) => {
@@ -646,8 +703,23 @@ async function handleGetVideoStatus(apiBaseUrl, token, args) {
646
703
  if (video.errorMessage) {
647
704
  result.push(` ❌ 错误: ${video.errorMessage}`);
648
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
+ }
649
718
  });
650
719
  }
720
+ if (response.statusHint) {
721
+ result.push(``, response.statusHint);
722
+ }
651
723
  return {
652
724
  content: [
653
725
  {
@@ -662,6 +734,21 @@ async function handleGetVideoStatus(apiBaseUrl, token, args) {
662
734
  progress: response.progress,
663
735
  totalVideos: response.totalVideos,
664
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,
665
752
  videos: response.videos || [],
666
753
  },
667
754
  };
@@ -831,7 +918,11 @@ async function callRestApi(apiBaseUrl, path, params, method = "GET") {
831
918
  const data = await response.json();
832
919
  // Check for error in response
833
920
  if (data.error) {
834
- 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;
835
926
  }
836
927
  return data;
837
928
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infimind/video-mcp-cli",
3
- "version": "1.0.6",
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",