corent-mcp 0.8.0 → 0.9.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.md CHANGED
@@ -9,11 +9,13 @@ Give any AI agent the ability to generate images, videos, voice, and text throug
9
9
  | `plan` | Describe a request in plain language; Corent returns the plan (image vs video, tier, settings) + cost estimate, without generating |
10
10
  | `create` | Describe what you want + a spend ceiling; Corent decides everything and generates it (the "zero decisions" path) |
11
11
  | `generate_image` | Text → image, synchronous, returns a permanent URL. Takes `reference_image_urls` for character and product consistency |
12
- | `generate_video` | Text (or image) → video, async job |
12
+ | `generate_video` | Text, image, or references (`@Image 1`, `@Video 1`, `@Audio 1`) → video, async job. `return_last_frame` hands back the final frame to chain shots |
13
+ | `edit_video` | Change something in a finished clip (add, remove, replace); keeps its length and shape |
14
+ | `extend_video` | Continue a finished clip forward or backward |
13
15
  | `generate_speech` | Text → spoken audio, synchronous |
14
16
  | `generate_text` | Prompt or full conversation → text from a frontier language model. Supports tool calling and JSON mode |
15
17
  | `generate_image_batch` | Up to 50 images in one call, async |
16
- | `generate_video_batch` | Up to 50 clips in one call, async |
18
+ | `generate_video_batch` | Up to 50 clips in one call, async; items take the same reference fields as `generate_video` |
17
19
  | `get_batch` | Progress of a submitted batch |
18
20
  | `list_models` | The direct-access menu: every model that can be pinned by name (`corent-*`), with quality and live status |
19
21
  | `list_tiers` | The tier menu: prices, shapes, video resolutions, and which tiers accept reference images or render sound |
@@ -44,8 +46,8 @@ publication as a skills repo.
44
46
  | Prompt | Arguments | What it makes |
45
47
  |---|---|---|
46
48
  | `product-ugc-reel` | `product_url`, `brief`, `budget_usd` | Vertical creator-style reel for one product |
47
- | `character-series` | `character`, `scenes`, `budget_usd` | One consistent character across many scenes |
48
- | `storyboard-to-clip` | `storyboard`, `style`, `budget_usd` | Numbered storyboard to stills, clips and narration |
49
+ | `character-series` | `character`, `scenes`, `budget_usd` | One consistent character across many scenes and clips |
50
+ | `storyboard-to-clip` | `storyboard`, `style`, `budget_usd` | Numbered storyboard to stills, clips chained through last frames, and narration |
49
51
  | `ad-variations` | `concept`, `count`, `product_url` | Many versions of one ad visual, in a single batch |
50
52
  | `faceless-explainer` | `topic`, `duration_s`, `budget_usd` | Narrated explainer with no presenter |
51
53
 
@@ -84,8 +86,9 @@ results back, and `tools` / `response_format` for function calling and JSON.
84
86
  that the prompt is applied as an *edit* of, so the same face, character, or
85
87
  product survives into a new scene. Use it whenever the user wants "the same
86
88
  person again", a product placed somewhere, or a matching series. Edit-capable
87
- models sit at premium and up, so pass `tier` `premium` / `pro` / `max_pro`, or
88
- no tier at all: `air` and `lite` are refused with a message saying so.
89
+ models sit at pro and up, so pass `tier` `pro` / `max_pro`, or no tier at all:
90
+ `air`, `lite` and `premium` are refused with a message saying so. (Check
91
+ `list_tiers` -> `supports_reference_images` for the live list.)
89
92
 
90
93
  ### What the generate tools can ask for
91
94
 
@@ -100,6 +103,34 @@ that actually render it, so a silent model can never quietly serve the ask),
100
103
  `end_image_url` for the frame to finish on, a `camera` move, `negative_prompt`,
101
104
  `seed` and `fps`.
102
105
 
106
+ ### Video with references, edits, extensions and chained shots
107
+
108
+ On `corent-seedance-2.5` `generate_video` also
109
+ takes **`reference_image_urls`** (up to 30), **`reference_video_urls`** (up to
110
+ 10) and **`reference_audio_urls`** (up to 10). The prompt points at them as
111
+ `@Image 1`, `@Video 1`, `@Audio 1` in the order given, so "the character in
112
+ @Image 1 walks through the market" keeps that character. `task` says what to
113
+ do with them: `auto` (default), `reference`, `edit` or `extend`.
114
+ `edit_video` and `extend_video` are the two-argument versions of the last two:
115
+ give them a prompt and the clip URL and they set the rest.
116
+
117
+ **`return_last_frame: true`** stores a PNG of the final frame and returns it as
118
+ `last_frame_url` on the finished job; pass it as the next clip's `image_url`
119
+ and the action continues without a cut. That is how a longer scene is built
120
+ from 5 to 10 second clips.
121
+
122
+ The API rules, mirrored here so nothing is spent on a request it would refuse:
123
+ first/last frame (`image_url` / `end_image_url`) and `reference_*` cannot be
124
+ mixed; an edit keeps the source length, so omit `duration_s`; on first/last
125
+ frame, edit and extend the clip keeps the source shape, so omit `aspect_ratio`
126
+ or pass `adaptive`. `list_models` reports `supports_reference_images`,
127
+ `max_reference_images`, `supports_video_edit`, `supports_video_extend` and
128
+ `supports_last_frame` per video model; an incapable model answers 400.
129
+
130
+ The supplier rejects photos of real people as references; characters
131
+ generated inside Corent are accepted, so make the person with
132
+ `generate_image` first and reference that.
133
+
103
134
  `generate_speech` takes `stability`, `similarity`, `style`, `speed` and
104
135
  `language`. Call `list_voices` first whenever the user wants a particular
105
136
  sounding narrator: `voice_id` cannot be guessed.
package/dist/prompts.js CHANGED
@@ -17,7 +17,12 @@ export const PRICE_NOTE = "Prices to quote before spending: image premium ~7c ea
17
17
  const BUDGET_RULE = "Add the numbers up, show the user the total against their budget, and do NOT call a paid tool until they say yes. " +
18
18
  "Call get_balance first: available_cents is what can actually be spent.";
19
19
  const POLLING = "Video is asynchronous: each generate_video returns a job id. Poll get_job every 10-15 seconds until status is " +
20
- "completed (a clip usually takes 1-4 minutes); the URL is in videos[0].url. If the user changes their mind, cancel_job releases the hold.";
20
+ "completed (a clip usually takes 1-4 minutes); the URL is in videos[0].url, and last_frame_url is on the finished job " +
21
+ "when return_last_frame was true. If the user changes their mind, cancel_job releases the hold.";
22
+ // Video references (Seedance 2.x): the prompt names them @Image 1, @Video 1,
23
+ // @Audio 1 in the order given. The one rule every recipe has to repeat:
24
+ export const REAL_FACE_RULE = "The supplier rejects photos of REAL people as references; characters generated inside Corent are accepted, " +
25
+ "so build the person in generate_image first and reference that.";
21
26
  const DELIVERY = "Corent does not stitch clips together. Deliver every still, clip and narration URL in order with its cost, " +
22
27
  "so the user can assemble them in any editor. list_media can find these again later by prompt keyword.";
23
28
  function user(text) {
@@ -38,14 +43,14 @@ export function registerPrompts(server) {
38
43
  "1. PLAN. Write a 4-shot vertical (9:16) shot list: hook, product in use, the benefit, call to action. One line of narration per shot; keep the whole script under 1,000 characters.",
39
44
  `2. COST. ${PRICE_NOTE} ${BUDGET_RULE}`,
40
45
  "3. STILLS. For each shot call generate_image with reference_image_urls=[product image], tier premium (never air or lite: references need an edit-capable model), aspect_ratio 9:16, style photorealistic. If the label or logo must be pixel-exact, generate the scene, then inpaint_image with a mask that is BLACK over the product and WHITE elsewhere: pixels outside the mask stay byte-for-byte.",
41
- "4. CLIPS. For each approved still call generate_video with image_url = that still, tier premium (pro if the user wants the best), duration_s 5. Omit aspect_ratio when animating a still. Pass audio false; the narration is a separate track.",
46
+ "4. CLIPS. For each approved still call generate_video with image_url = that still, tier premium (pro if the user wants the best), duration_s 5. Omit aspect_ratio when animating a still. Pass audio false; the narration is a separate track. When the product must stay exact while it moves (a label, a bottle turned in the hand), instead call generate_video with model corent-seedance-2.5, reference_image_urls=[product image] and a prompt that names it as @Image 1; never mix that with image_url. " + REAL_FACE_RULE,
42
47
  "5. NARRATION. Call list_voices, let the user pick a voice_id, then ONE generate_speech call with the full script.",
43
48
  `6. POLL. ${POLLING}`,
44
49
  `7. DELIVER. ${DELIVERY}`,
45
50
  ].join("\n")));
46
51
  server.registerPrompt("character-series", {
47
52
  title: "Character series",
48
- description: "One consistent character across many scenes: an anchor portrait, then every scene generated as an edit of that anchor.",
53
+ description: "One consistent character across many scenes and clips: an anchor portrait, then every scene generated as an edit of that anchor, and every clip referencing it.",
49
54
  argsSchema: {
50
55
  character: z.string().describe("Who the character is: age, look, clothing, mood"),
51
56
  scenes: z.string().describe("The scenes, comma separated, e.g. 'at a cafe, on a train, at the beach'"),
@@ -57,13 +62,13 @@ export function registerPrompts(server) {
57
62
  "1. ANCHOR. One generate_image, tier premium, aspect_ratio 3:4: the character alone, neutral pose, plain background, face clearly visible. Show it to the user and iterate (same seed, one change) until they approve. This is the only image the series is built from.",
58
63
  `2. COST. One image per scene at ~7c (premium). ${BUDGET_RULE}`,
59
64
  "3. SCENES. For each scene call generate_image with reference_image_urls=[anchor URL], tier premium or pro, the scene in the prompt, same style every time. Keep the anchor as the FIRST reference if you add a second (e.g. a product).",
60
- "4. OPTIONAL CLIPS. generate_video with image_url = the scene still, duration_s 5, omit aspect_ratio. Quote ~52c (premium) or ~84c (pro) per clip first.",
65
+ "4. OPTIONAL CLIPS. To keep the character in motion, call generate_video with model corent-seedance-2.5, reference_image_urls=[anchor URL] (add the scene still second if it helps), a prompt that names the character as @Image 1, duration_s 5, aspect_ratio 9:16 or 16:9, and return_last_frame true. Do not pass image_url with references. For the next clip in the same scene pass image_url = the previous job's last_frame_url (then omit references and aspect_ratio) so the action continues without a cut. Quote ~52c (premium) or ~84c (pro) per clip first. " + REAL_FACE_RULE,
61
66
  `5. POLL. ${POLLING}`,
62
67
  `6. DELIVER. ${DELIVERY} Tell the user the anchor URL: pass it as a reference any time they want this character again.`,
63
68
  ].join("\n")));
64
69
  server.registerPrompt("storyboard-to-clip", {
65
70
  title: "Storyboard to clips",
66
- description: "Turn a numbered storyboard into stills, then one clip per still with a camera move, plus optional narration.",
71
+ description: "Turn a numbered storyboard into stills, then one clip per still with a camera move, chained through last frames, plus optional narration.",
67
72
  argsSchema: {
68
73
  storyboard: z.string().describe("Numbered shots, one line each: what is in frame and what moves"),
69
74
  style: z.string().optional().describe("Look, e.g. cinematic, anime, documentary"),
@@ -76,10 +81,11 @@ export function registerPrompts(server) {
76
81
  "1. PLAN. Restate the shots as a table: number, frame, movement, seconds (5 each unless told otherwise), narration line if any.",
77
82
  `2. COST. One still (~7c) plus one clip (~52c premium, ~84c pro) per shot, plus ~30c if there is narration. ${BUDGET_RULE}`,
78
83
  "3. STILLS. generate_image per shot, aspect_ratio 16:9, style cinematic (or the requested style), tier premium. Use reference_image_urls when a character or object recurs across shots, with the first approved still as the reference.",
79
- "4. CLIPS. generate_video per approved still: image_url = the still, duration_s 5, a camera value that matches the movement (pan_left, zoom_in, orbit_right, static ...). For an A-to-B shot pass end_image_url = the next shot's still. Omit aspect_ratio when animating a still.",
80
- "5. NARRATION. If the storyboard has lines, list_voices then ONE generate_speech with the whole script.",
81
- `6. POLL. ${POLLING}`,
82
- `7. DELIVER. ${DELIVERY}`,
84
+ "4. CLIPS. generate_video per approved still: image_url = the still, duration_s 5, a camera value that matches the movement (pan_left, zoom_in, orbit_right, static ...), and return_last_frame true. For an A-to-B shot pass end_image_url = the next shot's still. Omit aspect_ratio when animating a still.",
85
+ "5. CHAIN. When two shots are one continuous action, start the second from where the first ended: image_url = the first job's last_frame_url (read it from get_job once completed), same prompt style, no still needed. To change something in a finished clip use edit_video; to make it longer use extend_video. " + REAL_FACE_RULE,
86
+ "6. NARRATION. If the storyboard has lines, list_voices then ONE generate_speech with the whole script.",
87
+ `7. POLL. ${POLLING}`,
88
+ `8. DELIVER. ${DELIVERY}`,
83
89
  ].join("\n")));
84
90
  server.registerPrompt("ad-variations", {
85
91
  title: "Ad variations",
package/dist/server.js CHANGED
@@ -18,7 +18,7 @@ import { z } from "zod";
18
18
  import { MEDIA_WIDGET_HTML } from "./widget-html.js";
19
19
  import { registerPrompts } from "./prompts.js";
20
20
  export const DEFAULT_API_URL = "https://api.corent.tech";
21
- export const SERVER_VERSION = "0.8.0";
21
+ export const SERVER_VERSION = "0.9.0";
22
22
  // Transport resilience, matching the two SDKs: a 429 or 5xx is retried with
23
23
  // backoff, but ONLY on calls that carry an Idempotency-Key (or are GETs), so a
24
24
  // retry can never buy a second generation.
@@ -237,6 +237,12 @@ export function createCorentServer(config = {}) {
237
237
  // deliverable in it (parity audit 2026-08-30).
238
238
  audio: z.array(z.object({ url: z.string() }).passthrough()).optional(),
239
239
  progress_percent: z.number().nullable().optional(),
240
+ // Video jobs (2026-09-12): the PNG of the final frame when return_last_frame
241
+ // was asked for (pass it as the next clip's image_url), the task the model
242
+ // ran (auto | reference | edit | extend) and the ACTUAL clip length.
243
+ last_frame_url: z.string().nullable().optional(),
244
+ task: z.string().nullable().optional(),
245
+ duration_s: z.number().nullable().optional(),
240
246
  meta: mediaMeta.optional(),
241
247
  error: z.string().optional(),
242
248
  };
@@ -255,7 +261,7 @@ export function createCorentServer(config = {}) {
255
261
  server.registerTool("generate_image", {
256
262
  title: "Generate image",
257
263
  _meta: toolMeta("Generating image", "Image ready", true),
258
- description: "Generate an image from a text prompt. Returns a permanent public URL. Synchronous, usually 2-20 seconds. Costs a few cents (premium ~7c); failed generations are never billed. Takes reference_image_urls to keep a face, character or product consistent (tier premium or up).",
264
+ description: "Generate an image from a text prompt. Returns a permanent public URL. Synchronous, usually 2-20 seconds. Costs a few cents (premium ~7c); failed generations are never billed. Takes reference_image_urls to keep a face, character or product consistent (tier pro or up).",
259
265
  inputSchema: {
260
266
  prompt: z.string().describe("What to generate, in plain language"),
261
267
  tier: z
@@ -321,8 +327,8 @@ export function createCorentServer(config = {}) {
321
327
  .describe("1 to 4 public https image URLs that keep a CHARACTER, FACE, or PRODUCT consistent across " +
322
328
  "generations. The prompt is applied as an EDIT of these references, so use it whenever the " +
323
329
  "user wants the same person/object again in a new scene, a product placed somewhere, or a " +
324
- "series that has to match. Served only by edit-capable models, which sit at premium and up: " +
325
- "pass tier premium/pro/max_pro (or omit tier), never air or lite. Omit entirely for plain " +
330
+ "series that has to match. Served only by edit-capable models, which sit at pro and up: " +
331
+ "pass tier pro/max_pro (or omit tier), never air/lite/premium. Omit entirely for plain " +
326
332
  "text-to-image."),
327
333
  },
328
334
  outputSchema: imageOutput,
@@ -332,10 +338,73 @@ export function createCorentServer(config = {}) {
332
338
  headers: { "Idempotency-Key": idempotencyKey() },
333
339
  body: JSON.stringify(args),
334
340
  })));
341
+ // --- Video references (Seedance 2.x, 2026-09-12). The prompt points at
342
+ // them as "@Image 1", "@Video 1", "@Audio 1" in the order given. The API
343
+ // enforces three rules; they are mirrored here so a bad combination is
344
+ // refused before it costs a round trip.
345
+ const VIDEO_ASPECTS = ["16:9", "9:16", "1:1", "adaptive"];
346
+ const VIDEO_TASKS = ["auto", "reference", "edit", "extend"];
347
+ const REAL_FACE_RULE = "The supplier rejects photos of REAL people as references; characters generated inside Corent are accepted.";
348
+ const videoReferenceFields = {
349
+ reference_image_urls: z
350
+ .array(z.string().url())
351
+ .min(1)
352
+ .max(30)
353
+ .optional()
354
+ .describe("1 to 30 public https images the clip should keep to (a character, a product, a setting). Refer to " +
355
+ "them in the prompt as @Image 1, @Image 2 in this order. Needs model " +
356
+ "(corent-seedance-2.5); cannot be combined with image_url/end_image_url. " + REAL_FACE_RULE),
357
+ reference_video_urls: z
358
+ .array(z.string().url())
359
+ .min(1)
360
+ .max(10)
361
+ .optional()
362
+ .describe("1 to 10 public https clips, @Video 1, @Video 2 in the prompt: the source for an edit or extend task, " +
363
+ "or motion/style to follow. Cannot be combined with image_url/end_image_url."),
364
+ reference_audio_urls: z
365
+ .array(z.string().url())
366
+ .min(1)
367
+ .max(10)
368
+ .optional()
369
+ .describe("1 to 10 public https audio files, @Audio 1 in the prompt, e.g. a track the clip should cut to."),
370
+ task: z
371
+ .enum(VIDEO_TASKS)
372
+ .optional()
373
+ .describe("What to do with the references (default auto). edit changes something in @Video 1 and keeps its " +
374
+ "length: omit duration_s. extend continues @Video 1. Both need reference_video_urls and " +
375
+ "aspect_ratio omitted or adaptive. The prompt must say the intent (add, remove, replace, change; " +
376
+ "extend, continue)."),
377
+ return_last_frame: z
378
+ .boolean()
379
+ .optional()
380
+ .describe("true also stores a PNG of the final frame and returns it as last_frame_url on the finished job, so the " +
381
+ "next clip can start exactly there (pass it as image_url). Use it to chain shots."),
382
+ output_format: z.enum(["mp4", "mov"]).optional().describe("Delivered file type (default mp4)."),
383
+ };
384
+ function checkVideoRules(args, where = "") {
385
+ const bad = (message) => {
386
+ throw new CorentApiError(422, { message: `${where}${message}` });
387
+ };
388
+ const frames = Boolean(args.image_url || args.end_image_url);
389
+ const refs = ["reference_image_urls", "reference_video_urls", "reference_audio_urls"].some((k) => Array.isArray(args[k]) && args[k].length > 0);
390
+ const task = args.task ?? "auto";
391
+ if (frames && refs) {
392
+ bad("image_url/end_image_url (first/last frame) cannot be combined with reference_*: pick one way to guide the clip.");
393
+ }
394
+ if ((task === "edit" || task === "extend") && !(Array.isArray(args.reference_video_urls) && args.reference_video_urls.length)) {
395
+ bad(`task ${task} needs at least one reference_video_urls entry: the clip to ${task}.`);
396
+ }
397
+ if (task === "edit" && args.duration_s !== undefined) {
398
+ bad("omit duration_s on an edit task: the edited clip keeps the source clip's length.");
399
+ }
400
+ if ((frames || task === "edit" || task === "extend") && args.aspect_ratio !== undefined && args.aspect_ratio !== "adaptive") {
401
+ bad('aspect_ratio must be omitted or "adaptive" on first/last-frame, edit and extend tasks: the clip keeps the source\'s shape.');
402
+ }
403
+ }
335
404
  server.registerTool("generate_video", {
336
405
  title: "Generate video",
337
406
  _meta: toolMeta("Starting video", "Video job started", true),
338
- description: "Start a video from a text prompt, or animate a source image. Asynchronous: returns a job id at once; poll get_job until status is completed. Costs tens of cents to a few dollars per clip (premium ~52c, pro ~84c). Failed generations are never billed.",
407
+ description: "Start a video from text, a source image, or references (@Image 1, @Video 1, @Audio 1 in the prompt). Asynchronous: returns a job id; poll get_job until completed. Premium ~52c, pro ~84c per clip; failed generations are never billed. References need corent-seedance-2.5; photos of real people are rejected as references.",
339
408
  inputSchema: {
340
409
  prompt: z.string().describe("What to generate, in plain language"),
341
410
  tier: z
@@ -347,15 +416,19 @@ export function createCorentServer(config = {}) {
347
416
  .optional()
348
417
  .describe("Optional content style hint, same vocabulary as generate_image"),
349
418
  aspect_ratio: z
350
- .enum(["16:9", "9:16", "1:1"])
419
+ .enum(VIDEO_ASPECTS)
351
420
  .optional()
352
- .describe("Shape of the clip. OMIT IT when animating a source image: those models render the still's own shape and cannot be given a different one, so naming a shape is refused rather than silently ignored. Omitted for text-to-video means the model's own default (landscape)."),
421
+ .describe("Shape of the clip. OMIT IT (or pass adaptive) when animating a source image, editing or extending a clip: those keep the source's own shape, so naming another is refused rather than silently ignored. Omitted for text-to-video means the model's own default (landscape)."),
353
422
  duration_s: z.number().int().min(1).max(30).optional().describe("Requested duration in seconds"),
354
423
  resolution: z
355
424
  .enum(["720p", "1080p", "4k"])
356
425
  .optional()
357
426
  .describe("Pixel resolution (default 720p). Tier-capped: pro allows up to 1080p, max_pro up to 4k; above the tier's cap it is clamped down, never rejected. Higher resolutions cost more (1080p ~2.5x, 4k ~5.5x). GET /v1/tiers lists each tier's menu with prices."),
358
- image_url: z.string().url().optional().describe("If set, animates this image instead of pure text-to-video"),
427
+ image_url: z
428
+ .string()
429
+ .url()
430
+ .optional()
431
+ .describe("If set, animates this image as the FIRST frame instead of pure text-to-video. Use a previous clip's last_frame_url to continue from it."),
359
432
  audio: z
360
433
  .boolean()
361
434
  .optional()
@@ -383,15 +456,105 @@ export function createCorentServer(config = {}) {
383
456
  model: z
384
457
  .string()
385
458
  .optional()
386
- .describe('Direct model access: pin an exact model by name, e.g. "corent-seedance-2.0" (list_models shows the menu; every name there is spelled corent-*). Bypasses routing -- never substituted; duration and resolution snap to THAT model\'s own menu rather than a tier cap. Mutually exclusive with tier.'),
459
+ .describe('Direct model access: pin an exact model by name, e.g. "corent-seedance-2.5" (list_models shows the menu; every name there is spelled corent-*). Bypasses routing -- never substituted; duration and resolution snap to THAT model\'s own menu rather than a tier cap. Mutually exclusive with tier. list_models reports supports_reference_images, supports_video_edit, supports_video_extend and supports_last_frame per video model.'),
460
+ ...videoReferenceFields,
387
461
  },
388
462
  outputSchema: jobOutput,
389
463
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
390
- }, wrap(async (args) => corent("/v1/videos/generate", {
391
- method: "POST",
392
- headers: { "Idempotency-Key": idempotencyKey() },
393
- body: JSON.stringify(args),
394
- })));
464
+ }, wrap(async (args) => {
465
+ checkVideoRules(args);
466
+ return corent("/v1/videos/generate", {
467
+ method: "POST",
468
+ headers: { "Idempotency-Key": idempotencyKey() },
469
+ body: JSON.stringify(args),
470
+ });
471
+ }));
472
+ // --- Two conveniences over generate_video for the clip-in, clip-out asks.
473
+ // Same endpoint, same Idempotency-Key, same widget; they only set task and
474
+ // put the source clip in reference_video_urls so the agent cannot get the
475
+ // plumbing wrong. ---
476
+ const editExtendShared = {
477
+ reference_image_urls: z
478
+ .array(z.string().url())
479
+ .min(1)
480
+ .max(30)
481
+ .optional()
482
+ .describe("Optional images the result should match (@Image 1, @Image 2 in the prompt), e.g. a character or product to bring in. " +
483
+ REAL_FACE_RULE),
484
+ reference_audio_urls: z
485
+ .array(z.string().url())
486
+ .min(1)
487
+ .max(10)
488
+ .optional()
489
+ .describe("Optional audio (@Audio 1 in the prompt) for the result to follow."),
490
+ model: z
491
+ .string()
492
+ .optional()
493
+ .describe('Model to run, default "corent-seedance-2.5", the model that can edit or extend a clip today.'),
494
+ resolution: z.enum(["720p", "1080p", "4k"]).optional().describe("Pixel resolution (default 720p); higher costs more."),
495
+ audio: z.boolean().optional().describe("Ask for native sound in the result."),
496
+ seed: z.number().int().min(0).optional().describe("Reproducibility, same contract as generate_video."),
497
+ negative_prompt: z.string().optional().describe("What must NOT appear in the result."),
498
+ return_last_frame: z
499
+ .boolean()
500
+ .optional()
501
+ .describe("true returns last_frame_url on the finished job, a PNG of the final frame to start the next clip from."),
502
+ output_format: z.enum(["mp4", "mov"]).optional().describe("Delivered file type (default mp4)."),
503
+ };
504
+ server.registerTool("edit_video", {
505
+ title: "Edit video",
506
+ _meta: toolMeta("Editing video", "Video edit started", true),
507
+ description: "Edit an existing clip with a prompt: add, remove, replace or change something in @Video 1. Keeps the source clip's length and shape. Asynchronous: returns a job id; poll get_job until completed. Runs on corent-seedance-2.5. Photos of real people are rejected as references; Corent-made characters are fine.",
508
+ inputSchema: {
509
+ prompt: z
510
+ .string()
511
+ .min(1)
512
+ .describe('The change, naming the source as @Video 1 and using an edit verb: "Replace the red car in @Video 1 with a blue bicycle".'),
513
+ video_url: z.string().url().describe("Public https URL of the clip to edit (becomes @Video 1)."),
514
+ ...editExtendShared,
515
+ },
516
+ outputSchema: jobOutput,
517
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
518
+ }, wrap(async ({ prompt, video_url, model, ...rest }) => {
519
+ const body = { prompt, task: "edit", reference_video_urls: [video_url], model: model ?? "corent-seedance-2.5", ...rest };
520
+ checkVideoRules(body);
521
+ return corent("/v1/videos/generate", {
522
+ method: "POST",
523
+ headers: { "Idempotency-Key": idempotencyKey() },
524
+ body: JSON.stringify(body),
525
+ });
526
+ }));
527
+ server.registerTool("extend_video", {
528
+ title: "Extend video",
529
+ _meta: toolMeta("Extending video", "Video extension started", true),
530
+ description: "Continue an existing clip (@Video 1) forward or backward, with a prompt for what happens next or before. Keeps the source shape; duration_s is the added length. Asynchronous: returns a job id; poll get_job until completed. Runs on corent-seedance-2.5. Pair with return_last_frame to chain shots.",
531
+ inputSchema: {
532
+ prompt: z.string().min(1).describe("What happens in the new footage. The direction line is added for you."),
533
+ video_url: z.string().url().describe("Public https URL of the clip to continue (becomes @Video 1)."),
534
+ direction: z
535
+ .enum(["forward", "backward"])
536
+ .optional()
537
+ .describe("forward (default) adds footage after the clip; backward adds what happened before it."),
538
+ duration_s: z.number().int().min(1).max(30).optional().describe("Seconds of NEW footage to add."),
539
+ ...editExtendShared,
540
+ },
541
+ outputSchema: jobOutput,
542
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
543
+ }, wrap(async ({ prompt, video_url, direction, model, ...rest }) => {
544
+ const body = {
545
+ prompt: `Extend @Video 1 ${direction ?? "forward"}. ${prompt}`,
546
+ task: "extend",
547
+ reference_video_urls: [video_url],
548
+ model: model ?? "corent-seedance-2.5",
549
+ ...rest,
550
+ };
551
+ checkVideoRules(body);
552
+ return corent("/v1/videos/generate", {
553
+ method: "POST",
554
+ headers: { "Idempotency-Key": idempotencyKey() },
555
+ body: JSON.stringify(body),
556
+ });
557
+ }));
395
558
  server.registerTool("generate_speech", {
396
559
  title: "Generate speech (voice)",
397
560
  _meta: toolMeta("Generating speech", "Speech ready"),
@@ -644,22 +807,31 @@ export function createCorentServer(config = {}) {
644
807
  .array(z.object({
645
808
  prompt: z.string(),
646
809
  tier: z.enum(TIERS).optional(),
810
+ model: z.string().optional().describe("Pin a model, e.g. corent-seedance-2.5 for reference/edit/extend items."),
647
811
  style: z.enum(IMAGE_STYLES).optional(),
648
- aspect_ratio: z.enum(["16:9", "9:16", "1:1"]).optional(),
812
+ aspect_ratio: z.enum(VIDEO_ASPECTS).optional(),
649
813
  duration_s: z.number().int().min(1).max(30).optional(),
650
814
  resolution: z.enum(["720p", "1080p", "4k"]).optional(),
815
+ image_url: z.string().url().optional(),
816
+ end_image_url: z.string().url().optional(),
817
+ audio: z.boolean().optional(),
818
+ seed: z.number().int().min(0).optional(),
819
+ ...videoReferenceFields,
651
820
  }))
652
821
  .min(1)
653
822
  .max(50)
654
- .describe("1 to 50 video requests"),
823
+ .describe("1 to 50 video requests; each item takes the same reference fields as generate_video"),
655
824
  },
656
825
  outputSchema: batchOutput,
657
826
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
658
- }, wrap(async ({ items }) => corent("/v1/videos/generate/batch", {
659
- method: "POST",
660
- headers: { "Idempotency-Key": idempotencyKey() },
661
- body: JSON.stringify({ items }),
662
- })));
827
+ }, wrap(async ({ items }) => {
828
+ items.forEach((item, i) => checkVideoRules(item, `items[${i}]: `));
829
+ return corent("/v1/videos/generate/batch", {
830
+ method: "POST",
831
+ headers: { "Idempotency-Key": idempotencyKey() },
832
+ body: JSON.stringify({ items }),
833
+ });
834
+ }));
663
835
  server.registerTool("get_batch", {
664
836
  title: "Check batch progress",
665
837
  _meta: toolMeta("Checking batch", "Batch checked"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "corent-mcp",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "MCP server for the Corent API — give any AI agent the ability to generate images, videos, voice, and text.",
5
5
  "mcpName": "io.github.gg13121/corent-mcp",
6
6
  "license": "MIT",