@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 +59 -21
- package/dist/index.js +11 -6
- package/dist/server.js +117 -26
- package/package.json +4 -2
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",
|
|
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
|
|
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 | `
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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"
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
25
|
+
const tokenFromEnv = process.env[tokenEnvName];
|
|
26
|
+
const token = tokenFromEnv || tokenArg?.split("=")[1];
|
|
22
27
|
if (!token) {
|
|
23
|
-
console.error(
|
|
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: ["
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
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",
|