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

235 lines • 7.35 kB
const CATEGORY_MATCHERS = [ { category: "output", test: (ct) => ct === "SaveImage" || ct === "PreviewImage" }, { category: "loading", test: (ct) => ct.startsWith("CheckpointLoader") || ct.startsWith("CLIPLoader") || ct.startsWith("VAELoader") || ct.startsWith("LoraLoader") || ct.startsWith("ControlNetLoader"), }, { category: "conditioning", test: (ct) => ct === "CLIPTextEncode" || ct === "ConditioningCombine" || ct === "ConditioningSetArea" || ct === "ControlNetApply", }, { category: "sampling", test: (ct) => ct.startsWith("KSampler") || ct === "SamplerCustom", }, { category: "image", test: (ct) => ct === "VAEDecode" || ct === "VAEEncode" || ct === "LoadImage" || ct.startsWith("ImageScale") || ct.startsWith("ImageUpscale"), }, ]; function categorizeNode(classType) { for (const matcher of CATEGORY_MATCHERS) { if (matcher.test(classType)) return matcher.category; } return "utility"; } export const CATEGORY_LABELS = { loading: "Loading", conditioning: "Conditioning", sampling: "Sampling", image: "Image", output: "Output", utility: "Utility", }; // Mermaid shapes per category export function wrapNodeLabel(id, label, category) { switch (category) { case "sampling": return `${id}{{"${label}"}}`; case "conditioning": return `${id}(["${label}"])`; case "loading": return `${id}["${label}"]`; case "image": return `${id}("${label}")`; case "output": return `${id}((("${label}")))`; case "utility": default: return `${id}["${label}"]`; } } // Color mapping for connection data types export const TYPE_COLORS = { MODEL: "blue", LATENT: "red", CONDITIONING: "orange", IMAGE: "green", CLIP: "purple", VAE: "teal", }; export function isConnection(value) { return (Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "number"); } // Values worth showing in node labels export const DISPLAY_VALUES = new Set([ "seed", "steps", "cfg", "denoise", "sampler_name", "scheduler", "width", "height", "ckpt_name", "text", "upscale_model", "image", ]); export function buildNodeLabel(node, showValues) { const title = node._meta?.title ?? node.class_type; if (!showValues) return title; const parts = []; for (const [key, value] of Object.entries(node.inputs)) { if (isConnection(value)) continue; if (!DISPLAY_VALUES.has(key)) continue; let display; if (typeof value === "string") { // Truncate long strings (prompts, filenames) display = value.length > 30 ? value.slice(0, 27) + "..." : value; } else { display = String(value); } parts.push(`${key}:${display}`); } if (parts.length === 0) return title; return `${title}<br/>${parts.join(" ")}`; } export function extractConnections(workflow) { const edges = []; for (const [targetId, node] of Object.entries(workflow)) { for (const [inputName, value] of Object.entries(node.inputs)) { if (isConnection(value)) { edges.push({ sourceId: value[0], outputIndex: value[1], targetId, inputName, }); } } } return edges; } export function guessOutputType(classType, outputIndex) { // Common patterns from ComfyUI node definitions const outputMap = { CheckpointLoaderSimple: ["MODEL", "CLIP", "VAE"], "CheckpointLoader|simple": ["MODEL", "CLIP", "VAE"], LoraLoader: ["MODEL", "CLIP"], CLIPTextEncode: ["CONDITIONING"], KSampler: ["LATENT"], KSamplerAdvanced: ["LATENT"], SamplerCustom: ["LATENT"], VAEDecode: ["IMAGE"], VAEEncode: ["LATENT"], EmptyLatentImage: ["LATENT"], LoadImage: ["IMAGE", "MASK"], ImageUpscaleWithModel: ["IMAGE"], ImageScale: ["IMAGE"], SaveImage: [], PreviewImage: [], SetLatentNoiseMask: ["LATENT"], UpscaleModelLoader: ["UPSCALE_MODEL"], ConditioningCombine: ["CONDITIONING"], ControlNetLoader: ["CONTROL_NET"], ControlNetApply: ["CONDITIONING"], CLIPLoader: ["CLIP"], VAELoader: ["VAE"], }; const outputs = outputMap[classType]; if (outputs && outputIndex < outputs.length) { return outputs[outputIndex]; } return ""; } export function escapeForMermaid(str) { return str.replace(/"/g, "#quot;"); } export function convertToMermaid(workflow, options = {}) { const { showValues = true, direction = "LR" } = options; const lines = [`flowchart ${direction}`]; // Group nodes by category const groups = new Map(); for (const [id, node] of Object.entries(workflow)) { const cat = categorizeNode(node.class_type); if (!groups.has(cat)) groups.set(cat, []); groups.get(cat).push({ id, node }); } // Emit subgraphs in a stable order const categoryOrder = [ "loading", "conditioning", "sampling", "image", "output", "utility", ]; for (const cat of categoryOrder) { const nodes = groups.get(cat); if (!nodes || nodes.length === 0) continue; lines.push(` subgraph ${CATEGORY_LABELS[cat]}`); for (const { id, node } of nodes) { const label = escapeForMermaid(buildNodeLabel(node, showValues)); lines.push(` ${wrapNodeLabel(id, label, cat)}`); } lines.push(" end"); } // Emit edges const edges = extractConnections(workflow); for (const edge of edges) { const sourceNode = workflow[edge.sourceId]; const dataType = sourceNode ? guessOutputType(sourceNode.class_type, edge.outputIndex) : ""; const color = dataType ? TYPE_COLORS[dataType] : undefined; const labelPart = dataType ? `|${dataType}|` : ""; if (color) { lines.push(` ${edge.sourceId} -->${labelPart} ${edge.targetId}`); } else { lines.push(` ${edge.sourceId} --> ${edge.targetId}`); } } // Add style classes for colored edges const usedTypes = new Set(edges .map((e) => { const src = workflow[e.sourceId]; return src ? guessOutputType(src.class_type, e.outputIndex) : ""; }) .filter(Boolean)); if (usedTypes.size > 0) { lines.push(""); for (const t of usedTypes) { const color = TYPE_COLORS[t]; if (color) { lines.push(` linkStyle default stroke:${color}`); break; // mermaid linkStyle default applies to all; skip individual for simplicity } } } return lines.join("\n"); } //# sourceMappingURL=mermaid-converter.js.map