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

336 lines • 14.3 kB
import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { config, getComfyUIBaseUrl } from "../config.js"; import { comfyuiFetch } from "../comfyui/fetch.js"; import { ComfyUIError, ValidationError } from "../utils/errors.js"; import { logger } from "../utils/logger.js"; import { detectManagerApi, managerApiPrefixFor } from "./node-management.js"; /** * ComfyUI-Manager configuration, mirroring `comfy-cli manager` capabilities. * * Mechanism (hybrid): * - Prefer the ComfyUI-Manager HTTP API (works against remote ComfyUI too). * - Fall back to editing Manager's `config.ini` (under config.comfyuiPath) only * for settings the HTTP API cannot change (network_mode, security_level). * * Endpoints / config keys are taken verbatim from Comfy-Org/ComfyUI-Manager: * - glob/manager_server.py (HTTP routes: preview_method, db_mode, * policy/component, policy/update, channel_url_list, queue/reset) * - glob/manager_core.py (config.ini `[default]` section keys + valid values) */ export class ManagerConfigError extends ComfyUIError { constructor(message, details) { super(message, "MANAGER_CONFIG_ERROR", details); this.name = "ManagerConfigError"; } } /** Small Manager-specific fetch helper (no shared client per unit conventions). */ async function managerFetch(path, init, base = getComfyUIBaseUrl()) { const url = `${base}${path}`; logger.debug("ComfyUI-Manager API request", { url, method: init?.method ?? "GET" }); let res; try { res = await comfyuiFetch(url, init); } catch (err) { throw new ManagerConfigError(`Could not reach ComfyUI-Manager at ${url}. Is ComfyUI running with ComfyUI-Manager installed?`, { url, cause: err instanceof Error ? err.message : String(err) }); } if (!res.ok) { // unknown-ok: "" is interpolated into an ERROR MESSAGE and nothing else — the // HTTP status is reported either way, so an unreadable body costs detail in the // text, never a wrong conclusion. Verified there is no branch on this value. const body = await res.text().catch(() => ""); throw new ManagerConfigError(`ComfyUI-Manager API ${res.status}: ${res.statusText || "request failed"}`, { url, status: res.status, body }); } return res; } // --- Valid values, taken from manager_core.py read_config() defaults --- // config.ini `[default]` section keys (manager_core.py write_config()). const VALID_NETWORK_MODES = ["public", "private", "offline"]; const VALID_SECURITY_LEVELS = ["strong", "normal", "normal-", "weak"]; // HTTP-settable values (manager_server.py set_* handlers). const VALID_PREVIEW_METHODS = ["auto", "latent2rgb", "taesd", "none"]; const VALID_DB_MODES = ["local", "cache", "remote"]; // Component sharing policy dropdown (js/comfyui-manager.js) → switch in // js/components-manager.js handles 'higher'/'mine'; anything else == 'workflow'. const VALID_COMPONENT_POLICIES = ["workflow", "higher", "mine"]; // Update policy dropdown (js/comfyui-manager.js); only 'nightly-comfyui' is // compared server-side (manager_server.py update_comfyui). The bare // 'stable'/'nightly' values are not valid update_policy settings. const VALID_UPDATE_POLICIES = ["stable-comfyui", "nightly-comfyui"]; function assertOneOf(value, allowed, label) { if (!allowed.includes(value)) { throw new ValidationError(`Invalid ${label} "${value}". Expected one of: ${allowed.join(", ")}.`); } return value; } // --- HTTP-API backed settings --- /** POST {value} to a Manager endpoint and read the resulting GET state. */ async function setViaApi(postPath, value, getPath = postPath, options = {}) { // Capture one target for both the mutation and its read-back. A panel // retarget must never make this call set A then report B's state (#670). const base = getComfyUIBaseUrl(); const api = await detectManagerApi(base); if (api !== "legacy" && options.v4Supported === false) { throw new ManagerConfigError(`ComfyUI-Manager v4 does not expose ${postPath}; this setting is only available on the legacy Manager API.`); } const prefix = managerApiPrefixFor(api); await managerFetch(`${prefix}${postPath}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ value }), }, base); // Read back the resulting state (these GET endpoints return text/plain). const res = await managerFetch(`${prefix}${getPath}`, { method: "GET" }, base); return (await res.text()).trim(); } export async function setPreviewMethod(value) { const v = assertOneOf(value, VALID_PREVIEW_METHODS, "preview method"); const state = await setViaApi("/manager/preview_method", v, undefined, { v4Supported: false }); return { message: `Set ComfyUI-Manager preview method to "${v}".`, via: "api", state, }; } export async function setDbMode(value) { const v = assertOneOf(value, VALID_DB_MODES, "db mode"); const state = await setViaApi("/manager/db_mode", v); return { message: `Set ComfyUI-Manager database mode to "${v}".`, via: "api", state, }; } export async function setComponentPolicy(value) { const v = assertOneOf(value, VALID_COMPONENT_POLICIES, "component policy"); const state = await setViaApi("/manager/policy/component", v, undefined, { v4Supported: false }); return { message: `Set ComfyUI-Manager component policy to "${v}".`, via: "api", state, }; } export async function setUpdatePolicy(value) { const v = assertOneOf(value, VALID_UPDATE_POLICIES, "update policy"); const state = await setViaApi("/manager/policy/update", v); return { message: `Set ComfyUI-Manager update policy to "${v}".`, via: "api", state, }; } /** * Set the active channel by name. The Manager POST expects a channel *name* * (e.g. "default", "dev"); it resolves it to a URL internally and ignores * unknown names, so we verify against the returned selection. */ export async function setChannel(value) { if (!value || !value.trim()) { throw new ValidationError("Channel name must be a non-empty string."); } const name = value.trim(); const base = getComfyUIBaseUrl(); const api = await detectManagerApi(base); const path = `${managerApiPrefixFor(api)}/manager/channel_url_list`; await managerFetch(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ value: name }), }, base); const res = await managerFetch(path, { method: "GET" }, base); const data = (await res.json()); const selected = data.selected ?? "custom"; // Manager silently ignores unknown channel names, leaving selection unchanged. if (selected !== name) { throw new ManagerConfigError(`ComfyUI-Manager did not switch to channel "${name}" (still "${selected}"). The channel name may be unknown.`, { requested: name, selected }); } return { message: `Set ComfyUI-Manager channel to "${name}".`, via: "api", state: selected, }; } /** * Reset the Manager task queue. This is the runtime, HTTP-reachable analog of * `comfy-cli manager clear` (which clears reserved startup actions locally). * * NOTE on blast radius: the reset drops EVERY pending task in the Manager * queue (Manager has no per-task cancel), while a task already dequeued by * the worker keeps running — callers reporting a cancel must say both halves. * * `base` pins the target for this call (the ManagerFetchOptions.base * convention): the pin-write cancellation path (#689) passes the server its * pending-op marker was recorded against, so the reset lands on the ORIGINAL * ComfyUI even if the orchestrator has been retargeted since. Defaults to the * current target. */ export async function resetQueue(base = getComfyUIBaseUrl()) { const api = await detectManagerApi(base); await managerFetch(`${managerApiPrefixFor(api)}/manager/queue/reset`, { method: "POST" }, base); return { message: "Reset the ComfyUI-Manager task queue (cleared pending actions).", via: "api", }; } // --- config.ini fallback for settings with no HTTP setter --- /** * Candidate locations of ComfyUI-Manager's config.ini relative to the ComfyUI * install root, newest layout first (see manager_core.update_user_directory / * manager_migration.get_manager_path). All live under `<comfyui>/user/`. */ function managerConfigCandidates(comfyuiPath) { return [ join(comfyuiPath, "user", "__manager", "config.ini"), join(comfyuiPath, "user", "default", "ComfyUI-Manager", "config.ini"), // Legacy git-clone install location. join(comfyuiPath, "custom_nodes", "ComfyUI-Manager", "config.ini"), ]; } function resolveManagerConfigPath() { if (!config.comfyuiPath) { throw new ManagerConfigError("This setting has no ComfyUI-Manager HTTP endpoint and must be written to config.ini, " + "but no ComfyUI install path is known. Set COMFYUI_PATH (or run against a local install)."); } const candidates = managerConfigCandidates(config.comfyuiPath); for (const candidate of candidates) { if (existsSync(candidate)) return candidate; } throw new ManagerConfigError(`Could not find ComfyUI-Manager config.ini under "${config.comfyuiPath}". Looked in: ${candidates.join(", ")}. Is ComfyUI-Manager installed?`, { candidates }); } /** * Minimal in-place editor for the `[default]` section of Manager's config.ini. * Updates an existing `key = value` line or appends one to the section. Avoids * a full INI rewrite so unrelated keys/comments are preserved. */ function setIniDefaultKey(contents, key, value) { // Strip any CR/NUL to mirror Manager's own CRLF-injection sanitization. const safeValue = value.replace(/[\r\n\x00]/g, ""); const lines = contents.split("\n"); let inDefault = false; let defaultHeaderIdx = -1; let lastDefaultLineIdx = -1; const keyRe = new RegExp(`^\\s*${key}\\s*=`, "i"); for (let i = 0; i < lines.length; i++) { const trimmed = lines[i].trim(); const sectionMatch = /^\[(.+)\]\s*$/.exec(trimmed); if (sectionMatch) { const section = sectionMatch[1].trim().toLowerCase(); inDefault = section === "default"; if (inDefault) defaultHeaderIdx = i; continue; } if (inDefault) { if (trimmed !== "") lastDefaultLineIdx = i; if (keyRe.test(lines[i])) { lines[i] = `${key} = ${safeValue}`; return lines.join("\n"); } } } if (defaultHeaderIdx === -1) { // No [default] section: append a fresh one. const sep = contents.endsWith("\n") || contents === "" ? "" : "\n"; return `${contents}${sep}[default]\n${key} = ${safeValue}\n`; } // Insert after the last non-empty line within [default]. const insertAt = (lastDefaultLineIdx === -1 ? defaultHeaderIdx : lastDefaultLineIdx) + 1; lines.splice(insertAt, 0, `${key} = ${safeValue}`); return lines.join("\n"); } function setManagerConfigKeyOnDisk(key, value) { const path = resolveManagerConfigPath(); let contents; try { contents = readFileSync(path, "utf-8"); } catch (err) { throw new ManagerConfigError(`Could not read ${path}.`, { path, cause: err instanceof Error ? err.message : String(err), }); } const updated = setIniDefaultKey(contents, key, value); try { writeFileSync(path, updated, "utf-8"); } catch (err) { throw new ManagerConfigError(`Could not write ${path}.`, { path, cause: err instanceof Error ? err.message : String(err), }); } return path; } export function setNetworkMode(value) { const v = assertOneOf(value, VALID_NETWORK_MODES, "network mode"); const path = setManagerConfigKeyOnDisk("network_mode", v); return { message: `Set ComfyUI-Manager network_mode to "${v}" in ${path}. ` + "Restart ComfyUI for the change to take effect.", via: "config-file", state: v, }; } export function setSecurityLevel(value) { const v = assertOneOf(value, VALID_SECURITY_LEVELS, "security level"); const path = setManagerConfigKeyOnDisk("security_level", v); return { message: `Set ComfyUI-Manager security_level to "${v}" in ${path}. ` + "Restart ComfyUI for the change to take effect.", via: "config-file", state: v, }; } // --- Action dispatch --- export const MANAGER_CONFIG_ACTIONS = [ "set_preview_method", "set_db_mode", "set_component_policy", "set_update_policy", "set_channel", "reset_queue", "set_network_mode", "set_security_level", ]; const ACTIONS_REQUIRING_VALUE = new Set([ "set_preview_method", "set_db_mode", "set_component_policy", "set_update_policy", "set_channel", "set_network_mode", "set_security_level", ]); export async function configureManager(action, value) { if (ACTIONS_REQUIRING_VALUE.has(action) && (value === undefined || value === "")) { throw new ValidationError(`Action "${action}" requires a "value".`); } switch (action) { case "set_preview_method": return setPreviewMethod(value); case "set_db_mode": return setDbMode(value); case "set_component_policy": return setComponentPolicy(value); case "set_update_policy": return setUpdatePolicy(value); case "set_channel": return setChannel(value); case "reset_queue": return resetQueue(); case "set_network_mode": return setNetworkMode(value); case "set_security_level": return setSecurityLevel(value); default: { // Exhaustiveness guard. const _never = action; throw new ValidationError(`Unknown action "${_never}".`); } } } //# sourceMappingURL=manager-config.js.map