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
- * #1359 an /object_info failure that names WHICH host it asked, and why that host.
1107
+ * Node definitions from the ComfyUI the PANEL is connected to (#1359 / #1006).
1108
1108
  *
1109
- * `panel_strip_workflow` reads the graph from the connected panel and then fetches node
1110
- * definitions through the global headless client, which resolves COMFYUI_URL. For a
1111
- * local session those are one machine. For a connected REMOTE panel they are two, and
1112
- * the tool fails with a bare
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
- * fetch failed: connect ECONNREFUSED 127.0.0.1:8188 while requesting
1115
- * http://127.0.0.1:8188/object_info
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
- * which says nothing about the canvas being on a different host. The reporter of #1359
1118
- * had to read the compiled orchestrator to find that out.
1119
- *
1120
- * This does NOT fix the split authority that needs the panel to serve its own
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 objectInfoHostMismatchMessage(ctx, err, liveCanvasSource) {
1130
- const raw = err instanceof Error ? err.message : String(err);
1131
- let panelOrigin = null;
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
- panelOrigin = ctx.bridge?.tabServerOrigin?.(ctx.tabId) ?? null;
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
- /* best effort the message stands without it */
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
- if (!liveCanvasSource) {
1140
- // pack / path / inline: COMFYUI_URL is the right authority, so the bare failure is
1141
- // already about the host the caller asked for. Say only what is true.
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
- try {
6922
- bulk = await getObjectInfo();
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
- catch (err) {
6925
- // Returned as a tool ERROR rather than thrown: a throw here escapes the
6926
- // handler and reaches the caller as a transport-shaped failure, which is how
6927
- // the bare ECONNREFUSED got to the reporter in the first place.
6928
- return fail(objectInfoHostMismatchMessage(ctx, err, liveCanvasSource));
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
- const objectInfo = await backfillObjectInfo(bulk, collectNodeTypes(ui));
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];