comfyui-mcp 0.50.100 → 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;
@@ -6675,12 +6697,46 @@ export function buildPanelToolDefs() {
6675
6697
  .optional()
6676
6698
  .describe("Canvas [x, y] (two numbers). Auto-placed beside existing nodes when omitted."),
6677
6699
  title: z.string().optional().describe("Optional custom node title."),
6678
- }, async (args, ctx) =>
6679
- // #599: the frontend gates the add on a FRESH /object_info (assertAddNode-
6680
- // ResolvableRefreshing) so an uninstalled class can't be added as a
6681
- // placeholder — that fetch can outlast the 6000 ms default on a large
6682
- // install. Give it the bounded refresh ack budget.
6683
- 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
+ }),
6684
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 })),
6685
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) => {
6686
6742
  const decision = await ctx.confirm("Clear the canvas? This removes every node from the open workflow. (One Ctrl+Z undoes it.)", "Clear canvas");