comfyui-mcp 0.50.111 → 0.50.112
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.
|
@@ -1104,58 +1104,212 @@ export function restartTimeoutFallbackAdvice({ headlessBase, panelBase, observed
|
|
|
1104
1104
|
"could not confirm which one this panel is running inside.");
|
|
1105
1105
|
}
|
|
1106
1106
|
/**
|
|
1107
|
-
*
|
|
1107
|
+
* Node definitions from the ComfyUI the PANEL is connected to (#1359 / #1006).
|
|
1108
1108
|
*
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
1111
|
-
*
|
|
1112
|
-
*
|
|
1109
|
+
* The panel has served these since 0.13.0 and the orchestrator never asked. Everything
|
|
1110
|
+
* that pairs a panel-captured graph with definitions was reading the graph from the tab
|
|
1111
|
+
* and the schema from COMFYUI_URL — one machine locally, two for a remote panel, and no
|
|
1112
|
+
* way at all to convert a live canvas in a tunnel or loopback-only topology, where the
|
|
1113
|
+
* browser is the only thing that can reach that ComfyUI.
|
|
1113
1114
|
*
|
|
1114
|
-
*
|
|
1115
|
-
*
|
|
1115
|
+
* FAIL CLOSED, DELIBERATELY, AND ALL THE WAY THROUGH. Every failure here returns a
|
|
1116
|
+
* message instead of falling back to `getObjectInfo()`. A fallback is the tempting move
|
|
1117
|
+
* and it is the dangerous one: both hosts can answer, and if they disagree the caller
|
|
1118
|
+
* gets a confident workflow converted against the wrong ComfyUI's schema — wrong widget
|
|
1119
|
+
* order, wrong input names, silently. That is worse than the ECONNREFUSED this issue was
|
|
1120
|
+
* filed about, which at least announced itself. The panel makes the same choice on its
|
|
1121
|
+
* side and says so in its own comment.
|
|
1116
1122
|
*
|
|
1117
|
-
*
|
|
1118
|
-
*
|
|
1119
|
-
*
|
|
1120
|
-
*
|
|
1121
|
-
* /object_info, a protocol change tracked separately. It makes the existing failure
|
|
1122
|
-
* diagnosable, and names the workaround that works today.
|
|
1123
|
-
*
|
|
1124
|
-
* The panel's origin is the SERVER-OBSERVED handshake Origin where available: the
|
|
1125
|
-
* browser sets it on the WS upgrade and page JS cannot forge it. It is only being
|
|
1126
|
-
* PRINTED here, never fetched from, so an unreadable one costs a detail rather than a
|
|
1127
|
-
* wrong conclusion.
|
|
1123
|
+
* An OLD PANEL is handled upstream and authoritatively: BRIDGE_CMD_MIN_PANEL_VERSION
|
|
1124
|
+
* carries `graph_get_object_info: "0.13.0"`, so a panel predating the command is refused
|
|
1125
|
+
* by the version gate with the version it needs — rather than answering the raw
|
|
1126
|
+
* `Unknown command "graph_get_object_info"`, which reads like a broken ComfyUI.
|
|
1128
1127
|
*/
|
|
1129
|
-
function
|
|
1130
|
-
|
|
1131
|
-
|
|
1128
|
+
async function panelObjectInfo(ctx,
|
|
1129
|
+
/** The node types on the canvas being converted, so the reply can be judged against the
|
|
1130
|
+
* graph it is supposed to define rather than against its own shape. */
|
|
1131
|
+
neededTypes = []) {
|
|
1132
|
+
let reply;
|
|
1132
1133
|
try {
|
|
1133
|
-
|
|
1134
|
+
// NO `if_none_match`, DELIBERATELY, EVEN THOUGH THE PAYLOAD IS LARGE.
|
|
1135
|
+
//
|
|
1136
|
+
// The panel offers a fingerprint cache and this caller declines it. Its fingerprint is
|
|
1137
|
+
// computed over the SORTED TYPE NAMES and nothing else (object-info-fingerprint.js), so
|
|
1138
|
+
// `unchanged: true` establishes only that the same node types exist. `convertUiToApi`
|
|
1139
|
+
// maps `widgets_values` POSITIONALLY onto each def's declared input order — so a
|
|
1140
|
+
// renamed widget, a reordered input, or an edited combo list changes the conversion
|
|
1141
|
+
// while leaving the type set, and therefore the fingerprint, identical.
|
|
1142
|
+
//
|
|
1143
|
+
// Reusing a cached map on `unchanged` would convert this canvas against a schema that
|
|
1144
|
+
// has since moved and return a confidently wrong workflow — the same failure this whole
|
|
1145
|
+
// change exists to prevent, bought back for a saved download. The panel's own reply
|
|
1146
|
+
// says as much: "That does not establish that individual definitions are identical …
|
|
1147
|
+
// Re-read without if_none_match if you need those."
|
|
1148
|
+
//
|
|
1149
|
+
// So the cost is accepted: a strip is user-initiated and infrequent, and correctness
|
|
1150
|
+
// here is worth more than the transfer.
|
|
1151
|
+
//
|
|
1152
|
+
// The panel fetches a full /object_info here — megabytes on a large install — so it
|
|
1153
|
+
// needs the bounded refresh budget, not the default ack. A false timeout would read as
|
|
1154
|
+
// "the panel cannot serve definitions" for a panel that served them.
|
|
1155
|
+
reply = await ctx.bridge.send({ cmd: "graph_get_object_info" }, { tabId: ctx.tabId, timeoutMs: OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS });
|
|
1134
1156
|
}
|
|
1135
|
-
catch {
|
|
1136
|
-
|
|
1157
|
+
catch (err) {
|
|
1158
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
1159
|
+
return {
|
|
1160
|
+
ok: false,
|
|
1161
|
+
message: `Could not read node definitions from the panel's own ComfyUI: ${raw}
|
|
1162
|
+
|
|
1163
|
+
` +
|
|
1164
|
+
`The live canvas is converted using definitions from the ComfyUI the PANEL is ` +
|
|
1165
|
+
`connected to, not from COMFYUI_URL — those are different machines whenever the ` +
|
|
1166
|
+
`panel is remote, and only the browser can reach a tunnelled or loopback-only host. ` +
|
|
1167
|
+
`Or pass an explicit \`pack\`/\`path\`/\`graph\` source, which is read from ` +
|
|
1168
|
+
`COMFYUI_URL by design.
|
|
1169
|
+
|
|
1170
|
+
No fallback to COMFYUI_URL is attempted on purpose ` +
|
|
1171
|
+
`(#1359): both hosts can answer, and converting this canvas against a different ` +
|
|
1172
|
+
`ComfyUI's schema would return a confidently wrong workflow instead of an error.`,
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
const r = (reply ?? {});
|
|
1176
|
+
if (r.ok === false) {
|
|
1177
|
+
return {
|
|
1178
|
+
ok: false,
|
|
1179
|
+
message: `The panel could not obtain node definitions from its own ComfyUI` +
|
|
1180
|
+
(r.served_by ? ` (${r.served_by})` : "") +
|
|
1181
|
+
`. ${r.detail ?? ""}
|
|
1182
|
+
|
|
1183
|
+
The conversion is refused rather than retried against ` +
|
|
1184
|
+
`COMFYUI_URL, which would convert this canvas against a different server's schema.`,
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
if (!r.object_info || typeof r.object_info !== "object") {
|
|
1188
|
+
// `unchanged: true` lands here too, and that is correct: this caller sends no
|
|
1189
|
+
// if_none_match, so a payload-free reply means the contract was not met.
|
|
1190
|
+
return {
|
|
1191
|
+
ok: false,
|
|
1192
|
+
message: `The panel replied without node definitions` +
|
|
1193
|
+
(r.served_by ? ` (it reported serving from ${r.served_by})` : "") +
|
|
1194
|
+
`. Nothing was converted — a partial or absent schema produces a wrong workflow, ` +
|
|
1195
|
+
`so this refuses instead of guessing.`,
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
// AN EMPTY MAP IS NOT A SCHEMA, and accepting one was the hole in the first version of
|
|
1199
|
+
// this "fail closed" path. `{ok: true, object_info: {}}` passed every check above, and
|
|
1200
|
+
// convertUiToApi SKIPS every node whose type it cannot find — so the caller got a
|
|
1201
|
+
// successful reply containing an empty or gutted workflow, with no error anywhere. A
|
|
1202
|
+
// silent wrong answer, which is the outcome this whole change exists to prevent, reached
|
|
1203
|
+
// through the success branch instead of the failure one.
|
|
1204
|
+
//
|
|
1205
|
+
// A running ComfyUI always defines core nodes, so zero entries cannot be a true schema;
|
|
1206
|
+
// it is a regressed panel, a proxy rewriting the body, or an error page. Nothing weaker
|
|
1207
|
+
// is asserted here — a map that is merely MISSING some of this graph's types is a real
|
|
1208
|
+
// and legitimate case (an uninstalled custom node), and convertUiToApi already reports
|
|
1209
|
+
// those as warnings rather than pretending they converted.
|
|
1210
|
+
if (Object.keys(r.object_info).length === 0) {
|
|
1211
|
+
return {
|
|
1212
|
+
ok: false,
|
|
1213
|
+
message: `The panel returned an EMPTY node-definition map` +
|
|
1214
|
+
(r.served_by ? ` from ${r.served_by}` : "") +
|
|
1215
|
+
`. A running ComfyUI always defines its core nodes, so this is a regressed panel, a ` +
|
|
1216
|
+
`proxy rewriting the response, or an error page — not a real schema. Converting ` +
|
|
1217
|
+
`against it would silently drop every node and hand back an empty workflow that ` +
|
|
1218
|
+
`looks like a success, so nothing was converted.`,
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
// …and "non-empty" is not the same as "a schema" (codex, round 2). A proxy or a backend
|
|
1222
|
+
// that answers 200 with `{"error": "..."}` produces a map with ONE key, which sailed
|
|
1223
|
+
// through a zero-length check and was handed to the converter — which then skips every
|
|
1224
|
+
// node it cannot find and returns a successful, empty workflow. The same silent loss,
|
|
1225
|
+
// one key up from the case I had just fixed.
|
|
1226
|
+
//
|
|
1227
|
+
// So the test is STRUCTURAL: at least one entry that actually looks like a node
|
|
1228
|
+
// definition (an object carrying `input` or `output`, which every real /object_info entry
|
|
1229
|
+
// does). Deliberately not "every entry" — a single malformed record among thousands is a
|
|
1230
|
+
// pack's problem and the converter reports it per node, whereas requiring perfection here
|
|
1231
|
+
// would refuse a working install over one bad custom node.
|
|
1232
|
+
//
|
|
1233
|
+
// AND THE TEST HAS TO BE ABOUT THIS GRAPH (codex, round 3). "at least one entry that
|
|
1234
|
+
// looks like a definition" is satisfied by a single unrelated record — `{meta: {input:
|
|
1235
|
+
// {}}}` passes — while none of the types this canvas actually uses are present, and the
|
|
1236
|
+
// converter then skips every node and returns the same empty workflow. A structural
|
|
1237
|
+
// decoy is still a decoy.
|
|
1238
|
+
//
|
|
1239
|
+
// So the question asked is the one that matters for a conversion: does this map define
|
|
1240
|
+
// ANY of the node types on the canvas? Zero coverage of a non-empty graph is not a
|
|
1241
|
+
// schema for it, whatever else the payload contains.
|
|
1242
|
+
const looksLikeNodeDef = (v) => !!v && typeof v === "object" && ("input" in v || "output" in v);
|
|
1243
|
+
if (!Object.values(r.object_info).some(looksLikeNodeDef)) {
|
|
1244
|
+
return {
|
|
1245
|
+
ok: false,
|
|
1246
|
+
message: `The panel returned something that is not a node-definition map` +
|
|
1247
|
+
(r.served_by ? ` from ${r.served_by}` : "") +
|
|
1248
|
+
` — ${Object.keys(r.object_info).length} key(s), none of which look like a node ` +
|
|
1249
|
+
`definition. That is characteristic of a proxy or backend answering 200 with an ` +
|
|
1250
|
+
`error body. Converting against it would silently drop every node and hand back an ` +
|
|
1251
|
+
`empty workflow that reads as success, so nothing was converted.`,
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
// ZERO COVERAGE OF THIS GRAPH is the decisive test, and the one a structural check
|
|
1255
|
+
// cannot make on its own. A payload can be well-formed, non-empty, and about something
|
|
1256
|
+
// else entirely — at which point the converter skips every node and returns an empty
|
|
1257
|
+
// workflow that reads as a success.
|
|
1258
|
+
//
|
|
1259
|
+
// Only a total miss refuses. PARTIAL coverage converts and warns, and that is not a
|
|
1260
|
+
// judgement call — MEASURED against this machine's live /object_info and three real pack
|
|
1261
|
+
// workflows:
|
|
1262
|
+
//
|
|
1263
|
+
// anima 52 types, 36 covered, 16 missing
|
|
1264
|
+
// anima-img2img 39 types, 33 covered, 6 missing
|
|
1265
|
+
// krea2-identity 14 types, 13 covered, 1 missing
|
|
1266
|
+
//
|
|
1267
|
+
// Every real workflow has misses, and all of them are legitimate: frontend-only virtual
|
|
1268
|
+
// nodes (Note, GetNode, SetNode, "Label (rgthree)"), UUID-typed SUBGRAPH nodes, and
|
|
1269
|
+
// uninstalled packs. So "refuse if ANY type is missing" would refuse ALL THREE — the
|
|
1270
|
+
// over-broad direction is not hypothetical here, it is the default outcome. Zero coverage
|
|
1271
|
+
// never occurred, which is what makes it a usable signal for "this payload is not about
|
|
1272
|
+
// this canvas".
|
|
1273
|
+
//
|
|
1274
|
+
// That measurement also settles the direction that would have been catastrophic:
|
|
1275
|
+
// `collectNodeTypes` does return the same strings that appear as /object_info KEYS. If it
|
|
1276
|
+
// did not, `t in object_info` would always be false and this would refuse EVERY
|
|
1277
|
+
// live-canvas strip.
|
|
1278
|
+
if (neededTypes.length > 0) {
|
|
1279
|
+
const covered = neededTypes.filter((t) => t in r.object_info);
|
|
1280
|
+
if (covered.length === 0) {
|
|
1281
|
+
return {
|
|
1282
|
+
ok: false,
|
|
1283
|
+
message: `The panel returned node definitions that do not describe this canvas` +
|
|
1284
|
+
(r.served_by ? ` (served from ${r.served_by})` : "") +
|
|
1285
|
+
`: none of its ${neededTypes.length} node type(s) — e.g. ${neededTypes.slice(0, 3).join(", ")} — ` +
|
|
1286
|
+
`appear among the ${Object.keys(r.object_info).length} definition(s) returned. ` +
|
|
1287
|
+
`Converting against it would drop every node and hand back an empty workflow that ` +
|
|
1288
|
+
`reads as success, so nothing was converted.`,
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1137
1291
|
}
|
|
1292
|
+
return { ok: true, objectInfo: r.object_info };
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* #1359 — an /object_info failure that names WHICH host it asked.
|
|
1296
|
+
*
|
|
1297
|
+
* ONLY pack/path/inline sources reach this now. The live-canvas branch it used to carry —
|
|
1298
|
+
* "THE GRAPH AND ITS NODE DEFINITIONS CAME FROM DIFFERENT PLACES", with a WORKAROUND
|
|
1299
|
+
* telling the user to repoint COMFYUI_URL — described a split that no longer exists: the
|
|
1300
|
+
* live canvas takes its definitions from the panel that supplied the graph. That message
|
|
1301
|
+
* was the best available answer while the split stood, and keeping it would now be a
|
|
1302
|
+
* confident explanation of a situation the code cannot produce.
|
|
1303
|
+
*
|
|
1304
|
+
* For a pack/path/inline source COMFYUI_URL genuinely IS the right authority, so the bare
|
|
1305
|
+
* failure is already about the host the caller asked for. Say only what is true.
|
|
1306
|
+
*/
|
|
1307
|
+
function objectInfoHostMismatchMessage(err) {
|
|
1308
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
1138
1309
|
const configured = getComfyUIBaseUrl();
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
return `${raw}\n\nNode definitions are read from COMFYUI_URL (${configured}) for a pack/path/inline source. That host did not answer /object_info.`;
|
|
1143
|
-
}
|
|
1144
|
-
const differs = panelOrigin != null && configured != null && !sameHttpBase(panelOrigin, configured);
|
|
1145
|
-
return (`${raw}\n\nTHE GRAPH AND ITS NODE DEFINITIONS CAME FROM DIFFERENT PLACES. The workflow was ` +
|
|
1146
|
-
`captured from the connected panel` +
|
|
1147
|
-
(panelOrigin ? ` (ComfyUI at ${panelOrigin})` : "") +
|
|
1148
|
-
`, but node definitions are fetched over COMFYUI_URL (${configured}) — and that is the ` +
|
|
1149
|
-
`request that failed` +
|
|
1150
|
-
(differs
|
|
1151
|
-
? `. Those are two different hosts, which is why this could not work: the orchestrator ` +
|
|
1152
|
-
`has no route to the panel's ComfyUI for /object_info.`
|
|
1153
|
-
: `.`) +
|
|
1154
|
-
`\n\nWORKAROUND: point COMFYUI_URL at the same ComfyUI the panel is connected to` +
|
|
1155
|
-
(panelOrigin ? ` (${panelOrigin})` : "") +
|
|
1156
|
-
` and retry, or pass an explicit \`graph\`/\`pack\`/\`path\` source instead of the live ` +
|
|
1157
|
-
`canvas. This is a known split of authority (#1359): stripping the LIVE canvas needs ` +
|
|
1158
|
-
`the definitions to come from the panel's own ComfyUI, which needs a panel-side change.`);
|
|
1310
|
+
return `${raw}
|
|
1311
|
+
|
|
1312
|
+
Node definitions are read from COMFYUI_URL (${configured}) for a pack/path/inline source. That host did not answer /object_info.`;
|
|
1159
1313
|
}
|
|
1160
1314
|
function captureRebootHealthBase(ctx) {
|
|
1161
1315
|
if (isCloudMode() || isRemoteMode())
|
|
@@ -6918,16 +7072,54 @@ export function buildPanelToolDefs() {
|
|
|
6918
7072
|
// deliberately tied to COMFYUI_URL, so its definitions belong there.
|
|
6919
7073
|
const liveCanvasSource = args.pack == null && args.path == null && args.graph == null;
|
|
6920
7074
|
let bulk;
|
|
6921
|
-
|
|
6922
|
-
|
|
7075
|
+
if (liveCanvasSource) {
|
|
7076
|
+
// THE DEFINITIONS NOW COME FROM THE SAME PLACE AS THE GRAPH (#1359).
|
|
7077
|
+
//
|
|
7078
|
+
// The panel has served its own /object_info since 0.13.0 (#1006) and nothing
|
|
7079
|
+
// here called it. Asking the browser is not a workaround for the remote case —
|
|
7080
|
+
// it is the only correct source for ANY case, because the tab that drew this
|
|
7081
|
+
// canvas is by definition able to reach the ComfyUI that defines its nodes. In a
|
|
7082
|
+
// tunnel or loopback-only topology the browser is the sole thing that can.
|
|
7083
|
+
//
|
|
7084
|
+
// FAIL CLOSED. If the panel cannot serve definitions we surface that; we do NOT
|
|
7085
|
+
// fall back to COMFYUI_URL. A fallback would convert the live canvas against a
|
|
7086
|
+
// DIFFERENT ComfyUI's schema and return a confident, wrong workflow — silently,
|
|
7087
|
+
// since the two hosts can both answer and disagree. That is strictly worse than
|
|
7088
|
+
// the ECONNREFUSED this issue was filed about, which at least failed loudly.
|
|
7089
|
+
const reply = await panelObjectInfo(ctx, collectNodeTypes(ui));
|
|
7090
|
+
if (!reply.ok)
|
|
7091
|
+
return fail(reply.message);
|
|
7092
|
+
bulk = reply.objectInfo;
|
|
6923
7093
|
}
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
7094
|
+
else {
|
|
7095
|
+
try {
|
|
7096
|
+
bulk = await getObjectInfo();
|
|
7097
|
+
}
|
|
7098
|
+
catch (err) {
|
|
7099
|
+
// Returned as a tool ERROR rather than thrown: a throw here escapes the
|
|
7100
|
+
// handler and reaches the caller as a transport-shaped failure, which is how
|
|
7101
|
+
// the bare ECONNREFUSED got to the reporter in the first place.
|
|
7102
|
+
return fail(objectInfoHostMismatchMessage(err));
|
|
7103
|
+
}
|
|
6929
7104
|
}
|
|
6930
|
-
|
|
7105
|
+
// THE FAIL-CLOSED GUARANTEE LEAKED ONE LINE LATER, so this branches too.
|
|
7106
|
+
//
|
|
7107
|
+
// `backfillObjectInfo` fetches each type it is missing from
|
|
7108
|
+
// `${getComfyUIBaseUrl()}/object_info/<Type>` — COMFYUI_URL, the exact authority
|
|
7109
|
+
// the live-canvas path just refused to consult. Refusing the bulk fetch and then
|
|
7110
|
+
// backfilling from that host would merge a DIFFERENT ComfyUI's definitions into
|
|
7111
|
+
// the panel's map, silently, which is the outcome the branch above exists to
|
|
7112
|
+
// prevent. It fails soft (each miss is swallowed), so on an unreachable
|
|
7113
|
+
// COMFYUI_URL it degrades to "type absent" — but when that host IS reachable and
|
|
7114
|
+
// is a different server, the schema is quietly wrong.
|
|
7115
|
+
//
|
|
7116
|
+
// For a panel-sourced map a miss is not something to repair from elsewhere: the
|
|
7117
|
+
// panel returned that ComfyUI's WHOLE /object_info, so a type absent from it is
|
|
7118
|
+
// absent from the server that will run the workflow. convertUiToApi already
|
|
7119
|
+
// reports unknown types as warnings, which is the honest outcome.
|
|
7120
|
+
const objectInfo = liveCanvasSource
|
|
7121
|
+
? bulk
|
|
7122
|
+
: await backfillObjectInfo(bulk, collectNodeTypes(ui));
|
|
6931
7123
|
const converted = convertUiToApi(ui, objectInfo);
|
|
6932
7124
|
const workflow = converted.workflow;
|
|
6933
7125
|
const warnings = [...captureNotes, ...converted.warnings];
|