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
JavaScript
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