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

229 lines • 11.4 kB
// #2411 — SHARP'S NATIVE LIBRARY CAN BE UNLOADABLE, AND NOTHING SAID SO. // // `sharp` was imported at module top level by three services: // // src/services/color-analysis.ts // src/services/image-convert.ts // src/services/inline-preview.ts // // All three are reached, statically, from `src/tools/index.ts` — which registers // EVERY tool. So a `sharp` that throws while it evaluates does not disable image // features; it takes the whole orchestrator down before any of our code runs. // Measured on this machine by moving `libvips-cpp-8.18.3.dll` aside and booting // `dist/index.js`: // // Error: Could not load the "sharp" module using the win32-x64 runtime // ERR_DLOPEN_FAILED: The specified module could not be found. // at …/dist/index.js:17:5 // // No tool registers, the server never starts, and the message names neither // comfyui-mcp nor the feature the user lost. This is the same defect class as // #1318 (a half-extracted dependency reported as a resolver path), and it gets // the same treatment: load it where the failure can be CAUGHT, and translate the // observable state into a sentence naming the library and the remedy. // // WHAT THIS DOES NOT CLAIM. #2411 was reported alongside a Windows Smart App // Control toast blocking `libvips-cpp-8.18.6.dll`, and SAC is a plausible cause // on that machine — but `ERR_DLOPEN_FAILED` is equally what an antivirus // quarantine, a partial extraction, or a genuinely absent optional dependency // produces. Following #1318's rule, the message states what was OBSERVED (the // native library would not load, plus the loader's own words) and lists the // causes worth checking. It does not assert which one happened. // // The reporter's own correlation — that this produced their undeliverable // completions ("no prompt id … origin is UNDETERMINED") — is NOT supported by // the measurement above: a blocked sharp yields no server at all, so a session // whose renders succeeded and whose panel displayed outputs was not running a // blocked sharp. Naming the failure is still the right fix; the symptom it will // actually prevent is a dead orchestrator, not a missing contact sheet. /** * Opt out of every sharp-backed feature without uninstalling anything. * * Set when the native library cannot be made loadable and the noise is worse * than the missing feature: the services below then refuse with the same worded * message instead of attempting a load that is known to fail. */ export const SHARP_DISABLE_ENV = "COMFYUI_MCP_NO_SHARP"; /** Marks "the user turned it off", so the message says that and not a fake load error. */ const DISABLED_SENTINEL = Symbol("sharp disabled by env"); /** Is the escape hatch set to something meaning "yes"? */ function disabledByEnv() { const raw = process.env[SHARP_DISABLE_ENV]; if (raw === undefined) return false; const v = raw.trim().toLowerCase(); // An empty value is how a shell spells "unset it", and "0"/"false" are how a // config file spells "no". Treating either as ON would disable image features // for anyone who wrote `COMFYUI_MCP_NO_SHARP=0` to mean the opposite. return v !== "" && v !== "0" && v !== "false" && v !== "no" && v !== "off"; } /** * Did this failure come from the NATIVE library, rather than from sharp's * JavaScript being absent? * * The two want different remedies — a dlopen failure is a blocked or damaged * binary, while a resolution failure is a dependency that was never installed * (`--omit=optional` does exactly this, which is why #1447 parked it). Detection * is deliberately loose: it only picks WHICH paragraph of advice to print, and * both paragraphs name sharp, so a misclassification costs a line of irrelevant * advice rather than a wrong diagnosis. */ function isNativeLoadFailure(err) { const code = err?.code; if (code === "ERR_DLOPEN_FAILED") return true; const msg = err instanceof Error ? err.message : String(err ?? ""); return /ERR_DLOPEN_FAILED|Could not load the "sharp" module|\.node\b/.test(msg); } /** The first line of whatever the loader said, for quoting back verbatim. */ function firstLine(err) { const msg = err instanceof Error ? err.message : String(err ?? ""); const line = msg .split("\n") .map((l) => l.trim()) .find((l) => l.length > 0); return line ?? "no error message"; } /** * The dlopen detail line, when sharp's help text carries one. * * sharp concatenates its own headline, then each underlying error as * `${code}: ${message}`, then a "Possible solutions:" block that is about * INSTALLING sharp — advice which is actively misleading when the package is * installed correctly and the binary is being refused. Lifting just the cause * line keeps what diagnoses and drops what misdirects. */ function dlopenDetail(err) { const msg = err instanceof Error ? err.message : String(err ?? ""); for (const raw of msg.split("\n")) { const line = raw.trim(); if (line.startsWith("ERR_DLOPEN_FAILED:")) return line; } return null; } /** * What to tell a user whose sharp will not load, for the feature they asked for. * * `feature` names the thing that is unavailable in the caller's own vocabulary * ("Colour analysis", "Image conversion"), because a message that says only * "sharp is unavailable" leaves the reader to work out which of their requests * died — which is the gap #2411 is about. */ export function sharpUnavailableMessage(feature, err) { if (err === DISABLED_SENTINEL) { return (`${feature} is unavailable: image processing is switched off by ` + `${SHARP_DISABLE_ENV}. Unset it to re-enable sharp-backed features.`); } const native = isNativeLoadFailure(err); const detail = dlopenDetail(err) ?? firstLine(err); const cause = native ? "its native library (libvips, loaded through `sharp`) could not be loaded" : "the `sharp` module could not be loaded"; const advice = native ? process.platform === "win32" ? "On Windows the binary is as often REFUSED as it is missing, so check both. In order: " + 'Windows Security > App & browser control for a "Part of this app has been blocked" entry ' + "naming libvips-cpp-<version>.dll (Smart App Control refuses binaries it cannot attribute to " + "a known publisher); then your antivirus quarantine; then reinstall with " + "`npm install -g comfyui-mcp@latest` in case the extraction was interrupted. " + "Do NOT disable Smart App Control to work around this — on Windows that is a one-way switch " + "you cannot turn back on without reinstalling the OS." : "The package is present and its binary is being refused or is unusable, so check in this " + "order: a quarantine flag or security policy on the .node/dylib (macOS Gatekeeper, SELinux, " + "an endpoint agent); an architecture mismatch between Node and the prebuilt (arm64 vs x64); " + "then reinstall with `npm install -g comfyui-mcp@latest` in case the extraction was " + "interrupted." : "sharp's optional platform package is probably absent — `--omit=optional` removes it. " + "Reinstall with `npm install -g comfyui-mcp@latest`."; return (`${feature} is unavailable: ${cause}.\n` + ` ${detail}\n` + `${advice}\n` + "Only the features that DECODE image pixels are affected — inline previews, conversion and " + "colour analysis. Generation, queueing and every file already written to disk are untouched. " + `Set ${SHARP_DISABLE_ENV}=1 to skip the affected features quietly instead.`); } /** * The same diagnosis as {@link sharpUnavailableMessage}, sized to be dropped * into the MIDDLE of someone else's sentence. * * `boundInlineImage` hands its reason to a caller that wraps it as * `NOT rendered inline: ${reason}. The full-resolution file is on disk…`, so the * standalone form — several lines, its own remedy paragraph, a closing full stop * — would read as two collided messages and push the caller's own remedy off the * end. This starts lower-case and carries no terminal punctuation. */ export function sharpUnavailableReason(err) { if (err === DISABLED_SENTINEL) { return `image processing is switched off by ${SHARP_DISABLE_ENV}, so no preview could be built`; } const detail = dlopenDetail(err) ?? firstLine(err); const what = isNativeLoadFailure(err) ? "sharp's native library (libvips) could not be loaded" : "the sharp module could not be loaded"; return (`${what} (${detail}), so the image could not be resized to fit — ` + "this is an install/permissions problem with that binary, not a problem with the render"); } /** * Cached outcome of the one load attempt. * * Caching the FAILURE matches Node's own ESM semantics — a module that throws * while evaluating stays errored in the module map and re-throws the identical * error on every later `import()` — so this adds no constraint that was not * already there. It only avoids re-deriving the wrapped message per call. * * The env check is NOT cached with it: reading `process.env` each time keeps the * switch honest for tests that set and clear it around a case. */ let cached = null; /** * The sharp factory, or the reason it is unavailable. * * For paths that DEGRADE — an inline preview that can be skipped while the * underlying file is still delivered. Callers that cannot proceed at all should * use {@link requireSharp}, which throws the same worded message. */ export async function tryLoadSharp() { if (disabledByEnv()) return { ok: false, err: DISABLED_SENTINEL }; if (cached) return cached; try { const mod = await import("sharp"); cached = { ok: true, sharp: mod.default }; } catch (err) { cached = { ok: false, err }; } return cached; } /** * The sharp factory, or a thrown Error naming the library and the remedy. * * The throw is what a caller that cannot degrade wants: `get_image * action:"convert"` has nothing to hand back if it cannot re-encode, and an * empty success would be worse than a refusal that says why. */ export async function requireSharp(feature) { const result = await tryLoadSharp(); if (result.ok) return result.sharp; throw new Error(sharpUnavailableMessage(feature, result.err)); } /** * One line for the startup log when sharp cannot load, or `null` when it can. * * A probe exists because this change makes the failure SURVIVABLE, and a * survivable failure is one nobody notices until the first operation that needs * it. (Not because it explains #2411's lost hour — see the header: a blocked * sharp yields no server at all, so it cannot have.) It deliberately returns the * line instead of logging it, so the caller owns the level and the tests do not * have to capture a logger. */ export async function probeSharp() { const result = await tryLoadSharp(); if (result.ok) return null; return sharpUnavailableMessage("Image resizing, conversion and colour analysis", result.err); } //# sourceMappingURL=sharp-loader.js.map