comfyui-mcp 0.50.99 → 0.50.101

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.
@@ -1341,6 +1341,28 @@ function toolResultText(res) {
1341
1341
  * else about it — including `isError` and any non-text content. Used where the
1342
1342
  * original message must reach the caller VERBATIM and we are only adding what we
1343
1343
  * additionally know, never restating (or re-classifying) what it already said. */
1344
+ /** All text in a tool result, joined — for quoting one result inside another. */
1345
+ function textOfToolResult(res) {
1346
+ return res.content.map((c) => c.text ?? "").join(" ").trim();
1347
+ }
1348
+ /**
1349
+ * #1329 — the panel's STALE NODE SCHEMA refusal, which is worth acting on rather than
1350
+ * relaying.
1351
+ *
1352
+ * It is the one refusal here whose prescribed remedy has no decision in it: the class
1353
+ * this page registered has drifted from the server's, and `refresh_nodes` re-fetches
1354
+ * /object_info and re-registers it in place. The reporter ran that by hand twice and it
1355
+ * worked twice.
1356
+ *
1357
+ * Matched on the drift sentence rather than on "panel_refresh_nodes", which appears in
1358
+ * several unrelated remedies — keying on the recommendation would make any of them
1359
+ * trigger an add-retry.
1360
+ */
1361
+ function isStaleNodeSchemaRefusal(res) {
1362
+ if (!res.isError)
1363
+ return false;
1364
+ return /added or retyped since this page loaded its node schema/i.test(textOfToolResult(res));
1365
+ }
1344
1366
  function appendToolResultText(res, extra) {
1345
1367
  const idx = res?.content?.findIndex((c) => c.type === "text") ?? -1;
1346
1368
  const block = idx >= 0 ? res.content[idx] : undefined;
@@ -5168,15 +5190,17 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
5168
5190
  catch (err) {
5169
5191
  // Only a card-reply TIMEOUT is recoverable/honest-as-timeout: poll the late
5170
5192
  // buffer, then report "timeout" if still unanswered. Any other error (no
5171
- // panel, transport failure) → "no" so the destructive op is SKIPPED, exactly
5172
- // as the previous catch-all did.
5193
+ // panel, transport failure) still SKIPS the destructive op — but it is reported
5194
+ // as `unreachable`, not `no` (#1332). The user did not decline; we never got to
5195
+ // ask them, and a caller that says "cancelled" on this is describing a decision
5196
+ // nobody made.
5173
5197
  if (isReplyTimeoutError(err)) {
5174
5198
  const late = await pollLateAskReply(bridge, askId, timing, budgetEnd);
5175
5199
  if (late !== undefined)
5176
5200
  return isAffirmative(late) ? "yes" : "no";
5177
5201
  return "timeout";
5178
5202
  }
5179
- return "no";
5203
+ return "unreachable";
5180
5204
  }
5181
5205
  };
5182
5206
  // EXPLICIT self-heal — see PanelToolCtx.rebindToActiveTab. Only rebinds when
@@ -6673,12 +6697,46 @@ export function buildPanelToolDefs() {
6673
6697
  .optional()
6674
6698
  .describe("Canvas [x, y] (two numbers). Auto-placed beside existing nodes when omitted."),
6675
6699
  title: z.string().optional().describe("Optional custom node title."),
6676
- }, async (args, ctx) =>
6677
- // #599: the frontend gates the add on a FRESH /object_info (assertAddNode-
6678
- // ResolvableRefreshing) so an uninstalled class can't be added as a
6679
- // placeholder — that fetch can outlast the 6000 ms default on a large
6680
- // install. Give it the bounded refresh ack budget.
6681
- ctx.call({ cmd: "graph_add_node", class_type: args.class_type, pos: args.pos, title: args.title }, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS)),
6700
+ }, async (args, ctx) => {
6701
+ // #599: the frontend gates the add on a FRESH /object_info (assertAddNode-
6702
+ // ResolvableRefreshing) so an uninstalled class can't be added as a
6703
+ // placeholder — that fetch can outlast the 6000 ms default on a large
6704
+ // install. Give it the bounded refresh ack budget.
6705
+ 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);
6706
+ const first = await add();
6707
+ if (!isStaleNodeSchemaRefusal(first))
6708
+ return first;
6709
+ // #1329 — DO THE REFRESH THE REFUSAL ASKS FOR, instead of billing the caller.
6710
+ //
6711
+ // The reporter uploaded two images, and both LoadImage adds were refused with
6712
+ // "call panel_refresh_nodes and retry". Doing exactly that worked, both times.
6713
+ // That is a round trip and an agent-visible error for something with one
6714
+ // correct response and no decision in it.
6715
+ //
6716
+ // Safe to automate for a checkable reason, not a hopeful one: the panel's guard
6717
+ // throws BEFORE it creates anything ("Refuse before creating anything"), so the
6718
+ // retry cannot leave two nodes behind. Mutations are otherwise never re-issued
6719
+ // on our own initiative, and that rule is intact — this is not a transport
6720
+ // failure with an unknown outcome, it is a refusal that states its own.
6721
+ const refreshed = await ctx.call({ cmd: "refresh_nodes" }, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS);
6722
+ if (refreshed.isError) {
6723
+ // The refusal is the better error: it names what is wrong with the SCHEMA and
6724
+ // what clears it. Carry the refresh failure alongside so the caller knows the
6725
+ // automatic attempt happened and why it did not help.
6726
+ return appendToolResultText(first, `\n\n(Tried to clear this automatically: panel_refresh_nodes was dispatched and FAILED, ` +
6727
+ `so the schema is unchanged and retrying the add will refuse again. ` +
6728
+ `${textOfToolResult(refreshed)})`);
6729
+ }
6730
+ const second = await add();
6731
+ if (!isStaleNodeSchemaRefusal(second))
6732
+ return second;
6733
+ // Still stale after a successful refresh: report THAT, because it means the
6734
+ // remedy the refusal prescribes does not fix this instance and a caller
6735
+ // following it by hand would loop.
6736
+ return appendToolResultText(second, `\n\n(This was already retried ONCE automatically: panel_refresh_nodes reported success ` +
6737
+ `and the add still refuses, so repeating panel_refresh_nodes will not clear it. ` +
6738
+ `Reload the ComfyUI browser tab, which rebuilds the page's node registry from scratch.)`);
6739
+ }),
6682
6740
  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 })),
6683
6741
  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) => {
6684
6742
  const decision = await ctx.confirm("Clear the canvas? This removes every node from the open workflow. (One Ctrl+Z undoes it.)", "Clear canvas");
@@ -6686,6 +6744,14 @@ export function buildPanelToolDefs() {
6686
6744
  return ok("Timed out waiting for your confirmation, so I left the canvas as-is. " +
6687
6745
  "Tell me to clear it again when you're ready.");
6688
6746
  }
6747
+ if (decision === "unreachable") {
6748
+ // #1332 — the canvas is still untouched, which is the part that matters, but
6749
+ // the user never saw the question. Saying "cancelled" would credit them with
6750
+ // a decision they were never offered.
6751
+ return ok("The canvas was left as-is — but NOT because it was declined: the panel could " +
6752
+ "not be reached to ask, so the question never appeared. Nothing was cleared. " +
6753
+ "Check the panel tab is connected, then ask again.");
6754
+ }
6689
6755
  if (decision !== "yes") {
6690
6756
  return ok("Cancelled — the canvas was left as-is.");
6691
6757
  }
@@ -8878,7 +8944,26 @@ export function buildPanelToolDefs() {
8878
8944
  : "Cancelled — no new restart was dispatched. ComfyUI was briefly " +
8879
8945
  "unreachable but is healthy again.");
8880
8946
  }
8881
- return ok("Cancelled — ComfyUI was not restarted.");
8947
+ // #1332 — the reporter's exact string, and it was FALSE. They accepted the
8948
+ // restart, ComfyUI restarted (a fresh startup in the server log), and this
8949
+ // said it had not — because the restart dropped the socket the answer had to
8950
+ // travel back on, and a transport failure used to arrive here as "no".
8951
+ //
8952
+ // The probes above already refuse to claim "not restarted" while the server
8953
+ // is DOWN. This is the remaining case: the server is HEALTHY, which is
8954
+ // equally true of "nothing happened" and of "it restarted and came back".
8955
+ // With an explicit decline we know which; without one we do not, and the
8956
+ // sentence must stop asserting it.
8957
+ return ok(decision === "unreachable"
8958
+ ? "This call did NOT dispatch a restart. Whether ComfyUI restarted for some " +
8959
+ "other reason cannot be told from here: the panel could not be reached to " +
8960
+ "ask for confirmation — the question never appeared — so no decision was " +
8961
+ "made either way, and the server is reachable now, which looks the same " +
8962
+ "whether it never went down or went down and came back. If you asked for a " +
8963
+ "restart and one has already happened, this is that transport loss, not a " +
8964
+ "cancellation. Check the ComfyUI log for a fresh startup line before " +
8965
+ "restarting again."
8966
+ : "Cancelled — ComfyUI was not restarted.");
8882
8967
  }
8883
8968
  // Heal an orphaned session onto the live tab FIRST, then bind the reboot dispatch
8884
8969
  // to that ONE tab id (no await between capture and dispatch, so JS run-to-