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

375 lines • 23.6 kB
// NEVER LEAVE THE CALLER WITH ONLY A TOOL THEY CANNOT INVOKE. (#774, #784) // // When the bridge refuses a graph WRITE because the connected panel is too old // to fence the command against the active workflow (#718), the refusal has // always ended with "Run install_comfyui(action:'panel', panel_action:'update')". In a local session // with a resolvable ComfyUI that is exactly right. In the two sessions that // filed #774 and #784 it is a dead end: // // - REMOTE/CLOUD (#774): install_comfyui(action:'panel') is deliberately local-only. It answers // `decision: "not-applicable"` and changes nothing, because the panel lives // on someone else's filesystem. panel_update_node then refuses too — it // cannot verify an on-disk move (#639/#641) — and points BACK at // install_comfyui(action:'panel'). Two tools, one circle, no way out. // // - EMBEDDED PANEL SESSION (#784): the sidebar surface is the disjoint // `panel_*` tool set. install_comfyui(action:'panel') was never in it, so the recommendation // named a tool that was not merely unhelpful but absent. // // A hard version gate is only defensible while its escape hatch works. So the // recovery text is built HERE, from the session's actual context, under two // rules: // // 1. Where install_comfyui(action:'panel') provably CANNOT do the job — remote, cloud, or a // local install we have positively determined does not exist — it is not // named at all. The text gives the commands to run on the ComfyUI host. // // 2. Where it can, it is named AND the host-side commands are given as the // alternative, because whether install_comfyui(action:'panel') is actually in the caller's // advertised tool set is a property of the client's surface, not of this // process, and is therefore not knowable from here. Offering both costs a // sentence and removes the only way this message can dead-end. // // Note what is deliberately NOT done: nothing here weakens the write gate. The // panel still cannot make an unfenced write. This module only fixes the remedy. // // It is the SINGLE source of that advice — the bridge refusal, the install_comfyui(action:'panel') // status note, the sync assessment and panel_update_node's refusal all render // it, so they cannot drift apart and contradict each other. import { isLocalMode, isRemoteMode } from "../config.js"; import { lastPanelBaseResolution, panelBaseSync } from "./panel-workspace.js"; /** Upstream source of truth for the panel pack — the manual-recovery clone URL. */ export const PANEL_REPO_URL = "https://github.com/artokun/comfyui-mcp-panel.git"; /** Canonical custom_nodes directory name for the panel pack (the Registry name). */ export const PANEL_DIR_NAME = "comfyui-agent-panel"; /** * The OTHER directory name that is the same panel: a plain `git clone` of the * repo lands in `comfyui-mcp-panel`, and the installer accepts both (see * panel-installer's FAST_PATH_DIRS). * * This matters here and not only there. The manual recovery tells a user to * clone the pack when it is "NOT PRESENT" — and if "present" is judged by the * Registry name alone, someone with a perfectly good repo checkout is told to * clone a SECOND copy alongside it. ComfyUI serves every directory under * custom_nodes, so that produces exactly the ambiguous two-panels state the rest * of this text exists to warn about (#641), and later panel management then * correctly refuses to act on the duplicate. Guidance that manufactures the * failure it warns about is worse than no guidance. * * `panel-recovery-cluster.test.ts` asserts this list matches the installer's, so * the two cannot drift (kept as a separate constant rather than an import * because panel-installer imports THIS module). */ export const PANEL_DIR_NAMES = [PANEL_DIR_NAME, "comfyui-mcp-panel"]; /** * Resolve the recovery context from the live deployment mode. * * Deliberately tolerant of a mode helper that cannot be read: this function's * only job is to compose an error message, and an error message must never be * the thing that throws. An unreadable mode resolves to "local" — the branch * that names install_comfyui(action:'panel') AND carries the host-side commands, so the caller is * still left with something to do either way. */ export function panelRecoveryContext() { const local = (() => { try { return isLocalMode(); } catch { return true; } })(); if (!local) { // Not local. `isRemoteMode()` distinguishes a non-loopback COMFYUI_URL from // Comfy Cloud (an API key), which get different host-side advice. const remote = (() => { try { return isRemoteMode(); } catch { return true; } })(); return { installPanelUsable: false, blocker: remote ? "remote" : "cloud" }; } // Local. Only a RESOLVED "there is no local ComfyUI" counts as a blocker — // an unprimed cache means we have not looked, not that there is nothing // there, and must not suppress the tool that would have fixed it. const resolution = (() => { try { return lastPanelBaseResolution(); } catch { return undefined; } })(); if (resolution && !resolution.base) { return { installPanelUsable: false, blocker: "no-local-workspace" }; } let comfyuiPath; try { comfyuiPath = panelBaseSync(); } catch { comfyuiPath = undefined; // the commands fall back to a <ComfyUI> placeholder } return { installPanelUsable: true, comfyuiPath }; } /** * Name an install_comfyui(action:'panel') action ONLY where install_comfyui(action:'panel') can be invoked. * * The bridge refusal was the loudest instance of the #774/#784 dead end, but it * was never the only one: the sync assessment, the pin notes, the auto-sync * failure message and the interrupted-swap guidance all named install_comfyui(action:'panel') * unconditionally, and the embedded `panel_*` surface — the very surface those * messages are pushed to — does not carry it. Every one of them routes through * here so the instruction is a real one wherever it is rendered. */ export function describeInstallPanelAction(action, /** What to say instead when install_comfyui(action:'panel') cannot be invoked here. */ hostSide, ctx = panelRecoveryContext()) { return ctx.installPanelUsable ? `install_comfyui(action:'panel', panel_action:'${action}')` : hostSide; } function blockerPhrase(blocker) { switch (blocker) { case "remote": return ("this session targets a REMOTE ComfyUI, so the panel pack is on that host's " + "disk and no tool here can move it"); case "cloud": return ("this session targets Comfy Cloud, which has no custom_nodes this " + "orchestrator can write"); case "no-local-workspace": return "no local ComfyUI install could be resolved from here"; default: return "install_comfyui(action:'panel') cannot perform the update from here"; } } /** * The manual, host-side update. Written as a real command sequence because the * caller may be an agent with no ability to ask a human to "use the Manager UI". * * IT MUST WORK FROM WHICHEVER STATE THE CALLER IS ACTUALLY IN, and this function * cannot see which one that is — the refusal that renders it knows the tab did * not advertise the fence, not what is on the host's disk. So it enumerates the * three real states and lets the reader match, instead of asserting one: * * (a) a git checkout — fast-forward; * (b) a Comfy Registry zip install with no `.git` — nothing to pull, so the * directory has to be REPLACED (#771); * (c) NOT THERE AT ALL (#819) — ComfyUI-Manager 3.x can report its install * queue drained without ever creating the pack, so a user who "installed * the panel" can still have an empty custom_nodes. Both (a) and (b) fail * in that state — `git -C <dir> pull` and `mv <dir> …` need a directory * that exists — which left the one instruction we gave them dead. A fresh * clone is the command that moves them, and it was missing. * * The BACKUP LEAVES custom_nodes on purpose. ComfyUI serves every directory * under custom_nodes as a web extension — including dot-prefixed ones — so a * copy parked next to the panel would shadow it in the browser and the update * would look like it did nothing (#641). Moving it to a sibling of custom_nodes * is the difference between a fix and a confusing non-fix. */ export function manualPanelUpdateCommands(comfyuiPath) { const root = comfyuiPath ?? "<ComfyUI>"; const either = PANEL_DIR_NAMES.join(" or "); return (`cd "${root}/custom_nodes". The panel pack is whichever of ${either} you have — ` + `BOTH are the same pack (the Registry installs the first, a plain git clone of the repo ` + `lands in the second), so check for both before deciding, and call the one you find ` + `<panel-dir>. Then run the ONE case that matches. ` + `(1) <panel-dir> exists and is a git checkout — fast-forward it: ` + `git -C <panel-dir> pull --ff-only. ` + `(2) <panel-dir> exists but has NO .git (a Comfy Registry zip install, so there ` + `is nothing to pull) — replace it IN PLACE, keeping its name: ` + `git clone --depth 1 ${PANEL_REPO_URL} ../.agent-panel-new && ` + `mkdir -p ../custom_nodes_backup && ` + `mv <panel-dir> ../custom_nodes_backup/ && ` + `mv ../.agent-panel-new <panel-dir>. ` + `(3) NEITHER ${either} is present — a stale ComfyUI-Manager 3.x reports its queue ` + `drained without creating the pack (#819), so "already installed" can mean an empty ` + `custom_nodes; install it outright: ` + `git clone --depth 1 ${PANEL_REPO_URL} ${PANEL_DIR_NAME}. ` + `Do NOT run case (3) while one of them exists under the other name — ComfyUI serves ` + `every directory in custom_nodes, so a second copy would leave two panels racing to ` + `register and the browser loading whichever sorts first (#641). For the same reason, in ` + `case (2) keep the old copy OUT of custom_nodes`); } /** The trailing "and then" every variant shares. */ const RESTART_AND_REFRESH = `restart ComfyUI, then hard-refresh the ComfyUI browser tab (Ctrl+Shift+R) so it ` + `loads the updated bundle — a restart alone leaves the tab running its cached old ` + `panel JS; rebinding cannot add the missing capability`; /** * WHY "install_comfyui(action:'panel') does not exist" is usually wrong, and what to do about it. * * #812 and #823 both report the same dead end: an error names install_comfyui(action:'panel'), the * agent searches its tool list, finds nothing, and concludes the remedy is * impossible. In almost every one of those sessions the tool was there — * COMPACT TOOL MODE IS THE DEFAULT (#667). It registers exactly three meta-tools * and leaves the other ~200, install_comfyui(action:'panel') among them, reachable only through * `call_tool`. A tool-name search cannot see it; `call_tool` can run it. * * Saying so is the difference between a remedy the caller can execute from where * they are and one that reads as "this is impossible". The host-side commands * still follow, for the surfaces where neither route exists (the embedded * `panel_*` sidebar set, #784). */ const COMPACT_ROUTER_FALLBACK = (action) => `If install_comfyui is not in this session's tool list, it is probably not missing — ` + `compact tool mode is the DEFAULT and exposes only list_tools / describe_tool / ` + `call_tool, with every other tool reachable through them. Try ` + `call_tool {"name": "install_comfyui", "args": {"action": "panel", "panel_action": "${action}"}} ` + `before concluding ` + `it is unavailable.`; /** * The recovery sentence for a panel that is too old for the write gate. Names * install_comfyui(action:'panel') only where install_comfyui(action:'panel') can really do it, and ALWAYS carries a * host-side command sequence so the caller is never left without a next step. */ export function describePanelUpdateRecovery(ctx = panelRecoveryContext(), skew, /** * #973 — set when the stale-bundle check could not read the pack's ON-DISK * version at all, so it proved nothing either way. * * The skew check returns a skew only on positive proof, and an unreadable disk * version resolves to "no skew" — which then falls through to "Run * install_comfyui(action:'panel', panel_action:'update')". That fall-through is right when the install is * genuinely behind and WRONG when it is already current, and this branch * cannot tell which. A reporter followed it, found their pack was already at * origin/main HEAD, and had spent a round trip on an update that could not * have helped. Leading with an unproven remedy is the thing to fix; the check * itself already re-reads the base in the background, so the cheap move is to * say so and let one retry answer properly. */ diskVersionUnconfirmed = false) { // STALE BUNDLE FIRST. When the caller has PROVEN the pack on disk already // satisfies the requirement, no update of any kind is the answer — not // install_comfyui(action:'panel'), not a host-side git pull. Sending this user to either is // what makes the loop feel unfixable, so this branch outranks both. if (skew) { // WHAT IS PROVEN vs WHAT IS INFERRED (codex gate). // // Proven in both variants: the pack on disk clears the floor, so no update // of the pack changes this refusal. That is the part the headline may state. // // The CAUSE is a different claim, and the evidence for it differs: // - the client advertised a version BELOW the floor → the served pack and // the announced build genuinely disagree. Both the version and the // capability are built by the same served file, so a client running the // installed bundle would have announced the installed version. An OLDER // bundle in the tab is then a conclusion, not a guess — though WHY it is // older stays open, so the text names the ordinary reason (the pack moved // under an open tab) without asserting a mechanism it did not observe. // - the client advertised NO version → nothing was observed. A browser tab // on an old bundle looks exactly like a relay or other non-panel client // that never implemented the fence at all, and "reload your tab" is // unactionable for the latter. Asserting a cause is the fabrication this // cluster exists to remove, so that variant RANKS the possibilities // instead — the actionable ones first, the others named rather than // hidden. The second rank (panel #1515) is the two-panels install, which // is the one possibility here that a reload provably CANNOT fix, so it // must be named rather than left to be discovered after the reload fails. // RELOAD FIRST, HARD-REFRESH AS THE ESCALATION (panel #1515). // // This used to open by asserting the HTTP cache as the mechanism ("the module // URLs carry no cache-busting key, so an ordinary reload can serve the stale // file again") and send the user straight to Ctrl+Shift+R. The panel repo has // since MEASURED that claim and recorded the opposite in its own __init__.py: // "ComfyUI 0.31.1 already answers `Cache-Control: no-store` on every // /extensions/ path — ours and other packs' alike — so the HTTP cache is not // the mechanism there", and the pack installs a no-store backstop for hosts // that set none. On any current host an ordinary reload DOES pick up new JS. // // Leading with the cache therefore named a cause that no longer applies and // buried the one that does — a tab that has simply been open since before the // pack changed. #1515's reporter had an in-place ComfyUI-Manager update at // 11:37 on a tab opened at 11:11, with no restart and no reload; they were // sent to diagnose a cache they did not have. The remedy survives (a reload // fixes both), so this is a change of CAUSE and of ORDER, not of action. const RELOAD_TAB = `RELOAD THIS COMFYUI TAB. An ordinary reload is normally enough — ComfyUI ` + `answers Cache-Control: no-store on /extensions/ assets (and the pack sets it ` + `as a backstop where the host does not), so the tab picks up the new JS. If the ` + `refusal survives a reload, escalate to a cache-bypassing one (Ctrl+Shift+R, or ` + `Cmd+Shift+R on macOS; if that does not take, open DevTools and use right-click ` + `reload → "Empty Cache and Hard Reload"). `; return ((skew.handshakeVersion ? `Do NOT update the panel — the pack ON DISK is already ${skew.diskVersion}, which ` + `meets the ${skew.requiredVersion}+ a graph write needs. This BROWSER TAB is running ` + `an OLDER copy of the panel's JavaScript (it advertised ` + `${skew.handshakeVersion}), and the capability check reads what the TAB ` + `announced — most often because the pack was updated in place while this tab ` + `stayed open. ${RELOAD_TAB}` : `Updating the panel will not fix this — the pack ON DISK is already ` + `${skew.diskVersion}, which meets the ${skew.requiredVersion}+ a graph write needs, ` + `so the SERVED panel is already capable. What connected advertised no version and ` + `no workflow fence, which does not by itself say why, so here are the ` + `possibilities, likeliest first. (1) THE PACK WAS UPDATED IN PLACE WHILE THIS TAB ` + `WAS OPEN, and the tab is still running the JavaScript it loaded before the ` + `update — ComfyUI-Manager says as much after every update ("After restarting ` + `ComfyUI, please refresh the browser"), and a tab opened before the update never ` + `did. ${RELOAD_TAB}` + `(2) The pack is INSTALLED TWICE — a git clone at custom_nodes/comfyui-mcp-panel ` + `beside a Registry install at custom_nodes/comfyui-agent-panel. ComfyUI loads ` + `both bundles into the page, and an old enough copy hellos without a version at ` + `all; reloading cannot fix that, because the stale copy is served from its own ` + `directory and re-registers on every load. Check custom_nodes for both names and ` + `remove the one you do not use. ` + `(3) It is NOT a panel tab (a relay or other client): then it does not implement ` + `the fence at all and there is nothing to reload — issue graph WRITES from a ` + `ComfyUI tab running the panel. `) + // Only name the tool where naming it is useful. In a remote/cloud session // it is not callable at all, so mentioning it is a pointless mention of an // absent tool; say the plain thing instead. // // NOT "it will report nothing to do" — that was only true while a // floor-clearing panel was believed to be the newest one (#806). An update // CAN pull a newer panel; what it cannot do is replace the JS an open tab // is already running, which is the whole point of this branch. // // And NO trailing period: every caller appends its own sentence break, and // adding one here rendered ".." into a real refusal (codex gate). (ctx.installPanelUsable ? `install_comfyui(action:'panel', panel_action:'update') is not the fix here — it may pull a newer ` + `panel, but no update replaces the JavaScript an open tab is already running` : `No update of any kind fixes this — the install is not the problem`)); } // #973 — the on-disk version could not be read, so "your install is behind" is // NOT established and must not lead. Two remedies are live and one of them is // free, so name the free one first and keep the update as the fallback it is. // (The check re-primes the panel base in the background precisely so a retry // can answer this properly, which is why retrying is the first suggestion.) const unconfirmedPrefix = diskVersionUnconfirmed ? `NOTE: the panel version ON DISK could not be read just now, so whether the install is ` + `actually behind is UNCONFIRMED — an already-current pack behind a stale cached browser ` + `tab looks identical from here. Two cheap checks before updating anything: (1) RETRY this ` + `command once — the disk version is being re-read in the background and a retry usually ` + `answers definitively; (2) HARD-REFRESH the ComfyUI tab (Ctrl+Shift+R, Cmd+Shift+R on ` + `macOS), which costs nothing and fixes it outright if the tab is the stale part. ` + `If it is genuinely behind: ` : ""; if (ctx.installPanelUsable) { return (`${unconfirmedPrefix}Run install_comfyui(action:'panel', panel_action:'update'). ${COMPACT_ROUTER_FALLBACK("update")} ` + `If neither route exists on this surface, update the pack on the ComfyUI host: ` + // No trailing period: every caller of this function appends its own // sentence break, and adding one here rendered ".." to the user. `${manualPanelUpdateCommands(ctx.comfyuiPath)}. Then ${RESTART_AND_REFRESH}`); } return (`${unconfirmedPrefix}Update the panel ON THE COMFYUI HOST — ${blockerPhrase(ctx.blocker)}. On the host ` + `run: ${manualPanelUpdateCommands(ctx.comfyuiPath)}. Then ${RESTART_AND_REFRESH}`); } /** * The same advice as a redirect for tools that REFUSE to manage the panel * themselves (panel_update_node, install_custom_node (action:"fix")). Those refusals used to end * with a flat "Use install_comfyui(action:'panel') instead", which is the #774/#784 dead end in * miniature. */ export function describePanelManagementRedirect(ctx = panelRecoveryContext()) { if (ctx.installPanelUsable) { return (`Use install_comfyui(action:'panel') instead: install_comfyui(action:'panel', panel_action:'sync') brings the panel in line ` + `with this orchestrator and re-reads the installed version from disk, and ` + `action='status' reports it. ${COMPACT_ROUTER_FALLBACK("sync")} ` + `If neither route exists on this surface, update the pack on the ComfyUI host ` + `instead: ${manualPanelUpdateCommands(ctx.comfyuiPath)}.`); } return (`install_comfyui(action:'panel') cannot help here either — ${blockerPhrase(ctx.blocker)}. Update the ` + `panel ON THE COMFYUI HOST: ${manualPanelUpdateCommands(ctx.comfyuiPath)}. Then ` + `restart ComfyUI and hard-refresh the ComfyUI browser tab (Ctrl+Shift+R).`); } //# sourceMappingURL=panel-recovery.js.map