comfyui-mcp 0.48.26 → 0.48.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/orchestrator/panel-tools.js +138 -8
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/services/node-management.js +97 -49
- package/dist/services/node-management.js.map +1 -1
- package/dist/services/ui-bridge.js +103 -5
- package/dist/services/ui-bridge.js.map +1 -1
- package/dist/tools/image-management.js +7 -1
- package/dist/tools/image-management.js.map +1 -1
- package/package.json +1 -1
|
@@ -222,6 +222,17 @@ function sleep(ms) {
|
|
|
222
222
|
// settle. Mutating graph edits (add_node/connect/set_widget/…) are deliberately
|
|
223
223
|
// EXCLUDED — re-issuing them could double-apply — so they keep surfacing the
|
|
224
224
|
// bridge's honest OUTCOME-UNKNOWN error.
|
|
225
|
+
// #599: commands whose FRONTEND handler intentionally awaits a fresh /object_info
|
|
226
|
+
// re-register before it can reply — the refresh-before-validate path in
|
|
227
|
+
// graph_set_widget (#338/#458) and graph_add_node (#289/#458), and the explicit
|
|
228
|
+
// forced node-def refresh in refresh_nodes (#608). On a large install with many
|
|
229
|
+
// custom-node packs a legitimate /object_info fetch can take longer than the
|
|
230
|
+
// bridge's 6000 ms DEFAULT ack window, so the panel replies late and the tool
|
|
231
|
+
// returns a FALSE "tab did not reply" timeout even though the write is valid and
|
|
232
|
+
// in progress. Give these a larger BOUNDED ack budget so a slow-but-valid refresh
|
|
233
|
+
// is not mistaken for a dead tab — still capped (never Infinity) so a genuinely
|
|
234
|
+
// frozen/backgrounded tab fails in bounded time instead of hanging forever.
|
|
235
|
+
const OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS = 30_000;
|
|
225
236
|
const RETRY_SAFE_CMDS = new Set([
|
|
226
237
|
// Idempotent reads (mirror UiBridge.READONLY_CMDS + list/status probes).
|
|
227
238
|
"graph_serialize",
|
|
@@ -237,6 +248,10 @@ const RETRY_SAFE_CMDS = new Set([
|
|
|
237
248
|
"node_queue_status",
|
|
238
249
|
// Idempotent full-replace UI state — re-sending the same list is a no-op (#481).
|
|
239
250
|
"set_todo",
|
|
251
|
+
// #608: a forced /object_info re-register + combo refresh. Non-destructive and
|
|
252
|
+
// idempotent (re-running just re-fetches the current defs), so a dropped
|
|
253
|
+
// transport can safely re-issue it once.
|
|
254
|
+
"refresh_nodes",
|
|
240
255
|
]);
|
|
241
256
|
/** A command whose result is unchanged by being re-issued after a reconnect —
|
|
242
257
|
* so it is safe to transparently retry once when the transport dropped. */
|
|
@@ -244,6 +259,57 @@ function isRetrySafeCmd(cmd) {
|
|
|
244
259
|
const name = typeof cmd.cmd === "string" ? cmd.cmd : "";
|
|
245
260
|
return RETRY_SAFE_CMDS.has(name);
|
|
246
261
|
}
|
|
262
|
+
// Graph-EDIT mutations that CHANGE the user's canvas (undoable edits). These are
|
|
263
|
+
// the #436 bug surface: a real side effect the bridge will NOT auto-retry, so —
|
|
264
|
+
// unlike a read — such a command can be neither parked mid-command nor retried
|
|
265
|
+
// once, and firing it into the post-restart "Connected: none" window fails with
|
|
266
|
+
// "no connected tab". It must await a stable binding BEFORE dispatch.
|
|
267
|
+
//
|
|
268
|
+
// This is an EXPLICIT ALLOWLIST, deliberately NOT "everything not read-only":
|
|
269
|
+
// several genuine reads/probes/views that flow through ctx.call are absent from
|
|
270
|
+
// BRIDGE_READONLY_CMDS (e.g. graph_list_subgraphs, training_get_state,
|
|
271
|
+
// graph_canvas, graph_screenshot), so an exclusion rule would wrongly make THOSE
|
|
272
|
+
// wait out the reconnect budget. Under-inclusion here is at worst an unfixed edge
|
|
273
|
+
// (a command keeps today's behavior); over-inclusion would regress a read — so we
|
|
274
|
+
// list only commands that unambiguously mutate the graph. Keep in sync when new
|
|
275
|
+
// graph-edit tools are added (mirrors the RETRY_SAFE_CMDS maintenance model).
|
|
276
|
+
const MUTATING_GRAPH_EDIT_CMDS = new Set([
|
|
277
|
+
"graph_add_node",
|
|
278
|
+
"graph_remove_node",
|
|
279
|
+
"graph_clear",
|
|
280
|
+
"graph_connect",
|
|
281
|
+
"graph_disconnect",
|
|
282
|
+
"graph_set_widget",
|
|
283
|
+
"graph_move_node",
|
|
284
|
+
"graph_resize_node",
|
|
285
|
+
"graph_set_title",
|
|
286
|
+
"graph_set_node_mode",
|
|
287
|
+
"graph_set_node_color",
|
|
288
|
+
"graph_set_node_collapsed",
|
|
289
|
+
"graph_update_node",
|
|
290
|
+
"graph_create_group",
|
|
291
|
+
"graph_edit_group",
|
|
292
|
+
"graph_remove_group",
|
|
293
|
+
"graph_move_group",
|
|
294
|
+
"graph_create_subgraph",
|
|
295
|
+
"graph_add_subgraph",
|
|
296
|
+
"graph_save_subgraph",
|
|
297
|
+
"graph_unpack_subgraph",
|
|
298
|
+
"graph_subgraph_group",
|
|
299
|
+
"graph_expose_subgraph_input",
|
|
300
|
+
"graph_expose_subgraph_output",
|
|
301
|
+
"graph_promote_widget",
|
|
302
|
+
"graph_move_rail",
|
|
303
|
+
"graph_paste_nodes",
|
|
304
|
+
"graph_auto_layout",
|
|
305
|
+
"graph_load",
|
|
306
|
+
]);
|
|
307
|
+
/** A MUTATING graph edit that must await a stable tab binding before dispatch so
|
|
308
|
+
* it never fires into the post-restart "Connected: none" window (#436). */
|
|
309
|
+
function isMutatingGraphCmd(cmd) {
|
|
310
|
+
const name = typeof cmd.cmd === "string" ? cmd.cmd : "";
|
|
311
|
+
return MUTATING_GRAPH_EDIT_CMDS.has(name);
|
|
312
|
+
}
|
|
247
313
|
/** True when an error is a TRANSIENT transport/reconnect drop (the tab went away
|
|
248
314
|
* or was replaced), NOT a genuine command error or a live-but-frozen reply
|
|
249
315
|
* timeout. Deliberately EXCLUDES "did not reply within N ms" (a backgrounded/
|
|
@@ -1525,6 +1591,21 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
|
|
|
1525
1591
|
};
|
|
1526
1592
|
const call = async (cmd, timeoutMs) => {
|
|
1527
1593
|
try {
|
|
1594
|
+
// #436: a MUTATING graph edit must not fire into the "Connected: none"
|
|
1595
|
+
// window a ComfyUI restart/reload opens. A read survives that window (it is
|
|
1596
|
+
// parked mid-command and is retry-safe), but a mutating edit is NEITHER — so
|
|
1597
|
+
// panel_graph_outline succeeds while the very next panel_add_node hits
|
|
1598
|
+
// resolveTarget's momentarily-empty registry and fails with
|
|
1599
|
+
// "no connected tab … Connected: none" (the flap). Await a stable binding
|
|
1600
|
+
// BEFORE dispatch — nothing is sent during the wait, so there is no
|
|
1601
|
+
// double-apply risk — exactly as workflow_open/save already do. This returns
|
|
1602
|
+
// INSTANTLY for a healthy session and only waits in the zero-tab window; the
|
|
1603
|
+
// read path (parking + retry-once below) is left exactly as-is. A wait that
|
|
1604
|
+
// times out unreached still falls through to sendRouted, whose authoritative
|
|
1605
|
+
// dispatched:false surfaces the actionable "nothing applied — rebind" message.
|
|
1606
|
+
if (isMutatingGraphCmd(cmd)) {
|
|
1607
|
+
await awaitReachable();
|
|
1608
|
+
}
|
|
1528
1609
|
ensureReachable();
|
|
1529
1610
|
return ok(await sendRouted(cmd, timeoutMs));
|
|
1530
1611
|
}
|
|
@@ -1552,6 +1633,33 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
|
|
|
1552
1633
|
return fail(err2);
|
|
1553
1634
|
}
|
|
1554
1635
|
}
|
|
1636
|
+
// #442 defect 4: a MUTATING command (deliberately excluded from RETRY_SAFE_CMDS)
|
|
1637
|
+
// that the bridge refused BEFORE any socket write surfaced the bare routing error
|
|
1638
|
+
// ("no connected tab … Connected: none") with no recovery path — whereas a
|
|
1639
|
+
// retry-safe read like graph_get_errors, via the branch above, names the rebind.
|
|
1640
|
+
// That asymmetry made a brief post-reconnect read/edit-channel disagreement look
|
|
1641
|
+
// like a dead agent (panel_list_workflows kept answering while panel_set_widget
|
|
1642
|
+
// failed, in a multi-tab session the strict-single silent auto-heal won't touch).
|
|
1643
|
+
// We must NOT retry the mutating command (double-apply risk), but the bridge's
|
|
1644
|
+
// AUTHORITATIVE typed flag proves nothing was dispatched (dispatchOutcomeOf ===
|
|
1645
|
+
// false) — so it is safe to state nothing was applied and name the rebind recovery,
|
|
1646
|
+
// preserving the raw cause. Keying on the TYPED flag (not error text) means a
|
|
1647
|
+
// POST-dispatch executor ok:false reply that merely quotes "no connected tab" is
|
|
1648
|
+
// never mis-wrapped as "nothing applied".
|
|
1649
|
+
if (dispatchOutcomeOf(err) === false) {
|
|
1650
|
+
const name = typeof cmd.cmd === "string" ? cmd.cmd : "panel command";
|
|
1651
|
+
// Neutral wording: a dispatched:false flag proves only that the command was NOT
|
|
1652
|
+
// dispatched — it can be a routing refusal (the bound tab is gone / reconnecting),
|
|
1653
|
+
// an ambiguous-or-multiple-tab resolver refusal (other tabs DO exist), or a socket
|
|
1654
|
+
// write failure. All share the same TRUE facts (nothing applied) and the same
|
|
1655
|
+
// recovery (rebind onto the tab that's live now); the raw cause carries the
|
|
1656
|
+
// specifics. Do NOT overstate "disconnected", which is false for the ambiguity case.
|
|
1657
|
+
return fail(`${name} could not be dispatched to this session's panel tab — nothing was applied. ` +
|
|
1658
|
+
`The tab may be disconnected, still reconnecting after a restart/reload, or the ` +
|
|
1659
|
+
`session's binding is stale (e.g. another workflow tab is now active). Retry in a ` +
|
|
1660
|
+
`moment, or rebind with panel_set_workflow_target({mode:"current"}) to follow the ` +
|
|
1661
|
+
`tab that's live now. (${err instanceof Error ? err.message : String(err)})`);
|
|
1662
|
+
}
|
|
1555
1663
|
return fail(err);
|
|
1556
1664
|
}
|
|
1557
1665
|
};
|
|
@@ -2130,7 +2238,12 @@ export function buildPanelToolDefs() {
|
|
|
2130
2238
|
.optional()
|
|
2131
2239
|
.describe("Canvas [x, y] (two numbers). Auto-placed beside existing nodes when omitted."),
|
|
2132
2240
|
title: z.string().optional().describe("Optional custom node title."),
|
|
2133
|
-
}, async (args, ctx) =>
|
|
2241
|
+
}, async (args, ctx) =>
|
|
2242
|
+
// #599: the frontend gates the add on a FRESH /object_info (assertAddNode-
|
|
2243
|
+
// ResolvableRefreshing) so an uninstalled class can't be added as a
|
|
2244
|
+
// placeholder — that fetch can outlast the 6000 ms default on a large
|
|
2245
|
+
// install. Give it the bounded refresh ack budget.
|
|
2246
|
+
ctx.call({ cmd: "graph_add_node", class_type: args.class_type, pos: args.pos, title: args.title }, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS)),
|
|
2134
2247
|
def("panel_remove_node", "Remove a node (and its connections) from the user's open graph by id. Undoable with Ctrl+Z.", { node_id: z.number().int().describe("Node id from panel_graph_outline / panel_query_graph.") }, async (args, ctx) => ctx.call({ cmd: "graph_remove_node", node_id: args.node_id })),
|
|
2135
2248
|
def("panel_clear", "Remove EVERY node from the user's open graph — only for an explicit 'clear/reset the canvas'. Just CALL THIS DIRECTLY when they ask to clear: the tool itself pops a confirm card and only wipes on a yes (don't ask separately first). The wipe is a single Ctrl+Z undo. NEVER use this for a 'new workflow' — that's panel_new_workflow (a new tab, leaves this graph intact).", {}, async (_args, ctx) => {
|
|
2136
2249
|
const decision = await ctx.confirm("Clear the canvas? This removes every node from the open workflow. (One Ctrl+Z undoes it.)", "Clear canvas");
|
|
@@ -2360,7 +2473,12 @@ export function buildPanelToolDefs() {
|
|
|
2360
2473
|
if (value === undefined) {
|
|
2361
2474
|
return fail("panel_set_widget needs a `value`. To set an empty string, pass `clear: true` (some clients drop an empty-string `value`).");
|
|
2362
2475
|
}
|
|
2363
|
-
|
|
2476
|
+
// #599: the frontend runs refresh-before-validate here (pulls a fresh
|
|
2477
|
+
// /object_info so a just-staged/-downloaded/-installed value is accepted on
|
|
2478
|
+
// a single revalidation, #338/#458) — that authoritative fetch can outlast
|
|
2479
|
+
// the 6000 ms default ack on a large install and return a FALSE timeout.
|
|
2480
|
+
// Give the guarded write the bounded refresh ack budget.
|
|
2481
|
+
return ctx.call({ cmd: "graph_set_widget", node_id: args.node_id, widget: args.widget, value }, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS);
|
|
2364
2482
|
}),
|
|
2365
2483
|
def("panel_move_node", "Move a node to a new canvas position [x, y] in the user's open graph. Undoable.", {
|
|
2366
2484
|
node_id: z.number().int().describe("Node id from panel_graph_outline / panel_query_graph."),
|
|
@@ -2492,6 +2610,11 @@ export function buildPanelToolDefs() {
|
|
|
2492
2610
|
return res;
|
|
2493
2611
|
}),
|
|
2494
2612
|
def("panel_get_errors", "WHY IS THAT NODE RED / WHY DID THE RUN FAIL? The single error surface for the user's open tab: every errored node JOINED TO ITS CAUSE, which ComfyUI itself does not show — LiteGraph only paints a red outline and stores no reason, which is why users report \"red node, no error message\". Call this whenever the user mentions a red/highlighted/erroring node, a failed run, or \"required models are missing\" — instead of guessing from widget values. Each entry in `nodes[]` is the node's full detail summary plus `red_outline` and `reasons[]`, drawn from every source: `missing_model` (exact file, its models directory, the widget holding it, and a download URL when known), `missing_media` (a referenced input image/video that isn't on disk — the usual cause of a red LoadImage), `validation` (per-input errors from the last queue attempt: message, details, offending input), and `execution` (runtime failure with `exception_type`, e.g. PIL.UnidentifiedImageError). TWO THINGS THAT MAKE THIS ESSENTIAL: (1) missing model/media assets paint nodes red AS SOON AS THE WORKFLOW LOADS, long before any queue attempt — so the raw validation map is still EMPTY while the user is staring at red nodes; (2) a node that throws AT RUNTIME is never painted red at all, so it can't be spotted on the canvas — it appears here with red_outline:false. Also returns graph-level `missing_models`, `missing_media`, `missing_node_types` (or `missing_node_count`), plus the raw `node_errors` map and `last_execution_error` for reference. A ⚠️ GRAPH VALIDATION block is auto-injected at your turn start when this state changes; call this to re-check on demand (e.g. after you edit widgets/links). Read-only.", {}, async (_args, ctx) => ctx.call({ cmd: "graph_get_errors" })),
|
|
2613
|
+
def("panel_refresh_nodes", "Re-pull the live ComfyUI server's /object_info and rebuild every combo/loader option list in the user's open tab, so an asset that appeared server-side AFTER the tab loaded becomes SELECTABLE without a manual reload (the 'press R' step) or a restart. Use this right after stage_output_as_input (chaining a stage's output into a LoadImage / VHS_LoadVideo / LoadAudio loader — the returned filename won't be in the loader's dropdown until you refresh), after downloading a model / LoRA / VAE (a freshly downloaded file is otherwise 'not a valid option' in its loader), or after installing a node pack. Then panel_set_widget / panel_add_node will accept the new value. Non-destructive: it only re-registers node defs and refreshes combo option lists — it does NOT change your graph and is undo-neutral. Idempotent (safe to call repeatedly). Returns whether the refresh authoritatively fetched fresh defs.", {}, async (_args, ctx) =>
|
|
2614
|
+
// Same bounded ack budget as the refresh-before-validate writes (#599): a
|
|
2615
|
+
// fresh /object_info on a large install routinely exceeds the 6000 ms
|
|
2616
|
+
// default, and this command's WHOLE purpose is to await that fetch.
|
|
2617
|
+
ctx.call({ cmd: "refresh_nodes" }, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS)),
|
|
2495
2618
|
def("panel_reload", "Soft-reload yourself to pick up code changes WITHOUT restarting ComfyUI — your chat session resumes automatically and you'll be nudged to continue. Use scope 'orchestrator' (default) after backend/orchestrator code changed (new tools, system prompt, services); use scope 'frontend' after the panel UI (web JS/CSS) changed. This ENDS the current turn — your tools/prompt are reloaded and you continue fresh. For custom-node or model changes that need a full ComfyUI restart, use panel_restart_comfyui instead. Only call this when code has actually changed and needs to take effect now.", {
|
|
2496
2619
|
scope: z
|
|
2497
2620
|
.enum(["orchestrator", "frontend"])
|
|
@@ -2514,9 +2637,14 @@ export function buildPanelToolDefs() {
|
|
|
2514
2637
|
// Count only INTERACTIVE (canvas-owning) tabs for the ambiguity guard: one
|
|
2515
2638
|
// desktop canvas alongside headless viewers is NOT ambiguous — rebindToActiveTab
|
|
2516
2639
|
// binds the sole desktop tab. Only 2+ real canvas tabs are unpickable here.
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2640
|
+
// Call isHeadless THROUGH the bridge (not an extracted reference): it is a
|
|
2641
|
+
// plain method that reads `this.conns` — invoking a detached `const headless
|
|
2642
|
+
// = ctx.bridge.isHeadless` loses `this`, so `this.conns` throws "Cannot read
|
|
2643
|
+
// properties of undefined (reading 'conns')" and panel_reload fails outright
|
|
2644
|
+
// (panel #478). Mirror the bound `isHeadlessTab` helper used elsewhere here.
|
|
2645
|
+
const isHeadlessTab = (id) => typeof ctx.bridge.isHeadless === "function" && ctx.bridge.isHeadless(id);
|
|
2646
|
+
const interactive = Array.isArray(live)
|
|
2647
|
+
? live.filter((t) => !isHeadlessTab(t.tab_id))
|
|
2520
2648
|
: live;
|
|
2521
2649
|
if (orphaned && Array.isArray(interactive) && interactive.length > 1) {
|
|
2522
2650
|
return fail("This session's ComfyUI tab was replaced and multiple tabs are now open — " +
|
|
@@ -2984,9 +3112,11 @@ export function buildPanelToolDefs() {
|
|
|
2984
3112
|
if (Array.isArray(live)) {
|
|
2985
3113
|
// Count only INTERACTIVE (canvas-owning) tabs: a headless-only reconnect is
|
|
2986
3114
|
// NOT a usable graph binding, so it defers (binds once a real canvas tab
|
|
2987
|
-
// connects) rather than failing as if a tab were pickable.
|
|
2988
|
-
|
|
2989
|
-
|
|
3115
|
+
// connects) rather than failing as if a tab were pickable. Call isHeadless
|
|
3116
|
+
// THROUGH the bridge (it reads `this.conns`) — a detached reference would
|
|
3117
|
+
// lose `this` and throw "reading 'conns'" (the same #478 unbound-method bug).
|
|
3118
|
+
const isHeadlessTab = (id) => typeof ctx.bridge.isHeadless === "function" && ctx.bridge.isHeadless(id);
|
|
3119
|
+
const interactive = live.filter((t) => !isHeadlessTab(t.tab_id));
|
|
2990
3120
|
noTabsConnected = interactive.length === 0;
|
|
2991
3121
|
}
|
|
2992
3122
|
else {
|