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),

120 lines • 4.54 kB
import { PANEL_IMAGE_RELAY_MAX_BYTES } from "../services/panel-image-relay.js"; /** One byte ceiling for every raw ComfyUI /view response, including Cloud. */ export const MAX_VIEW_RESPONSE_BYTES = PANEL_IMAGE_RELAY_MAX_BYTES; /** * Encoded-source ceiling when the caller will downscale instead of inlining * the original /view body. Still a hard cap — never an unbounded read. * Matches get_image action:"convert"'s default source limit. */ export const MAX_PREVIEW_SOURCE_BYTES = 64 * 1024 * 1024; /** Clamp a caller-supplied /view body ceiling onto the preview-source hard cap. */ export function clampViewResponseBytes(maxBytes) { if (maxBytes === undefined || !Number.isFinite(maxBytes) || maxBytes < 1) { return MAX_VIEW_RESPONSE_BYTES; } return Math.min(Math.floor(maxBytes), MAX_PREVIEW_SOURCE_BYTES); } /** * Ceiling for a ComfyUI history document before it reaches JSON.parse(). * History repeats prompt graphs and output metadata for completed jobs, so it * can grow much faster than the prompt-scoped responses used by most callers. */ export const MAX_HISTORY_RESPONSE_BYTES = 16 * 1024 * 1024; /** A bounded body read failed for a known, actionable reason. */ export class BoundedResponseError extends Error { kind; constructor(kind, message) { super(message); this.kind = kind; this.name = "BoundedResponseError"; } } function readChunkWithAbort(reader, signal) { if (signal.aborted) { return Promise.reject(signal.reason ?? new Error("The response read timed out")); } return new Promise((resolve, reject) => { let settled = false; const cleanup = () => signal.removeEventListener("abort", onAbort); const onAbort = () => { if (settled) return; settled = true; cleanup(); void reader.cancel(signal.reason).catch(() => undefined); reject(signal.reason ?? new Error("The response read timed out")); }; signal.addEventListener("abort", onAbort, { once: true }); reader.read().then((result) => { if (settled) return; settled = true; cleanup(); resolve(result); }, (error) => { if (settled) return; settled = true; cleanup(); reject(error); }); }); } /** * Read a raw response without ever accumulating more than the caller's * ceiling. A missing body is an empty response; it is never handed to an * unbounded arrayBuffer() fallback. */ export async function readResponseBodyBounded(res, timeoutMs, maxBytes = MAX_VIEW_RESPONSE_BYTES, externalSignal) { const declared = Number(res.headers.get("content-length") ?? ""); if (Number.isFinite(declared) && declared > maxBytes) { try { await res.body?.cancel(); } catch { // The size refusal is the useful error even if the producer is already gone. } throw new BoundedResponseError("too-large", `The response exceeds the ${maxBytes} byte safety limit.`); } if (!res.body) return Buffer.alloc(0); const reader = res.body.getReader(); const signal = externalSignal ?? AbortSignal.timeout(timeoutMs); const chunks = []; let total = 0; try { for (;;) { let result; try { result = await readChunkWithAbort(reader, signal); } catch (error) { if (signal.aborted) { throw new BoundedResponseError("timeout", "The response read timed out."); } throw error; } const { done, value } = result; if (done) break; if (!value) continue; total += value.byteLength; if (total > maxBytes) { try { await reader.cancel("ComfyUI /view response exceeded the safety limit"); } catch { // The size refusal is the useful error even if the producer is already gone. } throw new BoundedResponseError("too-large", `The response exceeds the ${maxBytes} byte safety limit.`); } chunks.push(value); } } finally { reader.releaseLock(); } return Buffer.concat(chunks, total); } //# sourceMappingURL=bounded-response.js.map