comfyui-mcp 0.52.39 → 0.52.40
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.
|
@@ -3070,6 +3070,137 @@ function isLoraManagerAutocompleteRefusal(res) {
|
|
|
3070
3070
|
return (/had no widget after .+ waiting for node extensions to register/i.test(text) &&
|
|
3071
3071
|
/AUTOCOMPLETE_TEXT_/i.test(text));
|
|
3072
3072
|
}
|
|
3073
|
+
/**
|
|
3074
|
+
* #1944 — the panel's add-node socket proof read "which datatypes does some
|
|
3075
|
+
* installed node output?" off a single-class `/object_info/<class_type>`
|
|
3076
|
+
* payload (or a whole-schema widen that did not land). Custom link types a
|
|
3077
|
+
* SIBLING node produces — SeedVR2LoadDiTModel → SEEDVR2_DIT — then look
|
|
3078
|
+
* unproven, and the refusal claims "no installed node outputs X" while that
|
|
3079
|
+
* very node sits on the canvas. Retrying the add re-runs the same proof;
|
|
3080
|
+
* panel_refresh_nodes re-registers the class, which is what arms the
|
|
3081
|
+
* single-class path. The live graph is the authority the guard did not consult.
|
|
3082
|
+
*/
|
|
3083
|
+
function isMissingInstalledOutputsRefusal(res) {
|
|
3084
|
+
if (!res.isError)
|
|
3085
|
+
return false;
|
|
3086
|
+
const text = textOfToolResult(res);
|
|
3087
|
+
return (/had no widget after .+ waiting for node extensions to register/i.test(text) &&
|
|
3088
|
+
/no installed node outputs/i.test(text));
|
|
3089
|
+
}
|
|
3090
|
+
/** Types the panel named as unproven in a #1944-shaped refusal. */
|
|
3091
|
+
function parseUnprovenSocketTypes(text) {
|
|
3092
|
+
const types = [];
|
|
3093
|
+
const add = (raw) => {
|
|
3094
|
+
const v = raw.trim();
|
|
3095
|
+
if (v && !types.includes(v))
|
|
3096
|
+
types.push(v);
|
|
3097
|
+
};
|
|
3098
|
+
for (const re of [
|
|
3099
|
+
/no installed node outputs "([^"]+)"/gi,
|
|
3100
|
+
/declared type "([^"]+)": no installed node outputs/gi,
|
|
3101
|
+
]) {
|
|
3102
|
+
re.lastIndex = 0;
|
|
3103
|
+
let m;
|
|
3104
|
+
while ((m = re.exec(text)))
|
|
3105
|
+
add(m[1]);
|
|
3106
|
+
}
|
|
3107
|
+
return types;
|
|
3108
|
+
}
|
|
3109
|
+
function nodeOutputDeclaresType(node, socketType) {
|
|
3110
|
+
const outputs = node.outputs;
|
|
3111
|
+
if (Array.isArray(outputs)) {
|
|
3112
|
+
for (const out of outputs) {
|
|
3113
|
+
if (!out || typeof out !== "object" || Array.isArray(out))
|
|
3114
|
+
continue;
|
|
3115
|
+
const t = out.type;
|
|
3116
|
+
if (typeof t !== "string" || !t)
|
|
3117
|
+
continue;
|
|
3118
|
+
if (t === socketType)
|
|
3119
|
+
return true;
|
|
3120
|
+
if (t.split(",").map((part) => part.trim()).includes(socketType))
|
|
3121
|
+
return true;
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
const matchedOn = node.matched_on;
|
|
3125
|
+
if (!Array.isArray(matchedOn))
|
|
3126
|
+
return false;
|
|
3127
|
+
const needle = `(${socketType.toLowerCase()})`;
|
|
3128
|
+
return matchedOn.some((entry) => typeof entry === "string" && entry.toLowerCase().includes(needle));
|
|
3129
|
+
}
|
|
3130
|
+
/**
|
|
3131
|
+
* Ask the live canvas whether each unproven type is actually produced there.
|
|
3132
|
+
* Returns null when a probe fails — an unreadable graph is not evidence the
|
|
3133
|
+
* producers are present, so the original refusal stays the last word.
|
|
3134
|
+
*/
|
|
3135
|
+
async function findLiveSocketProducers(ctx, types) {
|
|
3136
|
+
const found = new Map();
|
|
3137
|
+
for (const socketType of types) {
|
|
3138
|
+
const probe = await ctx.call({ cmd: "graph_find_nodes", output: socketType, limit: 5 }, 8000);
|
|
3139
|
+
if (probe.isError)
|
|
3140
|
+
return null;
|
|
3141
|
+
const payload = parseToolResultJson(probe);
|
|
3142
|
+
const matches = Array.isArray(payload?.matches) ? payload.matches : [];
|
|
3143
|
+
const producers = [];
|
|
3144
|
+
for (const row of matches) {
|
|
3145
|
+
if (!row || typeof row !== "object" || Array.isArray(row))
|
|
3146
|
+
continue;
|
|
3147
|
+
const rec = row;
|
|
3148
|
+
if (!nodeOutputDeclaresType(rec, socketType))
|
|
3149
|
+
continue;
|
|
3150
|
+
const id = rec.id;
|
|
3151
|
+
const type = rec.type ?? rec.class_type;
|
|
3152
|
+
producers.push({
|
|
3153
|
+
id: id == null ? "?" : String(id),
|
|
3154
|
+
type: typeof type === "string" && type ? type : "unknown",
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
found.set(socketType, producers);
|
|
3158
|
+
}
|
|
3159
|
+
return found;
|
|
3160
|
+
}
|
|
3161
|
+
function liveSocketProducerNote(classType, found) {
|
|
3162
|
+
const target = typeof classType === "string" && classType ? classType : "this node";
|
|
3163
|
+
const lines = [...found.entries()].map(([socket, nodes]) => {
|
|
3164
|
+
const who = nodes
|
|
3165
|
+
.slice(0, 3)
|
|
3166
|
+
.map((n) => `${n.type} #${n.id}`)
|
|
3167
|
+
.join(", ");
|
|
3168
|
+
return ` - ${socket}: ${who}`;
|
|
3169
|
+
});
|
|
3170
|
+
return (`\n\nThis is NOT a missing pack and NOT a widget that is still loading. ` +
|
|
3171
|
+
`The live canvas ALREADY has nodes that output every type the add-node guard ` +
|
|
3172
|
+
`claimed was missing:\n` +
|
|
3173
|
+
`${lines.join("\n")}\n` +
|
|
3174
|
+
`The guard consulted a single-class /object_info payload (or a whole-schema ` +
|
|
3175
|
+
`widen that did not land), not the live frontend graph, so it cannot see ` +
|
|
3176
|
+
`sibling producers added earlier in this session. ` +
|
|
3177
|
+
`Retrying panel_add_node will keep failing; panel_refresh_nodes will not ` +
|
|
3178
|
+
`clear it (it re-registers the class, which is what arms the single-class path). ` +
|
|
3179
|
+
`Workaround: copy an existing "${target}" from another open workflow and ` +
|
|
3180
|
+
`paste it here (panel_copy_nodes / panel_paste_nodes), or reload the ComfyUI ` +
|
|
3181
|
+
`browser tab so the page rebuilds its node registry. Do not retry this class_type ` +
|
|
3182
|
+
`until then.`);
|
|
3183
|
+
}
|
|
3184
|
+
async function withLiveSocketProducerNote(res, classType, ctx) {
|
|
3185
|
+
if (!isMissingInstalledOutputsRefusal(res))
|
|
3186
|
+
return res;
|
|
3187
|
+
// #1708 is a Vue-widget miss, not a sibling-producer miss. Querying the
|
|
3188
|
+
// canvas for AUTOCOMPLETE_TEXT_* would only delay the named remedy.
|
|
3189
|
+
if (isLoraManagerAutocompleteRefusal(res))
|
|
3190
|
+
return res;
|
|
3191
|
+
const missing = parseUnprovenSocketTypes(textOfToolResult(res));
|
|
3192
|
+
if (!missing.length)
|
|
3193
|
+
return res;
|
|
3194
|
+
const found = await findLiveSocketProducers(ctx, missing);
|
|
3195
|
+
if (!found)
|
|
3196
|
+
return res;
|
|
3197
|
+
if (!missing.every((t) => (found.get(t)?.length ?? 0) > 0))
|
|
3198
|
+
return res;
|
|
3199
|
+
return appendToolResultText(res, liveSocketProducerNote(classType, found));
|
|
3200
|
+
}
|
|
3201
|
+
async function annotateAddNodeRefusal(res, classType, ctx) {
|
|
3202
|
+
return withFrontendOnlyPanelSkewNote(withLoraManagerAutocompleteNote(await withLiveSocketProducerNote(res, classType, ctx)), classType, ctx);
|
|
3203
|
+
}
|
|
3073
3204
|
const LORA_MANAGER_AUTOCOMPLETE_NOTE = `\n\nThis is NOT a missing pack and NOT a widget that is still loading. ` +
|
|
3074
3205
|
`ComfyUI-LoRA-Manager registers AUTOCOMPLETE_TEXT_* via getCustomWidgets() into ` +
|
|
3075
3206
|
`the frontend widget store; the add-node guard only looks at app.widgets (core types), ` +
|
|
@@ -10234,7 +10365,7 @@ export function buildPanelToolDefs() {
|
|
|
10234
10365
|
},
|
|
10235
10366
|
},
|
|
10236
10367
|
])),
|
|
10237
|
-
def("panel_add_node", "Add a node to the user's OPEN ComfyUI graph by class_type (e.g. 'KSampler', 'CheckpointLoaderSimple'). The user sees it appear live; Ctrl+Z undoes it. Returns the created node's id, slots, and default widget values. Frontend-only virtual types are addable too: 'Note' and 'MarkdownNote' — the supported way to ANNOTATE a workflow with on-canvas instructions (add the node, then put the text in its 'text' widget via panel_set_widget) — plus 'Reroute' and 'PrimitiveNode'. These are LiteGraph-native and never appear in the backend node registry, so they legitimately bypass the backend class_type check. ComfyUI-LoRA-Manager's 'Lora Loader (LoraManager)' (and other AUTOCOMPLETE_TEXT_* nodes) cannot be added: the pack is healthy, but the add-node guard cannot see its Vue autocomplete widget. Use 'LoRA Text Loader (LoraManager)' — same outputs, lora_syntax is a STRING — or core LoraLoader; reload/retry will not clear it. ADD NODES ONE AT A TIME, not as a parallel batch: each add carries a fresh /object_info payload and those register SERIALLY, so N concurrent adds become N sequential refresh cycles. On a large install that outruns the 30s per-command deadline and the later adds time out WHILE STILL QUEUED — they then apply when their turn arrives, leaving nodes you were told had failed (panel#767). Sequential adds each get the refresh to themselves and stay well inside the deadline.", {
|
|
10368
|
+
def("panel_add_node", "Add a node to the user's OPEN ComfyUI graph by class_type (e.g. 'KSampler', 'CheckpointLoaderSimple'). The user sees it appear live; Ctrl+Z undoes it. Returns the created node's id, slots, and default widget values. Frontend-only virtual types are addable too: 'Note' and 'MarkdownNote' — the supported way to ANNOTATE a workflow with on-canvas instructions (add the node, then put the text in its 'text' widget via panel_set_widget) — plus 'Reroute' and 'PrimitiveNode'. These are LiteGraph-native and never appear in the backend node registry, so they legitimately bypass the backend class_type check. ComfyUI-LoRA-Manager's 'Lora Loader (LoraManager)' (and other AUTOCOMPLETE_TEXT_* nodes) cannot be added: the pack is healthy, but the add-node guard cannot see its Vue autocomplete widget. Use 'LoRA Text Loader (LoraManager)' — same outputs, lora_syntax is a STRING — or core LoraLoader; reload/retry will not clear it. A 'no installed node outputs' refusal for a custom link type (SEEDVR2_DIT, SEEDVR2_VAE, …) after sibling producer nodes were just added is the guard looking at a single-class /object_info, not the live graph; retry/refresh will not clear it — copy the node from another workflow (panel_copy_nodes / panel_paste_nodes) or reload the tab. ADD NODES ONE AT A TIME, not as a parallel batch: each add carries a fresh /object_info payload and those register SERIALLY, so N concurrent adds become N sequential refresh cycles. On a large install that outruns the 30s per-command deadline and the later adds time out WHILE STILL QUEUED — they then apply when their turn arrives, leaving nodes you were told had failed (panel#767). Sequential adds each get the refresh to themselves and stay well inside the deadline.", {
|
|
10238
10369
|
class_type: z.string().describe("Exact ComfyUI node class_type to create."),
|
|
10239
10370
|
pos: xy()
|
|
10240
10371
|
.optional()
|
|
@@ -10248,7 +10379,7 @@ export function buildPanelToolDefs() {
|
|
|
10248
10379
|
const add = () => ctx.call({ cmd: "graph_add_node", class_type: args.class_type, pos: args.pos, title: args.title }, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS);
|
|
10249
10380
|
const first = await add();
|
|
10250
10381
|
if (!isStaleNodeSchemaRefusal(first)) {
|
|
10251
|
-
return
|
|
10382
|
+
return annotateAddNodeRefusal(first, args.class_type, ctx);
|
|
10252
10383
|
}
|
|
10253
10384
|
// #1329 — DO THE REFRESH THE REFUSAL ASKS FOR, instead of billing the caller.
|
|
10254
10385
|
//
|
|
@@ -10283,9 +10414,9 @@ export function buildPanelToolDefs() {
|
|
|
10283
10414
|
// the refresh failure alongside so the caller knows the automatic attempt
|
|
10284
10415
|
// happened and why it did not help. No retry here, because the schema really is
|
|
10285
10416
|
// unchanged and a second add really would refuse again.
|
|
10286
|
-
return
|
|
10417
|
+
return annotateAddNodeRefusal(appendToolResultText(first, `\n\n(Tried to clear this automatically: panel_refresh_nodes was dispatched and FAILED, ` +
|
|
10287
10418
|
`so the schema is unchanged and retrying the add will refuse again. ` +
|
|
10288
|
-
`${textOfToolResult(refreshed)})`)
|
|
10419
|
+
`${textOfToolResult(refreshed)})`), args.class_type, ctx);
|
|
10289
10420
|
}
|
|
10290
10421
|
// panel#1518 — TAKE THE RETRY, instead of telling the caller to take it.
|
|
10291
10422
|
//
|
|
@@ -10310,7 +10441,7 @@ export function buildPanelToolDefs() {
|
|
|
10310
10441
|
// refresh left behind.
|
|
10311
10442
|
const second = await add();
|
|
10312
10443
|
if (!isStaleNodeSchemaRefusal(second)) {
|
|
10313
|
-
return
|
|
10444
|
+
return annotateAddNodeRefusal(
|
|
10314
10445
|
// An error from the RETRY, on the path where the caller never asked for a
|
|
10315
10446
|
// retry, must say which attempt it came from — otherwise a bare
|
|
10316
10447
|
// `graph_add_node` timeout reads as the first add's outcome and leaves the
|
|
@@ -10321,12 +10452,12 @@ export function buildPanelToolDefs() {
|
|
|
10321
10452
|
`node schema and created NOTHING, then panel_refresh_nodes was dispatched and did ` +
|
|
10322
10453
|
`NOT answer within its window. The error above is from the second attempt, so at ` +
|
|
10323
10454
|
`most ONE add is unaccounted for here.)`)
|
|
10324
|
-
: second
|
|
10455
|
+
: second, args.class_type, ctx);
|
|
10325
10456
|
}
|
|
10326
10457
|
// Still stale after the refresh: report THAT, because it means the remedy the
|
|
10327
10458
|
// refusal prescribes does not fix this instance and a caller following it by hand
|
|
10328
10459
|
// would loop.
|
|
10329
|
-
return
|
|
10460
|
+
return annotateAddNodeRefusal(appendToolResultText(second, refreshOutranItsAck
|
|
10330
10461
|
? // The refresh was never observed to finish, so "repeating it will not clear
|
|
10331
10462
|
// it" and "reload the tab" would both overclaim. What IS known: it was not
|
|
10332
10463
|
// cancelled, it is still registering the definitions this add needs, and the
|
|
@@ -10342,7 +10473,7 @@ export function buildPanelToolDefs() {
|
|
|
10342
10473
|
`the tab being frozen. ${textOfToolResult(refreshed)})`
|
|
10343
10474
|
: `\n\n(This was already retried ONCE automatically: panel_refresh_nodes reported success ` +
|
|
10344
10475
|
`and the add still refuses, so repeating panel_refresh_nodes will not clear it. ` +
|
|
10345
|
-
`Reload the ComfyUI browser tab, which rebuilds the page's node registry from scratch.)`)
|
|
10476
|
+
`Reload the ComfyUI browser tab, which rebuilds the page's node registry from scratch.)`), args.class_type, ctx);
|
|
10346
10477
|
}),
|
|
10347
10478
|
def("panel_remove_node", "Remove a node (and its connections) from the user's open graph by id. Undoable with Ctrl+Z.", { node_id: nodeId().describe("Node id from panel_graph_outline / panel_query_graph.") }, async (args, ctx) => ctx.call({ cmd: "graph_remove_node", node_id: args.node_id })),
|
|
10348
10479
|
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) => {
|