From 584012f6b89f881345a0bce198dd40fab50f72f7 Mon Sep 17 00:00:00 2001 From: TheDesignFounder Date: Fri, 9 Oct 2026 21:43:29 -0400 Subject: [PATCH 1/4] feat(video): add quote and execution tools with safe recovery --- README.md | 46 ++++++++++++++ RELEASE.md | 14 +++++ package.json | 4 +- server.json | 6 +- src/client.ts | 48 ++++++++++++++ src/tools.ts | 46 +++++++++++++- src/video.ts | 37 +++++++++++ test/packaged-install.test.mjs | 16 ++++- test/protocol.test.mjs | 2 + test/tools.test.mjs | 2 +- test/video.test.mjs | 112 +++++++++++++++++++++++++++++++++ 11 files changed, 323 insertions(+), 10 deletions(-) create mode 100644 src/video.ts create mode 100644 test/video.test.mjs diff --git a/README.md b/README.md index c57e6ea..7647179 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,52 @@ Image generation and editing tools for MCP-compatible AI clients, backed by the [DreamLayer Agent API](https://docs.dreamlayer.io/agent-api). +This working tree includes the **unpublished 0.5.0-beta.0 video candidate**. +The published version in the installation section below remains image/sprite-only. +For local review use Node >=22.12, `pnpm install --frozen-lockfile`, `pnpm build`, +and configure the MCP client to run `node /absolute/path/to/dist/index.js` with an +authorized video-enabled API endpoint. This does not publish or deploy anything. + +## Video candidate tools + +`dreamlayer_video_quote` performs no paid generation. `dreamlayer_video_execute` +admits only a ready owned quote under an explicit user-authorized credit ceiling. +Never invent that authorization. Save the idempotency key before execution. + +```json +{"tool":"dreamlayer_video_quote","arguments":{"operation":"text_to_video","prompt":"A 15-second silent forest video","options":{"duration_seconds":15,"resolution":"720p","generate_audio":false,"aspect_ratio":"16:9"}}} +``` + +For an image, first use `dreamlayer_upload_image` with an absolute local path, +then use the returned owned asset ID: + +```json +{"tool":"dreamlayer_video_quote","arguments":{"operation":"image_to_video","prompt":"Restrained product showcase on a clean white background","input_asset_id":"OWNED_ASSET_UUID","context":{"use_case":"product_showcase","requested_background":"clean_white"},"options":{"model":"auto","duration_seconds":15,"generate_audio":false}}} +{"tool":"dreamlayer_video_execute","arguments":{"quote_id":"READY_QUOTE_UUID","max_credits":40,"idempotency_key":"SAVED_UNIQUE_KEY"}} +{"tool":"dreamlayer_execution","arguments":{"execution_id":"EXECUTION_UUID"}} +{"tool":"dreamlayer_events","arguments":{"execution_id":"EXECUTION_UUID"}} +{"tool":"dreamlayer_download","arguments":{"execution_id":"EXECUTION_UUID","path":"/absolute/new/result.mp4"}} +``` + +The execute result includes `video_job`, not an inline/base64 video. Status and +events are read-only and never start work. Progress is actual execution stages, +not an invented percentage. A bounded wait or lost response does not cancel a +job; recover its identity with the same quote/key. Already consumed quotes remain +recoverable after expiry. Retry downloads without generating again. + +Default: 15s, native 720p, no audio unless requested; text aspect 16:9. One image +uses native/source-driven aspect. Explicit incompatible settings and extra images +are rejected before charging. White-background image requests and unknown product +backgrounds prefer Seedance; known non-white/lifestyle scenes prefer FLUX. Duration +over 20s selects Seedance; over 30s is unsupported. Explicit FLUX at over 20s returns +an error rather than silently selecting another model. Text/audio quality has not +been inferred from product-image samples. Source pixels are not analyzed. + +No automatic cross-provider retry, fidelity scoring, UI change, extension, +upscaling, keyframes or multiple-reference support is included. Downloads use +owned 30-day storage, bounded streaming, SHA-256 verification and no overwrite. +`pricing_pending`/disabled access must be reported honestly, not bypassed. + Use it for raster logo concepts, product imagery, marketing visuals, print artwork concepts and game assets. Inspect output quality and task-specific requirements. See the [installable agent workflows](https://github.com/TheDesignFounder/dreamlayer-agent-plugin) diff --git a/RELEASE.md b/RELEASE.md index 824945d..64acbae 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,5 +1,19 @@ # Agent readiness release +## Video candidate — not published + +`0.5.0-beta.0` adds video quotes, explicit ceiling-bound execution, status recovery +and owned downloads. `server.json` identifies this candidate under the existing +server namespace. Do not publish it until the compatible video backend is live, +paid route qualification and protected release approvals are complete, and the +exact tarball is approved. README installation pins below remain on the previously +published version intentionally. The candidate is available only as a local build. + +See the backend `VIDEO_V1_RELEASE.md` for qualification and rollback gates. Local +fixture tests do not establish production access, actual provider bills or quality. + +## Previous image/sprite release record + Prepared version: `0.4.0-beta.5`. Keep `latest` on the stable image release. beta.5 lets an agent order a sprite sheet that keeps its background. The `options` schema previously set diff --git a/package.json b/package.json index ec4d236..71248a0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@dreamlayer/mcp", - "version": "0.4.0-beta.5", - "description": "Image generation and editing tools for MCP-compatible AI clients, backed by the DreamLayer Agent API.", + "version": "0.5.0-beta.0", + "description": "Image generation, editing and durable video tools for MCP-compatible AI clients, backed by the DreamLayer Agent API.", "license": "MIT", "type": "module", "bin": { diff --git a/server.json b/server.json index 41d36de..fb86132 100644 --- a/server.json +++ b/server.json @@ -1,17 +1,17 @@ { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "name": "io.github.TheDesignFounder/dreamlayer", - "description": "Generate and edit images, remove backgrounds, upscale, and create sprite sheets through DreamLayer.", + "description": "Generate and edit images, create sprite sheets, and quote and recover video jobs through DreamLayer.", "repository": { "url": "https://github.com/TheDesignFounder/dreamlayer-mcp", "source": "github" }, - "version": "0.4.0-beta.5", + "version": "0.5.0-beta.0", "packages": [ { "registryType": "npm", "identifier": "@dreamlayer/mcp", - "version": "0.4.0-beta.5", + "version": "0.5.0-beta.0", "transport": { "type": "stdio" }, diff --git a/src/client.ts b/src/client.ts index 95bc5c7..87a1759 100644 --- a/src/client.ts +++ b/src/client.ts @@ -149,6 +149,7 @@ export type ManagedExecution = { conversation_id: string; status: string; image_job: Record | null; + video_job?: Record | null; }; export const PUBLIC_ERROR_REASONS = [ @@ -687,7 +688,54 @@ function requireEventStream(response: Response): void { } } +export function isVideoPrompt(prompt: string): boolean { + const p = prompt.toLowerCase(); + if (/\b(sprite|spritesheet|sprite sheet|gif)\b/.test(p)) return false; + if (/\b(txt2vid|img2vid)\b|\b(?:image|text)[- ]to[- ]video\b|\banimate (?:this|the|my) (?:image|photo|product)\b|\b(?:turn|convert)\b.+\binto\b.+\b(?:video|movie)\b/.test(p)) return true; + const target = /\b(video|movie|footage|image|photo|poster|logo|illustration|thumbnail|cover|icon|screenshot|artwork)\b/.exec(p); + return !!target && ["video","movie","footage"].includes(target[0]) && !/^video[- ]games?\b/.test(p.slice(target.index)); +} + export class ManagedClient { + // Existing image request/stream contracts remain unchanged. +async quoteVideo(input: Record): Promise> { + return this.request("/v1/video/quotes", { + method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(input), + }); + } + + async planVideoPrompt(prompt: string, inputAssetId?: string, aspectRatio?: string): Promise> { + return this.request("/v1/execute", { + method: "POST", headers: { "Content-Type": "application/json", "DreamLayer-Features": "video-quotes-v1", "Idempotency-Key": crypto.randomUUID() }, + body: JSON.stringify({ prompt, ...(inputAssetId ? { input_asset_id: inputAssetId } : {}), ...(aspectRatio ? {aspect_ratio: aspectRatio} : {}) }), + }); + } + + async executeVideo(quoteId: string, maxCredits: number, idempotencyKey: string): Promise { + if (!quoteId || !idempotencyKey || idempotencyKey.length > 200 || !Number.isFinite(maxCredits) || maxCredits <= 0 || maxCredits > 100) + throw new InputValidationError("A quote, explicit spending ceiling and stable idempotency key are required"); + return this.request("/v1/execute", { + method: "POST", headers: { "Content-Type": "application/json", "Idempotency-Key": idempotencyKey }, + body: JSON.stringify({ quote_id: quoteId, max_credits: maxCredits }), + }); + } + + async videoDownloadResponse(executionId: string): Promise { + const response = await fetch(`${this.baseUrl}/v1/video/executions/${encodeURIComponent(executionId)}/asset`, { + headers: { Authorization: `Bearer ${this.apiKey}`, "DreamLayer-Version": "1" }, + redirect: "manual", signal: AbortSignal.timeout(30_000), + }); + if (response.status !== 302) { + if (!response.ok) throw await apiError(response); + throw new ResponseContractError("Expected owned video redirect"); + } + const url = new URL(response.headers.get("Location") ?? ""); + if (url.protocol !== "https:" || url.hostname !== "storage.googleapis.com" || url.username || url.password || url.port || url.hash) + throw new ResponseContractError("Unapproved video download host"); + const download = await fetch(url, { redirect: "error", signal: AbortSignal.timeout(120_000) }); + if (!download.ok) throw new RecoveryRequiredError("Video download unavailable; retry delivery, not generation"); + return download; + } private readonly baseUrl: string; private capabilitiesPromise: Promise> | null = null; diff --git a/src/tools.ts b/src/tools.ts index e1d3da6..6b829bc 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -7,6 +7,7 @@ * runtime any more. */ import { randomUUID } from "node:crypto"; +import { downloadVideo } from "./video.js"; import { openAsBlob } from "node:fs"; import { stat, writeFile } from "node:fs/promises"; import path from "node:path"; @@ -15,6 +16,7 @@ import { KNOWN_OPERATIONS } from "./client.js"; import type { ManagedClient, ManagedEvent, ManagedOperation } from "./client.js"; import { ApiError, + isVideoPrompt, InputValidationError, RecoveryRequiredError, ResponseContractError, @@ -309,6 +311,8 @@ export async function toolDefinitionsFor(client: ManagedClient): Promise).operation as | { enum?: unknown } | undefined; - if (!property) return { ...tool, annotations, outputSchema }; + if (!property || tool.name === "dreamlayer_video_quote") return { ...tool, annotations, outputSchema }; return { name: tool.name, annotations, @@ -337,6 +341,35 @@ export async function toolDefinitionsFor(client: ManagedClient): Promise