UNPKG

comfyui-mcp

Version:

Local-first, agent-native control plane for ComfyUI — MCP server + autonomous sidebar agent that drives your live graph in natural language on ANY LLM: Claude/ChatGPT/Gemini on your subscription (no API key), free local models via Ollama (fully offline),

543 lines • 35.6 kB
import { z } from "zod"; import { generateImage } from "../services/generate-image.js"; import { enqueueWorkflow } from "../services/workflow-executor.js"; import { listLocalModels, } from "../services/model-resolver.js"; import { selectTxt2ImgCheckpoint } from "../services/checkpoint-capability.js"; import { REMBG_NODE } from "../services/remove-background.js"; import { errorToToolResult, ValidationError } from "../utils/errors.js"; import { generateAudioAction } from "./generate-audio.js"; import { generate3dAction } from "./generate-3d.js"; import { generateVideoAction } from "./generate-video.js"; import { upscaleImageAction } from "./upscale-image.js"; import { removeBackgroundAction } from "./remove-background.js"; import { generateWithControlNetAction, generateWithIpAdapterAction, } from "./generate-conditioned.js"; import { regenerateAction } from "./assets.js"; async function resolveCheckpoint() { let models; try { models = await listLocalModels("checkpoints"); } catch { // Listing failed (server unreachable, …) — the caller's generic // "no checkpoint found" error is the accurate report for that. return undefined; } if (models.length === 0) return undefined; // Skip checkpoints that cannot drive a txt2img graph (video models without // a text encoder, GGUF files) instead of taking the alphabetically-first // entry and failing at CLIPTextEncode every time (issue #892). const choice = await selectTxt2ImgCheckpoint(models); if (choice) return choice.name; const MAX_LISTED = 5; const listed = models .slice(0, MAX_LISTED) .map((m) => m.name) .join(", "); const more = models.length > MAX_LISTED ? `, … and ${models.length - MAX_LISTED} more` : ""; throw new ValidationError(`Found ${models.length} local checkpoint(s), but none appears txt2img-capable: ` + `every one lacks text-encoder weights in its safetensors header (a video or ` + `UNet-only model) or is a GGUF file CheckpointLoaderSimple cannot load ` + `(${listed}${more}). Download an image checkpoint with download_model ` + `(action:"search_civitai" can find one), then pass it as \`checkpoint\` or save ` + `it with get_defaults (action:"set"). If you believe one was misdetected, ` + `pass \`checkpoint\` explicitly to bypass this check.`); } /** * The nine HIGH-LEVEL GENERATION entry points collapsed into one * action-parameterized `generate_image` tool (0.50.0 surface consolidation, * slice 16). * * `generate_image` is the SURVIVOR and keeps its registration slot; the eight * sibling entry points it absorbed are listed, with their replacement call * forms, in DEAD_NAMES (src/tools/vocabulary.ts) — the one place a retired name * is allowed to live, so that a stale mention anywhere else stays detectable. * * NINE actions, one over the RFC's soft cap of eight, and deliberately so: every * one of them is "describe what you want, get a queued render back with a * prompt_id", and the two that most obviously could split off — the two * post-processing actions — are the ones an agent reaches for in the SAME * breath as a generation (render it, then clean it up), so a separate name * would cost a tool slot to re-separate what callers already chain. Splitting * them back out into their own two-action tool is a live option if a ninth * action proves to be one too many in practice; it costs one slot. * * SHAPE: a FLAT object with an `action` enum — deliberately NOT a * z.discriminatedUnion, which the MCP SDK renders as a schema with ZERO visible * parameters, hiding every input from the model. The price of flatness is a wide * parameter list, so every `.describe()` below names the action(s) it belongs to. * * REQUIREDNESS: only `action` can be schema-required. `prompt` was required by * five of the nine old tools and meaningless to three, `image` was required by * the two post-processing actions and optional for video/3d, and so on — so the * handler enforces per-action PRESENCE and NAMES the missing field, which is the * one deliberate behavioural difference a flat enum permits. The guards test * ABSENCE, not falsiness: `prompt: ""` passed z.string() before this * consolidation and reached the service (which builds a graph with an empty * positive prompt — a legitimate, if odd, render), and still does. * * ONE VALUE CONSTRAINT COULD NOT STAY IN ZOD. `strength` meant different things * with different ranges in the two tools that had it: video's i2v adherence was * `z.number().min(0).max(1)`, ControlNet's conditioning weight was * `z.number().positive()` (unbounded above). A flat shape has one `strength`, so * one of the two ranges would have been silently widened or narrowed — a real * behaviour change either way (ControlNet strength 1.5 is ordinary and would * have started failing; video strength 2.0 was rejected and would have started * reaching the service). The field is therefore `z.number()` at the schema layer * and RANGE-CHECKED PER ACTION in the handler, which reproduces each old * rejection exactly and names the field, the action and the range. Every OTHER * value constraint is untouched at the zod layer. */ const GENERATE_ACTIONS = [ "image", "audio", "video", "3d", "controlnet", "ip_adapter", "regenerate", "upscale", "remove_background", ]; export function registerGenerateImageTool(server) { server.tool("generate_image", "Generate media from a prompt or an existing image — the high-level entry points that build the graph for you. Every action enqueues on the connected ComfyUI and returns the prompt_id immediately; the resulting asset_id arrives in the completion notification. Driven by the `action` parameter:\n" + '- action:"image" — Text-to-image. Builds a txt2img workflow, filling any unspecified parameter from your configured defaults (get_defaults (action:"set") / COMFYUI_DEFAULT_* / config file), auto-selecting a local checkpoint when none is given — checkpoints known to lack a text encoder (e.g. video models) are skipped. `prompt` is required. For full control over the node graph, use create_workflow + enqueue_workflow instead.\n' + '- action:"audio" — Text-to-audio, supporting the ACE Step 1.5 and Stable Audio 3 model families. Builds the appropriate workflow graph, filling unspecified parameters from your defaults and auto-selecting local models. `model_family`, `prompt` and `duration` are required. Requires a running ComfyUI with the corresponding model files installed.\n' + '- action:"video" — Text-to-video, or image-to-video when `image` is given (animate a start frame). Composes an LTX-2.3 distilled workflow on your LOCAL GPU using the render-verified Comfy-Org node stack (gemma text encoder + abliterated/distilled LoRAs). Needs the LTX-2.3 models (~24-46GB): install with apply_manifest --path packs/ltx-2.3-txt2vid/manifest.yaml (or ltx-2.3-img2vid for i2v); returns an actionable error if the checkpoint is missing. `seconds` is converted to an 8n+1 frame count. For i2v, higher `strength` means MORE adherence to the start frame but LESS motion (1.0 can freeze the clip) — keep ~0.6. This minimal path omits the synchronized audio + stage-2 spatial upscale that the full ltx-2.3 packs ship. `prompt` is required. The video is written under output/video/ — find it with get_image (action:"list_outputs") (VHS/SaveVideo outputs may not appear in /history).\n' + '- action:"3d" — Generate a 3D model (glb/obj/fbx) from a text prompt or an input image, using the connected ComfyUI\'s hosted partner 3D nodes (Tripo, Meshy, Rodin, Hunyuan3D — auto-detected from the server; these are paid API nodes needing a comfy.org API key/login on the server or COMFY_API_KEY here). `mode` is required ("text" needs `prompt`, "image" needs `image`). Poll queue (action:"status") / get_history (action:"list") for the resulting model file (saved to ComfyUI\'s output directory). If the server has no 3D-capable API nodes, returns an actionable error naming local-pack alternatives.\n' + '- action:"controlnet" — Image conditioned by a ControlNet preprocessed image (pose skeleton, depth, canny, normal, etc.) plus a text prompt. Upload the control image first with upload_image (action:"image"), then pass its filename as `control_image`. `prompt` and `control_image` are required; `checkpoint` and `controlnet_model` auto-resolve from local models. control_image must ALREADY be a preprocessed map (this action does not run the preprocessor); requires a running ComfyUI with a matching controlnet model in models/controlnet/.\n' + '- action:"ip_adapter" — Image guided by a reference image\'s style/subject via IP-Adapter, plus a text prompt. Requires the ComfyUI_IPAdapter_plus custom nodes. Upload the reference first with upload_image (action:"image"), then pass its filename as `reference_image`. `prompt` and `reference_image` are required; `checkpoint` auto-resolves. Requires a running ComfyUI with ComfyUI_IPAdapter_plus and a matching IP-Adapter model installed, or the workflow will fail at execution time.\n' + '- action:"regenerate" — Re-enqueue the workflow that produced an EXISTING ASSET, optionally applying `overrides`. Overrides are applied to any node input matching the key name (e.g. cfg, steps, sampler_name, scheduler, seed, denoise, text). Seeds are re-randomized by default so each call yields a fresh image unless seed is explicitly passed in overrides. `asset_id` is required. To re-run from execution HISTORY rather than a registered asset, use enqueue_workflow (action:"rerun").\n' + '- action:"upscale" — Upscale an image with an ESRGAN super-resolution model. Builds an UpscaleModelLoader → ImageUpscaleWithModel workflow (scale=2 supersamples the 4x result back down for sharper output) and enqueues it on your LOCAL GPU. Upload the source first with upload_image (action:"image") (or stage a prior output with upload_image (action:"stage")), then pass its filename as `image`. Needs an upscale model in models/upscale_models/ (e.g. 4x-ClearRealityV1 / 4x_foolhardy_Remacri, provided by the anima/ernie packs or download_model); returns an actionable error if none is found. `image` is required.\n' + `- action:"remove_background" — Remove an image's background, returning a transparent (RGBA) cutout. Builds a LoadImage → ${REMBG_NODE} → SaveImage workflow using the ComfyUI-RMBG (BiRefNet) matting node and enqueues it on your LOCAL GPU. Upload the source first with upload_image (action:"image") (or stage a prior output with upload_image (action:"stage")), then pass its filename as \`image\`. Requires the ComfyUI-RMBG custom node (pack: wan-transparent, or install_custom_node 'comfyui-rmbg'); the BiRefNet model auto-downloads on first run. If the node isn't installed, returns an actionable error telling you how to install it. \`image\` is required.`, { action: z .enum(GENERATE_ACTIONS) .describe('What to generate. action:"image"/action:"video" require `prompt`; action:"audio" requires `model_family`+`prompt`+`duration`; action:"3d" requires `mode` (+ `prompt` or `image`); action:"controlnet" requires `prompt`+`control_image`; action:"ip_adapter" requires `prompt`+`reference_image`; action:"regenerate" requires `asset_id`; action:"upscale" and action:"remove_background" require `image`.'), // ── shared txt2img-family parameters ────────────────────────────────── prompt: z .string() .optional() .describe('Positive text prompt. REQUIRED for actions "image", "audio", "video", "controlnet" and "ip_adapter"; for action:"3d" it is required in mode "text" and optional (passed through only if the chosen node accepts it) in mode "image". Unused by action:"regenerate", action:"upscale" and action:"remove_background".'), negative_prompt: z .string() .optional() .describe('Negative prompt (default: empty / from defaults). Used by actions "image", "video", "controlnet", "ip_adapter" and — for the Stable Audio 3 family only — "audio".'), width: z .number() .int() .positive() .optional() .describe('Image width in pixels. Actions "image", "controlnet", "ip_adapter".'), height: z .number() .int() .positive() .optional() .describe('Image height in pixels. Actions "image", "controlnet", "ip_adapter".'), steps: z .number() .int() .positive() .optional() .describe('Sampling steps. Actions "image", "audio", "video", "controlnet", "ip_adapter" (video defaults to 8 for the distilled model).'), cfg: z .number() .positive() .optional() .describe('CFG scale. Actions "image", "audio", "video", "controlnet", "ip_adapter" (video defaults to 1.0 for the distilled model).'), sampler: z .string() .optional() .describe('Sampler name (e.g. euler, dpmpp_2m). Actions "image", "audio", "controlnet", "ip_adapter".'), scheduler: z .string() .optional() .describe('Scheduler (e.g. normal, karras). Actions "image", "audio", "controlnet", "ip_adapter".'), seed: z .number() .int() .optional() .describe('Seed (omit to randomize). Actions "image", "audio", "video", "controlnet", "ip_adapter".'), checkpoint: z .string() .optional() .describe('Checkpoint filename; auto-selected from local models if omitted. The relevant checkpoint differs per action: a diffusion checkpoint for "image"/"controlnet"/"ip_adapter", the LTX checkpoint for "video", the Stable Audio 3 checkpoint for "audio".'), batch_size: z .number() .int() .positive() .optional() .describe('action:"image" — number of images to generate.'), filename_prefix: z .string() .optional() .describe('Output filename prefix. action:"audio" (default audio/ace_step or audio/stable_audio_3), action:"video" (default \'video/ltx-2.3\') and action:"remove_background" (default \'ComfyUI_cutout\').'), // ── image inputs ────────────────────────────────────────────────────── image: z .string() .optional() .describe('Filename of an image in ComfyUI\'s input dir (upload it first with upload_image (action:"image"), or stage a prior output with upload_image (action:"stage")). REQUIRED for action:"upscale" and action:"remove_background"; the start frame for action:"video" image-to-video; the input image for action:"3d" in mode "image".'), model: z .string() .optional() .describe('Model file for the post-processing actions: action:"upscale" — an upscale model in models/upscale_models/ (auto-selected from local models if omitted); action:"remove_background" — the BiRefNet matting model (default \'BiRefNet_toonout\'; auto-downloaded by ComfyUI-RMBG).'), scale: z .union([z.literal(2), z.literal(4)]) .optional() .describe('action:"upscale" — net upscale factor: 2 or 4 (default 4).'), // ── conditioned generation ──────────────────────────────────────────── control_image: z .string() .optional() .describe('action:"controlnet" — filename of the (already-uploaded, already-preprocessed) control image in ComfyUI\'s input dir. REQUIRED for that action.'), controlnet_model: z .string() .optional() .describe('action:"controlnet" — ControlNet model file (in models/controlnet/); auto-selected if omitted.'), reference_image: z .string() .optional() .describe('action:"ip_adapter" — filename of the (already-uploaded) reference image in ComfyUI\'s input dir. REQUIRED for that action.'), weight: z .number() .optional() .describe('action:"ip_adapter" — IP-Adapter influence on the output, typically 0.0-1.0 (default 0.8); higher = closer to the reference.'), preset: z .string() .optional() .describe("action:\"ip_adapter\" — IPAdapterUnifiedLoader preset (default 'PLUS (high strength)')."), weight_type: z .enum(["standard", "prompt is more important", "style transfer"]) .optional() .describe("action:\"ip_adapter\" — IPAdapter weight mode (default 'standard' — required by current IPAdapter_plus builds)."), strength: z .number() .optional() .describe('Two DIFFERENT knobs sharing one field, each with its own range, checked when the action runs: action:"video" (i2v only) — adherence to the start frame, 0-1 inclusive (default 0.6; higher = LESS motion); action:"controlnet" — conditioning strength, must be > 0, typically 0.0-2.0 (default 1.0; higher = stronger adherence to the control image).'), // ── video ───────────────────────────────────────────────────────────── seconds: z .number() .positive() .optional() .describe('action:"video" — clip length in seconds (default 4; ~10s max).'), resolution: z .string() .optional() .describe("action:\"video\" — 'WIDTHxHEIGHT' e.g. '768x512' (rounded to multiples of 32; default 768x512)."), fps: z .number() .positive() .optional() .describe('action:"video" — frames per second (default 25).'), // ── 3d ──────────────────────────────────────────────────────────────── mode: z .enum(["text", "image"]) .optional() .describe('action:"3d" — "text" = text-to-3D from `prompt`; "image" = image-to-3D from an uploaded input `image`. REQUIRED for that action.'), node: z .string() .optional() .describe('action:"3d" — explicit 3D API node class_type to use (e.g. "MeshyTextToModelNode"); auto-selected if omitted. Use list_api_nodes with filter "3d" to see options.'), inputs: z .record(z.string(), z.any()) .optional() .describe('action:"3d" — provider-specific extra inputs passed through to the node (e.g. style, texture, quality). Use list_api_nodes (action:"schema") on the chosen node for valid keys.'), // ── audio ───────────────────────────────────────────────────────────── model_family: z .enum(["ace_step_1.5", "stable_audio_3"]) .optional() .describe('action:"audio" — audio model family; determines which workflow template and model loaders to use. REQUIRED for that action.'), duration: z .number() .positive() .optional() .describe('action:"audio" — audio duration in seconds. REQUIRED for that action.'), unet: z .string() .optional() .describe('action:"audio" — ACE UNet model filename (in models/diffusion_models/); auto-selected if omitted.'), vae: z .string() .optional() .describe('action:"audio" — ACE VAE model filename (in models/vae/); auto-selected if omitted.'), clip_a: z .string() .optional() .describe('action:"audio" — primary text encoder filename (in models/text_encoders/); auto-selected if omitted.'), clip_b: z .string() .optional() .describe('action:"audio" — secondary text encoder filename (in models/text_encoders/); auto-selected if omitted.'), clip: z .string() .optional() .describe('action:"audio" — Stable Audio CLIP encoder filename (in models/text_encoders/); auto-selected if omitted.'), lyrics: z .string() .optional() .describe('action:"audio" — lyrics or song structure description (ACE only — section-by-section breakdown).'), language: z .string() .optional() .describe("action:\"audio\" — language code for prompt (ACE only, default: 'en')."), musical_key: z .string() .optional() .describe("action:\"audio\" — target musical key (ACE only, e.g. 'C major', 'E minor'; default: 'C major')."), shift: z .number() .optional() .describe('action:"audio" — ModelSamplingAuraFlow shift parameter (ACE only, default: 3).'), guidance_scale: z .number() .optional() .describe('action:"audio" — TextEncodeAceStepAudio1.5 cfg_scale, the text encoder guidance scale (ACE only, default: 2).'), bpm: z .number() .int() .min(10) .max(300) .optional() .describe('action:"audio" — TextEncodeAceStepAudio1.5 tempo in beats per minute (ACE only, 10-300, default: 120).'), timesignature: z .enum(["2", "3", "4", "6"]) .optional() .describe("action:\"audio\" — TextEncodeAceStepAudio1.5 time signature (ACE only, one of '2'/'3'/'4'/'6', default: '4')."), temperature: z .number() .min(0) .max(2) .optional() .describe('action:"audio" — TextEncodeAceStepAudio1.5 LLM sampling temperature (ACE only, 0-2, default: 0.85).'), top_p: z .number() .min(0) .max(2000) .optional() .describe('action:"audio" — TextEncodeAceStepAudio1.5 LLM top-p nucleus sampling (ACE only, 0-2000, default: 0.9).'), top_k: z .number() .int() .min(0) .max(100) .optional() .describe('action:"audio" — TextEncodeAceStepAudio1.5 LLM top-k sampling (ACE only, 0-100, default: 0 = disabled).'), min_p: z .number() .min(0) .max(1) .optional() .describe('action:"audio" — TextEncodeAceStepAudio1.5 LLM min-p sampling (ACE only, 0-1, default: 0).'), generate_audio_codes: z .boolean() .optional() .describe('action:"audio" — generate audio codes via the TextEncodeAceStepAudio1.5 LLM (ACE only, default: true).'), audio_quality: z .enum(["V0", "128k", "320k"]) .optional() // #1458 — NOT "ACE only" any more. Both audio families end in SaveAudioMP3, and // stable_audio_3 now sets `quality` from this same parameter, so telling a // stable_audio_3 caller the knob does not apply to them is exactly the wrong // answer this issue is about. .describe("action:\"audio\" — SaveAudioMP3 bitrate/quality (ACE and stable_audio_3, one of 'V0'/'128k'/'320k', default: '320k')."), // ── re-render an existing asset ─────────────────────────────────────── asset_id: z .string() .optional() .describe('action:"regenerate" — asset id of the source generation. REQUIRED for that action.'), overrides: z .record(z.string(), z.any()) .optional() .describe('action:"regenerate" — map of input-name → new value applied to every node that already has that input. Common keys: cfg, steps, sampler_name, scheduler, seed, denoise, text.'), disable_random_seed: z .boolean() .optional() .describe('action:"regenerate" and action:"3d" — if true, do not randomize seed fields. For action:"regenerate", combine with `overrides.seed` to reproduce the exact original image.'), }, async (args) => { try { // Per-action PRESENCE, enforced here because a flat shape can only make // `action` schema-required. Each guard names the missing field, which is // the same information the old per-tool schemas gave a caller. // // ABSENCE only, never falsiness: `prompt: ""` and `asset_id: ""` both // passed z.string() before this consolidation and reached their services // (which answer with their own behaviour — an empty positive prompt, a // not-found asset). A `!x` guard would swallow those paths and substitute // generic text. const need = (value, action, field, what) => { if (value === undefined) { throw new ValidationError(`generate_image action:"${action}" requires \`${field}\` — ${what}.`); } return value; }; // The one constraint that could not stay in zod — see the header. Each // branch reproduces exactly what its old tool's schema rejected. const strengthFor = (action) => { const v = args.strength; if (v === undefined) return undefined; if (action === "video" && (v < 0 || v > 1)) { throw new ValidationError(`generate_image action:"video" requires \`strength\` between 0 and 1 (i2v adherence to the start frame); got ${v}.`); } if (action === "controlnet" && v <= 0) { throw new ValidationError(`generate_image action:"controlnet" requires \`strength\` greater than 0 (ControlNet conditioning strength, typically 0.0-2.0); got ${v}.`); } return v; }; switch (args.action) { case "image": { const result = await generateImage({ prompt: need(args.prompt, "image", "prompt", "what to draw"), negative_prompt: args.negative_prompt, width: args.width, height: args.height, steps: args.steps, cfg: args.cfg, sampler: args.sampler, scheduler: args.scheduler, seed: args.seed, checkpoint: args.checkpoint, batch_size: args.batch_size, }, { resolveCheckpoint, // The workflow composer already draws a random seed when none is // given, so executor-side re-randomization would only ever clobber // the seed this tool just resolved — including an explicit one // (issue #865). enqueue: (workflow) => enqueueWorkflow(workflow, { disable_random_seed: true }), }); return { content: [ { type: "text", text: JSON.stringify({ status: "enqueued", prompt_id: result.prompt_id, queue_remaining: result.queue_remaining, // #1037 — a 200 from /prompt does not mean every output was accepted; // ComfyUI queues the branches that validate and reports the rest. ...(result.rejectedOutputs ? { rejected_outputs: result.rejectedOutputs } : {}), checkpoint: result.checkpoint, note: 'asset_id will be available in the completion notification; use get_image (action:"view") or generate_image (action:"regenerate") with it.', }, null, 2), }, ], }; } case "audio": return await generateAudioAction({ model_family: need(args.model_family, "audio", "model_family", '"ace_step_1.5" or "stable_audio_3"'), prompt: need(args.prompt, "audio", "prompt", "what the audio should sound like"), duration: need(args.duration, "audio", "duration", "the clip length in seconds"), seed: args.seed, steps: args.steps, cfg: args.cfg, sampler: args.sampler, scheduler: args.scheduler, filename_prefix: args.filename_prefix, unet: args.unet, vae: args.vae, clip_a: args.clip_a, clip_b: args.clip_b, lyrics: args.lyrics, language: args.language, musical_key: args.musical_key, shift: args.shift, guidance_scale: args.guidance_scale, bpm: args.bpm, timesignature: args.timesignature, temperature: args.temperature, top_p: args.top_p, top_k: args.top_k, min_p: args.min_p, generate_audio_codes: args.generate_audio_codes, audio_quality: args.audio_quality, checkpoint: args.checkpoint, clip: args.clip, negative_prompt: args.negative_prompt, }); case "video": return await generateVideoAction({ prompt: need(args.prompt, "video", "prompt", "what happens in the clip"), image: args.image, negative_prompt: args.negative_prompt, seconds: args.seconds, resolution: args.resolution, fps: args.fps, seed: args.seed, steps: args.steps, cfg: args.cfg, strength: strengthFor("video"), checkpoint: args.checkpoint, filename_prefix: args.filename_prefix, }); case "3d": return await generate3dAction({ mode: need(args.mode, "3d", "mode", '"text" for text-to-3D or "image" for image-to-3D'), prompt: args.prompt, image: args.image, node: args.node, inputs: args.inputs, disable_random_seed: args.disable_random_seed, }); case "controlnet": return await generateWithControlNetAction({ prompt: need(args.prompt, "controlnet", "prompt", "what to draw"), control_image: need(args.control_image, "controlnet", "control_image", "the already-uploaded, already-preprocessed control map"), controlnet_model: args.controlnet_model, strength: strengthFor("controlnet"), negative_prompt: args.negative_prompt, width: args.width, height: args.height, steps: args.steps, cfg: args.cfg, sampler: args.sampler, scheduler: args.scheduler, seed: args.seed, checkpoint: args.checkpoint, }); case "ip_adapter": return await generateWithIpAdapterAction({ prompt: need(args.prompt, "ip_adapter", "prompt", "what to draw"), reference_image: need(args.reference_image, "ip_adapter", "reference_image", "the already-uploaded style/subject reference"), weight: args.weight, preset: args.preset, weight_type: args.weight_type, negative_prompt: args.negative_prompt, width: args.width, height: args.height, steps: args.steps, cfg: args.cfg, sampler: args.sampler, scheduler: args.scheduler, seed: args.seed, checkpoint: args.checkpoint, }); case "regenerate": return await regenerateAction({ asset_id: need(args.asset_id, "regenerate", "asset_id", "the source generation to re-run"), overrides: args.overrides, disable_random_seed: args.disable_random_seed, }); case "upscale": return await upscaleImageAction({ image: need(args.image, "upscale", "image", "the source image in ComfyUI's input dir"), scale: args.scale, model: args.model, }); case "remove_background": return await removeBackgroundAction({ image: need(args.image, "remove_background", "image", "the source image in ComfyUI's input dir"), model: args.model, filename_prefix: args.filename_prefix, }); default: { // Unreachable given the zod enum, but a clear runtime guard beats a // silent undefined if the schema and switch ever drift apart. const exhaustive = args.action; throw new Error(`Unknown generate_image action "${String(exhaustive)}". Expected one of: ${GENERATE_ACTIONS.join(", ")}.`); } } } catch (err) { return errorToToolResult(err); } }); } //# sourceMappingURL=generate-image.js.map