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
JavaScript
// 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