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

305 lines • 12 kB
/** * #2493 — `panel_expose_subgraph_input` cannot expose Anything Everywhere?'s * wildcard source after Convert to Subgraph. * * The panel's `resolveSlot` matches input `.name` only. After conversion the * live LiteGraph array still carries the frontend wildcard (type `*`, display * label / localized_name "anything") while the name lookup lists widget sockets * such as `group_regex`. `panel_graph_outline` shows `[renamed "anything"]`; * `to_input:"anything"` is refused with `available: group_regex`. * * This module maps the requested name onto the LIVE slot array so the handler * can retry with an address `resolveSlot` already accepts. A STRING widget * socket is never chosen as a stand-in for the virtual bus. Index retry is * refused when the live names and the refusal's available names disagree — * those are different arrays, and guessing an index would expose the wrong slot. */ export const AE_WILDCARD_NODE_TYPES = new Set([ "Anything Everywhere", "Anything Everywhere?", "Anything Everywhere3", ]); const AE_WILDCARD_NAME_RE = /^anything[23]?$/i; const MISSING_INPUT_SLOT_RE = /No input slot named "([^"]+)" \(available: ([^)]*)\)/; function asRecord(value) { if (value === null || typeof value !== "object" || Array.isArray(value)) return null; return value; } function lower(value) { if (typeof value !== "string" || value === "") return null; return value.toLowerCase(); } function slotType(value) { return typeof value === "string" && value !== "" ? value : null; } export function isAeWildcardName(name) { return AE_WILDCARD_NAME_RE.test(name); } export function isAeWildcardNodeType(type) { return typeof type === "string" && AE_WILDCARD_NODE_TYPES.has(type); } export function isWildcardSlotType(type) { return type === "*"; } export function missingInputSlotRefusal(text) { const match = MISSING_INPUT_SLOT_RE.exec(text); if (!match) return null; const requested = match[1]; const raw = match[2]?.trim() ?? ""; if (!requested) return null; const available = raw === "" || raw.toLowerCase() === "none" ? [] : raw .split(",") .map((part) => part.trim()) .filter((part) => part !== ""); return { requested, available }; } function queryNodeRecords(payload) { if (Array.isArray(payload)) { return payload.flatMap((entry) => { const rec = asRecord(entry); return rec ? [rec] : []; }); } const rec = asRecord(payload); if (!rec) return []; if (Array.isArray(rec.nodes)) { return rec.nodes.flatMap((entry) => { const node = asRecord(entry); return node ? [node] : []; }); } if (typeof rec.text === "string") { const rows = []; for (const line of rec.text.split(/\r?\n/)) { const trimmed = line.trim(); if (!trimmed.startsWith("{")) continue; try { const row = asRecord(JSON.parse(trimmed)); if (row) rows.push(row); } catch { /* skip a non-JSON detail line */ } } if (rows.length) return rows; } if (rec.id != null && (rec.inputs !== undefined || rec.type !== undefined)) return [rec]; return []; } function liveInputFromRecord(entry, index) { const rec = asRecord(entry); if (!rec) return null; const rawSlot = rec.slot; const slot = typeof rawSlot === "number" && Number.isInteger(rawSlot) && rawSlot >= 0 ? rawSlot : index; return { slot, name: typeof rec.name === "string" && rec.name ? rec.name : null, label: typeof rec.label === "string" && rec.label ? rec.label : null, localized_name: typeof rec.localized_name === "string" && rec.localized_name ? rec.localized_name : null, type: slotType(rec.type), }; } export function liveExposeNodeFromQuery(payload, nodeId) { if (nodeId == null) return null; const wanted = String(nodeId); for (const rec of queryNodeRecords(payload)) { if (rec.id == null || String(rec.id) !== wanted) continue; const rawInputs = rec.inputs; const inputs = []; if (Array.isArray(rawInputs)) { for (let i = 0; i < rawInputs.length; i++) { const slot = liveInputFromRecord(rawInputs[i], i); if (slot) inputs.push(slot); } } return { id: wanted, type: typeof rec.type === "string" && rec.type ? rec.type : null, inputs, }; } return null; } function nameSet(names) { const out = new Set(); for (const name of names) { const key = lower(name); if (key) out.add(key); } return out; } /** True when the refusal's available names are the live slot names — same array. */ export function liveNamesMatchAvailable(available, inputs) { const avail = nameSet(available); if (avail.size === 0) return false; const live = nameSet(inputs.map((slot) => slot.name ?? "")); if (avail.size !== live.size) return false; for (const name of avail) if (!live.has(name)) return false; return true; } function uniqueSlot(hits) { return hits.length === 1 ? (hits[0] ?? null) : null; } function displayKeys(slot) { const keys = [slot.label, slot.localized_name]; return keys.flatMap((key) => { const lowered = lower(key); return lowered ? [lowered] : []; }); } export function resolveExposeInputAgainstLiveSlots(opts) { const requested = opts.requested.trim(); const wanted = lower(requested); if (!wanted) return null; if (!liveNamesMatchAvailable(opts.available, opts.inputs)) return null; const wildcardRequest = isAeWildcardName(requested); const labelHits = opts.inputs.filter((slot) => displayKeys(slot).includes(wanted)); const labelHit = uniqueSlot(labelHits); if (labelHit && labelHit.name) { if (wildcardRequest && !isWildcardSlotType(labelHit.type)) return null; return { to_input: labelHit.name, via: "label", slot: labelHit.slot, name: labelHit.name }; } if (!wildcardRequest) return null; if (opts.nodeType != null && !isAeWildcardNodeType(opts.nodeType)) return null; const starHits = opts.inputs.filter((slot) => isWildcardSlotType(slot.type) && slot.name); const namedStar = uniqueSlot(starHits.filter((slot) => lower(slot.name) === wanted || displayKeys(slot).includes(wanted))); const star = namedStar ?? uniqueSlot(starHits); if (!star || !star.name) return null; return { to_input: star.name, via: "wildcard", slot: star.slot, name: star.name }; } export function extraLiveWildcardIndexNote(requested, available, node) { if (!node || !isAeWildcardName(requested)) return null; if (node.type != null && !isAeWildcardNodeType(node.type)) return null; if (liveNamesMatchAvailable(available, node.inputs)) return null; const wanted = lower(requested); if (!wanted) return null; const starHits = node.inputs.filter((slot) => isWildcardSlotType(slot.type)); const named = starHits.filter((slot) => lower(slot.name) === wanted || displayKeys(slot).includes(wanted)); const hit = uniqueSlot(named.length ? named : starHits); if (!hit) return null; const address = hit.name ? `"${hit.name}"` : `index ${hit.slot}`; return (`The live LiteGraph node ${node.id} (${node.type ?? "unknown type"}) still has a wildcard \`*\` ` + `input at slot ${hit.slot} (${address}), but the panel's name lookup only listed ` + `${available.length ? available.map((s) => JSON.stringify(s)).join(", ") : "no names"} ` + `(artokun/comfyui-mcp#2493). Retry panel_expose_subgraph_input with to_input:${hit.slot} ` + `(the live index). If that is refused as out of range, Convert to Subgraph dropped the ` + `frontend wildcard from the array the panel mutates — re-add Anything Everywhere? inside ` + `the subgraph so the live \`anything\` socket exists.`); } export function missingAeWildcardNote(requested, available, node) { if (!isAeWildcardName(requested)) return null; if (node?.type != null && !isAeWildcardNodeType(node.type)) return null; if (node && node.inputs.some((slot) => isWildcardSlotType(slot.type))) return null; const where = node ? `live node ${node.id} (${node.type ?? "unknown type"})` : "the interior node"; const listed = available.length ? available.map((s) => JSON.stringify(s)).join(", ") : "none"; return (`${where} has no wildcard \`*\` input after subgraph conversion (artokun/comfyui-mcp#2493). ` + `panel_graph_outline's [renamed "${requested}"] is a display label, not an addressable bus socket. ` + `The panel's available slot(s) are widget sockets (${listed}), not the Anything Everywhere? source. ` + `Re-add the node inside the subgraph so the live LiteGraph \`anything\` socket exists, then expose ` + `that slot from panel_query_graph (name, label, or index).`); } function toolText(res) { return res.content.map((c) => (c.type === "text" ? (c.text ?? "") : "")).join("\n"); } function parseJsonPayload(res) { if (res.isError) return null; const text = res.content.find((c) => c.type === "text")?.text; if (typeof text !== "string") return null; try { return JSON.parse(text); } catch { return null; } } function withNote(res, note) { if (!note) return res; return { ...res, content: [...res.content, { type: "text", text: note }] }; } /** * First try the panel's name lookup. On the #2493 missing-name refusal, query * the live node and retry with the addressable live slot name when that is * unique and safe. Never throws. */ export async function retryExposeSubgraphInput(args, call) { const first = await call({ cmd: "graph_expose_subgraph_input", to_node_id: args.to_node_id, to_input: args.to_input, name: args.name, }, 15000); if (!first.isError) return first; if (typeof args.to_input === "number") return first; const refusal = missingInputSlotRefusal(toolText(first)); if (!refusal) return first; const detail = await call({ cmd: "graph_query", ids: [args.to_node_id], fields: "detail", limit: 1 }, 8000); const node = liveExposeNodeFromQuery(parseJsonPayload(detail), args.to_node_id); const resolved = node ? resolveExposeInputAgainstLiveSlots({ requested: refusal.requested, available: refusal.available, nodeType: node.type, inputs: node.inputs, }) : null; if (resolved && lower(resolved.to_input) !== lower(refusal.requested)) { const retry = await call({ cmd: "graph_expose_subgraph_input", to_node_id: args.to_node_id, to_input: resolved.to_input, name: args.name, }, 15000); if (!retry.isError) { return withNote(retry, `Resolved to_input ${JSON.stringify(args.to_input)} to live slot ` + `${JSON.stringify(resolved.to_input)} (index ${resolved.slot}, via ${resolved.via}) ` + `because the panel's name lookup missed the frontend wildcard (artokun/comfyui-mcp#2493).`); } } return withNote(first, extraLiveWildcardIndexNote(refusal.requested, refusal.available, node) ?? missingAeWildcardNote(refusal.requested, refusal.available, node)); } //# sourceMappingURL=expose-ae-wildcard.js.map