comfyui-mcp 0.52.39 → 0.52.41
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/comfyui/client.js +20 -2
- package/dist/comfyui/client.js.map +1 -1
- package/dist/comfyui/fetch.js +99 -14
- package/dist/comfyui/fetch.js.map +1 -1
- package/dist/orchestrator/panel-agent.js +22 -4
- package/dist/orchestrator/panel-agent.js.map +1 -1
- package/dist/orchestrator/panel-tools.js +174 -28
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/orchestrator/run-completion-journal.js +127 -28
- package/dist/orchestrator/run-completion-journal.js.map +1 -1
- package/package.json +1 -1
|
@@ -1048,6 +1048,28 @@ function sessionRouteIsLive(ctx) {
|
|
|
1048
1048
|
return false;
|
|
1049
1049
|
}
|
|
1050
1050
|
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Snapshot of whether this tab can fence a graph WRITE right now.
|
|
1053
|
+
*
|
|
1054
|
+
* `known:false` (resolver threw, ctx threw) is `undefined`, not `false`: the
|
|
1055
|
+
* boolean probe fails closed, and rendering that as "your panel lacks the write
|
|
1056
|
+
* fence" is a claim built from our own failure to look.
|
|
1057
|
+
*/
|
|
1058
|
+
function readTabMutationCapability(ctx) {
|
|
1059
|
+
try {
|
|
1060
|
+
if (ctx.tabGraphMutationCapability) {
|
|
1061
|
+
const cap = ctx.tabGraphMutationCapability();
|
|
1062
|
+
return {
|
|
1063
|
+
canMutateNow: cap.known ? cap.canMutate : undefined,
|
|
1064
|
+
refusalCause: cap.known && !cap.canMutate ? cap.because : undefined,
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
return { canMutateNow: ctx.tabCanMutateGraph?.(), refusalCause: undefined };
|
|
1068
|
+
}
|
|
1069
|
+
catch {
|
|
1070
|
+
return { canMutateNow: undefined, refusalCause: undefined };
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1051
1073
|
/**
|
|
1052
1074
|
* #1331 — the bridge refused a mutation because the active workflow has no identity to
|
|
1053
1075
|
* fence against (ui-bridge's `no trusted identity` branch).
|
|
@@ -3070,6 +3092,137 @@ function isLoraManagerAutocompleteRefusal(res) {
|
|
|
3070
3092
|
return (/had no widget after .+ waiting for node extensions to register/i.test(text) &&
|
|
3071
3093
|
/AUTOCOMPLETE_TEXT_/i.test(text));
|
|
3072
3094
|
}
|
|
3095
|
+
/**
|
|
3096
|
+
* #1944 — the panel's add-node socket proof read "which datatypes does some
|
|
3097
|
+
* installed node output?" off a single-class `/object_info/<class_type>`
|
|
3098
|
+
* payload (or a whole-schema widen that did not land). Custom link types a
|
|
3099
|
+
* SIBLING node produces — SeedVR2LoadDiTModel → SEEDVR2_DIT — then look
|
|
3100
|
+
* unproven, and the refusal claims "no installed node outputs X" while that
|
|
3101
|
+
* very node sits on the canvas. Retrying the add re-runs the same proof;
|
|
3102
|
+
* panel_refresh_nodes re-registers the class, which is what arms the
|
|
3103
|
+
* single-class path. The live graph is the authority the guard did not consult.
|
|
3104
|
+
*/
|
|
3105
|
+
function isMissingInstalledOutputsRefusal(res) {
|
|
3106
|
+
if (!res.isError)
|
|
3107
|
+
return false;
|
|
3108
|
+
const text = textOfToolResult(res);
|
|
3109
|
+
return (/had no widget after .+ waiting for node extensions to register/i.test(text) &&
|
|
3110
|
+
/no installed node outputs/i.test(text));
|
|
3111
|
+
}
|
|
3112
|
+
/** Types the panel named as unproven in a #1944-shaped refusal. */
|
|
3113
|
+
function parseUnprovenSocketTypes(text) {
|
|
3114
|
+
const types = [];
|
|
3115
|
+
const add = (raw) => {
|
|
3116
|
+
const v = raw.trim();
|
|
3117
|
+
if (v && !types.includes(v))
|
|
3118
|
+
types.push(v);
|
|
3119
|
+
};
|
|
3120
|
+
for (const re of [
|
|
3121
|
+
/no installed node outputs "([^"]+)"/gi,
|
|
3122
|
+
/declared type "([^"]+)": no installed node outputs/gi,
|
|
3123
|
+
]) {
|
|
3124
|
+
re.lastIndex = 0;
|
|
3125
|
+
let m;
|
|
3126
|
+
while ((m = re.exec(text)))
|
|
3127
|
+
add(m[1]);
|
|
3128
|
+
}
|
|
3129
|
+
return types;
|
|
3130
|
+
}
|
|
3131
|
+
function nodeOutputDeclaresType(node, socketType) {
|
|
3132
|
+
const outputs = node.outputs;
|
|
3133
|
+
if (Array.isArray(outputs)) {
|
|
3134
|
+
for (const out of outputs) {
|
|
3135
|
+
if (!out || typeof out !== "object" || Array.isArray(out))
|
|
3136
|
+
continue;
|
|
3137
|
+
const t = out.type;
|
|
3138
|
+
if (typeof t !== "string" || !t)
|
|
3139
|
+
continue;
|
|
3140
|
+
if (t === socketType)
|
|
3141
|
+
return true;
|
|
3142
|
+
if (t.split(",").map((part) => part.trim()).includes(socketType))
|
|
3143
|
+
return true;
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
const matchedOn = node.matched_on;
|
|
3147
|
+
if (!Array.isArray(matchedOn))
|
|
3148
|
+
return false;
|
|
3149
|
+
const needle = `(${socketType.toLowerCase()})`;
|
|
3150
|
+
return matchedOn.some((entry) => typeof entry === "string" && entry.toLowerCase().includes(needle));
|
|
3151
|
+
}
|
|
3152
|
+
/**
|
|
3153
|
+
* Ask the live canvas whether each unproven type is actually produced there.
|
|
3154
|
+
* Returns null when a probe fails — an unreadable graph is not evidence the
|
|
3155
|
+
* producers are present, so the original refusal stays the last word.
|
|
3156
|
+
*/
|
|
3157
|
+
async function findLiveSocketProducers(ctx, types) {
|
|
3158
|
+
const found = new Map();
|
|
3159
|
+
for (const socketType of types) {
|
|
3160
|
+
const probe = await ctx.call({ cmd: "graph_find_nodes", output: socketType, limit: 5 }, 8000);
|
|
3161
|
+
if (probe.isError)
|
|
3162
|
+
return null;
|
|
3163
|
+
const payload = parseToolResultJson(probe);
|
|
3164
|
+
const matches = Array.isArray(payload?.matches) ? payload.matches : [];
|
|
3165
|
+
const producers = [];
|
|
3166
|
+
for (const row of matches) {
|
|
3167
|
+
if (!row || typeof row !== "object" || Array.isArray(row))
|
|
3168
|
+
continue;
|
|
3169
|
+
const rec = row;
|
|
3170
|
+
if (!nodeOutputDeclaresType(rec, socketType))
|
|
3171
|
+
continue;
|
|
3172
|
+
const id = rec.id;
|
|
3173
|
+
const type = rec.type ?? rec.class_type;
|
|
3174
|
+
producers.push({
|
|
3175
|
+
id: id == null ? "?" : String(id),
|
|
3176
|
+
type: typeof type === "string" && type ? type : "unknown",
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
3179
|
+
found.set(socketType, producers);
|
|
3180
|
+
}
|
|
3181
|
+
return found;
|
|
3182
|
+
}
|
|
3183
|
+
function liveSocketProducerNote(classType, found) {
|
|
3184
|
+
const target = typeof classType === "string" && classType ? classType : "this node";
|
|
3185
|
+
const lines = [...found.entries()].map(([socket, nodes]) => {
|
|
3186
|
+
const who = nodes
|
|
3187
|
+
.slice(0, 3)
|
|
3188
|
+
.map((n) => `${n.type} #${n.id}`)
|
|
3189
|
+
.join(", ");
|
|
3190
|
+
return ` - ${socket}: ${who}`;
|
|
3191
|
+
});
|
|
3192
|
+
return (`\n\nThis is NOT a missing pack and NOT a widget that is still loading. ` +
|
|
3193
|
+
`The live canvas ALREADY has nodes that output every type the add-node guard ` +
|
|
3194
|
+
`claimed was missing:\n` +
|
|
3195
|
+
`${lines.join("\n")}\n` +
|
|
3196
|
+
`The guard consulted a single-class /object_info payload (or a whole-schema ` +
|
|
3197
|
+
`widen that did not land), not the live frontend graph, so it cannot see ` +
|
|
3198
|
+
`sibling producers added earlier in this session. ` +
|
|
3199
|
+
`Retrying panel_add_node will keep failing; panel_refresh_nodes will not ` +
|
|
3200
|
+
`clear it (it re-registers the class, which is what arms the single-class path). ` +
|
|
3201
|
+
`Workaround: copy an existing "${target}" from another open workflow and ` +
|
|
3202
|
+
`paste it here (panel_copy_nodes / panel_paste_nodes), or reload the ComfyUI ` +
|
|
3203
|
+
`browser tab so the page rebuilds its node registry. Do not retry this class_type ` +
|
|
3204
|
+
`until then.`);
|
|
3205
|
+
}
|
|
3206
|
+
async function withLiveSocketProducerNote(res, classType, ctx) {
|
|
3207
|
+
if (!isMissingInstalledOutputsRefusal(res))
|
|
3208
|
+
return res;
|
|
3209
|
+
// #1708 is a Vue-widget miss, not a sibling-producer miss. Querying the
|
|
3210
|
+
// canvas for AUTOCOMPLETE_TEXT_* would only delay the named remedy.
|
|
3211
|
+
if (isLoraManagerAutocompleteRefusal(res))
|
|
3212
|
+
return res;
|
|
3213
|
+
const missing = parseUnprovenSocketTypes(textOfToolResult(res));
|
|
3214
|
+
if (!missing.length)
|
|
3215
|
+
return res;
|
|
3216
|
+
const found = await findLiveSocketProducers(ctx, missing);
|
|
3217
|
+
if (!found)
|
|
3218
|
+
return res;
|
|
3219
|
+
if (!missing.every((t) => (found.get(t)?.length ?? 0) > 0))
|
|
3220
|
+
return res;
|
|
3221
|
+
return appendToolResultText(res, liveSocketProducerNote(classType, found));
|
|
3222
|
+
}
|
|
3223
|
+
async function annotateAddNodeRefusal(res, classType, ctx) {
|
|
3224
|
+
return withFrontendOnlyPanelSkewNote(withLoraManagerAutocompleteNote(await withLiveSocketProducerNote(res, classType, ctx)), classType, ctx);
|
|
3225
|
+
}
|
|
3073
3226
|
const LORA_MANAGER_AUTOCOMPLETE_NOTE = `\n\nThis is NOT a missing pack and NOT a widget that is still loading. ` +
|
|
3074
3227
|
`ComfyUI-LoRA-Manager registers AUTOCOMPLETE_TEXT_* via getCustomWidgets() into ` +
|
|
3075
3228
|
`the frontend widget store; the add-node guard only looks at app.widgets (core types), ` +
|
|
@@ -10234,7 +10387,7 @@ export function buildPanelToolDefs() {
|
|
|
10234
10387
|
},
|
|
10235
10388
|
},
|
|
10236
10389
|
])),
|
|
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.", {
|
|
10390
|
+
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
10391
|
class_type: z.string().describe("Exact ComfyUI node class_type to create."),
|
|
10239
10392
|
pos: xy()
|
|
10240
10393
|
.optional()
|
|
@@ -10248,7 +10401,7 @@ export function buildPanelToolDefs() {
|
|
|
10248
10401
|
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
10402
|
const first = await add();
|
|
10250
10403
|
if (!isStaleNodeSchemaRefusal(first)) {
|
|
10251
|
-
return
|
|
10404
|
+
return annotateAddNodeRefusal(first, args.class_type, ctx);
|
|
10252
10405
|
}
|
|
10253
10406
|
// #1329 — DO THE REFRESH THE REFUSAL ASKS FOR, instead of billing the caller.
|
|
10254
10407
|
//
|
|
@@ -10283,9 +10436,9 @@ export function buildPanelToolDefs() {
|
|
|
10283
10436
|
// the refresh failure alongside so the caller knows the automatic attempt
|
|
10284
10437
|
// happened and why it did not help. No retry here, because the schema really is
|
|
10285
10438
|
// unchanged and a second add really would refuse again.
|
|
10286
|
-
return
|
|
10439
|
+
return annotateAddNodeRefusal(appendToolResultText(first, `\n\n(Tried to clear this automatically: panel_refresh_nodes was dispatched and FAILED, ` +
|
|
10287
10440
|
`so the schema is unchanged and retrying the add will refuse again. ` +
|
|
10288
|
-
`${textOfToolResult(refreshed)})`)
|
|
10441
|
+
`${textOfToolResult(refreshed)})`), args.class_type, ctx);
|
|
10289
10442
|
}
|
|
10290
10443
|
// panel#1518 — TAKE THE RETRY, instead of telling the caller to take it.
|
|
10291
10444
|
//
|
|
@@ -10310,7 +10463,7 @@ export function buildPanelToolDefs() {
|
|
|
10310
10463
|
// refresh left behind.
|
|
10311
10464
|
const second = await add();
|
|
10312
10465
|
if (!isStaleNodeSchemaRefusal(second)) {
|
|
10313
|
-
return
|
|
10466
|
+
return annotateAddNodeRefusal(
|
|
10314
10467
|
// An error from the RETRY, on the path where the caller never asked for a
|
|
10315
10468
|
// retry, must say which attempt it came from — otherwise a bare
|
|
10316
10469
|
// `graph_add_node` timeout reads as the first add's outcome and leaves the
|
|
@@ -10321,12 +10474,12 @@ export function buildPanelToolDefs() {
|
|
|
10321
10474
|
`node schema and created NOTHING, then panel_refresh_nodes was dispatched and did ` +
|
|
10322
10475
|
`NOT answer within its window. The error above is from the second attempt, so at ` +
|
|
10323
10476
|
`most ONE add is unaccounted for here.)`)
|
|
10324
|
-
: second
|
|
10477
|
+
: second, args.class_type, ctx);
|
|
10325
10478
|
}
|
|
10326
10479
|
// Still stale after the refresh: report THAT, because it means the remedy the
|
|
10327
10480
|
// refusal prescribes does not fix this instance and a caller following it by hand
|
|
10328
10481
|
// would loop.
|
|
10329
|
-
return
|
|
10482
|
+
return annotateAddNodeRefusal(appendToolResultText(second, refreshOutranItsAck
|
|
10330
10483
|
? // The refresh was never observed to finish, so "repeating it will not clear
|
|
10331
10484
|
// it" and "reload the tab" would both overclaim. What IS known: it was not
|
|
10332
10485
|
// cancelled, it is still registering the definitions this add needs, and the
|
|
@@ -10342,7 +10495,7 @@ export function buildPanelToolDefs() {
|
|
|
10342
10495
|
`the tab being frozen. ${textOfToolResult(refreshed)})`
|
|
10343
10496
|
: `\n\n(This was already retried ONCE automatically: panel_refresh_nodes reported success ` +
|
|
10344
10497
|
`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.)`)
|
|
10498
|
+
`Reload the ComfyUI browser tab, which rebuilds the page's node registry from scratch.)`), args.class_type, ctx);
|
|
10346
10499
|
}),
|
|
10347
10500
|
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
10501
|
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) => {
|
|
@@ -12732,26 +12885,12 @@ export function buildPanelToolDefs() {
|
|
|
12732
12885
|
// unreadable capability would be RENDERED as "your panel lacks the write
|
|
12733
12886
|
// fence" — a claim about the panel built from our own failure to look
|
|
12734
12887
|
// (codex gate). Only an OBSERVED negative may say that.
|
|
12735
|
-
|
|
12736
|
-
|
|
12737
|
-
|
|
12738
|
-
|
|
12739
|
-
|
|
12740
|
-
|
|
12741
|
-
if (cap.known && !cap.canMutate)
|
|
12742
|
-
refusalCause = cap.because;
|
|
12743
|
-
}
|
|
12744
|
-
else {
|
|
12745
|
-
// The legacy boolean probe cannot say WHY, and guessing would put us
|
|
12746
|
-
// back where the tri-state started. Leave the cause undefined so the
|
|
12747
|
-
// narration falls back to the generic wording rather than inventing one.
|
|
12748
|
-
canMutateNow = ctx.tabCanMutateGraph?.();
|
|
12749
|
-
}
|
|
12750
|
-
}
|
|
12751
|
-
catch {
|
|
12752
|
-
canMutateNow = undefined; // a guard that can throw is not a guard
|
|
12753
|
-
refusalCause = undefined;
|
|
12754
|
-
}
|
|
12888
|
+
//
|
|
12889
|
+
// This snapshot is taken BEFORE the settling read. After a restart the
|
|
12890
|
+
// tab can still be reconnecting, so the honest answer here is often
|
|
12891
|
+
// unknown (`undefined`) even though a moment later `graph_query` will
|
|
12892
|
+
// round-trip. #1696 re-reads after that probe, on the tab that answered.
|
|
12893
|
+
let { canMutateNow, refusalCause } = readTabMutationCapability(ctx);
|
|
12755
12894
|
// #1043 (codex review) — NO version note on THIS path, deliberately.
|
|
12756
12895
|
//
|
|
12757
12896
|
// The note claims that on 0.11.45+ "the command that re-pointed the canvas
|
|
@@ -12867,6 +13006,13 @@ export function buildPanelToolDefs() {
|
|
|
12867
13006
|
}
|
|
12868
13007
|
if (probeOk) {
|
|
12869
13008
|
probePassed = true;
|
|
13009
|
+
// #1696 — the snapshot above is taken BEFORE this read, while the tab
|
|
13010
|
+
// may still be reconnecting. `canMutateNow` is then undefined (the
|
|
13011
|
+
// probe could not inspect the tab) even though this command just
|
|
13012
|
+
// proved the socket and fence are live. Re-read on the tab that
|
|
13013
|
+
// answered; a still-unknown or still-refused write capability keeps
|
|
13014
|
+
// the failure below. Treating unknown as true is not this fix.
|
|
13015
|
+
({ canMutateNow, refusalCause } = readTabMutationCapability(ctx));
|
|
12870
13016
|
settledNote =
|
|
12871
13017
|
// ONE OBSERVATION, STATED AS ONE (codex r2). A passing graph_query proves that
|
|
12872
13018
|
// read passed this fence just now — not that every command will, and not that
|